From a2895a8061558c7c599a3addd186e338f4915d0d Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:09:17 +0100 Subject: [PATCH 1/5] new tool: autocheese --- autocheese.lua | 155 ++++++++++++++++++++++++++++ changelog.txt | 1 + docs/autocheese.rst | 38 +++++++ internal/control-panel/registry.lua | 2 + 4 files changed, 196 insertions(+) create mode 100644 autocheese.lua create mode 100644 docs/autocheese.rst diff --git a/autocheese.lua b/autocheese.lua new file mode 100644 index 000000000..fa01fc53d --- /dev/null +++ b/autocheese.lua @@ -0,0 +1,155 @@ +--@module = true + +local ic = reqscript('idle-crafting') + +---make cheese using a specific barrel and workshop +---@param barrel df.item +---@param workshop df.building_workshopst +---@return df.job +function makeCheese(barrel, workshop) + ---@type df.job + local job = ic.make_job() + job.job_type = df.job_type.MakeCheese + + local jitem = df.job_item:new() + jitem.quantity = 0 + jitem.vector_id = df.job_item_vector_id.ANY_COOKABLE + jitem.flags1.unrotten = true + jitem.flags1.milk = true + job.job_items.elements:insert('#', jitem) + + if not dfhack.job.attachJobItem(job, barrel, df.job_item_ref.T_role.Reagent, 0, -1) then + dfhack.error('could not attach item') + end + + ic.assignToWorkshop(job, workshop) + return job +end + + + +---unit is ready to take jobs +---@param unit df.unit +---@return boolean +function unitIsAvailable(unit) + if unit.job.current_job then + return false + elseif #unit.individual_drills > 0 then + return false + elseif unit.flags1.caged or unit.flags1.chained then + return false + elseif unit.military.squad_id ~= -1 then + local squad = df.squad.find(unit.military.squad_id) + -- this lookup should never fail + ---@diagnostic disable-next-line: need-check-nil + return #squad.orders == 0 and squad.activity == -1 + end + return true +end + +---find unit with a particular labor enabled +---@param unit_labor df.unit_labor +---@param job_skill df.job_skill +---@param workshop df.building +---@return df.unit|nil +---@return integer|nil + function findAvailableLaborer(unit_labor, job_skill, workshop) + local max_unit = nil + local max_skill = -1 + for _, unit in ipairs(dfhack.units.getCitizens(true, false)) do + if + unit.status.labors[unit_labor] and + unitIsAvailable(unit) and + ic.canAccessWorkshop(unit, workshop) + then + local unit_skill = dfhack.units.getNominalSkill(unit, job_skill, true) + if unit_skill > max_skill then + max_unit = unit + max_skill = unit_skill + end + end + end + return max_unit, max_skill +end + +local function findMilkBarrel(min_liquids) + for _, container in ipairs(df.global.world.items.other.FOOD_STORAGE) do + if + not (container.flags.in_job or container.flags.forbid) and + container.flags.container and #container.general_refs >= min_liquids + then + local content_reference = dfhack.items.getGeneralRef(container, df.general_ref_type.CONTAINS_ITEM) + local contained_item = df.item.find(content_reference and content_reference.item_id or -1) + if contained_item then + local mat_info = dfhack.matinfo.decode(contained_item) + if mat_info:matches { milk = true } then + return container + end + end + end + end +end + +function findWorkshop() + for _,workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do + if + not workshop.profile.blocked_labors[df.unit_labor.MAKE_CHEESE] and + #workshop.jobs == 0 and #workshop.profile.permitted_workers == 0 + then + return workshop + end + end +end + +if dfhack_flags.module then + return +end + +-- actual script action + +local argparse = require('argparse') + +local min_number = 50 + +local _ = argparse.processArgsGetopt({...}, +{ + { 'm', 'min-milk', hasArg = true, + handler = function(min) + min_number = argparse.nonnegativeInt(min, 'min-milk') + end } +}) + + +local reagent = findMilkBarrel(min_number) + +if not reagent then + -- print('autocheese: no sufficiently full barrel found') + return +end + +local workshop = findWorkshop() + +if not workshop then + print('autocheese: no workshop available') + return +end + +local worker, skill = findAvailableLaborer(df.unit_labor.MAKE_CHEESE, df.job_skill.CHEESEMAKING, workshop) +if not worker then + print('autocheese: no cheesemaker available') + return +end +local job = makeCheese(reagent, workshop) + +print(('autocheese: dispatching cheesemaking job for %s (%d milk) to %s'):format( + dfhack.items.getDescription(reagent, 0), + #reagent.general_refs, + dfhack.df2console(dfhack.units.getReadableName(worker)) +)) + + +-- assign a worker and send it to fetch the barrel +dfhack.job.addWorker(job, worker) +dfhack.units.setPathGoal(worker, reagent.pos, df.unit_path_goal.GrabJobResources) +job.items[0].flags.is_fetching = true +job.flags.fetching = true diff --git a/changelog.txt b/changelog.txt index 4bea5b4a6..95a560069 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools - `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map - `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (e.g. units, governments, fortresses, or the world) +- `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk ## New Features - `gui/settings-manager`: new overlay on the Labor -> Standing Orders tab for configuring the number of barrels to reserve for job use (so you can brew alcohol and not have all your barrels claimed by stockpiles for container storage) diff --git a/docs/autocheese.rst b/docs/autocheese.rst new file mode 100644 index 000000000..3ef926b5f --- /dev/null +++ b/docs/autocheese.rst @@ -0,0 +1,38 @@ +autocheese +========== + +.. dfhack-tool:: + :summary: Automatically make cheese using barrels that have accumulated sufficient milk. + :tags: fort auto + +Cheese making is difficult to automate using work orders, because a single job +can consume anything from a bucket was a single unit of milk to barrel +containing up to 100 units of milk. + +The script will scan your fort for barrels with a certain minimum amount of milk +(default: 50), create a cheese making job specifically for that barrel, and +assign this job to one of your idle dwarves (giving preference to skilled cheese +makers). + +When enabled using `gui/control-panel`, the script will run automatically, with +default options, twice a month. + +Usage +----- + +:: + + autocheese [] + +Examples +-------- + +``autocheese -m 100`` + Only create a job if there is a barrel that is filled to the maximum. + +Options +------- + +``-m``, ``--min-milk`` + Set the minimum number of milk items in a barrel for the barrel to be + considered for cheese making. \ No newline at end of file diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 04b28b3ff..6ffd0e3a7 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -18,6 +18,8 @@ COMMANDS_BY_IDX = { {command='autobutcher target 10 10 14 2 BIRD_PEAFOWL_BLUE', group='automation', mode='run', desc='Enable if you usually want to raise peafowl.'}, {command='autochop', group='automation', mode='enable'}, + {command='autocheese', group='automation', mode='repeat', + params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'autocheese', ']'}}, {command='autoclothing', group='automation', mode='enable'}, {command='autofarm', group='automation', mode='enable'}, {command='autofarm threshold 150 grass_tail_pig', group='automation', mode='run', From 70240fe3fbdf6ead3f3eeea78ef769ec675ba7af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Jan 2025 19:22:29 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/autocheese.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/autocheese.rst b/docs/autocheese.rst index 3ef926b5f..b96ffd651 100644 --- a/docs/autocheese.rst +++ b/docs/autocheese.rst @@ -35,4 +35,4 @@ Options ``-m``, ``--min-milk`` Set the minimum number of milk items in a barrel for the barrel to be - considered for cheese making. \ No newline at end of file + considered for cheese making. From 67e94aa8160aca4a2beadaf628ea36bdf229eab0 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:43:46 +0100 Subject: [PATCH 3/5] Stylistic improvements suggested by Myk Co-authored-by: Myk --- autocheese.lua | 4 ++-- docs/autocheese.rst | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/autocheese.lua b/autocheese.lua index fa01fc53d..135e933fe 100644 --- a/autocheese.lua +++ b/autocheese.lua @@ -130,7 +130,7 @@ end local workshop = findWorkshop() if not workshop then - print('autocheese: no workshop available') + print('autocheese: no Farmer's Workshop available') return end @@ -142,7 +142,7 @@ end local job = makeCheese(reagent, workshop) print(('autocheese: dispatching cheesemaking job for %s (%d milk) to %s'):format( - dfhack.items.getDescription(reagent, 0), + dfhack.df2console(dfhack.items.getReadableDescription(reagent)), #reagent.general_refs, dfhack.df2console(dfhack.units.getReadableName(worker)) )) diff --git a/docs/autocheese.rst b/docs/autocheese.rst index b96ffd651..fb92874df 100644 --- a/docs/autocheese.rst +++ b/docs/autocheese.rst @@ -2,12 +2,13 @@ autocheese ========== .. dfhack-tool:: - :summary: Automatically make cheese using barrels that have accumulated sufficient milk. + :summary: Schedule cheese making jobs based on milk reserves. :tags: fort auto -Cheese making is difficult to automate using work orders, because a single job -can consume anything from a bucket was a single unit of milk to barrel -containing up to 100 units of milk. +Cheese making is difficult to automate using work orders. A single job +can consume anything from a bucket with a single unit of milk to a barrel +with 100 units of milk. This makes it hard to predict how much cheese will +actually be produced by an automated order. The script will scan your fort for barrels with a certain minimum amount of milk (default: 50), create a cheese making job specifically for that barrel, and From 44b5547b70f6734b67b22cb6d4e0e1a5deb4f999 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:08:37 +0100 Subject: [PATCH 4/5] Implement feedback from code review - check that barrel can be brought to workshop - handle workshops with assigned masters --- autocheese.lua | 50 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/autocheese.lua b/autocheese.lua index 135e933fe..b365f3f7b 100644 --- a/autocheese.lua +++ b/autocheese.lua @@ -47,6 +47,17 @@ function unitIsAvailable(unit) return true end +---check if unit can perform labor at workshop +---@param unit df.unit +---@param unit_labor df.unit_labor +---@param workshop df.building +---@return boolean +function availableLaborer(unit, unit_labor, workshop) + return unit.status.labors[unit_labor] + and unitIsAvailable(unit) + and ic.canAccessWorkshop(unit, workshop) +end + ---find unit with a particular labor enabled ---@param unit_labor df.unit_labor ---@param job_skill df.job_skill @@ -58,9 +69,7 @@ end local max_skill = -1 for _, unit in ipairs(dfhack.units.getCitizens(true, false)) do if - unit.status.labors[unit_labor] and - unitIsAvailable(unit) and - ic.canAccessWorkshop(unit, workshop) + availableLaborer(unit, unit_labor, workshop) then local unit_skill = dfhack.units.getNominalSkill(unit, job_skill, true) if unit_skill > max_skill then @@ -90,13 +99,32 @@ local function findMilkBarrel(min_liquids) end end -function findWorkshop() +---find a workshop to which the barrel can be brought +---if the workshop has a master, only return workshop and master if the master is available +---@param pos df.coord +---@return df.building_workshopst? +---@return df.unit? +function findWorkshop(pos) for _,workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do if + dfhack.maps.canWalkBetween(pos, xyz2pos(workshop.centerx, workshop.centery, workshop.z)) and not workshop.profile.blocked_labors[df.unit_labor.MAKE_CHEESE] and - #workshop.jobs == 0 and #workshop.profile.permitted_workers == 0 + #workshop.jobs == 0 then - return workshop + if #workshop.profile.permitted_workers == 0 then + -- immediately return workshop without master + return workshop, nil + else + unit = df.unit.find(workshop.profile.permitted_workers[0]) + if + unit and availableLaborer(unit, df.unit_labor.MAKE_CHEESE, workshop) + then + -- return workshop and master, if master is available + return workshop, unit + else + print("autocheese: Skipping farmer's workshop with unavailable master") + end + end end end end @@ -127,14 +155,18 @@ if not reagent then return end -local workshop = findWorkshop() +local workshop, worker = findWorkshop(xyz2pos(dfhack.items.getPosition(reagent))) if not workshop then - print('autocheese: no Farmer's Workshop available') + print("autocheese: no Farmer's Workshop available") return end -local worker, skill = findAvailableLaborer(df.unit_labor.MAKE_CHEESE, df.job_skill.CHEESEMAKING, workshop) +-- try to find laborer for workshop without master +if not worker then + worker, _ = findAvailableLaborer(df.unit_labor.MAKE_CHEESE, df.job_skill.CHEESEMAKING, workshop) +end + if not worker then print('autocheese: no cheesemaker available') return From e07775adb2a58ca23b37fc3841b9e086c3cf09bb Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 25 Jan 2025 10:29:06 -0800 Subject: [PATCH 5/5] remove duplicate changelog line --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 38eda5431..fe674b701 100644 --- a/changelog.txt +++ b/changelog.txt @@ -51,7 +51,6 @@ Template for new versions: ## New Tools - `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map -- `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (e.g. units, governments, fortresses, or the world) - `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (units, governments, fortresses, the world, etc.) ## New Features