diff --git a/changelog.txt b/changelog.txt index 5da2f1ddf8..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 the selected types +- `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 diff --git a/prioritize.lua b/prioritize.lua index 1d3a5a69e0..6bdcb28162 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -6,22 +6,38 @@ 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) -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:: +This script can also continue to monitor new jobs and automatically boost the +priority of jobs of the specified types. - prioritize -a StoreItemInVehicle PullLever DestroyBuilding RemoveConstruction RecoverWounded FillPond DumpItem SlaughterAnimal +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,Body StoreItemInStockpile + prioritize -a --reaction-name=TAN_A_HIDE CustomReaction + 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. 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:: + + 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 selected +script, which boosts the priority of current jobs related to the selected job/unit/item/building/order. Usage:: @@ -40,41 +56,84 @@ Examples: ``prioritize ConstructBuilding DestroyBuilding`` Prioritizes all current building construction and destruction jobs. -``prioritize -a StoreItemInVehicle`` - Prioritizes all current and future vehicle loading jobs. +``prioritize -a --haul-labor=Food,Body StoreItemInStockpile`` + Prioritizes all current and future food and corpse hauling jobs. 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, 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, 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). :``-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. 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 (as a number) to +-- {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 {} +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_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 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 return true end @@ -82,9 +141,18 @@ 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 + if jm.reaction_matchers then + local rms = jm.reaction_matchers + rms[job.reaction_name] = rms[job.reaction_name] + 1 + end end end @@ -93,42 +161,76 @@ 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() + 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 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 get_annotation_str(annotation) + return (' (%s)'):format(annotation) +end + +local function get_unit_labor_str(unit_labor) + 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 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() 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_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_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 @@ -138,63 +240,233 @@ 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 print_add_message(job_type, annotation) + annotation = annotation or '' + print(('Automatically prioritizing future jobs of type: %s%s') + :format(df.job_type[job_type], annotation)) +end + +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], annotation)) +end + +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() + 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 + print_skip_add_message(job_type, annotation_fn(key)) + end + else + wspecial_matchers[key] = 0 + if not quiet then + print_add_message(job_type, annotation_fn(key)) + end + end + 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('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, annotation) + annotation = annotation or '' + print(('No longer automatically prioritizing jobs of type: %s%s') + :format(df.job_type[job_type], annotation)) +end + +local function print_skip_del_message(job_type, annotation) + annotation = annotation or '' + print(('Skipping unwatched type: %s%s') + :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('Skipping unwatched type: ' .. df.job_type[job_type]) + print_del_message(job_type, annotation_fn(key)) end + wspecial_matchers[key] = nil else - watched_job_types[job_type] = nil if not quiet then - print('No longer automatically prioritizing jobs of type: ' .. - df.job_type[job_type]) + 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) + 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 + -- 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 + 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 + 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 + 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 + return jm.reaction_matchers + end, + get_annotation_str, quiet) + else + error('unhandled case') -- should not ever happen + 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 ('%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 +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,27 +478,69 @@ 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 +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 = {}, status + 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}, {'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}, + {'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}, }) @@ -234,19 +548,66 @@ local function parse_commandline(args) 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 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 + ul_ids = ul_ids or {} + table.insert(ul_ids, df.unit_labor[ulabor]) + end + end + 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 + 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 + unit_labors or nil, + job_type == df.job_type.CustomReaction and + reaction_names 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 +620,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, diff --git a/test/prioritize.lua b/test/prioritize.lua index 679d88d981..68ea795538 100644 --- a/test/prioritize.lua +++ b/test/prioritize.lua @@ -1,29 +1,46 @@ 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 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 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}, {eventful, 'onJobInitiated', mock_eventful_onJobInitiated}, {prioritize, 'print', mock_print}, - {prioritize, 'get_watched_job_types', - get_mock_watched_job_types}, - {prioritize, 'get_postings', get_mock_postings}}, + {prioritize, 'get_watched_job_matchers', + get_mock_watched_job_matchers}, + {prioritize, 'get_postings', get_mock_postings}, + {prioritize, 'get_reactions', get_mock_reactions}}, test_fn) mock_eventful_onUnload, mock_eventful_onJobInitiated = {}, {} mock_print = mock.func() - mock_watched_job_types, mock_postings = {}, {} + 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 +local HAUL_BODY, HAUL_FOOD = df.unit_labor.HAUL_BODY, df.unit_labor.HAUL_FOOD +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() @@ -31,11 +48,29 @@ 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]) - expect.true_(mock_print.call_args[3][1]:find('Rest')) + expect.str_find('Rest', mock_print.call_args[3][1]) +end + +function test.status_labor() + mock_watched_job_matchers[STORE_ITEM_IN_STOCKPILE] = + {num_prioritized=5, hauler_matchers={[HAUL_BODY]=2}} + p.status() + 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() @@ -50,7 +85,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 +103,204 @@ 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.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]=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.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 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.str_find('Skipping unwatched', mock_print.call_args[1][1]) + 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')) + expect.str_find('No longer', mock_print.call_args[2][1]) 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.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.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.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 + +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.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.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.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 + +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.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) + + p.boost_and_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, + hauler_matchers={[HAUL_FOOD]=0}}}, {}) + expect.eq(4, mock_print.call_count) + expect.str_find('^Prioritized 0', mock_print.call_args[3][1]) + expect.str_find('^Automatically.*Food', mock_print.call_args[4][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, + hauler_matchers={[HAUL_ITEM]=0, [HAUL_FOOD]=0}}}, + 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.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) +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}} + + p.remove_watch({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=0, + hauler_matchers={[HAUL_FOOD]=0}}}, + {}) + expect.eq(1, mock_print.call_count) + expect.str_find('No longer.*Food', mock_print.call_args[1][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=5, + hauler_matchers={[HAUL_STONE]=0, [HAUL_WOOD]=0, [HAUL_BODY]=0, + [HAUL_REFUSE]=0, [HAUL_ITEM]=0, [HAUL_FURNITURE]=0, + [HAUL_ANIMALS]=0}}}, + 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.str_find('Skipping.*Food', mock_print.call_args[2][1]) + expect.table_eq({[STORE_ITEM_IN_STOCKPILE]={num_prioritized=5, + hauler_matchers={[HAUL_STONE]=0, [HAUL_WOOD]=0, [HAUL_BODY]=0, + [HAUL_REFUSE]=0, [HAUL_ITEM]=0, [HAUL_FURNITURE]=0, + [HAUL_ANIMALS]=0}}}, + 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) - 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) @@ -145,18 +314,69 @@ function test.eventful_callbacks() expect.table_eq(expected, job) -- watched job - local expected = {job_type=DIG, flags={do_now=true}} - p.boost_and_watch({[DIG]=true}, 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) -- 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 +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.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) @@ -168,20 +388,27 @@ 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={}}, + {job={job_type=CUSTOM_REACTION, + reaction_name='TAN_A_HIDE', flags={}}, flags={}}} p.print_current_jobs({}) - expect.eq(3, 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 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', + [df.job_type[CUSTOM_REACTION]]='1'}, + result) end function test.print_current_jobs_filtered() @@ -207,70 +434,110 @@ 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('Valid job types:', out) - goto continue - end expect.ne('nil', tostring(out)) - expect.true_(out:find('^%u%l')) 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'}) 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.printerr_match('Ignoring unknown unit labor', + function() + 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_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.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.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_types={}}, + expect.table_eq({action=p.print_registry, job_matchers={}}, p.parse_commandline{'--registry'}) end