We consume 18-22 generator words per turn and model none of them as a count.
All of it is inside StrategyServer::ProcessTurn; OnAllCombatDone_Tail costs 0
on every turn observed; the residual outside the two drivers is exactly 0. The
generator does not move between turns at all, so the interval a standalone has
to reproduce is closed at both ends.
The instrument reads generator STATE, not calls, and that choice paid: the
image has four draw entry points, not three (NextUInt 0x004f7670 is in no
lane's primitive set) plus inlined draws in twelve functions, two reachable
from the turn roots. A primitive-counting hook would have undercounted
silently.
Checked against the save files independently: the turn-6 autosave pair gives
18 words read from the two Sim.RNG blobs, and with twists == 0 that number
never passes through a twist implementation -- so the two instruments do not
share the hidden assumption they could have.
Corrections to combat-done-tail.md, in place:
* the node-line 0x20000-fleet check runs AFTER the Chance(0.5f) call and
cannot gate the draw; the expiry test is NodePath::RemainingLife 0x006e2130
and is now a formula rather than a description
* StrategyHost::Autosave is ret 8 and returns the std::string* in EAX
* SNMAllCombatDone IS delivered every End Turn (8 of 8) -- lane K's inference
was right; the stronger no-encounter reading is narrowed, not closed
* S+0x8 advances 12-14 times per turn, not twice
Node-line decay still has not fired. The hook reports the distance instead of
the absence: 51 of 53 lines are permanent, the mortal ones are dug ~1/turn by
the Zuul, each ~40 turns from expiry. It stays a labelled hypothesis.
84 lines
7.8 KiB
JSON
84 lines
7.8 KiB
JSON
{
|
|
"entries": [
|
|
{
|
|
"name": "StrategyServer_NodeLineDecay",
|
|
"addr": "0x007ae010",
|
|
"convention": "thiscall",
|
|
"prototype": "void (StrategyServer* this /*base S*/) // phase 11 of OnAllCombatDone_Tail, called at 0x007d9714 as `mov ecx,esi; call`. 1117 B, three loops. LOOP 1 (0x007ae07a..0x007ae1e2) walks the 0x30-stride Game::NodePath records in the vector at (*(S+0x154))+0x8/+0xc, re-reading _Myfirst/_Mylast every iteration, and per record: (1) NodePath::RemainingLife(r, S->Frame) 0x006e2130, `test eax,eax; jg` -> not expired, NEXT RECORD, NO DRAW; (2) THE DRAW, `mov ecx,[esi+0x16c]; fld [0x009e2ea0] /*0.5f*/; call 0x008e6dd0` = Mars::RNG::Chance(0.5f), exactly one MT word; (3) `test al,al; je` -> roll failed, next record; (4) the 0x20000-fleet scan over S->Fleets (S+0x64/+0x68) calling 0x00703500(fleet,0x20000,0) then 0x0078c360(fleet,npid) and dropping the record when that returns 3; (5) push_back npid into a scratch vector<int>. LOOP 2 collapses each collected line via 0x007a92e0(690 B) then 0x007a4700(2244 B); LOOP 3 posts the decay-stage events through NodePath::DecayStage 0x006e21b0. THE ONLY RNG SITE IN THE WHOLE 1117 BYTES: direct-call sweep to depth 5 over 140 functions from 0x007ae010 against {NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0, Twist 0x00426e00, Seed 0x0049fdf0} yields exactly one hit, 0x007ae010 -> 0x008e6dd0. Neither downstream function draws (138 and 49 functions reached, zero hits) -- caveat: direct calls only, their subtrees contain unresolved indirect sites",
|
|
"status": "verified",
|
|
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08, loop 1 read from the instruction stream)"
|
|
},
|
|
{
|
|
"name": "StrategyServer_NodeLineDecay_FleetSkipIsPostDraw",
|
|
"addr": "0x007ae0b2",
|
|
"convention": "site",
|
|
"prototype": "site, and a CORRECTION to findings/control-flow/combat-done-tail.md §3, which says \"the roll is skipped for a line if any fleet with flag 0x20000 is targeting it\". IT IS NOT: the fleet scan begins HERE, at 0x007ae0b2, which is 0x1d bytes AFTER the Chance(0.5f) call at 0x007ae0a5 and is reached only when the roll SUCCEEDED (`test al,al; je 0x007ae1e2` at 0x007ae0aa). The scan therefore cannot change the draw count -- it suppresses only the collapse (the 0x007a92e0 / 0x007a4700 pair), never the draw. The straight-line order in loop 1 is: expiry test -> DRAW -> roll gate -> fleet gate -> collect. Lane K's headline claim, one NextFloat per expired node line per turn, survives intact and is now pinned to a concrete expiry formula (NodePath_RemainingLife)",
|
|
"status": "verified",
|
|
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "NodePath_RemainingLife",
|
|
"addr": "0x006e2130",
|
|
"convention": "thiscall",
|
|
"prototype": "int (NodePath* this, int turn) // RET 4, whole 122-byte body read as instructions. THE EXPIRY PREDICATE for node-line decay. `if (npt(+0x4) == 0) return INT_MAX; if (npdtn(+0x1c) == INT_MAX) return INT_MAX; aged = (npctm(+0x14) >= 0 && turn >= npctm) ? turn - npctm : 0; wear = (npdtf(+0x20) != INT_MAX && npdtf > 0) ? nptf(+0x24) / npdtf : 0; /* SIGNED idiv, nptf is never sign-checked */ rem = npdtn - wear - aged; return rem > 0 ? rem : 0;`. Writes nothing -- the lifetime is DERIVED from a creation stamp and a traffic accumulator, never ticked, so there is no decrement-ordering question. Two never-expire escape hatches (npt == 0, npdtn == INT_MAX). A line is expired exactly when this returns 0",
|
|
"status": "verified",
|
|
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08, whole body from the instruction stream)"
|
|
},
|
|
{
|
|
"name": "NodePath_DecayStage",
|
|
"addr": "0x006e21b0",
|
|
"convention": "thiscall",
|
|
"prototype": "int (NodePath* this, int turn) // 52 B. Wraps NodePath::RemainingLife and buckets it: <=2 -> 0, <=5 -> 1, <=10 -> 2, else 3. Called TWICE per record by loop 3 of node-line decay (once with turn-1, once with turn) to detect a stage transition and post the two decay-stage events. Draw-free -- but note that instrumenting node-line decay by counting RemainingLife CALLS rather than Chance calls over-counts badly because of this wrapper",
|
|
"status": "verified",
|
|
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "StrategyServer_ProcessNodeSpaceTravel",
|
|
"addr": "0x007a0e20",
|
|
"convention": "thiscall",
|
|
"prototype": "void (StrategyServer* this /*base S*/) // 2945 B. Called TWICE per turn: StrategyServer::ProcessTurn phase 7 (0x007dc93a) and OnAllCombatDone_Tail phase 10 (0x007d970b). Body unread; hooked by lane Z only to measure whether it advances the strategic generator, because a draw inside it would be double-counted by anyone who modelled it as running once",
|
|
"status": "mapped",
|
|
"source": "findings/control-flow/turn-driver.md phase 7 + combat-done-tail.md phase 10; call shapes instruction-verified by those lanes"
|
|
},
|
|
{
|
|
"name": "StrategyServer_off_NodeGraph",
|
|
"offset": "0x00000150",
|
|
"convention": "offset",
|
|
"prototype": "Game::ServerNodeGraph* -- the node-line graph. Stored frame (S+4), so it is S+0x154 in the frame OnAllCombatDone_Tail and ProcessTurn receive; node-line decay reads it as `mov eax,[esi+0x154]` at 0x007ae03a with esi = S. Dereferenced without a null check by the original",
|
|
"status": "verified",
|
|
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "ServerNodeGraph_off_Paths",
|
|
"offset": "0x00000008",
|
|
"convention": "offset",
|
|
"prototype": "std::vector<Game::NodePath> (_Myfirst @+0x8, _Mylast @+0xc). Element stride 0x30, confirmed four ways: the reciprocal 0x2aaaaaab / sar 3 at four sites in node-line decay, `add [ebp-0x14],0x30` in its loop 1, `add [ebp-0x18],0x30` in its loop 3, and `add eax,0x30` in ServerNodeGraph::FindPathById 0x006e23d0. Loop 1 of node-line decay re-reads both words every iteration but writes neither, so an entry snapshot is a valid prediction basis",
|
|
"status": "verified",
|
|
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "ServerNodeGraph_FindPathById",
|
|
"addr": "0x006e23d0",
|
|
"convention": "thiscall",
|
|
"prototype": "NodePath* (ServerNodeGraph* this, int npid) // 59 B, linear scan of the 0x30-stride paths vector comparing npid(+0x8). Node-line decay collects npid HANDLES rather than record pointers in loop 1 and re-resolves them here in loop 2, which is how the original hedges against the collapse functions mutating the vector under it",
|
|
"status": "verified",
|
|
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "StarFleet_HasFlagShips",
|
|
"addr": "0x00703500",
|
|
"convention": "thiscall",
|
|
"prototype": "bool (StarFleet* this, uint maskA, uint maskB) // RET 8, a 29-byte thunk onto 0x00702d70(this, maskA, maskB, out = 0). Returns count > 0 where count is the number of ships in the fleet's NShips vector (+0xa4/+0xa8) passing the two-mask ship-flag predicate 0x00814da0, itself gated on (fleet->+0xb8 & maskA) == maskA. Called from node-line decay's post-draw fleet scan with maskA = 0x20000",
|
|
"status": "verified",
|
|
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "StarFleet_PathRelation",
|
|
"addr": "0x0078c360",
|
|
"convention": "cdecl",
|
|
"prototype": "int (StarFleet* fleet, int npid) // 234 B. Returns 3 exactly when the fleet's FRONT waypoint (the deque at fleet+0xc4, element +0x10) names this npid -- i.e. the fleet is currently riding this line; 0/1/2/4 otherwise. Node-line decay drops a rolled line when any 0x20000-flagged fleet returns 3 for it",
|
|
"status": "verified",
|
|
"source": "findings/control-flow/tail-rng-ledger.md (lane Z 2026-09-08)"
|
|
}
|
|
]
|
|
}
|