Skip to content

Commit

Permalink
boost all jobs, not just unclaimed ones
Browse files Browse the repository at this point in the history
also list all jobs for --jobs
and autofix bad keys when listing status
  • Loading branch information
myk002 committed Jul 1, 2024
1 parent 54cae97 commit fba07a4
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 83 deletions.
3 changes: 3 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
42 changes: 19 additions & 23 deletions docs/prioritize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,34 @@ 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
-----

::

enable prioritize
disable prioritize
prioritize [<options>] [defaults|<job_type> ...]

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).
Expand All @@ -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 <labor>[,<labor>...]``
For StoreItemInStockpile jobs, match only the specified hauling labor(s).
Valid ``labor`` strings are: "Stone", "Wood", "Body", "Food", "Refuse",
Expand Down Expand Up @@ -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??".

Expand All @@ -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.
136 changes: 76 additions & 60 deletions prioritize.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -87,30 +88,17 @@ 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
return false
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()
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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.',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit fba07a4

Please sign in to comment.