From 86e636f1b4327ab606c27cc36e306258ee809461 Mon Sep 17 00:00:00 2001 From: git--amade Date: Tue, 15 Jul 2025 19:08:38 +0800 Subject: [PATCH 01/17] Add entomb.lua and docs/entomb.rst --- docs/entomb.rst | 61 +++++++++++++++ entomb.lua | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 docs/entomb.rst create mode 100644 entomb.lua diff --git a/docs/entomb.rst b/docs/entomb.rst new file mode 100644 index 000000000..c519ff7bc --- /dev/null +++ b/docs/entomb.rst @@ -0,0 +1,61 @@ +entomb +====== + +.. dfhack-tool:: + :summary: Entomb any corpse into tomb zones. + :tags: fort items buildings + +Assign any corpse regardless of citizenship, residency, pet status, +or affiliation to an unassigned tomb zone for burial. + +Usage +----- + +``entomb []`` + +This script must be executed with either a unit's corpse or body part +selected or with a unit ID specified. An unassigned tomb zone will then +be assigned to the unit for burial and all its corpse and/or body parts +will become valid items for interment. + +Optionally, the zone ID may also be specified to assign a specific tomb +zone to the unit. + +A non-citizen, non-resident, or non-pet unit that is still alive may +even be assigned a tomb zone if they have lost any body part that can +be placed inside a tomb, e.g. teeth or severed limbs. New corpse items +after a tomb has already been assigned will not be properly interred +until the script is executed again on either the unit, its corpse, or +any of its body parts. + +If executed on slaughtered animals, all its butchering returns will +become valid burial items and no longer usable for cooking or crafting. + +Examples +-------- + +``entomb unit `` + Assign an unassigned tomb zone to the unit with the specified ID. + +``entomb tomb `` + Assign a tomb zone with the specified ID to the selected corpse + item's unit. + +``entomb unit tomb now`` + Assign a tomb zone with the specified ID to the unit with the + specified ID and teleport its corpse and/or body parts into the + coffin in the tomb zone. + +Options +------- + +``unit `` + Specify the ID of the unit to be assigned to a tomb zone. + +``tomb `` + Specify the ID of the zone into which a unit will be interred. + +``now`` + Instantly teleport the unit's corpse and/or body parts into the + coffin of its assigned tomb zone. This option can be called on + corpse items or units that are already assigned a tomb zone. diff --git a/entomb.lua b/entomb.lua new file mode 100644 index 000000000..2780232d2 --- /dev/null +++ b/entomb.lua @@ -0,0 +1,193 @@ +-- Entomb corpse items of any dead unit. +--@module = true + +local utils = require('utils') + +local unit_id +local unit +local building_id +local tomb +local forceBurial + +local args = {...} + +-- Get unit from selected corpse or corpse piece item. +local function GetUnitFromCorpse() + local item = dfhack.gui.getSelectedItem(true) + if item then + if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then + unit_id = item.unit_id + unit = df.unit.find(unit_id) + else + qerror('Selected item is not a corpse or body part.') + end + else + qerror('No item selected or unit specified.') + end +end + +-- Validate tomb zone assignment. +local function CheckTombZone(building, id) + if df.building_civzonest:is_instance(building) then + if building.type == 97 then + if building.assigned_unit_id == id then + return true + end + end + end +end + +-- Iterate through all available tomb zones. +local function IterateTombZone(id) + for _, building in pairs(df.global.world.buildings.all) do + if CheckTombZone(building, id) then return building end + end +end + +-- Check if any of the unit's corpse items are still not in a coffin. +local function isNotBuried() + for _, item_id in pairs(unit.corpse_parts) do + local item = df.item.find(item_id) + if item then + local inCoffin = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) + local coffinBuilding_id = inCoffin and inCoffin.building_id or nil + local coffin = coffinBuilding_id and df.building.find(coffinBuilding_id) or nil + local isCoffin = coffin and df.building_coffinst:is_instance(coffin) or nil + -- Return TRUE if even one item is not interred. + if not isCoffin then + return true + end + end + end +end + +local function GetEmptyTombZone() + -- Check if unit is already assigned to a tomb zone. + local isAlreadyAssigned = IterateTombZone(unit_id) + if isAlreadyAssigned then + if isNotBuried() or forceBurial then + tomb = isAlreadyAssigned + print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') + else + qerror('Unit is already interred in a tomb zone.') + end + else + -- Find an unassigned tomb zone. + tomb = IterateTombZone(-1) + end + if not tomb then + qerror('No unassigned tomb zones are available.') + end +end + +-- Set corpse items to be valid for burial. +local function FlagForBurial(corpseParts) + -- Undead units have empty corpse_parts vector. + if unit.enemy.undead then + for _, item in pairs(df.global.world.items.other.IN_PLAY) do + if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then + if item.unit_id == unit_id then + corpseParts:insert(#corpseParts, item.id) + end + end + end + utils.sort_vector(corpseParts) + end + local burialItemCount = 0 + for _, item_id in pairs(corpseParts) do + local item = df.item.find(item_id) + if item then + item.flags.dead_dwarf = true + -- Some corpse items may be lost/destroyed before burial. + burialItemCount = burialItemCount + 1 + end + end + if burialItemCount == 0 then + qerror('Unit has no corpse or body parts available for burial.') + end + tomb.assigned_unit_id = unit_id + return burialItemCount +end + +local function PutInCoffin(corpseParts) + local coffin + for _, building in pairs(tomb.contained_buildings) do + if df.building_coffinst:is_instance(building) then coffin = building end + end + if coffin then + -- Set df.building_item_role_type.PERM first before changing + -- it to TEMP to turn it into an interred corpse item. + for _, item_id in pairs(corpseParts) do + local item = df.item.find(item_id) + if item then + dfhack.items.moveToBuilding(item, coffin, 2) + end + end + for _, buildingItem in pairs(coffin.contained_items) do + local item = buildingItem.item + if not df.item_coffinst:is_instance(item) then + buildingItem.use_mode = 0 + end + end + print('Corpse items have been teleported into a coffin.') + else + print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') + end +end + +local function AssignToTomb() + local corpseParts = unit.corpse_parts + local strBurial = '%s assigned to a tomb zone for burial.' + local strCorpseItems = '(%d corpse or body part%s)' + local strUnitName = unit and dfhack.units.getReadableName(unit) + local strPlural = '' + local incident_id = unit.counters.death_id + if incident_id ~= -1 then + local incident = df.incident.find(incident_id) + -- Corpse will not be interred if not yet discovered. + incident.flags.discovered = true + end + local burialItemCount = FlagForBurial(corpseParts) + print(string.format(strBurial, strUnitName)) + if forceBurial then PutInCoffin(corpseParts) end + if burialItemCount > 1 then strPlural = 's' end + print(string.format(strCorpseItems, burialItemCount, strPlural)) +end + +local function parseArgs() + local building + if #args > 0 then + for i, v in ipairs(args) do + if v == 'unit' then + unit_id = tonumber(args[i+1]) or nil + unit = unit_id and df.unit.find(unit_id) + if not unit then qerror('Invalid unit ID.') end + end + if v == 'tomb' then + building_id = tonumber(args[i+1]) or nil + building = building_id and df.building.find(building_id) + if not building then qerror('Invalid zone ID.') end + -- Check if tomb zone is unassigned. + if CheckTombZone(building, -1) then + tomb = building + else + qerror('Specified zone ID does not point to an unassigned tomb zone.') + end + end + if v == 'now' then forceBurial = true end + end + end +end + +local function Main() + parseArgs() + if not unit then GetUnitFromCorpse() end + if unit then + if not tomb then GetEmptyTombZone() end + if tomb then AssignToTomb() end + end +end + +if not dfhack_flags.module then + Main() +end From f684542ccc42068a4785d9c9f296467be87d0521 Mon Sep 17 00:00:00 2001 From: git--amade Date: Tue, 15 Jul 2025 19:26:31 +0800 Subject: [PATCH 02/17] Update changelog.txt to add new tool: entomb --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index a937e2fb2..624dc7723 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: # Future ## New Tools +- `entomb`: allow any unit that has a corpse or body parts to be assigned a tomb zone ## New Features From 07594b542d594c9201c2332167cabeb087865a32 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 16 Jul 2025 21:31:53 +0800 Subject: [PATCH 03/17] Apply revisions to entomb.lua according to PR comments --- entomb.lua | 206 ++++++++++++++++++++++++++++------------------------- 1 file changed, 109 insertions(+), 97 deletions(-) diff --git a/entomb.lua b/entomb.lua index 2780232d2..e462456d4 100644 --- a/entomb.lua +++ b/entomb.lua @@ -1,100 +1,85 @@ -- Entomb corpse items of any dead unit. --@module = true -local utils = require('utils') - -local unit_id -local unit -local building_id -local tomb -local forceBurial - -local args = {...} - -- Get unit from selected corpse or corpse piece item. -local function GetUnitFromCorpse() - local item = dfhack.gui.getSelectedItem(true) +function GetUnitFromCorpse(item) + if math.type(item) == "integer" then item = df.item.find(item) + elseif not item then item = dfhack.gui.getSelectedItem(true) end if item then if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then - unit_id = item.unit_id - unit = df.unit.find(unit_id) + return df.unit.find(item.unit_id) else - qerror('Selected item is not a corpse or body part.') + qerror('Item is not a corpse or body part.') end - else - qerror('No item selected or unit specified.') end end -- Validate tomb zone assignment. -local function CheckTombZone(building, id) - if df.building_civzonest:is_instance(building) then - if building.type == 97 then - if building.assigned_unit_id == id then - return true - end +local function CheckTombZone(building, unit_id) + if building.type == df.civzone_type.Tomb then + if building.assigned_unit_id == unit_id then + return true end end end -- Iterate through all available tomb zones. -local function IterateTombZone(id) - for _, building in pairs(df.global.world.buildings.all) do - if CheckTombZone(building, id) then return building end +local function IterateTombZones(unit_id) + for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do + if CheckTombZone(building, unit_id) then return building end end end --- Check if any of the unit's corpse items are still not in a coffin. -local function isNotBuried() - for _, item_id in pairs(unit.corpse_parts) do +-- Check if any of the unit's corpse items are not yet placed in a coffin. +function isEntombed(unit) + -- Return FALSE for still living or undead units with empty corpse_parts vector. + if #unit.corpse_parts == 0 then return false end + for _, item_id in ipairs(unit.corpse_parts) do local item = df.item.find(item_id) if item then - local inCoffin = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) - local coffinBuilding_id = inCoffin and inCoffin.building_id or nil - local coffin = coffinBuilding_id and df.building.find(coffinBuilding_id) or nil - local isCoffin = coffin and df.building_coffinst:is_instance(coffin) or nil - -- Return TRUE if even one item is not interred. + local inBuilding = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) + local building_id = inBuilding and inBuilding.building_id or -1 + local building = df.building.find(building_id) + local isCoffin = (building and df.building_coffinst:is_instance(building)) or false + -- Return FALSE if even one item is not interred. if not isCoffin then - return true + return false end end end + return true end -local function GetEmptyTombZone() +local function GetTombZone(unit) + local unit_id = unit.id + local tomb + local entombed = false -- Check if unit is already assigned to a tomb zone. - local isAlreadyAssigned = IterateTombZone(unit_id) + local isAlreadyAssigned = IterateTombZones(unit_id) if isAlreadyAssigned then - if isNotBuried() or forceBurial then - tomb = isAlreadyAssigned - print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') - else - qerror('Unit is already interred in a tomb zone.') - end + tomb = isAlreadyAssigned + entombed = isEntombed(unit) else -- Find an unassigned tomb zone. - tomb = IterateTombZone(-1) - end - if not tomb then - qerror('No unassigned tomb zones are available.') + tomb = IterateTombZones(-1) end + return tomb, entombed end -- Set corpse items to be valid for burial. -local function FlagForBurial(corpseParts) +local function FlagForBurial(unit, corpseParts) -- Undead units have empty corpse_parts vector. if unit.enemy.undead then - for _, item in pairs(df.global.world.items.other.IN_PLAY) do + for _, item in ipairs(df.global.world.items.other.ANY_CORPSE) do if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then - if item.unit_id == unit_id then - corpseParts:insert(#corpseParts, item.id) + if item.unit_id == unit.id then + corpseParts:insert('#', item.id) end end end - utils.sort_vector(corpseParts) end local burialItemCount = 0 - for _, item_id in pairs(corpseParts) do + for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) if item then item.flags.dead_dwarf = true @@ -102,70 +87,87 @@ local function FlagForBurial(corpseParts) burialItemCount = burialItemCount + 1 end end - if burialItemCount == 0 then - qerror('Unit has no corpse or body parts available for burial.') - end - tomb.assigned_unit_id = unit_id return burialItemCount end -local function PutInCoffin(corpseParts) - local coffin - for _, building in pairs(tomb.contained_buildings) do - if df.building_coffinst:is_instance(building) then coffin = building end - end - if coffin then - -- Set df.building_item_role_type.PERM first before changing - -- it to TEMP to turn it into an interred corpse item. - for _, item_id in pairs(corpseParts) do - local item = df.item.find(item_id) - if item then - dfhack.items.moveToBuilding(item, coffin, 2) - end +function PutInCoffin(coffin, item) + if item then + -- Set df.building_item_role_type.PERM first before changing it to TEMP to turn the items + -- into interred burial items, otherwise the items will be hauled back to stockpiles. + -- https://discord.com/channels/793331351645323264/873014631315148840/1394242351345434654 + dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.PERM) end - for _, buildingItem in pairs(coffin.contained_items) do - local item = buildingItem.item - if not df.item_coffinst:is_instance(item) then - buildingItem.use_mode = 0 - end + for _, buildingItem in ipairs(coffin.contained_items) do + local item = buildingItem.item + if not df.item_coffinst:is_instance(item) then + buildingItem.use_mode = df.building_item_role_type.TEMP end - print('Corpse items have been teleported into a coffin.') - else - print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') end end -local function AssignToTomb() +local function GetCoffin(tomb) + local coffin + if tomb.type == df.civzone_type.Tomb then + for _, building in ipairs(tomb.contained_buildings) do + if df.building_coffinst:is_instance(building) then coffin = building end + end + -- Allow other scripts to call this function and pass the actual coffin building instead. + elseif df.building_coffinst:is_instance(tomb) then + coffin = tomb + end + return coffin +end + +function AssignToTomb(unit, tomb, forceBurial) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to a tomb zone for burial.' local strCorpseItems = '(%d corpse or body part%s)' + local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) local strPlural = '' local incident_id = unit.counters.death_id if incident_id ~= -1 then local incident = df.incident.find(incident_id) - -- Corpse will not be interred if not yet discovered. + -- Corpse will not be interred if not yet discovered, + -- which never happens for units not belonging to player's civ. incident.flags.discovered = true end - local burialItemCount = FlagForBurial(corpseParts) - print(string.format(strBurial, strUnitName)) - if forceBurial then PutInCoffin(corpseParts) end - if burialItemCount > 1 then strPlural = 's' end - print(string.format(strCorpseItems, burialItemCount, strPlural)) + local burialItemCount = FlagForBurial(unit, corpseParts) + if burialItemCount == 0 then + print(string.format(strNoCorpse, strUnitName)) + else + tomb.assigned_unit_id = unit.id + print(string.format(strBurial, strUnitName)) + if forceBurial then + local coffin = GetCoffin(tomb) + print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') + if coffin then + for _, item_id in ipairs(corpseParts) do + local item = df.item.find(item_id) + PutInCoffin(coffin, item) + end + print('Corpse items have been teleported into a coffin.') + else + print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') + end + end + if burialItemCount > 1 then strPlural = 's' end + print(string.format(strCorpseItems, burialItemCount, strPlural)) + end end -local function parseArgs() - local building - if #args > 0 then +local function parseArgs(args) + local unit, tomb, forceBurial + if args and #args > 0 then for i, v in ipairs(args) do if v == 'unit' then - unit_id = tonumber(args[i+1]) or nil + local unit_id = tonumber(args[i+1]) or nil unit = unit_id and df.unit.find(unit_id) if not unit then qerror('Invalid unit ID.') end end if v == 'tomb' then - building_id = tonumber(args[i+1]) or nil - building = building_id and df.building.find(building_id) + local building_id = tonumber(args[i+1]) or nil + local building = building_id and df.building.find(building_id) if not building then qerror('Invalid zone ID.') end -- Check if tomb zone is unassigned. if CheckTombZone(building, -1) then @@ -177,17 +179,27 @@ local function parseArgs() if v == 'now' then forceBurial = true end end end + return unit, tomb, forceBurial end -local function Main() - parseArgs() - if not unit then GetUnitFromCorpse() end +local function Main(args) + local unit, tomb, forceBurial = parseArgs(args) + local entombed + if not unit then unit = GetUnitFromCorpse() end if unit then - if not tomb then GetEmptyTombZone() end - if tomb then AssignToTomb() end + if not tomb then tomb, entombed = GetTombZone(unit) end + if entombed then + print('Unit is already completely interred in a tomb zone.') + elseif tomb then + AssignToTomb(unit, tomb, forceBurial) + else + print('No unassigned tomb zones are available.') + end + else + qerror('No item selected or unit specified.') end end if not dfhack_flags.module then - Main() + Main({...}) end From 0f75b5622740e2cfb40ab70e826748598012d1af Mon Sep 17 00:00:00 2001 From: git--amade Date: Thu, 17 Jul 2025 03:31:32 +0800 Subject: [PATCH 04/17] Apply 2nd revision to entomb.lua: improve PutInCoffin() --- entomb.lua | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/entomb.lua b/entomb.lua index e462456d4..88483d460 100644 --- a/entomb.lua +++ b/entomb.lua @@ -21,6 +21,7 @@ local function CheckTombZone(building, unit_id) return true end end + return false end -- Iterate through all available tomb zones. @@ -28,6 +29,7 @@ local function IterateTombZones(unit_id) for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do if CheckTombZone(building, unit_id) then return building end end + return nil end -- Check if any of the unit's corpse items are not yet placed in a coffin. @@ -92,15 +94,17 @@ end function PutInCoffin(coffin, item) if item then - -- Set df.building_item_role_type.PERM first before changing it to TEMP to turn the items - -- into interred burial items, otherwise the items will be hauled back to stockpiles. - -- https://discord.com/channels/793331351645323264/873014631315148840/1394242351345434654 - dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.PERM) + -- Remove job from item to allow it to be teleported. + if item.flags.in_job then + local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + local job = inJob and inJob.data.job + if job then + dfhack.job.removeJob(job) + end end - for _, buildingItem in ipairs(coffin.contained_items) do - local item = buildingItem.item - if not df.item_coffinst:is_instance(item) then - buildingItem.use_mode = df.building_item_role_type.TEMP + if (dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.TEMP)) then + -- Flag the item become an interred item, otherwise it will be hauled back to stockpiles. + item.flags.in_building = true end end end @@ -120,7 +124,9 @@ end function AssignToTomb(unit, tomb, forceBurial) local corpseParts = unit.corpse_parts - local strBurial = '%s assigned to a tomb zone for burial.' + local strBurial = '%s assigned to %s for burial.' + local strTomb = 'a tomb zone' + if #tomb.name > 0 then strTomb = tomb.name end local strCorpseItems = '(%d corpse or body part%s)' local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) @@ -137,10 +143,9 @@ function AssignToTomb(unit, tomb, forceBurial) print(string.format(strNoCorpse, strUnitName)) else tomb.assigned_unit_id = unit.id - print(string.format(strBurial, strUnitName)) + print(string.format(strBurial, strUnitName, strTomb)) if forceBurial then local coffin = GetCoffin(tomb) - print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') if coffin then for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) From 36b7771fab7892e5ed74ef39ee8438657e84904c Mon Sep 17 00:00:00 2001 From: SilasD Date: Fri, 18 Jul 2025 04:24:29 -0700 Subject: [PATCH 05/17] entomb.lua Implement a function that creates a job to haul an item to a coffin. Only the function has been created; the program's UI has not been updated. --- entomb.lua | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/entomb.lua b/entomb.lua index 88483d460..8d4db1433 100644 --- a/entomb.lua +++ b/entomb.lua @@ -1,5 +1,6 @@ -- Entomb corpse items of any dead unit. --@module = true +local utils = require('utils') -- Get unit from selected corpse or corpse piece item. function GetUnitFromCorpse(item) @@ -109,6 +110,51 @@ function PutInCoffin(coffin, item) end end +function HaulToCoffin(tomb, coffin, item) + if not tomb or not coffin or not item then return end + + if dfhack.items.getHolderBuilding(item) == coffin and item.flags.in_building == true then +print("DEBUG: item is already properly interred, skipping", tomb.id, dfhack.buildings.getName(tomb), +coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item)) + return -- already interred in this coffin, skip + end + + -- TODO Consider what should happen when certain item.flags are set, particularly .forbid and .dump. + -- TODO Consider copy-paste-modify scripts/internal/caravan/pedestal.lua::is_displayable_item() + + -- Remove current job from item to allow it to be moved to the tomb. + if item.flags.in_job then + local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + local job = inJob and inJob.data.job or nil + if job + and job.job_type == df.job_type.PlaceItemInTomb + and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER) ~= nil + and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER).building_id == tomb.id + then +print("DEBUG: desired job already exists, skipping", tomb.id, dfhack.buildings.getName(tomb), +coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item), job.id) + return -- desired job already exists, skip + end + if job then +print("DEBUG: removing current job from this item", item.id, dfhack.items.getReadableDescription(item), +job.id, df.job_type[job.job_type]) + dfhack.job.removeJob(job) + end + end + + local pos = utils.getBuildingCenter(coffin) + + local job = df.job:new() + job.job_type = df.job_type.PlaceItemInTomb + job.pos = pos + + dfhack.job.attachJobItem(job, item, df.job_role_type.Hauled, -1, -1) + dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, tomb.id) + tomb.jobs:insert('#', job) + + dfhack.job.linkIntoWorld(job, true) +end + local function GetCoffin(tomb) local coffin if tomb.type == df.civzone_type.Tomb then @@ -149,7 +195,8 @@ function AssignToTomb(unit, tomb, forceBurial) if coffin then for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) - PutInCoffin(coffin, item) + -- PutInCoffin(coffin, item) + HaulToCoffin(tomb, coffin, item) end print('Corpse items have been teleported into a coffin.') else From b359cbaa4c7ea3b62da9ccea24ca3fdf6e025581 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 19 Jul 2025 10:31:32 -0700 Subject: [PATCH 06/17] entomb.lua Very Important Bugfix completely assign unit to tomb. --- entomb.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/entomb.lua b/entomb.lua index 8d4db1433..f5e1895d5 100644 --- a/entomb.lua +++ b/entomb.lua @@ -189,6 +189,7 @@ function AssignToTomb(unit, tomb, forceBurial) print(string.format(strNoCorpse, strUnitName)) else tomb.assigned_unit_id = unit.id + tomb.assigned_unit = unit print(string.format(strBurial, strUnitName, strTomb)) if forceBurial then local coffin = GetCoffin(tomb) From 6a185fd92433b03c8e912597b3fdc9814f6c92ee Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 19 Jul 2025 21:09:30 -0700 Subject: [PATCH 07/17] entomb.lua Update the unit's owned buildings with the newly-assigned tomb. --- entomb.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/entomb.lua b/entomb.lua index f5e1895d5..0f0e08428 100644 --- a/entomb.lua +++ b/entomb.lua @@ -190,6 +190,9 @@ function AssignToTomb(unit, tomb, forceBurial) else tomb.assigned_unit_id = unit.id tomb.assigned_unit = unit + if not utils.linear_index(unit.owned_buildings, tomb) then + unit.owned_buildings:insert('#', tomb) + end print(string.format(strBurial, strUnitName, strTomb)) if forceBurial then local coffin = GetCoffin(tomb) From a2f45484b58a7ada57e3c7ea3f436424a873b744 Mon Sep 17 00:00:00 2001 From: SilasD Date: Wed, 23 Jul 2025 11:20:39 -0700 Subject: [PATCH 08/17] Undo commit b359cbaa4c7ea3b62da9ccea24ca3fdf6e025581 entomb.lua Very Important Bugfix completely assign unit to tomb. Because this field was removed in DF 0.51.11. --- entomb.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/entomb.lua b/entomb.lua index 0f0e08428..182981042 100644 --- a/entomb.lua +++ b/entomb.lua @@ -189,7 +189,6 @@ function AssignToTomb(unit, tomb, forceBurial) print(string.format(strNoCorpse, strUnitName)) else tomb.assigned_unit_id = unit.id - tomb.assigned_unit = unit if not utils.linear_index(unit.owned_buildings, tomb) then unit.owned_buildings:insert('#', tomb) end From 25714bf78b8b54705c50ad47c0f13c6b13207861 Mon Sep 17 00:00:00 2001 From: git--amade Date: Fri, 25 Jul 2025 10:20:28 +0800 Subject: [PATCH 09/17] Add function to validate moving items, separate job removal into own function --- entomb.lua | 158 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 58 deletions(-) diff --git a/entomb.lua b/entomb.lua index 182981042..a1e109661 100644 --- a/entomb.lua +++ b/entomb.lua @@ -17,7 +17,7 @@ end -- Validate tomb zone assignment. local function CheckTombZone(building, unit_id) - if building.type == df.civzone_type.Tomb then + if df.building_civzonest:is_instance(building) and building.type == df.civzone_type.Tomb then if building.assigned_unit_id == unit_id then return true end @@ -93,36 +93,21 @@ local function FlagForBurial(unit, corpseParts) return burialItemCount end -function PutInCoffin(coffin, item) - if item then - -- Remove job from item to allow it to be teleported. - if item.flags.in_job then - local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) - local job = inJob and inJob.data.job - if job then - dfhack.job.removeJob(job) - end - end - if (dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.TEMP)) then - -- Flag the item become an interred item, otherwise it will be hauled back to stockpiles. - item.flags.in_building = true - end - end -end - -function HaulToCoffin(tomb, coffin, item) - if not tomb or not coffin or not item then return end - - if dfhack.items.getHolderBuilding(item) == coffin and item.flags.in_building == true then -print("DEBUG: item is already properly interred, skipping", tomb.id, dfhack.buildings.getName(tomb), -coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item)) - return -- already interred in this coffin, skip +-- Adapted from scripts/internal/caravan/pedestal.lua::is_displayable_item() +-- Allow checks for possible use case of interring of non-corpse items. +local function isMoveableItem(tomb, coffin, item, options) + if not item or + item.flags.hostile or + item.flags.removed or + item.flags.spider_web or + item.flags.construction or + item.flags.encased or + item.flags.trader or + item.flags.owned or + item.flags.on_fire + then + return false end - - -- TODO Consider what should happen when certain item.flags are set, particularly .forbid and .dump. - -- TODO Consider copy-paste-modify scripts/internal/caravan/pedestal.lua::is_displayable_item() - - -- Remove current job from item to allow it to be moved to the tomb. if item.flags.in_job then local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) local job = inJob and inJob.data.job or nil @@ -130,34 +115,68 @@ coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDe and job.job_type == df.job_type.PlaceItemInTomb and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER) ~= nil and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER).building_id == tomb.id + -- Allow task to be cancelled if teleporting. + and not options.teleport then -print("DEBUG: desired job already exists, skipping", tomb.id, dfhack.buildings.getName(tomb), -coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item), job.id) - return -- desired job already exists, skip + return false end - if job then -print("DEBUG: removing current job from this item", item.id, dfhack.items.getReadableDescription(item), -job.id, df.job_type[job.job_type]) - dfhack.job.removeJob(job) + elseif item.flags.in_inventory then + local inContainer = dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINED_IN_ITEM) + if not inContainer then return false end + end + if not dfhack.maps.isTileVisible(xyz2pos(dfhack.items.getPosition(item))) then + return false + end + if item.flags.in_building then + local building = dfhack.items.getHolderBuilding(item) + -- Item is already interred. + if building and building == coffin then return false end + for _, containedItem in ipairs(building.contained_items) do + -- Item is part of a building. + if item == contained_item.item then return false end end end + return true +end - local pos = utils.getBuildingCenter(coffin) +-- Remove job from item to allow for hauling or teleportation. +local function RemoveJob(item) + local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + local job = inJob and inJob.data.job + if job then dfhack.job.removeJob(job) end +end + +function TeleportToCoffin(tomb, coffin, item) + if not tomb or not coffin then return end + local itemName = item and dfhack.items.getReadableDescription(item) or nil + if item.flags.in_job then RemoveJob(item) end + if (dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.TEMP)) then + -- Flag the item to become an interred item, otherwise it will be hauled back to stockpiles. + item.flags.in_building = true + local strMove = 'Teleporting %d %s into a coffin.' + print(string.format(strMove, item.id, itemName)) + end +end +function HaulToCoffin(tomb, coffin, item) + if not tomb or not coffin then return end + local itemName = item and dfhack.items.getReadableDescription(item) or nil + if item.flags.in_job then RemoveJob(item) end + local pos = utils.getBuildingCenter(coffin) local job = df.job:new() job.job_type = df.job_type.PlaceItemInTomb job.pos = pos - dfhack.job.attachJobItem(job, item, df.job_role_type.Hauled, -1, -1) dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, tomb.id) tomb.jobs:insert('#', job) - dfhack.job.linkIntoWorld(job, true) + local strMove = 'Tasking %d %s for immediate burial.' + print(string.format(strMove, item.id, itemName)) end -local function GetCoffin(tomb) +function GetCoffin(tomb) local coffin - if tomb.type == df.civzone_type.Tomb then + if df.building_civzonest:is_instance(tomb) and tomb.type == df.civzone_type.Tomb then for _, building in ipairs(tomb.contained_buildings) do if df.building_coffinst:is_instance(building) then coffin = building end end @@ -168,15 +187,15 @@ local function GetCoffin(tomb) return coffin end -function AssignToTomb(unit, tomb, forceBurial) +function AssignToTomb(unit, tomb, options) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to %s for burial.' local strTomb = 'a tomb zone' if #tomb.name > 0 then strTomb = tomb.name end local strCorpseItems = '(%d corpse or body part%s)' + local strPlural = '' local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) - local strPlural = '' local incident_id = unit.counters.death_id if incident_id ~= -1 then local incident = df.incident.find(incident_id) @@ -185,6 +204,7 @@ function AssignToTomb(unit, tomb, forceBurial) incident.flags.discovered = true end local burialItemCount = FlagForBurial(unit, corpseParts) + if burialItemCount > 1 then strPlural = 's' end if burialItemCount == 0 then print(string.format(strNoCorpse, strUnitName)) else @@ -193,28 +213,36 @@ function AssignToTomb(unit, tomb, forceBurial) unit.owned_buildings:insert('#', tomb) end print(string.format(strBurial, strUnitName, strTomb)) - if forceBurial then + print(string.format(strCorpseItems, burialItemCount, strPlural)) + if options.haulNow or options.teleport then local coffin = GetCoffin(tomb) if coffin then for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) - -- PutInCoffin(coffin, item) - HaulToCoffin(tomb, coffin, item) + if isMoveableItem(tomb, coffin, item, options) then + if options.teleport then + TeleportToCoffin(tomb, coffin, item) + elseif options.haulNow then + HaulToCoffin(tomb, coffin, item) + end + end end - print('Corpse items have been teleported into a coffin.') else - print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') + print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') end end - if burialItemCount > 1 then strPlural = 's' end - print(string.format(strCorpseItems, burialItemCount, strPlural)) end end -local function parseArgs(args) - local unit, tomb, forceBurial +local function ParseArgs(args) + local unit, tomb + local options = { + haulNow = false, + teleport = false + } if args and #args > 0 then for i, v in ipairs(args) do + if v == 'help' then print(dfhack.script_help()) return end if v == 'unit' then local unit_id = tonumber(args[i+1]) or nil unit = unit_id and df.unit.find(unit_id) @@ -231,22 +259,36 @@ local function parseArgs(args) qerror('Specified zone ID does not point to an unassigned tomb zone.') end end - if v == 'now' then forceBurial = true end + if v == 'now' then options.haulNow = true end + if v == 'teleport' then options.teleport = true end + if options.haulNow and options.teleport then + qerror('Burial items cannot be teleported and tasked for hauling simultaneously.') + end end end - return unit, tomb, forceBurial + return unit, tomb, options end local function Main(args) - local unit, tomb, forceBurial = parseArgs(args) - local entombed + if not dfhack.isSiteLoaded() and not dfhack.world.isFortressMode() then + qerror('This script requires the game to be in fortress mode.') + end + local unit, tomb, options = ParseArgs(args) if not unit then unit = GetUnitFromCorpse() end if unit then + local entombed if not tomb then tomb, entombed = GetTombZone(unit) end if entombed then print('Unit is already completely interred in a tomb zone.') elseif tomb then - AssignToTomb(unit, tomb, forceBurial) + -- Prevent multiple tomb zone assignments when tomb ID is specified in the command line. + -- Iterating through building.assigned_unit_id is probably safer than checking in + -- unit.owned_buildings, as a reference in one does not guarantee a reference in the other. + building = IterateTombZones(unit.id) + if building and tomb ~= building then + qerror('Unit already has an assigned tomb zone.') + end + AssignToTomb(unit, tomb, options) else print('No unassigned tomb zones are available.') end From 7952e132191f312996cbd0662720220622d3703c Mon Sep 17 00:00:00 2001 From: git--amade Date: Fri, 25 Jul 2025 15:51:26 +0800 Subject: [PATCH 10/17] Assign new name to unnamed tombs during assignment --- entomb.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/entomb.lua b/entomb.lua index a1e109661..f5a00e268 100644 --- a/entomb.lua +++ b/entomb.lua @@ -190,9 +190,16 @@ end function AssignToTomb(unit, tomb, options) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to %s for burial.' - local strTomb = 'a tomb zone' - if #tomb.name > 0 then strTomb = tomb.name end - local strCorpseItems = '(%d corpse or body part%s)' + local strTomb = 'Tomb %d' + -- Provide the tomb's ID so users can invoke it when interring arbitrary items. + strTomb = string.format(strTomb, tomb.id) + if #tomb.name > 0 then + strTomb = tomb.name + else + -- Assign name to unnamed tombs for easier search/reference. + tomb.name = strTomb + end + local strCorpseItems = '(%d corpse, body part%s, or burial item%s)' local strPlural = '' local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) @@ -213,7 +220,7 @@ function AssignToTomb(unit, tomb, options) unit.owned_buildings:insert('#', tomb) end print(string.format(strBurial, strUnitName, strTomb)) - print(string.format(strCorpseItems, burialItemCount, strPlural)) + print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) if options.haulNow or options.teleport then local coffin = GetCoffin(tomb) if coffin then From f484912b8efe47aa733b553186f4e7c3a108745d Mon Sep 17 00:00:00 2001 From: git--amade Date: Fri, 25 Jul 2025 17:20:08 +0800 Subject: [PATCH 11/17] Move logic to call HaulToCoffin() and TeleportToCoffin() into new function --- entomb.lua | 127 ++++++++++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 61 deletions(-) diff --git a/entomb.lua b/entomb.lua index f5a00e268..f9af94393 100644 --- a/entomb.lua +++ b/entomb.lua @@ -93,6 +93,56 @@ local function FlagForBurial(unit, corpseParts) return burialItemCount end +function AssignToTomb(unit, tomb) + local corpseParts = unit.corpse_parts + local strBurial = '%s assigned to %s for burial.' + local strTomb = 'Tomb %d' + -- Provide the tomb's ID so users can invoke it when interring arbitrary items. + strTomb = string.format(strTomb, tomb.id) + if #tomb.name > 0 then + strTomb = tomb.name + else + -- Assign name to unnamed tombs for easier search/reference. + tomb.name = strTomb + end + local strCorpseItems = '(%d corpse, body part%s, or burial item%s)' + local strPlural = '' + local strNoCorpse = '%s has no corpse or body parts available for burial.' + local strUnitName = unit and dfhack.units.getReadableName(unit) + local incident_id = unit.counters.death_id + if incident_id ~= -1 then + local incident = df.incident.find(incident_id) + -- Corpse will not be interred if not yet discovered, + -- which never happens for units not belonging to player's civ. + incident.flags.discovered = true + end + local burialItemCount = FlagForBurial(unit, corpseParts) + if burialItemCount > 1 then strPlural = 's' end + if burialItemCount == 0 then + print(string.format(strNoCorpse, strUnitName)) + else + tomb.assigned_unit_id = unit.id + if not utils.linear_index(unit.owned_buildings, tomb) then + unit.owned_buildings:insert('#', tomb) + end + print(string.format(strBurial, strUnitName, strTomb)) + print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) + end +end + +function GetCoffin(tomb) + local coffin + if df.building_civzonest:is_instance(tomb) and tomb.type == df.civzone_type.Tomb then + for _, building in ipairs(tomb.contained_buildings) do + if df.building_coffinst:is_instance(building) then coffin = building end + end + -- Allow other scripts to call this function and pass the actual coffin building instead. + elseif df.building_coffinst:is_instance(tomb) then + coffin = tomb + end + return coffin +end + -- Adapted from scripts/internal/caravan/pedestal.lua::is_displayable_item() -- Allow checks for possible use case of interring of non-corpse items. local function isMoveableItem(tomb, coffin, item, options) @@ -174,70 +224,22 @@ function HaulToCoffin(tomb, coffin, item) print(string.format(strMove, item.id, itemName)) end -function GetCoffin(tomb) - local coffin - if df.building_civzonest:is_instance(tomb) and tomb.type == df.civzone_type.Tomb then - for _, building in ipairs(tomb.contained_buildings) do - if df.building_coffinst:is_instance(building) then coffin = building end - end - -- Allow other scripts to call this function and pass the actual coffin building instead. - elseif df.building_coffinst:is_instance(tomb) then - coffin = tomb - end - return coffin -end - -function AssignToTomb(unit, tomb, options) +local function InterItems(tomb, unit, options) local corpseParts = unit.corpse_parts - local strBurial = '%s assigned to %s for burial.' - local strTomb = 'Tomb %d' - -- Provide the tomb's ID so users can invoke it when interring arbitrary items. - strTomb = string.format(strTomb, tomb.id) - if #tomb.name > 0 then - strTomb = tomb.name - else - -- Assign name to unnamed tombs for easier search/reference. - tomb.name = strTomb - end - local strCorpseItems = '(%d corpse, body part%s, or burial item%s)' - local strPlural = '' - local strNoCorpse = '%s has no corpse or body parts available for burial.' - local strUnitName = unit and dfhack.units.getReadableName(unit) - local incident_id = unit.counters.death_id - if incident_id ~= -1 then - local incident = df.incident.find(incident_id) - -- Corpse will not be interred if not yet discovered, - -- which never happens for units not belonging to player's civ. - incident.flags.discovered = true - end - local burialItemCount = FlagForBurial(unit, corpseParts) - if burialItemCount > 1 then strPlural = 's' end - if burialItemCount == 0 then - print(string.format(strNoCorpse, strUnitName)) - else - tomb.assigned_unit_id = unit.id - if not utils.linear_index(unit.owned_buildings, tomb) then - unit.owned_buildings:insert('#', tomb) - end - print(string.format(strBurial, strUnitName, strTomb)) - print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) - if options.haulNow or options.teleport then - local coffin = GetCoffin(tomb) - if coffin then - for _, item_id in ipairs(corpseParts) do - local item = df.item.find(item_id) - if isMoveableItem(tomb, coffin, item, options) then - if options.teleport then - TeleportToCoffin(tomb, coffin, item) - elseif options.haulNow then - HaulToCoffin(tomb, coffin, item) - end - end + local coffin = GetCoffin(tomb) + if coffin then + for _, item_id in ipairs(corpseParts) do + local item = df.item.find(item_id) + if isMoveableItem(tomb, coffin, item, options) then + if options.teleport then + TeleportToCoffin(tomb, coffin, item) + elseif options.haulNow then + HaulToCoffin(tomb, coffin, item) end - else - print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') end end + else + print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') end end @@ -295,10 +297,13 @@ local function Main(args) if building and tomb ~= building then qerror('Unit already has an assigned tomb zone.') end - AssignToTomb(unit, tomb, options) + AssignToTomb(unit, tomb) else print('No unassigned tomb zones are available.') end + if options.haulNow or options.teleport then + InterItems(tomb, unit, options) + end else qerror('No item selected or unit specified.') end From ba5e5d151555d51599d741a12f29efb4a6673ef4 Mon Sep 17 00:00:00 2001 From: git--amade Date: Mon, 28 Jul 2025 18:21:45 +0800 Subject: [PATCH 12/17] Implement add-item option, revise Main() logic, switch to use argparse module --- entomb.lua | 313 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 232 insertions(+), 81 deletions(-) diff --git a/entomb.lua b/entomb.lua index f9af94393..fa40d7960 100644 --- a/entomb.lua +++ b/entomb.lua @@ -1,8 +1,31 @@ -- Entomb corpse items of any dead unit. --@module = true + +local argparse = require('argparse') local utils = require('utils') +local guidm = require('gui.dwarfmode') --- Get unit from selected corpse or corpse piece item. +-- Check if any of the unit's corpse items are not yet placed in a coffin. +function isEntombed(unit) + -- Return FALSE for still living or undead units with empty corpse_parts vector. + if #unit.corpse_parts == 0 then return false end + for _, item_id in ipairs(unit.corpse_parts) do + local item = df.item.find(item_id) + if item then + local inBuilding = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) + local building_id = inBuilding and inBuilding.building_id or -1 + local building = df.building.find(building_id) + local isCoffin = (building and df.building_coffinst:is_instance(building)) or false + -- Return FALSE if even one item is not interred. + if not isCoffin then + return false + end + end + end + return true +end + +-- Get unit from selected corpse or body part item. function GetUnitFromCorpse(item) if math.type(item) == "integer" then item = df.item.find(item) elseif not item then item = dfhack.gui.getSelectedItem(true) end @@ -10,9 +33,10 @@ function GetUnitFromCorpse(item) if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then return df.unit.find(item.unit_id) else - qerror('Item is not a corpse or body part.') + qerror('Selected item is not a corpse or body part.') end end + return nil end -- Validate tomb zone assignment. @@ -33,43 +57,39 @@ local function IterateTombZones(unit_id) return nil end --- Check if any of the unit's corpse items are not yet placed in a coffin. -function isEntombed(unit) - -- Return FALSE for still living or undead units with empty corpse_parts vector. - if #unit.corpse_parts == 0 then return false end - for _, item_id in ipairs(unit.corpse_parts) do - local item = df.item.find(item_id) - if item then - local inBuilding = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) - local building_id = inBuilding and inBuilding.building_id or -1 - local building = df.building.find(building_id) - local isCoffin = (building and df.building_coffinst:is_instance(building)) or false - -- Return FALSE if even one item is not interred. - if not isCoffin then - return false +-- Use when user inputs coffin building ID instead of tomb zone ID. +function GetTombFromCoffin(building) + if #building.relations > 0 then + for _, v in ipairs(building.relations) do + if df.building_civzonest:is_instance(v) and v.type == df.civzone_type.Tomb then + return v end end end - return true + return nil end -local function GetTombZone(unit) - local unit_id = unit.id - local tomb - local entombed = false - -- Check if unit is already assigned to a tomb zone. - local isAlreadyAssigned = IterateTombZones(unit_id) - if isAlreadyAssigned then - tomb = isAlreadyAssigned - entombed = isEntombed(unit) +function GetTombFromZone(building) + if df.building_civzonest:is_instance(building) and building.type == df.civzone_type.Tomb then + return building + elseif df.building_coffinst:is_instance(building) then + return GetTombFromCoffin(building) + end + return nil +end + +function GetTombFromUnit(unit) + -- Check if unit already has a tomb zone assigned. + local alreadyAssignedTomb = unit and IterateTombZones(unit.id) + if alreadyAssignedTomb then + return alreadyAssignedTomb else - -- Find an unassigned tomb zone. - tomb = IterateTombZones(-1) + -- Get an unassigned tomb zone. + return IterateTombZones(-1) end - return tomb, entombed end --- Set corpse items to be valid for burial. +-- Set unit's corpse items to be valid for burial. local function FlagForBurial(unit, corpseParts) -- Undead units have empty corpse_parts vector. if unit.enemy.undead then @@ -97,7 +117,7 @@ function AssignToTomb(unit, tomb) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to %s for burial.' local strTomb = 'Tomb %d' - -- Provide the tomb's ID so users can invoke it when interring arbitrary items. + -- Provide the tomb's ID so the user can invoke it when interring arbitrary items. strTomb = string.format(strTomb, tomb.id) if #tomb.name > 0 then strTomb = tomb.name @@ -144,9 +164,10 @@ function GetCoffin(tomb) end -- Adapted from scripts/internal/caravan/pedestal.lua::is_displayable_item() --- Allow checks for possible use case of interring of non-corpse items. +-- Allow checks for possible use case of interring arbitrary items. local function isMoveableItem(tomb, coffin, item, options) if not item or + -- Allow forbid/dump/melt designated items to be valid. item.flags.hostile or item.flags.removed or item.flags.spider_web or @@ -154,10 +175,15 @@ local function isMoveableItem(tomb, coffin, item, options) item.flags.encased or item.flags.trader or item.flags.owned or + item.flags.garbage_collect or item.flags.on_fire then return false end + -- Allow user to exclude items by forbidding when adding arbitrary items. + if options.addItem and item.flags.forbid then + return false + end if item.flags.in_job then local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) local job = inJob and inJob.data.job or nil @@ -189,6 +215,81 @@ local function isMoveableItem(tomb, coffin, item, options) return true end +function isAlreadyBurialItem(unit, item) + -- Prevent duplicating unit's own corpse parts in corpse_parts. + for _, v in ipairs(unit.corpse_parts) do + if item.id == v then return true end + end + -- Prevent adding burial items belonging to other units with an assigned tomb. + for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do + if not CheckTombZone(building, -1) then + local otherUnit = df.unit.find(building.assigned_unit_id) + for _, v in ipairs(otherUnit.corpse_parts) do + if item.id == v then return true end + end + end + end + return false +end + +-- Set additional arbitrary items to be valid for burial. +function AddBurialItems(unit, tomb, options) + local coffin = GetCoffin(tomb) + local item = dfhack.gui.getSelectedItem(true) + local cursor = guidm.getCursorPos() + local burialItems = {} + local strAddItem = 'Adding %s for burial with unit.' + local strItemName + local strCannotInter = 'Unable to inter additional item(s);\n ...%s.' + local strNoCoffin = 'no coffin in assigned tomb zone' + local strNotValidItem = 'selected item is not valid for burial' + local strNoCursorItems = 'no items at cursor are valid for burial' + local strNoSelect = 'no item selected and keyboard cursor not enabled' + if not coffin then + print(string.format(strCannotInter, strNoCoffin)) + elseif item then + if isMoveableItem(tomb, coffin, item, options) and + not isAlreadyBurialItem(unit, item) + then + strItemName = item and dfhack.items.getReadableDescription(item) or nil + print(string.format(strAddItem, strItemName)) + table.insert(burialItems, item) + else + print(string.format(strCannotInter, strNotValidItem)) + end + -- Use keyboard cursor to set multiple items for burial. + elseif cursor then + -- Filter items to iterate according to tile block at cursor. + local block = dfhack.maps.getTileBlock(cursor) + for _, blockItem_id in ipairs(block.items) do + local blockItem = df.item.find(blockItem_id) + local x, y, _ = dfhack.items.getPosition(blockItem) + if x == cursor.x and y == cursor.y then + item = blockItem + if isMoveableItem(tomb, coffin, item, options) and + not isAlreadyBurialItem(unit, item) + then + strItemName = item and dfhack.items.getReadableDescription(item) or nil + print(string.format(strAddItem, strItemName)) + table.insert(burialItems, item) + end + end + end + if #burialItems == 0 then + print(string.format(strCannotInter, strNoCursorItems)) + end + else + print(string.format(strCannotInter, strNoSelect)) + end + if #burialItems > 0 then + local corpseParts = unit.corpse_parts + for _, burialItem in ipairs(burialItems) do + burialItem.flags.dead_dwarf = true + corpseParts:insert('#', burialItem.id) + end + end +end + -- Remove job from item to allow for hauling or teleportation. local function RemoveJob(item) local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) @@ -239,73 +340,123 @@ local function InterItems(tomb, unit, options) end end else - print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') + print('Unable to move burial item(s);\n ...no coffin in assigned tomb zone.') end end -local function ParseArgs(args) - local unit, tomb - local options = { - haulNow = false, - teleport = false - } - if args and #args > 0 then - for i, v in ipairs(args) do - if v == 'help' then print(dfhack.script_help()) return end - if v == 'unit' then - local unit_id = tonumber(args[i+1]) or nil - unit = unit_id and df.unit.find(unit_id) - if not unit then qerror('Invalid unit ID.') end +-- Process unit and tomb before executing operations. +local function PreOpProcess(unit, building, options) + local tomb = building and GetTombFromZone(building) + local entombed = false + if not options.addItem then + if not unit then + unit = GetUnitFromCorpse() + end + if not tomb then + tomb = GetTombFromUnit(unit) + end + if unit and tomb then + -- Unit has a tomb, but it's not the specified tomb. + if IterateTombZones(unit.id) and tomb ~= IterateTombZones(unit.id) then + qerror('Unit already has an assigned tomb zone.') + -- Specified tomb is not assigned to unit, and specified tomb is not unassigned. + elseif not CheckTombZone(tomb, unit.id) and not CheckTombZone(tomb, -1) then + qerror('Specified tomb zone is already assigned to a different unit.') end - if v == 'tomb' then - local building_id = tonumber(args[i+1]) or nil - local building = building_id and df.building.find(building_id) - if not building then qerror('Invalid zone ID.') end - -- Check if tomb zone is unassigned. - if CheckTombZone(building, -1) then - tomb = building - else - qerror('Specified zone ID does not point to an unassigned tomb zone.') - end + end + if unit then + if not tomb then + qerror('No unassigned tomb zones are available.') + end + entombed = isEntombed(unit) + else + qerror('No item selected or unit specified.') + end + else + -- Either a unit or an assigned tomb zone must be specified when add-item is called, + -- as corpse/body part items cannot be used to assign tomb zones with this option. + local strCannotInter = 'Unable to inter additional item(s);\n ...%s.' + local strNoUnit = 'specified tomb zone is not assigned to a unit' + local strNoTomb = 'specified unit has no assigned tomb zone' + local strWrongPair = 'specified tomb zone is not assigned to specified unit' + local strNotSpecified = 'no assigned tomb zone or unit with assigned tomb zone specified' + if tomb and not unit then + if tomb.assigned_unit_id == -1 then + qerror(string.format(strCannotInter, strNoUnit)) end - if v == 'now' then options.haulNow = true end - if v == 'teleport' then options.teleport = true end - if options.haulNow and options.teleport then - qerror('Burial items cannot be teleported and tasked for hauling simultaneously.') + unit = df.unit.find(tomb.assigned_unit_id) + if not unit then + qerror(string.format(strCannotInter, strNoUnit)) end + elseif unit and not tomb then + tomb = GetTombFromUnit(unit) + if not tomb then + -- Equivalent to having no available unassigned tomb zones, + -- but emphasize on unit having no assigned tomb. + qerror(string.format(strCannotInter, strNoTomb)) + end + elseif tomb and unit then + if not CheckTombZone(tomb, unit.id) and not CheckTombZone(tomb, -1) then + qerror(string.format(strCannotInter, strWrongPair)) + end + else + qerror(string.format(strCannotInter, strNotSpecified)) end end - return unit, tomb, options + return unit, tomb, entombed +end + +local function ParseCommandLine(args) + local unit, building + local options = { + help = false, + addItem = false, + haulNow = false, + teleport = false + } + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler = function() options.help = true end}, + {'u', 'unit', hasArg = true, handler = function(arg) + local unit_id = argparse.positiveInt(arg, 'unit') + unit = unit_id and df.unit.find(unit_id) + if not unit then qerror('Invalid unit ID.') end end + }, + {'t', 'tomb', hasArg = true, handler = function(arg) + local building_id = argparse.positiveInt(arg, 'tomb') + building = building_id and df.building.find(building_id) + if not building then qerror('Invalid zone ID.') end end + }, + {'a', 'add-item', handler = function() options.addItem = true end}, + {'h', 'haul-now', handler = function() options.haulNow = true end}, + {'', 'teleport', handler = function() options.teleport = true end} + }) + return unit, building, options end local function Main(args) if not dfhack.isSiteLoaded() and not dfhack.world.isFortressMode() then qerror('This script requires the game to be in fortress mode.') end - local unit, tomb, options = ParseArgs(args) - if not unit then unit = GetUnitFromCorpse() end - if unit then - local entombed - if not tomb then tomb, entombed = GetTombZone(unit) end - if entombed then - print('Unit is already completely interred in a tomb zone.') - elseif tomb then - -- Prevent multiple tomb zone assignments when tomb ID is specified in the command line. - -- Iterating through building.assigned_unit_id is probably safer than checking in - -- unit.owned_buildings, as a reference in one does not guarantee a reference in the other. - building = IterateTombZones(unit.id) - if building and tomb ~= building then - qerror('Unit already has an assigned tomb zone.') - end - AssignToTomb(unit, tomb) - else - print('No unassigned tomb zones are available.') + local unit, building, options = ParseCommandLine(args) + if args == 'help' or options.help then + print(dfhack.script_help()) + return + end + if options.haulNow and options.teleport then + qerror('Burial items cannot be teleported and tasked for hauling simultaneously.') + end + local tomb, entombed + unit, tomb, entombed = PreOpProcess(unit, building, options) + if entombed then + print('Unit is already completely interred in a tomb zone.') + elseif unit and tomb then + AssignToTomb(unit, tomb) + if options.addItem then + AddBurialItems(unit, tomb, options) end if options.haulNow or options.teleport then InterItems(tomb, unit, options) end - else - qerror('No item selected or unit specified.') end end From e11d9680a59f761224ccf5787fc2cd258ddb08de Mon Sep 17 00:00:00 2001 From: git--amade Date: Mon, 28 Jul 2025 20:14:04 +0800 Subject: [PATCH 13/17] Disable teleport function, update documentation --- docs/entomb.rst | 51 +++++++++++++++++++++++++++---------------------- entomb.lua | 3 ++- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/docs/entomb.rst b/docs/entomb.rst index c519ff7bc..64f708a3d 100644 --- a/docs/entomb.rst +++ b/docs/entomb.rst @@ -5,7 +5,7 @@ entomb :summary: Entomb any corpse into tomb zones. :tags: fort items buildings -Assign any corpse regardless of citizenship, residency, pet status, +Assign any unit regardless of citizenship, residency, pet status, or affiliation to an unassigned tomb zone for burial. Usage @@ -13,20 +13,20 @@ Usage ``entomb []`` -This script must be executed with either a unit's corpse or body part -selected or with a unit ID specified. An unassigned tomb zone will then -be assigned to the unit for burial and all its corpse and/or body parts -will become valid items for interment. +Select a unit's corpse or body part, or specify the unit's ID +when executing this script to assign an unassigned tomb zone to +the unit, and flag the unit's corpse as well as any severed body +parts to become valid items for interment. -Optionally, the zone ID may also be specified to assign a specific tomb +Optionally, specify the tomb zone's ID to assign a specific tomb zone to the unit. -A non-citizen, non-resident, or non-pet unit that is still alive may -even be assigned a tomb zone if they have lost any body part that can -be placed inside a tomb, e.g. teeth or severed limbs. New corpse items -after a tomb has already been assigned will not be properly interred -until the script is executed again on either the unit, its corpse, or -any of its body parts. +A non-citizen, non-resident, or non-pet unit that is still alive +may even be assigned a tomb zone if they have lost any body part +that can be placed inside a tomb, e.g. teeth or severed limbs. +New corpse items after a tomb has already been assigned will not +be properly interred until the script is executed again with the +unit ID specified, or the unit's corpse or any body part selected. If executed on slaughtered animals, all its butchering returns will become valid burial items and no longer usable for cooking or crafting. @@ -34,28 +34,33 @@ become valid burial items and no longer usable for cooking or crafting. Examples -------- -``entomb unit `` +``entomb --unit `` Assign an unassigned tomb zone to the unit with the specified ID. -``entomb tomb `` +``entomb --tomb `` Assign a tomb zone with the specified ID to the selected corpse item's unit. -``entomb unit tomb now`` +``entomb -u -t -h`` Assign a tomb zone with the specified ID to the unit with the - specified ID and teleport its corpse and/or body parts into the - coffin in the tomb zone. + specified ID and task all its burial items for simultaneous + hauling into the coffin in the tomb zone. Options ------- -``unit `` +``-u``, ``--unit `` Specify the ID of the unit to be assigned to a tomb zone. -``tomb `` +``-t``, ``--tomb `` Specify the ID of the zone into which a unit will be interred. -``now`` - Instantly teleport the unit's corpse and/or body parts into the - coffin of its assigned tomb zone. This option can be called on - corpse items or units that are already assigned a tomb zone. +``-a``, ``add-item`` + Add a selected item, or multiple items at the keyboard cursor's + position to be interred together with a unit. A unit or tomb + zone ID must be specified when calling this option. + +``-h``, ``haul-now`` + Task all of the unit's burial items for simultaneous hauling + into the coffin of its assigned tomb zone. This option can be + called even after a tomb zone is already assigned to the unit. diff --git a/entomb.lua b/entomb.lua index fa40d7960..2160a77fd 100644 --- a/entomb.lua +++ b/entomb.lua @@ -428,7 +428,8 @@ local function ParseCommandLine(args) }, {'a', 'add-item', handler = function() options.addItem = true end}, {'h', 'haul-now', handler = function() options.haulNow = true end}, - {'', 'teleport', handler = function() options.teleport = true end} + -- Commenting out to make this script a non-Armok tool. + -- {'', 'teleport', handler = function() options.teleport = true end} }) return unit, building, options end From 8f4feca6f6fce604fb8d1ea5baaac91ce2225bde Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 30 Jul 2025 03:43:57 +0800 Subject: [PATCH 14/17] Fix argparse short-form conflict --- docs/entomb.rst | 2 +- entomb.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/entomb.rst b/docs/entomb.rst index 64f708a3d..cef602519 100644 --- a/docs/entomb.rst +++ b/docs/entomb.rst @@ -60,7 +60,7 @@ Options position to be interred together with a unit. A unit or tomb zone ID must be specified when calling this option. -``-h``, ``haul-now`` +``-n``, ``haul-now`` Task all of the unit's burial items for simultaneous hauling into the coffin of its assigned tomb zone. This option can be called even after a tomb zone is already assigned to the unit. diff --git a/entomb.lua b/entomb.lua index 2160a77fd..dd4f22ca9 100644 --- a/entomb.lua +++ b/entomb.lua @@ -427,7 +427,7 @@ local function ParseCommandLine(args) if not building then qerror('Invalid zone ID.') end end }, {'a', 'add-item', handler = function() options.addItem = true end}, - {'h', 'haul-now', handler = function() options.haulNow = true end}, + {'n', 'haul-now', handler = function() options.haulNow = true end}, -- Commenting out to make this script a non-Armok tool. -- {'', 'teleport', handler = function() options.teleport = true end} }) From 785741bf8043d1148eac8d02ac4b4ca04fe7afd6 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 30 Jul 2025 03:46:38 +0800 Subject: [PATCH 15/17] Fix documentation --- docs/entomb.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/entomb.rst b/docs/entomb.rst index cef602519..1352b990e 100644 --- a/docs/entomb.rst +++ b/docs/entomb.rst @@ -55,12 +55,12 @@ Options ``-t``, ``--tomb `` Specify the ID of the zone into which a unit will be interred. -``-a``, ``add-item`` +``-a``, ``--add-item`` Add a selected item, or multiple items at the keyboard cursor's position to be interred together with a unit. A unit or tomb zone ID must be specified when calling this option. -``-n``, ``haul-now`` +``-n``, ``--haul-now`` Task all of the unit's burial items for simultaneous hauling into the coffin of its assigned tomb zone. This option can be called even after a tomb zone is already assigned to the unit. From 062ef18de351faf25a471ab1d8e562d9515b3cb7 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 13 Aug 2025 11:42:50 +0800 Subject: [PATCH 16/17] Assign only active tomb zones and make it unavailable for auto assigment to other units --- entomb.lua | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/entomb.lua b/entomb.lua index dd4f22ca9..735b2d65f 100644 --- a/entomb.lua +++ b/entomb.lua @@ -52,7 +52,14 @@ end -- Iterate through all available tomb zones. local function IterateTombZones(unit_id) for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do - if CheckTombZone(building, unit_id) then return building end + if unit_id == -1 then + -- Use only active (unpaused) zones when assigning unassigned tomb zones. + if building.spec_sub_flag.active then + if CheckTombZone(building, unit_id) then return building end + end + else + if CheckTombZone(building, unit_id) then return building end + end end return nil end @@ -60,9 +67,9 @@ end -- Use when user inputs coffin building ID instead of tomb zone ID. function GetTombFromCoffin(building) if #building.relations > 0 then - for _, v in ipairs(building.relations) do - if df.building_civzonest:is_instance(v) and v.type == df.civzone_type.Tomb then - return v + for _, zone in ipairs(building.relations) do + if df.building_civzonest:is_instance(zone) and zone.type == df.civzone_type.Tomb then + return zone end end end @@ -134,6 +141,7 @@ function AssignToTomb(unit, tomb) local incident = df.incident.find(incident_id) -- Corpse will not be interred if not yet discovered, -- which never happens for units not belonging to player's civ. + -- Only needed for units that have a death incident. incident.flags.discovered = true end local burialItemCount = FlagForBurial(unit, corpseParts) @@ -145,6 +153,9 @@ function AssignToTomb(unit, tomb) if not utils.linear_index(unit.owned_buildings, tomb) then unit.owned_buildings:insert('#', tomb) end + -- Make tomb zone unavailable for automatic assignment to other dead units. + tomb.zone_settings.tomb.flags.no_pets = true + tomb.zone_settings.tomb.flags.no_citizens = true print(string.format(strBurial, strUnitName, strTomb)) print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) end From 5f1c7b7658dc6f7c1902077e5ac416f13e711477 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 13 Aug 2025 13:32:38 +0800 Subject: [PATCH 17/17] Remove duplicated code in IterateTombZones() --- entomb.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/entomb.lua b/entomb.lua index 735b2d65f..0455f6b71 100644 --- a/entomb.lua +++ b/entomb.lua @@ -52,14 +52,10 @@ end -- Iterate through all available tomb zones. local function IterateTombZones(unit_id) for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do - if unit_id == -1 then - -- Use only active (unpaused) zones when assigning unassigned tomb zones. - if building.spec_sub_flag.active then - if CheckTombZone(building, unit_id) then return building end - end - else - if CheckTombZone(building, unit_id) then return building end - end + -- Use only active (unpaused) zones when assigning unassigned tomb zones. + if unit_id == -1 and not building.spec_sub_flag.active then goto skipIteration end + if CheckTombZone(building, unit_id) then return building end + ::skipIteration:: end return nil end