Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions autocheese.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
--@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

---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
---@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
availableLaborer(unit, unit_labor, 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

---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
then
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

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, worker = findWorkshop(xyz2pos(dfhack.items.getPosition(reagent)))

if not workshop then
print("autocheese: no Farmer's Workshop available")
return
end

-- 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
end
local job = makeCheese(reagent, workshop)

print(('autocheese: dispatching cheesemaking job for %s (%d milk) to %s'):format(
dfhack.df2console(dfhack.items.getReadableDescription(reagent)),
#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
2 changes: 2 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Template for new versions:

## New Tools

- `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs to be moved up to the current version


## New Features

## Fixes
Expand Down
39 changes: 39 additions & 0 deletions docs/autocheese.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
autocheese
==========

.. dfhack-tool::
:summary: Schedule cheese making jobs based on milk reserves.
:tags: fort auto

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
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 [<options>]

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.
2 changes: 2 additions & 0 deletions internal/control-panel/registry.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading