Lane W recovered what the type is; this is what moves it. Nothing in the turn calls a method on a child script object directly. A driver notifies the root with an integer event id, the root fans the delivery out to every child, and each delivery is a generic handler plus one event-specific vtable slot chosen from a 33-entry jump table at 0x007a6480. That table is what proves the hand-written pairs lane K read in the tail are event deliveries and not ad-hoc calls, and five of its rows are not in slot order. Six deliveries in a turn, from five functions, with the ids they send. Three handlers write the eight leaves that diverged: the slavers' difficulty tier (a three-record stack table against the frame, and at frame >= 100 the scan runs off the end and stores nothing, so the tier can never reach 2), the refugees' one-shot latch at turn begin, and the swarm queen's hives -- registered on the systems whose EggScio equals the SWARM's scenario tag, which the queen's constructor stores at +0x4 while its own encounter id sits at +0x8. The hive target turn slips forward by one every turn the spawn gates fail. That single `inc` is the whole explanation of a field that reads 31 after turn 1 and 32 after turn 2, which no re-roll can produce on two hives at once. Corrects combat-done-tail.md: the tail has a FOURTH script-hook site, at 0x007d9820, and it sends event 0x1c -- the same id ProcessTurn sends, which lane K attributed to that driver alone. Two leaves stay blocked, and not on a workload: the refugees' design id needs handle allocation and data-file instantiation, and the hive target turn needs one MT draw plus two config constants behind pointers no reference initialises in a form this lane could follow. Fitting them from a single two-hive observation would have been fitting, not derivation, so it was not done. An RNG claim for the next lane to falsify cheaply: lane Z's "residual outside the two turn drivers is exactly zero" was measured on turns where the hives already existed. Hive creation draws inside BeginProcessTurn, outside both drivers and before either. 13 addresses in ghidra/addresses.d/lane-sv.json; validated to a scratch path, merges to 1,204 with no duplicate name. A fourteenth was dropped: this lane reached 0x004271c0 independently and would have filed it as RNG_NextIntInclusive, but addresses.json already carries it as RNG_NextInt with the same convention and four lanes depend on that name. The merger only detects duplicate NAMES, so a second name for one address would have merged silently and forked the vocabulary for the campaign's most-used RNG primitive.
108 lines
7.5 KiB
JSON
108 lines
7.5 KiB
JSON
{
|
|
"entries": [
|
|
{
|
|
"name": "SVScriptObject_DispatchEvent",
|
|
"addr": "0x007a60d0",
|
|
"convention": "thiscall",
|
|
"prototype": "int (Game::SVScriptObject* this, int evt, void* arg) // the script-object event bus. Calls this->vft[0x10](evt, arg) -- the GENERIC handler every object sees -- then `cmp evt,0x20; ja done; jmp dword [evt*4 + SVScriptObject_EventSlotJumpTable]`, which dispatches to ONE event-specific vtable slot with the argument shape that event carries. Every hand-written `vft[0x10](id,0); vft[slot]()` pair in the two turn drivers is this same two-step done on the root object",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVScriptObject_EventSlotJumpTable",
|
|
"addr": "0x007a6480",
|
|
"convention": "data",
|
|
"prototype": "void* [33] // evt (0..0x20) -> the vtable slot SVScriptObject_DispatchEvent calls. Slot byte offsets in evt order: 0x14 0x18 0x1c 0x20 0x24 0x28 0x2c 0x30 0x34 0x38 0x3c 0x40 0x44 0x48 0x4c 0x50 0x54 0x58 0x5c 0x60 0x64 0x6c 0x70 0x74 0x68 0x7c 0x80 0x84 0x78 0x88 0x8c 0x90 0x94. Note 0x15->+0x6c, 0x16->+0x70, 0x17->+0x74, 0x18->+0x68 and 0x1c->+0x78 are NOT in slot order",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSots_HandleEvent",
|
|
"addr": "0x005a7e40",
|
|
"convention": "thiscall",
|
|
"prototype": "int (Game::SVSOSots* this, int evt, void* arg) // Game::SVSOSots vftable 0x00A063C4 slot +0x10, the ONLY slot that class overrides. evt 3 or 0x1b re-runs the new-game seeder 0x005a7d70; evt 0x1a runs 0x005a37e0; then it fans the delivery out to every child in the pointer vector at this+0x1c..0x20 through SVScriptObject_DispatchEvent. The loop re-reads both bounds every iteration, so a callee may resize the child vector under it",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSlaversRefuel_HandleEvent",
|
|
"addr": "0x0051a800",
|
|
"convention": "thiscall",
|
|
"prototype": "int (Game::SVSOSlaversRefuel* this, int evt, void* arg) // generic handler; the class overrides no event-specific slot at all. Body is `if (evt == 0x14) SVSOSlaversRefuel_UpdateDifficultyTier(this)` and nothing else",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSlaversRefuel_UpdateDifficultyTier",
|
|
"addr": "0x00515820",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOSlaversRefuel* this) // writes CDiff at this+0x38. Builds a 3x3-dword table on the stack -- thresholds 1 / 50 / 100 -- and scans for the FIRST threshold GREATER than StrategyServer+0xc (the frame), then stores index-1 if it differs from the stored value. Frame <= 0 exits at index 0; frame >= 100 runs off the end and stores NOTHING, so the tier can never reach 2. Only on a change does it continue into the per-system pass at 0x005158d4",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSORefugees_OnTurnBegin",
|
|
"addr": "0x00511260",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSORefugees* this) // vtable slot +0x60, event 0x13, sent from BeginProcessTurn. `if (!this->ini(+0x14)) { this->ini = 1; obj = <instantiate \"Mission\" / \"_Refugee_Trader\" from the data files>; if (obj) this->dids(+0x18).push_back(obj->handle(+0xa0)->id(+4)); }`. The store to the latch is unconditional on the lookup's result",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSORefugees_OnCombatDone",
|
|
"addr": "0x00511310",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSORefugees* this) // vtable slot +0x34, event 8, sent from OnAllCombatDone_Tail phase 8. Walks StrategyServer+0x44 (Systems) and drains the object vector at this+0x28..0x2c, destroying what it resolves. It does NOT write dids -- that is SVSORefugees_OnTurnBegin",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSwarmQueen_OnTurnBegin",
|
|
"addr": "0x00529930",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOSwarmQueen* this) // vtable slot +0x60, event 0x13. Runs SVSOSwarmQueen_RegisterHives, then prunes hives whose system's EggScio (+0x184) no longer equals this->scenarioTag (+0x4), then SVSOSwarmQueen_TickHives and 0x00505100",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSwarmQueen_RegisterHives",
|
|
"addr": "0x00527630",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOSwarmQueen* this) // for every system in StrategyServer+0x44 whose EggScio (+0x184) equals this->scenarioTag (+0x4, which the ctor sets to 3 -- the SWARM's tag, not the queen's own EncID 10), and that no hive already references, appends a HiveInfo {vptr 0x009f1a68, sys, nextQ, queen=0} to the vector at this+0x10..0x14, stride 0x10. nextQ = frame + *(int*)[0x00ae0204] + RNG_NextInt (0x004271c0, already in addresses.json; INCLUSIVE of its bound) over (*(int*)[0x00ae0208] - *(int*)[0x00ae0204]) -- ONE strategic-generator draw per new hive, taken inside BeginProcessTurn and therefore OUTSIDE both turn drivers",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSwarmQueen_TickHives",
|
|
"addr": "0x00527770",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOSwarmQueen* this) // per hive with queen (+0xc) == 0: if any spawn gate fails, `inc [hive+8]` -- the target turn SLIPS FORWARD BY ONE, which is why NextQ reads 31 after turn 1 and 32 after turn 2. Otherwise, if nextQ <= frame, spawn a queen (0x0050dfc0 then 0x004fe810) and append to the Queens vector at this+0x20. Gates read config pointers at 0x00ae0210, 0x00ae0228 and 0x00ae0220",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSwarmQueen_OnTurnEnd",
|
|
"addr": "0x005275d0",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOSwarmQueen* this) // vtable slot +0x64, event 0x14, sent from OnAllCombatDone_Tail phase 20. The same hive prune as the turn-begin handler, then 0x00521150. It does NOT register hives",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSwarmQueen_HiveInfo_Write",
|
|
"addr": "0x004fe730",
|
|
"convention": "thiscall",
|
|
"prototype": "void (Game::SVSOSwarmQueen::HiveInfo* this, Mars::IStream* s) // vftable 0x009f1a68 slot 2. Fixes the element layout: +0x4 -> HiveID (handle), +0x8 -> NextQ (int), +0xc -> QueenID (handle). The wire order is NOT the member order",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SVSOSwarmQueen_Ctor",
|
|
"addr": "0x0051ae20",
|
|
"convention": "cdecl",
|
|
"prototype": "Game::SVSOSwarmQueen* () // operator new(0x40); vftable 0x009f49e4; and the two ids that settle who the queen works for: [+0x4] = 3 (the scenario tag it selects systems by) and [+0x8] = 10 (its own EncID)",
|
|
"status": "verified",
|
|
"source": "findings/objects/svsctob-writers.md (lane SV 2026-09-08)"
|
|
}
|
|
]
|
|
}
|