Skip to content

dead-noble: recover from a deceased baron/count/duke - #1653

Open
ab9rf wants to merge 2 commits into
DFHack:masterfrom
ab9rf:dead-noble
Open

ab9rf wants to merge 2 commits into
DFHack:masterfrom
ab9rf:dead-noble

Conversation

@ab9rf

@ab9rf ab9rf commented Nov 12, 2026

Copy link
Copy Markdown
Member

This script provides tools for recovering from a deceased baron/count/duke.

When the landholder dies, the game selects a successor, but this successor is almost always at another site. The game is supposed to push the successor into an army that will path to the site and join it as the new landholder, but the vanilla code to do this includes a test that prevents it from working for landholders (it works for any other civ-level noble position where this happens, just not for landholders. go figure.) so clearly this is what is supposed to happen, it just doesn't because Toady's code is broken.

This script finds the already-appointed successor, creates an army, shoves the successor into it, and emulates a unitless "makeown" so that when the successor arrives they'll (re)join the site as the new landholder automatically.

Tested on a fort with a dead baron obtained from BlindIRL.

This script provides tools for recovering from a deceased baron/count/duke.

When the landholder dies, the game selects a successor, but this successor is almost always at another site. The game is supposed to push the successor into an army that will path to the site and join it as the new landholder, but the vanilla code to do this includes a test that prevents it from working for landholders (it works for any _other_ civ-level noble position where this happens, just not for landholders. go figure.) so clearly this is what is _supposed_ to happen, it just doesn't because Toady's code is broken.

This script finds the already-appointed successor, creates an army, shoves the successor into it, and emulates a unitless "makeown" so that when the successor arrives they'll (re)join the site as the new landholder automatically.

Tested on a fort with a dead baron obtained from BlindIRL.
Comment thread dead-noble.lua Outdated
print((' [%s] %s'):format(ok and 'PASS' or 'FAIL', name))
return ok
end
if not gate(rd_i32(hf, 0x30) == -1,

@quietust quietust Nov 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this not just fetching hf.died_year directly?
(also, this offset might be invalid on Linux)

Comment thread dead-noble.lua Outdated
dest = df.world_site.find(last_asg.st_id)
print((' [dest] st_id path (asg %d st_id=%d) -> site %s'):
format(last_asg.id, last_asg.st_id, dest and dest.id or 'nil'))
elseif tostring(ent.type):find('CIVILIZATION') then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this even work? "CIVILIZATION" is the Bay12 name, while ours is title-case ("Civilization").

This should probably be checking ent.type == df.historical_entity_type.Civilization instead, like the ones below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it doesn't work. it returns a string converted from the integer type, e.g. 0.

Comment thread dead-noble.lua Outdated
gate(lm_hf ~= nil and lm_hf == lm_dest,
('landmass match %s == %s'):format(tostring(lm_hf), tostring(lm_dest)))
local _, saddr = df.sizeof(dest)
local g = df.reinterpret_cast('int32_t', saddr + 0x348).value

@quietust quietust Nov 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

dest.min_depth?
(also, Linux)

Comment thread dead-noble.lua Outdated
local home = nil
for _, l in ipairs(hf.site_links) do
local t = l:getType()
if t >= 3 and t <= 7 and l.site ~= -1 then home = l.site break end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These should be matching against individual df.histfig_site_link_type entries

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this would be any of HOME_SITE_ABSTRACT_BUILDING, HOME_SITE_REALIZATION_BUILDING, LAIR, HOME_SITE_REALIZATION_SUL, or HOME_SITE_SAVED_CIVZONE

not sure why toady's code has that specific set of requirements

Comment thread dead-noble.lua Outdated
then return end
for _, l in ipairs(hf.entity_links) do
local t = l:getType()
if (t == 6 or t == 4) and l.link_strength ~= 0 then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And df.histfig_entity_link_type here

Comment thread dead-noble.lua Outdated
end
end
for _, l in ipairs(hf.histfig_links) do
if tostring(l._type):find('IMPRISONER') and l.link_strength ~= 0 then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

df.histfig_hf_link_type.IMPRISONER

Comment thread dead-noble.lua Outdated
c and tostring(c.site_id) or '?')
end

-- native gate at world_site+0x348 == mined_hours (FUN_14147d730 @0x14147e08c):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Offset 0x348 should be min_depth, not mined_hours (presumably to make sure nobles can't migrate to subterranean sites)

Comment thread dead-noble.lua Outdated
local site = df.world_site.find(site_id)
if not site then return end
local _, saddr = df.sizeof(site)
local gate = df.reinterpret_cast('int32_t', saddr + 0x348).value

@quietust quietust Nov 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

site.min_depth

Comment thread dead-noble.lua Outdated
-- HOME_SITE* site links (types 3-7): native treats the first as "home"
for _, l in ipairs(hf.site_links) do
local t = l:getType()
if t >= df.histfig_site_link_type.HOME_SITE_ABSTRACT_BUILDING

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be an array of distinct entries, not a range check (in case Toady rearranges them some day)

Comment thread dead-noble.lua Outdated
ent and ent.id or 'nil', best_prec)) then return end
local wb = hf.info and hf.info.whereabouts
if not gate(wb ~= nil, 'whereabouts present') then return end
if not gate(wb.state ~= 3 and wb.state ~= 4,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

whereabouts_type.army_died / whereabouts_type.army_survived

@SilasD SilasD left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpicky stuff, not the important stuff Quietust flagged.

I'll try to give it more attention later.

Comment thread dead-noble.lua
'has returned' announcement) rather than a guest. The native
army-advance code does the march; no ``settle`` step is needed.

]====]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think all of this isn't needed, because you have the .rst help file, which takes precedence.

It also look to be out-of-sync with the .rst file, from a glance at dead-noble fetch.

Comment thread dead-noble.lua Outdated
dest = df.world_site.find(last_asg.st_id)
print((' [dest] st_id path (asg %d st_id=%d) -> site %s'):
format(last_asg.id, last_asg.st_id, dest and dest.id or 'nil'))
elseif tostring(ent.type):find('CIVILIZATION') then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it doesn't work. it returns a string converted from the integer type, e.g. 0.

Comment thread dead-noble.lua
Comment on lines +593 to +603
ev.id = df.global.hist_event_next_id - 1
ev.year = df.global.cur_year
ev.seconds = df.global.cur_year_tick
ev.hfid = hf.id
ev.state = state
ev.reason = reason or df.history_event_reason.none
ev.site = site_id
ev.region = -1
ev.layer = -1
if site then ev.region_pos:assign(site.pos) end
world.history.events:insert('#', ev)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

as a style & consistency thing, I think auto-vivification would be better here, like the above functions have.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yeah this is copying the structure in the actual code which really does do it this way - most of toady's constructors are default-only and it's up to the consumer to actually fill in the details

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

also in this case autovivificaton is messier because of the conditional logic for setting site position

Comment thread dead-noble.lua Outdated
end

local function new_remove_link_event(entity, hf, position_id)
df.global.hist_event_next_id = df.global.hist_event_next_id + 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think, for these history events, you should increment the global after the insertion;
that way if the insertion fails, the state remains consistent.

that's a nitpick, not a big deal.

if you do change it, you will also have to change the id math below, just delete the - 1

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

DF's own code tends to do it the other way around, but you do have a point

@ab9rf

ab9rf commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

Verified that the updated version still works in the test fort I'm using, at least for dead-noble fetch

@Bumber64 Bumber64 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just skimmed, but noticed a few things.

Comment thread dead-noble.lua
('figure+0x30 (died_year?) == -1 [=%d]'):format(rd_i32(hf, 0x30)))
then return end
if not gate(nem.group_leader_id == -1, 'group_leader_id == -1') then return end
local uok = nem.unit == nil or ((nem.unit.flags1.whole >> 31) & 1) == 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is flags1.important_historical_figure (Bay12: NEMESIS), right? It's used below in cmd_army also.

Comment thread dead-noble.lua
g.reason = df.history_event_reason.none
c.data.goal_move_to_site = g
c.goal = df.army_controller_goal_type.MOVE_TO_SITE
world.army_controllers.all:insert('#', c)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A civ's historical_entity also has vectors for armies and army_controllers. Is DF doing that for us later?

Comment thread dead-noble.lua
end
end

-- emulate FUN_14147d730's gate chain for one nemesis, printing each verdict

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

List DF version when referencing FUN_*?

Comment thread dead-noble.lua
end

local function hf_of(hfid)
return hfid ~= -1 and df.historical_figure.find(hfid) or nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIRC, find already returns nil on -1?

Comment thread docs/dead-noble.rst
Comment on lines +131 to +140
- ``recall`` for land-holder positions is limited by a native gate: the
successor-travel sweep skips any position whose definition has
``land_holder > 0`` *before* reading the destination, so a baron,
count, or duke never gets a native travel army no matter what
``st_id`` says. The ``st_id`` write is still correct and consumed
by the same native path -- it just needs the one native check
bypassed for the target only. A production fix should scope the
bypass (call the creation path for the single intended nemesis, or
patch for a single sweep window); a global patch makes every
stranded land-holder in the world travel at once.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Quite a bit of jargon here.

Comment thread dead-noble.lua
end
for _, l in ipairs(hf.histfig_links) do
if tostring(l._type):find('IMPRISONER') and l.link_strength ~= 0 then
if l._type == df.histfig_hf_link_type.IMPRISONER and l.link_strength ~= 0 then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this actually needs to check :getType()

Comment thread dead-noble.lua
@@ -329,26 +254,31 @@ local function trace_sweep(nem, hf)
gate(lm_hf ~= nil and lm_hf == lm_dest,
('landmass match %s == %s'):format(tostring(lm_hf), tostring(lm_dest)))
local _, saddr = df.sizeof(dest)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is saddr still needed for anything?

Comment thread dead-noble.lua
end
if not gate(rd_i32(hf, 0x30) == -1,
if not gate(hf.died_year == -1,
('figure+0x30 (died_year?) == -1 [=%d]'):format(rd_i32(hf, 0x30)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

need to fix the message too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants