From f5648cd2790ba38d5cf458d9bbc81eef744276c2 Mon Sep 17 00:00:00 2001 From: myk002 Date: Sun, 22 Aug 2021 15:08:44 -0700 Subject: [PATCH 1/9] differentiate StoreItemInStockpile jobs by hauler --- prioritize.lua | 312 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 248 insertions(+), 64 deletions(-) diff --git a/prioritize.lua b/prioritize.lua index 1d3a5a69e0..1bc776cd6f 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -13,15 +13,25 @@ the specified types. This is most useful for ensuring important (but low-priority -- according to DF) tasks don't get indefinitely ignored in busy forts. The list of monitored job -types is cleared whenever you load a new map, so you can add a line like the one -below to your onMapLoad.init file to ensure important job types are always -completed promptly in your forts:: +types is cleared whenever you load a new map, so you can add a section like the +one below to your ``onMapLoad.init`` file to ensure important job types are +always completed promptly in your forts:: - prioritize -a StoreItemInVehicle PullLever DestroyBuilding RemoveConstruction RecoverWounded FillPond DumpItem SlaughterAnimal + prioritize -a StoreItemInVehicle StoreItemInBag StoreItemInBarrel PullLever + prioritize -a DestroyBuilding RemoveConstruction RecoverWounded DumpItem + prioritize -a CleanSelf SlaughterAnimal PrepareRawFish ExtractFromRawFish + prioritize -a --hauler-type=Food StoreItemInStockpile + +Tanning hides is also a time-sensitive task, but it doesn't have a specific job +type associated with it. You can prioritize them via:: + + prioritize -a CustomReaction + +but this is likely to prioritize other, unrelated jobs as well. Also see the ``do-job-now`` `tweak` for adding a hotkey to the jobs screen that can toggle the priority of specific individual jobs and the `do-job-now` -script, which sets the priority of jobs related to the selected +script, which sets the priority of jobs related to the current selected job/unit/item/building/order. Usage:: @@ -40,7 +50,7 @@ Examples: ``prioritize ConstructBuilding DestroyBuilding`` Prioritizes all current building construction and destruction jobs. -``prioritize -a StoreItemInVehicle`` +``prioritize -a --hauler-type=Food StoreItemInStockpile StoreItemInVehicle`` Prioritizes all current and future vehicle loading jobs. Options: @@ -59,22 +69,48 @@ Options: Suppress informational output (error messages are still printed). :``-r``, ``--registry``: Print out the full list of valid job types. +:``-t``, ``--hauler-type`` [,...]: + For StoreItemInStockpile jobs, restrict prioritization to the specified + hauler type(s). Valid types are: "Any", "Stone", "Wood", "Item", "Bin", + "Body", "Food", "Refuse", "Furniture", and "Animal". "Bin" includes any + kind of container. If not specified, defaults to "Any". ]====] local argparse = require('argparse') local eventful = require('plugins.eventful') --- set of job types that we are watching. this needs to be global so we don't --- lose player-set state when the script is reparsed. Also a getter function --- that can be mocked out by unit tests. -g_watched_job_types = g_watched_job_types or {} -function get_watched_job_types() return g_watched_job_types end +-- set of job types that we are watching. maps job_type=number to +-- {num_prioritized=number, hauler_types=map of type to num_prioritized} this +-- needs to be global so we don't lose player-set state when the script is +-- reparsed. Also a getter function that can be mocked out by unit tests. +g_watched_job_matchers = g_watched_job_matchers or {} +function get_watched_job_matchers() return g_watched_job_matchers end eventful.enableEvent(eventful.eventType.UNLOAD, 1) eventful.enableEvent(eventful.eventType.JOB_INITIATED, 5) -local function boost_job_if_member(job, job_types) - if job_types[job.job_type] and not job.flags.do_now then +local function make_job_matcher(hauler_types) + local matcher = {num_prioritized=0} + if hauler_types then + local ht_table = {} + for _,ht in ipairs(hauler_types) do + ht_table[ht] = 0 + end + matcher.hauler_matchers = ht_table + end + return matcher +end + +local function matches(job_matcher, job) + if not job_matcher then return false end + if job_matcher.hauler_matchers then + return job_matcher.hauler_matchers[job.item_subtype] + end + return true +end + +local function boost_job_if_matches(job, job_matchers) + if matches(job_matchers[job.job_type], job) and not job.flags.do_now then job.flags.do_now = true return true end @@ -82,9 +118,14 @@ local function boost_job_if_member(job, job_types) end local function on_new_job(job) - local watched_job_types = get_watched_job_types() - if boost_job_if_member(job, watched_job_types) then - watched_job_types[job.job_type] = watched_job_types[job.job_type] + 1 + local watched_job_matchers = get_watched_job_matchers() + if boost_job_if_matches(job, watched_job_matchers) then + jm = watched_job_matchers[job.job_type] + jm.num_prioritized = jm.num_prioritized + 1 + if jm.hauler_matchers then + local hms = jm.hauler_matchers + hms[job.item_subtype] = hms[job.item_subtype] + 1 + end end end @@ -93,34 +134,41 @@ local function has_elements(collection) return false end -local function clear_watched_job_types() - local watched_job_types = get_watched_job_types() - for job_type in pairs(watched_job_types) do - watched_job_types[job_type] = nil +local function clear_watched_job_matchers() + local watched_job_matchers = get_watched_job_matchers() + for job_type in pairs(watched_job_matchers) do + watched_job_matchers[job_type] = nil end eventful.onUnload.prioritize = nil eventful.onJobInitiated.prioritize = nil end local function update_handlers() - local watched_job_types = get_watched_job_types() - if has_elements(watched_job_types) then - eventful.onUnload.prioritize = clear_watched_job_types + local watched_job_matchers = get_watched_job_matchers() + if has_elements(watched_job_matchers) then + eventful.onUnload.prioritize = clear_watched_job_matchers eventful.onJobInitiated.prioritize = on_new_job else - clear_watched_job_types() + clear_watched_job_matchers() end end local function status() local first = true - local watched_job_types = get_watched_job_types() - for k,v in pairs(watched_job_types) do + local watched_job_matchers = get_watched_job_matchers() + for k,v in pairs(watched_job_matchers) do if first then print('Automatically prioritized jobs:') first = false end - print(('%d\t%s'):format(v, df.job_type[k])) + if v.hauler_matchers then + for hk,hv in pairs(v.hauler_matchers) do + print(('%d\t%s (%s)') + :format(hv, df.job_type[k], df.hauler_type[hk])) + end + else + print(('%d\t%s'):format(v.num_prioritized, df.job_type[k])) + end end if first then print('Not automatically prioritizing any jobs.') end end @@ -138,63 +186,172 @@ local function for_all_live_postings(cb) end end -local function boost(job_types, quiet) +local function boost(job_matchers, opts) local count = 0 for_all_live_postings( function(posting) - if boost_job_if_member(posting.job, job_types) then + if boost_job_if_matches(posting.job, job_matchers) then count = count + 1 end end) - if not quiet then + if not opts.quiet then print(('Prioritized %d job%s.'):format(count, count == 1 and '' or 's')) end end -local function boost_and_watch(job_types, quiet) - boost(job_types, quiet) - local watched_job_types = get_watched_job_types() - for job_type in pairs(job_types) do - if watched_job_types[job_type] then +local function get_hauler_type_str(hauler_type) + if not hauler_type then + return '' + end + return (' (%s)'):format(df.hauler_type[hauler_type]) +end + +local function print_add_message(job_type, hauler_type) + local ht_str = get_hauler_type_str(hauler_type) + print(('Automatically prioritizing future jobs of type: %s%s') + :format(df.job_type[job_type], ht_str)) +end + +local function print_skip_add_message(job_type, hauler_type) + local ht_str = get_hauler_type_str(hauler_type) + print(('Skipping already-watched type: %s%s') + :format(df.job_type[job_type], ht_str)) +end + +local function boost_and_watch(job_matchers, opts) + local quiet = opts.quiet + boost(job_matchers, opts) + local watched_job_matchers = get_watched_job_matchers() + for job_type,job_matcher in pairs(job_matchers) do + local wjm = watched_job_matchers[job_type] + if job_type == df.job_type.StoreItemInStockpile then + if not wjm then + watched_job_matchers[job_type] = job_matcher + if not quiet then + local hms = job_matcher.hauler_matchers + if not hms then + print_add_message(job_type, df.hauler_type.Any) + else + for ht in pairs(hms) do + print_add_message(job_type, ht) + end + end + end + else + if not wjm.hauler_matchers + and not job_matcher.hauler_matchers then + if not quiet then + print_skip_add_message(job_type, df.hauler_type.Any) + end + elseif not wjm.hauler_matchers then + for ht in pairs(job_matcher.hauler_matchers) do + if not quiet then + print_skip_add_message(job_type, ht) + end + end + elseif not job_matcher.hauler_matchers then + if not quiet then + print_add_message(job_type, df.hauler_type.Any) + end + wjm.hauler_matchers = nil + else + for ht in pairs(job_matcher.hauler_matchers) do + if wjm.hauler_matchers[ht] then + if not quiet then + print_skip_add_message(job_type, ht) + end + else + wjm.hauler_matchers[ht] = 0 + if not quiet then + print_add_message(job_type, ht) + end + end + end + end + end + elseif wjm then if not quiet then - print('Skipping already-watched type: '..df.job_type[job_type]) + print_skip_add_message(job_type) end else - watched_job_types[job_type] = 0 + watched_job_matchers[job_type] = job_matcher if not quiet then - print('Automatically prioritizing future jobs of type: ' .. - df.job_type[job_type]) + print_add_message(job_type) end end end update_handlers() end -local function remove_watch(job_types, quiet) - local watched_job_types = get_watched_job_types() - for job_type in pairs(job_types) do - if not watched_job_types[job_type] then +local function print_del_message(job_type, hauler_type) + local ht_str = get_hauler_type_str(hauler_type) + print(('No longer automatically prioritizing jobs of type: %s%s') + :format(df.job_type[job_type], ht_str)) +end + +local function print_skip_del_message(job_type, hauler_type) + local ht_str = get_hauler_type_str(hauler_type) + print(('Skipping unwatched type: %s%s') + :format(df.job_type[job_type], ht_str)) +end + +local function remove_watch(job_matchers, opts) + local quiet = opts.quiet + local watched_job_matchers = get_watched_job_matchers() + for job_type,job_matcher in pairs(job_matchers) do + local wjm = watched_job_matchers[job_type] + if not wjm then if not quiet then - print('Skipping unwatched type: ' .. df.job_type[job_type]) + print_skip_del_message(job_type) end - else - watched_job_types[job_type] = nil + elseif not job_matcher.hauler_matchers then + watched_job_matchers[job_type] = nil if not quiet then - print('No longer automatically prioritizing jobs of type: ' .. - df.job_type[job_type]) + print_del_message(job_type) + end + else + if not wjm.hauler_matchers then + wjm.hauler_matchers = {} + for id in ipairs(df.hauler_type) do + if id ~= df.hauler_type.Any then + wjm.hauler_matchers[id] = 0 + end + end + end + for ht in pairs(job_matcher.hauler_matchers) do + if wjm.hauler_matchers[ht] then + if not quiet then + print_del_message(job_type, ht) + end + wjm.hauler_matchers[ht] = nil + else + if not quiet then + print_skip_del_message(job_type, ht) + end + end end end end update_handlers() end -local function print_current_jobs(job_types) +local function get_job_type_str(job) + local job_type = job.job_type + local job_type_str = df.job_type[job_type] + if job_type ~= df.job_type.StoreItemInStockpile then + return job_type_str + end + return ('%s%s'):format(job_type_str, get_hauler_type_str(job.item_subtype)) +end + +local function print_current_jobs(job_matchers, opts) local job_counts_by_type = {} - local filtered = has_elements(job_types) + local filtered = has_elements(job_matchers) for_all_live_postings( function(posting) - local job_type = posting.job.job_type - if filtered and not job_types[job_type] then return end + local job = posting.job + if filtered and not job_matchers[job.job_type] then return end + local job_type = get_job_type_str(job) if not job_counts_by_type[job_type] then job_counts_by_type[job_type] = 0 end @@ -206,7 +363,7 @@ local function print_current_jobs(job_types) print('Current job counts by type:') first = false end - print(('%d\t%s'):format(v, df.job_type[k])) + print(('%d\t%s'):format(v, k)) end if first then print('No current jobs.') end end @@ -221,7 +378,7 @@ local function print_registry() end local function parse_commandline(args) - local opts, action = {}, status + local opts, action, hauler_types = {}, status, nil local positionals = argparse.processArgsGetopt(args, { {'a', 'add', handler=function() action = boost_and_watch end}, {'d', 'delete', handler=function() action = remove_watch end}, @@ -229,24 +386,51 @@ local function parse_commandline(args) {'j', 'jobs', handler=function() action = print_current_jobs end}, {'q', 'quiet', handler=function() opts.quiet = true end}, {'r', 'registry', handler=function() action = print_registry end}, + {'t', 'hauler-type', hasArg=true, + handler=function(arg) hauler_types = argparse.stringList(arg) end}, }) if positionals[1] == 'help' then opts.help = true end if opts.help then return opts end - -- validate the specified job types and convert the list to a map - local job_types = {} - for _,jtype in ipairs(positionals) do - if not df.job_type[jtype] then + -- validate any specified hauler types and convert the list to ids + if hauler_types then + local ht_ids = nil + for _,htype in ipairs(hauler_types) do + if not df.hauler_type[htype] then + dfhack.printerr(('Ignoring unknown hauler type: "%s". Run' .. + ' "prioritize -h" for a list of valid hauler types.') + :format(htype)) + else + if htype == df.hauler_type.Any then + ht_ids = nil + break + end + ht_ids = ht_ids or {} + table.insert(ht_ids, df.hauler_type[htype]) + end + end + hauler_types = ht_ids + end + + -- validate the specified job types and create matchers + local job_matchers = {} + for _,job_type_name in ipairs(positionals) do + local job_type = df.job_type[job_type_name] + if not job_type then dfhack.printerr(('Ignoring unknown job type: "%s". Run' .. - ' "prioritize -r" for a list of valid job types.'):format(jtype)) + ' "prioritize -r" for a list of valid job types.') + :format(job_type_name)) else - job_types[df.job_type[jtype]] = true + local job_matcher = make_job_matcher( + job_type == df.job_type.StoreItemInStockpile + and hauler_types or nil) + job_matchers[job_type] = job_matcher end end - opts.job_types = job_types + opts.job_matchers = job_matchers - if action == status and has_elements(job_types) then + if action == status and has_elements(job_matchers) then action = boost end opts.action = action @@ -259,12 +443,12 @@ if not dfhack_flags.module then local opts = parse_commandline({...}) if opts.help then print(dfhack.script_help()) return end - opts.action(opts.job_types, opts.quiet) + opts.action(opts.job_matchers, opts) end if dfhack.internal.IN_TEST then unit_test_hooks = { - clear_watched_job_types=clear_watched_job_types, + clear_watched_job_matchers=clear_watched_job_matchers, on_new_job=on_new_job, status=status, boost=boost, From e25c82df6cd4e501532d8a912eed90736bf3ca65 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 23 Aug 2021 00:21:54 -0700 Subject: [PATCH 2/9] migrate to using unit_labor instead of hauler_type --- prioritize.lua | 147 ++++++++++++++++++++++++------------------------- 1 file changed, 73 insertions(+), 74 deletions(-) diff --git a/prioritize.lua b/prioritize.lua index 1bc776cd6f..e4777603e1 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -20,7 +20,7 @@ always completed promptly in your forts:: prioritize -a StoreItemInVehicle StoreItemInBag StoreItemInBarrel PullLever prioritize -a DestroyBuilding RemoveConstruction RecoverWounded DumpItem prioritize -a CleanSelf SlaughterAnimal PrepareRawFish ExtractFromRawFish - prioritize -a --hauler-type=Food StoreItemInStockpile + prioritize -a --haul-labor=Food StoreItemInStockpile Tanning hides is also a time-sensitive task, but it doesn't have a specific job type associated with it. You can prioritize them via:: @@ -50,8 +50,8 @@ Examples: ``prioritize ConstructBuilding DestroyBuilding`` Prioritizes all current building construction and destruction jobs. -``prioritize -a --hauler-type=Food StoreItemInStockpile StoreItemInVehicle`` - Prioritizes all current and future vehicle loading jobs. +``prioritize -a --haul-labor=Food StoreItemInStockpile StoreItemInVehicle`` + Prioritizes all current and future food hauling and vehicle loading jobs. Options: @@ -65,22 +65,22 @@ Options: Print out how many jobs of each type there are. This is useful for discovering the types of the jobs which you can prioritize right now. If any job types are specified, only returns the current count for those types. +:``-l``, ``--haul-labor`` [,...]: + For StoreItemInStockpile jobs, restrict prioritization to the specified + hauling labor(s). Valid types are: "Stone", "Wood", "Body", "Food", + "Refuse", "Item", "Furniture", and "Animals". If not specified, defaults to + all. :``-q``, ``--quiet``: Suppress informational output (error messages are still printed). :``-r``, ``--registry``: Print out the full list of valid job types. -:``-t``, ``--hauler-type`` [,...]: - For StoreItemInStockpile jobs, restrict prioritization to the specified - hauler type(s). Valid types are: "Any", "Stone", "Wood", "Item", "Bin", - "Body", "Food", "Refuse", "Furniture", and "Animal". "Bin" includes any - kind of container. If not specified, defaults to "Any". ]====] local argparse = require('argparse') local eventful = require('plugins.eventful') -- set of job types that we are watching. maps job_type=number to --- {num_prioritized=number, hauler_types=map of type to num_prioritized} this +-- {num_prioritized=number, unit_labors=map of type to num_prioritized} this -- needs to be global so we don't lose player-set state when the script is -- reparsed. Also a getter function that can be mocked out by unit tests. g_watched_job_matchers = g_watched_job_matchers or {} @@ -89,14 +89,14 @@ function get_watched_job_matchers() return g_watched_job_matchers end eventful.enableEvent(eventful.eventType.UNLOAD, 1) eventful.enableEvent(eventful.eventType.JOB_INITIATED, 5) -local function make_job_matcher(hauler_types) +local function make_job_matcher(unit_labors) local matcher = {num_prioritized=0} - if hauler_types then - local ht_table = {} - for _,ht in ipairs(hauler_types) do - ht_table[ht] = 0 + if unit_labors then + local ul_table = {} + for _,ul in ipairs(unit_labors) do + ul_table[ul] = 0 end - matcher.hauler_matchers = ht_table + matcher.hauler_matchers = ul_table end return matcher end @@ -153,6 +153,14 @@ local function update_handlers() end end +local function get_unit_labor_str(unit_labor) + if not unit_labor then + return '' + end + local labor_str = df.unit_labor[unit_labor] + return (' (%s%s)'):format(labor_str:sub(6,6), labor_str:sub(7):lower()) +end + local function status() local first = true local watched_job_matchers = get_watched_job_matchers() @@ -163,8 +171,8 @@ local function status() end if v.hauler_matchers then for hk,hv in pairs(v.hauler_matchers) do - print(('%d\t%s (%s)') - :format(hv, df.job_type[k], df.hauler_type[hk])) + print(('%d\t%s%s') + :format(hv, df.job_type[k], get_unit_labor_str(hk))) end else print(('%d\t%s'):format(v.num_prioritized, df.job_type[k])) @@ -199,23 +207,16 @@ local function boost(job_matchers, opts) end end -local function get_hauler_type_str(hauler_type) - if not hauler_type then - return '' - end - return (' (%s)'):format(df.hauler_type[hauler_type]) -end - -local function print_add_message(job_type, hauler_type) - local ht_str = get_hauler_type_str(hauler_type) +local function print_add_message(job_type, unit_labor) + local ul_str = get_unit_labor_str(unit_labor) print(('Automatically prioritizing future jobs of type: %s%s') - :format(df.job_type[job_type], ht_str)) + :format(df.job_type[job_type], ul_str)) end -local function print_skip_add_message(job_type, hauler_type) - local ht_str = get_hauler_type_str(hauler_type) +local function print_skip_add_message(job_type, unit_labor) + local ul_str = get_unit_labor_str(unit_labor) print(('Skipping already-watched type: %s%s') - :format(df.job_type[job_type], ht_str)) + :format(df.job_type[job_type], ul_str)) end local function boost_and_watch(job_matchers, opts) @@ -230,10 +231,10 @@ local function boost_and_watch(job_matchers, opts) if not quiet then local hms = job_matcher.hauler_matchers if not hms then - print_add_message(job_type, df.hauler_type.Any) + print_add_message(job_type) else - for ht in pairs(hms) do - print_add_message(job_type, ht) + for ul in pairs(hms) do + print_add_message(job_type, ul) end end end @@ -241,29 +242,29 @@ local function boost_and_watch(job_matchers, opts) if not wjm.hauler_matchers and not job_matcher.hauler_matchers then if not quiet then - print_skip_add_message(job_type, df.hauler_type.Any) + print_skip_add_message(job_type) end elseif not wjm.hauler_matchers then - for ht in pairs(job_matcher.hauler_matchers) do + for ul in pairs(job_matcher.hauler_matchers) do if not quiet then - print_skip_add_message(job_type, ht) + print_skip_add_message(job_type, ul) end end elseif not job_matcher.hauler_matchers then if not quiet then - print_add_message(job_type, df.hauler_type.Any) + print_add_message(job_type) end wjm.hauler_matchers = nil else - for ht in pairs(job_matcher.hauler_matchers) do - if wjm.hauler_matchers[ht] then + for ul in pairs(job_matcher.hauler_matchers) do + if wjm.hauler_matchers[ul] then if not quiet then - print_skip_add_message(job_type, ht) + print_skip_add_message(job_type, ul) end else - wjm.hauler_matchers[ht] = 0 + wjm.hauler_matchers[ul] = 0 if not quiet then - print_add_message(job_type, ht) + print_add_message(job_type, ul) end end end @@ -283,16 +284,16 @@ local function boost_and_watch(job_matchers, opts) update_handlers() end -local function print_del_message(job_type, hauler_type) - local ht_str = get_hauler_type_str(hauler_type) +local function print_del_message(job_type, unit_labor) + local ul_str = get_unit_labor_str(unit_labor) print(('No longer automatically prioritizing jobs of type: %s%s') - :format(df.job_type[job_type], ht_str)) + :format(df.job_type[job_type], ul_str)) end -local function print_skip_del_message(job_type, hauler_type) - local ht_str = get_hauler_type_str(hauler_type) +local function print_skip_del_message(job_type, unit_labor) + local ul_str = get_unit_labor_str(unit_labor) print(('Skipping unwatched type: %s%s') - :format(df.job_type[job_type], ht_str)) + :format(df.job_type[job_type], ul_str)) end local function remove_watch(job_matchers, opts) @@ -312,21 +313,22 @@ local function remove_watch(job_matchers, opts) else if not wjm.hauler_matchers then wjm.hauler_matchers = {} - for id in ipairs(df.hauler_type) do - if id ~= df.hauler_type.Any then + for id,name in ipairs(df.unit_labor) do + if name:startswith('HAUL_') + and id <= df.unit_labor.HAUL_ANIMALS then wjm.hauler_matchers[id] = 0 end end end - for ht in pairs(job_matcher.hauler_matchers) do - if wjm.hauler_matchers[ht] then + for ul in pairs(job_matcher.hauler_matchers) do + if wjm.hauler_matchers[ul] then if not quiet then - print_del_message(job_type, ht) + print_del_message(job_type, ul) end - wjm.hauler_matchers[ht] = nil + wjm.hauler_matchers[ul] = nil else if not quiet then - print_skip_del_message(job_type, ht) + print_skip_del_message(job_type, ul) end end end @@ -341,7 +343,7 @@ local function get_job_type_str(job) if job_type ~= df.job_type.StoreItemInStockpile then return job_type_str end - return ('%s%s'):format(job_type_str, get_hauler_type_str(job.item_subtype)) + return ('%s%s'):format(job_type_str, get_unit_labor_str(job.item_subtype)) end local function print_current_jobs(job_matchers, opts) @@ -378,39 +380,36 @@ local function print_registry() end local function parse_commandline(args) - local opts, action, hauler_types = {}, status, nil + local opts, action, unit_labors = {}, status, nil local positionals = argparse.processArgsGetopt(args, { {'a', 'add', handler=function() action = boost_and_watch end}, {'d', 'delete', handler=function() action = remove_watch end}, {'h', 'help', handler=function() opts.help = true end}, {'j', 'jobs', handler=function() action = print_current_jobs end}, + {'l', 'haul-labor', hasArg=true, + handler=function(arg) unit_labors = argparse.stringList(arg) end}, {'q', 'quiet', handler=function() opts.quiet = true end}, {'r', 'registry', handler=function() action = print_registry end}, - {'t', 'hauler-type', hasArg=true, - handler=function(arg) hauler_types = argparse.stringList(arg) end}, }) if positionals[1] == 'help' then opts.help = true end if opts.help then return opts end -- validate any specified hauler types and convert the list to ids - if hauler_types then - local ht_ids = nil - for _,htype in ipairs(hauler_types) do - if not df.hauler_type[htype] then - dfhack.printerr(('Ignoring unknown hauler type: "%s". Run' .. - ' "prioritize -h" for a list of valid hauler types.') - :format(htype)) + if unit_labors then + local ul_ids = nil + for _,ulabor in ipairs(unit_labors) do + ulabor = 'HAUL_'..ulabor:upper() + if not df.unit_labor[ulabor] then + dfhack.printerr(('Ignoring unknown unit labor: "%s". Run' .. + ' "prioritize -h" for a list of valid hauling labors.') + :format(ulabor)) else - if htype == df.hauler_type.Any then - ht_ids = nil - break - end - ht_ids = ht_ids or {} - table.insert(ht_ids, df.hauler_type[htype]) + ul_ids = ul_ids or {} + table.insert(ul_ids, df.unit_labor[ulabor]) end end - hauler_types = ht_ids + unit_labors = ul_ids end -- validate the specified job types and create matchers @@ -424,7 +423,7 @@ local function parse_commandline(args) else local job_matcher = make_job_matcher( job_type == df.job_type.StoreItemInStockpile - and hauler_types or nil) + and unit_labors or nil) job_matchers[job_type] = job_matcher end end From 3ed1f825a5416f819d9fa34394edf922e6e50005 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 23 Aug 2021 11:52:10 -0700 Subject: [PATCH 3/9] fix existing unit tests; update docs --- prioritize.lua | 45 +++++++++++++------- test/prioritize.lua | 100 +++++++++++++++++++++++--------------------- 2 files changed, 81 insertions(+), 64 deletions(-) diff --git a/prioritize.lua b/prioritize.lua index e4777603e1..baa5748241 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -6,21 +6,23 @@ prioritize ========== The prioritize script sets the ``do_now`` flag on all of the specified types of -jobs that are currently ready to be picked up by a dwarf. This will force them -to complete those jobs as soon as possible. This script can also continue to -monitor creation of new jobs and automatically boost the priority of jobs of -the specified types. +jobs that are ready to be picked up by a dwarf but not yet assigned to a dwarf. +This will force them to get assigned and completed as soon as possible. -This is most useful for ensuring important (but low-priority -- according to DF) +This script can also continue to monitor new jobs and automatically boost the +priority of jobs of the specified types. + +This is useful for ensuring important (but low-priority -- according to DF) tasks don't get indefinitely ignored in busy forts. The list of monitored job types is cleared whenever you load a new map, so you can add a section like the one below to your ``onMapLoad.init`` file to ensure important job types are always completed promptly in your forts:: - prioritize -a StoreItemInVehicle StoreItemInBag StoreItemInBarrel PullLever - prioritize -a DestroyBuilding RemoveConstruction RecoverWounded DumpItem - prioritize -a CleanSelf SlaughterAnimal PrepareRawFish ExtractFromRawFish prioritize -a --haul-labor=Food StoreItemInStockpile + prioritize -a PullLever CleanSelf RecoverWounded DumpItem + prioritize -a DestroyBuilding RemoveConstruction + prioritize -a StoreItemInVehicle StoreItemInBag StoreItemInBarrel + prioritize -a SlaughterAnimal PrepareRawFish ExtractFromRawFish Tanning hides is also a time-sensitive task, but it doesn't have a specific job type associated with it. You can prioritize them via:: @@ -29,6 +31,17 @@ type associated with it. You can prioritize them via:: but this is likely to prioritize other, unrelated jobs as well. +It is important to automatically prioritize only the *most* important job types. +If you add too many job types, or if there are simply too many jobs of those +types in your fort, the other tasks in your fort can get ignored. This causes +the same problem the ``prioritize`` script is designed to solve. The example +commands above have been extensively playtested and are a good default set. If +you need a bunch of jobs of a specific type prioritized *right now*, consider +running ``prioritize`` without the ``-a`` parameter, which only affects +currently available jobs. For example:: + + prioritize ConstructBuilding + Also see the ``do-job-now`` `tweak` for adding a hotkey to the jobs screen that can toggle the priority of specific individual jobs and the `do-job-now` script, which sets the priority of jobs related to the current selected @@ -56,20 +69,20 @@ Examples: Options: :``-a``, ``--add``: - Prioritize all current and future new jobs of the specified job types. + Prioritize all current and future jobs of the specified job types. :``-d``, ``--delete``: Stop automatically prioritizing new jobs of the specified job types. :``-h``, ``--help``: Show help text. :``-j``, ``--jobs``: - Print out how many jobs of each type there are. This is useful for - discovering the types of the jobs which you can prioritize right now. If any - job types are specified, only returns the current count for those types. + Print out how many unassigned jobs of each type there are. This is useful + for discovering the types of the jobs that you can prioritize right now. If + any job types are specified, only returns the count for those types. :``-l``, ``--haul-labor`` [,...]: For StoreItemInStockpile jobs, restrict prioritization to the specified hauling labor(s). Valid types are: "Stone", "Wood", "Body", "Food", "Refuse", "Item", "Furniture", and "Animals". If not specified, defaults to - all. + matching all StoreItemInStockpile jobs. :``-q``, ``--quiet``: Suppress informational output (error messages are still printed). :``-r``, ``--registry``: @@ -79,9 +92,9 @@ Options: local argparse = require('argparse') local eventful = require('plugins.eventful') --- set of job types that we are watching. maps job_type=number to --- {num_prioritized=number, unit_labors=map of type to num_prioritized} this --- needs to be global so we don't lose player-set state when the script is +-- set of job types that we are watching. maps job_type (as a number) to +-- {num_prioritized=number, hauler_matchers=map of type to num_prioritized} +-- this needs to be global so we don't lose player-set state when the script is -- reparsed. Also a getter function that can be mocked out by unit tests. g_watched_job_matchers = g_watched_job_matchers or {} function get_watched_job_matchers() return g_watched_job_matchers end diff --git a/test/prioritize.lua b/test/prioritize.lua index 679d88d981..5707670bb1 100644 --- a/test/prioritize.lua +++ b/test/prioritize.lua @@ -5,21 +5,23 @@ local p = prioritize.unit_test_hooks -- mock out state and external dependencies local mock_eventful_onUnload, mock_eventful_onJobInitiated = {}, {} local mock_print = mock.func() -local mock_watched_job_types = {} -local function get_mock_watched_job_types() return mock_watched_job_types end +local mock_watched_job_matchers = {} +local function get_mock_watched_job_matchers() + return mock_watched_job_matchers +end local mock_postings = {} local function get_mock_postings() return mock_postings end local function test_wrapper(test_fn) mock.patch({{eventful, 'onUnload', mock_eventful_onUnload}, {eventful, 'onJobInitiated', mock_eventful_onJobInitiated}, {prioritize, 'print', mock_print}, - {prioritize, 'get_watched_job_types', - get_mock_watched_job_types}, + {prioritize, 'get_watched_job_matchers', + get_mock_watched_job_matchers}, {prioritize, 'get_postings', get_mock_postings}}, test_fn) mock_eventful_onUnload, mock_eventful_onJobInitiated = {}, {} mock_print = mock.func() - mock_watched_job_types, mock_postings = {}, {} + mock_watched_job_matchers, mock_postings = {}, {} end config.wrapper = test_wrapper @@ -31,7 +33,7 @@ function test.status() expect.eq('Not automatically prioritizing any jobs.', mock_print.call_args[1][1]) - mock_watched_job_types[REST] = 5 + mock_watched_job_matchers[REST] = {num_prioritized=5} p.status() expect.eq(3, mock_print.call_count) expect.eq('Automatically prioritized jobs:', mock_print.call_args[2][1]) @@ -50,7 +52,7 @@ function test.boost() {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]=true}) + p.boost({[EAT]={num_prioritized=0}}, {}) expect.eq(1, mock_print.call_count) expect.eq('Prioritized 1 job.', mock_print.call_args[1][1]) expect.table_eq(expected_postings, mock_postings) @@ -68,70 +70,70 @@ function test.boost_quiet() {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]=true}, true) + p.boost({[EAT]={num_prioritized=0}}, {quiet=true}) expect.eq(0, mock_print.call_count) expect.table_eq(expected_postings, mock_postings) end function test.boost_and_watch() - p.boost_and_watch({[DIG]=true}) + p.boost_and_watch({[DIG]={num_prioritized=0}}, {}) expect.eq(2, mock_print.call_count) expect.true_(mock_print.call_args[1][1]:find('^Prioritized')) expect.true_(mock_print.call_args[2][1]:find('^Automatically')) - expect.table_eq({[DIG]=0}, mock_watched_job_types) + expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) - p.boost_and_watch({[DIG]=true}) + p.boost_and_watch({[DIG]={num_prioritized=0}}, {}) expect.eq(4, mock_print.call_count) expect.true_(mock_print.call_args[3][1]:find('^Prioritized')) expect.true_(mock_print.call_args[4][1]:find('^Skipping')) - expect.table_eq({[DIG]=0}, mock_watched_job_types) + expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) end function test.boost_and_watch_quiet() - p.boost_and_watch({[DIG]=true}, true) + p.boost_and_watch({[DIG]={num_prioritized=0}}, {quiet=true}) expect.eq(0, mock_print.call_count) - expect.table_eq({[DIG]=0}, mock_watched_job_types) + expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) - p.boost_and_watch({[DIG]=true}, true) + p.boost_and_watch({[DIG]={num_prioritized=0}}, {quiet=true}) expect.eq(0, mock_print.call_count) - expect.table_eq({[DIG]=0}, mock_watched_job_types) + expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) end function test.remove_watch() - p.remove_watch({[DIG]=true}) + p.remove_watch({[DIG]={num_prioritized=0}}, {}) expect.eq(1, mock_print.call_count) expect.true_(mock_print.call_args[1][1]:find('Skipping unwatched')) - expect.table_eq({}, mock_watched_job_types) + expect.table_eq({}, mock_watched_job_matchers) - mock_watched_job_types[DIG] = 0 - p.remove_watch({[DIG]=true}) + mock_watched_job_matchers[DIG] = {num_prioritized=0} + p.remove_watch({[DIG]={num_prioritized=0}}, {}) expect.eq(2, mock_print.call_count) expect.true_(mock_print.call_args[2][1]:find('No longer')) end function test.remove_watch_quiet() - p.remove_watch({[DIG]=true}, true) + p.remove_watch({[DIG]={num_prioritized=0}}, {quiet=true}) expect.eq(0, mock_print.call_count) - expect.table_eq({}, mock_watched_job_types) + expect.table_eq({}, mock_watched_job_matchers) - mock_watched_job_types[DIG] = 0 - p.remove_watch({[DIG]=true}, true) + mock_watched_job_matchers[DIG] = {num_prioritized=0} + p.remove_watch({[DIG]={num_prioritized=0}}, {quiet=true}) expect.eq(0, mock_print.call_count) - expect.table_eq({}, mock_watched_job_types) + expect.table_eq({}, mock_watched_job_matchers) end function test.eventful_hook_lifecycle() expect.nil_(mock_eventful_onUnload.prioritize) expect.nil_(mock_eventful_onJobInitiated.prioritize) - p.boost_and_watch({[DIG]=true}, true) - expect.table_eq({[DIG]=0}, mock_watched_job_types) + p.boost_and_watch({[DIG]={num_prioritized=0}}, {quiet=true}) + expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) - expect.eq(p.clear_watched_job_types, mock_eventful_onUnload.prioritize) + 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]=true}, true) - expect.table_eq({}, mock_watched_job_types) + p.remove_watch({[DIG]={num_prioritized=0}}, {quiet=true}) + expect.table_eq({}, mock_watched_job_matchers) expect.nil_(mock_eventful_onUnload.prioritize) expect.nil_(mock_eventful_onJobInitiated.prioritize) @@ -146,13 +148,13 @@ function test.eventful_callbacks() -- watched job local expected = {job_type=DIG, flags={do_now=true}} - p.boost_and_watch({[DIG]=true}, true) + p.boost_and_watch({[DIG]={num_prioritized=0}}, {quiet=true}) p.on_new_job(job) expect.table_eq(expected, job) -- map unload - p.clear_watched_job_types() - expect.table_eq({}, mock_watched_job_types) + p.clear_watched_job_matchers() + expect.table_eq({}, mock_watched_job_matchers) expect.nil_(mock_eventful_onUnload.prioritize) expect.nil_(mock_eventful_onJobInitiated.prioritize) end @@ -221,56 +223,58 @@ function test.print_registry() end end +local SUTURE = df.job_type['Suture'] function test.parse_commandline() expect.table_eq({help=true}, p.parse_commandline{'help'}) expect.table_eq({help=true}, p.parse_commandline{'-h'}) expect.table_eq({help=true}, p.parse_commandline{'--help'}) - expect.table_eq({action=p.status, job_types={}}, p.parse_commandline{}) - expect.table_eq({action=p.boost, job_types={[df.job_type['Suture']]=true}}, + expect.table_eq({action=p.status, job_matchers={}}, p.parse_commandline{}) + expect.table_eq({action=p.boost, + job_matchers={[SUTURE]={num_prioritized=0}}}, p.parse_commandline{'Suture'}) expect.printerr_match('Ignoring unknown job type', function() - expect.table_eq({action=p.status, job_types={}}, + expect.table_eq({action=p.status, job_matchers={}}, p.parse_commandline{'XSutureX'}) end) expect.printerr_match('Ignoring unknown job type', function() expect.table_eq({action=p.boost, - job_types={[df.job_type['Suture']]=true}}, + job_matchers={[SUTURE]={num_prioritized=0}}}, p.parse_commandline{'XSutureX', 'Suture'}) end) - expect.table_eq({action=p.status, job_types={}, quiet=true}, + expect.table_eq({action=p.status, job_matchers={}, quiet=true}, p.parse_commandline{'-q'}) - expect.table_eq({action=p.status, job_types={}, quiet=true}, + expect.table_eq({action=p.status, job_matchers={}, quiet=true}, p.parse_commandline{'--quiet'}) expect.table_eq({action=p.boost_and_watch, - job_types={[df.job_type['Suture']]=true}}, + job_matchers={[SUTURE]={num_prioritized=0}}}, p.parse_commandline{'-a', 'Suture'}) expect.table_eq({action=p.boost_and_watch, - job_types={[df.job_type['Suture']]=true}}, + job_matchers={[SUTURE]={num_prioritized=0}}}, p.parse_commandline{'--add', 'Suture'}) expect.table_eq({action=p.remove_watch, - job_types={[df.job_type['Suture']]=true}}, + job_matchers={[SUTURE]={num_prioritized=0}}}, p.parse_commandline{'-d', 'Suture'}) expect.table_eq({action=p.remove_watch, - job_types={[df.job_type['Suture']]=true}}, + job_matchers={[SUTURE]={num_prioritized=0}}}, p.parse_commandline{'--delete', 'Suture'}) - expect.table_eq({action=p.print_current_jobs, job_types={}}, + expect.table_eq({action=p.print_current_jobs, job_matchers={}}, p.parse_commandline{'-j'}) expect.table_eq({action=p.print_current_jobs, - job_types={[df.job_type['Suture']]=true}}, + job_matchers={[SUTURE]={num_prioritized=0}}}, p.parse_commandline{'-j', 'Suture'}) expect.table_eq({action=p.print_current_jobs, - job_types={[df.job_type['Suture']]=true}}, + job_matchers={[SUTURE]={num_prioritized=0}}}, p.parse_commandline{'--jobs', 'Suture'}) - expect.table_eq({action=p.print_registry, job_types={}}, + expect.table_eq({action=p.print_registry, job_matchers={}}, p.parse_commandline{'-r'}) - expect.table_eq({action=p.print_registry, job_types={}}, + expect.table_eq({action=p.print_registry, job_matchers={}}, p.parse_commandline{'--registry'}) end From a1fd5161b78338580bbfbed002d65d70980bc83e Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 23 Aug 2021 23:29:29 -0700 Subject: [PATCH 4/9] unit test labor code --- test/prioritize.lua | 207 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 193 insertions(+), 14 deletions(-) diff --git a/test/prioritize.lua b/test/prioritize.lua index 5707670bb1..07be06b30f 100644 --- a/test/prioritize.lua +++ b/test/prioritize.lua @@ -1,5 +1,6 @@ local eventful = require('plugins.eventful') local prioritize = reqscript('prioritize') +local utils = require('utils') local p = prioritize.unit_test_hooks -- mock out state and external dependencies @@ -26,6 +27,15 @@ end config.wrapper = test_wrapper local DIG, EAT, REST = df.job_type.Dig, df.job_type.Eat, df.job_type.Rest +local STORE_ITEM_IN_STOCKPILE = df.job_type.StoreItemInStockpile +local SUTURE = df.job_type.Suture + +local HAUL_FOOD, HAUL_ITEM = df.unit_labor.HAUL_FOOD, df.unit_labor.HAUL_ITEM + +local HAUL_STONE, HAUL_WOOD = df.unit_labor.HAUL_STONE, df.unit_labor.HAUL_WOOD +local HAUL_BODY, HAUL_FOOD = df.unit_labor.HAUL_BODY, df.unit_labor.HAUL_FOOD +local HAUL_REFUSE, HAUL_ITEM = df.unit_labor.HAUL_REFUSE, df.unit_labor.HAUL_ITEM +local HAUL_FURNITURE, HAUL_ANIMALS = df.unit_labor.HAUL_FURNITURE, df.unit_labor.HAUL_ANIMALS function test.status() p.status() @@ -37,7 +47,21 @@ function test.status() p.status() expect.eq(3, mock_print.call_count) expect.eq('Automatically prioritized jobs:', mock_print.call_args[2][1]) - expect.true_(mock_print.call_args[3][1]:find('Rest')) + expect.find('Rest', mock_print.call_args[3][1]) +end + +function test.status_labor() + 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[STORE_ITEM_IN_STOCKPILE] = + {num_prioritized=5, hauler_matchers={[HAUL_BODY]=2}} + p.status() + expect.eq(3, mock_print.call_count) + expect.eq('Automatically prioritized jobs:', mock_print.call_args[2][1]) + expect.find('Stockpile.*Body', mock_print.call_args[3][1]) end function test.boost() @@ -78,14 +102,14 @@ end function test.boost_and_watch() p.boost_and_watch({[DIG]={num_prioritized=0}}, {}) expect.eq(2, mock_print.call_count) - expect.true_(mock_print.call_args[1][1]:find('^Prioritized')) - expect.true_(mock_print.call_args[2][1]:find('^Automatically')) + expect.find('^Prioritized', mock_print.call_args[1][1]) + expect.find('^Automatically', mock_print.call_args[2][1]) expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) p.boost_and_watch({[DIG]={num_prioritized=0}}, {}) expect.eq(4, mock_print.call_count) - expect.true_(mock_print.call_args[3][1]:find('^Prioritized')) - expect.true_(mock_print.call_args[4][1]:find('^Skipping')) + expect.find('^Prioritized', mock_print.call_args[3][1]) + expect.find('^Skipping', mock_print.call_args[4][1]) expect.table_eq({[DIG]={num_prioritized=0}}, mock_watched_job_matchers) end @@ -102,13 +126,13 @@ end function test.remove_watch() p.remove_watch({[DIG]={num_prioritized=0}}, {}) expect.eq(1, mock_print.call_count) - expect.true_(mock_print.call_args[1][1]:find('Skipping unwatched')) + expect.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}}, {}) expect.eq(2, mock_print.call_count) - expect.true_(mock_print.call_args[2][1]:find('No longer')) + expect.find('No longer', mock_print.call_args[2][1]) end function test.remove_watch_quiet() @@ -122,6 +146,115 @@ function test.remove_watch_quiet() 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}}}, + {}) + expect.eq(2, mock_print.call_count) + expect.find('^Prioritized 2', mock_print.call_args[1][1]) + expect.find('^Automatically', mock_print.call_args[2][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, + 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) + expect.find('^Prioritized 1', mock_print.call_args[3][1]) + expect.find('^Automatically', mock_print.call_args[4][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, + mock_watched_job_matchers) +end + +function test.boost_and_watch_store_all_labors() + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, {}) + expect.eq(2, mock_print.call_count) + expect.find('^Prioritized 0', mock_print.call_args[1][1]) + expect.find('^Automatically', mock_print.call_args[2][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, + mock_watched_job_matchers) + + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, {}) + expect.eq(4, mock_print.call_count) + expect.find('^Prioritized 0', mock_print.call_args[3][1]) + expect.find('^Skipping', mock_print.call_args[4][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, + mock_watched_job_matchers) + + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, + hauler_matchers={[HAUL_ITEM]=0}}}, {}) + expect.eq(6, mock_print.call_count) + expect.find('^Prioritized 0', mock_print.call_args[3][1]) + expect.find('^Skipping.*Item', mock_print.call_args[4][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, + 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}}}, {}) + expect.eq(2, mock_print.call_count) + expect.find('^Prioritized 0', mock_print.call_args[1][1]) + expect.find('^Automatically.*Item', mock_print.call_args[2][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, + 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}}}, {}) + expect.eq(4, mock_print.call_count) + expect.find('^Prioritized 0', mock_print.call_args[3][1]) + expect.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}}}, + mock_watched_job_matchers) + + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, + hauler_matchers={[HAUL_FOOD]=0}}}, {}) + expect.eq(6, mock_print.call_count) + expect.find('^Prioritized 0', mock_print.call_args[5][1]) + expect.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}}}, + 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}} + + p.remove_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, + hauler_matchers={[HAUL_FOOD]=0}}}, + {}) + expect.eq(1, mock_print.call_count) + expect.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}}}, + mock_watched_job_matchers) + + p.remove_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, + hauler_matchers={[HAUL_FOOD]=0}}}, + {}) + expect.eq(2, mock_print.call_count) + expect.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}}}, + mock_watched_job_matchers) +end + function test.eventful_hook_lifecycle() expect.nil_(mock_eventful_onUnload.prioritize) expect.nil_(mock_eventful_onJobInitiated.prioritize) @@ -147,7 +280,7 @@ function test.eventful_callbacks() expect.table_eq(expected, job) -- watched job - local expected = {job_type=DIG, flags={do_now=true}} + expected = {job_type=DIG, flags={do_now=true}} p.boost_and_watch({[DIG]={num_prioritized=0}}, {quiet=true}) p.on_new_job(job) expect.table_eq(expected, job) @@ -159,6 +292,32 @@ function test.eventful_callbacks() expect.nil_(mock_eventful_onJobInitiated.prioritize) end +function test.eventful_callbacks_labor() + mock_watched_job_matchers[STORE_ITEM_IN_STOCKPILE] = + {num_prioritized=0, hauler_matchers={[HAUL_FOOD]=0}} + + -- unwatched job + local job = {job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_BODY, + flags={}} + local expected_job = utils.clone(job) + local expected_watched_job_matchers = utils.clone(mock_watched_job_matchers) + p.on_new_job(job) + expect.table_eq(expected_job, job) + expect.table_eq(expected_watched_job_matchers, mock_watched_job_matchers) + + -- watched job + job = {job_type=STORE_ITEM_IN_STOCKPILE, item_subtype=HAUL_FOOD, + 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) +end + function test.print_current_jobs_empty() p.print_current_jobs({}) expect.eq(1, mock_print.call_count) @@ -170,20 +329,24 @@ function test.print_current_jobs_full() {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=REST}, flags={dead=true}}, + {job={job_type=STORE_ITEM_IN_STOCKPILE, + item_subtype=HAUL_FOOD, flags={}}, flags={}}} p.print_current_jobs({}) - expect.eq(3, mock_print.call_count) + expect.eq(4, mock_print.call_count) expect.eq('Current job counts by type:', 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+)') + 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[DIG]]='2', [df.job_type[EAT]]='1'}, result) + expect.table_eq({[df.job_type[DIG]]='2', [df.job_type[EAT]]='1', + [df.job_type[STORE_ITEM_IN_STOCKPILE]]='1'}, + result) end function test.print_current_jobs_filtered() @@ -217,13 +380,12 @@ function test.print_registry() goto continue end expect.ne('nil', tostring(out)) - expect.true_(out:find('^%u%l')) + expect.find('^%u%l', out) expect.ne('NONE', out) ::continue:: end end -local SUTURE = df.job_type['Suture'] function test.parse_commandline() expect.table_eq({help=true}, p.parse_commandline{'help'}) expect.table_eq({help=true}, p.parse_commandline{'-h'}) @@ -244,6 +406,11 @@ function test.parse_commandline() job_matchers={[SUTURE]={num_prioritized=0}}}, p.parse_commandline{'XSutureX', 'Suture'}) end) + expect.printerr_match('Ignoring unknown unit labor', + function() + expect.table_eq({action=p.status, job_matchers={}}, + p.parse_commandline{'-lXFoodX'}) + end) expect.table_eq({action=p.status, job_matchers={}, quiet=true}, p.parse_commandline{'-q'}) @@ -273,6 +440,18 @@ function test.parse_commandline() job_matchers={[SUTURE]={num_prioritized=0}}}, 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}}}, + 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}}}}, + p.parse_commandline{'-jlfood', 'StoreItemInStockpile'}) + expect.table_eq({action=p.print_registry, job_matchers={}}, p.parse_commandline{'-r'}) expect.table_eq({action=p.print_registry, job_matchers={}}, From aa490393fb88b7660b514a2e7f3d67e86c49b69c Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 24 Aug 2021 21:06:48 -0700 Subject: [PATCH 5/9] use new expect.str_find naming --- test/prioritize.lua | 62 ++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/test/prioritize.lua b/test/prioritize.lua index 07be06b30f..c4524fcd9e 100644 --- a/test/prioritize.lua +++ b/test/prioritize.lua @@ -30,12 +30,12 @@ local DIG, EAT, REST = df.job_type.Dig, df.job_type.Eat, df.job_type.Rest local STORE_ITEM_IN_STOCKPILE = df.job_type.StoreItemInStockpile local SUTURE = df.job_type.Suture -local HAUL_FOOD, HAUL_ITEM = df.unit_labor.HAUL_FOOD, df.unit_labor.HAUL_ITEM - local HAUL_STONE, HAUL_WOOD = df.unit_labor.HAUL_STONE, df.unit_labor.HAUL_WOOD local HAUL_BODY, HAUL_FOOD = df.unit_labor.HAUL_BODY, df.unit_labor.HAUL_FOOD -local HAUL_REFUSE, HAUL_ITEM = df.unit_labor.HAUL_REFUSE, df.unit_labor.HAUL_ITEM -local HAUL_FURNITURE, HAUL_ANIMALS = df.unit_labor.HAUL_FURNITURE, df.unit_labor.HAUL_ANIMALS +local HAUL_REFUSE = df.unit_labor.HAUL_REFUSE +local HAUL_ITEM = df.unit_labor.HAUL_ITEM +local HAUL_FURNITURE = df.unit_labor.HAUL_FURNITURE +local HAUL_ANIMALS = df.unit_labor.HAUL_ANIMALS function test.status() p.status() @@ -47,7 +47,7 @@ function test.status() p.status() expect.eq(3, mock_print.call_count) expect.eq('Automatically prioritized jobs:', mock_print.call_args[2][1]) - expect.find('Rest', mock_print.call_args[3][1]) + expect.str_find('Rest', mock_print.call_args[3][1]) end function test.status_labor() @@ -61,7 +61,7 @@ function test.status_labor() p.status() expect.eq(3, mock_print.call_count) expect.eq('Automatically prioritized jobs:', mock_print.call_args[2][1]) - expect.find('Stockpile.*Body', mock_print.call_args[3][1]) + expect.str_find('Stockpile.*Body', mock_print.call_args[3][1]) end function test.boost() @@ -102,14 +102,14 @@ end function test.boost_and_watch() p.boost_and_watch({[DIG]={num_prioritized=0}}, {}) expect.eq(2, mock_print.call_count) - expect.find('^Prioritized', mock_print.call_args[1][1]) - expect.find('^Automatically', mock_print.call_args[2][1]) + 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) p.boost_and_watch({[DIG]={num_prioritized=0}}, {}) expect.eq(4, mock_print.call_count) - expect.find('^Prioritized', mock_print.call_args[3][1]) - expect.find('^Skipping', mock_print.call_args[4][1]) + 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) end @@ -126,13 +126,13 @@ end function test.remove_watch() p.remove_watch({[DIG]={num_prioritized=0}}, {}) expect.eq(1, mock_print.call_count) - expect.find('Skipping unwatched', mock_print.call_args[1][1]) + 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}}, {}) expect.eq(2, mock_print.call_count) - expect.find('No longer', mock_print.call_args[2][1]) + expect.str_find('No longer', mock_print.call_args[2][1]) end function test.remove_watch_quiet() @@ -161,16 +161,16 @@ function test.boost_and_watch_labor() hauler_matchers={[HAUL_ITEM]=0}}}, {}) expect.eq(2, mock_print.call_count) - expect.find('^Prioritized 2', mock_print.call_args[1][1]) - expect.find('^Automatically', mock_print.call_args[2][1]) + 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}}}, mock_watched_job_matchers) p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, {}) expect.eq(4, mock_print.call_count) - expect.find('^Prioritized 1', mock_print.call_args[3][1]) - expect.find('^Automatically', mock_print.call_args[4][1]) + 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) end @@ -178,23 +178,23 @@ end function test.boost_and_watch_store_all_labors() p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, {}) expect.eq(2, mock_print.call_count) - expect.find('^Prioritized 0', mock_print.call_args[1][1]) - expect.find('^Automatically', mock_print.call_args[2][1]) + 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}}, mock_watched_job_matchers) p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0}}, {}) expect.eq(4, mock_print.call_count) - expect.find('^Prioritized 0', mock_print.call_args[3][1]) - expect.find('^Skipping', mock_print.call_args[4][1]) + 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}}, mock_watched_job_matchers) p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, hauler_matchers={[HAUL_ITEM]=0}}}, {}) expect.eq(6, mock_print.call_count) - expect.find('^Prioritized 0', mock_print.call_args[3][1]) - expect.find('^Skipping.*Item', mock_print.call_args[4][1]) + 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}}, mock_watched_job_matchers) end @@ -203,8 +203,8 @@ function test.boost_and_watch_store_add_labors() p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, hauler_matchers={[HAUL_ITEM]=0}}}, {}) expect.eq(2, mock_print.call_count) - expect.find('^Prioritized 0', mock_print.call_args[1][1]) - expect.find('^Automatically.*Item', mock_print.call_args[2][1]) + 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}}}, mock_watched_job_matchers) @@ -212,8 +212,8 @@ function test.boost_and_watch_store_add_labors() p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, hauler_matchers={[HAUL_FOOD]=0}}}, {}) expect.eq(4, mock_print.call_count) - expect.find('^Prioritized 0', mock_print.call_args[3][1]) - expect.find('^Automatically.*Food', mock_print.call_args[4][1]) + 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}}}, mock_watched_job_matchers) @@ -221,8 +221,8 @@ function test.boost_and_watch_store_add_labors() p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, hauler_matchers={[HAUL_FOOD]=0}}}, {}) expect.eq(6, mock_print.call_count) - expect.find('^Prioritized 0', mock_print.call_args[5][1]) - expect.find('^Skipping.*Food', mock_print.call_args[6][1]) + 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}}}, mock_watched_job_matchers) @@ -236,7 +236,7 @@ function test.remove_one_labor_from_all() hauler_matchers={[HAUL_FOOD]=0}}}, {}) expect.eq(1, mock_print.call_count) - expect.find('No longer.*Food', mock_print.call_args[1][1]) + 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, @@ -247,7 +247,7 @@ function test.remove_one_labor_from_all() hauler_matchers={[HAUL_FOOD]=0}}}, {}) expect.eq(2, mock_print.call_count) - expect.find('Skipping.*Food', mock_print.call_args[2][1]) + 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, @@ -380,7 +380,7 @@ function test.print_registry() goto continue end expect.ne('nil', tostring(out)) - expect.find('^%u%l', out) + expect.str_find('^%u%l', out) expect.ne('NONE', out) ::continue:: end From 0942596f3110b02f8cf5e224d972feef01db1078 Mon Sep 17 00:00:00 2001 From: myk002 Date: Tue, 24 Aug 2021 21:09:28 -0700 Subject: [PATCH 6/9] reword changelog entry --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 5da2f1ddf8..0c831bd3cf 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,7 +17,7 @@ that repo. - `autonick`: gives dwarves unique nicknames - `build-now`: instantly completes planned building constructions - `do-job-now`: makes a job involving current selection high priority -- `prioritize`: automatically boosts the priority of current and/or future jobs of the selected types +- `prioritize`: automatically boosts the priority of current and/or future jobs of specified types, such as hauling food or pulling levers - `reveal-adv-map`: exposes/hides all world map tiles in adventure mode ## Fixes From 7d9831289f4de78a7b6e2c95a01e75719cf963f6 Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 30 Aug 2021 00:13:25 -0700 Subject: [PATCH 7/9] add --reaction-name param; generalize code --- prioritize.lua | 407 +++++++++++++++++++++++++++++++------------- test/prioritize.lua | 7 +- 2 files changed, 294 insertions(+), 120 deletions(-) diff --git a/prioritize.lua b/prioritize.lua index baa5748241..460e55972b 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -19,18 +19,12 @@ one below to your ``onMapLoad.init`` file to ensure important job types are always completed promptly in your forts:: prioritize -a --haul-labor=Food StoreItemInStockpile + prioritize -a --reaction-name=TAN_A_HIDE CustomReaction prioritize -a PullLever CleanSelf RecoverWounded DumpItem prioritize -a DestroyBuilding RemoveConstruction prioritize -a StoreItemInVehicle StoreItemInBag StoreItemInBarrel prioritize -a SlaughterAnimal PrepareRawFish ExtractFromRawFish -Tanning hides is also a time-sensitive task, but it doesn't have a specific job -type associated with it. You can prioritize them via:: - - prioritize -a CustomReaction - -but this is likely to prioritize other, unrelated jobs as well. - It is important to automatically prioritize only the *most* important job types. If you add too many job types, or if there are simply too many jobs of those types in your fort, the other tasks in your fort can get ignored. This causes @@ -38,7 +32,7 @@ the same problem the ``prioritize`` script is designed to solve. The example commands above have been extensively playtested and are a good default set. If you need a bunch of jobs of a specific type prioritized *right now*, consider running ``prioritize`` without the ``-a`` parameter, which only affects -currently available jobs. For example:: +currently available (but unassigned) jobs. For example:: prioritize ConstructBuilding @@ -63,8 +57,8 @@ Examples: ``prioritize ConstructBuilding DestroyBuilding`` Prioritizes all current building construction and destruction jobs. -``prioritize -a --haul-labor=Food StoreItemInStockpile StoreItemInVehicle`` - Prioritizes all current and future food hauling and vehicle loading jobs. +``prioritize -a --haul-labor=Food,Body StoreItemInStockpile`` + Prioritizes all current and future food and corpse hauling jobs. Options: @@ -83,17 +77,24 @@ Options: hauling labor(s). Valid types are: "Stone", "Wood", "Body", "Food", "Refuse", "Item", "Furniture", and "Animals". If not specified, defaults to matching all StoreItemInStockpile jobs. +:``-n``, ``--reaction-name`` [,...]: + For CustomReaction jobs, restrict prioritization to the specified reaction + name(s). See the registry output (``-r``) for the full list. If not, + specified, defaults to matching all CustomReaction jobs. :``-q``, ``--quiet``: Suppress informational output (error messages are still printed). :``-r``, ``--registry``: - Print out the full list of valid job types. + Print out the full list of valid job types, hauling labors, and reaction + names. ]====] local argparse = require('argparse') local eventful = require('plugins.eventful') -- set of job types that we are watching. maps job_type (as a number) to --- {num_prioritized=number, hauler_matchers=map of type to num_prioritized} +-- {num_prioritized=number, +-- hauler_matchers=map of type to num_prioritized, +-- reaction_matchers=map of string to num_prioritized} -- this needs to be global so we don't lose player-set state when the script is -- reparsed. Also a getter function that can be mocked out by unit tests. g_watched_job_matchers = g_watched_job_matchers or {} @@ -102,26 +103,36 @@ function get_watched_job_matchers() return g_watched_job_matchers end eventful.enableEvent(eventful.eventType.UNLOAD, 1) eventful.enableEvent(eventful.eventType.JOB_INITIATED, 5) -local function make_job_matcher(unit_labors) - local matcher = {num_prioritized=0} - if unit_labors then - local ul_table = {} - for _,ul in ipairs(unit_labors) do - ul_table[ul] = 0 - end - matcher.hauler_matchers = ul_table +local function make_matcher_map(keys) + if not keys then return nil end + local t = {} + for _,key in ipairs(keys) do + t[key] = 0 end + return t +end + +local function make_job_matcher(unit_labors, reaction_names) + local matcher = {num_prioritized=0} + matcher.hauler_matchers = make_matcher_map(unit_labors) + matcher.reaction_matchers = make_matcher_map(reaction_names) return matcher end local function matches(job_matcher, job) if not job_matcher then return false end - if job_matcher.hauler_matchers then - return job_matcher.hauler_matchers[job.item_subtype] + if job_matcher.hauler_matchers and + not job_matcher.hauler_matchers[job.item_subtype] then + return false + end + if job_matcher.reaction_matchers and + not job_matcher.reaction_matchers[job.reaction_name] then + return false end return true end +-- returns true if the job is matched and it is not already high priority local function boost_job_if_matches(job, job_matchers) if matches(job_matchers[job.job_type], job) and not job.flags.do_now then job.flags.do_now = true @@ -139,6 +150,10 @@ local function on_new_job(job) local hms = jm.hauler_matchers hms[job.item_subtype] = hms[job.item_subtype] + 1 end + if jm.reaction_matchers then + local rms = jm.reaction_matchers + rms[job.reaction_name] = rms[job.reaction_name] + 1 + end end end @@ -149,6 +164,9 @@ end local function clear_watched_job_matchers() local watched_job_matchers = get_watched_job_matchers() + if has_elements(watched_job_matchers) then + print('map unloaded: cleared watched job types for prioritize script') + end for job_type in pairs(watched_job_matchers) do watched_job_matchers[job_type] = nil end @@ -166,12 +184,26 @@ local function update_handlers() end end -local function get_unit_labor_str(unit_labor) - if not unit_labor then - return '' +local function get_annotation_str(annotation) + if not annotation then + return nil end + return (' (%s)'):format(annotation) +end + +local function get_unit_labor_str(unit_labor) + if not unit_labor then return nil end local labor_str = df.unit_labor[unit_labor] - return (' (%s%s)'):format(labor_str:sub(6,6), labor_str:sub(7):lower()) + return ('%s%s'):format(labor_str:sub(6,6), labor_str:sub(7):lower()) +end + +local function get_unit_labor_annotation_str(unit_labor) + return get_annotation_str(get_unit_labor_str(unit_labor)) +end + +local function print_status_line(num_jobs, job_type, annotation) + annotation = annotation or '' + print(('%d\t%s%s'):format(num_jobs, df.job_type[job_type], annotation)) end local function status() @@ -184,20 +216,26 @@ local function status() end if v.hauler_matchers then for hk,hv in pairs(v.hauler_matchers) do - print(('%d\t%s%s') - :format(hv, df.job_type[k], get_unit_labor_str(hk))) + print_status_line(hv, k, get_unit_labor_annotation_str(hk)) + end + elseif v.reaction_matchers then + for rk,rv in pairs(v.reaction_matchers) do + print_status_line(rv, k, get_annotation_str(rk)) end else - print(('%d\t%s'):format(v.num_prioritized, df.job_type[k])) + print_status_line(v.num_prioritized, k) end end if first then print('Not automatically prioritizing any jobs.') end end --- encapsulate this in a function so unit tests can mock it out +-- encapsulate df state in functions so unit tests can mock them out function get_postings() return df.global.world.jobs.postings end +function get_reactions() + return df.global.world.raws.reactions.reactions +end local function for_all_live_postings(cb) for _,posting in ipairs(get_postings()) do @@ -220,70 +258,91 @@ local function boost(job_matchers, opts) end end -local function print_add_message(job_type, unit_labor) - local ul_str = get_unit_labor_str(unit_labor) +local function print_add_message(job_type, annotation) + annotation = annotation or '' print(('Automatically prioritizing future jobs of type: %s%s') - :format(df.job_type[job_type], ul_str)) + :format(df.job_type[job_type], annotation)) end -local function print_skip_add_message(job_type, unit_labor) - local ul_str = get_unit_labor_str(unit_labor) +local function print_skip_add_message(job_type, annotation) + annotation = annotation or '' print(('Skipping already-watched type: %s%s') - :format(df.job_type[job_type], ul_str)) + :format(df.job_type[job_type], annotation)) end -local function boost_and_watch(job_matchers, opts) - local quiet = opts.quiet - boost(job_matchers, opts) +local function boost_and_watch_special(job_type, job_matcher, + get_special_matchers_fn, + clear_special_matchers_fn, annotation_fn, + quiet) local watched_job_matchers = get_watched_job_matchers() - for job_type,job_matcher in pairs(job_matchers) do - local wjm = watched_job_matchers[job_type] - if job_type == df.job_type.StoreItemInStockpile then - if not wjm then - watched_job_matchers[job_type] = job_matcher + local watched_job_matcher = watched_job_matchers[job_type] + local special_matchers = get_special_matchers_fn(job_matcher) + local wspecial_matchers = watched_job_matcher and + get_special_matchers_fn(watched_job_matcher) or nil + if not watched_job_matcher then + -- no similar job already being watched; add the matcher verbatim + watched_job_matchers[job_type] = job_matcher + if not quiet then + if not special_matchers then + print_add_message(job_type) + else + for key in pairs(special_matchers) do + print_add_message(job_type, annotation_fn(key)) + end + end + end + elseif not wspecial_matchers and not special_matchers then + -- no special matchers for existing matcher or new matcher; nothing new + -- to watch + if not quiet then + print_skip_add_message(job_type) + end + elseif not wspecial_matchers then + -- existing matcher is broader than new matchers; nothing new to watch + for key in pairs(special_matchers) do + if not quiet then + print_skip_add_message(job_type, annotation_fn(key)) + end + end + elseif not special_matchers then + -- new matcher is broader than existing matcher; overwrite with new + if not quiet then + print_add_message(job_type) + end + clear_special_matchers_fn(watched_job_matcher) + else + -- diff new matcher into existing matcher + for key in pairs(special_matchers) do + if wspecial_matchers[key] then if not quiet then - local hms = job_matcher.hauler_matchers - if not hms then - print_add_message(job_type) - else - for ul in pairs(hms) do - print_add_message(job_type, ul) - end - end + print_skip_add_message(job_type, annotation_fn(key)) end else - if not wjm.hauler_matchers - and not job_matcher.hauler_matchers then - if not quiet then - print_skip_add_message(job_type) - end - elseif not wjm.hauler_matchers then - for ul in pairs(job_matcher.hauler_matchers) do - if not quiet then - print_skip_add_message(job_type, ul) - end - end - elseif not job_matcher.hauler_matchers then - if not quiet then - print_add_message(job_type) - end - wjm.hauler_matchers = nil - else - for ul in pairs(job_matcher.hauler_matchers) do - if wjm.hauler_matchers[ul] then - if not quiet then - print_skip_add_message(job_type, ul) - end - else - wjm.hauler_matchers[ul] = 0 - if not quiet then - print_add_message(job_type, ul) - end - end - end + wspecial_matchers[key] = 0 + if not quiet then + print_add_message(job_type, annotation_fn(key)) end end - elseif wjm then + end + end +end + +local function boost_and_watch(job_matchers, opts) + local quiet = opts.quiet + boost(job_matchers, opts) + local watched_job_matchers = get_watched_job_matchers() + for job_type,job_matcher in pairs(job_matchers) do + if job_type == df.job_type.StoreItemInStockpile then + boost_and_watch_special(job_type, job_matcher, + function(jm) return jm.hauler_matchers end, + function(jm) jm.hauler_matchers = nil end, + get_unit_labor_annotation_str, quiet) + elseif job_type == df.job_type.CustomReaction then + boost_and_watch_special(job_type, job_matcher, + function(jm) return jm.reaction_matchers end, + function(jm) jm.reaction_matchers = nil end, + get_annotation_str, quiet) + elseif watched_job_matchers[job_type] then if not quiet then print_skip_add_message(job_type) end @@ -297,16 +356,49 @@ local function boost_and_watch(job_matchers, opts) update_handlers() end -local function print_del_message(job_type, unit_labor) - local ul_str = get_unit_labor_str(unit_labor) +local function print_del_message(job_type, annotation) + annotation = annotation or '' print(('No longer automatically prioritizing jobs of type: %s%s') - :format(df.job_type[job_type], ul_str)) + :format(df.job_type[job_type], annotation)) end -local function print_skip_del_message(job_type, unit_labor) - local ul_str = get_unit_labor_str(unit_labor) +local function print_skip_del_message(job_type, annotation) + annotation = annotation or '' print(('Skipping unwatched type: %s%s') - :format(df.job_type[job_type], ul_str)) + :format(df.job_type[job_type], annotation)) +end + +local function remove_watch_special(job_type, job_matcher, + get_special_matchers_fn, + fill_special_matcher_fn, + annotation_fn, quiet) + local watched_job_matchers = get_watched_job_matchers() + local watched_job_matcher = watched_job_matchers[job_type] + local special_matchers = get_special_matchers_fn(job_matcher) + local wspecial_matchers = watched_job_matcher and + get_special_matchers_fn(watched_job_matcher) or nil + if not wspecial_matchers then + -- if we're removing specific subtypes from an all-inclusive spec, then + -- add all the possible individual subtypes before we remove the ones + -- that were specified. + wspecial_matchers = fill_special_matcher_fn(watched_job_matcher) + end + -- remove the specified subtypes + for key in pairs(special_matchers) do + if wspecial_matchers[key] then + if not quiet then + print_del_message(job_type, annotation_fn(key)) + end + wspecial_matchers[key] = nil + else + if not quiet then + print_skip_del_message(job_type, annotation_fn(key)) + end + end + end + if not has_elements(wspecial_matchers) then + watched_job_matchers[job_type] = nil + end end local function remove_watch(job_matchers, opts) @@ -315,36 +407,45 @@ local function remove_watch(job_matchers, opts) for job_type,job_matcher in pairs(job_matchers) do local wjm = watched_job_matchers[job_type] if not wjm then + -- job type not being watched; nothing to remove if not quiet then print_skip_del_message(job_type) end - elseif not job_matcher.hauler_matchers then + elseif not job_matcher.hauler_matchers + and not job_matcher.reaction_matchers then + -- no special matchers in job_matchers; stop watching all watched_job_matchers[job_type] = nil if not quiet then print_del_message(job_type) end - else - if not wjm.hauler_matchers then - wjm.hauler_matchers = {} - for id,name in ipairs(df.unit_labor) do - if name:startswith('HAUL_') - and id <= df.unit_labor.HAUL_ANIMALS then - wjm.hauler_matchers[id] = 0 - end - end - end - for ul in pairs(job_matcher.hauler_matchers) do - if wjm.hauler_matchers[ul] then - if not quiet then - print_del_message(job_type, ul) + elseif job_type == df.job_type.StoreItemInStockpile then + remove_watch_special(job_type, job_matcher, + function(jm) return jm.hauler_matchers end, + function(jm) + jm.hauler_matchers = {} + for id,name in ipairs(df.unit_labor) do + if name:startswith('HAUL_') + and id <= df.unit_labor.HAUL_ANIMALS then + jm.hauler_matchers[id] = 0 + end end - wjm.hauler_matchers[ul] = nil - else - if not quiet then - print_skip_del_message(job_type, ul) + return jm.hauler_matchers + end, + get_unit_labor_annotation_str, quiet) + elseif job_type == df.job_type.CustomReaction then + remove_watch_special(job_type, job_matcher, + function(jm) return jm.reaction_matchers end, + function(jm) + jm.reaction_matchers = {} + for _,v in ipairs(get_reactions()) + do + jm.reaction_matchers[v.code] = 0 end - end - end + return jm.reaction_matchers + end, + get_annotation_str, quiet) + else + error('unhandled case') -- should not ever happen end end update_handlers() @@ -353,10 +454,15 @@ end local function get_job_type_str(job) local job_type = job.job_type local job_type_str = df.job_type[job_type] - if job_type ~= df.job_type.StoreItemInStockpile then + if job_type == df.job_type.StoreItemInStockpile then + return ('%s%s'):format(job_type_str, + get_unit_labor_annotation_str(job.item_subtype)) + elseif job_type == df.job_type.CustomReaction then + return ('%s%s'):format(job_type_str, + get_annotation_str(job.reaction_name)) + else return job_type_str end - return ('%s%s'):format(job_type_str, get_unit_labor_str(job.item_subtype)) end local function print_current_jobs(job_matchers, opts) @@ -383,17 +489,54 @@ local function print_current_jobs(job_matchers, opts) if first then print('No current jobs.') end end +local function print_registry_section(header, t) + print('\n' .. header .. ':') + table.sort(t) + for _,v in ipairs(t) do + print(' ' .. v) + end +end + local function print_registry() - print('Valid job types:') - for k,v in ipairs(df.job_type) do - if v and df.job_type[v] and v:find('^%u%l') then - print(' ' .. v) + local t = {} + for _,v in ipairs(df.job_type) do + -- don't clutter the output with esoteric or non-prioritizable job types + if v and df.job_type[v] and v:find('^%u%l') + and not v:find('^StrangeMood') then + table.insert(t, v) + end + end + print_registry_section('Job types', t) + + t = {} + for i,v in ipairs(df.unit_labor) do + if v:startswith('HAUL_') then + table.insert(t, get_unit_labor_str(i)) + end + if i >= df.unit_labor.HAUL_ANIMALS then + -- don't include irrelevant HAUL_TRADE or HAUL_WATER labors + break + end + end + print_registry_section('Hauling labors (for StoreItemInStockpile jobs)', t) + + t = {} + for _,v in ipairs(get_reactions()) do + -- don't clutter the output with generated reactions (like instrument + -- piece creation reactions). space characters seem to be a good + -- discriminator. + if not v.code:find(' ') then + table.insert(t, v.code) end end + if not has_elements(t) then + t = {'Load a game to see reactions'} + end + print_registry_section('Reaction names (for CustomReaction jobs)', t) end local function parse_commandline(args) - local opts, action, unit_labors = {}, status, nil + local opts, action, unit_labors, reaction_names = {}, status, nil, nil local positionals = argparse.processArgsGetopt(args, { {'a', 'add', handler=function() action = boost_and_watch end}, {'d', 'delete', handler=function() action = remove_watch end}, @@ -401,6 +544,9 @@ local function parse_commandline(args) {'j', 'jobs', handler=function() action = print_current_jobs end}, {'l', 'haul-labor', hasArg=true, handler=function(arg) unit_labors = argparse.stringList(arg) end}, + {'n', 'reaction-name', hasArg=true, + handler=function(arg) + reaction_names = argparse.stringList(arg) end}, {'q', 'quiet', handler=function() opts.quiet = true end}, {'r', 'registry', handler=function() action = print_registry end}, }) @@ -425,6 +571,29 @@ local function parse_commandline(args) unit_labors = ul_ids end + -- validate any specified reaction names + if reaction_names then + local rns = nil + for _,v in ipairs(reaction_names) do + local found = false + for _,r in ipairs(get_reactions()) do + if r.code == v then + found = true + break + end + end + if not found then + dfhack.printerr(('Ignoring unknown reaction name: "%s". Run' .. + ' "prioritize -r" for a list of valid reaction names.') + :format(v)) + else + rns = rns or {} + table.insert(rns, v) + end + end + reaction_names = rns + end + -- validate the specified job types and create matchers local job_matchers = {} for _,job_type_name in ipairs(positionals) do @@ -435,8 +604,10 @@ local function parse_commandline(args) :format(job_type_name)) else local job_matcher = make_job_matcher( - job_type == df.job_type.StoreItemInStockpile - and unit_labors or nil) + job_type == df.job_type.StoreItemInStockpile and + unit_labors or nil, + job_type == df.job_type.CustomReaction and + reaction_names or nil) job_matchers[job_type] = job_matcher end end diff --git a/test/prioritize.lua b/test/prioritize.lua index c4524fcd9e..d12666d888 100644 --- a/test/prioritize.lua +++ b/test/prioritize.lua @@ -12,13 +12,16 @@ local function get_mock_watched_job_matchers() end local mock_postings = {} local function get_mock_postings() return mock_postings end +local mock_reactions = {} +local function get_mock_reactions() return mock_reactions end local function test_wrapper(test_fn) mock.patch({{eventful, 'onUnload', mock_eventful_onUnload}, {eventful, 'onJobInitiated', mock_eventful_onJobInitiated}, {prioritize, 'print', mock_print}, {prioritize, 'get_watched_job_matchers', get_mock_watched_job_matchers}, - {prioritize, 'get_postings', get_mock_postings}}, + {prioritize, 'get_postings', get_mock_postings}, + {prioritize, 'get_reactions', get_mock_reactions}}, test_fn) mock_eventful_onUnload, mock_eventful_onJobInitiated = {}, {} mock_print = mock.func() @@ -376,7 +379,7 @@ function test.print_registry() for i,v in ipairs(mock_print.call_args) do local out = v[1]:trim() if i == 1 then - expect.eq('Valid job types:', out) + expect.eq('Job types:', out) goto continue end expect.ne('nil', tostring(out)) From 9c7b3dfdaf7ba3588099d75592d13276cc3ad64e Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 30 Aug 2021 21:35:31 -0700 Subject: [PATCH 8/9] add unit tests for reacton matching; update docs --- prioritize.lua | 46 ++++++++--------- test/prioritize.lua | 121 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 121 insertions(+), 46 deletions(-) diff --git a/prioritize.lua b/prioritize.lua index 460e55972b..6bdcb28162 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -12,25 +12,24 @@ This will force them to get assigned and completed as soon as possible. This script can also continue to monitor new jobs and automatically boost the priority of jobs of the specified types. -This is useful for ensuring important (but low-priority -- according to DF) -tasks don't get indefinitely ignored in busy forts. The list of monitored job -types is cleared whenever you load a new map, so you can add a section like the -one below to your ``onMapLoad.init`` file to ensure important job types are -always completed promptly in your forts:: +This is useful for ensuring important (but low-priority -- according to DF) jobs +don't get indefinitely ignored in busy forts. The list of monitored job types is +cleared whenever you unload a map, so you can add a section like the one below +to your ``onMapLoad.init`` file to ensure important and time-sensitive job types +are always completed promptly in your forts:: - prioritize -a --haul-labor=Food StoreItemInStockpile + prioritize -a --haul-labor=Food,Body StoreItemInStockpile prioritize -a --reaction-name=TAN_A_HIDE CustomReaction - prioritize -a PullLever CleanSelf RecoverWounded DumpItem - prioritize -a DestroyBuilding RemoveConstruction - prioritize -a StoreItemInVehicle StoreItemInBag StoreItemInBarrel - prioritize -a SlaughterAnimal PrepareRawFish ExtractFromRawFish + prioritize -a PrepareRawFish ExtractFromRawFish CleanSelf It is important to automatically prioritize only the *most* important job types. If you add too many job types, or if there are simply too many jobs of those types in your fort, the other tasks in your fort can get ignored. This causes -the same problem the ``prioritize`` script is designed to solve. The example -commands above have been extensively playtested and are a good default set. If -you need a bunch of jobs of a specific type prioritized *right now*, consider +the same problem the ``prioritize`` script is designed to solve. See the +`onMapLoad-dreamfort-init` file in the ``hack/examples/init`` folder for a more +complete, playtested set of job types to automatically prioritize. + +If you need a bunch of jobs of a specific type prioritized *right now*, consider running ``prioritize`` without the ``-a`` parameter, which only affects currently available (but unassigned) jobs. For example:: @@ -38,7 +37,7 @@ currently available (but unassigned) jobs. For example:: Also see the ``do-job-now`` `tweak` for adding a hotkey to the jobs screen that can toggle the priority of specific individual jobs and the `do-job-now` -script, which sets the priority of jobs related to the current selected +script, which boosts the priority of current jobs related to the selected job/unit/item/building/order. Usage:: @@ -73,13 +72,13 @@ Options: for discovering the types of the jobs that you can prioritize right now. If any job types are specified, only returns the count for those types. :``-l``, ``--haul-labor`` [,...]: - For StoreItemInStockpile jobs, restrict prioritization to the specified - hauling labor(s). Valid types are: "Stone", "Wood", "Body", "Food", - "Refuse", "Item", "Furniture", and "Animals". If not specified, defaults to - matching all StoreItemInStockpile jobs. + For StoreItemInStockpile jobs, match only the specified hauling labor(s). + Valid strings are: "Stone", "Wood", "Body", "Food", "Refuse", + "Item", "Furniture", and "Animals". If not specified, defaults to matching + all StoreItemInStockpile jobs. :``-n``, ``--reaction-name`` [,...]: - For CustomReaction jobs, restrict prioritization to the specified reaction - name(s). See the registry output (``-r``) for the full list. If not, + For CustomReaction jobs, match only the specified reaction name(s). See the + registry output (``-r``) for the full list of reaction names. If not specified, defaults to matching all CustomReaction jobs. :``-q``, ``--quiet``: Suppress informational output (error messages are still printed). @@ -185,14 +184,10 @@ local function update_handlers() end local function get_annotation_str(annotation) - if not annotation then - return nil - end return (' (%s)'):format(annotation) end local function get_unit_labor_str(unit_labor) - if not unit_labor then return nil end local labor_str = df.unit_labor[unit_labor] return ('%s%s'):format(labor_str:sub(6,6), labor_str:sub(7):lower()) end @@ -437,8 +432,7 @@ local function remove_watch(job_matchers, opts) function(jm) return jm.reaction_matchers end, function(jm) jm.reaction_matchers = {} - for _,v in ipairs(get_reactions()) - do + for _,v in ipairs(get_reactions()) do jm.reaction_matchers[v.code] = 0 end return jm.reaction_matchers diff --git a/test/prioritize.lua b/test/prioritize.lua index d12666d888..68ea795538 100644 --- a/test/prioritize.lua +++ b/test/prioritize.lua @@ -12,7 +12,7 @@ local function get_mock_watched_job_matchers() end local mock_postings = {} local function get_mock_postings() return mock_postings end -local mock_reactions = {} +local mock_reactions = {{code='TAN_A_HIDE'}} local function get_mock_reactions() return mock_reactions end local function test_wrapper(test_fn) mock.patch({{eventful, 'onUnload', mock_eventful_onUnload}, @@ -26,11 +26,13 @@ local function test_wrapper(test_fn) mock_eventful_onUnload, mock_eventful_onJobInitiated = {}, {} mock_print = mock.func() mock_watched_job_matchers, mock_postings = {}, {} + mock_reactions = {{code='TAN_A_HIDE'}} end config.wrapper = test_wrapper local DIG, EAT, REST = df.job_type.Dig, df.job_type.Eat, df.job_type.Rest local STORE_ITEM_IN_STOCKPILE = df.job_type.StoreItemInStockpile +local CUSTOM_REACTION = df.job_type.CustomReaction local SUTURE = df.job_type.Suture local HAUL_STONE, HAUL_WOOD = df.unit_labor.HAUL_STONE, df.unit_labor.HAUL_WOOD @@ -54,17 +56,21 @@ function test.status() end function test.status_labor() - 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[STORE_ITEM_IN_STOCKPILE] = {num_prioritized=5, hauler_matchers={[HAUL_BODY]=2}} p.status() - expect.eq(3, mock_print.call_count) - expect.eq('Automatically prioritized jobs:', mock_print.call_args[2][1]) - expect.str_find('Stockpile.*Body', mock_print.call_args[3][1]) + expect.eq(2, mock_print.call_count) + expect.eq('Automatically prioritized jobs:', mock_print.call_args[1][1]) + expect.str_find('Stockpile.*Body', mock_print.call_args[2][1]) +end + +function test.status_reaction() + mock_watched_job_matchers[CUSTOM_REACTION] = + {num_prioritized=5, reaction_matchers={TAN_A_HIDE=2}} + p.status() + expect.eq(2, mock_print.call_count) + expect.eq('Automatically prioritized jobs:', mock_print.call_args[1][1]) + expect.str_find('Custom.*TAN_A_HIDE', mock_print.call_args[2][1]) end function test.boost() @@ -231,6 +237,17 @@ function test.boost_and_watch_store_add_labors() 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}}}, {}) + 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}}}, + 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}} @@ -248,7 +265,7 @@ function test.remove_one_labor_from_all() p.remove_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, 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, @@ -258,6 +275,20 @@ function test.remove_one_labor_from_all() mock_watched_job_matchers) end +function test.remove_all_reactions_from_all() + mock_watched_job_matchers = {[CUSTOM_REACTION]={num_prioritized=5}} + + -- 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}}}, + {}) + expect.eq(1, mock_print.call_count) + expect.str_find('No longer.*TAN_A_HIDE', mock_print.call_args[1][1]) + expect.table_eq({}, mock_watched_job_matchers) +end + function test.eventful_hook_lifecycle() expect.nil_(mock_eventful_onUnload.prioritize) expect.nil_(mock_eventful_onJobInitiated.prioritize) @@ -321,6 +352,31 @@ function test.eventful_callbacks_labor() expect.table_eq(expected_watched_job_matchers, mock_watched_job_matchers) end +function test.eventful_callbacks_reaction() + mock_watched_job_matchers[CUSTOM_REACTION] = + {num_prioritized=0, reaction_matchers={TAN_A_HIDE=0}} + + -- unwatched job + local job = {job_type=CUSTOM_REACTION, reaction_name='STEEL_MAKING', + flags={}} + local expected_job = utils.clone(job) + local expected_watched_job_matchers = utils.clone(mock_watched_job_matchers) + p.on_new_job(job) + expect.table_eq(expected_job, job) + expect.table_eq(expected_watched_job_matchers, mock_watched_job_matchers) + + -- watched job + 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) +end + function test.print_current_jobs_empty() p.print_current_jobs({}) expect.eq(1, mock_print.call_count) @@ -334,9 +390,11 @@ function test.print_current_jobs_full() {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={}}} + item_subtype=HAUL_FOOD, flags={}}, flags={}}, + {job={job_type=CUSTOM_REACTION, + reaction_name='TAN_A_HIDE', flags={}}, flags={}}} p.print_current_jobs({}) - expect.eq(4, mock_print.call_count) + expect.eq(5, mock_print.call_count) expect.eq('Current job counts by type:', mock_print.call_args[1][1]) local result = {} for i,v in ipairs(mock_print.call_args) do @@ -348,7 +406,8 @@ function test.print_current_jobs_full() ::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[STORE_ITEM_IN_STOCKPILE]]='1', + [df.job_type[CUSTOM_REACTION]]='1'}, result) end @@ -375,20 +434,22 @@ end function test.print_registry() p.print_registry() - expect.lt(0, mock_print.call_count) + expect.lt(1, mock_print.call_count) for i,v in ipairs(mock_print.call_args) do local out = v[1]:trim() - if i == 1 then - expect.eq('Job types:', out) - goto continue - end expect.ne('nil', tostring(out)) - expect.str_find('^%u%l', out) expect.ne('NONE', out) - ::continue:: end end +function test.print_registry_no_raws() + mock_reactions = {} + p.print_registry() + expect.lt(1, mock_print.call_count) + expect.eq('Load a game to see reactions', + mock_print.call_args[#mock_print.call_args][1]:trim()) +end + function test.parse_commandline() expect.table_eq({help=true}, p.parse_commandline{'help'}) expect.table_eq({help=true}, p.parse_commandline{'-h'}) @@ -414,6 +475,11 @@ function test.parse_commandline() expect.table_eq({action=p.status, job_matchers={}}, p.parse_commandline{'-lXFoodX'}) end) + expect.printerr_match('Ignoring unknown reaction name', + function() + expect.table_eq({action=p.status, job_matchers={}}, + p.parse_commandline{'-nXTAN_A_HIDEX'}) + end) expect.table_eq({action=p.status, job_matchers={}, quiet=true}, p.parse_commandline{'-q'}) @@ -455,6 +521,21 @@ function test.parse_commandline() hauler_matchers={[HAUL_FOOD]=0}}}}, 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}}}, + p.parse_commandline{'-nTAN_A_HIDE', 'Suture'}) + expect.table_eq({action=p.boost, + job_matchers={[STORE_ITEM_IN_STOCKPILE]= + {num_prioritized=0}}}, + 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}}}}, + p.parse_commandline{'-nTAN_A_HIDE', 'CustomReaction'}) + expect.table_eq({action=p.print_registry, job_matchers={}}, p.parse_commandline{'-r'}) expect.table_eq({action=p.print_registry, job_matchers={}}, From 0d61b19c182cdd2dd010e5686f2deff6a385572a Mon Sep 17 00:00:00 2001 From: myk002 Date: Mon, 30 Aug 2021 23:14:37 -0700 Subject: [PATCH 9/9] update changelog --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 0c831bd3cf..0b38661726 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,7 +17,7 @@ that repo. - `autonick`: gives dwarves unique nicknames - `build-now`: instantly completes planned building constructions - `do-job-now`: makes a job involving current selection high priority -- `prioritize`: automatically boosts the priority of current and/or future jobs of specified types, such as hauling food or pulling levers +- `prioritize`: automatically boosts the priority of current and/or future jobs of specified types, such as hauling food, tanning hides, or pulling levers - `reveal-adv-map`: exposes/hides all world map tiles in adventure mode ## Fixes