sots-re/ghidra/vtable-owners.json
alex 648028db67 lane V2: vtable inversion — resolve indirect call edges image-wide
Every call-graph result in this repo was computed over direct (E8) edges.
5,045 of the 5,207 functions named by a vftable slot have zero direct call
sites, so all of those results were lower bounds. Lane Z's dominant RNG
consumer hung off exactly such an edge.

tools/vtable_map.py builds, from the RTTI walk plus a full sweep to the next
function start (rule 17):
  * vftable -> class -> sub-object offset -> slot -> target, and its inverse
  * the class hierarchy from the RTTI base lists, so an abstract interface
    with one concrete override resolves uniquely
  * constructor-derived member typing (ctor result -> [this+d])
  * the slot index at every indirect call site, with a backward register
    resolver that refuses to cross a branch target rather than guess
  * `this`-carrier spans and this/member call-graph propagation of class

Validation (12/12): rediscovers ServerTradeManagerImpl slot 10 ->
GenerateTradeRaidEncounters from the dispatch at 0x007d8469 with nothing
hand-fed, and re-derives the *Impl rule for both managers. Receiver-class
pinning reaches only 2.5% of the 6,398 virtual sites, at 0.6% out-of-range
against a 70% chance baseline; the displacement-only route measured worse
than random (81% vs 58%) and is rejected outright.

Closes lane K's tier-4 blind spot: all nine phase-23 calls and both phase-33
calls named. Four of the eleven reach a draw on the strategic generator
(StrategyServer+0x16c) at eight instruction-verified sites, none ever
observed firing — so "the tail draws nothing" is a property of eight turns,
not of the code. Also resolves the nine parked inlined-draw functions to
their vtable roots (correcting how that was recorded: none is itself in a
vftable; their topmost ancestors are), and finds 14,958 inter-function tail
jump edges without which three of them look like dead code.
2026-09-08 12:05:05 -04:00

38 lines
1.6 KiB
JSON

{
"_readme": [
"Hand-asserted receiver typings for functions that appear in NO vftable and",
"are not constructors, so tools/vtable_map.py's automatic seeds cannot reach",
"them. Every entry must carry evidence a reader can re-derive. These are",
"seeds for the `this`-propagation fixpoint, so one entry types a whole",
"subtree -- which is also why a wrong entry is expensive. Keep it short."
],
"owners": [
{
"func": "0x007dc6c0",
"name": "StrategyServer::ProcessTurn",
"class": "Game::StrategyServer",
"offset": 0,
"evidence": [
"first member op is `inc [esi+0x8]` = ModCount; lane Z read S+0x8 as",
"ModCount from StrategyServer::Write's wire tag at 0x0079fb2f",
"hands its own `this` to 0x007d7f70, which dispatches on [this+0x158]",
"and [this+0x15c]; the StrategyServer ctor 0x007d78d0 constructs a",
"ServerTradeManagerImpl into +0x158 and a ServerSpyManager into +0x15c",
"(vtable_map field 0x158 / field 0x15c)"
]
},
{
"func": "0x007d92a0",
"name": "StrategyServer::OnAllCombatDone_Tail",
"class": "Game::StrategyServer",
"offset": 0,
"evidence": [
"lane K read [ebx+0x54]/[ebx+0x58] as vector<ServerPlayer*> Players",
"dispatches on [ebx+0x158] and [ebx+0x15c] in one straight-line block,",
"matching the two managers the StrategyServer ctor 0x007d78d0 builds",
"there; slot 15 needs >=16 slots and slot 13 needs >=14, which the two",
"concrete vtables 0x00a31b74 (22) and 0x00a3073c (18) supply"
]
}
]
}