Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

idle-crafting: make choice of crafting job dependent on linked stockpiles. #1300

Merged
merged 3 commits into from
Sep 15, 2024
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
15 changes: 11 additions & 4 deletions docs/idle-crafting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,16 @@ needs to craft objects. Workshops that have a master assigned cannot be used in
this way.

When a workshop is designated for idle crafting, this tool will create crafting
jobs and assign them to idle dwarves who have a need for crafting
objects. Currently, bone carving and stonecrafting are supported, with
stonecrafting being the default option. This script respects the setting for
permitted general work orders from the "Workers" tab. Thus, to designate a
jobs and assign them to idle dwarves who have a need for crafting objects. This
script respects the setting for permitted general work order labors from the "Workers"
tab.

For workshops without input stockpile links, bone carving and stonecrafting are
supported, with stonecrafting being the default option. Thus, to designate a
workshop for bone carving, disable the stonecrafting labor while keeping the
bone carving labor enabled.

For workshops with input stockpile links, the creation of totems and horn crafts
are supported as well. In this case, the choice of job is made randomly based on
the resources available in the input stockpiles (respecting the permitted
labors from the workshop profile).
182 changes: 168 additions & 14 deletions idle-crafting.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,55 @@ local widgets = require('gui.widgets')
local repeatutil = require("repeat-util")
local orders = require('plugins.orders')

---iterate over input materials of workshop with stockpile links
---@param workshop df.building_workshopst
---@param action fun(item:df.item):any
local function for_inputs(workshop, action)
if #workshop.profile.links.take_from_pile == 0 then
dfhack.error('workshop has no links')
else
Copy link
Member

Choose a reason for hiding this comment

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

you can turn the else into an end and save some indentation in the rest of the function. dfhack.error throws and does not allow the function to continue.

for _, stockpile in ipairs(workshop.profile.links.take_from_pile) do
for _, item in ipairs(dfhack.buildings.getStockpileContents(stockpile)) do
if item:isAssignedToThisStockpile(stockpile.id) then
for _, contained_item in ipairs(dfhack.items.getContainedItems(item)) do
action(contained_item)
end
else
action(item)
end
end
end
for _, contained_item in ipairs(workshop.contained_items) do
if contained_item.use_mode == df.building_item_role_type.TEMP then
action(contained_item.item)
end
end
end
end

---choose random value based on positive integer weights
---@generic T
---@param choices table<T,integer>
---@return T
function weightedChoice(choices)
local sum = 0
for _, weight in pairs(choices) do
sum = sum + weight
end
if sum <= 0 then
return nil
end
local random = math.random(sum)
for choice, weight in pairs(choices) do
if random > weight then
random = random - weight
else
return choice
end
end
return nil --never reached on well-formed input
end

---create a new linked job
---@return df.job
function make_job()
Expand All @@ -14,6 +63,61 @@ function make_job()
return job
end

function assignToWorkshop(job, workshop)
job.pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z)
dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, workshop.id)
workshop.jobs:insert("#", job)
end

---make totem at specified workshop
---@param unit df.unit
---@param workshop df.building_workshopst
---@return boolean
function makeTotem(unit, workshop)
local job = make_job()
job.job_type = df.job_type.MakeTotem
job.mat_type = -1

local jitem = df.job_item:new()
jitem.item_type = df.item_type.NONE --the game seems to leave this uninitialized
jitem.mat_type = -1
jitem.mat_index = -1
jitem.quantity = 1
jitem.vector_id = df.job_item_vector_id.ANY_REFUSE
jitem.flags1.unrotten = true
jitem.flags2.totemable = true
jitem.flags2.body_part = true
job.job_items.elements:insert('#', jitem)

assignToWorkshop(job, workshop)
return dfhack.job.addWorker(job, unit)
end

---make totem at specified workshop
---@param unit df.unit
---@param workshop df.building_workshopst
---@return boolean
function makeHornCrafts(unit, workshop)
local job = make_job()
job.job_type = df.job_type.MakeCrafts
job.mat_type = -1
job.material_category.horn = true

local jitem = df.job_item:new()
jitem.item_type = df.item_type.NONE --the game seems to leave this uninitialized
jitem.mat_type = -1
jitem.mat_index = -1
jitem.quantity = 1
jitem.vector_id = df.job_item_vector_id.ANY_REFUSE
jitem.flags1.unrotten = true
jitem.flags2.horn = true
jitem.flags2.body_part = true
job.job_items.elements:insert('#', jitem)

assignToWorkshop(job, workshop)
return dfhack.job.addWorker(job, unit)
end

---make bone crafts at specified workshop
---@param unit df.unit
---@param workshop df.building_workshopst
Expand All @@ -23,7 +127,6 @@ function makeBoneCraft(unit, workshop)
job.job_type = df.job_type.MakeCrafts
job.mat_type = -1
job.material_category.bone = true
job.pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z)

local jitem = df.job_item:new()
jitem.item_type = df.item_type.NONE
Expand All @@ -36,8 +139,7 @@ function makeBoneCraft(unit, workshop)
jitem.flags2.body_part = true
job.job_items.elements:insert('#', jitem)

dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, workshop.id)
workshop.jobs:insert("#", job)
assignToWorkshop(job, workshop)
return dfhack.job.addWorker(job, unit)
end

Expand All @@ -49,7 +151,6 @@ function makeRockCraft(unit, workshop)
local job = make_job()
job.job_type = df.job_type.MakeCrafts
job.mat_type = 0
job.pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z)

local jitem = df.job_item:new()
jitem.item_type = df.item_type.BOULDER
Expand All @@ -61,12 +162,27 @@ function makeRockCraft(unit, workshop)
jitem.flags3.hard = true
job.job_items.elements:insert('#', jitem)

dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, workshop.id)
workshop.jobs:insert("#", job)

assignToWorkshop(job, workshop)
return dfhack.job.addWorker(job, unit)
end

---categorize and count crafting materials (for Craftsdwarf's workshop)
---@param tab table<string,integer>
---@param item df.item
local function categorize_craft(tab,item)
if df.item_corpsepiecest:is_instance(item) then
if item.corpse_flags.bone then
tab['bone'] = (tab['bone'] or 0) + item.material_amount.Bone
elseif item.corpse_flags.skull then
tab['skull'] = (tab['skull'] or 0) + 1
elseif item.corpse_flags.horn then
tab['horn'] = (tab['horn'] or 0) + item.material_amount.Horn
end
elseif df.item_boulderst:is_instance(item) then
tab['boulder'] = (tab['boulder'] or 0) + 1
end
end

-- script logic

local GLOBAL_KEY = 'idle-crafting'
Expand Down Expand Up @@ -180,6 +296,32 @@ function unitIsAvailable(unit)
return true
end

---select crafting job based on available resources
---@param workshop df.building_workshopst
---@return (fun(unit:df.unit, workshop:df.building_workshopst):boolean)?
function select_crafting_job(workshop)
local tab = {}
for_inputs(workshop, curry(categorize_craft,tab))
local blocked_labors = workshop.profile.blocked_labors
if blocked_labors[STONE_CRAFT] then
tab['boulder'] = nil
end
if blocked_labors[BONE_CARVE] then
tab['bone'] = nil
tab['skull'] = nil
tab['horn'] = nil
end
local material = weightedChoice(tab)
if material == 'bone' then return makeBoneCraft
elseif material == 'skull' then return makeTotem
elseif material == 'horn' then return makeHornCrafts
elseif material == 'boulder' then return makeRockCraft
else
return nil
end
end


---check if unit is ready and try to create a crafting job for it
---@param workshop df.building_workshopst
---@param idx integer "index of the unit's group"
Expand All @@ -200,19 +342,31 @@ local function processUnit(workshop, idx, unit_id)
end
-- We have an available unit
local success = false
if workshop.profile.blocked_labors[STONE_CRAFT] == false then
success = makeRockCraft(unit, workshop)
end
if not success and workshop.profile.blocked_labors[BONE_CARVE] == false then
success = makeBoneCraft(unit, workshop)
if #workshop.profile.links.take_from_pile == 0 then
-- can we do something smarter here?
if workshop.profile.blocked_labors[STONE_CRAFT] == false then
success = makeRockCraft(unit, workshop)
end
if not success and workshop.profile.blocked_labors[BONE_CARVE] == false then
success = makeBoneCraft(unit, workshop)
end
if not success then
dfhack.printerr('idle-crafting: profile allows neither bone carving nor stonecrafting')
end
else
local craftItem = select_crafting_job(workshop)
if craftItem then
success = craftItem(unit, workshop)
else
print('idle-crafting: workshop has no usable materials in linked stockpiles')
failing[workshop.id] = true
end
end
if success then
-- Why is the encoding still wrong, even when using df2console?
print('idle-crafting: assigned crafting job to ' .. dfhack.df2console(dfhack.units.getReadableName(unit)))
watched[idx][unit_id] = nil
allowed[workshop.id] = df.global.world.frame_counter
else
dfhack.printerr('idle-crafting: profile allows neither bone carving nor stonecrafting, disabling workshop')
end
return true
end
Expand Down