From fba07a49ac261681ebee45bc032a7a973759e37f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 1 Jul 2024 16:54:08 -0700 Subject: [PATCH] boost all jobs, not just unclaimed ones also list all jobs for --jobs and autofix bad keys when listing status --- changelog.txt | 3 + docs/prioritize.rst | 42 +++++++------- prioritize.lua | 136 +++++++++++++++++++++++++------------------- 3 files changed, 98 insertions(+), 83 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4c97444b35..277530ba9d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: - `gui/notify`: the notification panel no longer responds to the Enter key so Enter key is passed through to the vanilla UI - `clear-smoke`: properly tag smoke flows for garbage collection to avoid memory leak - `warn-stranded`: don't warn for babies carried by mothers who happen to be gathering fruit from trees +- `prioritize`: also boost priority of already-claimed jobs when boosting priority of a job type so those jobs are not interrupted ## Misc Improvements - `item`: option for ignoring uncollected spider webs when you search for "silk" @@ -89,6 +90,8 @@ Template for new versions: - `gui/control-panel`: highlight preferences that have been changed from the defaults - `gui/quickfort`: buildings can now be constructed in a "high priority" state, giving them first dibs on `buildingplan` materials and setting their construction jobs to the highest priority - `prioritize`: add ``ButcherAnimal`` to the default prioritization list (``SlaughterAnimal`` was already there, but ``ButcherAnimal`` -- which is different -- was missing) +- `prioritize`: list both unclaimed and total counts for current jobs when the --jobs option is specified +- `prioritize`: boost performance of script by not tracking number of times a job type was prioritized - `gui/unit-syndromes`: make werecreature syndromes easier to search for ## Removed diff --git a/docs/prioritize.rst b/docs/prioritize.rst index 50f18df489..acc1d55f65 100644 --- a/docs/prioritize.rst +++ b/docs/prioritize.rst @@ -8,20 +8,20 @@ prioritize This tool encourages specified types of jobs to get assigned and completed as soon as possible. Finally, you can be sure your food will be hauled before rotting, your hides will be tanned before going bad, and the corpses of your -enemies will be cleared from your entranceway expediently. +enemies will be cleared expediently from your entranceway. You can prioritize a bunch of active jobs that you need done *right now*, or you -can mark certain job types as high priority, and ``prioritize`` will watch for +can register types of jobs as high priority, and ``prioritize`` will watch for and boost the priority of those types of jobs as they are created. This is especially useful for ensuring important (but low-priority -- according to DF) jobs don't get ignored indefinitely in busy forts. -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 that ``prioritize`` is designed to solve. The script provides -a good default set of job types to prioritize that have been suggested and -playtested by the DF community. +When registering job types, choose 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 that ``prioritize`` is designed to solve. The script provides a good +default set of job types to prioritize that have been suggested and playtested +by the DF community. Usage ----- @@ -29,17 +29,13 @@ Usage :: enable prioritize - disable prioritize prioritize [] [defaults| ...] Examples -------- ``prioritize`` - Print out which job types are being automatically prioritized and how many - jobs of each type we have prioritized since we started watching them. The - counts are saved with your game, so they will be accurate even if the game - has been saved and reloaded since ``prioritize`` was started. + Print out which job types are being automatically prioritized. ``enable prioritize``, ``prioritize -a defaults`` Watch for and prioritize the default set of job types that the community has suggested and playtested (see below for details). @@ -60,9 +56,9 @@ Options ``-d``, ``--delete`` Stop automatically prioritizing new jobs of the specified job types. ``-j``, ``--jobs`` - 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 jobs of those types are listed. + Print out how many current 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 jobs of those types are listed. ``-l``, ``--haul-labor [,...]`` For StoreItemInStockpile jobs, match only the specified hauling labor(s). Valid ``labor`` strings are: "Stone", "Wood", "Body", "Food", "Refuse", @@ -96,29 +92,29 @@ It is also convenient to prioritize tasks that block you (the player) from doing other things. When you designate a group of trees for chopping, it's often because you want to *do* something with those logs and/or that free space. Prioritizing tree chopping will get your dwarves on the task and keep you from -staring at the screen too long. +staring at the screen in annoyance for too long. You may be tempted to automatically prioritize ``ConstructBuilding`` jobs, but beware that if you engage in megaprojects where many constructions must be built, these jobs can consume your entire fortress if prioritized. It is often better to run ``prioritize ConstructBuilding`` by itself (i.e. without the ``-a`` parameter) as needed to just prioritize the construction jobs that you -have ready at the time. +have ready at the time if you need to "clear the queue". Default list of job types to prioritize --------------------------------------- The community has assembled a good default list of job types that most players -will benefit from. They have been playtested across a wide variety of fort -types. It is a good idea to enable `prioritize` with at least these defaults -for all your forts. +will benefit from. They have been playtested across a wide variety of forts. It +is a good idea to enable `prioritize` with at least these defaults for all your +forts. The default prioritize list includes: - Handling items that can rot - Medical, hygiene, and hospice tasks - Interactions with animals and prisoners -- Noble-specific tasks (like managing workorders) +- Noble-specific tasks (like managing work orders) - Dumping items, felling trees, and other tasks that you, as a player, might stare at and internally scream "why why why isn't this getting done??". @@ -129,5 +125,5 @@ This script also provides an overlay that is managed by the `overlay` framework. A panel is added to the info sheet for buildings that are queued for construction or destruction. If a unit has taken the job, their name will be listed. Click on the name to zoom to the unit. There is also a toggle button -for high priority status for the job. Toggle it on if the job is not being +for the high priority status for the job. Toggle it on if the job is not being taken and you need it to be completed quickly. diff --git a/prioritize.lua b/prioritize.lua index 85fe3ed56b..2605ecb216 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -34,9 +34,10 @@ local DEFAULT_JOB_TYPES = { } -- 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} +-- { +-- 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 {} @@ -51,7 +52,7 @@ end local function persist_state() local data_to_persist = {} - -- convert enum keys into strings so json doesn't get confused and think the map is a list + -- convert enum keys into strings so json doesn't get confused and think the map is a sparse list for k, v in pairs(get_watched_job_matchers()) do data_to_persist[tostring(k)] = v end @@ -62,16 +63,16 @@ local function make_matcher_map(keys) if not keys then return nil end local t = {} for _,key in ipairs(keys) do - t[key] = 0 + t[key] = true 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 + return { + hauler_matchers=make_matcher_map(unit_labors), + reaction_matchers=make_matcher_map(reaction_names), + } end local function matches(job_matcher, job) @@ -87,9 +88,9 @@ local function matches(job_matcher, job) return true end --- returns true if the job is matched and it is not already high priority +-- returns true if the job is matched local function boost_job_if_matches(job, job_matchers) - if matches(job_matchers[job.job_type], job) and not job.flags.do_now then + if matches(job_matchers[job.job_type], job) then job.flags.do_now = true return true end @@ -97,20 +98,7 @@ local function boost_job_if_matches(job, job_matchers) end local function on_new_job(job) - 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 - persist_state() - end + boost_job_if_matches(job, get_watched_job_matchers()) end local function clear_watched_job_matchers() @@ -123,8 +111,7 @@ local function clear_watched_job_matchers() end local function update_handlers() - local watched_job_matchers = get_watched_job_matchers() - if next(watched_job_matchers) then + if next(get_watched_job_matchers()) then eventful.onUnload.prioritize = clear_watched_job_matchers eventful.onJobInitiated.prioritize = on_new_job else @@ -145,32 +132,47 @@ local function get_reaction_annotation_str(reaction) return (' --reaction-name %s'):format(reaction) end -local function print_status_line(num_jobs, job_type, annotation) +local function get_status_line(job_type, annotation) annotation = annotation or '' - print(('%6d %s%s'):format(num_jobs, df.job_type[job_type], annotation)) + return (' %s%s'):format(df.job_type[job_type], annotation) end local function status() - local first = true + local lines = {} 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 + if type(k) ~= 'number' then + -- fix up any stringified numbers that made their way into this list + -- it is unclear how this happens, but fix it here if it does + local num_key = tonumber(k) + dfhack.printerr('autofixing non-numeric job type: ' .. tostring(k)) + if num_key then + watched_job_matchers[num_key] = v + end + watched_job_matchers[k] = nil + k = num_key end 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)) + for hk in pairs(v.hauler_matchers) do + table.insert(lines, get_status_line(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_reaction_annotation_str(rk)) + for rk in pairs(v.reaction_matchers) do + table.insert(lines, get_status_line(k, get_reaction_annotation_str(rk))) end else - print_status_line(v.num_prioritized, k) + table.insert(lines, get_status_line(k)) end end - if first then print('Not automatically prioritizing any jobs.') end + if not next(lines) then + print('Not automatically prioritizing any jobs.') + return + end + table.sort(lines) + print('Automatically prioritized jobs:') + for _, line in ipairs(lines) do + print(line) + end end -- encapsulate df state in functions so unit tests can mock them out @@ -180,6 +182,9 @@ end function get_reactions() return df.global.world.raws.reactions.reactions end +function get_job_list() + return df.global.world.jobs.list +end local function for_all_live_postings(cb) for _,posting in ipairs(get_postings()) do @@ -189,11 +194,19 @@ local function for_all_live_postings(cb) end end +local function for_all_jobs(cb) + for _,job in utils.listpairs(get_job_list()) do + if not job.flags.special then + cb(job) + end + end +end + local function boost(job_matchers, opts) local count = 0 - for_all_live_postings( - function(posting) - if boost_job_if_matches(posting.job, job_matchers) then + for_all_jobs( + function(job) + if not job.flags.do_now and boost_job_if_matches(job, job_matchers) then count = count + 1 end end) @@ -286,7 +299,7 @@ local JOB_TYPES_DENYLIST = utils.invert{ } local DIG_SMOOTH_WARNING = { - 'Priortizing current pending jobs, but skipping automatic boosting of dig and', + 'Priortizing current jobs, but skipping automatic boosting of dig and', 'smooth/engrave job types. Automatic priority boosting of these types of jobs', 'will overwhelm the DF job scheduler. Instead, consider specializing units for', 'mining and related work details, and using vanilla designation priorities.', @@ -434,27 +447,27 @@ local function get_job_type_str(job) end local function print_current_jobs(job_matchers, opts) - local job_counts_by_type = {} + local all_jobs, unclaimed_jobs = {}, {} local filtered = next(job_matchers) - for_all_live_postings( - function(posting) - 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 - job_counts_by_type[job_type] = job_counts_by_type[job_type] + 1 - end) + local function count_job(jobs, job) + if filtered and not job_matchers[job.job_type] then return end + local job_type = get_job_type_str(job) + jobs[job_type] = (jobs[job_type] or 0) + 1 + end + for_all_jobs(curry(count_job, all_jobs)) + for_all_live_postings(function(posting) count_job(unclaimed_jobs, posting.job) end) local first = true - for k,v in pairs(job_counts_by_type) do + for k,v in pairs(all_jobs) do if first then - print('Current unclaimed jobs:') + print('Current prioritizable jobs:') + print() + print(('unclaimed total job type')) + print(('--------- ----- --------')) first = false end - print(('%4d %s'):format(v, k)) + print(('%9d %5d %s'):format(unclaimed_jobs[k] or 0, v, k)) end - if first then print('No current unclaimed jobs.') end + if first then print('No current prioritizable jobs.') end end local function print_registry_section(header, t) @@ -617,8 +630,11 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) -- convert the string keys back into enum values for k,v in pairs(persisted_data) do - if type(k) == 'string' then - persisted_data[tonumber(k)] = v + if type(k) ~= 'number' then + local num = tonumber(k) + if num then + persisted_data[num] = v + end persisted_data[k] = nil end end