From 973b5e8d1554bbf400e86d8f56b4d648077f7f8f Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 26 Jan 2025 16:03:09 +0800 Subject: [PATCH 01/43] Add emigrate-nobles.lua and rst --- docs/emigrate-nobles.rst | 16 ++++++++++++++ emigrate-nobles.lua | 46 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 docs/emigrate-nobles.rst create mode 100644 emigrate-nobles.lua diff --git a/docs/emigrate-nobles.rst b/docs/emigrate-nobles.rst new file mode 100644 index 0000000000..23975a2fa4 --- /dev/null +++ b/docs/emigrate-nobles.rst @@ -0,0 +1,16 @@ +emigrate-nobles +========== + +.. dfhack-tool:: + :summary: Allow inherited nobles to emigrate from fort for their rightful land. + :tags: fort units + +Tired of inherited nobility freeloading off your fortress making inane demands? Use this tool +to find them and have them (willingly) emigrate to their rightful land. + +Usage +----- + +:: + + emigrate-nobles [-h] diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua new file mode 100644 index 0000000000..d72c5f12b8 --- /dev/null +++ b/emigrate-nobles.lua @@ -0,0 +1,46 @@ +--Deport resident nobles of other lands and summon your rightful lord if he/she is elsewhere + +local argparse = require("argparse") + +--[[ +Planned modes/options: + - d/deport = remove inherited freeloaders + - list = list possible nobles to evict + - index = specific noble to kick + - all = kick all listed nobles + - i/import = find and invite heir to fortress (need a better name) +]]-- +local options = { + help = false +} + +function isNoble(unit) + for _, pos in ipairs(dfhack.units.getNoblePositions(unit) or {}) do + entity_pos = pos.position + if entity_pos.flags.IS_LAW_MAKER then + name = dfhack.df2console(dfhack.units.getReadableName(unit)) + print(name.." is a noble") + end + end +end + +function main() + for _, unit in ipairs(dfhack.units.getCitizens()) do + if not options.deport or dfhack.units.isDead(unit) then goto continue end + isNoble(unit) + ::continue:: + end +end + + +argparse.processArgsGetopt({...}, { + {"h", "help", handler=function() options.help = true end}, + {"a", "all", handler=function() options.deport = true end}, +}) + +if options.help then + print(dfhack.script_help()) + return +end + +main() From 7a8f07ea2f8b81119e2789107d1fff33561767f5 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Tue, 28 Jan 2025 03:48:36 +0800 Subject: [PATCH 02/43] Add logic for finding noble's sites --- emigrate-nobles.lua | 76 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua index d72c5f12b8..7af10c5a95 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -14,33 +14,89 @@ local options = { help = false } +fortId=nil + function isNoble(unit) - for _, pos in ipairs(dfhack.units.getNoblePositions(unit) or {}) do - entity_pos = pos.position - if entity_pos.flags.IS_LAW_MAKER then - name = dfhack.df2console(dfhack.units.getReadableName(unit)) - print(name.." is a noble") + local nps = dfhack.units.getNoblePositions(unit) or {} + local isLawMaker = false + + local noblePos, nobleName + + for _, np in ipairs(nps) do + pos = np.position + if pos.flags.IS_LAW_MAKER then + isLawMaker = true + noblePos = np + nobleName = dfhack.df2console(dfhack.units.getReadableName(unit)) + break end end + + if not isLawMaker then return false end + + civ = noblePos.entity -- lawmakers seem to be all civ-level positions + assignments = civ.positions.assignments + for _, link in ipairs(civ.site_links) do + siteId = link.target + posProfId = link.position_profile_id + if posProfId < 0 then goto continue end + + assignment = assignments[posProfId] + if assignment.id ~= noblePos.assignment.id then goto continue end + + site = df.world_site.find(siteId) + siteName = dfhack.translation.translateName(site.name, true, true) + + if siteId == fortId then + print(nobleName.." holds a position in this fortress - "..siteName) + goto continue + end + + print(nobleName.." is lord of "..siteName) + ::continue:: + end + + return isLawMaker end function main() + fort = df.historical_entity.find(fortId) + fortName = dfhack.translation.translateName(fort.name, true) + print("Current fort is "..fortName) + for _, unit in ipairs(dfhack.units.getCitizens()) do - if not options.deport or dfhack.units.isDead(unit) then goto continue end + if dfhack.units.isDead(unit) then goto continue end isNoble(unit) ::continue:: end end +function initChecks() + if options.help then + print(dfhack.script_help()) + return false + end + + if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('needs a loaded fortress map') + return false + end + + fortId = dfhack.world.GetCurrentSiteId() + if fortId == -1 then + qerror('could not find current site') + return false + end + + return true +end argparse.processArgsGetopt({...}, { {"h", "help", handler=function() options.help = true end}, {"a", "all", handler=function() options.deport = true end}, }) -if options.help then - print(dfhack.script_help()) - return -end +pass = initChecks() +if not pass then return end main() From 414111e31a0d301076b79a279350fc3200812808 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Tue, 28 Jan 2025 22:40:39 +0800 Subject: [PATCH 03/43] Add monarch handling logic --- emigrate-nobles.lua | 109 +++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua index 7af10c5a95..e3da35ce1f 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -14,61 +14,85 @@ local options = { help = false } -fortId=nil +fort = nil +civ = nil +capital = nil + +-- adapted from Units::get_land_title(np) +function findSiteOfRule(np) + site = nil + civ = np.entity -- lawmakers seem to be all civ-level positions + assignments = civ.positions.assignments + 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 -function isNoble(unit) - local nps = dfhack.units.getNoblePositions(unit) or {} - local isLawMaker = false + site = df.world_site.find(link.target) + break + ::continue:: + end - local noblePos, nobleName + return site +end - for _, np in ipairs(nps) do - pos = np.position - if pos.flags.IS_LAW_MAKER then - isLawMaker = true - noblePos = np - nobleName = dfhack.df2console(dfhack.units.getReadableName(unit)) +---comment +---@return df.world_site|nil +function findCapital(civ) + local capital = nil + for _, link in ipairs(civ.site_links) do + siteId = link.target + if link.flags.capital then + capital = df.world_site.find(siteId) break end end - if not isLawMaker then return false end - - civ = noblePos.entity -- lawmakers seem to be all civ-level positions - assignments = civ.positions.assignments - for _, link in ipairs(civ.site_links) do - siteId = link.target - posProfId = link.position_profile_id - if posProfId < 0 then goto continue end + return capital +end - assignment = assignments[posProfId] - if assignment.id ~= noblePos.assignment.id then goto continue end +function addIfRulesOtherSite(unit, freeloaders) + 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 - site = df.world_site.find(siteId) - siteName = dfhack.translation.translateName(site.name, true, true) + if noblePos == nil then return end -- unit is not nobility - if siteId == fortId then - print(nobleName.." holds a position in this fortress - "..siteName) - goto continue + -- Monarchs do not seem to have an world_site associated to them (?) + if noblePos.position.code == "MONARCH" then + if capital.id ~= fort.id then + freeloaders[unit.id] = capital end - - print(nobleName.." is lord of "..siteName) - ::continue:: + return end - return isLawMaker + name = dfhack.units.getReadableName(unit) + -- Logic for non-monarch nobility (dukes, counts, barons) + site = findSiteOfRule(noblePos) + if site == nil then qerror("could not find land of "..name) end + + if site.id == fort.id then return end -- noble rules current fort + freeloaders[unit.id] = site end function main() - fort = df.historical_entity.find(fortId) - fortName = dfhack.translation.translateName(fort.name, true) - print("Current fort is "..fortName) - + freeloaders = {} for _, unit in ipairs(dfhack.units.getCitizens()) do - if dfhack.units.isDead(unit) then goto continue end - isNoble(unit) + if dfhack.units.isDead(unit) or not dfhack.units.isSane(unit) then goto continue end + addIfRulesOtherSite(unit, freeloaders) ::continue:: end + + for unitId, site in pairs(freeloaders) do + unit = df.unit.find(unitId) + unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) + siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) + print(unitName.." is lord of "..siteName) + end end function initChecks() @@ -82,11 +106,14 @@ function initChecks() return false end - fortId = dfhack.world.GetCurrentSiteId() - if fortId == -1 then - qerror('could not find current site') - return false - end + fort = dfhack.world.getCurrentSite() + if fort == nil then qerror("could not find current site") end + + civ = df.historical_entity.find(df.global.plotinfo.civ_id) + if civ == nil then qerror("could not find current civ") end + + capital = findCapital(civ) + if capital == nil then qerror("could not find capital") end return true end From 16f5939f96a081fd8538ce623f06281349f5fc90 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Wed, 29 Jan 2025 00:19:56 +0800 Subject: [PATCH 04/43] Add emigration logic --- emigrate-nobles.lua | 146 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 125 insertions(+), 21 deletions(-) diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua index e3da35ce1f..ae8c1a9b11 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -11,18 +11,17 @@ Planned modes/options: - i/import = find and invite heir to fortress (need a better name) ]]-- local options = { - help = false + help = false, } fort = nil civ = nil capital = nil --- adapted from Units::get_land_title(np) +-- adapted from Units::get_land_title() function findSiteOfRule(np) - site = nil - civ = np.entity -- lawmakers seem to be all civ-level positions - assignments = civ.positions.assignments + 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 @@ -35,14 +34,12 @@ function findSiteOfRule(np) return site end ----comment ---@return df.world_site|nil function findCapital(civ) local capital = nil for _, link in ipairs(civ.site_links) do - siteId = link.target if link.flags.capital then - capital = df.world_site.find(siteId) + capital = df.world_site.find(link.target) break end end @@ -50,7 +47,7 @@ function findCapital(civ) return capital end -function addIfRulesOtherSite(unit, freeloaders) +function addNobleOfOtherSite(unit, nobleList) local nps = dfhack.units.getNoblePositions(unit) or {} local noblePos = nil for _, np in ipairs(nps) do @@ -65,33 +62,141 @@ function addIfRulesOtherSite(unit, freeloaders) -- Monarchs do not seem to have an world_site associated to them (?) if noblePos.position.code == "MONARCH" then if capital.id ~= fort.id then - freeloaders[unit.id] = capital + table.insert(nobleList, {id = unit.id, site = capital}) end return end - name = dfhack.units.getReadableName(unit) + local name = dfhack.units.getReadableName(unit) -- Logic for non-monarch nobility (dukes, counts, barons) - site = findSiteOfRule(noblePos) + local site = findSiteOfRule(noblePos) if site == nil then qerror("could not find land of "..name) end if site.id == fort.id then return end -- noble rules current fort - freeloaders[unit.id] = site + table.insert(nobleList, {id = unit.id, site = site}) +end + +-- adapted from emigration::desert() +function emigrate(nobleId, site) + local unit = df.unit.find(nobleId) + local unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) + local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) + print(unitName.." <"..siteName..">") + + unit.following = nil + local histFigId = unit.hist_figure_id + local histFig = df.historical_figure.find(unit.hist_figure_id) + local fortEnt = df.global.plotinfo.main.fortress_entity + + unit.civ_id = civ.id + unit.flags1.forest = true + unit.flags2.visitor = true + unit.animal.leave_countdown = 2 + + -- 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 + + -- erase the unit from the fortress entity + for k,v in ipairs(fortEnt.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(fortEnt.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(fortEnt.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 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 = fortEnt.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 == fortEnt.id then + histFig.entity_links:erase(k) + break + end + end + + -- have unit join site government + local newEntId = site.cur_owner_id + histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newEntId, link_strength = 100}) + + -- have unit join new site + local newSiteId = site.id + local newEntity = df.historical_entity.find(newEntId) + newEntity.histfig_ids:insert('#', histFigId) + newEntity.hist_figures:insert('#', histFig) + 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 = newEntId, histfig = histFigId, link_type = 0}) + + 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 = histFigId, state = 1, reason = -1, site = newSiteId}) end function main() freeloaders = {} for _, unit in ipairs(dfhack.units.getCitizens()) do if dfhack.units.isDead(unit) or not dfhack.units.isSane(unit) then goto continue end - addIfRulesOtherSite(unit, freeloaders) + addNobleOfOtherSite(unit, freeloaders) ::continue:: end - for unitId, site in pairs(freeloaders) do - unit = df.unit.find(unitId) - unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) - siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) - print(unitName.." is lord of "..siteName) + -- for index, record in pairs(freeloaders) do + -- unit = df.unit.find(record.id) + -- unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) + -- siteName = dfhack.df2console(dfhack.translation.translateName(record.site.name, true)) + -- print(index..": "..unitName.." <"..siteName..">") + -- end + + for i, record in pairs(freeloaders) do + if i == 2 then emigrate(record.id, record.site) end end end @@ -119,8 +224,7 @@ function initChecks() end argparse.processArgsGetopt({...}, { - {"h", "help", handler=function() options.help = true end}, - {"a", "all", handler=function() options.deport = true end}, + {"h", "help", handler=function() options.help = true end} }) pass = initChecks() From c110f250f76c9d37609dcb390edccc9ae36c9224 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Wed, 29 Jan 2025 00:33:56 +0800 Subject: [PATCH 05/43] Add announcement --- emigrate-nobles.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua index ae8c1a9b11..ded848fe6f 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -178,6 +178,11 @@ function emigrate(nobleId, site) 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 = histFigId, state = 1, reason = -1, site = newSiteId}) + + -- announce the changes + local line = unitName .. " has left the settlement to govern " .. siteName + print(dfhack.df2console(line)) + dfhack.gui.showAnnouncement(line, COLOR_WHITE) end function main() @@ -195,8 +200,8 @@ function main() -- print(index..": "..unitName.." <"..siteName..">") -- end - for i, record in pairs(freeloaders) do - if i == 2 then emigrate(record.id, record.site) end + for _, record in pairs(freeloaders) do + emigrate(record.id, record.site) end end From 2d6e5883abf7beb88120dee2ef5bfd9061386b92 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Wed, 29 Jan 2025 22:11:35 +0800 Subject: [PATCH 06/43] Add options handling --- emigrate-nobles.lua | 166 +++++++++++++++++++++++++++++++------------- 1 file changed, 118 insertions(+), 48 deletions(-) diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua index ded848fe6f..b577adfa41 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -12,13 +12,17 @@ Planned modes/options: ]]-- local options = { help = false, + all = false, + unitId = -1, + list = false } -fort = nil -civ = nil -capital = nil +fort = nil ---@type df.world_site +civ = nil ---@type df.historical_entity +capital = nil ---@type df.world_site -- adapted from Units::get_land_title() +---@return df.world_site|nil function findSiteOfRule(np) local site = nil local civ = np.entity -- lawmakers seem to be all civ-level positions @@ -68,7 +72,7 @@ function addNobleOfOtherSite(unit, nobleList) end local name = dfhack.units.getReadableName(unit) - -- Logic for non-monarch nobility (dukes, counts, barons) + -- Logic for dukes, counts, barons local site = findSiteOfRule(noblePos) if site == nil then qerror("could not find land of "..name) end @@ -76,22 +80,35 @@ function addNobleOfOtherSite(unit, nobleList) table.insert(nobleList, {id = unit.id, site = site}) end --- adapted from emigration::desert() -function emigrate(nobleId, site) - local unit = df.unit.find(nobleId) - local unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) - local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) - print(unitName.." <"..siteName..">") +---@param histFig df.historical_figure +---@param newSite df.world_site +local function addHistFigToSite(histFig, newSite) + -- have unit join site government + local siteGovId = newSite.cur_owner_id + histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = siteGovId, link_strength = 100}) + local histFigId = histFig.id - unit.following = nil - local histFigId = unit.hist_figure_id - local histFig = df.historical_figure.find(unit.hist_figure_id) - local fortEnt = df.global.plotinfo.main.fortress_entity + -- have unit join new site + local siteId = newSite.id + local siteGov = df.historical_entity.find(siteGovId) + if siteGov == nil then qerror("could not find site!") end - unit.civ_id = civ.id - unit.flags1.forest = true - unit.flags2.visitor = true - unit.animal.leave_countdown = 2 + 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 = siteGovId, histfig = histFigId, link_type = 0}) + + 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_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hfEventId, hfid = histFigId, state = 1, reason = -1, site = siteId}) +end + +---@param unit df.unit +---@param histFig df.historical_figure +---@param oldSite df.historical_entity +local function removeUnitFromSiteEntity(unit, histFig, oldSite) + local histFigId = histFig.id -- free owned rooms for i = #unit.owned_buildings-1, 0, -1 do @@ -133,19 +150,19 @@ function emigrate(nobleId, site) end -- erase the unit from the fortress entity - for k,v in ipairs(fortEnt.histfig_ids) do + for k,v in ipairs(oldSite.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(fortEnt.hist_figures) do + for k,v in ipairs(oldSite.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(fortEnt.nemesis) do + for k,v in ipairs(oldSite.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) @@ -154,54 +171,88 @@ function emigrate(nobleId, site) 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 = fortEnt.id, link_strength = 100}) + histFig.entity_links:insert("#", {new = df.histfig_entity_link_former_memberst, entity_id = oldSite.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 == fortEnt.id then + if v._type == df.histfig_entity_link_memberst and v.entity_id == oldSite.id then histFig.entity_links:erase(k) break end end +end - -- have unit join site government - local newEntId = site.cur_owner_id - histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newEntId, link_strength = 100}) +-- adapted from emigration::desert() +---@param unit df.unit +---@param toSite df.world_site +function emigrate(unit, toSite) + local histFig = df.historical_figure.find(unit.hist_figure_id) + if histFig == nil then qerror("could not find histfig!") end - -- have unit join new site - local newSiteId = site.id - local newEntity = df.historical_entity.find(newEntId) - newEntity.histfig_ids:insert('#', histFigId) - newEntity.hist_figures:insert('#', histFig) - 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 = newEntId, histfig = histFigId, link_type = 0}) + local fortEnt = df.global.plotinfo.main.fortress_entity - 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 = histFigId, state = 1, reason = -1, site = newSiteId}) + unit.following = nil + unit.civ_id = civ.id + unit.flags1.forest = true + unit.flags2.visitor = true + unit.animal.leave_countdown = 2 + + removeUnitFromSiteEntity(unit, histFig, fortEnt) + addHistFigToSite(histFig, toSite) -- announce the changes - local line = unitName .. " has left the settlement to govern " .. siteName - print(dfhack.df2console(line)) + local unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) + local siteName = dfhack.df2console(dfhack.translation.translateName(toSite.name, true)) + local line = unitName .. " has left to govern " .. siteName .. "." + print("[+] "..dfhack.df2console(line)) dfhack.gui.showAnnouncement(line, COLOR_WHITE) end +function listNoblesFound(nobleList) + for _, record in pairs(nobleList) do + local unit = df.unit.find(record.id) + local site = record.site + if unit == nil then qerror("could not find unit!") end + + local nobleName = dfhack.df2console(dfhack.units.getReadableName(unit)) + local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) + print(unit.id..": "..nobleName.." - to be sent to "..siteName) + end +end + function main() - freeloaders = {} + local freeloaders = {} for _, unit in ipairs(dfhack.units.getCitizens()) do + if options.unitId ~= -1 and unit.id ~= options.unitId then goto continue end if dfhack.units.isDead(unit) or not dfhack.units.isSane(unit) then goto continue end + addNobleOfOtherSite(unit, freeloaders) ::continue:: end - -- for index, record in pairs(freeloaders) do - -- unit = df.unit.find(record.id) - -- unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) - -- siteName = dfhack.df2console(dfhack.translation.translateName(record.site.name, true)) - -- print(index..": "..unitName.." <"..siteName..">") - -- end + if #freeloaders == 0 then + if options.unitId ~= -1 then + print("No eligible nobles to be emigrated.") + else + print("No eligible nobles found with ID = "..options.unitId) + end + end + + if options.list then + listNoblesFound(freeloaders) + return + end for _, record in pairs(freeloaders) do - emigrate(record.id, record.site) + local noble = df.unit.find(record.id) + local site = record.site + if noble == nil then qerror("could not find unit!") end + + if noble.military.squad_id ~= -1 then + local squadName = dfhack.military.getSquadName(noble.military.squad_id) + local nobleName = dfhack.units.getReadableName(noble) + print("[x] "..nobleName.." is a soldier of "..squadName..". Unassign him from the squad and try again.") + else + emigrate(noble, site) + end end end @@ -225,11 +276,30 @@ function initChecks() capital = findCapital(civ) if capital == nil then qerror("could not find capital") 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 + print("No options selected, defaulting to list mode.") + options.list = true + 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 +------------------------------ +-- [[ SCRIPT STARTS HERE ]] -- +------------------------------ + argparse.processArgsGetopt({...}, { - {"h", "help", handler=function() options.help = true end} + {"h", "help", handler=function() options.help = true end}, + {"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} }) pass = initChecks() From fe549d99d170ef31bc66a4ba9c7c6faafd8dff1e Mon Sep 17 00:00:00 2001 From: yg-ong Date: Wed, 29 Jan 2025 22:36:22 +0800 Subject: [PATCH 07/43] Add documentation --- docs/emigrate-nobles.rst | 28 ++++++++++++++++++++++++---- emigrate-nobles.lua | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/docs/emigrate-nobles.rst b/docs/emigrate-nobles.rst index 23975a2fa4..c44ac912ca 100644 --- a/docs/emigrate-nobles.rst +++ b/docs/emigrate-nobles.rst @@ -2,15 +2,35 @@ emigrate-nobles ========== .. dfhack-tool:: - :summary: Allow inherited nobles to emigrate from fort for their rightful land. + :summary: Allow inherited nobles to emigrate from fort to their site of governance. :tags: fort units Tired of inherited nobility freeloading off your fortress making inane demands? Use this tool -to find them and have them (willingly) emigrate to their rightful land. +to have them (willingly) emigrate to their rightful lands. + +The unit must not be assigned to a squad. + +Warning! Nobles will not surrender any assigned items, be sure to unassign your artefacts before +using this tool. Usage ----- -:: +``emigrate-nobles --list`` + List all nobles that do not rule your fortress +``emigrate-nobles --all`` + Emigrate all nobles that do not rule your fortress +``emigrate-nobles --unit `` + Emigrate a noble matching the specified unit ID that does not rule your fortress + +Options +------- - emigrate-nobles [-h] +``-h``, ``--help`` + View help +``-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/emigrate-nobles.lua b/emigrate-nobles.lua index b577adfa41..5ffea6f35c 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -249,7 +249,7 @@ function main() if noble.military.squad_id ~= -1 then local squadName = dfhack.military.getSquadName(noble.military.squad_id) local nobleName = dfhack.units.getReadableName(noble) - print("[x] "..nobleName.." is a soldier of "..squadName..". Unassign him from the squad and try again.") + print("[-] "..nobleName.." is a soldier of "..squadName..". Unassign him from the squad and try again.") else emigrate(noble, site) end From 9777b1a39c744db7bdf8338694e5d9dc291b7706 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Wed, 29 Jan 2025 23:40:59 +0800 Subject: [PATCH 08/43] Attempt to fix units stuck in social activities --- emigrate-nobles.lua | 61 +++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua index 5ffea6f35c..1fed596ee1 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -1,15 +1,13 @@ --Deport resident nobles of other lands and summon your rightful lord if he/she is elsewhere -local argparse = require("argparse") - --[[ -Planned modes/options: - - d/deport = remove inherited freeloaders - - list = list possible nobles to evict - - index = specific noble to kick - - all = kick all listed nobles - - i/import = find and invite heir to fortress (need a better name) +TODO: + * Feature: have rightful ruler immigrate to fort if off-site + * QoL: make sure items are unassigned ]]-- + +local argparse = require("argparse") + local options = { help = false, all = false, @@ -23,7 +21,7 @@ capital = nil ---@type df.world_site -- adapted from Units::get_land_title() ---@return df.world_site|nil -function findSiteOfRule(np) +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 @@ -39,7 +37,7 @@ function findSiteOfRule(np) end ---@return df.world_site|nil -function findCapital(civ) +local function findCapital(civ) local capital = nil for _, link in ipairs(civ.site_links) do if link.flags.capital then @@ -61,7 +59,7 @@ function addNobleOfOtherSite(unit, nobleList) end end - if noblePos == nil then return end -- unit is not nobility + 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.code == "MONARCH" then @@ -74,7 +72,7 @@ function addNobleOfOtherSite(unit, nobleList) local name = dfhack.units.getReadableName(unit) -- Logic for dukes, counts, barons local site = findSiteOfRule(noblePos) - if site == nil then qerror("could not find land of "..name) end + if not site then qerror("could not find land of "..name) end if site.id == fort.id then return end -- noble rules current fort table.insert(nobleList, {id = unit.id, site = site}) @@ -91,7 +89,7 @@ local function addHistFigToSite(histFig, newSite) -- have unit join new site local siteId = newSite.id local siteGov = df.historical_entity.find(siteGovId) - if siteGov == nil then qerror("could not find site!") end + if not siteGov then qerror("could not find site!") end siteGov.histfig_ids:insert('#', histFigId) siteGov.hist_figures:insert('#', histFig) @@ -189,12 +187,22 @@ function emigrate(unit, toSite) local fortEnt = df.global.plotinfo.main.fortress_entity + -- mark for leaving unit.following = nil - unit.civ_id = civ.id + unit.civ_id = civ.id -- should be redundant but oh well unit.flags1.forest = true unit.flags2.visitor = true unit.animal.leave_countdown = 2 + -- 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 + removeUnitFromSiteEntity(unit, histFig, fortEnt) addHistFigToSite(histFig, toSite) @@ -206,11 +214,20 @@ function emigrate(unit, toSite) dfhack.gui.showAnnouncement(line, COLOR_WHITE) end +---@param unit df.unit +local function inStrangeMood(unit) + local job = unit.job.current_job + if not job then return false end + + local jobType = job.job_type -- taken from notifications::for_moody() + return df.job_type_class[df.job_type.attrs[jobType].type] == 'StrangeMood' +end + function listNoblesFound(nobleList) for _, record in pairs(nobleList) do local unit = df.unit.find(record.id) local site = record.site - if unit == nil then qerror("could not find unit!") end + if not unit then qerror("could not find unit!") end local nobleName = dfhack.df2console(dfhack.units.getReadableName(unit)) local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) @@ -244,12 +261,14 @@ function main() for _, record in pairs(freeloaders) do local noble = df.unit.find(record.id) local site = record.site - if noble == nil then qerror("could not find unit!") end + if not noble then qerror("could not find unit!") end + local nobleName = dfhack.units.getReadableName(noble) if noble.military.squad_id ~= -1 then local squadName = dfhack.military.getSquadName(noble.military.squad_id) - local nobleName = dfhack.units.getReadableName(noble) - print("[-] "..nobleName.." is a soldier of "..squadName..". Unassign him from the squad and try again.") + print("[-] "..nobleName.." is a soldier of "..squadName..". Unassign from squad and try again.") + elseif inStrangeMood(noble) then + print("[-] "..nobleName.." is in a strange mood! Leave alone for now.") else emigrate(noble, site) end @@ -268,13 +287,13 @@ function initChecks() end fort = dfhack.world.getCurrentSite() - if fort == nil then qerror("could not find current site") end + if not fort then qerror("could not find current site") end civ = df.historical_entity.find(df.global.plotinfo.civ_id) - if civ == nil then qerror("could not find current civ") end + if not civ then qerror("could not find current civ") end capital = findCapital(civ) - if capital == nil then qerror("could not find capital") end + if not capital then qerror("could not find capital") end if options.list then return true end -- list option does not require unit options From 16544b2741f75dd07421bebbed10ad0442cdbb8f Mon Sep 17 00:00:00 2001 From: yg-ong Date: Thu, 30 Jan 2025 19:36:39 +0800 Subject: [PATCH 09/43] Fix title underline --- docs/emigrate-nobles.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/emigrate-nobles.rst b/docs/emigrate-nobles.rst index c44ac912ca..154d4cac61 100644 --- a/docs/emigrate-nobles.rst +++ b/docs/emigrate-nobles.rst @@ -1,5 +1,5 @@ emigrate-nobles -========== +=============== .. dfhack-tool:: :summary: Allow inherited nobles to emigrate from fort to their site of governance. From 537af8efd00ba2b11ddcf7a01f36d116db0d314f Mon Sep 17 00:00:00 2001 From: yg-ong Date: Thu, 30 Jan 2025 19:50:37 +0800 Subject: [PATCH 10/43] Simplify logic --- emigrate-nobles.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua index 1fed596ee1..55519a040e 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -64,7 +64,7 @@ function addNobleOfOtherSite(unit, nobleList) -- Monarchs do not seem to have an world_site associated to them (?) if noblePos.position.code == "MONARCH" then if capital.id ~= fort.id then - table.insert(nobleList, {id = unit.id, site = capital}) + table.insert(nobleList, {unit = unit, site = capital}) end return end @@ -75,7 +75,7 @@ function addNobleOfOtherSite(unit, nobleList) if not site then qerror("could not find land of "..name) end if site.id == fort.id then return end -- noble rules current fort - table.insert(nobleList, {id = unit.id, site = site}) + table.insert(nobleList, {unit = unit, site = site}) end ---@param histFig df.historical_figure @@ -225,9 +225,8 @@ end function listNoblesFound(nobleList) for _, record in pairs(nobleList) do - local unit = df.unit.find(record.id) + local unit = record.unit local site = record.site - if not unit then qerror("could not find unit!") end local nobleName = dfhack.df2console(dfhack.units.getReadableName(unit)) local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) @@ -259,9 +258,8 @@ function main() end for _, record in pairs(freeloaders) do - local noble = df.unit.find(record.id) + local noble = record.unit local site = record.site - if not noble then qerror("could not find unit!") end local nobleName = dfhack.units.getReadableName(noble) if noble.military.squad_id ~= -1 then From ebc3976d6736946b4479cf02ce1a548989b6306f Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Thu, 30 Jan 2025 19:52:49 +0800 Subject: [PATCH 11/43] Update docs/emigrate-nobles.rst Co-authored-by: Myk --- docs/emigrate-nobles.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/emigrate-nobles.rst b/docs/emigrate-nobles.rst index 154d4cac61..8f8c0563cb 100644 --- a/docs/emigrate-nobles.rst +++ b/docs/emigrate-nobles.rst @@ -16,11 +16,19 @@ using this tool. Usage ----- +:: + + emigrate-nobles [--list] + emigrate-nobles + +Examples +-------- + ``emigrate-nobles --list`` List all nobles that do not rule your fortress ``emigrate-nobles --all`` Emigrate all nobles that do not rule your fortress -``emigrate-nobles --unit `` +``emigrate-nobles --unit 34534`` Emigrate a noble matching the specified unit ID that does not rule your fortress Options From 4272ee7ec754489132198d7cbf24e7cd07b45cc9 Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Thu, 30 Jan 2025 19:53:03 +0800 Subject: [PATCH 12/43] Update docs/emigrate-nobles.rst Co-authored-by: Myk --- docs/emigrate-nobles.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/emigrate-nobles.rst b/docs/emigrate-nobles.rst index 8f8c0563cb..90d3e21bca 100644 --- a/docs/emigrate-nobles.rst +++ b/docs/emigrate-nobles.rst @@ -40,5 +40,5 @@ Options List all nobles that do not rule your fortress ``-a``, ``--all`` Emigrate all nobles do not rule your fortress -``-u ``, ``--unit `` +``-u``, ``--unit `` Emigrate noble matching specified unit ID that does not rule your fortress From 8de561ee6fd4d84b76e336ade594d1b9dc069c35 Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Thu, 30 Jan 2025 19:53:24 +0800 Subject: [PATCH 13/43] Update docs/emigrate-nobles.rst Co-authored-by: Myk --- docs/emigrate-nobles.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/emigrate-nobles.rst b/docs/emigrate-nobles.rst index 90d3e21bca..2259f349a1 100644 --- a/docs/emigrate-nobles.rst +++ b/docs/emigrate-nobles.rst @@ -34,8 +34,6 @@ Examples Options ------- -``-h``, ``--help`` - View help ``-l``, ``--list`` List all nobles that do not rule your fortress ``-a``, ``--all`` From 18a28d53a5f3d33790ae8abaa4f7dc4cc4d5caa0 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Thu, 30 Jan 2025 19:55:01 +0800 Subject: [PATCH 14/43] Update docs/emigrate-nobles.rst --- docs/emigrate-nobles.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/emigrate-nobles.rst b/docs/emigrate-nobles.rst index 2259f349a1..53ecf9124f 100644 --- a/docs/emigrate-nobles.rst +++ b/docs/emigrate-nobles.rst @@ -10,8 +10,8 @@ to have them (willingly) emigrate to their rightful lands. The unit must not be assigned to a squad. -Warning! Nobles will not surrender any assigned items, be sure to unassign your artefacts before -using this tool. +Warning! Nobles will not surrender any symbols of office before leaving, be sure to unassign +your artefacts before using this tool. Usage ----- From 3b40700f69f52226601f17a5f9047d1f61a90196 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Thu, 30 Jan 2025 22:58:34 +0800 Subject: [PATCH 15/43] Add functionality to remove from squad --- emigrate-nobles.lua | 71 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua index 55519a040e..58a4fd16ca 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -83,6 +83,7 @@ end local function addHistFigToSite(histFig, newSite) -- have unit join site government local siteGovId = newSite.cur_owner_id + print("new site gov = "..siteGovId) histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = siteGovId, link_strength = 100}) local histFigId = histFig.id @@ -100,6 +101,8 @@ local function addHistFigToSite(histFig, newSite) 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_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hfEventId, hfid = histFigId, state = 1, reason = -1, site = siteId}) + + return siteGov end ---@param unit df.unit @@ -178,6 +181,63 @@ local function removeUnitFromSiteEntity(unit, histFig, oldSite) end end +---@param unit df.unit +local function removeUnitFromAnySquad(unit) + if unit.military.squad_id == -1 then return end + local fortEnt = df.global.plotinfo.main.fortress_entity + + -- remove from squad records + local squadId = unit.military.squad_id + local squadPos = unit.military.squad_position + local squad = df.squad.find(squadId) + if not squad then qerror("could not find squad") end + + squad.positions[squadPos].occupant = -1 + + -- remove from unit information + unit.military.squad_id = -1 + unit.military.squad_position = -1 + + -- remove assignment if captain or commander + local assignmentId = -1 + if squadPos == 0 then + for _, np in ipairs(dfhack.units.getNoblePositions(unit) or {}) do + if np.entity.id ~= fortEnt then goto continue end + if np.assignment.squad_id ~= squadId then goto continue end + + np.assignment.histfig = -1 + np.assignment.histfig2 = -1 + assignmentId = np.assignment.id + ::continue:: + end + end + + -- remove the old entity link and create new one to indicate former membership + local histFig = df.historical_figure.find(unit.hist_figure_id) + local yearOfPos = -1 + if not histFig then qerror("could not find histfig") end + for k,v in ipairs(histFig.entity_links) do + if df.histfig_entity_link_positionst:is_instance(v) + and v.assignment_id == assignmentId + and v.entity_id == fortEnt.id + then + histFig.entity_links:erase(k) -- unit was captain/commander + yearOfPos = v.start_year + elseif df.histfig_entity_link_squadst:is_instance(v) and v.squad_id == squadId then + histFig.entity_links:erase(k) -- unit was regular soldier + yearOfPos = v.start_year + else goto continue end + break + ::continue:: + end + + if assignmentId ~= -1 then -- unit was a captain/commander + histFig.entity_links:insert("#", {new = df.histfig_entity_link_former_positionst, entity_id = fortEnt.id, assignment_id = assignmentId, start_year = yearOfPos, end_year = df.global.cur_year, link_strength = 100}) + else + histFig.entity_links:insert("#", {new = df.histfig_entity_link_former_squadst, entity_id = fortEnt.id, squad_id = squadId, start_year = yearOfPos, end_year = df.global.cur_year, link_strength = 100}) + end +end + -- adapted from emigration::desert() ---@param unit df.unit ---@param toSite df.world_site @@ -186,6 +246,7 @@ function emigrate(unit, toSite) if histFig == nil then qerror("could not find histfig!") end local fortEnt = df.global.plotinfo.main.fortress_entity + removeUnitFromAnySquad(unit) -- self-explanatory -- mark for leaving unit.following = nil @@ -204,12 +265,13 @@ function emigrate(unit, toSite) end removeUnitFromSiteEntity(unit, histFig, fortEnt) - addHistFigToSite(histFig, toSite) + local siteGov = addHistFigToSite(histFig, toSite) -- announce the changes local unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) local siteName = dfhack.df2console(dfhack.translation.translateName(toSite.name, true)) - local line = unitName .. " has left to govern " .. siteName .. "." + 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 @@ -262,10 +324,7 @@ function main() local site = record.site local nobleName = dfhack.units.getReadableName(noble) - if noble.military.squad_id ~= -1 then - local squadName = dfhack.military.getSquadName(noble.military.squad_id) - print("[-] "..nobleName.." is a soldier of "..squadName..". Unassign from squad and try again.") - elseif inStrangeMood(noble) then + if inStrangeMood(noble) then print("[-] "..nobleName.." is in a strange mood! Leave alone for now.") else emigrate(noble, site) From c521f5fe51372117a522ecb712c861544706955b Mon Sep 17 00:00:00 2001 From: yg-ong Date: Fri, 7 Feb 2025 20:53:56 +0800 Subject: [PATCH 16/43] Remove squad removal logic --- docs/emigrate-nobles.rst | 8 +++++--- emigrate-nobles.lua | 41 +++++++++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/docs/emigrate-nobles.rst b/docs/emigrate-nobles.rst index 53ecf9124f..eba1071b70 100644 --- a/docs/emigrate-nobles.rst +++ b/docs/emigrate-nobles.rst @@ -8,10 +8,12 @@ emigrate-nobles Tired of inherited nobility freeloading off your fortress making inane demands? Use this tool to have them (willingly) emigrate to their rightful lands. -The unit must not be assigned to a squad. +Nobles assigned to squads will not be emigrated. Remove them from the squad before retrying. -Warning! Nobles will not surrender any symbols of office before leaving, be sure to unassign -your artefacts before using this tool. +.. warning:: + + Emigrated nobles will not surrender any symbols of office before leaving. + Unassign your artefacts before using this tool. Usage ----- diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua index 58a4fd16ca..e12d3299e7 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -182,7 +182,7 @@ local function removeUnitFromSiteEntity(unit, histFig, oldSite) end ---@param unit df.unit -local function removeUnitFromAnySquad(unit) +local function removeUnitFromSquad(unit) if unit.military.squad_id == -1 then return end local fortEnt = df.global.plotinfo.main.fortress_entity @@ -200,17 +200,18 @@ local function removeUnitFromAnySquad(unit) -- remove assignment if captain or commander local assignmentId = -1 - if squadPos == 0 then - for _, np in ipairs(dfhack.units.getNoblePositions(unit) or {}) do - if np.entity.id ~= fortEnt then goto continue end - if np.assignment.squad_id ~= squadId then goto continue end - - np.assignment.histfig = -1 - np.assignment.histfig2 = -1 - assignmentId = np.assignment.id - ::continue:: - end + if squadPos ~= 0 then goto next end + for _, np in ipairs(dfhack.units.getNoblePositions(unit) or {}) do + if np.entity.id ~= fortEnt then goto continue end + if np.assignment.squad_id ~= squadId then goto continue end + + np.assignment.histfig = -1 + np.assignment.histfig2 = -1 + assignmentId = np.assignment.id + break + ::continue:: end + ::next:: -- remove the old entity link and create new one to indicate former membership local histFig = df.historical_figure.find(unit.hist_figure_id) @@ -246,7 +247,6 @@ function emigrate(unit, toSite) if histFig == nil then qerror("could not find histfig!") end local fortEnt = df.global.plotinfo.main.fortress_entity - removeUnitFromAnySquad(unit) -- self-explanatory -- mark for leaving unit.following = nil @@ -285,6 +285,11 @@ local function inStrangeMood(unit) 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 + function listNoblesFound(nobleList) for _, record in pairs(nobleList) do local unit = record.unit @@ -292,7 +297,15 @@ function listNoblesFound(nobleList) local nobleName = dfhack.df2console(dfhack.units.getReadableName(unit)) local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) - print(unit.id..": "..nobleName.." - to be sent to "..siteName) + local unitMsg = unit.id..": "..nobleName.." to be sent to "..siteName + if isSoldier(unit) then + local squad = df.squad.find(unit.military.squad_id) + if not squad then qerror("could not find unit's squad") end + local squadName = dfhack.df2console(dfhack.translation.translateName(squad.name, true)) + unitMsg = unitMsg.." [!] Unit is soldier in "..squadName + end + + print(unitMsg) end end @@ -326,6 +339,8 @@ function main() local nobleName = dfhack.units.getReadableName(noble) if inStrangeMood(noble) then print("[-] "..nobleName.." is in a strange mood! Leave alone for now.") + elseif isSoldier(noble) then + print("[-] "..nobleName.." is in a squad! Unassign the unit before proceeding.") else emigrate(noble, site) end From fdb16986b0c8dac173f3b35d5ec44537f465fd7f Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 00:04:57 +0800 Subject: [PATCH 17/43] Remove unused function --- emigrate-nobles.lua | 58 --------------------------------------------- 1 file changed, 58 deletions(-) diff --git a/emigrate-nobles.lua b/emigrate-nobles.lua index e12d3299e7..f6626afa19 100644 --- a/emigrate-nobles.lua +++ b/emigrate-nobles.lua @@ -181,64 +181,6 @@ local function removeUnitFromSiteEntity(unit, histFig, oldSite) end end ----@param unit df.unit -local function removeUnitFromSquad(unit) - if unit.military.squad_id == -1 then return end - local fortEnt = df.global.plotinfo.main.fortress_entity - - -- remove from squad records - local squadId = unit.military.squad_id - local squadPos = unit.military.squad_position - local squad = df.squad.find(squadId) - if not squad then qerror("could not find squad") end - - squad.positions[squadPos].occupant = -1 - - -- remove from unit information - unit.military.squad_id = -1 - unit.military.squad_position = -1 - - -- remove assignment if captain or commander - local assignmentId = -1 - if squadPos ~= 0 then goto next end - for _, np in ipairs(dfhack.units.getNoblePositions(unit) or {}) do - if np.entity.id ~= fortEnt then goto continue end - if np.assignment.squad_id ~= squadId then goto continue end - - np.assignment.histfig = -1 - np.assignment.histfig2 = -1 - assignmentId = np.assignment.id - break - ::continue:: - end - ::next:: - - -- remove the old entity link and create new one to indicate former membership - local histFig = df.historical_figure.find(unit.hist_figure_id) - local yearOfPos = -1 - if not histFig then qerror("could not find histfig") end - for k,v in ipairs(histFig.entity_links) do - if df.histfig_entity_link_positionst:is_instance(v) - and v.assignment_id == assignmentId - and v.entity_id == fortEnt.id - then - histFig.entity_links:erase(k) -- unit was captain/commander - yearOfPos = v.start_year - elseif df.histfig_entity_link_squadst:is_instance(v) and v.squad_id == squadId then - histFig.entity_links:erase(k) -- unit was regular soldier - yearOfPos = v.start_year - else goto continue end - break - ::continue:: - end - - if assignmentId ~= -1 then -- unit was a captain/commander - histFig.entity_links:insert("#", {new = df.histfig_entity_link_former_positionst, entity_id = fortEnt.id, assignment_id = assignmentId, start_year = yearOfPos, end_year = df.global.cur_year, link_strength = 100}) - else - histFig.entity_links:insert("#", {new = df.histfig_entity_link_former_squadst, entity_id = fortEnt.id, squad_id = squadId, start_year = yearOfPos, end_year = df.global.cur_year, link_strength = 100}) - end -end - -- adapted from emigration::desert() ---@param unit df.unit ---@param toSite df.world_site From c4ac1d6c5a5a61337280dca13c46b2e2f87a8a1f Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 02:14:27 +0800 Subject: [PATCH 18/43] Move emigrate-nobles into emigration --- emigration.lua | 4 + .../emigration/emigrate-nobles.lua | 81 +++++++++++-------- 2 files changed, 50 insertions(+), 35 deletions(-) rename emigrate-nobles.lua => internal/emigration/emigrate-nobles.lua (86%) diff --git a/emigration.lua b/emigration.lua index ea494a7e36..e4f179f65b 100644 --- a/emigration.lua +++ b/emigration.lua @@ -2,6 +2,7 @@ --@enable = true local utils = require('utils') +local nobles = reqscript('internal/emigration/emigrate-nobles') local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence @@ -251,6 +252,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/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua similarity index 86% rename from emigrate-nobles.lua rename to internal/emigration/emigrate-nobles.lua index f6626afa19..1d2eeff656 100644 --- a/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -1,3 +1,5 @@ +--@module = true + --Deport resident nobles of other lands and summon your rightful lord if he/she is elsewhere --[[ @@ -9,15 +11,14 @@ TODO: local argparse = require("argparse") local options = { - help = false, all = false, unitId = -1, list = false } -fort = nil ---@type df.world_site -civ = nil ---@type df.historical_entity -capital = nil ---@type df.world_site +local fort = nil ---@type df.world_site +local playerCiv = nil ---@type df.historical_entity +local capital = nil ---@type df.world_site -- adapted from Units::get_land_title() ---@return df.world_site|nil @@ -38,18 +39,18 @@ end ---@return df.world_site|nil local function findCapital(civ) - local capital = nil + local civCapital = nil for _, link in ipairs(civ.site_links) do if link.flags.capital then - capital = df.world_site.find(link.target) + civCapital = df.world_site.find(link.target) break end end - return capital + return civCapital end -function addNobleOfOtherSite(unit, nobleList) +local function addNobleOfOtherSite(unit, nobleList) local nps = dfhack.units.getNoblePositions(unit) or {} local noblePos = nil for _, np in ipairs(nps) do @@ -83,7 +84,6 @@ end local function addHistFigToSite(histFig, newSite) -- have unit join site government local siteGovId = newSite.cur_owner_id - print("new site gov = "..siteGovId) histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = siteGovId, link_strength = 100}) local histFigId = histFig.id @@ -184,7 +184,7 @@ end -- adapted from emigration::desert() ---@param unit df.unit ---@param toSite df.world_site -function emigrate(unit, toSite) +local function emigrate(unit, toSite) local histFig = df.historical_figure.find(unit.hist_figure_id) if histFig == nil then qerror("could not find histfig!") end @@ -192,7 +192,7 @@ function emigrate(unit, toSite) -- mark for leaving unit.following = nil - unit.civ_id = civ.id -- should be redundant but oh well + unit.civ_id = playerCiv.id -- should be redundant but oh well unit.flags1.forest = true unit.flags2.visitor = true unit.animal.leave_countdown = 2 @@ -232,26 +232,28 @@ local function isSoldier(unit) return unit.military.squad_id ~= -1 end -function listNoblesFound(nobleList) +local function listNoblesFound(nobleList) for _, record in pairs(nobleList) do local unit = record.unit local site = record.site local nobleName = dfhack.df2console(dfhack.units.getReadableName(unit)) - local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) - local unitMsg = unit.id..": "..nobleName.." to be sent to "..siteName + local unitMsg = unit.id..": "..nobleName if isSoldier(unit) then local squad = df.squad.find(unit.military.squad_id) if not squad then qerror("could not find unit's squad") end local squadName = dfhack.df2console(dfhack.translation.translateName(squad.name, true)) - unitMsg = unitMsg.." [!] Unit is soldier in "..squadName + unitMsg = "[!] "..unitMsg.." - soldier in "..squadName + else + local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) + unitMsg = unitMsg.." to be sent to "..siteName end print(unitMsg) end end -function main() +local function main() local freeloaders = {} for _, unit in ipairs(dfhack.units.getCitizens()) do if options.unitId ~= -1 and unit.id ~= options.unitId then goto continue end @@ -262,7 +264,7 @@ function main() end if #freeloaders == 0 then - if options.unitId ~= -1 then + if options.unitId == -1 then print("No eligible nobles to be emigrated.") else print("No eligible nobles found with ID = "..options.unitId) @@ -289,12 +291,7 @@ function main() end end -function initChecks() - if options.help then - print(dfhack.script_help()) - return false - end - +local function initChecks() if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then qerror('needs a loaded fortress map') return false @@ -303,10 +300,10 @@ function initChecks() fort = dfhack.world.getCurrentSite() if not fort then qerror("could not find current site") end - civ = df.historical_entity.find(df.global.plotinfo.civ_id) - if not civ then qerror("could not find current civ") end + playerCiv = df.historical_entity.find(df.global.plotinfo.civ_id) + if not playerCiv then qerror("could not find current civ") end - capital = findCapital(civ) + capital = findCapital(playerCiv) if not capital then qerror("could not find capital") end if options.list then return true end -- list option does not require unit options @@ -324,18 +321,32 @@ function initChecks() return true end +local function resetState() + options.all = false + options.unitId = -1 + options.list = false + + fort = nil + capital = nil + playerCiv = nil +end + ------------------------------ -- [[ SCRIPT STARTS HERE ]] -- ------------------------------ -argparse.processArgsGetopt({...}, { - {"h", "help", handler=function() options.help = true end}, - {"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} -}) +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} + }) -pass = initChecks() -if not pass then return end + pass = initChecks() + if not pass then goto reset end -main() + main() + + ::reset:: + resetState() +end From b2ad9d1820248e3f48bdf61c9b9dac8d647a0b5d Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 02:22:59 +0800 Subject: [PATCH 19/43] Move emigrate-nobles.rst into emigration.rst --- docs/emigrate-nobles.rst | 44 ---------------------------------------- docs/emigration.rst | 34 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 44 deletions(-) delete mode 100644 docs/emigrate-nobles.rst diff --git a/docs/emigrate-nobles.rst b/docs/emigrate-nobles.rst deleted file mode 100644 index eba1071b70..0000000000 --- a/docs/emigrate-nobles.rst +++ /dev/null @@ -1,44 +0,0 @@ -emigrate-nobles -=============== - -.. dfhack-tool:: - :summary: Allow inherited nobles to emigrate from fort to their site of governance. - :tags: fort units - -Tired of inherited nobility freeloading off your fortress making inane demands? Use this tool -to have them (willingly) emigrate to their rightful lands. - -Nobles assigned to squads will not be emigrated. Remove them from the squad before retrying. - -.. warning:: - - Emigrated nobles will not surrender any symbols of office before leaving. - Unassign your artefacts before using this tool. - -Usage ------ - -:: - - emigrate-nobles [--list] - emigrate-nobles - -Examples --------- - -``emigrate-nobles --list`` - List all nobles that do not rule your fortress -``emigrate-nobles --all`` - Emigrate all nobles that do not rule your fortress -``emigrate-nobles --unit 34534`` - Emigrate a noble matching the specified unit ID that does not rule your fortress - -Options -------- - -``-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/docs/emigration.rst b/docs/emigration.rst index 58a3d623e7..23a34ad97f 100644 --- a/docs/emigration.rst +++ b/docs/emigration.rst @@ -16,9 +16,43 @@ 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 will not be emigrated. +Remove them from the squad before retrying. + +.. warning:: + + Emigrated nobles will not surrender any symbols of office before leaving. + Unassign your artefacts before calling ``emigration nobles``. + Usage ----- :: enable emigration + emigration nobles [--list] + emigration nobles + +Examples +-------- + +``emigration nobles --list`` + List all nobles that do not rule your fortress +``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 From 07e633027480a08b574d33589c7408d41faa8baa Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 8 Feb 2025 14:27:46 +0800 Subject: [PATCH 20/43] Update docs/emigration.rst Co-authored-by: Myk --- docs/emigration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/emigration.rst b/docs/emigration.rst index 23a34ad97f..d04e07e178 100644 --- a/docs/emigration.rst +++ b/docs/emigration.rst @@ -33,7 +33,7 @@ Usage enable emigration emigration nobles [--list] - emigration nobles + emigration nobles [] Examples -------- From 39dac77ac019384ea53709f42c1354d58e42e6c6 Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 8 Feb 2025 14:29:12 +0800 Subject: [PATCH 21/43] Update internal/emigration/emigrate-nobles.lua Co-authored-by: Myk --- internal/emigration/emigrate-nobles.lua | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 1d2eeff656..b7f1664fef 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -342,11 +342,9 @@ function run(args) {"l", "list", handler=function() options.list = true end} }) - pass = initChecks() - if not pass then goto reset end - - main() + if initChecks() then + main() + end - ::reset:: resetState() end From a6d65e45e8c4e13cf91d0eaf64f1547b5d280d10 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 15:04:38 +0800 Subject: [PATCH 22/43] Remove module globals --- internal/emigration/emigrate-nobles.lua | 69 ++++++++++++------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index b7f1664fef..599f68823b 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -16,10 +16,6 @@ local options = { list = false } -local fort = nil ---@type df.world_site -local playerCiv = nil ---@type df.historical_entity -local capital = nil ---@type df.world_site - -- adapted from Units::get_land_title() ---@return df.world_site|nil local function findSiteOfRule(np) @@ -50,7 +46,11 @@ local function findCapital(civ) return civCapital end -local function addNobleOfOtherSite(unit, nobleList) +---@param unit df.unit +---@param nobleList { unit: df.unit, site: df.world_site }[] +---@param playerFort df.world_site +---@param civ df.historical_entity +local function addNobleOfOtherSite(unit, nobleList, playerFort, civ) local nps = dfhack.units.getNoblePositions(unit) or {} local noblePos = nil for _, np in ipairs(nps) do @@ -64,7 +64,8 @@ local function addNobleOfOtherSite(unit, nobleList) -- Monarchs do not seem to have an world_site associated to them (?) if noblePos.position.code == "MONARCH" then - if capital.id ~= fort.id then + local capital = findCapital(civ) + if capital and capital.id ~= playerFort.id then table.insert(nobleList, {unit = unit, site = capital}) end return @@ -75,7 +76,7 @@ local function addNobleOfOtherSite(unit, nobleList) local site = findSiteOfRule(noblePos) if not site then qerror("could not find land of "..name) end - if site.id == fort.id then return end -- noble rules current fort + if site.id == playerFort.id then return end -- noble rules current fort table.insert(nobleList, {unit = unit, site = site}) end @@ -182,9 +183,10 @@ local function removeUnitFromSiteEntity(unit, histFig, oldSite) end -- adapted from emigration::desert() ----@param unit df.unit ----@param toSite df.world_site -local function emigrate(unit, toSite) +---@param unit df.unit +---@param toSite df.world_site +---@param civ df.historical_entity +local function emigrate(unit, toSite, civ) local histFig = df.historical_figure.find(unit.hist_figure_id) if histFig == nil then qerror("could not find histfig!") end @@ -192,7 +194,7 @@ local function emigrate(unit, toSite) -- mark for leaving unit.following = nil - unit.civ_id = playerCiv.id -- should be redundant but oh well + unit.civ_id = civ.id -- should be redundant but oh well unit.flags1.forest = true unit.flags2.visitor = true unit.animal.leave_countdown = 2 @@ -253,22 +255,33 @@ local function listNoblesFound(nobleList) end end +local function printNoNobles() + if options.unitId == -1 then + print("No eligible nobles to be emigrated.") + else + print("No eligible nobles found with ID = "..options.unitId) + end +end + local function main() + local fort = dfhack.world.getCurrentSite() + if not fort then qerror("could not find current site") end + + 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 }[] local freeloaders = {} for _, unit in ipairs(dfhack.units.getCitizens()) do if options.unitId ~= -1 and unit.id ~= options.unitId then goto continue end - if dfhack.units.isDead(unit) or not dfhack.units.isSane(unit) then goto continue end - addNobleOfOtherSite(unit, freeloaders) + addNobleOfOtherSite(unit, freeloaders, fort, civ) ::continue:: end if #freeloaders == 0 then - if options.unitId == -1 then - print("No eligible nobles to be emigrated.") - else - print("No eligible nobles found with ID = "..options.unitId) - end + printNoNobles() + return end if options.list then @@ -286,7 +299,7 @@ local function main() elseif isSoldier(noble) then print("[-] "..nobleName.." is in a squad! Unassign the unit before proceeding.") else - emigrate(noble, site) + emigrate(noble, site, civ) end end end @@ -294,18 +307,8 @@ end local function initChecks() if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then qerror('needs a loaded fortress map') - return false end - fort = dfhack.world.getCurrentSite() - if not fort then qerror("could not find current site") end - - playerCiv = df.historical_entity.find(df.global.plotinfo.civ_id) - if not playerCiv then qerror("could not find current civ") end - - capital = findCapital(playerCiv) - if not capital then qerror("could not find capital") end - if options.list then return true end -- list option does not require unit options local noOptions = options.unitId == -1 and not options.all @@ -321,14 +324,10 @@ local function initChecks() return true end -local function resetState() +local function resetOptions() options.all = false options.unitId = -1 options.list = false - - fort = nil - capital = nil - playerCiv = nil end ------------------------------ @@ -346,5 +345,5 @@ function run(args) main() end - resetState() + resetOptions() end From b1cefb210677223f5b67c666818af0f0dcb9155f Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 15:07:18 +0800 Subject: [PATCH 23/43] Fix pair iteration --- internal/emigration/emigrate-nobles.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 599f68823b..d8a0bd7518 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -234,8 +234,9 @@ local function isSoldier(unit) return unit.military.squad_id ~= -1 end +---@param nobleList { unit: df.unit, site: df.world_site }[] local function listNoblesFound(nobleList) - for _, record in pairs(nobleList) do + for _, record in ipairs(nobleList) do local unit = record.unit local site = record.site @@ -289,7 +290,7 @@ local function main() return end - for _, record in pairs(freeloaders) do + for _, record in ipairs(freeloaders) do local noble = record.unit local site = record.site From ecd42a550b958f2b7c7a871c98e9f59a7d9c293a Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 16:13:20 +0800 Subject: [PATCH 24/43] Refactor logic --- emigration.lua | 81 +----------- internal/emigration/emigrate-nobles.lua | 129 +++--------------- internal/emigration/unit-link-utils.lua | 167 ++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 188 deletions(-) create mode 100644 internal/emigration/unit-link-utils.lua diff --git a/emigration.lua b/emigration.lua index e4f179f65b..c9b5af28d0 100644 --- a/emigration.lua +++ b/emigration.lua @@ -2,7 +2,9 @@ --@enable = true 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 @@ -38,15 +40,12 @@ 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 @@ -54,74 +53,8 @@ function desert(u,method,civ) 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 diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index d8a0bd7518..48f976ffaa 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -9,6 +9,7 @@ TODO: ]]-- local argparse = require("argparse") +local unit_link_utils = reqscript("unit-site-links") local options = { all = false, @@ -80,124 +81,19 @@ local function addNobleOfOtherSite(unit, nobleList, playerFort, civ) table.insert(nobleList, {unit = unit, site = site}) end ----@param histFig df.historical_figure ----@param newSite df.world_site -local function addHistFigToSite(histFig, newSite) - -- have unit join site government - local siteGovId = newSite.cur_owner_id - histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = siteGovId, link_strength = 100}) - local histFigId = histFig.id - - -- have unit join new site - local siteId = newSite.id - local siteGov = df.historical_entity.find(siteGovId) - if not siteGov then qerror("could not find site!") end - - 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 = siteGovId, histfig = histFigId, link_type = 0}) - - 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_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hfEventId, hfid = histFigId, state = 1, reason = -1, site = siteId}) - - return siteGov -end - ----@param unit df.unit ----@param histFig df.historical_figure ----@param oldSite df.historical_entity -local function removeUnitFromSiteEntity(unit, histFig, oldSite) - local histFigId = histFig.id - - -- 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 - - -- erase the unit from the fortress entity - for k,v in ipairs(oldSite.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(oldSite.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(oldSite.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 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 = oldSite.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 == oldSite.id then - histFig.entity_links:erase(k) - break - end - end -end - -- adapted from emigration::desert() ---@param unit df.unit ---@param toSite df.world_site ---@param civ df.historical_entity local function emigrate(unit, toSite, civ) local histFig = df.historical_figure.find(unit.hist_figure_id) - if histFig == nil then qerror("could not find histfig!") end + if not histFig then + print("Could not find associated historical figure!") + return + end local fortEnt = df.global.plotinfo.main.fortress_entity - - -- mark for leaving - unit.following = nil - unit.civ_id = civ.id -- should be redundant but oh well - unit.flags1.forest = true - unit.flags2.visitor = true - unit.animal.leave_countdown = 2 + 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 @@ -208,8 +104,11 @@ local function emigrate(unit, toSite, civ) if act then act.events[0].flags.dismissed = true end end - removeUnitFromSiteEntity(unit, histFig, fortEnt) - local siteGov = addHistFigToSite(histFig, toSite) + unit_link_utils.removeUnitAssociations(unit) + unit_link_utils.removeHistFigFromEntity(histFig, fortEnt) + + local siteGov = unit_link_utils.addHistFigToSite(histFig, toSite) + if not siteGov then qerror("could not add unit to new site") end -- announce the changes local unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) @@ -244,8 +143,10 @@ local function listNoblesFound(nobleList) local unitMsg = unit.id..": "..nobleName if isSoldier(unit) then local squad = df.squad.find(unit.military.squad_id) - if not squad then qerror("could not find unit's squad") end - local squadName = dfhack.df2console(dfhack.translation.translateName(squad.name, true)) + local squadName = squad + and dfhack.df2console(dfhack.translation.translateName(squad.name, true)) + or "unknown squad" + unitMsg = "[!] "..unitMsg.." - soldier in "..squadName else local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua new file mode 100644 index 0000000000..9ed6d22292 --- /dev/null +++ b/internal/emigration/unit-link-utils.lua @@ -0,0 +1,167 @@ +--@module true + +---@param histFig df.historical_figure +---@param oldEntity df.historical_entity +function removeHistFigFromEntity(histFig, oldEntity) + 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 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 + +---@param histFig df.historical_figure +---@param newEntity df.historical_entity +function addHistFigToEntity(histFig, newEntity) + if not histFig or not newEntity then return end + + local histFigId = histFig.id + local newEntId = newEntity.id + + -- have unit join site government + histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newEntId, link_strength = 100}) + + -- create event indicating new membership + newEntity.histfig_ids:insert('#', histFigId) + newEntity.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 = newEntId, histfig = histFigId, link_type = 0}) +end + +---@param histFig df.historical_figure +---@param newSite df.historical_entity +function createHistFigJoinSiteEvent(histFig, newSite) + if not histFig or not newSite then return end + + -- create event indicating histfig moved to site + local histFigId = histFig.id + local siteId = newSite.id + 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 = 1, reason = -1, site = siteId}) +end + +---@param histFig df.historical_figure +---@param entity df.historical_entity +function insertNewHistFigEntityLink(histFig, entity) + if not histFig or not entity then return end + + local entityId = entity.id + histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = entityId, link_strength = 100}) +end + +---@param histFig df.historical_figure +---@param newSite df.world_site +---@return df.historical_entity|nil siteGov New site entity histfig is associated with +function addHistFigToSite(histFig, newSite) + if not histFig or not newSite then return nil end + + -- have unit join site government + local siteGovId = newSite.cur_owner_id + histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = siteGovId, link_strength = 100}) + local histFigId = histFig.id + + -- have unit join new site + local siteId = newSite.id + local siteGov = df.historical_entity.find(siteGovId) + if not siteGov then qerror("could not find site!") end + + 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 = siteGovId, histfig = histFigId, link_type = 0}) + + 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 = 1, reason = -1, site = siteId}) + + return siteGov +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 From ec335cb01e58d218a17a0e3584111f5c2b0eef8b Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 17:03:58 +0800 Subject: [PATCH 25/43] Refactor emigration logic --- emigration.lua | 26 +++------- internal/emigration/emigrate-nobles.lua | 6 ++- internal/emigration/unit-link-utils.lua | 66 ++++++------------------- 3 files changed, 25 insertions(+), 73 deletions(-) diff --git a/emigration.lua b/emigration.lua index c9b5af28d0..1bba9810e1 100644 --- a/emigration.lua +++ b/emigration.lua @@ -46,7 +46,6 @@ function desert(u,method,civ) 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) @@ -57,35 +56,24 @@ function desert(u,method,civ) 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) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 48f976ffaa..89acce67c3 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -107,8 +107,10 @@ local function emigrate(unit, toSite, civ) unit_link_utils.removeUnitAssociations(unit) unit_link_utils.removeHistFigFromEntity(histFig, fortEnt) - local siteGov = unit_link_utils.addHistFigToSite(histFig, toSite) - if not siteGov then qerror("could not add unit to new site") end + -- 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)) diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua index 9ed6d22292..104b67ce68 100644 --- a/internal/emigration/unit-link-utils.lua +++ b/internal/emigration/unit-link-utils.lua @@ -38,74 +38,36 @@ function removeHistFigFromEntity(histFig, oldEntity) end end ----@param histFig df.historical_figure ----@param newEntity df.historical_entity -function addHistFigToEntity(histFig, newEntity) - if not histFig or not newEntity then return end - - local histFigId = histFig.id - local newEntId = newEntity.id - - -- have unit join site government - histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newEntId, link_strength = 100}) - - -- create event indicating new membership - newEntity.histfig_ids:insert('#', histFigId) - newEntity.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 = newEntId, histfig = histFigId, link_type = 0}) -end - ----@param histFig df.historical_figure ----@param newSite df.historical_entity -function createHistFigJoinSiteEvent(histFig, newSite) - if not histFig or not newSite then return end - - -- create event indicating histfig moved to site - local histFigId = histFig.id - local siteId = newSite.id - 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 = 1, reason = -1, site = siteId}) -end - ---@param histFig df.historical_figure ---@param entity df.historical_entity -function insertNewHistFigEntityLink(histFig, entity) - if not histFig or not entity then return end - - local entityId = entity.id - histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = entityId, link_strength = 100}) +function addNewHistFigEntityLink(histFig, entity) end +---Creates events indicating a histfig's move to a new site and joining its entity. ---@param histFig df.historical_figure ----@param newSite df.world_site ----@return df.historical_entity|nil siteGov New site entity histfig is associated with -function addHistFigToSite(histFig, newSite) - if not histFig or not newSite then return nil end - - -- have unit join site government - local siteGovId = newSite.cur_owner_id - histFig.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = siteGovId, link_strength = 100}) +---@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 - -- have unit join new site - local siteId = newSite.id - local siteGov = df.historical_entity.find(siteGovId) - if not siteGov then qerror("could not find site!") end + -- 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 = siteGovId, histfig = histFigId, link_type = 0}) + 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 = 0}) + + 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 = 1, reason = -1, site = siteId}) - - return siteGov end ---@param unit df.unit From 32afe4e51091f6d7fd1036cdd2e65e73ac1da7a9 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 17:23:24 +0800 Subject: [PATCH 26/43] Fix stupid typo --- internal/emigration/emigrate-nobles.lua | 3 ++- internal/emigration/unit-link-utils.lua | 7 +------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 89acce67c3..8a29fe5802 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -1,6 +1,6 @@ --@module = true ---Deport resident nobles of other lands and summon your rightful lord if he/she is elsewhere +--Deport resident nobles of other lands --[[ TODO: @@ -9,6 +9,7 @@ TODO: ]]-- local argparse = require("argparse") + local unit_link_utils = reqscript("unit-site-links") local options = { diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua index 104b67ce68..2188bb7117 100644 --- a/internal/emigration/unit-link-utils.lua +++ b/internal/emigration/unit-link-utils.lua @@ -1,4 +1,4 @@ ---@module true +--@module = true ---@param histFig df.historical_figure ---@param oldEntity df.historical_entity @@ -38,11 +38,6 @@ function removeHistFigFromEntity(histFig, oldEntity) end end ----@param histFig df.historical_figure ----@param entity df.historical_entity -function addNewHistFigEntityLink(histFig, entity) -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 From 4fefcfb3c31d9f7297afded2f2fd86ee6163a03e Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 17:24:11 +0800 Subject: [PATCH 27/43] Fix stupid typo --- internal/emigration/emigrate-nobles.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 8a29fe5802..0c28e0a251 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -10,7 +10,7 @@ TODO: local argparse = require("argparse") -local unit_link_utils = reqscript("unit-site-links") +local unit_link_utils = reqscript("unit-link-utils") local options = { all = false, From 4419093ea97452fa306d8e4d44c757c7c83c3a51 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 17:30:49 +0800 Subject: [PATCH 28/43] Fix import error --- internal/emigration/emigrate-nobles.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 0c28e0a251..912b44fbf9 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -10,7 +10,7 @@ TODO: local argparse = require("argparse") -local unit_link_utils = reqscript("unit-link-utils") +local unit_link_utils = reqscript("internal/emigration/unit-link-utils") local options = { all = false, @@ -200,9 +200,9 @@ local function main() local nobleName = dfhack.units.getReadableName(noble) if inStrangeMood(noble) then - print("[-] "..nobleName.." is in a strange mood! Leave alone for now.") + print("[!] "..nobleName.." is in a strange mood! Leave alone for now.") elseif isSoldier(noble) then - print("[-] "..nobleName.." is in a squad! Unassign the unit before proceeding.") + print("[!] "..nobleName.." is in a squad! Unassign the unit before proceeding.") else emigrate(noble, site, civ) end From 2b42b13f750e45cd59100a5108ca03406ed80eb1 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 18:33:11 +0800 Subject: [PATCH 29/43] Add support for current unit --- internal/emigration/emigrate-nobles.lua | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 912b44fbf9..24a8e87ee4 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -150,10 +150,10 @@ local function listNoblesFound(nobleList) and dfhack.df2console(dfhack.translation.translateName(squad.name, true)) or "unknown squad" - unitMsg = "[!] "..unitMsg.." - soldier in "..squadName + unitMsg = "! "..unitMsg.." - soldier in "..squadName else local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) - unitMsg = unitMsg.." to be sent to "..siteName + unitMsg = " "..unitMsg.." - to "..siteName end print(unitMsg) @@ -164,7 +164,7 @@ local function printNoNobles() if options.unitId == -1 then print("No eligible nobles to be emigrated.") else - print("No eligible nobles found with ID = "..options.unitId) + print("Unit ID "..options.unitId.." is not an eligible noble.") end end @@ -218,8 +218,16 @@ local function initChecks() local noOptions = options.unitId == -1 and not options.all if noOptions then - print("No options selected, defaulting to list mode.") - options.list = true + 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 From 64c5b64759e6b6c79e330ca4cd65d68f1d54b8a9 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 8 Feb 2025 18:42:38 +0800 Subject: [PATCH 30/43] Disable cancelling special jobs --- internal/emigration/emigrate-nobles.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 24a8e87ee4..aafe471405 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -123,10 +123,12 @@ local function emigrate(unit, toSite, civ) end ---@param unit df.unit -local function inStrangeMood(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 @@ -199,8 +201,8 @@ local function main() local site = record.site local nobleName = dfhack.units.getReadableName(noble) - if inStrangeMood(noble) then - print("[!] "..nobleName.." is in a strange mood! Leave alone for now.") + if inSpecialJob(noble) then + print("[!] "..nobleName.." is busy! Leave alone for now.") elseif isSoldier(noble) then print("[!] "..nobleName.." is in a squad! Unassign the unit before proceeding.") else From 16ff909d8dc4301277e7bc5a3c899ceb7873b93d Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 9 Feb 2025 13:34:56 +0800 Subject: [PATCH 31/43] Add mandate removal logic --- internal/emigration/emigrate-nobles.lua | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index aafe471405..0fbe5bdcfc 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -1,7 +1,5 @@ --@module = true ---Deport resident nobles of other lands - --[[ TODO: * Feature: have rightful ruler immigrate to fort if off-site @@ -48,10 +46,10 @@ local function findCapital(civ) return civCapital end ----@param unit df.unit ----@param nobleList { unit: df.unit, site: df.world_site }[] ----@param playerFort df.world_site ----@param civ df.historical_entity +---@param unit df.unit +---@param nobleList { unit: df.unit, site: df.world_site }[] +---@param playerFort df.world_site +---@param civ df.historical_entity local function addNobleOfOtherSite(unit, nobleList, playerFort, civ) local nps = dfhack.units.getNoblePositions(unit) or {} local noblePos = nil @@ -82,6 +80,18 @@ local function addNobleOfOtherSite(unit, nobleList, playerFort, civ) table.insert(nobleList, {unit = unit, site = site}) end +---@param unit df.unit +local function removeMandates(unit) + local mandates = df.global.world.mandates + 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 @@ -105,6 +115,9 @@ local function emigrate(unit, toSite, civ) 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, fortEnt) From 1fa7b042ace086d3506bd5a40ad0749b8c74483e Mon Sep 17 00:00:00 2001 From: yg-ong Date: Mon, 10 Feb 2025 00:09:31 +0800 Subject: [PATCH 32/43] Add check for fort admins --- internal/emigration/emigrate-nobles.lua | 64 ++++++++++++++++++------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 0fbe5bdcfc..2d769d9791 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -48,12 +48,13 @@ end ---@param unit df.unit ---@param nobleList { unit: df.unit, site: df.world_site }[] ----@param playerFort df.world_site +---@param thisSite df.world_site ---@param civ df.historical_entity -local function addNobleOfOtherSite(unit, nobleList, playerFort, civ) +local function addNobleOfOtherSite(unit, nobleList, thisSite, civ) local nps = dfhack.units.getNoblePositions(unit) or {} local noblePos = nil for _, np in ipairs(nps) do + -- TODO: also check if civ is not your fort? Some site govs have IS_LAW_MAKER positions if np.position.flags.IS_LAW_MAKER then noblePos = np break @@ -62,10 +63,14 @@ local function addNobleOfOtherSite(unit, nobleList, playerFort, civ) if not noblePos then return end -- unit is not nobility + -- TODO: support other races that may not use MONARCH as position code + -- entity.type == df.historical_entity_type.Civilization + -- position.flags.RULES_FROM_LOCATION + -- Monarchs do not seem to have an world_site associated to them (?) if noblePos.position.code == "MONARCH" then local capital = findCapital(civ) - if capital and capital.id ~= playerFort.id then + if capital and capital.id ~= thisSite.id then table.insert(nobleList, {unit = unit, site = capital}) end return @@ -76,7 +81,7 @@ local function addNobleOfOtherSite(unit, nobleList, playerFort, civ) local site = findSiteOfRule(noblePos) if not site then qerror("could not find land of "..name) end - if site.id == playerFort.id then return end -- noble rules current fort + if site.id == thisSite.id then return end -- noble rules current fort table.insert(nobleList, {unit = unit, site = site}) end @@ -93,17 +98,17 @@ local function removeMandates(unit) end -- adapted from emigration::desert() ----@param unit df.unit ----@param toSite df.world_site ----@param civ df.historical_entity -local function emigrate(unit, toSite, civ) +---@param unit df.unit +---@param toSite df.world_site +---@param prevEnt df.historical_entity +---@param civ df.historical_entity +local function emigrate(unit, toSite, prevEnt, civ) local histFig = df.historical_figure.find(unit.hist_figure_id) if not histFig then print("Could not find associated historical figure!") return end - local fortEnt = df.global.plotinfo.main.fortress_entity unit_link_utils.markUnitForEmigration(unit, civ.id, true) -- remove current job @@ -119,7 +124,7 @@ local function emigrate(unit, toSite, civ) removeMandates(unit) unit_link_utils.removeUnitAssociations(unit) - unit_link_utils.removeHistFigFromEntity(histFig, fortEnt) + unit_link_utils.removeHistFigFromEntity(histFig, prevEnt) -- have unit join new site government local siteGov = df.historical_entity.find(toSite.cur_owner_id) @@ -151,8 +156,27 @@ local function isSoldier(unit) return unit.military.squad_id ~= -1 end +---@param unit df.unit +---@param fortEnt df.historical_entity +---@param includeElected boolean +local function isAdministrator(unit, fortEnt, includeElected) + ---@diagnostic disable-next-line: missing-parameter + local nps = dfhack.units.getNoblePositions(unit) or {} + + ---@diagnostic disable-next-line: param-type-mismatch + for _, np in ipairs(nps) do + -- Elected officials can be chosen again + local isAdmin = np.entity.id == fortEnt.id + if not includeElected then isAdmin = isAdmin and not np.position.flags.ELECTED end + if isAdmin then return true end + end + return false +end + ---@param nobleList { unit: df.unit, site: df.world_site }[] -local function listNoblesFound(nobleList) +---@param fort df.world_site +---@param fortEnt df.historical_entity +local function listNoblesFound(nobleList, fort, fortEnt) for _, record in ipairs(nobleList) do local unit = record.unit local site = record.site @@ -166,6 +190,9 @@ local function listNoblesFound(nobleList) or "unknown squad" unitMsg = "! "..unitMsg.." - soldier in "..squadName + elseif isAdministrator(unit, fortEnt, true) then + local fortName = dfhack.df2console(dfhack.translation.translateName(fort.name, true)) + unitMsg = "! "..unitMsg.." - administrator of "..fortName else local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) unitMsg = " "..unitMsg.." - to "..siteName @@ -184,9 +211,12 @@ local function printNoNobles() end local function main() - local fort = dfhack.world.getCurrentSite() + ---@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 @@ -205,7 +235,7 @@ local function main() end if options.list then - listNoblesFound(freeloaders) + listNoblesFound(freeloaders, fort, fortEnt) return end @@ -215,11 +245,13 @@ local function main() local nobleName = dfhack.units.getReadableName(noble) if inSpecialJob(noble) then - print("[!] "..nobleName.." is busy! Leave alone for now.") + print("! "..nobleName.." is busy! Leave alone for now.") elseif isSoldier(noble) then - print("[!] "..nobleName.." is in a squad! Unassign the unit before proceeding.") + print("! "..nobleName.." is in a squad! Unassign the unit and try again.") + elseif isAdministrator(noble, fortEnt, false) then + print("! "..nobleName.." is an administrator! Unassign the unit and try again.") else - emigrate(noble, site, civ) + emigrate(noble, site, fortEnt, civ) end end end From 5501751450404dab1ccddf7e70c210b27f34a6b7 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Mon, 10 Feb 2025 00:13:01 +0800 Subject: [PATCH 33/43] Add signposts for navigation --- internal/emigration/emigrate-nobles.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 2d769d9791..236bb01889 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -140,6 +140,10 @@ local function emigrate(unit, toSite, prevEnt, civ) dfhack.gui.showAnnouncement(line, COLOR_WHITE) end +------------------------ +-- [[ GUARD CHECKS ]] -- +------------------------ + ---@param unit df.unit local function inSpecialJob(unit) local job = unit.job.current_job @@ -173,6 +177,10 @@ local function isAdministrator(unit, fortEnt, includeElected) return false end +----------------------- +-- [[ PRINT MODES ]] -- +----------------------- + ---@param nobleList { unit: df.unit, site: df.world_site }[] ---@param fort df.world_site ---@param fortEnt df.historical_entity @@ -210,6 +218,10 @@ local function printNoNobles() end end +------------------------- +-- [[ MAIN FUNCTION ]] -- +------------------------- + local function main() ---@diagnostic disable-next-line: assign-type-mismatch local fort = dfhack.world.getCurrentSite() ---@type df.world_site @@ -290,10 +302,6 @@ local function resetOptions() options.list = false end ------------------------------- --- [[ SCRIPT STARTS HERE ]] -- ------------------------------- - function run(args) argparse.processArgsGetopt(args, { {"a", "all", handler=function() options.all = true end}, From dd2718ea491b784fec590edbe2096754964b6d41 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Mon, 10 Feb 2025 00:27:48 +0800 Subject: [PATCH 34/43] Change isAdministrator --- internal/emigration/emigrate-nobles.lua | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 236bb01889..5753b73b72 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -162,17 +162,13 @@ end ---@param unit df.unit ---@param fortEnt df.historical_entity ----@param includeElected boolean -local function isAdministrator(unit, fortEnt, includeElected) +local function isAdministrator(unit, fortEnt) ---@diagnostic disable-next-line: missing-parameter local nps = dfhack.units.getNoblePositions(unit) or {} ---@diagnostic disable-next-line: param-type-mismatch for _, np in ipairs(nps) do - -- Elected officials can be chosen again - local isAdmin = np.entity.id == fortEnt.id - if not includeElected then isAdmin = isAdmin and not np.position.flags.ELECTED end - if isAdmin then return true end + if np.entity.id == fortEnt.id then return true end end return false end @@ -182,9 +178,8 @@ end ----------------------- ---@param nobleList { unit: df.unit, site: df.world_site }[] ----@param fort df.world_site ---@param fortEnt df.historical_entity -local function listNoblesFound(nobleList, fort, fortEnt) +local function listNoblesFound(nobleList, fortEnt) for _, record in ipairs(nobleList) do local unit = record.unit local site = record.site @@ -198,9 +193,8 @@ local function listNoblesFound(nobleList, fort, fortEnt) or "unknown squad" unitMsg = "! "..unitMsg.." - soldier in "..squadName - elseif isAdministrator(unit, fortEnt, true) then - local fortName = dfhack.df2console(dfhack.translation.translateName(fort.name, true)) - unitMsg = "! "..unitMsg.." - administrator of "..fortName + elseif isAdministrator(unit, fortEnt) then + unitMsg = "! "..unitMsg.." - administrator of this fort" else local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) unitMsg = " "..unitMsg.." - to "..siteName @@ -247,7 +241,7 @@ local function main() end if options.list then - listNoblesFound(freeloaders, fort, fortEnt) + listNoblesFound(freeloaders, fortEnt) return end @@ -260,7 +254,7 @@ local function main() print("! "..nobleName.." is busy! Leave alone for now.") elseif isSoldier(noble) then print("! "..nobleName.." is in a squad! Unassign the unit and try again.") - elseif isAdministrator(noble, fortEnt, false) then + elseif isAdministrator(noble, fortEnt) then print("! "..nobleName.." is an administrator! Unassign the unit and try again.") else emigrate(noble, site, fortEnt, civ) From 23728b8c6c671425eee670c40189367cecd81f61 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Mon, 10 Feb 2025 00:41:00 +0800 Subject: [PATCH 35/43] Add support for other civ monarchs, change print messages --- internal/emigration/emigrate-nobles.lua | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 5753b73b72..5d48c3312f 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -54,7 +54,6 @@ local function addNobleOfOtherSite(unit, nobleList, thisSite, civ) local nps = dfhack.units.getNoblePositions(unit) or {} local noblePos = nil for _, np in ipairs(nps) do - -- TODO: also check if civ is not your fort? Some site govs have IS_LAW_MAKER positions if np.position.flags.IS_LAW_MAKER then noblePos = np break @@ -63,12 +62,8 @@ local function addNobleOfOtherSite(unit, nobleList, thisSite, civ) if not noblePos then return end -- unit is not nobility - -- TODO: support other races that may not use MONARCH as position code - -- entity.type == df.historical_entity_type.Civilization - -- position.flags.RULES_FROM_LOCATION - -- Monarchs do not seem to have an world_site associated to them (?) - if noblePos.position.code == "MONARCH" then + 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}) @@ -136,7 +131,7 @@ local function emigrate(unit, toSite, prevEnt, civ) 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)) + print("+ "..dfhack.df2console(line)) dfhack.gui.showAnnouncement(line, COLOR_WHITE) end @@ -184,19 +179,19 @@ local function listNoblesFound(nobleList, fortEnt) local unit = record.unit local site = record.site - local nobleName = dfhack.df2console(dfhack.units.getReadableName(unit)) + 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.df2console(dfhack.translation.translateName(squad.name, true)) + and dfhack.translation.translateName(squad.name, true) or "unknown squad" unitMsg = "! "..unitMsg.." - soldier in "..squadName elseif isAdministrator(unit, fortEnt) then - unitMsg = "! "..unitMsg.." - administrator of this fort" + unitMsg = "! "..unitMsg.." - fort administrator" else - local siteName = dfhack.df2console(dfhack.translation.translateName(site.name, true)) + local siteName = dfhack.translation.translateName(site.name, true) unitMsg = " "..unitMsg.." - to "..siteName end @@ -253,9 +248,9 @@ local function main() if inSpecialJob(noble) then print("! "..nobleName.." is busy! Leave alone for now.") elseif isSoldier(noble) then - print("! "..nobleName.." is in a squad! Unassign the unit and try again.") + print("! "..nobleName.." is in a squad! Unassign unit and try again.") elseif isAdministrator(noble, fortEnt) then - print("! "..nobleName.." is an administrator! Unassign the unit and try again.") + print("! "..nobleName.." is an administrator! Unassign unit and try again.") else emigrate(noble, site, fortEnt, civ) end From 161207ebfdebd340bd7f31d5dc9b6f7c1210f169 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Mon, 10 Feb 2025 21:17:00 +0800 Subject: [PATCH 36/43] Add support for evicting noble mayors --- internal/emigration/emigrate-nobles.lua | 66 ++++++++++++++++++++----- internal/emigration/unit-link-utils.lua | 16 +++++- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 5d48c3312f..6c1d98c54f 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -97,7 +97,8 @@ end ---@param toSite df.world_site ---@param prevEnt df.historical_entity ---@param civ df.historical_entity -local function emigrate(unit, toSite, prevEnt, civ) +---@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!") @@ -119,7 +120,7 @@ local function emigrate(unit, toSite, prevEnt, civ) removeMandates(unit) unit_link_utils.removeUnitAssociations(unit) - unit_link_utils.removeHistFigFromEntity(histFig, prevEnt) + unit_link_utils.removeHistFigFromEntity(histFig, prevEnt, removeMayor) -- have unit join new site government local siteGov = df.historical_entity.find(toSite.cur_owner_id) @@ -155,17 +156,34 @@ 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 isAdministrator(unit, fortEnt) +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 return true end + 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 false + return result end ----------------------- @@ -179,6 +197,10 @@ local function listNoblesFound(nobleList, fortEnt) 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 @@ -188,13 +210,22 @@ local function listNoblesFound(nobleList, fortEnt) or "unknown squad" unitMsg = "! "..unitMsg.." - soldier in "..squadName - elseif isAdministrator(unit, fortEnt) then - unitMsg = "! "..unitMsg.." - fort administrator" - else - local siteName = dfhack.translation.translateName(site.name, true) - unitMsg = " "..unitMsg.." - to "..siteName + 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 @@ -243,17 +274,26 @@ local function main() 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.") - elseif isAdministrator(noble, fortEnt) then + goto continue + end + + adminType = getAdminType(noble, fortEnt) + if adminType == AdminType.IS_ADMIN then print("! "..nobleName.." is an administrator! Unassign unit and try again.") - else - emigrate(noble, site, fortEnt, civ) + goto continue end + + local isElected = adminType == AdminType.IS_ELECTED + emigrate(noble, site, fortEnt, civ, isElected) + ::continue:: end end diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua index 2188bb7117..6dfe81550d 100644 --- a/internal/emigration/unit-link-utils.lua +++ b/internal/emigration/unit-link-utils.lua @@ -1,8 +1,9 @@ --@module = true ----@param histFig df.historical_figure +---@param histFig df.historical_figure ---@param oldEntity df.historical_entity -function removeHistFigFromEntity(histFig, oldEntity) +---@param removeMayor boolean +function removeHistFigFromEntity(histFig, oldEntity, removeMayor) if not histFig or not oldEntity then return end local histFigId = histFig.id @@ -28,6 +29,17 @@ function removeHistFigFromEntity(histFig, oldEntity) end end + -- remove mayor assignment if exists + if removeMayor then + 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 + end + end + 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 From fb8d15e67251e69551dfaa099c8a12b0f1175756 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Mon, 10 Feb 2025 22:55:17 +0800 Subject: [PATCH 37/43] Add symbol dropping logic --- internal/emigration/emigrate-nobles.lua | 9 ++-- internal/emigration/unit-link-utils.lua | 55 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 6c1d98c54f..ba6bdebb5b 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -47,7 +47,7 @@ local function findCapital(civ) end ---@param unit df.unit ----@param nobleList { unit: df.unit, site: df.world_site }[] +---@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) @@ -66,7 +66,7 @@ local function addNobleOfOtherSite(unit, nobleList, thisSite, civ) 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}) + table.insert(nobleList, {unit = unit, site = capital, id = noblePos.assignment.id}) end return end @@ -77,7 +77,7 @@ local function addNobleOfOtherSite(unit, nobleList, thisSite, civ) 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}) + table.insert(nobleList, {unit = unit, site = site, id = noblePos.assignment.id}) end ---@param unit df.unit @@ -252,7 +252,7 @@ local function main() 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 }[] + ---@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 @@ -293,6 +293,7 @@ local function main() local isElected = adminType == AdminType.IS_ELECTED emigrate(noble, site, fortEnt, civ, isElected) + unit_link_utils.unassignSymbols(record.id, civ, fort) ::continue:: end end diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua index 6dfe81550d..557f938db3 100644 --- a/internal/emigration/unit-link-utils.lua +++ b/internal/emigration/unit-link-utils.lua @@ -134,3 +134,58 @@ function markUnitForEmigration(unit, civId, leaveNow) 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 From 5569bb070516c335ca8ff3c9337a1ddc988f5e28 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Tue, 11 Feb 2025 00:04:24 +0800 Subject: [PATCH 38/43] Update TODO --- internal/emigration/emigrate-nobles.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index ba6bdebb5b..07826db2a7 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -3,7 +3,7 @@ --[[ TODO: * Feature: have rightful ruler immigrate to fort if off-site - * QoL: make sure items are unassigned + * Noble of other civ residing in fort? ]]-- local argparse = require("argparse") From 16b4fe94505581d16b417531a4d96c6b2a6c4205 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Tue, 11 Feb 2025 00:47:04 +0800 Subject: [PATCH 39/43] Update docs/emigration.rst --- docs/emigration.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/emigration.rst b/docs/emigration.rst index d04e07e178..9d8c0503c6 100644 --- a/docs/emigration.rst +++ b/docs/emigration.rst @@ -18,13 +18,9 @@ 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 will not be emigrated. -Remove them from the squad before retrying. - -.. warning:: - - Emigrated nobles will not surrender any symbols of office before leaving. - Unassign your artefacts before calling ``emigration nobles``. +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 ----- @@ -38,12 +34,17 @@ Usage 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 + 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 + 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 + Emigrate a noble matching the specified unit ID that does not rule your fortress. Options ------- From bce191d0d1e87a6948e11dfb229bc5222b73df99 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Tue, 11 Feb 2025 01:34:20 +0800 Subject: [PATCH 40/43] Fix bad mayor assignment logic --- internal/emigration/unit-link-utils.lua | 41 +++++++++++++++++++------ 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua index 557f938db3..16b7a2a2d1 100644 --- a/internal/emigration/unit-link-utils.lua +++ b/internal/emigration/unit-link-utils.lua @@ -1,5 +1,36 @@ --@module = true +---@param histFig df.historical_figure +---@param oldEntity df.historical_entity +local function unassignMayor(histFig, oldEntity) + local assignmentId = -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 + 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 }) +end + ---@param histFig df.historical_figure ---@param oldEntity df.historical_entity ---@param removeMayor boolean @@ -30,15 +61,7 @@ function removeHistFigFromEntity(histFig, oldEntity, removeMayor) end -- remove mayor assignment if exists - if removeMayor then - 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 - end - end - end + 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}) From 3c71aeb0d891d7d26a9501e48d407ece56f77701 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Tue, 11 Feb 2025 20:34:12 +0800 Subject: [PATCH 41/43] Update for new structures, add missing event --- internal/emigration/emigrate-nobles.lua | 2 +- internal/emigration/unit-link-utils.lua | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 07826db2a7..8e6efd3746 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -82,7 +82,7 @@ end ---@param unit df.unit local function removeMandates(unit) - local mandates = df.global.world.mandates + 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 diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua index 16b7a2a2d1..e2585cff83 100644 --- a/internal/emigration/unit-link-utils.lua +++ b/internal/emigration/unit-link-utils.lua @@ -4,12 +4,14 @@ ---@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 @@ -29,6 +31,10 @@ local function unassignMayor(histFig, oldEntity) 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_add_hf_entity_linkst, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hfEventId, civ = oldEntity.id, histfig = histFig.id, link_type = 11, position_id = positionId}) end ---@param histFig df.historical_figure From bd58a05c8e0fa04a26adcb001c65056cad12c8d3 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Tue, 11 Feb 2025 21:55:53 +0800 Subject: [PATCH 42/43] Fix incorrect history event --- internal/emigration/unit-link-utils.lua | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua index e2585cff83..35e98e2990 100644 --- a/internal/emigration/unit-link-utils.lua +++ b/internal/emigration/unit-link-utils.lua @@ -30,11 +30,27 @@ local function unassignMayor(histFig, oldEntity) 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 }) + 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_add_hf_entity_linkst, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hfEventId, civ = oldEntity.id, histfig = histFig.id, link_type = 11, position_id = positionId}) + 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 = 10, + position_id = positionId + }) end ---@param histFig df.historical_figure From a69dc66bff0b763b12b0a361e6a4a1ec3b08ded4 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Tue, 11 Feb 2025 23:15:44 +0800 Subject: [PATCH 43/43] Update styling of new links and events --- internal/emigration/emigrate-nobles.lua | 1 - internal/emigration/unit-link-utils.lua | 29 +++++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua index 8e6efd3746..f4ef605de0 100644 --- a/internal/emigration/emigrate-nobles.lua +++ b/internal/emigration/emigrate-nobles.lua @@ -3,7 +3,6 @@ --[[ TODO: * Feature: have rightful ruler immigrate to fort if off-site - * Noble of other civ residing in fort? ]]-- local argparse = require("argparse") diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua index 35e98e2990..e3bc294d46 100644 --- a/internal/emigration/unit-link-utils.lua +++ b/internal/emigration/unit-link-utils.lua @@ -48,7 +48,7 @@ local function unassignMayor(histFig, oldEntity) id = hfEventId, civ = oldEntity.id, histfig = histFig.id, - link_type = 10, + link_type = df.histfig_entity_link_type.POSITION, position_id = positionId }) end @@ -105,21 +105,42 @@ function addHistFigToSite(histFig, siteId, siteGov) 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}) + 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 = 0}) + 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 = 1, reason = -1, site = siteId}) + 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