regenerate addresses header: lanes AZ and BR fragments (1291 entries)

This commit is contained in:
alex 2026-09-09 03:03:17 -04:00
parent 165478592a
commit dea127d4e1

View file

@ -1,5 +1,5 @@
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1). // GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
// Source: sots-re ghidra/addresses.json @ 48db3cc, generated 2026-09-08 by tools/gen_addresses.py // Source: sots-re ghidra/addresses.json @ 2ad6652, generated 2026-09-09 by tools/gen_addresses.py
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated). // Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
#pragma once #pragma once
#include <cstdint> #include <cstdint>
@ -1259,7 +1259,7 @@ constexpr uint32_t TradeSector_FleetRaidsOwnRoutes = 0x0040ecf0;
constexpr uint32_t TradeManager_ResolveRouteEndpointOwner = 0x00441cd0; constexpr uint32_t TradeManager_ResolveRouteEndpointOwner = 0x00441cd0;
// thiscall bool __thiscall (ServerTradeManager* this, Vector3* out, TradeRoute* rt, void* arg3) // 425 B, RET 0xC. Places the raid interception point along the route: a = Systems[rt->trfr]->Pos, b = Systems[rt->trto]->Pos, dist = |b-a|; frac = 1.0 when the route owner's species (+0x5c) is 1 or when either of two species tests on arg3 passes, ELSE frac = float(NextFloat()) drawn at 0x00820c1b; out = a + dir*(frac*dist + <double 0x009e20a0>). TWO CALLERS: 0x0082cf65 (inside CreateRaidEncounter's subtree) and 0x0088b62c (TradeManager_Slot13RngCalleeB, immediately after the target pick) [verified] // thiscall bool __thiscall (ServerTradeManager* this, Vector3* out, TradeRoute* rt, void* arg3) // 425 B, RET 0xC. Places the raid interception point along the route: a = Systems[rt->trfr]->Pos, b = Systems[rt->trto]->Pos, dist = |b-a|; frac = 1.0 when the route owner's species (+0x5c) is 1 or when either of two species tests on arg3 passes, ELSE frac = float(NextFloat()) drawn at 0x00820c1b; out = a + dir*(frac*dist + <double 0x009e20a0>). TWO CALLERS: 0x0082cf65 (inside CreateRaidEncounter's subtree) and 0x0088b62c (TradeManager_Slot13RngCalleeB, immediately after the target pick) [verified]
constexpr uint32_t TradeManager_ComputeRaidInterceptPoint = 0x00420af0; constexpr uint32_t TradeManager_ComputeRaidInterceptPoint = 0x00420af0;
// site call RNG_NextFloat (0x0047d830) inside TradeManager_ComputeRaidInterceptPoint, ECX = GetGame()->RNG(+0x16c) + 4. ONE STRATEGIC-GENERATOR WORD, unless the species short-circuit at 0x00820bc2/0x00820bd7/0x00820bf2 sets frac = 1.0 first. NOT IN ANY EARLIER INVENTORY. It makes TradeManager_Slot13RngCalleeB cost TWO words on a success (this plus RNG_NextInt at 0x0088b613), not the 0 lane AC measured on a state whose candidate list was empty [verified] // site call RNG_NextFloat (0x0047d830) inside TradeManager_ComputeRaidInterceptPoint, ECX = GetGame()->RNG(+0x16c) + 4. ONE STRATEGIC-GENERATOR WORD, unless one of THREE short-circuits fires first and sets frac = **0.0** (NOT 1.0 -- corrected by lane AZ 2026-09-09 from the x87 stack at 0x00820bf8, `fldz; fstp [ebp+0xc]`; the draw arm computes 0.0 + (1.0-0.0)*NextFloat()). The three are: 0x00820bc2 `cmp [routeOwner+0x5c],1` -- the ROUTE OWNER's Species, so a HIVER-owned route is intercepted with no draw whoever raids it; 0x00820bd7 `cmp eax,1` on StarFleet_GetCrewSpeciesForIntercept(raider) -- a HIVER raider draws nothing; and 0x00820bf2 SpeciesDef_HasInterceptFlag -- Human (0) and Zuul (5) only. So the site draws only for a raider of species 2 Tarkas / 3 Liir / 6 Morrigi (or 4 _NPC) against a non-Hiver-owned route. NOT IN ANY EARLIER INVENTORY. It makes TradeManager_Slot13RngCalleeB cost TWO words on a success (this plus RNG_NextInt at 0x0088b613), not the 0 lane AC measured on a state whose candidate list was empty [verified]
constexpr uint32_t TradeManager_RaidInterceptPoint_DrawSite = 0x00420c1b; constexpr uint32_t TradeManager_RaidInterceptPoint_DrawSite = 0x00420c1b;
// thiscall ServerTradeManager method called at 0x008938ed as the FIRST act of ServerTradeManager_CreateRaidEncounter; fills the local vector at [ebp-0x1c]. When it comes back EMPTY, CreateRaidEncounter returns false at 0x008938fa -- BEFORE every draw in the function and its subtree. This is the gate lane H measured as `entered 3 times, 0 words'. Body not read [mapped] // thiscall ServerTradeManager method called at 0x008938ed as the FIRST act of ServerTradeManager_CreateRaidEncounter; fills the local vector at [ebp-0x1c]. When it comes back EMPTY, CreateRaidEncounter returns false at 0x008938fa -- BEFORE every draw in the function and its subtree. This is the gate lane H measured as `entered 3 times, 0 words'. Body not read [mapped]
constexpr uint32_t TradeManager_BuildRaidCandidates = 0x0043b110; constexpr uint32_t TradeManager_BuildRaidCandidates = 0x0043b110;
@ -1281,6 +1281,50 @@ constexpr uint32_t RaidEncounter_Helper848e50 = 0x00448e50;
constexpr uint32_t RaidEncounter_Helper848e50_DrawSite = 0x00448fd9; constexpr uint32_t RaidEncounter_Helper848e50_DrawSite = 0x00448fd9;
// thiscall Called only from TradeManager_BuildRaidEncounter at 0x00892700; calls TradeManager_ComputeRaidInterceptPoint at 0x0082cf65, which is where the NextFloat at 0x00820c1b is spent on this path. Body not read [mapped] // thiscall Called only from TradeManager_BuildRaidEncounter at 0x00892700; calls TradeManager_ComputeRaidInterceptPoint at 0x0082cf65, which is where the NextFloat at 0x00820c1b is spent on this path. Body not read [mapped]
constexpr uint32_t RaidEncounter_Helper82ce00 = 0x0042ce00; constexpr uint32_t RaidEncounter_Helper82ce00 = 0x0042ce00;
// __thiscall 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 */ [verified]
constexpr uint32_t SpyManager_ShipActionDeploySpyHandler = 0x0038c930;
// __thiscall 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 */ [verified]
constexpr uint32_t Ship_GetAttachedSpyId = 0x0040c860;
// __thiscall 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 */ [verified]
constexpr uint32_t SpyManager_DestroySpyById = 0x00438480;
// site /* 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 */ [verified]
constexpr uint32_t SpyManager_AttributionRoll_MorrigiBranch = 0x00440929;
// site /* 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 */ [verified]
constexpr uint32_t SpyManager_AttributionRoll_MorrigiPick = 0x004409c7;
// site /* 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 */ [verified]
constexpr uint32_t SpyManager_AttributionRoll_DefaultBranch = 0x00440a3c;
// site /* 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 */ [verified]
constexpr uint32_t SpyManager_CounterMissionRollSite = 0x0048dc43;
// site /* 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 */ [verified]
constexpr uint32_t SpyManager_DeploySpyDrawSite = 0x0038c97f;
// thiscall void __thiscall (TradeManager* this) // 214 B, 0x00841700-0x008417d5, decoded to the next function start. THE WRITER OF TradeManager+0x0c that lane AG's gate-indexed audit section 7 lists as NOT FOUND, and it settles AG section 3.3: (1) `lea ecx,[this+0x0c]; push 0; call 0x00459f70` clears the vector; (2) resizes it to `count(([this+4])+0x40 .. +0x44)` -- the SYSTEMS vector, the very container FUN_00841cd0 indexes with rt->trfr at 0x00841d36 -- growing with vector::_Insert_n 0x0050e6a0 (fill value NULL) or shrinking with erase 0x004ddfc0; (3) walks this->+0x1c (the SECTOR vector) x sector->+0x78 (that sector's MEMBER SYSTEM vector) and executes `mov [this->+0x0c + m->+0x5c * 4], sector` at 0x008417a8. ServerSystem+0x5c is the serialised `Idx` field (objects/layouts.md, grade verified), so this is literally `containingSector[system.Idx] = sector`. CONSEQUENCE: TradeManager+0x0c is a PER-SYSTEM table whose length is BY CONSTRUCTION the length of the container trfr indexes, so Slot13RngCalleeB's bounds check G_B1a can never reject a valid trfr and 0x0088b613 is reachable. Five callers; FUN_00858a10 is the DESERIALISER (ReadInt/ReadFloat throughout, calls this at 0x00858f07), so the table is rebuilt on load [verified]
constexpr uint32_t TradeManager_RebuildSystemToSectorTable = 0x00441700;
// thiscall void __thiscall (TradeManager* this, ServerSystem* sys) // 83 B, RET 4. Reads TradeManager+0x0c back the same way the writer fills it: bounds-checks `sys->+0x5c` (= the `Idx` tag) against `(this->+0x10 - this->+0x0c)/4` and indexes `((TradeSector**)this->+0x0c)[sys->Idx]`, then passes `sector + 0x78` -- the sector's MEMBER SYSTEM vector, the same vector the writer iterates -- to FUN_0059ec00 with the system. Independent confirmation that +0x5c on the indexing object and +0x78 on the table element are the two halves of one relation. Tail-calls TradeManager_RebuildSystemToSectorTable 0x00841700 [verified]
constexpr uint32_t TradeManager_GetSectorForSystem = 0x00441c70;
// thiscall void __thiscall (std::vector<void*>* this, size_t n) // RET 4. resize(n): when size > n it moves _Mylast down to _Myfirst + n (the shrink arm the trade manager uses with n = 0, i.e. clear); when size < n it calls 0x00538180 to grow [verified]
constexpr uint32_t Vector_ResizePtr = 0x00059f70;
// data int, at g_SpeciesDefTable + 0*0x184 + 0x144 -- i.e. the +0x144 slot of SpeciesDef[0]. The SAME slot in every element is written ONLY by SpeciesDef_InitTable 0x005453a0 at 0x0054562f (`or [ebx+0x144],2` when the element's species index is 5) and 0x00545638 (`or [ebx+0x144],1` when it is 0). So the word is 1 for HUMAN, 2 for ZUUL and 0 for the other five species, and SpeciesDef_HasInterceptFlag 0x0053baf0 tests it. This is the value that decides whether TradeManager's raid interception draws a NextFloat at 0x00820c1b; the table base g_SpeciesDefTable 0x00b10a00 and accessor SpeciesDef_Get 0x00545cc0 were already named in addresses.json and this lane's independent derivation reproduced both [verified]
constexpr uint32_t SpeciesDef_InterceptFlagWord = 0x00710b44;
// thiscall bool __thiscall (SpeciesDef* this) // 12 B: `xor eax,eax; cmp [ecx+0x144],eax; setne al; ret`. TRUE for HUMAN (species 0) and ZUUL (species 5) only. This is the third disjunct of FUN_00820af0's short-circuit, so a HUMAN raider intercepts at frac = 1.0 and the NextFloat at 0x00820c1b IS NOT DRAWN -- correcting lane AG section 3.3's committed `3 words on a success` and section 5.2's `B costs 2 on a success`, both of which hold only for a raider whose species does not set this flag [verified]
constexpr uint32_t SpeciesDef_HasInterceptFlag = 0x0013baf0;
// cdecl int __cdecl (StarFleet* fleet) // returns the fleet owner's species (FUN_0071e280(fleet)->+0x5c) on the default path; only when that species is 4 does it scan the fleet's ship vector (+0xa4..+0xa8) for a ship whose design->+0x14->+0xac..+0xb4 slot is non-null and return THAT record's +0x4 instead. Called TWICE from FUN_00820af0 (0x00820bcf, 0x00820bdd) -- once compared against 1, once used as the index into g_SpeciesDefTable via SpeciesDef_Get [verified]
constexpr uint32_t StarFleet_GetCrewSpeciesForIntercept = 0x0041a2d0;
// __thiscall void (Game::ServerSystem* this, Game::SpyCraft* spy) /* 48 B, complete: `if (!spy) return; spy->deat(+0x10) = this ? this->[+4] : 0; push_back(&this->spies2(+0x1cc), &spy->sid(+0x4))` via lane AI3's 0x0059f1a0. THE ONLY WRITER THAT EVER MAKES `deat` NON-ZERO AT RUNTIME (the other stores to SpyCraft+0x10 are the zero-init in CreateSpyCraft, the deserializer, and three field copies). Exactly one caller, 0x008874d4 inside ServerSpyManager_DeploySpy, and no vtable slot. THIS SETTLES `spies2`: ServerSystem+0x1cc is the per-system vector of DEPLOYED spy ids, written in the same two instructions as `deat`, which is why it is 0 in every save whose only spy is docked to its tender */ [verified]
constexpr uint32_t ServerSystem_AddSpy = 0x003514c0;
// __thiscall void (Game::ServerSystem* this, Game::SpyCraft* spy) /* 48 B, complete: `if (!spy) return; spy->deat(+0x10) = 0; erase(&this->spies2(+0x1cc), &spy->sid(+0x4))` via 0x0059ec00. The exact mirror of ServerSystem_AddSpy; the two together are the whole life cycle of `deat` and of `spies2` */ [verified]
constexpr uint32_t ServerSystem_RemoveSpy = 0x0034f550;
// __thiscall bool (Game::ServerSpyManager* this, int spyHandle, float cbh) /* Game::ServerSpyManager vftable 0x00a3073c SLOT 7; zero direct call sites -- reached only from the SHIPACTION_DEPLOYSPY handler 0x0078c930 through [[dispatcher+0x15c]+0x1c]. Read at the field writes: spy->Reset() (0x00838070), then sdo(+0x3c) := 0.0f, sdet(+0x40) := -1, cbh(+0x18) := arg, tdep(+0x14) := server->[+0xc] (the current turn), atto(+0xc) := 0 and carrierShip->[+0xa8] := 0 (detach), then ServerSystem_AddSpy(system, spy) at 0x008874d4, then a player message built from system->Name (+0xa8, capacity at +0xbc -- which is what proves the receiver is a ServerSystem). CONSEQUENCE FOR THE TAIL: a spy is deployed with sdet == -1 and sdo == 0, so ServerSpyManager_vslot13's detection branch is entered on the very next OnAllCombatDone_Tail */ [verified]
constexpr uint32_t ServerSpyManager_DeploySpy = 0x00487410;
// __thiscall int (Game::ServerSystem* this) /* 16 B, complete: `eax = this->TerrFl(+0x19c); if (this->ARes2(+0x6c) <= 0) eax &= ~1; return eax`. BIT 0 OF THE RETURN IS "this system still has a usable asteroid belt" -- TerrFl bit 0 masked by the belt's remaining resources. Corroborated from the save side without a debugger: ARes2 > 0 and TerrFl & 1 agree on all 616 system records of the 22-save corpus and on all 28 of the state this lane built, where TerrFl also takes the values 2 and 3, so bit 1 is a second, independent terrain feature. Called at 0x00887c71 (the detection roll's gate) and at 0x0083d15d inside the shared placement validator 0x0083ce50, where a clear bit 0 sets refusal bit 0x1000000 */ [verified]
constexpr uint32_t ServerSystem_BeltUsableFlags = 0x00343f80;
// __stdcall void (Game::SpyCraft* spy, Game::ServerSystem* sys, Game::ServerPlayer* spyOwner) /* 512 B, `ret 0xc`. Writes spy->sdo (+0x3c) and is called unconditionally at 0x00887c66, immediately before the detection roll's gate. EARLY OUT: if sys->PID == 0 (unowned) it stores 0.0f and returns, so a spy at an unowned system rolls at p = 0 and costs no word. Otherwise it walks the system's fleet vector accumulating three counters and computes, with every literal read as the four bytes in the image (rule 23): p = 0.01f + (cA>0 ? 0.01f : 0) + (cB>0 ? 0.02f : 0) + 0.001*cC, then p *= (spyOwner->[+0x5c]==5 ? 0.75f : spyOwner->[+0x5c]==6 ? 0.5f : 1.0f), then p *= 0.7, then sdo = min(sdo + p, 1.0) -- IT ACCUMULATES onto the old value and is CLAMPED AT 1.0. So a quiet foreign target gives sdo = 0.007 on the first turn and ~0.007/turn after, and a spy left in place long enough reaches p >= 1, at which point RNG_Chance costs 0 words and always succeeds */ [verified]
constexpr uint32_t SpyCraft_AccumulateDetectionOdds = 0x0041f570;
// site /* THE SPY DETECTION ROLL. A plain E8 rel32 `call Mars_RNG_Chance 0x008e6dd0` in the BODY of ServerSpyManager_vslot13 0x008877b0 -- NOT an inlined draw (the campaign carried `inline` for a day; the draw is a normal entry-point call, it simply sits in the caller rather than in SpyManager_Slot13RngCallee 0x008408e0, which is why an entry probe on that callee reads zero either way). Generator: `mov ecx,[server+0x16c]` at 0x00887c80 -- the STRATEGIC generator. Probability: `fld [spy+0x3c]` -- sdo. Return address for lane Z's draw_sites ledger: 0x00887c8f. Gates, in order: spy.deat != 0, spy.sdet == -1, and ServerSystem_BeltUsableFlags(sys(deat)) & 1. WHEN THAT LAST BIT IS CLEAR THE ROLL IS SKIPPED AND THE SPY IS DETECTED UNCONDITIONALLY (`je 0x887c97`, which stores sdet := turn) -- a zero here is therefore not always a negative */ [verified]
constexpr uint32_t ServerSpyManager_DetectionRoll_DrawSite = 0x00487c8a;
// __thiscall void (Game::SpyCraft* this) /* Called by ServerSpyManager_DeploySpy at 0x0088749e and by ServerSpyManager_vslot14 at 0x0088dc11 when the target system changes hands. NOT READ: only its position in those two chains and the fields its callers write immediately afterwards (sdo := 0, sdet := -1) are established here */ [mapped]
constexpr uint32_t SpyCraft_ResetMission = 0x00438070;
// data void*[7], the switch jump table SpeciesDef_InitTable 0x005453a0 dispatches through at 0x005453eb (`jmp [eax*4+0x545b60]`) to choose the `Species/%s/...` data-path prefix. READ AS BYTES, the seven entries are 0x005453f2 "Human", 0x005453f9 "Hiver", 0x00545400 "Tarkas", 0x00545407 "Liir", 0x0054541c "_NPC", 0x0054540e "Zuul", 0x00545415 "Morrigi" -- so the species enum is 0 Human, 1 HIVER, 2 TARKAS, 3 LIIR, 4 _NPC, 5 Zuul, 6 MORRIGI. NOTE the case bodies are NOT laid out in case order (index 4 jumps PAST index 5's body), so reading the disassembly top to bottom gives the wrong enum; only the table bytes settle it. SpeciesDef_Get 0x00545cc0 bounds-checks k <= 6, so there are exactly seven. This confirms lane AR's species 0 = Human / species 5 = Zuul from an independent direction and names 1, 2, 3, 4 and 6 for the first time [verified]
constexpr uint32_t SpeciesDef_NameJumpTable = 0x00145b60;
// thiscall void (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) // the body of every `Player.<id>.AIAgent` CD block. 36 wire items, NO conditionals: the only `if` the decompiler shows around `lnat` is an inlined std::vector destructor whose operator delete is marked noreturn, and both paths converge at 0x006c72e8. The agent object is *(this+4) [verified] // thiscall void (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) // the body of every `Player.<id>.AIAgent` CD block. 36 wire items, NO conditionals: the only `if` the decompiler shows around `lnat` is an inlined std::vector destructor whose operator delete is marked noreturn, and both paths converge at 0x006c72e8. The agent object is *(this+4) [verified]
constexpr uint32_t Game_StrategyAIAgent_Streamable_Write = 0x002c6f00; constexpr uint32_t Game_StrategyAIAgent_Streamable_Write = 0x002c6f00;
// thiscall bool (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) [verified] // thiscall bool (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) [verified]