diff --git a/docs/emigration.rst b/docs/emigration.rst index 58a3d623e7..9d8c0503c6 100644 --- a/docs/emigration.rst +++ b/docs/emigration.rst @@ -16,9 +16,44 @@ even in the company of a visiting elven bard! The check is made monthly. A happy dwarf (i.e. with negative stress) will never emigrate. +The tool also supports ``nobles``, a manually-invoked command that makes nobles +emigrate to their rightful land of rule. No more freeloaders making inane demands! +Nobles assigned to squads or to fort administrator positions will not be emigrated. +Remove their assignments before retrying. Nobles holding elected positions +(i.e. mayors) may be emigrated, but will have a ``*`` icon when listed. + Usage ----- :: enable emigration + emigration nobles [--list] + emigration nobles [] + +Examples +-------- + +``emigration nobles`` + Emigrate the selected noble if it does not rule your fortress. + If no unit is selected, list all nobles that do not rule your fortress. +``emigration nobles --list`` + List all nobles that do not rule your fortress. Nobles that cannot be emigrated + (see above) will have a ``!`` indicator while nobles holding elected positions + will have a ``*`` indicator. +``emigration nobles --all`` + Emigrate all nobles that do not rule your fortress. +``emigration nobles --unit 34534`` + Emigrate a noble matching the specified unit ID that does not rule your fortress. + +Options +------- + +These options are exclusive to the ``emigration nobles`` command. + +``-l``, ``--list`` + List all nobles that do not rule your fortress +``-a``, ``--all`` + Emigrate all nobles do not rule your fortress +``-u``, ``--unit `` + Emigrate noble matching specified unit ID that does not rule your fortress diff --git a/emigration.lua b/emigration.lua index ea494a7e36..1bba9810e1 100644 --- a/emigration.lua +++ b/emigration.lua @@ -3,6 +3,9 @@ local utils = require('utils') +local nobles = reqscript('internal/emigration/emigrate-nobles') +local unit_link_utils = reqscript('internal/emigration/unit-link-utils') + local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence local function get_default_state() @@ -37,121 +40,40 @@ function desert(u,method,civ) local line = dfhack.units.getReadableName(u) .. " has " if method == 'merchant' then line = line.."joined the merchants" - u.flags1.merchant = true - u.civ_id = civ + unit_link_utils.markUnitForEmigration(u, civ, false) else line = line.."abandoned the settlement in search of a better life." - u.civ_id = civ - u.flags1.forest = true - u.flags2.visitor = true - u.animal.leave_countdown = 2 + unit_link_utils.markUnitForEmigration(u, civ, true) end - local hf_id = u.hist_figure_id + local hf = df.historical_figure.find(u.hist_figure_id) local fort_ent = df.global.plotinfo.main.fortress_entity local civ_ent = df.historical_entity.find(hf.civ_id) local newent_id = -1 local newsite_id = -1 - -- free owned rooms - for i = #u.owned_buildings-1, 0, -1 do - local temp_bld = df.building.find(u.owned_buildings[i].id) - dfhack.buildings.setOwner(temp_bld, nil) - end - - -- remove from workshop profiles - for _, bld in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do - for k, v in ipairs(bld.profile.permitted_workers) do - if v == u.id then - bld.profile.permitted_workers:erase(k) - break - end - end - end - for _, bld in ipairs(df.global.world.buildings.other.FURNACE_ANY) do - for k, v in ipairs(bld.profile.permitted_workers) do - if v == u.id then - bld.profile.permitted_workers:erase(k) - break - end - end - end - - -- disassociate from work details - for _, detail in ipairs(df.global.plotinfo.labor_info.work_details) do - for k, v in ipairs(detail.assigned_units) do - if v == u.id then - detail.assigned_units:erase(k) - break - end - end - end - - -- unburrow - for _, burrow in ipairs(df.global.plotinfo.burrows.list) do - dfhack.burrows.setAssignedUnit(burrow, u, false) - end - - -- erase the unit from the fortress entity - for k,v in ipairs(fort_ent.histfig_ids) do - if v == hf_id then - df.global.plotinfo.main.fortress_entity.histfig_ids:erase(k) - break - end - end - for k,v in ipairs(fort_ent.hist_figures) do - if v.id == hf_id then - df.global.plotinfo.main.fortress_entity.hist_figures:erase(k) - break - end - end - for k,v in ipairs(fort_ent.nemesis) do - if v.figure.id == hf_id then - df.global.plotinfo.main.fortress_entity.nemesis:erase(k) - df.global.plotinfo.main.fortress_entity.nemesis_ids:erase(k) - break - end - end - - -- remove the old entity link and create new one to indicate former membership - hf.entity_links:insert("#", {new = df.histfig_entity_link_former_memberst, entity_id = fort_ent.id, link_strength = 100}) - for k,v in ipairs(hf.entity_links) do - if v._type == df.histfig_entity_link_memberst and v.entity_id == fort_ent.id then - hf.entity_links:erase(k) - break - end - end + unit_link_utils.removeUnitAssociations(u) + unit_link_utils.removeHistFigFromEntity(hf, fort_ent) -- try to find a new entity for the unit to join - for k,v in ipairs(civ_ent.entity_links) do - if v.type == df.entity_entity_link_type.CHILD and v.target ~= fort_ent.id then - newent_id = v.target + for _,entity_link in ipairs(civ_ent.entity_links) do + if entity_link.type == df.entity_entity_link_type.CHILD and entity_link.target ~= fort_ent.id then + newent_id = entity_link.target break end end if newent_id > -1 then - hf.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newent_id, link_strength = 100}) - -- try to find a new site for the unit to join - for k,v in ipairs(df.global.world.entities.all[hf.civ_id].site_links) do + for _,site_link in ipairs(df.global.world.entities.all[hf.civ_id].site_links) do local site_id = df.global.plotinfo.site_id - if v.type == df.entity_site_link_type.Claim and v.target ~= site_id then - newsite_id = v.target + if site_link.type == df.entity_site_link_type.Claim and site_link.target ~= site_id then + newsite_id = site_link.target break end end local newent = df.historical_entity.find(newent_id) - newent.histfig_ids:insert('#', hf_id) - newent.hist_figures:insert('#', hf) - local hf_event_id = df.global.hist_event_next_id - df.global.hist_event_next_id = df.global.hist_event_next_id+1 - df.global.world.history.events:insert("#", {new = df.history_event_add_hf_entity_linkst, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, civ = newent_id, histfig = hf_id, link_type = 0}) - if newsite_id > -1 then - local hf_event_id = df.global.hist_event_next_id - df.global.hist_event_next_id = df.global.hist_event_next_id+1 - df.global.world.history.events:insert("#", {new = df.history_event_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, hfid = hf_id, state = 1, reason = -1, site = newsite_id}) - end + unit_link_utils.addHistFigToSite(hf, newsite_id, newent) end print(dfhack.df2console(line)) dfhack.gui.showAnnouncement(line, COLOR_WHITE) @@ -251,6 +173,9 @@ if args[1] == "enable" then state.enabled = true elseif args[1] == "disable" then state.enabled = false +elseif args[1] == "nobles" then + table.remove(args, 1) + nobles.run(args) else print('emigration is ' .. (state.enabled and 'enabled' or 'not enabled')) return diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua new file mode 100644 index 0000000000..f4ef605de0 --- /dev/null +++ b/internal/emigration/emigrate-nobles.lua @@ -0,0 +1,346 @@ +--@module = true + +--[[ +TODO: + * Feature: have rightful ruler immigrate to fort if off-site +]]-- + +local argparse = require("argparse") + +local unit_link_utils = reqscript("internal/emigration/unit-link-utils") + +local options = { + all = false, + unitId = -1, + list = false +} + +-- adapted from Units::get_land_title() +---@return df.world_site|nil +local function findSiteOfRule(np) + local site = nil + local civ = np.entity -- lawmakers seem to be all civ-level positions + for _, link in ipairs(civ.site_links) do + if not link.flags.land_for_holding then goto continue end + if link.position_profile_id ~= np.assignment.id then goto continue end + + site = df.world_site.find(link.target) + break + ::continue:: + end + + return site +end + +---@return df.world_site|nil +local function findCapital(civ) + local civCapital = nil + for _, link in ipairs(civ.site_links) do + if link.flags.capital then + civCapital = df.world_site.find(link.target) + break + end + end + + return civCapital +end + +---@param unit df.unit +---@param nobleList { unit: df.unit, site: df.world_site, id: number }[] +---@param thisSite df.world_site +---@param civ df.historical_entity +local function addNobleOfOtherSite(unit, nobleList, thisSite, civ) + local nps = dfhack.units.getNoblePositions(unit) or {} + local noblePos = nil + for _, np in ipairs(nps) do + if np.position.flags.IS_LAW_MAKER then + noblePos = np + break + end + end + + if not noblePos then return end -- unit is not nobility + + -- Monarchs do not seem to have an world_site associated to them (?) + if noblePos.position.flags.RULES_FROM_LOCATION and noblePos.entity.id == civ.id then + local capital = findCapital(civ) + if capital and capital.id ~= thisSite.id then + table.insert(nobleList, {unit = unit, site = capital, id = noblePos.assignment.id}) + end + return + end + + local name = dfhack.units.getReadableName(unit) + -- Logic for dukes, counts, barons + local site = findSiteOfRule(noblePos) + if not site then qerror("could not find land of "..name) end + + if site.id == thisSite.id then return end -- noble rules current fort + table.insert(nobleList, {unit = unit, site = site, id = noblePos.assignment.id}) +end + +---@param unit df.unit +local function removeMandates(unit) + local mandates = df.global.world.mandates.all + for i=#mandates-1,0,-1 do + local mandate = mandates[i] + if mandate.unit and mandate.unit.id == unit.id then + mandates:erase(i) + mandate:delete() + end + end +end + +-- adapted from emigration::desert() +---@param unit df.unit +---@param toSite df.world_site +---@param prevEnt df.historical_entity +---@param civ df.historical_entity +---@param removeMayor boolean +local function emigrate(unit, toSite, prevEnt, civ, removeMayor) + local histFig = df.historical_figure.find(unit.hist_figure_id) + if not histFig then + print("Could not find associated historical figure!") + return + end + + unit_link_utils.markUnitForEmigration(unit, civ.id, true) + + -- remove current job + if unit.job.current_job then dfhack.job.removeJob(unit.job.current_job) end + + -- break up any social activities + for _, actId in ipairs(unit.social_activities) do + local act = df.activity_entry.find(actId) + if act then act.events[0].flags.dismissed = true end + end + + -- cancel any associated mandates + removeMandates(unit) + + unit_link_utils.removeUnitAssociations(unit) + unit_link_utils.removeHistFigFromEntity(histFig, prevEnt, removeMayor) + + -- have unit join new site government + local siteGov = df.historical_entity.find(toSite.cur_owner_id) + if not siteGov then qerror("could not find entity associated with new site") end + unit_link_utils.addHistFigToSite(histFig, toSite.id, siteGov) + + -- announce the changes + local unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) + local siteName = dfhack.df2console(dfhack.translation.translateName(toSite.name, true)) + local govName = dfhack.df2console(dfhack.translation.translateName(siteGov.name, true)) + local line = unitName .. " has left to join " ..govName.. " as lord of " .. siteName .. "." + print("+ "..dfhack.df2console(line)) + dfhack.gui.showAnnouncement(line, COLOR_WHITE) +end + +------------------------ +-- [[ GUARD CHECKS ]] -- +------------------------ + +---@param unit df.unit +local function inSpecialJob(unit) + local job = unit.job.current_job + if not job then return false end + + if job.flags.special then return true end -- cannot cancel + + local jobType = job.job_type -- taken from notifications::for_moody() + return df.job_type_class[df.job_type.attrs[jobType].type] == 'StrangeMood' +end + +---@param unit df.unit +local function isSoldier(unit) + return unit.military.squad_id ~= -1 +end + +-- just an enum +local AdminType = { + NOT_ADMIN = { sym = " " }, + IS_ELECTED = { sym = "*" }, + IS_ADMIN = { sym = "!" } +} + +---@param unit df.unit +---@param fortEnt df.historical_entity +local function getAdminType(unit, fortEnt) + ---@diagnostic disable-next-line: missing-parameter + local nps = dfhack.units.getNoblePositions(unit) or {} + local result = AdminType.NOT_ADMIN + + ---@diagnostic disable-next-line: param-type-mismatch + for _, np in ipairs(nps) do + if np.entity.id ~= fortEnt.id then goto continue end + if np.position.flags.ELECTED then + result = AdminType.IS_ELECTED + goto continue + end + + -- Mayors cannot be evicted if they are also appointed administrators (e.g. manager) + result = AdminType.IS_ADMIN + break + ::continue:: + end + return result +end + +----------------------- +-- [[ PRINT MODES ]] -- +----------------------- + +---@param nobleList { unit: df.unit, site: df.world_site }[] +---@param fortEnt df.historical_entity +local function listNoblesFound(nobleList, fortEnt) + for _, record in ipairs(nobleList) do + local unit = record.unit + local site = record.site + + -- avoid scoping errors + local adminType = nil + local siteName = "" + + local nobleName = dfhack.units.getReadableName(unit) + local unitMsg = unit.id..": "..nobleName + if isSoldier(unit) then + local squad = df.squad.find(unit.military.squad_id) + local squadName = squad + and dfhack.translation.translateName(squad.name, true) + or "unknown squad" + + unitMsg = "! "..unitMsg.." - soldier in "..squadName + goto print + end + + adminType = getAdminType(unit, fortEnt) + if adminType ~= AdminType.NOT_ADMIN then + local status = adminType == AdminType.IS_ADMIN + and "fort administrator" -- isAdmin + or "elected official" -- isElected + unitMsg = adminType.sym.." "..unitMsg.." - "..status + goto print + end + + siteName = dfhack.translation.translateName(site.name, true) + unitMsg = " "..unitMsg.." - to "..siteName + + ::print:: + print(unitMsg) + end +end + +local function printNoNobles() + if options.unitId == -1 then + print("No eligible nobles to be emigrated.") + else + print("Unit ID "..options.unitId.." is not an eligible noble.") + end +end + +------------------------- +-- [[ MAIN FUNCTION ]] -- +------------------------- + +local function main() + ---@diagnostic disable-next-line: assign-type-mismatch + local fort = dfhack.world.getCurrentSite() ---@type df.world_site + if not fort then qerror("could not find current site") end + + local fortEnt = df.global.plotinfo.main.fortress_entity + + local civ = df.historical_entity.find(df.global.plotinfo.civ_id) + if not civ then qerror("could not find current civ") end + + ---@type { unit: df.unit, site: df.world_site, id: number }[] + local freeloaders = {} + for _, unit in ipairs(dfhack.units.getCitizens()) do + if options.unitId ~= -1 and unit.id ~= options.unitId then goto continue end + + addNobleOfOtherSite(unit, freeloaders, fort, civ) + ::continue:: + end + + if #freeloaders == 0 then + printNoNobles() + return + end + + if options.list then + listNoblesFound(freeloaders, fortEnt) + return + end + + for _, record in ipairs(freeloaders) do + local noble = record.unit + local site = record.site + local adminType = nil + + local nobleName = dfhack.units.getReadableName(noble) + if inSpecialJob(noble) then + print("! "..nobleName.." is busy! Leave alone for now.") + goto continue + elseif isSoldier(noble) then + print("! "..nobleName.." is in a squad! Unassign unit and try again.") + goto continue + end + + adminType = getAdminType(noble, fortEnt) + if adminType == AdminType.IS_ADMIN then + print("! "..nobleName.." is an administrator! Unassign unit and try again.") + goto continue + end + + local isElected = adminType == AdminType.IS_ELECTED + emigrate(noble, site, fortEnt, civ, isElected) + unit_link_utils.unassignSymbols(record.id, civ, fort) + ::continue:: + end +end + +local function initChecks() + if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('needs a loaded fortress map') + end + + if options.list then return true end -- list option does not require unit options + + local noOptions = options.unitId == -1 and not options.all + if noOptions then + unit = dfhack.gui.getSelectedUnit(true) + if unit then + options.unitId = unit.id + local name = dfhack.units.getReadableName(unit) + print("Selecting "..name.." (ID "..unit.id..")") + else + options.list = true + print("Defaulting to list mode:") + end + + return true + end + + local invalidUnit = options.unitId ~= -1 and options.all + if invalidUnit then qerror("Either specify one unit or all.") end + + return true +end + +local function resetOptions() + options.all = false + options.unitId = -1 + options.list = false +end + +function run(args) + argparse.processArgsGetopt(args, { + {"a", "all", handler=function() options.all = true end}, + {"u", "unit", hasArg=true, handler=function(id) options.unitId = tonumber(id) end}, + {"l", "list", handler=function() options.list = true end} + }) + + if initChecks() then + main() + end + + resetOptions() +end diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua new file mode 100644 index 0000000000..e3bc294d46 --- /dev/null +++ b/internal/emigration/unit-link-utils.lua @@ -0,0 +1,257 @@ +--@module = true + +---@param histFig df.historical_figure +---@param oldEntity df.historical_entity +local function unassignMayor(histFig, oldEntity) + local assignmentId = -1 + local positionId = -1 + local nps = dfhack.units.getNoblePositions(histFig) or {} + for _,pos in ipairs(nps) do + if pos.entity.id == oldEntity.id and pos.position.flags.ELECTED then + pos.assignment.histfig = -1 + pos.assignment.histfig2 = -1 + assignmentId = pos.assignment.id + positionId = pos.position.id + end + end + if assignmentId == -1 then qerror("could not find mayor assignment!") end + + local startYear = -1 -- remove mayor assignment + for k,v in ipairs(histFig.entity_links) do + if v.entity_id == oldEntity.id + and df.histfig_entity_link_positionst:is_instance(v) + and v.assignment_id == assignmentId + then + startYear = v.start_year + histFig.entity_links:erase(k) + v:delete() + break + end + end + if startYear == -1 then qerror("could not find entity link!") end + + histFig.entity_links:insert('#', { + new = df.histfig_entity_link_former_positionst, + assignment_id = assignmentId, + start_year = startYear, + entity_id = oldEntity.id, + end_year = df.global.cur_year, + link_strength = 100 + }) + + local hfEventId = df.global.hist_event_next_id + df.global.hist_event_next_id = df.global.hist_event_next_id+1 + df.global.world.history.events:insert("#", { + new = df.history_event_remove_hf_entity_linkst, + year = df.global.cur_year, + seconds = df.global.cur_year_tick, + id = hfEventId, + civ = oldEntity.id, + histfig = histFig.id, + link_type = df.histfig_entity_link_type.POSITION, + position_id = positionId + }) +end + +---@param histFig df.historical_figure +---@param oldEntity df.historical_entity +---@param removeMayor boolean +function removeHistFigFromEntity(histFig, oldEntity, removeMayor) + if not histFig or not oldEntity then return end + + local histFigId = histFig.id + + -- erase the unit from the fortress entity + for k,v in ipairs(oldEntity.histfig_ids) do + if v == histFigId then + df.global.plotinfo.main.fortress_entity.histfig_ids:erase(k) + break + end + end + for k,v in ipairs(oldEntity.hist_figures) do + if v.id == histFigId then + df.global.plotinfo.main.fortress_entity.hist_figures:erase(k) + break + end + end + for k,v in ipairs(oldEntity.nemesis) do + if v.figure.id == histFigId then + df.global.plotinfo.main.fortress_entity.nemesis:erase(k) + df.global.plotinfo.main.fortress_entity.nemesis_ids:erase(k) + break + end + end + + -- remove mayor assignment if exists + if removeMayor then unassignMayor(histFig, oldEntity) end + + -- remove the old entity link and create new one to indicate former membership + histFig.entity_links:insert("#", {new = df.histfig_entity_link_former_memberst, entity_id = oldEntity.id, link_strength = 100}) + for k,v in ipairs(histFig.entity_links) do + if v._type == df.histfig_entity_link_memberst and v.entity_id == oldEntity.id then + histFig.entity_links:erase(k) + break + end + end +end + +---Creates events indicating a histfig's move to a new site and joining its entity. +---@param histFig df.historical_figure +---@param siteId number Set to -1 if unneeded +---@param siteGov df.historical_entity +function addHistFigToSite(histFig, siteId, siteGov) + if not histFig or not siteGov then return nil end + + local histFigId = histFig.id + + -- add new site gov to histfig links + histFig.entity_links:insert("#", { + new = df.histfig_entity_link_memberst, + entity_id = siteGov.id, + link_strength = 100 + }) + + -- add histfig to new site gov + siteGov.histfig_ids:insert('#', histFigId) + siteGov.hist_figures:insert('#', histFig) + local hfEventId = df.global.hist_event_next_id + df.global.hist_event_next_id = df.global.hist_event_next_id+1 + df.global.world.history.events:insert("#", { + new = df.history_event_add_hf_entity_linkst, + year = df.global.cur_year, + seconds = df.global.cur_year_tick, + id = hfEventId, + civ = siteGov.id, + histfig = histFigId, + link_type = df.histfig_entity_link_type.MEMBER + }) + + if siteId <= -1 then return end -- skip site join event + + -- create event indicating histfig moved to site + hfEventId = df.global.hist_event_next_id + df.global.hist_event_next_id = df.global.hist_event_next_id+1 + df.global.world.history.events:insert("#", { + new = df.history_event_change_hf_statest, + year = df.global.cur_year, + seconds = df.global.cur_year_tick, + id = hfEventId, + hfid = histFigId, + state = df.whereabouts_type.settler, + reason = df.history_event_reason.none, + site = siteId + }) +end + +---@param unit df.unit +function removeUnitAssociations(unit) + -- free owned rooms + for i = #unit.owned_buildings-1, 0, -1 do + local tmp = df.building.find(unit.owned_buildings[i].id) + dfhack.buildings.setOwner(tmp, nil) + end + + -- remove from workshop profiles + for _, bld in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do + for k, v in ipairs(bld.profile.permitted_workers) do + if v == unit.id then + bld.profile.permitted_workers:erase(k) + break + end + end + end + for _, bld in ipairs(df.global.world.buildings.other.FURNACE_ANY) do + for k, v in ipairs(bld.profile.permitted_workers) do + if v == unit.id then + bld.profile.permitted_workers:erase(k) + break + end + end + end + + -- disassociate from work details + for _, detail in ipairs(df.global.plotinfo.labor_info.work_details) do + for k, v in ipairs(detail.assigned_units) do + if v == unit.id then + detail.assigned_units:erase(k) + break + end + end + end + + -- unburrow + for _, burrow in ipairs(df.global.plotinfo.burrows.list) do + dfhack.burrows.setAssignedUnit(burrow, unit, false) + end +end + +---@param unit df.unit +---@param civId number +---@param leaveNow boolean Decides if unit leaves immediately or with merchants +function markUnitForEmigration(unit, civId, leaveNow) + unit.following = nil + unit.civ_id = civId + + if leaveNow then + unit.flags1.forest = true + unit.flags2.visitor = true + unit.animal.leave_countdown = 2 + else + unit.flags1.merchant = true + end +end + +---@param item df.item +local function getPos(item) + local x, y, z = dfhack.items.getPosition(item) + if not x or not y or not z then + return nil + end + + if dfhack.maps.isTileVisible(x, y, z) then + return xyz2pos(x, y, z) + end +end + +---@param assignmentId number +---@param entity df.historical_entity +---@param site df.world_site +function unassignSymbols(assignmentId, entity, site) + local claims = entity.artifact_claims + local artifacts = df.global.world.artifacts.all + + for i=#claims-1,0,-1 do + local claim = claims[i] + if claim.claim_type ~= df.artifact_claim_type.Symbol then goto continue end + if claim.symbol_claim_id ~= assignmentId then goto continue end + + local artifact = artifacts[claim.artifact_id] + local item = artifact.item + local artifactName = dfhack.translation.translateName(artifact.name) + + -- we can probably keep artifact.entity_claims since we still hold it + local itemPos = getPos(item) + local success = false + if not itemPos then + if artifact.site == site.id then + print(" ! "..artifactName.." cannot be found!") + goto removeClaim + else + print(" ! "..artifactName.." is not in this site!") + goto continue + end + end + + success = dfhack.items.moveToGround(item, itemPos) + if success then print(" + dropped "..artifactName) + else print(" ! could not drop "..artifactName) + end + + -- they do not seem to "own" their artifacts, no additional cleaning seems necessary + + ::removeClaim:: + claims:erase(i) + claim:delete() + ::continue:: + end +end