diff --git a/changelog.txt b/changelog.txt index b492b6d2ec..efad74348b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools ## New Features +- `prioritize`: ``prioritize this`` boosts the job related to the currently selected entity, absorbing the functionality of `do-job-now` (which still works and forwards to the new command) ## Fixes - `bodyswap`: fix "invalid argument count" when the target unit has no nemesis record diff --git a/do-job-now.lua b/do-job-now.lua index 3ef9585a9b..268db29436 100644 --- a/do-job-now.lua +++ b/do-job-now.lua @@ -1,202 +1,11 @@ -- makes a job involving current selection high priority -local utils = require('utils') +-- the implementation moved to `prioritize this`; this script forwards for +-- compatibility with existing keybindings and aliases -local function print_help() +if (...) then print(dfhack.script_help()) + return end -local function doJobNow(job) - local job_str = dfhack.job.getName(job) - if not job.flags.do_now then - job.flags.do_now = true - print("Made the job " .. job_str .. " top priority") - else - print("The job " .. job_str .. " is already top priority") - end - local building = dfhack.job.getHolder(job) - if building then - print("... at " .. utils.getBuildingName(building)) - end - local unit = dfhack.job.getWorker(job) - if unit then - print("... by " .. dfhack.df2console(dfhack.units.getReadableName(unit))) - end -end - -local function doItemJobNow(item) - if not item.flags.in_job then - qerror(dfhack.items.getDescription(item, 0) .. " must be in a job! (look for 'TSK')") - end - - local sref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) - if sref then - doJobNow(sref.data.job) - return - end - print("Couldn't find any job for " .. dfhack.items.getDescription(item, 0)) -end - -local function doBuildingJobNow(building) - --print('This will attempt to make a job of a building a top priority') - - if #building.jobs > 0 - and ( - building.jobs[0].job_type == df.job_type.ConstructBuilding - or building.jobs[0].job_type == df.job_type.DestroyBuilding - ) - then - doJobNow(building.jobs[0]) - return - end - print("Couldn't find either construct or destroy building job for " .. utils.getBuildingName(building)) -end - -local function doUnitJobNow(unit) - if dfhack.units.isCitizen(unit) then - local t_job = unit.job - if t_job then - local job = t_job.current_job - if job then - doJobNow(job) - return - end - end - print("Couldn't find any job for " .. dfhack.df2console(dfhack.units.getReadableName(unit))) - else - local needle = unit.id - for _link, job in utils.listpairs(df.global.world.jobs.list) do - if #job.general_refs > 0 then - for _, gref in ipairs(job.general_refs) do - --if gref:getType() == df.general_ref_type.UNIT then -- can't do this: there are many different types that can be units - local u = gref:getUnit() - if u and u.id == needle then - doJobNow(job) - return - end - --end - end - end - end - - print("Couldn't find any job involving " .. dfhack.df2console(dfhack.units.getReadableName(unit))) - end -end - -local function doPlantJobNow(plant) - --print('This will attempt to make a job with a plant a top priority') - - for _link, job in utils.listpairs(df.global.world.jobs.list) do - if plant.pos.x == job.pos.x - and plant.pos.y == job.pos.y - and plant.pos.z == job.pos.z - then - doJobNow(job) - return - end - end - - print("Couldn't find any job involving this plant.") -end - -local function doWorkOrderJobsNow(order) - local needle = order.id - local cnt = 0 - for _link, job in utils.listpairs(df.global.world.jobs.list) do - if job.order_id == needle then - doJobNow(job) - cnt = cnt + 1 - end - end - - if cnt > 0 then - print("Found " .. cnt .. " jobs for this work order.") - else - print("Couldn't find any job for this work order.") - end -end - -local function getSelectedWorkOrder() - local scr = dfhack.gui.getCurViewscreen() - local orders - local idx - if df.viewscreen_jobmanagementst:is_instance(scr) then - orders = df.global.world.manager_orders.all - idx = scr.sel_idx - elseif df.viewscreen_workshop_profilest:is_instance(scr) - and scr.tab == df.viewscreen_workshop_profilest.T_tab.Orders - then - orders = scr.orders - idx = scr.order_idx - end - if orders then - if idx < #orders then - return orders[idx] - else - qerror("Invalid work order selected") - end - end - - return nil -end - -local function doSelectedEntityJobNow() - -- do we have a job selected? - local job = dfhack.gui.getSelectedJob(true) - if job then - doJobNow(job) - return - end - - -- do we have an item selected? - local item = dfhack.gui.getSelectedItem(true) - if item then - doItemJobNow(item) - return - end - - -- do we have a building selected? - local building = dfhack.gui.getSelectedBuilding(true) - if building then - doBuildingJobNow(building) - return - end - - -- do we have a unit selected? - local unit = dfhack.gui.getSelectedUnit(true) - if unit then - doUnitJobNow(unit) - return - end - - -- do we have a plant selected? - local plant = dfhack.gui.getSelectedPlant(true) - if plant then - doPlantJobNow(plant) - return - end - - -- do we have a work order selected? - local order = getSelectedWorkOrder() - if order then - doWorkOrderJobsNow(order) - return - end - - qerror("Select something job-related in game.") -end - -local default_action = print_help - -local actions = { - -- help - ["-?"] = print_help, - ["?"] = print_help, - ["--help"] = print_help, - ["help"] = print_help, - -- action - ["default"] = doSelectedEntityJobNow, -} - --- Lua is beautiful. -(actions[ (...) or "default" ] or default_action)(...) +reqscript('prioritize').prioritize_this() diff --git a/docs/do-job-now.rst b/docs/do-job-now.rst index 6f687b7400..3988e3a061 100644 --- a/docs/do-job-now.rst +++ b/docs/do-job-now.rst @@ -9,6 +9,9 @@ The script will try its best to find a job related to the selected entity (which can be a job, dwarf, animal, item, building, plant or work order) and then mark the job as high priority. +This functionality is now part of `prioritize` (see ``prioritize this``), and +``do-job-now`` forwards to it for compatibility with existing keybindings. + Apart from jobs that are queued from buildings, there is normally no visual indicator that the job is now high priority. If you use ``do-job-now`` from the keybinding, you have to check the dfhack console for output to see if the @@ -17,10 +20,6 @@ command succeeded. If a work order is selected, every job currently active from this work order is adjusted, but not the future ones. -Also see the ``do-job-now`` `tweak`, which allows you to adjust job priorities -from the :kbd:`j`obs screen, and `prioritize`, which can automatically adjust -priorities based on the type of job. - Usage ----- diff --git a/docs/prioritize.rst b/docs/prioritize.rst index bdfb5aca2a..a5c459fff6 100644 --- a/docs/prioritize.rst +++ b/docs/prioritize.rst @@ -30,6 +30,7 @@ Usage enable prioritize prioritize [] [defaults| ...] + prioritize this Examples -------- @@ -44,6 +45,10 @@ Examples right now. ``prioritize ConstructBuilding DestroyBuilding`` Prioritize all current building construction and destruction jobs. +``prioritize this`` + Prioritize the job related to the selected entity (a job, dwarf, animal, + item, building, plant, or work order). This is a good candidate for a + keybinding. ``prioritize -a --haul-labor=Food,Body StoreItemInStockpile`` Prioritize all current and future food and corpse hauling jobs. ``disable prioritize`` diff --git a/prioritize.lua b/prioritize.lua index 70c0c470fd..9d6ae0f851 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -468,6 +468,156 @@ local function print_current_jobs(job_matchers, opts) if first then print('No current prioritizable jobs.') end end +-------------------------------- +-- "prioritize this": boost the job related to the selected entity +-- + +-- encapsulate df state in functions so unit tests can mock them out +function get_selected_job() return dfhack.gui.getSelectedJob(true) end +function get_selected_item() return dfhack.gui.getSelectedItem(true) end +function get_selected_building() return dfhack.gui.getSelectedBuilding(true) end +function get_selected_unit() return dfhack.gui.getSelectedUnit(true) end +function get_selected_plant() return dfhack.gui.getSelectedPlant(true) end +function get_selected_work_order() + local scr = dfhack.gui.getCurViewscreen() + local orders, idx + -- viewscreen class descriptors are unavailable without a loaded map + if df.viewscreen_jobmanagementst and + df.viewscreen_jobmanagementst:is_instance(scr) then + orders = df.global.world.manager_orders.all + idx = scr.sel_idx + elseif df.viewscreen_workshop_profilest and + df.viewscreen_workshop_profilest:is_instance(scr) and + scr.tab == df.viewscreen_workshop_profilest.T_tab.Orders then + orders = scr.orders + idx = scr.order_idx + end + if orders then + if idx < #orders then + return orders[idx] + else + qerror('Invalid work order selected') + end + end +end + +local function boost_job(job) + local job_str = dfhack.job.getName(job) + if not job.flags.do_now then + job.flags.do_now = true + print(('Made the job %s top priority'):format(job_str)) + else + print(('The job %s is already top priority'):format(job_str)) + end + local building = dfhack.job.getHolder(job) + if building then + print('... at ' .. utils.getBuildingName(building)) + end + local unit = dfhack.job.getWorker(job) + if unit then + print('... by ' .. + dfhack.df2console(dfhack.units.getReadableName(unit))) + end +end + +local function boost_item_job(item) + if not item.flags.in_job then + qerror(dfhack.items.getDescription(item, 0) .. + ' must be in a job! (look for \'TSK\')') + end + local sref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if sref then + boost_job(sref.data.job) + return + end + print('Couldn\'t find any job for ' .. dfhack.items.getDescription(item, 0)) +end + +local function boost_building_job(building) + if #building.jobs > 0 and + (building.jobs[0].job_type == df.job_type.ConstructBuilding or + building.jobs[0].job_type == df.job_type.DestroyBuilding) then + boost_job(building.jobs[0]) + return + end + print('Couldn\'t find either construct or destroy building job for ' .. + utils.getBuildingName(building)) +end + +local function boost_unit_job(unit) + if dfhack.units.isCitizen(unit) then + local job = unit.job and unit.job.current_job + if job then + boost_job(job) + return + end + print('Couldn\'t find any job for ' .. + dfhack.df2console(dfhack.units.getReadableName(unit))) + return + end + for _,job in utils.listpairs(df.global.world.jobs.list) do + for _,gref in ipairs(job.general_refs) do + local u = gref:getUnit() + if u and u.id == unit.id then + boost_job(job) + return + end + end + end + print('Couldn\'t find any job involving ' .. + dfhack.df2console(dfhack.units.getReadableName(unit))) +end + +local function boost_plant_job(plant) + for _,job in utils.listpairs(df.global.world.jobs.list) do + if plant.pos.x == job.pos.x and plant.pos.y == job.pos.y and + plant.pos.z == job.pos.z then + boost_job(job) + return + end + end + print('Couldn\'t find any job involving this plant.') +end + +local function boost_order_jobs(order) + local count = 0 + for _,job in utils.listpairs(df.global.world.jobs.list) do + if job.order_id == order.id then + boost_job(job) + count = count + 1 + end + end + if count > 0 then + print(('Found %d jobs for this work order.'):format(count)) + else + print('Couldn\'t find any job for this work order.') + end +end + +-- mark the job related to the selected entity (job, item, building, unit, +-- plant, or work order) as high priority +function prioritize_this() + local job = get_selected_job() + if job then return boost_job(job) end + + local item = get_selected_item() + if item then return boost_item_job(item) end + + local building = get_selected_building() + if building then return boost_building_job(building) end + + local unit = get_selected_unit() + if unit then return boost_unit_job(unit) end + + local plant = get_selected_plant() + if plant then return boost_plant_job(plant) end + + local order = get_selected_work_order() + if order then return boost_order_jobs(order) end + + qerror('Select something job-related in game.') +end + local function print_registry_section(header, t) print('\n' .. header .. ':') table.sort(t) @@ -533,6 +683,17 @@ local function parse_commandline(args) if positionals[1] == 'help' then opts.help = true end if opts.help then return opts end + -- "prioritize this" boosts the job related to the selected entity + if positionals[1] == 'this' then + if #positionals > 1 or action ~= status or + unit_labors or reaction_names then + qerror('"this" cannot be combined with other arguments') + end + opts.action = prioritize_this + opts.job_matchers = {} + return opts + end + -- expand defaults, if requested for i,job_type_name in ipairs(positionals) do if not job_type_name:lower():find('^defaults?') then diff --git a/test/prioritize.lua b/test/prioritize.lua index ded0f4ab55..e1e0fc5b6a 100644 --- a/test/prioritize.lua +++ b/test/prioritize.lua @@ -1,3 +1,5 @@ +config.target = 'prioritize' + local eventful = require('plugins.eventful') local prioritize = reqscript('prioritize') local utils = require('utils') @@ -12,6 +14,8 @@ local function get_mock_watched_job_matchers() end local mock_postings = {} local function get_mock_postings() return mock_postings end +local mock_job_list = {} +local function get_mock_job_list() return mock_job_list end local mock_reactions = {{code='TAN_A_HIDE'}} local function get_mock_reactions() return mock_reactions end local function test_wrapper(test_fn) @@ -21,11 +25,13 @@ local function test_wrapper(test_fn) {prioritize, 'get_watched_job_matchers', get_mock_watched_job_matchers}, {prioritize, 'get_postings', get_mock_postings}, + {prioritize, 'get_job_list', get_mock_job_list}, {prioritize, 'get_reactions', get_mock_reactions}}, test_fn) mock_eventful_onUnload, mock_eventful_onJobInitiated = {}, {} mock_print = mock.func() mock_watched_job_matchers, mock_postings = {}, {} + mock_job_list = {} mock_reactions = {{code='TAN_A_HIDE'}} end config.wrapper = test_wrapper @@ -42,13 +48,24 @@ local HAUL_ITEM = df.unit_labor.HAUL_ITEM local HAUL_FURNITURE = df.unit_labor.HAUL_FURNITURE local HAUL_ANIMALS = df.unit_labor.HAUL_ANIMALS +-- build a df-style linked list (sentinel head node) containing the given jobs +local function make_job_list(jobs) + local list = {} + local tail = list + for _,job in ipairs(jobs) do + tail.next = {item=job} + tail = tail.next + end + return list +end + function test.status() p.status() expect.eq(1, mock_print.call_count) expect.eq('Not automatically prioritizing any jobs.', mock_print.call_args[1][1]) - mock_watched_job_matchers[REST] = {num_prioritized=5} + mock_watched_job_matchers[REST] = {} p.status() expect.eq(3, mock_print.call_count) expect.eq('Automatically prioritized jobs:', mock_print.call_args[2][1]) @@ -57,7 +74,7 @@ end function test.status_labor() mock_watched_job_matchers[STORE_ITEM_IN_STOCKPILE] = - {num_prioritized=5, hauler_matchers={[HAUL_BODY]=2}} + {hauler_matchers={[HAUL_BODY]=0}} p.status() expect.eq(2, mock_print.call_count) expect.eq('Automatically prioritized jobs:', mock_print.call_args[1][1]) @@ -66,7 +83,7 @@ end function test.status_reaction() mock_watched_job_matchers[CUSTOM_REACTION] = - {num_prioritized=5, reaction_matchers={TAN_A_HIDE=2}} + {reaction_matchers={TAN_A_HIDE=0}} p.status() expect.eq(2, mock_print.call_count) expect.eq('Automatically prioritized jobs:', mock_print.call_args[1][1]) @@ -74,215 +91,214 @@ function test.status_reaction() end function test.boost() - mock_postings = {{job={job_type=DIG, flags={}}, flags={}}, - {job={job_type=DIG, flags={}}, flags={}}, - {job={job_type=EAT, flags={}}, flags={dead=true}}, - {job={job_type=EAT, flags={}}, flags={}}, - {job={job_type=REST, flags={}}, flags={dead=true}}} - local expected_postings = - {{job={job_type=DIG, flags={}}, flags={}}, - {job={job_type=DIG, flags={}}, flags={}}, - {job={job_type=EAT, flags={}}, flags={dead=true}}, - {job={job_type=EAT, flags={do_now=true}}, flags={}}, - {job={job_type=REST, flags={}}, flags={dead=true}}} - p.boost({[EAT]={num_prioritized=0}}, {}) - expect.eq(1, mock_print.call_count) + local dig1 = {job_type=DIG, flags={}} + local dig2 = {job_type=DIG, flags={}} + local eat1 = {job_type=EAT, flags={do_now=true}} + local eat2 = {job_type=EAT, flags={}} + local special = {job_type=EAT, flags={special=true}} + mock_job_list = make_job_list{dig1, dig2, eat1, eat2, special} + p.boost({[EAT]={}}, {}) + expect.eq(2, mock_print.call_count) expect.eq('Prioritized 1 job.', mock_print.call_args[1][1]) - expect.table_eq(expected_postings, mock_postings) + expect.eq('1 job already prioritized.', mock_print.call_args[2][1]) + expect.nil_(dig1.flags.do_now) + expect.nil_(dig2.flags.do_now) + expect.true_(eat1.flags.do_now) + expect.true_(eat2.flags.do_now) + expect.nil_(special.flags.do_now) end function test.boost_quiet() - mock_postings = {{job={job_type=DIG, flags={}}, flags={}}, - {job={job_type=DIG, flags={}}, flags={}}, - {job={job_type=EAT, flags={}}, flags={dead=true}}, - {job={job_type=EAT, flags={}}, flags={}}, - {job={job_type=REST, flags={}}, flags={dead=true}}} - local expected_postings = - {{job={job_type=DIG, flags={}}, flags={}}, - {job={job_type=DIG, flags={}}, flags={}}, - {job={job_type=EAT, flags={}}, flags={dead=true}}, - {job={job_type=EAT, flags={do_now=true}}, flags={}}, - {job={job_type=REST, flags={}}, flags={dead=true}}} - p.boost({[EAT]={num_prioritized=0}}, {quiet=true}) + local eat1 = {job_type=EAT, flags={}} + local eat2 = {job_type=EAT, flags={}} + mock_job_list = make_job_list{eat1, eat2} + p.boost({[EAT]={}}, {quiet=true}) expect.eq(0, mock_print.call_count) - expect.table_eq(expected_postings, mock_postings) + expect.true_(eat1.flags.do_now) + expect.true_(eat2.flags.do_now) end function test.boost_and_watch() - p.boost_and_watch({[DIG]={num_prioritized=0}}, {}) + p.boost_and_watch({[SUTURE]={}}, {}) expect.eq(2, mock_print.call_count) expect.str_find('^Prioritized', mock_print.call_args[1][1]) expect.str_find('^Automatically', mock_print.call_args[2][1]) - expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) + expect.table_eq({[SUTURE]={}}, mock_watched_job_matchers) - p.boost_and_watch({[DIG]={num_prioritized=0}}, {}) + p.boost_and_watch({[SUTURE]={}}, {}) expect.eq(4, mock_print.call_count) expect.str_find('^Prioritized', mock_print.call_args[3][1]) expect.str_find('^Skipping', mock_print.call_args[4][1]) - expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) + expect.table_eq({[SUTURE]={}}, mock_watched_job_matchers) +end + +-- dig/smooth job types are on the denylist: current jobs still get boosted, +-- but no watch is registered and a warning is printed +function test.boost_and_watch_denylisted() + expect.printerr_match({'Priortizing current jobs', 'smooth/engrave', + 'overwhelm', 'mining'}, + function() + p.boost_and_watch({[DIG]={}}, {}) + end) + expect.table_eq({}, mock_watched_job_matchers) end function test.boost_and_watch_quiet() - p.boost_and_watch({[DIG]={num_prioritized=0}}, {quiet=true}) + p.boost_and_watch({[SUTURE]={}}, {quiet=true}) expect.eq(0, mock_print.call_count) - expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) + expect.table_eq({[SUTURE]={}}, mock_watched_job_matchers) - p.boost_and_watch({[DIG]={num_prioritized=0}}, {quiet=true}) + p.boost_and_watch({[SUTURE]={}}, {quiet=true}) expect.eq(0, mock_print.call_count) - expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) + expect.table_eq({[SUTURE]={}}, mock_watched_job_matchers) end function test.remove_watch() - p.remove_watch({[DIG]={num_prioritized=0}}, {}) + p.remove_watch({[SUTURE]={}}, {}) expect.eq(1, mock_print.call_count) expect.str_find('Skipping unwatched', mock_print.call_args[1][1]) expect.table_eq({}, mock_watched_job_matchers) - mock_watched_job_matchers[DIG] = {num_prioritized=0} - p.remove_watch({[DIG]={num_prioritized=0}}, {}) + mock_watched_job_matchers[SUTURE] = {} + p.remove_watch({[SUTURE]={}}, {}) expect.eq(2, mock_print.call_count) expect.str_find('No longer', mock_print.call_args[2][1]) end function test.remove_watch_quiet() - p.remove_watch({[DIG]={num_prioritized=0}}, {quiet=true}) + p.remove_watch({[SUTURE]={}}, {quiet=true}) expect.eq(0, mock_print.call_count) expect.table_eq({}, mock_watched_job_matchers) - mock_watched_job_matchers[DIG] = {num_prioritized=0} - p.remove_watch({[DIG]={num_prioritized=0}}, {quiet=true}) + mock_watched_job_matchers[SUTURE] = {} + p.remove_watch({[SUTURE]={}}, {quiet=true}) expect.eq(0, mock_print.call_count) expect.table_eq({}, mock_watched_job_matchers) end function test.boost_and_watch_labor() - mock_postings = {{job={job_type=DIG, flags={}}, flags={}}, - {job={job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_FOOD, - flags={}}, flags={}}, - {job={job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_ITEM, - flags={}}, flags={}}, - {job={job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_ITEM, - flags={}}, flags={}}, - {job={job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_ITEM, - flags={}}, flags={dead=true}}} - - p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_ITEM]=0}}}, + mock_job_list = make_job_list{ + {job_type=DIG, flags={}}, + {job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_FOOD, flags={}}, + {job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_ITEM, flags={}}, + {job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_ITEM, flags={}}, + {job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_ITEM, + flags={special=true}}} + + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_ITEM]=0}}}, {}) expect.eq(2, mock_print.call_count) expect.str_find('^Prioritized 2', mock_print.call_args[1][1]) expect.str_find('^Automatically', mock_print.call_args[2][1]) - expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_ITEM]=0}}}, + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_ITEM]=0}}}, mock_watched_job_matchers) - p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, {}) - expect.eq(4, mock_print.call_count) + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={}}, {}) + expect.eq(5, mock_print.call_count) expect.str_find('^Prioritized 1', mock_print.call_args[3][1]) - expect.str_find('^Automatically', mock_print.call_args[4][1]) - expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, - mock_watched_job_matchers) + expect.str_find('^2 jobs already prioritized', mock_print.call_args[4][1]) + expect.str_find('^Automatically', mock_print.call_args[5][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={}}, mock_watched_job_matchers) end function test.boost_and_watch_store_all_labors() - p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, {}) + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={}}, {}) expect.eq(2, mock_print.call_count) expect.str_find('^Prioritized 0', mock_print.call_args[1][1]) expect.str_find('^Automatically', mock_print.call_args[2][1]) - expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={}}, mock_watched_job_matchers) - p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, {}) + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={}}, {}) expect.eq(4, mock_print.call_count) expect.str_find('^Prioritized 0', mock_print.call_args[3][1]) expect.str_find('^Skipping', mock_print.call_args[4][1]) - expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={}}, mock_watched_job_matchers) - p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_ITEM]=0}}}, {}) + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_ITEM]=0}}}, {}) expect.eq(6, mock_print.call_count) - expect.str_find('^Prioritized 0', mock_print.call_args[3][1]) - expect.str_find('^Skipping.*Item', mock_print.call_args[4][1]) - expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, + expect.str_find('^Prioritized 0', mock_print.call_args[5][1]) + expect.str_find('^Skipping.*Item', mock_print.call_args[6][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={}}, mock_watched_job_matchers) end function test.boost_and_watch_store_add_labors() - p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_ITEM]=0}}}, {}) + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_ITEM]=0}}}, {}) expect.eq(2, mock_print.call_count) expect.str_find('^Prioritized 0', mock_print.call_args[1][1]) expect.str_find('^Automatically.*Item', mock_print.call_args[2][1]) - expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_ITEM]=0}}}, + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_ITEM]=0}}}, mock_watched_job_matchers) - p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_FOOD]=0}}}, {}) + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_FOOD]=0}}}, {}) expect.eq(4, mock_print.call_count) expect.str_find('^Prioritized 0', mock_print.call_args[3][1]) expect.str_find('^Automatically.*Food', mock_print.call_args[4][1]) - expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_ITEM]=0, [HAUL_FOOD]=0}}}, + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_ITEM]=0, [HAUL_FOOD]=0}}}, mock_watched_job_matchers) - p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_FOOD]=0}}}, {}) + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_FOOD]=0}}}, {}) expect.eq(6, mock_print.call_count) expect.str_find('^Prioritized 0', mock_print.call_args[5][1]) expect.str_find('^Skipping.*Food', mock_print.call_args[6][1]) - expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_ITEM]=0, [HAUL_FOOD]=0}}}, + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_ITEM]=0, [HAUL_FOOD]=0}}}, mock_watched_job_matchers) end function test.boost_and_watch_reactions() - p.boost_and_watch({[CUSTOM_REACTION]={num_prioritized=0, - reaction_matchers={TAN_A_HIDE=0}}}, {}) + p.boost_and_watch({[CUSTOM_REACTION]= + {reaction_matchers={TAN_A_HIDE=0}}}, {}) expect.eq(2, mock_print.call_count) expect.str_find('^Prioritized 0', mock_print.call_args[1][1]) expect.str_find('^Automatically.*TAN_A_HIDE', mock_print.call_args[2][1]) - expect.table_eq({[CUSTOM_REACTION]={num_prioritized=0, - reaction_matchers={TAN_A_HIDE=0}}}, + expect.table_eq({[CUSTOM_REACTION]={reaction_matchers={TAN_A_HIDE=0}}}, mock_watched_job_matchers) end function test.remove_one_labor_from_all() - -- top-level num_prioritized should be persisted - mock_watched_job_matchers = {[STORE_ITEM_IN_STOCKPILE]={num_prioritized=5}} + mock_watched_job_matchers = {[STORE_ITEM_IN_STOCKPILE]={}} - p.remove_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_FOOD]=0}}}, + p.remove_watch({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_FOOD]=0}}}, {}) expect.eq(1, mock_print.call_count) expect.str_find('No longer.*Food', mock_print.call_args[1][1]) - expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=5, - hauler_matchers={[HAUL_STONE]=0, [HAUL_WOOD]=0, [HAUL_BODY]=0, - [HAUL_REFUSE]=0, [HAUL_ITEM]=0, [HAUL_FURNITURE]=0, - [HAUL_ANIMALS]=0}}}, + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_STONE]=0, [HAUL_WOOD]=0, [HAUL_BODY]=0, + [HAUL_REFUSE]=0, [HAUL_ITEM]=0, [HAUL_FURNITURE]=0, + [HAUL_ANIMALS]=0}}}, mock_watched_job_matchers) - p.remove_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, - hauler_matchers={[HAUL_FOOD]=0}}}, + p.remove_watch({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_FOOD]=0}}}, {}) expect.eq(2, mock_print.call_count) expect.str_find('Skipping.*Food', mock_print.call_args[2][1]) - expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=5, - hauler_matchers={[HAUL_STONE]=0, [HAUL_WOOD]=0, [HAUL_BODY]=0, - [HAUL_REFUSE]=0, [HAUL_ITEM]=0, [HAUL_FURNITURE]=0, - [HAUL_ANIMALS]=0}}}, + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]= + {hauler_matchers={[HAUL_STONE]=0, [HAUL_WOOD]=0, [HAUL_BODY]=0, + [HAUL_REFUSE]=0, [HAUL_ITEM]=0, [HAUL_FURNITURE]=0, + [HAUL_ANIMALS]=0}}}, mock_watched_job_matchers) end function test.remove_all_reactions_from_all() - mock_watched_job_matchers = {[CUSTOM_REACTION]={num_prioritized=5}} + mock_watched_job_matchers = {[CUSTOM_REACTION]={}} -- we only have one reaction in our mock registry. if we remove it by name -- from an unrestricted CUSTOM_REACTION matcher, the entire matcher should -- disappear - p.remove_watch({[CUSTOM_REACTION]={num_prioritized=0, - reaction_matchers={TAN_A_HIDE=0}}}, + p.remove_watch({[CUSTOM_REACTION]={reaction_matchers={TAN_A_HIDE=0}}}, {}) expect.eq(1, mock_print.call_count) expect.str_find('No longer.*TAN_A_HIDE', mock_print.call_args[1][1]) @@ -293,13 +309,13 @@ function test.eventful_hook_lifecycle() expect.nil_(mock_eventful_onUnload.prioritize) expect.nil_(mock_eventful_onJobInitiated.prioritize) - p.boost_and_watch({[DIG]={num_prioritized=0}}, {quiet=true}) - expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) + p.boost_and_watch({[SUTURE]={}}, {quiet=true}) + expect.table_eq({[SUTURE]={}}, mock_watched_job_matchers) expect.eq(p.clear_watched_job_matchers, mock_eventful_onUnload.prioritize) expect.eq(p.on_new_job, mock_eventful_onJobInitiated.prioritize) - p.remove_watch({[DIG]={num_prioritized=0}}, {quiet=true}) + p.remove_watch({[SUTURE]={}}, {quiet=true}) expect.table_eq({}, mock_watched_job_matchers) expect.nil_(mock_eventful_onUnload.prioritize) @@ -314,8 +330,9 @@ function test.eventful_callbacks() expect.table_eq(expected, job) -- watched job - expected = {job_type=DIG, flags={do_now=true}} - p.boost_and_watch({[DIG]={num_prioritized=0}}, {quiet=true}) + job = {job_type=SUTURE, flags={}} + expected = {job_type=SUTURE, flags={do_now=true}} + p.boost_and_watch({[SUTURE]={}}, {quiet=true}) p.on_new_job(job) expect.table_eq(expected, job) @@ -328,7 +345,7 @@ end function test.eventful_callbacks_labor() mock_watched_job_matchers[STORE_ITEM_IN_STOCKPILE] = - {num_prioritized=0, hauler_matchers={[HAUL_FOOD]=0}} + {hauler_matchers={[HAUL_FOOD]=0}} -- unwatched job local job = {job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_BODY, @@ -344,9 +361,6 @@ function test.eventful_callbacks_labor() flags={}} expected_job = {job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_FOOD, flags={do_now=true}} - expected_watched_job_matchers = - {[STORE_ITEM_IN_STOCKPILE]={num_prioritized=1, - hauler_matchers={[HAUL_FOOD]=1}}} p.on_new_job(job) expect.table_eq(expected_job, job) expect.table_eq(expected_watched_job_matchers, mock_watched_job_matchers) @@ -354,7 +368,7 @@ end function test.eventful_callbacks_reaction() mock_watched_job_matchers[CUSTOM_REACTION] = - {num_prioritized=0, reaction_matchers={TAN_A_HIDE=0}} + {reaction_matchers={TAN_A_HIDE=0}} -- unwatched job local job = {job_type=CUSTOM_REACTION, reaction_name='STEEL_MAKING', @@ -369,9 +383,6 @@ function test.eventful_callbacks_reaction() job = {job_type=CUSTOM_REACTION, reaction_name='TAN_A_HIDE', flags={}} expected_job = {job_type=CUSTOM_REACTION, reaction_name='TAN_A_HIDE', flags={do_now=true}} - expected_watched_job_matchers = - {[CUSTOM_REACTION]={num_prioritized=1, - reaction_matchers={TAN_A_HIDE=1}}} p.on_new_job(job) expect.table_eq(expected_job, job) expect.table_eq(expected_watched_job_matchers, mock_watched_job_matchers) @@ -380,56 +391,67 @@ end function test.print_current_jobs_empty() p.print_current_jobs({}) expect.eq(1, mock_print.call_count) - expect.eq('No current unclaimed jobs.', mock_print.call_args[1][1]) + expect.eq('No current prioritizable jobs.', mock_print.call_args[1][1]) end function test.print_current_jobs_full() - mock_postings = {{job={job_type=DIG}, flags={}}, - {job={job_type=DIG}, flags={}}, - {job={job_type=EAT}, flags={dead=true}}, - {job={job_type=EAT}, flags={}}, - {job={job_type=REST}, flags={dead=true}}, - {job={job_type=STORE_ITEM_IN_STOCKPILE, - item_subtype=HAUL_FOOD, flags={}}, flags={}}, - {job={job_type=CUSTOM_REACTION, - reaction_name='TAN_A_HIDE', flags={}}, flags={}}} + local dig1 = {job_type=DIG, flags={}} + local store_job = {job_type=STORE_ITEM_IN_STOCKPILE, + item_subtype=HAUL_FOOD, flags={}} + local custom_job = {job_type=CUSTOM_REACTION, + reaction_name='TAN_A_HIDE', flags={}} + mock_job_list = make_job_list{ + dig1, + {job_type=DIG, flags={}}, + {job_type=EAT, flags={}}, + {job_type=EAT, flags={do_now=true}}, + {job_type=EAT, flags={special=true}}, + {job_type=REST, flags={}}, + store_job, + custom_job} + mock_postings = {{job=dig1, flags={}}, + {job=store_job, flags={}}, + {job=custom_job, flags={}}, + {job={job_type=DIG, flags={}}, flags={dead=true}}} p.print_current_jobs({}) - expect.eq(5, mock_print.call_count) - expect.eq('Current unclaimed jobs:', mock_print.call_args[1][1]) + -- 4 header lines plus one line per job type + expect.eq(9, mock_print.call_count) + expect.eq('Current prioritizable jobs:', mock_print.call_args[1][1]) local result = {} for i,v in ipairs(mock_print.call_args) do - if i == 1 then goto continue end - local _,_,num,job_type = v[1]:find('^(%d+)%s+(%S+)') - expect.ne(nil, num) + if i <= 4 then goto continue end + local _,_,unclaimed,total,job_type = v[1]:find('^%s*(%d+)%s+(%d+)%s+(.+)$') + expect.ne(nil, unclaimed) expect.nil_(result[job_type]) - result[job_type] = num + result[job_type] = {unclaimed, total} ::continue:: end - expect.table_eq({[df.job_type[DIG]]='2', [df.job_type[EAT]]='1', - [df.job_type[STORE_ITEM_IN_STOCKPILE]]='1', - [df.job_type[CUSTOM_REACTION]]='1'}, - result) + expect.table_eq({ + ['Dig']={'1', '2'}, + ['Eat']={'0', '1'}, + ['Rest']={'0', '1'}, + ['StoreItemInStockpile --haul-labor Food']={'1', '1'}, + ['CustomReaction --reaction-name TAN_A_HIDE']={'1', '1'}, + }, result) end function test.print_current_jobs_filtered() - mock_postings = {{job={job_type=DIG}, flags={}}, - {job={job_type=DIG}, flags={}}, - {job={job_type=EAT}, flags={dead=true}}, - {job={job_type=EAT}, flags={}}, - {job={job_type=REST}, flags={dead=true}}} - p.print_current_jobs({[EAT]=true}) - expect.eq(2, mock_print.call_count) - expect.eq('Current unclaimed jobs:', mock_print.call_args[1][1]) - local result = {} - for i,v in ipairs(mock_print.call_args) do - if i == 1 then goto continue end - local _,_,num,job_type = v[1]:find('(%d+)%s+(%S+)') - expect.ne(nil, num) - expect.nil_(result[job_type]) - result[job_type] = num - ::continue:: - end - expect.table_eq({[df.job_type[EAT]]='1'}, result) + local dig1 = {job_type=DIG, flags={}} + mock_job_list = make_job_list{ + dig1, + {job_type=DIG, flags={}}, + {job_type=EAT, flags={}}, + {job_type=EAT, flags={special=true}}, + {job_type=REST, flags={}}} + mock_postings = {{job=dig1, flags={}}} + p.print_current_jobs({[EAT]={}}) + expect.eq(5, mock_print.call_count) + expect.eq('Current prioritizable jobs:', mock_print.call_args[1][1]) + local _,_,unclaimed,total,job_type = + mock_print.call_args[5][1]:find('^%s*(%d+)%s+(%d+)%s+(.+)$') + expect.eq('0', unclaimed) + expect.eq('1', total) + expect.eq('Eat', job_type) end function test.print_registry() @@ -457,7 +479,7 @@ function test.parse_commandline() expect.table_eq({action=p.status, job_matchers={}}, p.parse_commandline{}) expect.table_eq({action=p.boost, - job_matchers={[SUTURE]={num_prioritized=0}}}, + job_matchers={[SUTURE]={}}}, p.parse_commandline{'Suture'}) expect.printerr_match('Ignoring unknown job type', function() @@ -467,7 +489,7 @@ function test.parse_commandline() expect.printerr_match('Ignoring unknown job type', function() expect.table_eq({action=p.boost, - job_matchers={[SUTURE]={num_prioritized=0}}}, + job_matchers={[SUTURE]={}}}, p.parse_commandline{'XSutureX', 'Suture'}) end) expect.printerr_match('Ignoring unknown unit labor', @@ -487,53 +509,50 @@ function test.parse_commandline() p.parse_commandline{'--quiet'}) expect.table_eq({action=p.boost_and_watch, - job_matchers={[SUTURE]={num_prioritized=0}}}, + job_matchers={[SUTURE]={}}}, p.parse_commandline{'-a', 'Suture'}) expect.table_eq({action=p.boost_and_watch, - job_matchers={[SUTURE]={num_prioritized=0}}}, + job_matchers={[SUTURE]={}}}, p.parse_commandline{'--add', 'Suture'}) expect.table_eq({action=p.remove_watch, - job_matchers={[SUTURE]={num_prioritized=0}}}, + job_matchers={[SUTURE]={}}}, p.parse_commandline{'-d', 'Suture'}) expect.table_eq({action=p.remove_watch, - job_matchers={[SUTURE]={num_prioritized=0}}}, + job_matchers={[SUTURE]={}}}, p.parse_commandline{'--delete', 'Suture'}) expect.table_eq({action=p.print_current_jobs, job_matchers={}}, p.parse_commandline{'-j'}) expect.table_eq({action=p.print_current_jobs, - job_matchers={[SUTURE]={num_prioritized=0}}}, + job_matchers={[SUTURE]={}}}, p.parse_commandline{'-j', 'Suture'}) expect.table_eq({action=p.print_current_jobs, - job_matchers={[SUTURE]={num_prioritized=0}}}, + job_matchers={[SUTURE]={}}}, p.parse_commandline{'--jobs', 'Suture'}) expect.table_eq({action=p.status, job_matchers={}}, p.parse_commandline{'-lfood'}) expect.table_eq({action=p.print_current_jobs, - job_matchers={[SUTURE]={num_prioritized=0}}}, + job_matchers={[SUTURE]={}}}, p.parse_commandline{'-jlfood', 'Suture'}) expect.table_eq({action=p.print_current_jobs, job_matchers={[STORE_ITEM_IN_STOCKPILE]= - {num_prioritized=0, - hauler_matchers={[HAUL_FOOD]=0}}}}, + {hauler_matchers={[HAUL_FOOD]=true}}}}, p.parse_commandline{'-jlfood', 'StoreItemInStockpile'}) expect.table_eq({action=p.status, job_matchers={}}, p.parse_commandline{'-nTAN_A_HIDE'}) expect.table_eq({action=p.boost, - job_matchers={[SUTURE]={num_prioritized=0}}}, + job_matchers={[SUTURE]={}}}, p.parse_commandline{'-nTAN_A_HIDE', 'Suture'}) expect.table_eq({action=p.boost, - job_matchers={[STORE_ITEM_IN_STOCKPILE]= - {num_prioritized=0}}}, + job_matchers={[STORE_ITEM_IN_STOCKPILE]={}}}, p.parse_commandline{'-nTAN_A_HIDE', 'StoreItemInStockpile'}) expect.table_eq({action=p.boost, job_matchers={[CUSTOM_REACTION]= - {num_prioritized=0, - reaction_matchers={TAN_A_HIDE=0}}}}, + {reaction_matchers={TAN_A_HIDE=true}}}}, p.parse_commandline{'-nTAN_A_HIDE', 'CustomReaction'}) expect.table_eq({action=p.print_registry, job_matchers={}}, @@ -541,3 +560,87 @@ function test.parse_commandline() expect.table_eq({action=p.print_registry, job_matchers={}}, p.parse_commandline{'--registry'}) end + +local function selected_patches(sel) + local patches = {} + for _,kind in ipairs{'job', 'item', 'building', 'unit', 'plant', + 'work_order'} do + table.insert(patches, {prioritize, 'get_selected_' .. kind, + function() return sel[kind] end}) + end + return patches +end + +function test.parse_commandline_this() + expect.table_eq({action=prioritize.prioritize_this, job_matchers={}}, + p.parse_commandline{'this'}) + expect.error_match('cannot be combined', function() + p.parse_commandline{'this', 'Suture'} + end) + expect.error_match('cannot be combined', function() + p.parse_commandline{'-a', 'this'} + end) + expect.error_match('cannot be combined', function() + p.parse_commandline{'-lfood', 'this'} + end) +end + +function test.this_boosts_selected_job() + local job = {job_type=DIG, flags={}} + local patches = selected_patches{job=job} + table.insert(patches, {dfhack.job, 'getName', function() return 'Dig' end}) + table.insert(patches, {dfhack.job, 'getHolder', function() return nil end}) + table.insert(patches, {dfhack.job, 'getWorker', function() return nil end}) + mock.patch(patches, function() + prioritize.prioritize_this() + end) + expect.true_(job.flags.do_now) + expect.str_find('top priority', mock_print.call_args[1][1]) +end + +function test.this_already_boosted_job() + local job = {job_type=DIG, flags={do_now=true}} + local patches = selected_patches{job=job} + table.insert(patches, {dfhack.job, 'getName', function() return 'Dig' end}) + table.insert(patches, {dfhack.job, 'getHolder', function() return nil end}) + table.insert(patches, {dfhack.job, 'getWorker', function() return nil end}) + mock.patch(patches, function() + prioritize.prioritize_this() + end) + expect.str_find('already top priority', mock_print.call_args[1][1]) +end + +function test.this_boosts_item_job() + local job = {job_type=DIG, flags={}} + local item = {flags={in_job=true}} + local patches = selected_patches{item=item} + table.insert(patches, {dfhack.items, 'getSpecificRef', + function() return {data={job=job}} end}) + table.insert(patches, {dfhack.job, 'getName', function() return 'Dig' end}) + table.insert(patches, {dfhack.job, 'getHolder', function() return nil end}) + table.insert(patches, {dfhack.job, 'getWorker', function() return nil end}) + mock.patch(patches, function() + prioritize.prioritize_this() + end) + expect.true_(job.flags.do_now) +end + +function test.this_item_not_in_job() + local item = {flags={in_job=false}} + local patches = selected_patches{item=item} + table.insert(patches, {dfhack.items, 'getDescription', + function() return 'a thing' end}) + mock.patch(patches, function() + expect.error_match('must be in a job', function() + prioritize.prioritize_this() + end) + end) +end + +function test.this_nothing_selected() + mock.patch(selected_patches{}, function() + expect.error_match('Select something', function() + prioritize.prioritize_this() + end) + end) +end