From d8c6cac2676af5a690baedc8669f7b8a04c4855c Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:52:19 +0200 Subject: [PATCH 1/2] idle-crafting: make choice of crafting job dependent on resources in linked stockpiles. --- changelog.txt | 1 + docs/idle-crafting.rst | 15 +++- idle-crafting.lua | 182 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 180 insertions(+), 18 deletions(-) diff --git a/changelog.txt b/changelog.txt index d4b6641e5..50bfd543b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: - `position`: option to copy keyboard cursor position to the clipboard - `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) - `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use +- `idle-crafting`: make choice of crafting job dependent on resources in linked stockpiles. ## Documentation - `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses diff --git a/docs/idle-crafting.rst b/docs/idle-crafting.rst index 39086443a..d1cd23c51 100644 --- a/docs/idle-crafting.rst +++ b/docs/idle-crafting.rst @@ -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 orders 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). diff --git a/idle-crafting.lua b/idle-crafting.lua index b618a0a67..92673f51b 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -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 + 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 == 0 then + action(contained_item.item) + end + end + end +end + +---choose random value based on positive integer weights +---@generic T +---@param choices table +---@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() @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 +---@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' @@ -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" @@ -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 From f239c68fa60e6498a423ec9408172058ace2df09 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 14 Sep 2024 20:24:12 -0700 Subject: [PATCH 2/2] Apply suggestions from code review --- changelog.txt | 1 - docs/idle-crafting.rst | 2 +- idle-crafting.lua | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index 30ace4473..ae1b4851e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -57,7 +57,6 @@ Template for new versions: - `position`: option to copy keyboard cursor position to the clipboard - `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) - `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use -- `idle-crafting`: make choice of crafting job dependent on resources in linked stockpiles. - `exterminate`: show descriptive names for the listed races in addition to their IDs ## Documentation diff --git a/docs/idle-crafting.rst b/docs/idle-crafting.rst index d1cd23c51..5f99d24df 100644 --- a/docs/idle-crafting.rst +++ b/docs/idle-crafting.rst @@ -46,7 +46,7 @@ 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. This -script respects the setting for permitted general work orders from the "Workers" +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 diff --git a/idle-crafting.lua b/idle-crafting.lua index 92673f51b..2ccf1d30f 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -15,7 +15,7 @@ local function for_inputs(workshop, action) else 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 + if item:isAssignedToThisStockpile(stockpile.id) then for _, contained_item in ipairs(dfhack.items.getContainedItems(item)) do action(contained_item) end @@ -25,7 +25,7 @@ local function for_inputs(workshop, action) end end for _, contained_item in ipairs(workshop.contained_items) do - if contained_item.use_mode == 0 then + if contained_item.use_mode == df.building_item_role_type.TEMP then action(contained_item.item) end end