TradeManager::Slot13RngCalleeA, entered 0 times in every measurement any lane had taken, is entered 1x/turn once a player fleet is parked on a trade-sector node, and OnAllCombatDone_Tail's RNG word delta goes 0 -> 1. Lane W3's open risk is realised, not retired: the RNG ledger and the standalone's generator model need extending by one draw site. The gate is neither freighters nor a deployed spy. It is a fleet whose LocID resolves to a node of kind 2 (TradeSector), positioned bit-equal to the sector, whose owner's bit is set in the sector's tscr mask -- which the CCC_ComRaid tech sets, taking tscr from 252 to 253 and setting ServerPlayer CnRad. Before the tech the game refuses in as many words: 'Alpha Fleet cannot raid trade sector.' Control: same guest, same build, same shim.cfg.l3probe, one End Turn on lane L3's turn-15 save -> all three inner callees 0, tail delta 0. Guest adoption: ref-turn2 hooks=off reproduces the published determinism oracle exactly. Corrects lane L3 §3.5 (the turn-15 save already had a freighter in service -- the AI's -- so 'freighters' was falsified before this lane started) and lane V2's 'trade slot 13 is draw-free'. The spy half is decoded but NOT reached: no AI system in that game has an asteroid belt, so a deploy was impossible, and spies2 is therefore still untested and stays a rule-6 hypothesis. Two new corpus saves; expect the coverage ratchet to break on CnRad, tscr=253, three rt records and a fleet whose LocID is a TradeSector.
53 lines
7.1 KiB
JSON
53 lines
7.1 KiB
JSON
{
|
|
"_note": "Lane AC (2026-09-08). The gate chain that stands between StrategyServer::OnAllCombatDone_Tail and its trade-raid RNG draw, decoded from the instruction stream of dumps/sots.exe (image base 0x00400000, disassembled to the next function start per rule 17) and then FIRED LIVE on VM141: with a player fleet parked on a trade-sector node, TradeManager::Slot13RngCalleeA (0x00820ca0, already in lane V2's fragment) was entered 1x per turn on two instrumented End Turns and OnAllCombatDone_Tail's RNG word delta went 0 -> 1. ReVa/CT111 was unreachable this session (ReVa CONNECTION_CLOSED), so nothing here was read in Ghidra; every entry was disassembled with capstone against the PE section table and the quoted instructions were re-checked byte for byte at the addresses given. Status is `mapped` for that reason, except where a live entry probe or a save field corroborates the decode. Evidence: findings/control-flow/trade-raid-rng-gate.md, verify/traces/ac-probe-turn21-turn23.jsonl (active) and ac-probe-baseline-turn15.jsonl (control).",
|
|
"entries": [
|
|
{
|
|
"name": "StarFleet_GetLocationAsTradeSector",
|
|
"addr": "0x006fe320",
|
|
"convention": "__thiscall",
|
|
"prototype": "Game::TradeSector* (Game::StarFleet* this) /* THE GATE NOBODY HAD SATISFIED. Reads StarFleet+0xa0 (`LocID`, resolved to a StarMapNode*) and returns it ONLY if node+0x14 == 2, else NULL. Whole body, validated at this address: `mov eax,[ecx+0xa0]; test eax,eax; je +8; cmp dword ptr [eax+0x14],2; je +2; xor eax,eax; ret`. Called twice from ServerTradeManagerImpl::vslot13 (0x0088ef80+0x39 and +0x98) and once from the raid predicate 0x0080ed50. Because every fleet in every corpus save before this lane was parked at a STAR (kind 0), this returned NULL for all of them, which is why the trade-raid callees had never been entered by any lane. CORROBORATED BY SAVE: after this lane moved a fleet onto a sector node, its serialized `LocID` reads 832, which is the `TradeID` of trade sector 6 in the same file, and its `Pos` is bit-equal to that sector's. */",
|
|
"status": "mapped",
|
|
"source": "findings/control-flow/trade-raid-rng-gate.md (lane AC 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "StarMapNode_ctor_kind",
|
|
"addr": "0x00730900",
|
|
"convention": "__thiscall",
|
|
"prototype": "void (Mars::StarMapNode* this, int kind) /* StarMapNode::StarMapNode(int). Stores the ctor argument to this+0x14, the word GetLocationAsTradeSector tests. Enumerating EVERY call site is what pins the kind constants, and there are exactly three: Game::StarSystem's ctor 0x00752ea0 passes 0, Game::StarFleet's 0x00706140 passes 1, and Game::TradeSector's 0x00858020 passes 2. So node+0x14 == 2 means TradeSector and nothing else. Prologue validated at this address (push ebp; mov ebp,esp; push -1; push 0x982dc8; fs:[0] SEH frame; security cookie from 0xaf8b78). NOT READ IN GHIDRA: the rest of the body. */",
|
|
"status": "mapped",
|
|
"source": "findings/control-flow/trade-raid-rng-gate.md (lane AC 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "TradeSector_FleetMayRaid",
|
|
"addr": "0x0080ed50",
|
|
"convention": "__cdecl",
|
|
"prototype": "bool (Game::StarFleet* fleet) /* The raid predicate -- gate G3 of ServerTradeManagerImpl::vslot13, called at 0x0088efc7. Returns false unless: the fleet's location is a trade-sector node; FUN_00813ab0(fleet, 0x80000, 0) is false (a runtime-only 64-bit flag pair at StarFleet+0xb8/+0xbc, NOT serialized, so it has no save tag); the fleet has an owner (StarFleet+0x58 -> Game::ServerPlayer*); and the owner's player index bit is set in the sector's `tscr` mask at TradeSector+0x90. If the owner's bit is ALSO set in `tsct` (TradeSector+0x8c), or the owner's ServerPlayer+0x5c == 4, the fleet must additionally carry StarFleet.FtFlg (+0x10c) & 0x800. Mask test validated at 0x0080ed8f: `mov ecx,[edx+0x28]; push edi; mov edi,1; shl edi,cl; test [eax+0x90],edi`. THE UI SURFACES THIS PREDICATE VERBATIM: ordering a fleet onto a sector node without it pops `<fleet> cannot raid trade sector.` (MOVEWARNING_NO_RAID); the server-side twin string is `OnCommand: Fleet not able to raid.` */",
|
|
"status": "mapped",
|
|
"source": "findings/control-flow/trade-raid-rng-gate.md (lane AC 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "TradeSector_PlayerTradesHere",
|
|
"addr": "0x0080e9c0",
|
|
"convention": "__cdecl",
|
|
"prototype": "bool (Game::TradeSector* sector, Game::ServerPlayer* player) /* Tests the player's index bit in the sector's `tsct` mask (TradeSector+0x8c) at 0x0080e9de..0x0080e9e8, short-circuiting true when player->+0x5c == 4. Used by the raid predicate to decide whether the raider additionally needs FtFlg & 0x800, and by the juicy-target scan 0x0081a150. CORROBORATED BY SAVE: `tsct` is 2 on the three sectors the AI (player index 1) trades through and 0 on sector 6, which is the human player's -- and the human raider on sector 6 needed no flag. */",
|
|
"status": "mapped",
|
|
"source": "findings/control-flow/trade-raid-rng-gate.md (lane AC 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "TradeSector_TrimPlayerMasks",
|
|
"addr": "0x0080eac0",
|
|
"convention": "__cdecl",
|
|
"prototype": "void (Game::TradeSector* sector, uint32 keepMask) /* ANDs six consecutive words with a low-N-players mask: +0x88 `tssec`, +0x8c `tsct`, +0x90 `tscr`, +0x94 `ptssec`, +0x98 `ptsct`, +0x9c `ptscr` (stores at 0x0080ead7, 0x0080eadd, 0x0080eae3, 0x0080eae9, 0x0080eaef, 0x0080eaf5). This is what identifies all six of those save tags as PER-PLAYER BITMASKS rather than counts or colours, which had never been established -- `tscr` reads 252 in every save in the corpus and 252 is 0b11111100, i.e. the six NPC player slots. CORROBORATED BY SAVE: researching CCC_ComRaid takes `tscr` from 252 to 253 on all six sectors of the same game, i.e. it sets bit 0, the researching player's index, and sets that player's serialized `CnRad`. NOT LOCATED: the writer that sets a bit -- no `or`/`bts` against +0x8c/+0x90 was found in .text, so the masks are rebuilt wholesale somewhere this lane did not find. */",
|
|
"status": "mapped",
|
|
"source": "findings/control-flow/trade-raid-rng-gate.md (lane AC 2026-09-08)"
|
|
},
|
|
{
|
|
"name": "SpyManager_DetectionRoll_Branch",
|
|
"addr": "0x00887c5b",
|
|
"convention": "__none",
|
|
"prototype": "/* NOT A FUNCTION -- the `sdet == -1` arm inside Game::ServerSpyManager::vslot13 (0x008877b0), branched to from 0x00887856. It calls FUN_0081f570(spy, system, owner), tests FUN_00743f80(system) & 1, and on that bit ROLLS FUN_008e6dd0(StrategyServer+0x16c, the RNG, with spy->sdo at SpyCraft+0x3c), then stamps sdet = turn and posts EVENT_SPY_DETECTED. Recorded here because it is a SECOND, INDEPENDENT WAY FOR THE TAIL TO DRAW that no entry probe on SpyManager::Slot13RngCallee can see: the draw is inline in the outer callee. It is unreachable in every save in the corpus because the gate before it, `SpyCraft.deat (+0x10) != 0` at 0x0088781d, rejects a built-but-undeployed spy. HYPOTHESIS, NOT MEASURED (rule 6): no save has a deployed spy and this lane did not build one. */",
|
|
"status": "hypothesis",
|
|
"source": "findings/control-flow/trade-raid-rng-gate.md (lane AC 2026-09-08)"
|
|
}
|
|
]
|
|
}
|