sots-re/ghidra/addresses.d/ap.json
alex 2ac52350f0 AP: P's three draws are two species branches - 0x00840a3c fires, 0x0088dc43 fires, the deploy NextFloat measured
From as-turn15-spydeployed.sav, 11 End Turns under probes=8. Detection at Frame
22, P at Frame 25: 0x00840a3c 1 call / 1 word, the first firing of any of P's
sites. 0x00840929 and 0x008409c7 are the MORRIGI branch and cannot be reached in
a Human game - correcting every inventory that lists them as three sites behind
one gate. The counter-mission roll 0x0088dc43 fires from Frame 19 (corpus was 0
of 22) and the deploy NextFloat 0x0078c97f is measured with cbh reproducing lane
AS's value bit for bit. Species table decoded, which also names AS's 0.75f as
Zuul and 0.5f as Morrigi. Two corrections to lane AG 3.2 (CnTrd is the target's;
ncp is a scalar counter). PB4 was wrong and is named: the trade-raid roll entered
the tail at Frame 20, re-confirming lane AD's per-fleet model. Control pair
disagrees (three outcomes in six processes) - not a calibration pair - and
probes=8 is byte-identical to a control over 44,438 leaves. Exposure fields
recorded per turn: saturated on this map.
2026-09-09 01:33:23 -04:00

69 lines
10 KiB
JSON

{
"_note": "Lane AP (2026-09-09). The rest of the spy program's draw sites, read from the instruction stream of dumps/sots.exe (disassembled to the NEXT FUNCTION START, rule 17 -- no Ghidra size is used below) and then MEASURED live on VM144 under probes=8, build ap-989c692-20260909T0425Z. Evidence: findings/subsystems/spy-program-draws.md. The headline is that SpyManager_Slot13RngCallee 0x008408e0 (already named in lane-h.json, not re-minted here) contains TWO MUTUALLY EXCLUSIVE BRANCHES selected by the spy owner's Species, so its three draw sites can never all be reached: 0x00840929 + 0x008409c7 are Morrigi-only and 0x00840a3c is everyone else. NOT minted here because other fragments already have them: SpyManager_Slot13RngCallee 0x008408e0 (lane H), ServerSpyManager_vslot13 0x008877b0 and vslot14 0x0088db80 (lane V2), ServerSpyManager_DeploySpy 0x00887410 and ServerSystem_AddSpy/RemoveSpy (lane AS), Mars_RNG_NextFloat/NextInt/Chance (addresses.json).",
"entries": [
{
"name": "SpyManager_ShipActionDeploySpyHandler",
"addr": "0x0078c930",
"convention": "__thiscall",
"prototype": "bool (Game::StrategyServer* this, Game::Ship* ship) /* body 0x0078c930..0x0078c9e0 (next function start), `ret 4`. Entry of the STACK-BUILT SHIP-ACTION TABLE (lane B6's indirection class): no direct callers, no vtable slot, invisible to every call-graph sweep this campaign has run. One gate -- a 12-byte stack query object (vtable 0x00a09648) built at 0x0078c93d and asked about the ship at 0x0078c964; false => return with NO DRAW. Past it: `mov ecx,[esi+0x16c]; add ecx,4; call Mars_RNG_NextFloat` at 0x0078c97f -- ONE STRATEGIC-GENERATOR WORD PER APPLIED Deploy Spy ORDER, inside ApplyAllTurnCommands, i.e. the ProcessTurn bracket and NOT the tail's. The angle is lerp(0.0, 2*pi, r) computed through a FLOAT32 temp at [ebp+8] (fldz supplies LO; HI is the double 6.2831854820251465 at 0x009e21b0), stored to the stack slot pushed at 0x0078c998, which is DeploySpy's SECOND argument. 0x0080c860 is then called ACROSS that half-built frame and does not touch it. MEASURED lane AP 2026-09-09: 1 call / 1 word / no_draw_calls=0 / strategic=true at ret_rva 0x0078c984, and spy.cbh came out 5.033599376678467 -- BIT-IDENTICAL to lane AS's value from a different process, build and session, so this draw is reproducible from the save. ITS PREDICATE IS ON THE COMMAND STREAM, not on save fields: `a SHIPACTION_DEPLOYSPY command in this turn's command stream`. The gate-indexed audit has no column for that and should grow one */",
"status": "verified",
"source": "findings/subsystems/spy-program-draws.md (lane AP 2026-09-09)"
},
{
"name": "Ship_GetAttachedSpyId",
"addr": "0x0080c860",
"convention": "__thiscall",
"prototype": "int (Game::Ship* this) /* 7 bytes, complete: `mov eax,[ecx+0xa8]; ret`. A zero-argument getter for the ship's attached-spy id -- the same field ServerSpyManager_DeploySpy clears when it detaches the craft from its tender. Recorded because at its ONE call site (0x0078c9ac, in the SHIPACTION_DEPLOYSPY handler) it looks like it takes a float: the caller has already pushed DeploySpy's second argument and stored the deploy angle into it, and this call runs across that half-built frame without disturbing it. An eye following eax there misses where the angle goes */",
"status": "verified",
"source": "findings/subsystems/spy-program-draws.md (lane AP 2026-09-09)"
},
{
"name": "SpyManager_DestroySpyById",
"addr": "0x00838480",
"convention": "__thiscall",
"prototype": "void (Game::ServerSpyManager* this, int sid) /* Game::ServerSpyManager vftable 0x00a3073c SLOT 10. Looks the SpyCraft up by id through vtable slot 5, then: if spy->deat(+0x10) != 0, registry_lookup(server+0x84, deat) and ServerSystem_RemoveSpy 0x0074f550 on that system (which clears deat and erases sid from spies2); if spy->atto(+0xc) != 0, clear the tender's ship->[+0xa8] and spy->atto; erase the craft from the manager's vector at this+0x10 via 0x0059ec00; then the scalar deleting destructor with flag 1. CALLED FROM ServerSpyManager_vslot13's drain loop at 0x00887eb0, back to front over the local vector<int> of sids collected at 0x008878a1 -- so a spy detected on turn T is DESTROYED at the end of phase 23 on turn T+3, BEFORE vslot14 runs at phase 33. MEASURED lane AP 2026-09-09: nspy 1 -> 0 and spies2 emptied between the Frame-24 and Frame-25 autosaves, and 0x0088dc43 cost 0 on Frame 25 although vslot14 was still entered */",
"status": "verified",
"source": "findings/subsystems/spy-program-draws.md (lane AP 2026-09-09)"
},
{
"name": "SpyManager_AttributionRoll_MorrigiBranch",
"addr": "0x00840929",
"convention": "site",
"prototype": "/* NOT A FUNCTION -- the `call Mars_RNG_Chance 0x008e6dd0` at 0x00840929 inside SpyManager_Slot13RngCallee 0x008408e0, return address 0x0084092e. Reached ONLY when the destroyed spy's owner has ServerPlayer.Species (+0x5c) == 6, i.e. MORRIGI (species table read from the save's own ISsp list: 0 Human, 1 Hiver, 2 Tarkas, 3 Liir, 4 _NPC, 5 Zuul, 6 Morrigi). Probability is the WIDENED FLOAT at 0x009e5ac4 = 0.75f -- the same four bytes SpyCraft_AccumulateDetectionOdds uses as its species-5 multiplier. Generator: `mov ecx,[eax+0x16c]` -- the STRATEGIC generator OBJECT (not the +4 sub-object). One word at 0 < p < 1. On success the function walks StrategyServer+0x54..+0x58 collecting every player with [+0xfb]==0 and [+0xf8]==0 that is neither the spy's owner nor the target system's owner, and picks one -- a FALSE FLAG; the true owner is never returned for a Morrigi spy. UNFIRED: lane AP measured twelve turns including the one turn the function ran and this branch was never taken, because its Species was 0. THE WORKLOAD THAT CLOSES IT is a Morrigi empire, not a different save of a Human one */",
"status": "verified",
"source": "findings/subsystems/spy-program-draws.md (lane AP 2026-09-09)"
},
{
"name": "SpyManager_AttributionRoll_MorrigiPick",
"addr": "0x008409c7",
"convention": "site",
"prototype": "/* NOT A FUNCTION -- the `call Mars_RNG_NextInt 0x004271c0` at 0x008409c7 inside SpyManager_Slot13RngCallee 0x008408e0, return address 0x008409cc. Generator: `mov ecx,[...+0x16c]; add ecx,4` -- the +4 SUB-OBJECT. Bound is `count - 1` passed BY POINTER at [ebp+8] (inclusive), so the index is uniform over the candidate list. TWO conditions past the Morrigi branch: the 0.75f roll at 0x00840929 must SUCCEED, and the candidate list must be NON-EMPTY -- `cmp ecx,eax; je 0x840a07` at 0x008409ac returns NULL with no draw when it is empty, which needs a third non-eliminated empire that is neither the spy's owner nor the target's. UNFIRED for the same reason as 0x00840929 */",
"status": "verified",
"source": "findings/subsystems/spy-program-draws.md (lane AP 2026-09-09)"
},
{
"name": "SpyManager_AttributionRoll_DefaultBranch",
"addr": "0x00840a3c",
"convention": "site",
"prototype": "/* NOT A FUNCTION -- the `call Mars_RNG_Chance 0x008e6dd0` at 0x00840a3c inside SpyManager_Slot13RngCallee 0x008408e0, return address 0x00840a41. Taken for EVERY species except 6 (Morrigi), i.e. the `jne 0x840a29` at 0x00840910. Probability is the WIDENED FLOAT at 0x009e5ac0 = 0.25f. Generator: `mov ecx,[edx+0x16c]` -- the STRATEGIC generator object. One word. On success the function returns arg1, the spy's OWN owner: the victim correctly identifies who sent the spy, 25% of the time; otherwise NULL and the event names no empire. FIRED FOR THE FIRST TIME IN THIS CAMPAIGN, lane AP 2026-09-09: 1 call / 1 word / no_draw_calls=0 / strategic=true at ret_rva 0x00840a41, on the End Turn producing Frame sdet+3 (sdet=22, so Frame 25), from verify/results/saves/as-turn15-spydeployed.sav after seven End Turns. On that same turn the ENTRY PROBE on SpyManager_Slot13RngCallee 0x008408e0 read 1 -- its first non-zero ever, the complement of lane AS's demonstration that its zero said nothing about the subtree */",
"status": "verified",
"source": "findings/subsystems/spy-program-draws.md (lane AP 2026-09-09)"
},
{
"name": "SpyManager_CounterMissionRollSite",
"addr": "0x0088dc43",
"convention": "site",
"prototype": "/* NOT A FUNCTION -- the `call Mars_RNG_Chance 0x008e6dd0` at 0x0088dc43 inside ServerSpyManager_vslot14 0x0088db80 (phase 33), return address 0x0088dc48. Decoded statically by lane AG (gate-indexed-rng-audit.md 3.2) with CORPUS COUNT 0 OF 22; FIRED FOR THE FIRST TIME lane AP 2026-09-09, 1 call / 1 word / strategic=true on each of Frames 19..24. Probability is spy.cmo (+0x28). MEASURED BEHAVIOUR: the bootstrap turn (Frame == tdep + 3, here Frame 18) sets spyon, runs the stage machine cm 0 -> 1 and cmo := 0.2f and DOES NOT DRAW; from the next turn the site costs one word until cmo reaches 1.0. cmo steps +0.2f per failure and resets to 0.2f on success. TWO CORRECTIONS TO LANE AG 3.2: (a) the CnTrd (+0xff) the stage machine consults at cm 1 -> 2/3 is the TARGET SYSTEM OWNER's, not the spy owner's -- measured, my player has CnTrd=False and the machine still took 1 -> 2 because Kepler's owner has CnTrd=True; (b) `ncp` is a scalar int counter of COMPLETED counter-mission stages (measured 0,0,1,1,2,2,3 in lock-step with cm), so the cm==0 branch's `ncp non-empty` test is `ncp != 0` -- and that branch was never reached, because vslot13 destroyed the spy at cm==4 */",
"status": "verified",
"source": "findings/subsystems/spy-program-draws.md (lane AP 2026-09-09)"
},
{
"name": "SpyManager_DeploySpyDrawSite",
"addr": "0x0078c97f",
"convention": "site",
"prototype": "/* NOT A FUNCTION -- the `call Mars_RNG_NextFloat 0x0047d830` at 0x0078c97f inside SpyManager_ShipActionDeploySpyHandler 0x0078c930, return address 0x0078c984, ECX = GetGame()->RNG(+0x16c) + 4. ONE STRATEGIC-GENERATOR WORD PER APPLIED Deploy Spy ORDER, in the ProcessTurn bracket. Decoded but not measured by lane AS; MEASURED lane AP 2026-09-09 (1 call / 1 word / no_draw_calls=0 / strategic=true) and the resulting spy.cbh reproduced lane AS's 5.033599376678467 bit for bit across processes, builds and sessions. IN NO EARLIER INVENTORY -- not lane V2's eight, not lane I's 22, not lane AG's audit -- because it hangs off the stack-built ship-action table and leaves no static call edge. Its predicate is on the COMMAND STREAM, not on save fields */",
"status": "verified",
"source": "findings/subsystems/spy-program-draws.md (lane AP 2026-09-09)"
}
]
}