// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1). // Source: sots-re ghidra/addresses.json @ b77a611, generated 2026-09-08 by tools/gen_addresses.py // Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated). #pragma once #include namespace sots::addr { constexpr uint32_t IMAGE_BASE = 0x00400000; // thiscall bool (Application* this, AppStartup* startup) /* ONE stack arg, RET 4, returns AL; EDX unused. startup = {HINSTANCE hInst; std::vector args; ...; int nShow; const char* wndClass; int iconId} built in WinMain */ [verified] constexpr uint32_t Mars_Application_Initialize = 0x004a0e50; // thiscall void (Application* this) /* no stack args, plain RET, EDX not an input, EAX = last PumpMessages result (ignore) */ [verified] constexpr uint32_t Mars_Application_Run = 0x0049f5b0; // cdecl void (int coreIndex) [unverified] constexpr uint32_t Process_PinAffinity = 0x0049ee70; // thiscall GlobalConst* (GlobalConst* this, void* storage, const char* key, ParseFn parser, const char* file) // RET 0x10 [verified] constexpr uint32_t CfgVar_RegisterKey = 0x004b76a0; // thiscall bool (DemoApp* this) /* no stack args; returns 0 when quitting (this+0x1b2), else 1 */ [verified] constexpr uint32_t DemoApp_OnTick = 0x0049a640; // thiscall void (StrategyClient* this) [unverified] constexpr uint32_t StrategyClient_EndTurn = 0x00383be0; // thiscall void (StrategyServer* this) [unverified] constexpr uint32_t StrategyServer_BeginProcessTurn = 0x003d98e0; // thiscall void (StrategyServer* this) [unverified] constexpr uint32_t StrategyServer_ProcessTurn = 0x003dc6c0; // thiscall void (ServerPlayer* this, Budget* out, bool projected) /* Budget = { int slot[22]; std::vector<{Tech* node, int points}> researchAlloc (3 words @+0x58); int overBudget @+0x64 }. The array is 22 ints, not 25: the three words after slot 21 are the allocation vector and grow by one 8-byte element exactly when the player has a research target. Slots: 0 Sav, 1 systemIncome+, 2 trade, 3 shipCarriedPop, 4 secondaryManager, 5 savingsInterest, 6 bonusIncome, 7 systemIncome-, 8 maintenance, 9 researchMoneyKept (written only when ResT is set), 10 debtInterest, 11 construction, 12 expenses, 13 researchMoneyGiven, 14 savingsGiven, 15 available, 16 researchMoney, 17 researchPoints, 18 TRA, 19 researchPointsGiven, 20 TRP, 21 totalResearchPoints */ [verified-by-trace] constexpr uint32_t ServerPlayer_ComputeBudget = 0x00463030; // thiscall void (TechTree* this, Mars::RNG* rng, std::vector<{TechDef* target, int points}>* alloc, int* overbudget) /* args confirmed at the single call site: rng = StrategyServer+0x16c object, passed as the RNG *object* and re-based to &mt with lea ecx,[rng+4] before each NextFloat. Loop: node = this->nodes[*(int*)entry.target]; lo/hi from Cost x 50/150 /100 (32-bit imul, signed /100); spend = min(points, hi-progress) with NO clamp at 0; *overbudget += points-spend; odds and roll are both narrowed to float32 before the compare; decay loop hits every state-2 node with progress != 0; the trailing unlock-event loop makes no RNG draw and writes no node */ [verified] constexpr uint32_t TechTree_ProcessResearch = 0x001876c0; // thiscall void (ServerSystem* this /*+8 IStreamable*/, Stream* s) [verified-by-save] constexpr uint32_t ServerSystem_Read = 0x0035d4b0; // thiscall void (ServerSystem* this /*+8*/, Stream* s) [verified-by-save] constexpr uint32_t ServerSystem_Write = 0x00349630; // thiscall void (ServerPlayer* this /*+0x3a0*/, Stream* s) [verified-by-save] constexpr uint32_t ServerPlayer_Read = 0x004804d0; // thiscall void (ServerPlayer* this /*+0x3a0*/, Stream* s) [verified-by-save] constexpr uint32_t ServerPlayer_Write = 0x004563e0; // thiscall void (StarFleet* this /*+8*/, Stream* s) [verified-by-save] constexpr uint32_t StarFleet_Read = 0x00302470; // thiscall void (StarFleet* this /*+8*/, Stream* s) [verified-by-save] constexpr uint32_t StarFleet_Write = 0x00301070; // thiscall void (StarShip* this /*+8*/, Stream* s) [verified-by-save] constexpr uint32_t StarShip_Read = 0x00453fa0; // thiscall void (StarShip* this /*+8*/, Stream* s) [verified-by-save] constexpr uint32_t StarShip_Write = 0x004291f0; // thiscall void (StrategyServer* this, Stream* s) [verified-by-save] constexpr uint32_t StrategyServer_Read = 0x003d27a0; // thiscall void (StrategyServer* this, Stream* s) [verified-by-save] constexpr uint32_t StrategyServer_Write = 0x0039fa70; // thiscall void (Stream* this, const char* tag, NetworkObject* obj) [unverified] constexpr uint32_t Stream_WriteNetworkObjectId = 0x00416490; // thiscall void (uint32_t* mtBlock) // ECX = &mt = RNG object + 4 (NOT the object). N=624 M=397 MATRIX_A 0x9908b0df, 227/396 split; ends with left(&mt+0x9c4) = 624 and next(&mt+0x9c0) = &mt[0]. RET 0 [verified] constexpr uint32_t RNG_Twist = 0x00026e00; // thiscall RNG* (RNG* this, uint32_t seed) // RET 4; writes vftable 0x009e9aec, seeds from &mt = this+4: mt[0]=seed, mt[i]=0x6c078965*(mt[i-1]^(mt[i-1]>>30))+i for i=1..623, then Twist(&mt). OBJECT layout: {vftable @+0; uint32 mt[624] @+4 .. +0x9c3; uint32* next @+0x9c4; int left @+0x9c8} = 0x9cc bytes. Beware: NextFloat/NextInt/Twist take &mt, so *their* this+0x9c0/+0x9c4 are next/left [verified] constexpr uint32_t RNG_Seed = 0x0009fdf0; // thiscall float (uint32_t* mtBlock) // ECX = &mt = RNG object + 4. Lazy twist (if left==0 Twist()), y = *next++, left--, standard MT tempering, then value = (double)(uint32)y * 1/(2^32-1) -- the multiplier at 0x009e61b0 is 0x3df0000000001000 = 1/4294967295.0, NOT 2^-32; the unsigned fix-up at 0x009e61b8 is +2^32 after a signed fild. Left in st(0) at the current x87 precision and narrowed by the caller; range [0,1] INCLUSIVE [verified] constexpr uint32_t RNG_NextFloat = 0x0007d830; // thiscall uint32_t (uint32_t* mtBlock, const uint32_t* n) // ECX = &mt; the bound is passed BY POINTER, RET 4. mask = smallest 2^k-1 >= *n via the or/shift cascade; draws (lazy twist + tempering) until (y & mask) <= *n -- note the loop condition is `ja`, so the result is uniform on [0, *n] INCLUSIVE, not [0, n) [verified] constexpr uint32_t RNG_NextInt = 0x000271c0; // offset sizeof(Mars::RNG) -- {vftable @+0; uint32 mt[624] @+4; uint32* next @+0x9c4; int left @+0x9c8} [verified] constexpr uint32_t RNG_size = 0x000009cc; // offset uint32 mt[624]; also the pointer Twist/NextFloat/NextInt receive in ECX [verified] constexpr uint32_t RNG_off_State = 0x00000004; // offset uint32* next; always &mt[624 - left], and recomputed from left on load, so `left` alone pins the stream position [verified] constexpr uint32_t RNG_off_Next = 0x000009c4; // offset int left; words still unread in the current block [verified] constexpr uint32_t RNG_off_Left = 0x000009c8; // offset int tech id -- the index into TechTree_off_Nodes [verified] constexpr uint32_t TechDef_off_TechId = 0x00000000; // offset sizeof({TechDef* target, int points}) -- the element of the vector ProcessResearch walks [verified] constexpr uint32_t ResearchAlloc_stride = 0x00000008; // cdecl void (void) /* sots.ini [Modules] Mount -> gobio::Init */ [verified] constexpr uint32_t Mars_Application_MountModules = 0x004a0a20; // cdecl void (int* count, const char*** table) /* count 3 @0x00b2d514, table @0x00a35e98 */ [verified] constexpr uint32_t Game_GetDefaultMounts = 0x004991c0; // thiscall GlobalConst* (GlobalConst* this, void* storage, const char* key, GlobalConstParseFn parser, const char* file) /* RET 0x10; static-init stub call. GlobalConst 0x10 bytes: {+0 void* storage (int/float/float[4]), +4 const char* key, +8 parser, +c const char* file} */ [verified] constexpr uint32_t GlobalConst_ctor = 0x004b76a0; // cdecl void (GlobalConst* g) [verified] constexpr uint32_t GlobalConst_Register = 0x004b7610; // cdecl void (int* storage, const char* text) [verified] constexpr uint32_t GlobalConst_ParseInt = 0x004b7000; // cdecl void (float* storage, const char* text) [verified] constexpr uint32_t GlobalConst_ParseFloat = 0x004b7020; // cdecl void (float* storage, const char* text) /* sscanf(%f) into *storage, then *storage = (float)((double)*storage * *(double*)0x00af5210) */ [verified] constexpr uint32_t GlobalConst_ParseFloatScaled = 0x004b70e0; // cdecl void (float rgba[4], const char* text) /* '%d %d %d %d', defaults 255, /255, clamp 0..1 */ [verified] constexpr uint32_t GlobalConst_ParseColour = 0x004b7110; // cdecl void (int rect[4], const char* text) /* '%d %d %d %d' (x y w h); 2 keys (COMBATSETUP_CARD_*_RECT); layout inferred from the M1 trace, int vs float unconfirmed */ [verified-by-trace] constexpr uint32_t GlobalConst_ParseRect = 0x004b7040; // cdecl void (float xyz[3], const char* text) /* '%f %f %f'; 5 keys (HIVER_SPAWN_*_GATE_HEIGHT / _SHIP_OFFSET ...); inferred from the M1 trace */ [verified-by-trace] constexpr uint32_t GlobalConst_ParseVec3 = 0x004b7080; // cdecl void (std::string* storage, const char* text) /* *storage = text; MSVC std::string is 0x1c bytes {+0 char buf[16] | char* ptr when capacity > 15, +0x10 _Mysize, +0x14 _Myres, +0x18 _Alval (empty allocator, never read/written)} -- see std_string_sizeof; 146 keys (*_NAME, *_SOUND, *_TEXTURENAME, DERELICT_SECTION_nn ...); seen in the M1 trace */ [verified-by-trace] constexpr uint32_t GlobalConst_ParseString = 0x004b7670; // cdecl void (void) /* called once from Application::Initialize; sets g_GlobalConstsLoaded */ [verified] constexpr uint32_t GlobalConsts_LoadAll = 0x004b76d0; // cdecl void (const char* file, GlobalConstMap* consts) /* consts = std::map of the keys registered for this file; MSVC2010 layout: map {+4 head node, +8 size}; node {+0 left, +4 parent, +8 right, +0xc const char* key, +0x10 GlobalConst*, +0x14 color, +0x15 isnil}; head: isnil=1, parent=root. Consumed keys are erased from the map. Hook this for the file-level read */ [verified] constexpr uint32_t GlobalConsts_LoadFile = 0x004b73c0; // data bool [verified] constexpr uint32_t g_GlobalConstsLoaded = 0x0072d740; // data std::map /* +4 (0x00b2d748) = header node pointer */ [verified] constexpr uint32_t g_GlobalConstRegistry = 0x0072d744; // data double /* pi/180 as a DOUBLE (0x3f91df46a2529d39); GlobalConst_ParseFloatScaled does float word *= this in x87 and stores float */ [verified] constexpr uint32_t g_GlobalConstFloatScale = 0x006f5210; // thiscall Script* (Script* this) /* sizeof(Script) = 0x28 */ [verified] constexpr uint32_t Script_ctor = 0x004cd700; // thiscall void (Script* this) [verified] constexpr uint32_t Script_dtor = 0x004cd720; // thiscall bool (Script* this, const char* path) /* gobio::ReadFile into this->buf */ [verified] constexpr uint32_t Script_Open = 0x004cd7d0; // thiscall void (Script* this) [verified] constexpr uint32_t Script_Close = 0x004cd6c0; // thiscall int (Script* this, char* out, uint outMax) /* 0 token, 1 no buffer/past end, 2 reached end */ [verified] constexpr uint32_t Script_ReadToken = 0x004cd2f0; // thiscall int (Script* this, ScriptToken* tok) /* tok: {int type; char key[1024]; char value[1024]}; type 1 kv, 2 block open, 3 '}' */ [verified] constexpr uint32_t Script_Next = 0x004cd3e0; // thiscall int (Script* this, int depth) /* 0 ok, nonzero EOF */ [verified] constexpr uint32_t Script_SkipBlock = 0x004cd4b0; // custom /* EAX=cursor, ECX="edFlag, EDX=out; stack: end, outEnd, &len — internal, do not hook */ [verified] constexpr uint32_t Script_ScanToken = 0x004cd1f0; // cdecl void (IConfigCallback* cb, const char* path, const char* blockName, bool* found) /* cb->vft[1](Script*) per matching block */ [verified] constexpr uint32_t ConfigParser_ParseFile = 0x004cd820; // fastcall int (IBuffer* obj) /* ECX=obj; --refcount, vft[0](1) at 0 */ [verified] constexpr uint32_t RefCounted_Release = 0x004a7230; // thiscall void (WeaponDictionary* this) /* reads Weapons/_weapons.txt manifest */ [verified] constexpr uint32_t WeaponDictionary_Init = 0x0019a4c0; // thiscall void (WeaponDictionary* this, const char* path, int id, WeaponDef** out) [verified] constexpr uint32_t WeaponDictionary_LoadWeapon = 0x0019a230; // thiscall WeaponDef* (WeaponDictionary* this, const char* name) /* _stricmp over def->name @+0x40 */ [verified] constexpr uint32_t WeaponDictionary_FindByName = 0x00190a10; // thiscall WeaponDef* (WeaponDictionary* this, const char* name) /* 'Weapon not found: "%s" - Was it added to the index file?' */ [verified] constexpr uint32_t WeaponDictionary_FindByNameSorted = 0x00191e30; // thiscall WeaponDef* (WeaponDef* this, int index, int id, const char* path) /* sizeof 0x278 */ [verified] constexpr uint32_t WeaponDef_ctor = 0x00198a20; // thiscall bool (WeaponDef* this, Script* script, int* rc, TechTree* tree) [verified] constexpr uint32_t WeaponDef_ParseScript = 0x00199070; // thiscall SectionDictionary* (SectionDictionary* this, TechTree* tree) /* reads Species//_shipsections.txt for species 0..6 */ [verified] constexpr uint32_t SectionDictionary_ctor = 0x00176f40; // thiscall SectionDef* (SectionDictionary* this, const char* path, int species, int id) [verified] constexpr uint32_t SectionDictionary_LoadSection = 0x00176cd0; // thiscall SectionDef* (SectionDef* this, int index, int species, int id) /* sizeof 0x3d8 */ [verified] constexpr uint32_t SectionDef_ctor = 0x00174020; // thiscall bool (SectionDef* this, Script* script, int* rc, TechTree* tree) [verified] constexpr uint32_t SectionDef_ParseScript = 0x001744e0; // cdecl const char* (uint species) [verified] constexpr uint32_t Species_GetDirName = 0x00145ec0; // thiscall MasterTechTree* (MasterTechTree* this) /* TechTree/MasterTechList.tech */ [verified] constexpr uint32_t MasterTechTree_ctor = 0x0018b870; // thiscall void (MasterTechTree* this, const char* path, int flag) [verified] constexpr uint32_t MasterTechTree_LoadTechFile = 0x0018b770; // thiscall void (MasterTechTree* this, Script* script, int flag) /* one tech{} block */ [verified] constexpr uint32_t MasterTechTree_ParseTech = 0x0018b050; // thiscall void (DemoApp* this) /* MasterTechTree -> +0x110, WeaponDictionary -> +0x118, SectionDictionary -> +0x114 */ [verified] constexpr uint32_t DemoApp_LoadGameData = 0x00499280; // thiscall void (DemoApp* this) /* once, from OnTick when this+0x10c is set */ [verified] constexpr uint32_t DemoApp_EnsureGameData = 0x00499870; // cdecl void (std::vector* mountPaths) [verified] constexpr uint32_t gobio_Init = 0x004d7010; // thiscall FileSystemSet* (FileSystemSet* this, std::vector* mounts) [verified] constexpr uint32_t gobio_FileSystemSet_ctor = 0x004d6d60; // thiscall bool (FileSystemSet* this, const char* path) [verified] constexpr uint32_t gobio_FileSystemSet_Open = 0x004d5060; // thiscall void (FileSystemSet* this, const char* pattern, std::vector* out) [verified] constexpr uint32_t gobio_FileSystemSet_ListFiles = 0x004d7090; // cdecl bool (const char* path, IBuffer** out) /* whole file by relative path; THE hook point for M4 */ [verified] constexpr uint32_t gobio_ReadFile = 0x004d5140; // cdecl bool (const char* path) [verified] constexpr uint32_t gobio_Open = 0x004d50d0; // cdecl void (void) [verified] constexpr uint32_t gobio_Close = 0x004d4d20; // cdecl uint (void) [verified] constexpr uint32_t gobio_Size = 0x004d4d50; // cdecl uint (void* dst, uint n) [verified] constexpr uint32_t gobio_Read = 0x004d4d80; // cdecl bool (const char* path) [verified] constexpr uint32_t gobio_Exists = 0x004d50f0; // cdecl void (const char* pattern, std::vector* out) [verified] constexpr uint32_t gobio_ListFiles = 0x004d71d0; // custom /* size in EBX, IBuffer** out on stack — internal, do not hook */ [verified] constexpr uint32_t gobio_Buffer_Create = 0x004d4c90; // thiscall void* (IBuffer* this) /* vft[1] and vft[2]; = this+8 */ [verified] constexpr uint32_t gobio_Buffer_Data = 0x00282d50; // thiscall uint (IBuffer* this) /* vft[3]; = this+0xc */ [verified] constexpr uint32_t gobio_Buffer_Size = 0x004d4c80; // thiscall NativeFileSystem* (NativeFileSystem* this, const char* root) /* sizeof 0x28; vtable 0x00a398ac */ [verified] constexpr uint32_t gobio_NativeFileSystem_ctor = 0x004d57a0; // thiscall bool (NativeFileSystem* this, const char* path) /* vft[1] fopen(root+path,'rb') */ [verified] constexpr uint32_t gobio_NativeFileSystem_Open = 0x004d4ef0; // thiscall uint (NativeFileSystem* this, void* dst, uint n) /* vft[4] */ [verified] constexpr uint32_t gobio_NativeFileSystem_Read = 0x004d4c30; // thiscall ZipFileSystem* (ZipFileSystem* this, const char* zipPath) /* sizeof 0x34; vtable 0x00a39934 */ [verified] constexpr uint32_t gobio_ZipFileSystem_ctor = 0x004d6c40; // thiscall bool (ZipFileSystem* this, const char* path) /* vft[1]; case- and slash-insensitive */ [verified] constexpr uint32_t gobio_ZipFileSystem_Open = 0x004d56a0; // thiscall uint (ZipFileSystem* this, void* dst, uint n) /* vft[4] */ [verified] constexpr uint32_t gobio_ZipFileSystem_Read = 0x004d4b40; // data FileSystemSet* /* -> 0x00b2e274 after gobio::Init */ [verified] constexpr uint32_t g_gobio = 0x0072e270; // data const char*[3] /* used when sots.ini has no [Modules] Mount; contains sots_local_en.gob, sots.gob */ [verified] constexpr uint32_t g_DefaultMountTable = 0x00635e98; // cdecl Stream* (const char* path) /* sniffs 'TXT' magic, returns a TextFileStream (0x420 bytes) opened for read */ [verified] constexpr uint32_t Stream_OpenFile = 0x004cfcb0; // thiscall TextFileStream* (TextFileStream* this) /* vtable 0x00a428c0 */ [verified] constexpr uint32_t TextFileStream_ctor = 0x0051e460; // thiscall bool (TextFileStream* this, const char* path, int mode /*1*/) [verified] constexpr uint32_t TextFileStream_Open = 0x004cfb90; // thiscall uint (Stream* this, char* out, int max) [verified] constexpr uint32_t FileStream_ReadLine = 0x004cfa30; // thiscall const char* (TextFileStream* this, const char* line, const char* label) /* NULL on END/mismatch */ [verified] constexpr uint32_t TextFileStream_MatchLabel = 0x0051e760; // thiscall bool (TextFileStream* this, const char* label, char* out, int outMax) /* vft[1] */ [verified] constexpr uint32_t TextFileStream_ReadString = 0x0051ec90; // thiscall bool (TextFileStream* this, const char* label, bool* out) /* vft[2] */ [verified] constexpr uint32_t TextFileStream_ReadBool = 0x0051eba0; // thiscall bool (TextFileStream* this, const char* label, float* out) /* vft[3] */ [verified] constexpr uint32_t TextFileStream_ReadFloat = 0x0051eb30; // thiscall bool (TextFileStream* this, const char* label, int* out, int dflt) /* vft[4] */ [verified] constexpr uint32_t TextFileStream_ReadInt = 0x0051eab0; // thiscall void (TextFileStream* this, const char* label, IStreamable* obj /*NULL=skip*/) /* vft[5]; BEGIN..END */ [verified] constexpr uint32_t TextFileStream_ReadNested = 0x0051e8e0; // thiscall void (ParticleSystem* this, Stream* s) /* .effect body */ [verified] constexpr uint32_t ParticleSystem_Read = 0x004db650; // thiscall void (ParticleSystem* this, Stream* s) [verified] constexpr uint32_t ParticleSystem_Write = 0x004dad60; // thiscall void* (EffectDictionary* this, const char* name) /* 'effects/' + name; Stream::OpenFile; 0x1c0-byte effect */ [unverified] constexpr uint32_t EffectDictionary_Load = 0x004b42b0; // cdecl void (Stream* s, const char* label, std::string* out, int flag) /* wrapper over vft[1] */ [verified] constexpr uint32_t Stream_ReadString = 0x004b9d90; // cdecl void (Stream* s, const char* label, int* out, int dflt) /* wrapper over vft[4] */ [verified] constexpr uint32_t Stream_ReadInt = 0x004b9d20; // cdecl void (Stream* s, const char* label, bool* out) /* wrapper over vft[2] */ [verified] constexpr uint32_t Stream_ReadBool = 0x004b9c00; // cdecl void (Stream* s, const char* label, float* out, int flag) /* wrapper over vft[3] */ [verified] constexpr uint32_t Stream_ReadFloat = 0x004b9bc0; // thiscall void (ServerPlayer* this, TechDef* def, bool silent) /* vft slot 4 of 0x00a327a4; hard-coded tech effects */ [verified] constexpr uint32_t ServerPlayer_OnTechResearched = 0x00491790; // thiscall bool (MasterTechTree* this, TechDef* def, int techId) /* techId = 10000+index into g_TechIdNames; 0xc5 = none */ [verified] constexpr uint32_t MasterTechTree_IsTech = 0x0017d5d0; // thiscall TechDef* (MasterTechTree* this, int techId) [verified] constexpr uint32_t MasterTechTree_GetTechDef = 0x0017d610; // thiscall bool (TechTree* this, int techId) [verified] constexpr uint32_t TechTree_HasResearched = 0x0017d810; // thiscall int (TechTree* this, TechNode* node) // RET 4. node==0 -> INT_MAX; node->costRP(+0x18)==INT_MAX -> INT_MAX; costRP<=0 or this->owner(+0xc)==0 or mult<=0 -> 0; else max(1, _ftol2((float)TechCostMult(owner, node->def) * costRP)). Reads only (no RNG, no writes), so a reimplementation may call it on a scratch tree whose +0xc is the real owner [verified] constexpr uint32_t TechTree_Cost = 0x0017da00; // thiscall float (ServerPlayer* this, TechDef* def) // 1.0 - 0.25 per applicable species research-bonus tech owned; read-only [verified] constexpr uint32_t ServerPlayer_TechCostMult = 0x0040db50; // thiscall void (TechTree* this, TechDef* def, int flags) // state 4 + turn/order stamps + owner callback + child unlock cascade. Makes no direct RNG draw; the owner callback is not audited, so a compare that runs it is out of scope [verified] constexpr uint32_t TechTree_SetResearched = 0x00181e10; // constant flags bit 1 of SetResearched(def, flags): skip the PrereqsMet test and complete unconditionally. TechTree::ProcessResearch passes flags = 2 [verified] constexpr uint32_t TechTree_SetResearched_flag_Force = 0x00000002; // constant flags bit 2 of SetResearched(def, flags): the owner callback is invoked as vft+0x10(def, (flags>>2)&1), so this bit IS OnTechResearched's `silent` argument. ProcessResearch passes flags = 2, so silent = FALSE and the completion event IS posted [verified] constexpr uint32_t TechTree_SetResearched_flag_Silent = 0x00000004; // offset ServerPlayer* owner (0 for a tree with no player) [verified] constexpr uint32_t TechTree_off_Owner = 0x0000000c; // offset std::vector indexed by tech id (MSVC2010: 3 words {first@+0x10, last@+0x14, end@+0x18}); entries may be NULL [verified] constexpr uint32_t TechTree_off_Nodes = 0x00000010; // offset int completion-order counter; SetResearched stamps node+0x28 from it and post-increments it. Harness-audit row 9 [verified] constexpr uint32_t TechTree_off_OrderCounter = 0x00000020; // offset sizeof(TechNode) -- the ctor's operator new argument [verified] constexpr uint32_t TechNode_size = 0x00000034; // offset TechDef* def; *(int*)def is the tech id used to index TechTree_off_Nodes [verified] constexpr uint32_t TechNode_off_Def = 0x00000000; // offset int state (0 hidden, 1 parent researched, 2 available, 3 available AND selected as the current research target, 4 researched); ctor writes 0. The decay sweep tests == 2, so a state-3 node never decays -- confirmed by the B3 live trace [verified] constexpr uint32_t TechNode_off_State = 0x00000014; // offset int costRP, INT_MAX = no researched parent yet; ctor writes 0x7fffffff [verified] constexpr uint32_t TechNode_off_CostRP = 0x00000018; // offset int progress in RP; the only node word ProcessResearch itself writes besides the flag [verified] constexpr uint32_t TechNode_off_Progress = 0x0000001c; // offset int turnAvailable; SetResearched's second sweep stamps it with the server ModCount ONLY when it currently reads -1 (a sticky first-availability stamp). EVENT_TECHS_UNLOCKED collects nodes with state==2 && turnAvailable==currentTurn [verified] constexpr uint32_t TechNode_off_TurnAvailable = 0x00000020; // offset int turnResearched; SetResearched writes the server ModCount [verified] constexpr uint32_t TechNode_off_TurnResearched = 0x00000024; // offset int order; SetResearched writes the tree's completion-order counter, then bumps it [verified] constexpr uint32_t TechNode_off_Order = 0x00000028; // offset std::vector children (3 words at +0x04/+0x08/+0x0c) [verified] constexpr uint32_t TechNode_off_Children = 0x00000004; // offset int RP cost carried by the edge; SetResearched sets child.costRP = min(child.costRP, edge.costRP) [verified] constexpr uint32_t TechEdge_off_CostRP = 0x0000001c; // offset TechDef* the edge's child tech; SetResearched indexes tree->nodes by childDef->[0] [verified] constexpr uint32_t TechEdge_off_ChildDef = 0x00000040; // offset int flag (1 default from the ctor, 0 completed below 80% of cost, 2 over-budget event raised) [verified] constexpr uint32_t TechNode_off_Flag = 0x0000002c; // thiscall void (MasterTechTree* this) /* fills TechDef*[196] at this+0 from g_TechIdNames */ [verified] constexpr uint32_t MasterTechTree_ResolveTechIds = 0x00181c10; // cdecl int (std::string* name) /* membership in g_CombatTechNames (116) */ [verified] constexpr uint32_t IsCombatTechName = 0x002965c0; // fastcall void (ServerPlayer* this) /* flags[7] at +0x348 from SpeciesDef xnc tech ids */ [verified] constexpr uint32_t ServerPlayer_RebuildSpeciesTechFlags = 0x0042bf10; // thiscall void (ServerPlayer* this, TechDef* def) [verified] constexpr uint32_t ServerPlayer_ApplyAITechBonus = 0x0040e330; // thiscall void (ServerPlayer* this, bool on) [verified] constexpr uint32_t ServerPlayer_SetAIBenefit = 0x004186b0; // cdecl bool (TechDef* def, uint* maskOut) [verified] constexpr uint32_t TechDef_GetPlagueCureMask = 0x00137240; // cdecl uint* (uint out[2], TechTree* tree) /* EBX-passed tree in caller; tables 0x00adf378 / 0x00adf478 */ [unverified] constexpr uint32_t ComputeTechBitmasks = 0x000d7960; // cdecl SpeciesDef* (uint species) /* 0x00b10a00 + i*0x184 */ [verified] constexpr uint32_t SpeciesDef_Get = 0x00145cc0; // cdecl void (void) [verified] constexpr uint32_t SpeciesDef_InitTable = 0x001453a0; // fastcall void (ServerPlayer* this) [verified] constexpr uint32_t ServerPlayer_UpdateBankruptcyLimits = 0x00418600; // thiscall void (ServerPlayer* this, int level) [verified] constexpr uint32_t ServerPlayer_SetBankruptcyState = 0x0040e260; // fastcall int (ServerPlayer* this) [verified] constexpr uint32_t ServerPlayer_BankruptcyLevel = 0x0040db10; // cdecl double (double suit, double ideal, double tol) /* clamp01(1-|suit-ideal|/(tol+0.1)) */ [verified] constexpr uint32_t ServerSystem_HazardMod = 0x00347ae0; // thiscall double (ServerSystem* this, double tradePoints) /* RET 8 */ [verified] constexpr uint32_t ServerSystem_TradePointsToMoney = 0x003505b0; // thiscall double (ServerSystem* this, int species) [verified] constexpr uint32_t ServerSystem_CalcSuitMod = 0x003484d0; // fastcall void (ServerSystem* this) [verified] constexpr uint32_t ServerSystem_AccrueSystemBonus = 0x0034d4f0; // thiscall int (ServerSystem* this, float frac) [verified] constexpr uint32_t ServerSystem_SystemBonusPopTarget = 0x0034b5a0; // data struct { const char* name; int x; }[196] /* TechId = 10000+i */ [verified] constexpr uint32_t g_TechIdNames = 0x005ff9e4; // data const char*[116] [verified] constexpr uint32_t g_CombatTechNames = 0x00619718; // data SpeciesDef[7] (0x184 B each, .bss, filled by SpeciesDef_InitTable) [verified] constexpr uint32_t g_SpeciesDefTable = 0x00710a00; // data struct { int techId; uint bit; }[32] [verified] constexpr uint32_t g_TechBitmaskTableA = 0x006df378; // data struct { int techId; uint bit; }[29] [verified] constexpr uint32_t g_TechBitmaskTableB = 0x006df478; // data struct { int techId; float rebellionOdds; float bonus; }[6] /* rows: CCC_AI/CCC_AIAdmin/CCC_AIFac odds 0.1f bonus 0.5f, CCC_AIFRCON odds 0.2f bonus 0, CCC_AIVrus/CCC_AISlv odds 0 bonus 0 */ [verified] constexpr uint32_t g_AITechValueTable = 0x00617888; // data int (config storage; PTR slot 0x00aedfe0) [verified] constexpr uint32_t g_PERGATETRAFFIC_DRV_TpGate = 0x00723e2c; // data int (config storage; PTR slot 0x00aedfe4) [verified] constexpr uint32_t g_PERGATETRAFFIC_DRV_GatAmp = 0x00723e30; // offset int PlyrIdx [verified-by-save] constexpr uint32_t ServerPlayer_off_PlyrIdx = 0x00000028; // offset std::vector owned systems (MSVC2010 release layout: 3 words {first@+0, last@+4, end@+8}, handle ids, 4 B stride); save tag NumOwn/OwnId [verified-by-save] constexpr uint32_t ServerPlayer_off_OwnedSystems = 0x00000030; // offset int Species (0 Human .. 6 Morrigi) [verified-by-save] constexpr uint32_t ServerPlayer_off_Species = 0x0000005c; // offset float ResRate (research share of available money, 0..1) [verified-by-save] constexpr uint32_t ServerPlayer_off_ResRate = 0x000000bc; // offset float ResMod [verified-by-save] constexpr uint32_t ServerPlayer_off_ResMod = 0x000000c0; // offset float ResScl [verified-by-save] constexpr uint32_t ServerPlayer_off_ResScl = 0x000000c4; // offset float TRM (timed research multiplier bonuses) [verified-by-save] constexpr uint32_t ServerPlayer_off_TRM = 0x000000d0; // offset int TRA (per-turn research-point contribution) [verified-by-save] constexpr uint32_t ServerPlayer_off_TRA = 0x000000d4; // offset int TRP (per-turn research-point contribution) [verified-by-save] constexpr uint32_t ServerPlayer_off_TRP = 0x000000d8; // offset bool Elim [verified-by-save] constexpr uint32_t ServerPlayer_off_Elim = 0x000000f8; // offset bool isAI /* gate ComputeBudget reads for the construction slot and for picking the AI difficulty-mod row */ [verified] constexpr uint32_t ServerPlayer_off_IsAI = 0x000000f9; // offset bool NPC [verified-by-save] constexpr uint32_t ServerPlayer_off_NPC = 0x000000fb; // offset bool RebAI [verified-by-save] constexpr uint32_t ServerPlayer_off_RebAI = 0x000000fc; // offset int Maint (raw fleet upkeep, before the difficulty divisor) [verified-by-save] constexpr uint32_t ServerPlayer_off_Maint = 0x0000015c; // offset float shrm (shared research modifier) [verified-by-save] constexpr uint32_t ServerPlayer_off_shrm = 0x00000160; // offset std::vector expense sliders (3 words; entry 16 B {int xid, int xmin, int xmax, float xper}); save tag Nexp [verified-by-save] constexpr uint32_t ServerPlayer_off_Nexp = 0x00000204; // offset float income multiplier from the game-setup handicap block; ComputeBudget bonus slot = ftol((this - 1.0) x net) [verified] constexpr uint32_t ServerPlayer_off_SetupIncomeMult = 0x00000228; // offset float research multiplier from the game-setup handicap block; a factor of the research-points slot [verified] constexpr uint32_t ServerPlayer_off_SetupResearchMult = 0x0000022c; // offset int Sav (treasury) [verified-by-save] constexpr uint32_t ServerPlayer_off_Sav = 0x00000284; // offset Tech* current research target (ResT); NULL = none [verified-by-save] constexpr uint32_t ServerPlayer_off_ResearchTarget = 0x00000294; // offset std::vector otch -- 3 words {_Myfirst@0x274,_Mylast@0x278,_Myend@0x27c}; save tag otch. sizeof(ObservedTech) = 0x2c (44), PINNED three ways: magic divide 0x2e8ba2e9 sar 3 (=/44) in vector_ObservedTech_assign 0x0087239f, imul reg,reg,0x2c at 0x0087243a / 0x007b735b, and the linear search stride `add edi,0x2c` at 0x007ba257. Append site = RecordObservedTech+0xdf (0x007ba27f): lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320. All three words move because push_back reallocs through vector_44B_grow 0x007b5820. LIVE CONFIRMATION (lane V, 2026-09-08, VM140, build eventlive-dd38117-20260908T0916Z): the ProcessResearch `observed_techs` Result region measured the vector's byte span growing by exactly 44 on each of the two tech-completion calls of the five-End-Turn run from ref-turn2 (call 3: 440 -> 484; call 9: 484 -> 528) -- an independent behavioural confirmation of the static pin. Both non-completing players' spans were 880 = 20 x 44 and never moved. [verified] constexpr uint32_t ServerPlayer_off_ObservedTechs = 0x00000274; // offset float IncMod [verified-by-save] constexpr uint32_t ServerPlayer_off_IncMod = 0x0000030c; // offset std::vector (3 words; entry 0x18 B, {+0x8 int researchPercent, +0xc int researchActive, +0x10 int savings, +0x14 int savingsActive}) [verified] constexpr uint32_t ServerPlayer_off_Aid = 0x00000310; // offset float SuitTol -- raised by the two adaptation techs; also caps the hazard money cost [verified] constexpr uint32_t ServerPlayer_off_SuitTol = 0x000000b4; // offset float MaxOH -- max over-harvest slider, raised by a float32 max() against 0.1f [verified] constexpr uint32_t ServerPlayer_off_MaxOH = 0x000000b8; // offset TechTree* the player's own tech tree; its +4 is the MasterTechTree the IsTech chain uses [verified] constexpr uint32_t ServerPlayer_off_TechTree = 0x000000f4; // offset bool AIBn -- AI benefit active; gates ApplyAITechBonus and flips the bonus sign [verified] constexpr uint32_t ServerPlayer_off_AIBn = 0x000000fe; // offset bool CnTrd -- trade routes allowed (set by CCC_FtlEcon unless RebAI) [verified] constexpr uint32_t ServerPlayer_off_CnTrd = 0x000000ff; // offset bool CnRad -- commerce raiding allowed (CCC_ComRaid) [verified] constexpr uint32_t ServerPlayer_off_CnRad = 0x00000100; // offset bool CnVItl -- may view other empires' intel (CCC_DatCor) [verified] constexpr uint32_t ServerPlayer_off_CnVItl = 0x00000101; // offset bool hgs -- gravitic-syncing drive researched (DRV_GrvSyn); client-synced only [verified] constexpr uint32_t ServerPlayer_off_hgs = 0x00000102; // offset bool hadvs -- advanced sensors (CCC_AdvSens) [verified] constexpr uint32_t ServerPlayer_off_hadvs = 0x00000103; // offset bool harcc -- arcologies (IND_ArcCon) [verified] constexpr uint32_t ServerPlayer_off_harcc = 0x00000104; // offset float pddm -- multiplied by 0.25 by IND_HrdStrct [verified] constexpr uint32_t ServerPlayer_off_pddm = 0x00000108; // offset float ConMod[3] -- construction cost per hull class; techs subtract from all three, IND_OrbDry only from [1] and [2] [verified] constexpr uint32_t ServerPlayer_off_ConMod = 0x0000010c; // offset float SavMod[3] -- IND_OrbFound subtracts 0.05f from all three [verified] constexpr uint32_t ServerPlayer_off_SavMod = 0x00000118; // offset float OutMod -- industrial output multiplier; several techs add, IND_HrdStrct multiplies by 0.9f [verified] constexpr uint32_t ServerPlayer_off_OutMod = 0x00000124; // offset float PopMod [verified] constexpr uint32_t ServerPlayer_off_PopMod = 0x00000130; // offset float TerraMod [verified] constexpr uint32_t ServerPlayer_off_TerraMod = 0x00000134; // offset bool AMine -- asteroid mining (IND_AstMine) [verified] constexpr uint32_t ServerPlayer_off_AMine = 0x00000138; // offset float MinRate -- mining rate; IND_MsMine adds 1.0 [verified] constexpr uint32_t ServerPlayer_off_MinRate = 0x00000140; // offset int PrGtTrf -- per-gate traffic capacity; the two gate techs raise it with a SIGNED INTEGER max against a config int, not a float compare [verified] constexpr uint32_t ServerPlayer_off_PrGtTrf = 0x00000148; // offset float CstR, CstE, CstT at +0x150/+0x154/+0x158 -- set to 10.0f / 2.0f / 1.0f (FLD1) by DRV_FarCast [verified] constexpr uint32_t ServerPlayer_off_CstR = 0x00000150; // offset uint32 design-option mask A; +0x194 is mask B. Rewritten wholesale by ComputeTechBitmasks on every completion; not serialised [verified] constexpr uint32_t ServerPlayer_off_TechMaskA = 0x00000190; // offset uint32 sticky 'level-1 translation researched for species sp' bit mask, one bit per species, NPC (4) excluded. Second pass of RebuildSpeciesTechFlags; only ever ORed [verified] constexpr uint32_t ServerPlayer_off_TranslationKnown = 0x000001a4; // offset uint32 HasVac; +0x28c HasImm. Both get |= the plague-cure mask of the completing tech [verified] constexpr uint32_t ServerPlayer_off_HasVac = 0x00000288; // offset uint32 NPTrk -- bit per species whose node-space traffic is visible; set by that species' node-track tech [verified] constexpr uint32_t ServerPlayer_off_NPTrk = 0x00000290; // offset int (*)[3] -- pointer to a separately allocated 3-word node-bore parameter block, NULL while no bore drive is researched (the updater allocates and frees it) [verified] constexpr uint32_t ServerPlayer_off_NodeBore = 0x00000308; // offset bool cdp -- set once CCC_SpyBm and IND_SlvgTech are both researched; the test is in the completion tail, not in either tech's branch [verified] constexpr uint32_t ServerPlayer_off_CaptureDesigns = 0x00000330; // offset uint32 flags[7] xenotech bits per target species, followed by a count word 7 at +0x364 (0x20 bytes assigned as a unit) [verified] constexpr uint32_t ServerPlayer_off_SpeciesTechFlags = 0x00000348; // offset bool -- a pending plague-cure roll; run and cleared when the completing tech is the current research target [verified] constexpr uint32_t ServerPlayer_off_ResearchRollPending = 0x000003b4; // offset AIRebellion* -- non-null while an AI rebellion object exists; the two AI techs notify it [verified] constexpr uint32_t ServerPlayer_off_AIRebellion = 0x000003b8; // offset MasterTechTree* -- the `this` MasterTechTree::IsTech / GetTechDef are called on [verified] constexpr uint32_t TechTree_off_Master = 0x00000004; // fastcall void (ServerPlayer* this) -- re-selects the node-bore parameters from the researched set and stores them in the +0x308 block, allocating it on first use and freeing it when no bore drive is researched [verified] constexpr uint32_t ServerPlayer_UpdateNodeBoreParams = 0x004182c0; // cdecl bool (int out[3], ServerPlayer* p) -- highest researched bore drive wins: DRV_RAD {95,60,5}, else DRV_REND {65,35,4}, else DRV_RIP {45,15,3}; false (and out left at {INT_MAX,INT_MAX,0}) when none. No species gate [verified] constexpr uint32_t SelectNodeBoreParams = 0x002e18e0; // custom int (TechDef* def in EBX) -- index of the species whose SpeciesDef+0x74 node-track tech this def is, or -1 [verified] constexpr uint32_t ServerPlayer_SpeciesOfTranslationTech = 0x0040e410; // cdecl struct {int techId; float rebellionOdds; float bonus;}* (TechDef* def) -- linear search of g_AITechValueTable, NULL when the def is none of them [verified] constexpr uint32_t AITechRow = 0x00290f70; // data int -- number of rows in g_AITechValueTable (6) [verified] constexpr uint32_t g_AITechValueCount = 0x006ea2ec; // offset void* -- the StrategyServer subobject pointer; StrategyServer = *(void**)(player+8) - 4. Its +0x16c is the Mars::RNG* the strategic sim draws from, and its +8 the event manager [verified] constexpr uint32_t ServerPlayer_off_ServerLink = 0x00000008; // offset Mars::RNG* -- the strategic generator (same object TechTree::ProcessResearch is handed) [verified] constexpr uint32_t StrategyServer_off_RNG = 0x0000016c; // fastcall void (ServerPlayer* this) -- odds = ResearchEventOdds(this, this->ResT); draws ONE NextFloat from StrategyServer's generator (entered at rng+4), unconditionally, then fires ServerPlayer_OnResearchRollSucceeded (0x00889d60) when odds > roll (fcompp + test ah,0x41, so equality also skips). THAT ONE WORD IS THE COST OF REACHING THE BRANCH, NOT OF A FIRED ROLL: the plague path inside draws a SECOND word (NextInt) to pick an owned system and posts EVENT_PLAGUE_OUTBREAK; the rebellion path allocates an AIRebellion at ServerPlayer+0x3b8 and cancels the research with no further draw. So a fired roll costs one or two words. Called from OnTechResearched when the completing def is the current research target and the pending-roll byte at +0x3b4 is set. This is the extra RNG draw B3 observed on a completion [verified] constexpr uint32_t ServerPlayer_RollResearchEvent = 0x0048df20; // thiscall float (ServerPlayer* this, TechDef* def) -- 0 when def is null; a plague-family path via 0x00535480, else the AI-rebellion path via AITechRow (odds column) gated on !NPC. Read-only, makes no draw [verified] constexpr uint32_t ServerPlayer_ResearchEventOdds = 0x00420380; // thiscall void (ServerSystem* this) /* NO stack arguments -- plain RET, nothing reads [ebp+8]; Ghidra's decompile shows a spurious second parameter. Body order: unowned Infra decay -> ApplyInfraBonus -> ApplyPopBonus -> independent pop drift -> IsStable/ntdev -> AccrueSystemBonus -> ProcessPlague -> ProcessBuildQueue -> imperial growth -> civilian growth -> AdjustResources(-out[2]) -> TRes=0 -> RefuelInOrbit(1) -> Bats2 tick -> rcex tick -> haltv[0..2]=false -> ProcessSlaves -> ProcessRebellion -> addiction sweep. Consumes no RNG itself; ProcessPlague / civilian growth / ProcessSlaves are draw-free to depth 1, and ProcessRebellion is the only consumer */ [verified] constexpr uint32_t ServerSystem_ProcessTurn = 0x003598e0; // thiscall bool (StrategyServer* this, StarFleet* fleet, float dt) /* RET 8; dt is a 4-byte float pushed with fstp DWORD [esp]; returns AL, true only when the fleet ends exactly on a waypoint destination at the deepest recursion level. range = MinRange(fleet, +0.05f) -- ADDED, not subtracted; out of range with MinRange(0)==0 zeroes the RANGE, not the step; move = min(min(range, step), distance) with no floor at 0; recursion iff fraction < (double)0.9999f with dt' = float32((1-fraction)*dt) */ [verified] constexpr uint32_t StrategyServer_MoveFleet = 0x003d9ee0; // thiscall void (StrategyServer* this) /* no stack args, plain RET. Pursuit schedule, not a departing/in-transit split: classify every fleet whose current waypoint targets a fleet by owner relation (0 = pursuer, else follower); pass1 prey dt 0.5, pass2 pursuers dt 0.5 (an arrival retires the pair), pass3 remaining prey dt 0.5, pass4 everything unscheduled dt 1.0 (an uncaught pursuer gets 0.5), pass5 followers dt 1.0. Then FPdpos, gate traffic, OnFleetArrived, and clears flag 0x100 on every fleet. Makes no RNG draw of its own */ [verified] constexpr uint32_t StrategyServer_ProcessFleetMovement = 0x003da9a0; // thiscall float (StarFleet* this, float bias) /* RET 4; min over ships of ship->Range seeded with FLT_MAX (an empty fleet is unconstrained), then + bias, narrowed to float32. No ship is skipped -- range-exempt tankers still clamp the fleet */ [verified] constexpr uint32_t StarFleet_MinRange = 0x002ff6a0; // thiscall bool (StarFleet* this, uint32 mask) /* (flags & mask) == mask, flags at fleet+0x10c */ [verified] constexpr uint32_t StarFleet_HasAllFlags = 0x002fe1d0; // thiscall void (StarFleet* this, uint32 mask, bool on) /* the only two writers of fleet+0x10c in the image */ [verified] constexpr uint32_t StarFleet_SetFlag = 0x002fe1a0; // thiscall void* (StarFleet* this) /* no args; NULL when the waypoint vector is empty or the front waypoint's target id does not resolve in the entity hash at fleet->galaxy(+0x10)+0x80 */ [verified] constexpr uint32_t StarFleet_ResolveWaypoint = 0x00301390; // cdecl bool (int waypointType) /* 7-entry jump table: true only for 4 and 5; false default */ [verified] constexpr uint32_t IsGateTransitWaypoint = 0x0016e6e0; // cdecl bool (int waypointType) /* 7-entry jump table: true only for 3. NOTE the node-LINE case of the movement switch is type 2, which this does NOT accept */ [verified] constexpr uint32_t IsNodeWaypoint = 0x0016e720; // thiscall void (StrategyServer* this, StarFleet* fleet, void* dest) /* waypoint type 5. v = float32(NextFloat() * player->CstE); arrives iff !(v > player->CstT) (equality arrives); on a miss the fleet is placed at dest + randomUnitVector * v -- scattered AROUND the destination by v, not advanced a fraction along the vector -- which costs a second raw draw. 1 draw on success, 2 on a miss */ [verified] constexpr uint32_t ProbabilisticJump = 0x003b6700; // cdecl void (Vector3* posOut, bool* arrivedOut, float nodespeed, float dt, vector* systems, const Vector3* from, const Vector3* to) /* builds the stutter segments, reverses them and pops from the back (so ascending by start); plain nodespeed between spheres, a constant per-segment speed inside one; arrived = (time < dt) || |along - length| < FLT_EPSILON, and on arrival the destination is copied verbatim */ [verified] constexpr uint32_t NodeLine_Step = 0x00305510; // cdecl void (vector* out, vector* systems, const Vector3* from, const Vector3* to) /* chord parameters scaled to world distance and clamped to [0, length]; a chord with |start-end| <= 0.01f is dropped; std::sort ascending by start; then ONE forward pass over adjacent pairs sets BOTH boundaries of an overlap to end_i + 0.5*(end_i - start_{i+1}) -- the mirror of the midpoint, pushed forward past both chords. Nothing is dropped or clipped back, so a swallowed chord comes out inverted */ [verified] constexpr uint32_t NodeLine_BuildStutterSegments = 0x00305280; // cdecl bool (float* tNear, float* tFar, const Sphere4* s, const Vector3* from, const Vector3* to) /* wraps the quadratic solver 0x008a62c0; rejects a sphere the segment does not reach and reports an open end as -FLT_MAX / +FLT_MAX */ [verified] constexpr uint32_t SegmentSphereIntersect = 0x004a64f0; // cdecl float (const Vector3* p, const Vector3* a, const Vector3* b) /* projection parameter clamped to [0,1]. NOTE 0x008c8eb0 is a varargs formatter, not this */ [verified] constexpr uint32_t DistPointToSegment = 0x004e8eb0; // thiscall int (ServerSystem* this, ServerPlayer* p, int flag) /* RET 8 -- (player, flag), NOT (species, groupType). Species is derived from p->Species and the group type is hard-coded to 0, which makes the cross-species and INDSYS branches dead in this specialisation. Returns the int64 result clamped to [0, INT32_MAX], low dword only */ [verified] constexpr uint32_t ServerSystem_MaxPop = 0x0034ab20; // thiscall int64 (ServerSystem* this, int groupType, int species, ServerPlayer* p, float* suitOverride) /* RET 0x10 */ [verified] constexpr uint32_t ServerSystem_MaxPopGeneric = 0x0034a4a0; // thiscall bool (ServerSystem* this) /* false without an owner, with Infra < 1.0, or with Suit != IdealSuit */ [verified] constexpr uint32_t ServerSystem_IsStable = 0x0034ad90; // thiscall void (ServerSystem* this) /* returns early unless ibon > 0 and Infra < 1; resets ntdev to 0 when the system is not the owner's home system; applied = min(ibon, float32(1 - Infra)); Infra becomes EXACTLY 1.0f when applied == the remainder, else float32(Infra + applied); ibon -= applied */ [verified] constexpr uint32_t ServerSystem_ApplyInfraBonus = 0x00346780; // thiscall void (ServerSystem* this) /* nothing unless pbon > 0; an unowned system drops the whole pool; returns when Pop >= MaxPop; resets ntdev for a non-home system; Pop += min(cap - Pop, pbon); pbon -= same. Pop and pbon are int32 */ [verified] constexpr uint32_t ServerSystem_ApplyPopBonus = 0x0034b510; // thiscall void (ServerSystem* this, float delta) /* no-op once Infra >= 1; clamps to 1 and re-normalises the system's own output rates when it lands there */ [verified] constexpr uint32_t ServerSystem_ApplyInfraDelta = 0x00348270; // thiscall void (ServerSystem* this, float delta) /* clamps at the ideal from whichever side it approached, so suitability never overshoots; re-normalises the rates when it lands exactly on the ideal */ [verified] constexpr uint32_t ServerSystem_ApplySuitDelta = 0x003481c0; // thiscall void (ServerSystem* this, int out[12], float rates[7]) /* RET 8. `out` is 12 dwords of MIXED type: 0 total (truncated), 1 strip-mined resources, 2 resources consumed, 3 money, 7 gross construction, 8 points to the queue, 9 points SPENT on repairs, 10 float32 infra delta, 11 float32 suitability delta. `rates` is copied to the stack first, so the caller's struct is untouched. NOT side-effect free: it repairs damaged ships in orbit via 0x00751590(points, estimateOnly=0) */ [verified] constexpr uint32_t ServerSystem_ComputeOutputFromRates = 0x00351bb0; // cdecl void (float rates[7], ServerSystem* sys, float* pinned) /* `pinned` defaults to &rates[0] (trade) and every call site passes NULL or that. Suppress terraform at the ideal (exact ==) and infra when float32(ibon+Infra) >= 1; zero any channel <= (double)1e-4f; clamp ONLY the pinned channel to [0,1]; sum the other three in float32; an exactly-zero sum seeds them with 1e-4f (honouring the suppressions); each becomes (r/sum)*(1-pinned) */ [verified] constexpr uint32_t ServerSystem_NormaliseOutputRates = 0x00347390; // thiscall void (ServerSystem* this, int* resourcesConsumedOut) /* computes the turn's output vector, writes out[2] (resources consumed) through the pointer, applies out[11] via ApplySuitDelta and out[10] via ApplyInfraDelta, and feeds out[8] to BuildQueue::ProcessTurn. Increments TnsOH while SRoh > 0 && out[1] > 0 */ [verified] constexpr uint32_t ServerSystem_ProcessBuildQueue = 0x00352500; // thiscall int (BuildQueue* this, ServerSystem* sys, int points) /* RET 8; `points` is BY VALUE and the leftover is the RETURN value. An order needing more than what is left absorbs everything and stops the pass; a design that costs money asks this->vft[9](sys, (int64)cost) and a refusal SKIPS that order and continues rather than stopping. Removal is a separate sweep afterwards that unlinks every order with conleft <= 0 */ [verified] constexpr uint32_t BuildQueue_ProcessTurn = 0x00490d50; // thiscall void (ServerSystem* this, int delta, int mode) /* RET 8; ProcessTurn passes -out[2] with mode 0 when the owner has AMine, else mode 3. Mode 3 takes it all from Res; mode 0 splits it proportionally over Res / ARes2 / MRes */ [verified] constexpr uint32_t ServerSystem_AdjustResources = 0x00345f30; // thiscall int64 (ServerSystem* this, int groupType, int species) /* RET 8; 0 without an owner or when haltv[groupType] is set */ [verified] constexpr uint32_t ServerSystem_PopGrowthDelta = 0x00348100; // thiscall float (ServerPlayer* p, int groupType, int species, float suit, float factor) /* the growth curve. base = 1 - clamp01(min(|ideal - clamp(suit,0,20)|, SuitTol) / SuitTol) -- there is NO pop/capacity term anywhere; g = clamp01(pow(base, clamp(POPULATION_GROWTH_EXP, 0.01f, 1000))) then x MOD x PopMod x factor x groupdef[+4], each gated on a strict > 0 and each stored back to a float32 */ [verified] constexpr uint32_t PopGrowthFraction = 0x00136fb0; // thiscall float (ServerSystem* this, int species) /* RET 4; 1.0 when unowned. mod = (bit0 ? 0.8f : 1) - 0.2*bit1 - 0.2*bit2, no clamp; rate = ((|Ideal-Suit| x BYHAZARD + DEATH_RATE) + SRs x BYOUTPUT) x mod, every step narrowed to float32 */ [verified] constexpr uint32_t ServerSystem_SlaveDeathRate = 0x0034b110; // thiscall void (ServerSystem* this) /* the worst plague's rate is ADDED to the death rate; SLAVES_MIN/MAX_DEATHS are each disabled by ANY negative value; deaths are clamped into [0, adjusted slave count]. Consumes no RNG */ [verified] constexpr uint32_t ServerSystem_ProcessSlaves = 0x003537b0; // thiscall void (ServerSystem* this) /* consumes no RNG, at depth 1 */ [verified] constexpr uint32_t ServerSystem_ProcessPlague = 0x00356a90; // thiscall void (ServerSystem* this) /* the ONLY RNG consumer in a colony turn, and its draw count is data-dependent: one RandChance per iteration of a 64-bit rebel counter (0x0074fbe0), a short-circuiting per-species roll loop (0x00753c60), one outcome roll (0x00756350) and one 0.2f continuation roll */ [verified] constexpr uint32_t ServerSystem_ProcessRebellion = 0x003583b0; // thiscall void (ServerSystem* this) /* consumes no RNG, at depth 1 */ [verified] constexpr uint32_t ServerSystem_GrowCivilianPops = 0x00354220; // stdcall bool (RNG* this /*ecx, the object*/, float p) /* p <= 0 -> false and p >= 1 -> true, both WITHOUT a draw; otherwise exactly one NextFloat and `r < p` */ [verified] constexpr uint32_t RNG_Chance = 0x004e6dd0; // thiscall void (ServerPlayer* this, MoraleEvent* ev, ServerSystem* sys) /* the actual apply. 0x00752a10 is only the MoraleEvent constructor; the per-species delta is carried in ev->deltas[species] (int[7] at ev+0x18), not as an argument */ [verified] constexpr uint32_t ServerPlayer_AddMoraleEvent = 0x00439d60; // thiscall MoraleEvent* (MoraleEvent* this) /* 0x50 bytes: {vptr, ints (+0x10 = event id), vptr2 @+0x14, int deltas[7] @+0x18, std::string name @+0x34} */ [verified] constexpr uint32_t MoraleEvent_ctor = 0x00352a10; // offset StrategyServer* owner (the RAW base; the RNG accessor uses owner-4) [verified] constexpr uint32_t ServerSystem_off_Owner = 0x00000010; // offset int Idx [verified] constexpr uint32_t ServerSystem_off_Idx = 0x0000005c; // offset int Size (1..10); the capacity base is Size * 100000000 as an exact int64 product [verified] constexpr uint32_t ServerSystem_off_Size = 0x00000060; // offset float Suit [verified] constexpr uint32_t ServerSystem_off_Suit = 0x00000064; // offset int Res [verified] constexpr uint32_t ServerSystem_off_Res = 0x00000068; // offset int ARes2 [verified] constexpr uint32_t ServerSystem_off_ARes2 = 0x0000006c; // offset int MRes [verified] constexpr uint32_t ServerSystem_off_MRes = 0x00000070; // offset int TRes -- zeroed every turn by ProcessTurn [verified] constexpr uint32_t ServerSystem_off_TRes = 0x00000074; // offset bool haltv[3] -- growth halt per group; cleared every turn by ProcessTurn [verified] constexpr uint32_t ServerSystem_off_haltv = 0x00000078; // offset float OutMod [verified] constexpr uint32_t ServerSystem_off_OutMod = 0x0000007c; // offset int TAcq (turn acquired) [verified] constexpr uint32_t ServerSystem_off_TAcq = 0x00000080; // offset StarSystem::OutputRates (0x1c): float SRt, SRsc, SRtf, SRi, SRoh, SRs; int SRnr [verified] constexpr uint32_t ServerSystem_off_Rates = 0x00000088; // offset float SRoh -- the over-harvest slider (Rates + 0x10) [verified] constexpr uint32_t ServerSystem_off_SRoh = 0x00000098; // offset BuildQueue* [verified] constexpr uint32_t ServerSystem_off_BuildQueue = 0x000000a4; // offset std::string Name (0x1c, MSVC SSO; capacity word at +0xbc) [verified] constexpr uint32_t ServerSystem_off_Name = 0x000000a8; // offset bool Abdn [verified] constexpr uint32_t ServerSystem_off_Abdn = 0x000000c4; // offset int64 Bats2 -- player i's 4-bit battle-recent counter at bits [4i, 4i+4), i < 15 [verified] constexpr uint32_t ServerSystem_off_Bats2 = 0x000000f0; // offset int64 rcex -- the same shape for the explored/recon-recent counter [verified] constexpr uint32_t ServerSystem_off_Rcex = 0x000000f8; // offset ServerPlayer* PID (null = unowned) [verified] constexpr uint32_t ServerSystem_off_PID = 0x00000100; // offset Morale cm (0x20: vptr + int[7]) [verified] constexpr uint32_t ServerSystem_off_Morale = 0x0000011c; // offset std::vector [verified] constexpr uint32_t ServerSystem_off_Fleets = 0x0000016c; // offset int Pop (imperial) [verified] constexpr uint32_t ServerSystem_off_Pop = 0x0000018c; // offset float Infra [verified] constexpr uint32_t ServerSystem_off_Infra = 0x00000190; // offset int pbon -- the pending population bonus (int32, not a float) [verified] constexpr uint32_t ServerSystem_off_pbon = 0x00000194; // offset float ibon -- the pending infrastructure bonus [verified] constexpr uint32_t ServerSystem_off_ibon = 0x00000198; // offset Population Pop2 (civilians): {vptr, vector @+4}, 24-byte entries {?, int type @+4, int species @+8, int64 count @+0x10} [verified] constexpr uint32_t ServerSystem_off_Pop2 = 0x000001a0; // offset Population pbon2 -- the civilian analogue of pbon [verified] constexpr uint32_t ServerSystem_off_pbon2 = 0x000001b4; // offset IndependenceInfo* indi (int indsp at +4) [verified] constexpr uint32_t ServerSystem_off_Indi = 0x000001c8; // offset int rbfl -- rebelling-species bitmask [verified] constexpr uint32_t ServerSystem_off_rbfl = 0x000001dc; // offset int adt[7] -- per-species addiction start turn; 0 means never addicted [verified] constexpr uint32_t ServerSystem_off_adt = 0x000001e4; // offset uint32 -- bit i is cleared when player i's Bats2 counter reaches 0 [verified] constexpr uint32_t ServerSystem_off_BatsMask = 0x000002a0; // offset uint32 -- the same for rcex [verified] constexpr uint32_t ServerSystem_off_RcexMask = 0x000002a4; // offset int TnsOH (turns over-harvesting) [verified] constexpr uint32_t ServerSystem_off_TnsOH = 0x000002b8; // offset int ntdev -- turns developing; ++ when IsStable, reset to 0 otherwise AND by either bonus-apply helper on a non-home system. This is the second SYSTEMBONUS_MINTURNS gate, not rbtn [verified] constexpr uint32_t ServerSystem_off_ntdev = 0x000002c4; // offset int rbtn [verified] constexpr uint32_t ServerSystem_off_rbtn = 0x000002cc; // offset int ModCount (the turn counter), relative to the RAW server base a ServerSystem's +0x10 points at [verified] constexpr uint32_t StrategyServer_off_ModCount = 0x00000008; // offset std::vector (begin @+0x50, end @+0x54); numPlayers = (end-begin)>>2 /* TWO BASES: every StrategyServer_off_* here is relative to the RAW base a ServerSystem's owner word (+0x10) points at. The class's own methods receive a base FOUR BYTES LOWER in ECX (0x007437f0 does owner-4), so a hook on MoveFleet or ProcessFleetMovement must add 4 to `this` before applying these */ [verified] constexpr uint32_t StrategyServer_off_Players = 0x00000050; // offset std::vector (begin @+0x60, end @+0x64) -- ALL fleets of ALL players, one flat global list. B4 live: the earlier 0x64 was the Ghidra-base number transcribed as a raw-base one; it made ProcessFleetMovement enumerate the vector's spare capacity instead of its elements [verified] constexpr uint32_t StrategyServer_off_Fleets = 0x00000060; // offset 16-bucket hash of entity id -> object, hashed by (key & 0xF); 0x008b9240 is the lookup [verified] constexpr uint32_t StrategyServer_off_EntityHash = 0x00000080; // offset Mars::RNG* relative to the RAW base (== the -4-adjusted base's +0x16c) [verified] constexpr uint32_t StrategyServer_off_RNGPtr = 0x00000168; // offset std::set> cleared at the head of ProcessFleetMovement [verified] constexpr uint32_t StrategyServer_off_ArrivedSet = 0x00000200; // offset std::set> that MoveFleet fills for fleets still in motion; OnFleetArrived takes the set difference [verified] constexpr uint32_t StrategyServer_off_InMotionSet = 0x00000210; // offset int id (also the entity-hash key) [verified] constexpr uint32_t StarFleet_off_Id = 0x00000004; // offset the object whose +0x80 holds the entity hash the waypoint target is resolved in [verified] constexpr uint32_t StarFleet_off_Galaxy = 0x00000010; // offset Vector3 Pos [verified] constexpr uint32_t StarFleet_off_Pos = 0x00000018; // offset Vector3 PrvPos -- set to the ENTRY position when the fleet moved [verified] constexpr uint32_t StarFleet_off_PrvPos = 0x0000004c; // offset ServerPlayer* owner [verified] constexpr uint32_t StarFleet_off_PID = 0x00000058; // offset Location*; kind at Location+0x14 (0 system, 1 fleet, 2 point) [verified] constexpr uint32_t StarFleet_off_Location = 0x000000a0; // offset std::vector (begin @+0xa4, end @+0xa8) [verified] constexpr uint32_t StarFleet_off_Ships = 0x000000a4; // offset int16 -- SIGNED, summed into the owner's gate traffic [verified] constexpr uint32_t StarFleet_off_GateTraffic = 0x000000c0; // offset std::vector _Myfirst (proxy @+0xc4, last @+0xcc, end @+0xd0) [verified] constexpr uint32_t StarFleet_off_Waypoints = 0x000000c8; // offset float FPsp2 -- the fleet's strategic speed [verified] constexpr uint32_t StarFleet_off_Speed = 0x000000d8; // offset Vector3 -- set after every pass to the current waypoint target's position. The notes called this FPogn2; by the FlightPlan layout it is FPdpos [verified] constexpr uint32_t StarFleet_off_DestPos = 0x000000ec; // offset uint32; bit 0x100 = held this turn (cleared for every fleet at the end of ProcessFleetMovement), bit 0x2 cleared in the destination pass, bit 0x1 set by MoveFleet when the fleet moved [verified] constexpr uint32_t StarFleet_off_Flags = 0x0000010c; // offset sizeof(Waypoint) -- pinned by the divide-by-28 reciprocal multiply in the iterator arithmetic [verified] constexpr uint32_t Waypoint_stride = 0x0000001c; // offset int -- the destination entity id [verified] constexpr uint32_t Waypoint_off_Target = 0x00000004; // offset int -- 2 node line, 3 node route, 4 gate teleport, 5 probabilistic jump, otherwise a straight run [verified] constexpr uint32_t Waypoint_off_Type = 0x00000008; // offset uint64 flag pair; bit 0x1000 exempts the ship from movement fuel and marks it a tanker [verified] constexpr uint32_t StarShip_off_Flags = 0x00000018; // offset float Range -- remaining strategic range [verified] constexpr uint32_t StarShip_off_Range = 0x00000020; // offset float -- the cap the refuel helper clamps Range to [verified] constexpr uint32_t StarShip_off_MaxRange = 0x00000078; // offset ServerSystem* -- the bonus-apply helpers skip the ntdev reset for this system [verified] constexpr uint32_t ServerPlayer_off_HomeSystem = 0x0000002c; // offset int GTraf -- assigned (not accumulated) once per turn [verified] constexpr uint32_t ServerPlayer_off_GateTraffic = 0x0000014c; // offset float CstE -- the probabilistic jump's efficiency [verified] constexpr uint32_t ServerPlayer_off_CstE = 0x00000154; // offset float CstT -- the probabilistic jump's threshold [verified] constexpr uint32_t ServerPlayer_off_CstT = 0x00000158; // data int** -- the GlobalConst pointer slot; the storage word is *slot [verified] constexpr uint32_t g_ptr_SYSTEMBONUS_MINTURNS = 0x006eca10; // data float** -- pointer slot [verified] constexpr uint32_t g_ptr_SYSTEMBONUS_POPBONUS = 0x006eca18; // data float** -- pointer slot [verified] constexpr uint32_t g_ptr_SYSTEMBONUS_INFRABONUS = 0x006eca20; // data float** -- pointer slot [verified] constexpr uint32_t g_ptr_SYSTEMBONUS_POPBONUS_INC = 0x006eca38; // data float** -- pointer slot [verified] constexpr uint32_t g_ptr_SYSTEMBONUS_INFRABONUS_INC = 0x006eca40; // data int** -- pointer slot (image default 10) [verified] constexpr uint32_t g_ptr_ADDICTION_PHASE2_START = 0x006eca58; // data int** -- pointer slot (image default 15) [verified] constexpr uint32_t g_ptr_ADDICTION_PHASE3_START = 0x006eca60; // data float -- 0.05f, a hard-coded literal in .data, NOT a GlobalConst; both ServerSystem::ProcessTurn calls to the independent pop-drift helper pass it [verified] constexpr uint32_t g_IndependentPopDriftRate = 0x006eca80; // data float** -- pointer slot; storage 0x00b212cc (shipped value 2) [verified] constexpr uint32_t g_ptr_STUTTER_SYSTEM_INFLUENCE_RADIUS = 0x006ebc44; // data float** -- pointer slot; storage 0x00b212d0 (shipped value 0.33) [verified] constexpr uint32_t g_ptr_STUTTER_MIN_SPEED = 0x006ebc48; // data float** -- pointer slot; storage 0x00b212d4 (shipped value 0.33). NOTE min == max in the shipped data, so the stutter ramp collapses to a constant 0.33x inside any influence sphere [verified] constexpr uint32_t g_ptr_STUTTER_MAX_SPEED = 0x006ebc4c; // thiscall EventStorage* (ServerPlayer* this) /* whole body: lea eax,[ecx+0x29c]; ret. No stack args, plain RET */ [verified] constexpr uint32_t ServerPlayer_GetEventStorage = 0x0040db00; // thiscall void* (ServerPlayer* this) /* eax = [this+8] ? [this+8]-4 : 0. StrategyServer primary base; turn = *(int*)(result+0x0c) */ [verified] constexpr uint32_t ServerPlayer_GetServer = 0x0040e320; // thiscall int (EventStorage* this, std::string summary /*BY VALUE 0x1c -> EvDsc*/, std::string message /*BY VALUE 0x1c -> EvMsg*/, void* obj, Vector3* pos, int turn, const char* img, int act) RET 0x4c. Returns the event id (a duplicate's id if one already exists in the turn bucket). Callee frees both by-value string buffers. img==NULL -> "". act==0 && obj==NULL && pos==NULL -> stored EvAct becomes 2. EvLoc = obj ? obj[+4] : 0; EvPos = obj ? obj[+0x18..0x20] : pos ? *pos : FLT_MAX triple. 161 call sites in 113 functions: this is the whole simulation's event API [verified] constexpr uint32_t EventStorage_PostEvent = 0x004862b0; // thiscall TurnEvents* (EventStorage* this, int turn) RET 4. Linear scan with NO early exit, so it returns the LAST bucket whose EvTurn == turn; otherwise appends a new bucket (ctor 0x00884cb0, vtable 0x00a0f07c) and sets its EvTurn [verified] constexpr uint32_t EventStorage_GetOrCreateTurnBucket = 0x00485380; // thiscall PlayerEvent* (EventStorage* this, TurnEvents* bucket, PlayerEvent* candidate) RET 8. NULL bucket -> 0. Match requires EvAct, EvLoc, all three EvPos floats (fucompp), EvMsg and EvImg to be equal. EvDsc is NOT compared [verified] constexpr uint32_t EventStorage_FindDuplicate = 0x00425d40; // thiscall void (EventStorage* this, int turn) RET 4. Cutoff = turn - 0x32 (50), a code constant. Shifts from the LAST bucket of the leading run with EvTurn < cutoff, so it erases n-1 of n leading stale buckets: one stale bucket always survives and a single leading stale bucket is never removed [verified] constexpr uint32_t EventStorage_PruneOldTurns = 0x00479eb0; // thiscall void (EventStorage* this, IStreamable* s) RET 4. Order: EvNxID (int, tag 0x00a2bd90), then nested collection "Events" (tag 0x00a2bda0, descriptor vtable 0x00a2da8c) over this+4 [verified] constexpr uint32_t EventStorage_Read = 0x00425cc0; // thiscall void (TurnEvents* this, IStreamable* s) RET 4. Order: EvTurn (int, tag 0x00a2bd98), then nested collection "Events" (descriptor vtable 0x00a2da7c) over this+8 [verified] constexpr uint32_t TurnEvents_Write = 0x00425bb0; // thiscall void (TurnEvents* this, IStreamable* s) RET 4. Same field order as TurnEvents_Write [verified] constexpr uint32_t TurnEvents_Read = 0x00425c40; // thiscall PlayerEvent* (PlayerEvent* this) RET 0. vptr=0x00a21958; EvEID=EvLoc=EvAct=EvCID=0; the three std::strings = "" (0x009e100c); EvPos = the Vector3 global at 0x00af0dc8 = {FLT_MAX, FLT_MAX, FLT_MAX} (0x7f7fffff x3, NOT infinity) [verified] constexpr uint32_t PlayerEvent_ctor = 0x0044ee30; // thiscall void (PlayerEvent* this, IStreamable* s) RET 4. vftable slot 1. Field order on the wire: EvEID(+4) EvDsc(+8) EvMsg(+0x24) EvImg(+0x50) EvLoc(+0x40) EvPos(+0x44) EvAct(+0x6c) EvCID(+0x70) [verified] constexpr uint32_t PlayerEvent_Serialize = 0x00425970; // thiscall call site: EventStorage::PostEvent for EVENT_RESEARCH_OVERBUDGET. Guard at 0x005879e9: !wasDone && nowDone && owner, inside the completion-roll-FAILED branch (chance < draw). Sets TechNode.flag(+0x2c)=2 at 0x00587ba3. EvAct=1, obj=NULL, pos=NULL [verified] constexpr uint32_t ProcessResearch_PostEventOverbudget = 0x00187b97; // thiscall call site: EventStorage::PostEvent for EVENT_TECHS_UNLOCKED, once after the per-node loop, if any node has state==2 and turnAvailable==currentTurn. EvAct=1, obj=NULL, pos=NULL [verified] constexpr uint32_t ProcessResearch_PostEventTechsUnlocked = 0x00187ff4; // thiscall call site: EventStorage::PostEvent for EVENT_RESEARCH_COMPLETE / _UNDERBUDGET. Guarded by !silent ([ebp+0xc]==0). Message is _snprintf'd (0x008c8eb0) into a 0x100-byte buffer, so >255 chars truncate. EvAct=1 [verified] constexpr uint32_t OnTechResearched_PostEventComplete = 0x004919b5; // thiscall call site: EventStorage::PostEvent for EVENT_TEMPERANCE. Guarded by !silent AND by the 'a system was cured' local at [ebp-0x189]. Pushed act=0 with obj=pos=NULL, so the STORED EvAct is 2 [verified] constexpr uint32_t OnTechResearched_PostEventTemperance = 0x00492427; // thiscall call site: EventStorage::PostEvent for EVENT_NO_RESEARCH. Condition at 0x0089162a: ResT(+0x294)==NULL && ListAvailableTechs(0x00584e50, turn, INT_MAX, 1) returned empty && TechTree 0x0057da90 != 0. EvAct=1 [verified] constexpr uint32_t ServerPlayer_ProcessTurn_PostEventNoResearch = 0x0049168c; // data const char* "Research completed at %d of %d (%.1f%%). (Odds: %.2f, Roll: %.2f)\n" -- log line on the completion-roll-SUCCEEDED branch, 0x00587977 [verified] constexpr uint32_t TechTree_ProcessResearch_LogFormat = 0x006007a8; // offset EventStorage ServerPlayer::Events -- embedded, size 0x1c. Confirmed by ServerPlayer_GetEventStorage [verified] constexpr uint32_t ServerPlayer_off_Events = 0x0000029c; // offset std::vector _Myfirst (element stride 0x18; _Mylast +0x08, _Myend +0x0c, _Alval +0x10) [verified] constexpr uint32_t EventStorage_off_Events = 0x00000004; // offset int EvNxID -- next event id. Starts at 0; PostEvent promotes 0->1 on the first post, then post-increments. ServerPlayer+0x2b0 [verified] constexpr uint32_t EventStorage_off_EvNxID = 0x00000014; // offset sizeof(EventStorage) [verified] constexpr uint32_t EventStorage_sizeof = 0x0000001c; // offset int EvTurn (after the vptr at +0) [verified] constexpr uint32_t TurnEvents_off_EvTurn = 0x00000004; // offset std::vector _Myfirst (stride 0x74; _Mylast +0x0c, _Myend +0x10, _Alval +0x14) [verified] constexpr uint32_t TurnEvents_off_Events = 0x00000008; // offset sizeof(TurnEvents); the outer vector's stride, from the /24 divide at 0x008853b3 [verified] constexpr uint32_t TurnEvents_sizeof = 0x00000018; // offset int EvEID (after the vptr at +0) [verified] constexpr uint32_t PlayerEvent_off_EvEID = 0x00000004; // offset std::string EvDsc (summary / title), 0x1c bytes [verified] constexpr uint32_t PlayerEvent_off_EvDsc = 0x00000008; // offset std::string EvMsg (body), 0x1c bytes [verified] constexpr uint32_t PlayerEvent_off_EvMsg = 0x00000024; // offset int EvLoc (object id, or 0) [verified] constexpr uint32_t PlayerEvent_off_EvLoc = 0x00000040; // offset float[3] EvPos (default FLT_MAX x3) [verified] constexpr uint32_t PlayerEvent_off_EvPos = 0x00000044; // offset std::string EvImg (event-type name, e.g. "EVENT_RESEARCH_OVERBUDGET"), 0x1c bytes [verified] constexpr uint32_t PlayerEvent_off_EvImg = 0x00000050; // offset int EvAct [verified] constexpr uint32_t PlayerEvent_off_EvAct = 0x0000006c; // offset int EvCID -- ctor sets 0 and PostEvent never writes it [verified] constexpr uint32_t PlayerEvent_off_EvCID = 0x00000070; // offset sizeof(PlayerEvent) = 116. Two independent confirmations: the /116 divide at 0x00825d5f and PostEvent's 'mov [_Mylast-0x70], id' writing EvEID at element+4 [verified] constexpr uint32_t PlayerEvent_sizeof = 0x00000074; // offset prune cutoff = turn - 50; a code constant at 0x00879ec3, not config [verified] constexpr uint32_t EventStorage_PruneWindowTurns = 0x00000032; // data void** -- PlayerEvent vftable. slot0 dtor 0x007694d0, slot1 Serialize 0x00825970, slot2 0x00825ab0. RTTI locator 0x00a80a7c [verified] constexpr uint32_t g_vft_PlayerEvent = 0x00621958; // data void** -- TurnEvents vftable (bucket ctor 0x00884cb0); slot0 is the virtual dtor the pruner calls [verified] constexpr uint32_t g_vft_TurnEvents = 0x0060f07c; // data const char[12][8] -- stride 8: EvEID EvNxID EvTurn Events EvPos EvLoc EvMsg EvImg EvDsc EvCID EvAct sasc [verified] constexpr uint32_t g_EventTagTable = 0x0062bd88; // data float[3] = {0x7f7fffff, 0x7f7fffff, 0x7f7fffff} = FLT_MAX. PlayerEvent's default EvPos. NOT infinity [verified] constexpr uint32_t g_Vector3_Invalid = 0x006f0dc8; // data const char* "EVENT_RESEARCH_OVERBUDGET" -- EvImg pushed at 0x00587b24 [verified] constexpr uint32_t g_str_EVENT_RESEARCH_OVERBUDGET = 0x0060078c; // data const char* "EVENT_RESEARCH_COMPLETE" -- EvImg for progress/cost >= 0.8 [verified] constexpr uint32_t g_str_EVENT_RESEARCH_COMPLETE = 0x00633368; // data const char* "EVENT_RESEARCH_UNDERBUDGET" -- EvImg for progress/cost < 0.8 [verified] constexpr uint32_t g_str_EVENT_RESEARCH_UNDERBUDGET = 0x00633380; // data const char* "EVENT_TEMPERANCE" -- EvImg at 0x008923ae; posted with act=0 so the stored EvAct is 2 [verified] constexpr uint32_t g_str_EVENT_TEMPERANCE = 0x00633340; // data const char* "EVENT_TECHS_UNLOCKED" (length 0x14 pushed at 0x00587eb6) [verified] constexpr uint32_t g_str_EVENT_TECHS_UNLOCKED = 0x00600774; // data const char* "EVENT_NO_RESEARCH" -- EvImg at 0x0089168c [verified] constexpr uint32_t g_str_EVENT_NO_RESEARCH = 0x0063332c; // data const char** -> slot 0x00ae48e0; Strings.csv key EVENTSUM_RESEARCH_OVERBUDGET = "Research Over Budget" [verified] constexpr uint32_t g_ptr_EVENTSUM_RESEARCH_OVERBUDGET = 0x006e48e4; // data const char** -> slot 0x00ae48e8; key EVENTMSG_RESEARCH_OVERBUDGET = "Research for %s has gone overbudget." [verified] constexpr uint32_t g_ptr_EVENTMSG_RESEARCH_OVERBUDGET = 0x006e48ec; // data const char** -> slot 0x00ae48f0; key EVENTSUM_UNLOCKEDTECHS = "New Technologies Available" [verified] constexpr uint32_t g_ptr_EVENTSUM_UNLOCKEDTECHS = 0x006e48f4; // data const char** -> slot 0x00ae48f8; key EVENTMSG_UNLOCKEDTECHS [verified] constexpr uint32_t g_ptr_EVENTMSG_UNLOCKEDTECHS = 0x006e48fc; // data const char** -> slot 0x00af09e8; key EVENTSUM_RESEARCH_COMPLETE = "Research Complete" [verified] constexpr uint32_t g_ptr_EVENTSUM_RESEARCH_COMPLETE = 0x006f09ec; // data const char** -> slot 0x00af09f0; key EVENTMSG_RESEARCH_COMPLETE = "Tech %s has been acquired" [verified] constexpr uint32_t g_ptr_EVENTMSG_RESEARCH_COMPLETE = 0x006f09f4; // data const char** -> slot 0x00af09f8; key EVENTSUM_RESEARCH_UNDERBUDGET = "Research Breakthrough!" [verified] constexpr uint32_t g_ptr_EVENTSUM_RESEARCH_UNDERBUDGET = 0x006f09fc; // data const char** -> slot 0x00af0a00; key EVENTMSG_RESEARCH_UNDERBUDGET [verified] constexpr uint32_t g_ptr_EVENTMSG_RESEARCH_UNDERBUDGET = 0x006f0a04; // data const char** -> slot 0x00af0a88; key EVENTSUM_ADDICTION_TEMPERENCE (shipped misspelling) [verified] constexpr uint32_t g_ptr_EVENTSUM_ADDICTION_TEMPERENCE = 0x006f0a8c; // data const char** -> slot 0x00af0a90; key EVENTMSG_ADDICTION_TEMPERENCE [verified] constexpr uint32_t g_ptr_EVENTMSG_ADDICTION_TEMPERENCE = 0x006f0a94; // data double 0.800000011920929 = (double)0.8f. progress/cost >= this -> EVENT_RESEARCH_COMPLETE, else EVENT_RESEARCH_UNDERBUDGET; the same constant gates TechNode.flag=0 in ProcessResearch [verified] constexpr uint32_t g_dbl_ResearchUnderbudgetThreshold = 0x005e20c8; // data double 0.0 -- the research completion draw is NextFloat()*(1.0-this)+this, so it is a plain NextFloat() [verified] constexpr uint32_t g_dbl_ResearchRollBias = 0x005e1e68; // cdecl float (Vec3* out, const Vec3* in) /* normalises in-place-capable (MoveFleet passes the same pointer twice) and RETURNS THE LENGTH. FOUR float32 narrowings in this order: sumsq = float32(x*x+y*y+z*z) (products/adds stay in the x87 53-bit registers, only the sum is stored to a dword and reloaded); len = float32(sqrt(sumsq)); inv = float32(1.0/len) -- a RECIPROCAL, stored to a dword and reloaded, then MULTIPLIED through, NOT three divides; out.c = float32(in.c * inv). Zero branch: !(len > 2^-23, the float at 0x009e1ef8) => out = {0,0,0} and it returns with an EMPTY x87 stack, i.e. no return value at all (original bug, only reachable on a zero-length vector). 123 callers. */ [verified] constexpr uint32_t Mars_Vec3_Normalize = 0x00022520; // cdecl float (const Vec3* v) /* float32(sqrt(float32(x*x+y*y+z*z))) -- the same two narrowings as the first half of Mars_Vec3_Normalize */ [verified] constexpr uint32_t Mars_Vec3_Length = 0x000224b0; // cdecl float (const Vec3* v) /* float32(x*x+y*y+z*z), one narrowing on the sum */ [verified] constexpr uint32_t Mars_Vec3_LengthSquared = 0x000224f0; // data const float = 0x34000000 = 2^-23 = 1.1920928955078125e-07 /* Mars_Vec3_Normalize zeroes the direction when !(len > this) */ [verified] constexpr uint32_t Mars_Vec3_NormaliseEpsilon = 0x005e1ef8; // site site inside StrategyServer::MoveFleet /* the straight-run leg. Each delta dest.c - fleet.pos.c is computed on the x87 stack and STORED BACK TO A FLOAT32 SLOT before Mars_Vec3_Normalize(&v, &v) is called on it in place; that single call returns the leg DISTANCE and leaves the float32 unit direction in the same slots. The distance is never recomputed. */ [verified] constexpr uint32_t StrategyServer_MoveFleet_straight_leg = 0x003da0f2; // site site inside StrategyServer::MoveFleet /* the move != distance branch: exactly two roundings per component, tmp.c = float32(dir.c * move) then pos.c = float32(pos.c + tmp.c). `move` is reloaded from a float32 slot. The sibling branch (move == distance, an EXACT float compare) copies the destination's three words verbatim with mov, so an arrival never steps onto its destination. */ [verified] constexpr uint32_t StrategyServer_MoveFleet_position_update = 0x003da2ac; // constant sizeof(Game::ObservedTech) = 0x2c (44 bytes). Confirmed independently by (a) the compiler's magic division by 44 (mov eax,0x2e8ba2e9; imul; sar edx,3) at 0x0087239f and 0x007b7339, (b) imul reg,reg,0x2c at 0x0087243a, 0x00872468, 0x007b735b, and (c) the iterator advance `add edi,0x2c` in RecordObservedTech's linear search at 0x007ba257. FULL MEMBER MAP, from ObservedTech_Write 0x00817cf0 / ObservedTech_Read 0x00817c40 (lane S 2026-09-08): +0x00 vptr 0x00a2439c; +0x04 uint16 otnF; +0x06 uint16 otnL; +0x08 bool odet (1 byte, +3 pad); +0x0c std::string otch (0x1c, so _Mysize at +0x1c, _Myres at +0x20, _Alval at +0x24); +0x28 int owith. 4 + 2 + 2 + 4 + 0x1c + 4 = 0x2c exactly, no padding slack and no unaccounted field. LIVE CONFIRMATION (lane V, 2026-09-08, VM140, build eventlive-dd38117-20260908T0916Z): the ProcessResearch `observed_techs` Result region measured the vector's byte span growing by exactly 44 on each of the two tech-completion calls of the five-End-Turn run from ref-turn2 (call 3: 440 -> 484; call 9: 484 -> 528) -- an independent behavioural confirmation of the static pin. Both non-completing players' spans were 880 = 20 x 44 and never moved. [verified] constexpr uint32_t ObservedTech_sizeof = 0x0000002c; // data Game::ObservedTech vftable. RTTI COL 0x00a81c78 -> type descriptor 0x00aeede4 = '.?AVObservedTech@Game@@'. Written to element+0x00 by ObservedTech_ctor 0x008562a0 -- so ObservedTech is polymorphic and its first word is the vptr, not a data field. [verified] constexpr uint32_t ObservedTech_vftable = 0x0062439c; // offset std::string (save tag `otch`, the tech name) at ObservedTech+0x0c, 0x1c bytes, spanning +0x0c..+0x27. MSVC layout relative to the string object: _Bx[16] @+0x00, _Mysize @+0x10, _Myres @+0x14, _Alval @+0x18 -- i.e. ObservedTech+0x1c is the length, +0x20 the capacity, +0x24 the empty-allocator word (never read or written by anything). Evidence: ObservedTech_ctor 0x008562a0 writes [elem+0x0c]=0, [elem+0x1c]=0, [elem+0x20]=0xf; RecordObservedTech's search reads [elem+0x1c] as the length and calls compare with this=elem+0x0c; the post-append assign uses lea ecx,[_Mylast-0x20]. CORRECTION (lane S 2026-09-08): an earlier revision called this string 0x18 bytes and listed +0x24 as an unaccounted data field. It is not one -- three independent whole-object enumerations skip +0x24 entirely: ObservedTech_ctor 0x008562a0, ObservedTech_copy_ctor 0x0079a184, and ObservedTech_Write 0x00817cf0 (which serialises +0x04,+0x06,+0x08,+0x0c,+0x28 and nothing else). sizeof(std::string)=0x1c holds binary-wide; see std_string_sizeof. [verified] constexpr uint32_t ObservedTech_off_Name = 0x0000000c; // thiscall void (this, ServerPlayer* observer, ?, std::string* techName) -- appends to observer->otch. Linear-searches observer->otch (ServerPlayer+0x274) with stride 0x2c comparing each element's name string; if not found, default-constructs an ObservedTech on the stack (0x008562a0) and push_backs it (0x007b7320 @0x007ba288), then writes otnF (+0x04) and otnL (+0x06), both 16-bit, from the same source word param_1+0xc -- i.e. first-observed turn == last-observed turn on the first sighting, which is what the tag names now confirm. DIRECT CALLEE of ServerPlayer::OnTechResearched 0x00891790; also called from 0x007be228, 0x007be4e1, 0x007be535. This is the append lane P could not find. DE-DUPLICATING: appends only when no existing element carries that tech name, so a reimplementation must not naively push_back on re-observation. [verified] constexpr uint32_t RecordObservedTech = 0x003ba1a0; // thiscall void (std::vector* this, ObservedTech* value) RET 4. Stride 0x2c. Calls vector_44B_grow 0x007b5820 when _Mylast==_Myend -- the realloc that moves all three vector words. Exactly one caller (RecordObservedTech), so this instantiation is not COMDAT-ambiguous. [verified] constexpr uint32_t vector_ObservedTech_push_back = 0x003b7320; // thiscall std::vector& operator=(const std::vector&) RET 4. Both callers pass ServerPlayer+0x274 (0x00878091 in the settings copy-out, 0x00892507 in the copy-in). [verified] constexpr uint32_t vector_ObservedTech_assign = 0x00472380; // thiscall Game::ObservedTech* (ObservedTech* this) -- default ctor. Writes exactly: vptr 0x00a2439c at +0x00; dword 0 at +0x04 (otnF+otnL zeroed together); BYTE 0 at +0x08 (`mov [esi+0x8],bl`, 0x008562f4 -- odet is a bool, not an int); the empty string at +0x0c (_Buf[0]=0, _Mysize=0, _Myres=0xf, then assign("") 0x00425550); dword 0 at +0x28 (owith). It never touches +0x24 -- that word is the string's _Alval. [verified] constexpr uint32_t ObservedTech_ctor = 0x004562a0; // thiscall void Game::ObservedTech::Write(Mars::Stream* s) RET 4. Vftable 0x00a2439c slot [2]. Serialises the whole object, in order and with nothing else: `otnF` = movzx word [this+0x04] via stream vft+0x24 (int); `otnL` = movzx word [this+0x06] via vft+0x24; `odet` = WriteBool 0x008b9c20 (&this[+0x08]); `otch` = WriteString 0x008b9d70 (&this[+0x0c]); `owith` = [this+0x28] via vft+0x24. THIS IS THE MEMBER MAP: there is no field at +0x24. Tag pointers 0x00a2b38c/0x00a2b384/0x00a2b36c/0x00a2b3e8/0x00a2b364. [verified] constexpr uint32_t ObservedTech_Write = 0x00417cf0; // thiscall void Game::ObservedTech::Read(Mars::Stream* s) RET 4. Vftable 0x00a2439c slot [1]. Mirror of ObservedTech_Write: `otnF`/`otnL` through stream vft+0x10 (int-by-ref) stored back as 16-bit (`mov word [ebx],ax`) at +0x04/+0x06, `odet` = ReadBool 0x008b9c00 (&this[+0x08]), `otch` = ReadString 0x008b9d90 (&this[+0x0c]), `owith` at +0x28 (reached as `add edi,0x28`). [verified] constexpr uint32_t ObservedTech_Read = 0x00417c40; // thiscall ObservedTech* (ObservedTech* this, int flags) RET 4. Vftable 0x00a2439c slot [0], scalar deleting dtor. Inlines ~basic_string on the name at +0x0c (`cmp [esi+0x20],0x10` -> operator delete [esi+0x0c], then _Tidy: [esi+0x20]=0xf, [esi+0x1c]=0, byte [esi+0x0c]=0), restores the base vptr 0x009e22bc, and frees `this` when flags&1. The string is the ONLY member needing destruction -- confirming there is no second string or owned pointer in the element. [verified] constexpr uint32_t ObservedTech_dtor = 0x00393610; // site site inside the vector uninitialised-copy helper FUN_0079a150(&_Alval, dest, src). The inlined copy constructor writes vptr 0x00a2439c, `mov word [dst+0x04],[src+0x04]`, `mov word [dst+0x06],[src+0x06]`, `mov byte [dst+0x08],[src+0x08]`, the string at +0x0c (via basic_string::assign 0x00425430), and `mov [dst+0x28],[src+0x28]`. Nothing is copied at +0x24 -- second independent proof that +0x24 belongs to the 0x1c string, not to a data member. [verified] constexpr uint32_t ObservedTech_copy_ctor = 0x0039a184; // offset uint16 otnF -- turn the tech was first observed. Widened to int on disk by stream vft+0x24. Written together with otnL from one source word at RecordObservedTech 0x007ba2b0. [verified] constexpr uint32_t ObservedTech_off_TurnFirst = 0x00000004; // offset uint16 otnL -- turn the tech was last observed. Widened to int on disk. Written at RecordObservedTech 0x007ba2c6 from the same source word as otnF. [verified] constexpr uint32_t ObservedTech_off_TurnLast = 0x00000006; // offset bool odet -- ONE BYTE (ctor `mov [esi+0x8],bl` 0x008562f4; copy-ctor `mov byte` 0x0079a1a0), serialised through WriteBool/ReadBool, so on disk it is 1 byte + 3 NUL joint padding. Note the save reader types `odet` as int; for a 4-character tag a bool item and an int item are the same 12 bytes and the value is identical, so that is benign, not a parse error. [verified] constexpr uint32_t ObservedTech_off_Detected = 0x00000008; // offset int owith -- last member of the element; serialised through stream vft+0x24 with tag 0x00a2b364. +0x28..+0x2b is the tail of the 0x2c-byte element, which is why sizeof is 0x2c with no padding slack. [verified] constexpr uint32_t ObservedTech_off_With = 0x00000028; // thiscall void Game::ObservedWeapon::Write(Mars::Stream* s) RET 4. Byte-for-byte the same shape as ObservedTech_Write with tag `owep` (0x00a2b3f0) in place of `otch`: otnF u16 @+0x04, otnL u16 @+0x06, odet bool @+0x08, owep std::string (0x1c) @+0x0c, owith int @+0x28. Reader is 0x00817b10. Independent second instance of the same 0x2c element shape. [verified] constexpr uint32_t ObservedWeapon_Write = 0x00417bc0; // constant sizeof(std::basic_string) = 0x1c (28) binary-wide, ONE layout only: _Bx (16-byte SSO union, char[16] or char*) @+0x00, _Mysize @+0x10, _Myres @+0x14, _Alval (empty allocator) @+0x18. The allocator word is trailing, never read and never written -- the same allocator-last shape as this build's std::vector {_Myfirst,_Mylast,_Myend,_Alval} = 0x10. Evidence: (a) Stream::WriteString 0x008b9d76 `cmp [str+0x14],0x10` / `mov eax,[str]` and basic_string::assign 0x00425550 (_Mysize +0x10, _Myres +0x14) fix the field offsets; (b) the vector scan loop FUN_00699bd0 advances `add esi,0x1c` (0x00699c29) while doing the SSO test at [esi+0x14]/[esi] -- element base == string base, so the stride IS the sizeof; (c) PostEvent 0x008862b0 takes two by-value strings at [ebp+0x08] and [ebp+0x24] and RET 0x4c = 2*0x1c + 5*4; (d) a whole-binary sweep of the Stream string helpers recovered 65 std::string members off a non-stack base across all serializers -- ZERO have any sibling member inside (N, N+0x1c), and 51 of the 52 that have a next member have it at exactly N+0x1c (the one exception, StrategyServer KeyPath @+0x134, is +0x20 on the Write side and +0x1c on the Read side, i.e. the writer skips a member). There is no 0x18 instantiation, no empty-base variant and no game-local string class. [verified] constexpr uint32_t std_string_sizeof = 0x0000001c; // thiscall vector::_Reserve/grow for a 44-byte element type. Shared with FUN_0086dec0, so it may be a COMDAT-folded body -- do NOT assume it is ObservedTech-specific. [mapped] constexpr uint32_t vector_44B_grow = 0x003b5820; // thiscall void (Game_AIAutoPeaceRun* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a199b4, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_AIAutoPeaceRun_Write = 0x00292cf0; // thiscall void (Game_AIAutoPeaceRun* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a199b4, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_AIAutoPeaceRun_Read = 0x00292c90; // layout sizeof(Game::AIAutoPeaceRun) -- container stride [verified] constexpr uint32_t sizeof_Game_AIAutoPeaceRun = 0x00000010; // thiscall void (Game_BackEngProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a3185c, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_BackEngProject_Write = 0x00453390; // thiscall void (Game_BackEngProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a3185c, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_BackEngProject_Read = 0x00453280; // thiscall void (Game_CombatPlayerReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a0623c, COL offset +0x0; 11 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CombatPlayerReport_Write = 0x0019d6d0; // thiscall void (Game_CombatPlayerReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a0623c, COL offset +0x0; 11 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CombatPlayerReport_Read = 0x0019d360; // layout sizeof(Game::CombatPlayerReport) -- container stride [verified] constexpr uint32_t sizeof_Game_CombatPlayerReport = 0x00000054; // thiscall void (Game_CombatPlayerStats* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009e4b74, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CombatPlayerStats_Write = 0x00059140; // thiscall void (Game_CombatPlayerStats* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009e4b74, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CombatPlayerStats_Read = 0x00059070; // layout sizeof(Game::CombatPlayerStats) -- container stride [verified] constexpr uint32_t sizeof_Game_CombatPlayerStats = 0x00000024; // thiscall void (Game_CombatReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a0624c, COL offset +0x0; 14 member fields; 6/6 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CombatReport_Write = 0x0019db30; // thiscall void (Game_CombatReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a0624c, COL offset +0x0; 14 member fields; 6/6 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CombatReport_Read = 0x0019d8f0; // thiscall void (Game_CombatShipReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a06064, COL offset +0x0; 12 member fields; 7/7 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CombatShipReport_Write = 0x0019b960; // thiscall void (Game_CombatShipReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a06064, COL offset +0x0; 12 member fields; 7/7 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CombatShipReport_Read = 0x0019c650; // layout sizeof(Game::CombatShipReport) -- container stride [verified] constexpr uint32_t sizeof_Game_CombatShipReport = 0x00000050; // thiscall void (Game_CombatWeaponReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a06074, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CombatWeaponReport_Write = 0x0019bb70; // thiscall void (Game_CombatWeaponReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a06074, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CombatWeaponReport_Read = 0x0019bad0; // layout sizeof(Game::CombatWeaponReport) -- container stride [verified] constexpr uint32_t sizeof_Game_CombatWeaponReport = 0x00000030; // thiscall void (Game_CommDeclareShareSystemNotes* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a3157c, COL offset +0x0; 8 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CommDeclareShareSystemNotes_Write = 0x00410da0; // thiscall void (Game_CommDeclareShareSystemNotes* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a3157c, COL offset +0x0; 8 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CommDeclareShareSystemNotes_Read = 0x00410d50; // thiscall void (Game_CommonPlague* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f8b50, COL offset +0x0; 6 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CommonPlague_Write = 0x001360e0; // thiscall void (Game_CommonPlague* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f8b50, COL offset +0x0; 6 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_CommonPlague_Read = 0x001373e0; // thiscall void (Game_DefenceFleetAssignment* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009e3c48, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_DefenceFleetAssignment_Write = 0x004119d0; // thiscall void (Game_DefenceFleetAssignment* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009e3c48, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_DefenceFleetAssignment_Read = 0x00432330; // thiscall void (Game_DiplomacyStats* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21430, COL offset +0x0; 14 member fields; 13/13 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_DiplomacyStats_Write = 0x00418cb0; // thiscall void (Game_DiplomacyStats* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21430, COL offset +0x0; 14 member fields; 13/13 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_DiplomacyStats_Read = 0x00418b80; // layout sizeof(Game::DiplomacyStats) -- container stride [verified] constexpr uint32_t sizeof_Game_DiplomacyStats = 0x00000024; // thiscall void (Game_EventStorage* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a313c8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_EventStorage_Write = 0x00425cc0; // thiscall void (Game_EventStorage* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a313c8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_EventStorage_Read = 0x004850f0; // thiscall void (Game_EventStorage_Event* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21958, COL offset +0x0; 8 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_EventStorage_Event_Write = 0x00425ab0; // thiscall void (Game_EventStorage_Event* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21958, COL offset +0x0; 8 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_EventStorage_Event_Read = 0x00425970; // thiscall void (Game_EventStorage_TurnEvents* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a0f07c, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_EventStorage_TurnEvents_Write = 0x00425c40; // thiscall void (Game_EventStorage_TurnEvents* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a0f07c, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_EventStorage_TurnEvents_Read = 0x00425bb0; // layout sizeof(Game::EventStorage::TurnEvents) -- container stride [verified] constexpr uint32_t sizeof_Game_EventStorage_TurnEvents = 0x00000018; // thiscall void (Game_FleetNameGenerator* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a32760, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_FleetNameGenerator_Write = 0x00417460; // thiscall void (Game_FleetNameGenerator* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a32760, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_FleetNameGenerator_Read = 0x00417440; // thiscall void (Game_FlightPlan* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1d50c, COL offset +0x0; 6 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_FlightPlan_Write = 0x00300f60; // thiscall void (Game_FlightPlan* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1d50c, COL offset +0x0; 6 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_FlightPlan_Read = 0x00304c70; // layout sizeof(Game::FlightPlan) -- enumeration meets embedding [verified] constexpr uint32_t sizeof_Game_FlightPlan = 0x00000038; // thiscall void (Game_IndependenceInfo* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2005c, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_IndependenceInfo_Write = 0x00348ee0; // thiscall void (Game_IndependenceInfo* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2005c, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_IndependenceInfo_Read = 0x00348df0; // thiscall void (Game_JewelsProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa438, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_JewelsProject_Write = 0x00148910; // thiscall void (Game_JewelsProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa438, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_JewelsProject_Read = 0x001488d0; // thiscall void (Game_MonitorProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a3180c, COL offset +0x0; 7 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_MonitorProject_Write = 0x004287d0; // thiscall void (Game_MonitorProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a3180c, COL offset +0x0; 7 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_MonitorProject_Read = 0x00428740; // thiscall void (Game_Morale* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1f7c8, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_Morale_Write = 0x00344ea0; // thiscall void (Game_Morale* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1f7c8, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_Morale_Read = 0x00344dd0; // thiscall void (Game_MoraleEvent* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2003c, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_MoraleEvent_Write = 0x003491b0; // thiscall void (Game_MoraleEvent* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2003c, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_MoraleEvent_Read = 0x003490b0; // layout sizeof(Game::MoraleEvent) -- container stride [verified] constexpr uint32_t sizeof_Game_MoraleEvent = 0x00000050; // thiscall void (Game_NodePath* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1cb50, COL offset +0x0; 11 member fields; 6/6 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_NodePath_Write = 0x002e1fd0; // thiscall void (Game_NodePath* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1cb50, COL offset +0x0; 11 member fields; 6/6 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_NodePath_Read = 0x002e1e90; // layout sizeof(Game::NodePath) -- container stride [verified] constexpr uint32_t sizeof_Game_NodePath = 0x00000030; // thiscall void (Game_ObservedTech* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2439c, COL offset +0x0; 5 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ObservedTech_Write = 0x00417cf0; // thiscall void (Game_ObservedTech* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2439c, COL offset +0x0; 5 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ObservedTech_Read = 0x00417c40; // layout sizeof(Game::ObservedTech) -- container stride [verified] constexpr uint32_t sizeof_Game_ObservedTech = 0x0000002c; // thiscall void (Game_ObservedWeapon* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2438c, COL offset +0x0; 5 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ObservedWeapon_Write = 0x00417bc0; // thiscall void (Game_ObservedWeapon* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2438c, COL offset +0x0; 5 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ObservedWeapon_Read = 0x00417b10; // layout sizeof(Game::ObservedWeapon) -- container stride [verified] constexpr uint32_t sizeof_Game_ObservedWeapon = 0x0000002c; // thiscall void (Game_PlayerAid* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a286dc, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_PlayerAid_Write = 0x00418dc0; // thiscall void (Game_PlayerAid* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a286dc, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_PlayerAid_Read = 0x004207b0; // layout sizeof(Game::PlayerAid) -- container stride [verified] constexpr uint32_t sizeof_Game_PlayerAid = 0x00000018; // thiscall void (Game_PlayerNotes* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21948, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_PlayerNotes_Write = 0x004132b0; // thiscall void (Game_PlayerNotes* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21948, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_PlayerNotes_Read = 0x00413250; // thiscall void (Game_PlayerReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21440, COL offset +0x0; 11 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_PlayerReport_Write = 0x00417480; // thiscall void (Game_PlayerReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21440, COL offset +0x0; 11 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_PlayerReport_Read = 0x004200a0; // layout sizeof(Game::PlayerReport) -- container stride [verified] constexpr uint32_t sizeof_Game_PlayerReport = 0x00000030; // thiscall void (Game_PopulationGroup* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f8d50, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_PopulationGroup_Write = 0x00136af0; // thiscall void (Game_PopulationGroup* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f8d50, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_PopulationGroup_Read = 0x00136a80; // thiscall void (Game_RaidTargets* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21968, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_RaidTargets_Write = 0x00418ec0; // thiscall void (Game_RaidTargets* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21968, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_RaidTargets_Read = 0x0043a090; // thiscall void (Game_SVSOAntiquarians* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fab94, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOAntiquarians_Write = 0x0014bb40; // thiscall void (Game_SVSOAntiquarians* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fab94, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOAntiquarians_Read = 0x00151e90; // thiscall void (Game_SVSOCrowDefenders* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f5904, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOCrowDefenders_Write = 0x000f8c90; // thiscall void (Game_SVSOCrowDefenders* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f5904, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOCrowDefenders_Read = 0x0010fe90; // thiscall void (Game_SVSOLandGrab* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa92c, COL offset +0x0; 2 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOLandGrab_Write = 0x00148f70; // thiscall void (Game_SVSOLandGrab* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa92c, COL offset +0x0; 2 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOLandGrab_Read = 0x0014a9e0; // thiscall void (Game_SVSOLocust* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f56bc, COL offset +0x0; 5 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOLocust_Write = 0x00108ef0; // thiscall void (Game_SVSOLocust* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f56bc, COL offset +0x0; 5 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOLocust_Read = 0x0010d380; // thiscall void (Game_SVSOOrtgay* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f0e94, COL offset +0x0; 9 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOOrtgay_Write = 0x00109510; // thiscall void (Game_SVSOOrtgay* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f0e94, COL offset +0x0; 9 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOOrtgay_Read = 0x00109390; // thiscall void (Game_SVSOProgressionWars* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fb34c, COL offset +0x0; 10 member fields; 8/8 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOProgressionWars_Write = 0x0014cdb0; // thiscall void (Game_SVSOProgressionWars* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fb34c, COL offset +0x0; 10 member fields; 8/8 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOProgressionWars_Read = 0x00153490; // thiscall void (Game_SVSOPuppetMaster* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f3dcc, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOPuppetMaster_Write = 0x00109f80; // thiscall void (Game_SVSOPuppetMaster* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f3dcc, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOPuppetMaster_Read = 0x00111b80; // thiscall void (Game_SVSORefugees* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f575c, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSORefugees_Write = 0x00109640; // thiscall void (Game_SVSORefugees* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f575c, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSORefugees_Read = 0x00126420; // thiscall void (Game_SVSORefugees_PlayerStatus* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f11cc, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSORefugees_PlayerStatus_Write = 0x000fbf50; // thiscall void (Game_SVSORefugees_PlayerStatus* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f11cc, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSORefugees_PlayerStatus_Read = 0x000fbeb0; // thiscall void (Game_SVSOSlaversRefuel* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f4834, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOSlaversRefuel_Write = 0x000fdf80; // thiscall void (Game_SVSOSlaversRefuel* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f4834, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOSlaversRefuel_Read = 0x00120b00; // thiscall void (Game_SVSOSwarm_Infestation* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f1720, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOSwarm_Infestation_Write = 0x000fe0e0; // thiscall void (Game_SVSOSwarm_Infestation* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f1720, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOSwarm_Infestation_Read = 0x00104bd0; // thiscall void (Game_SVSOSwarmQueen_HiveInfo* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f1a68, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOSwarmQueen_HiveInfo_Write = 0x000fe730; // thiscall void (Game_SVSOSwarmQueen_HiveInfo* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f1a68, COL offset +0x0; 3 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOSwarmQueen_HiveInfo_Read = 0x000fe6f0; // layout sizeof(Game::SVSOSwarmQueen::HiveInfo) -- container stride [verified] constexpr uint32_t sizeof_Game_SVSOSwarmQueen_HiveInfo = 0x00000010; // thiscall void (Game_SVSOSystemKiller* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f3e6c, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOSystemKiller_Write = 0x0010af20; // thiscall void (Game_SVSOSystemKiller* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f3e6c, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOSystemKiller_Read = 0x00111f90; // thiscall void (Game_SVSOUpstartApes* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fb4cc, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_Write = 0x0014d1b0; // thiscall void (Game_SVSOUpstartApes* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fb4cc, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_Read = 0x00163850; // thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_SHIPS* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fb290, COL offset +0x0; 6 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_Write = 0x0014cfe0; // thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_SHIPS* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fb290, COL offset +0x0; 6 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_Read = 0x0014cef0; // thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_TRADE_GOODS* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa564, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_TRADE_GOODS_Write = 0x00148ce0; // thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_TRADE_GOODS* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa564, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_TRADE_GOODS_Read = 0x00148c90; // thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_VICTORY* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa534, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_VICTORY_Write = 0x00148ce0; // thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_VICTORY* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa534, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_VICTORY_Read = 0x00148c90; // thiscall void (Game_SVSOUpstartApes_EO_ENERGY_WEAPONS_DECADENT* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa504, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_ENERGY_WEAPONS_DECADENT_Write = 0x00148ce0; // thiscall void (Game_SVSOUpstartApes_EO_ENERGY_WEAPONS_DECADENT* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa504, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_ENERGY_WEAPONS_DECADENT_Read = 0x00148c90; // thiscall void (Game_SVSOUpstartApes_EO_FEARS_YOUR_POWER* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa4d4, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_FEARS_YOUR_POWER_Write = 0x00148ce0; // thiscall void (Game_SVSOUpstartApes_EO_FEARS_YOUR_POWER* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa4d4, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_FEARS_YOUR_POWER_Read = 0x00148c90; // thiscall void (Game_SVSOUpstartApes_EO_GIFTS* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa54c, COL offset +0x0; 2 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_GIFTS_Write = 0x00148c50; // thiscall void (Game_SVSOUpstartApes_EO_GIFTS* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa54c, COL offset +0x0; 2 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_GIFTS_Read = 0x00148c10; // thiscall void (Game_SVSOUpstartApes_EO_MUST_EXPAND* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa51c, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_MUST_EXPAND_Write = 0x00148bb0; // thiscall void (Game_SVSOUpstartApes_EO_MUST_EXPAND* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa51c, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_MUST_EXPAND_Read = 0x00148b50; // thiscall void (Game_SVSOUpstartApes_EO_REQUIRES_CASH* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa4ec, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_REQUIRES_CASH_Write = 0x00148ce0; // thiscall void (Game_SVSOUpstartApes_EO_REQUIRES_CASH* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa4ec, COL offset +0x0; 3 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_REQUIRES_CASH_Read = 0x00148c90; // thiscall void (Game_SVSOVonNeumann* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f59cc, COL offset +0x0; 24 member fields; 14/14 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOVonNeumann_Write = 0x0012b8d0; // thiscall void (Game_SVSOVonNeumann* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f59cc, COL offset +0x0; 24 member fields; 14/14 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOVonNeumann_Read = 0x0010e130; // thiscall void (Game_SVSOVonNeumann_DefeatRecord* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f1c9c, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOVonNeumann_DefeatRecord_Write = 0x000ff7f0; // thiscall void (Game_SVSOVonNeumann_DefeatRecord* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f1c9c, COL offset +0x0; 5 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOVonNeumann_DefeatRecord_Read = 0x001051f0; // layout sizeof(Game::SVSOVonNeumann::DefeatRecord) -- container stride [verified] constexpr uint32_t sizeof_Game_SVSOVonNeumann_DefeatRecord = 0x00000018; // thiscall void (Game_ServerNodeGraph* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1cc4c, COL offset +0x44; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ServerNodeGraph_Write = 0x002e3530; // thiscall void (Game_ServerNodeGraph* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1cc4c, COL offset +0x44; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ServerNodeGraph_Read = 0x002e61e0; // thiscall void (Game_ServerSpyManager* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a30728, COL offset +0x4; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ServerSpyManager_Write = 0x00428a40; // thiscall void (Game_ServerSpyManager* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a30728, COL offset +0x4; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ServerSpyManager_Read = 0x004381b0; // thiscall void (Game_ServerTradeManagerImpl* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a31b64, COL offset +0x4c; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ServerTradeManagerImpl_Write = 0x0042cb60; // thiscall void (Game_ServerTradeManagerImpl* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a31b64, COL offset +0x4c; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ServerTradeManagerImpl_Read = 0x00458a10; // thiscall void (Game_ServerTradeSector* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a31b30, COL offset +0x8; 12 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ServerTradeSector_Write = 0x0042c8c0; // thiscall void (Game_ServerTradeSector* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a31b30, COL offset +0x8; 12 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ServerTradeSector_Read = 0x00458290; // thiscall void (Game_ServerTradeSector_FreighterWarning* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2d858, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ServerTradeSector_FreighterWarning_Write = 0x004197d0; // thiscall void (Game_ServerTradeSector_FreighterWarning* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2d858, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ServerTradeSector_FreighterWarning_Read = 0x00419780; // layout sizeof(Game::ServerTradeSector::FreighterWarning) -- container stride [verified] constexpr uint32_t sizeof_Game_ServerTradeSector_FreighterWarning = 0x0000000c; // thiscall void (Game_ShipBuildOrder* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a0c160, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ShipBuildOrder_Write = 0x00413800; // thiscall void (Game_ShipBuildOrder* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a0c160, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ShipBuildOrder_Read = 0x00413770; // thiscall void (Game_ShipBuildOrderDef* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a0ad08, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ShipBuildOrderDef_Write = 0x00413800; // thiscall void (Game_ShipBuildOrderDef* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a0ad08, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ShipBuildOrderDef_Read = 0x00413770; // layout sizeof(Game::ShipBuildOrderDef) -- container stride [verified] constexpr uint32_t sizeof_Game_ShipBuildOrderDef = 0x00000018; // thiscall void (Game_ShipRecords* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a3144c, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ShipRecords_Write = 0x004176a0; // thiscall void (Game_ShipRecords* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a3144c, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ShipRecords_Read = 0x004560d0; // thiscall void (Game_SpecialProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a21938, COL offset +0x0; 6 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpecialProject_Write = 0x00414760; // thiscall void (Game_SpecialProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a21938, COL offset +0x0; 6 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpecialProject_Read = 0x004146c0; // layout sizeof(Game::SpecialProject) -- enumeration meets embedding [verified] constexpr uint32_t sizeof_Game_SpecialProject = 0x00000034; // thiscall void (Game_SpyCraft* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a30718, COL offset +0x0; 13 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpyCraft_Write = 0x00414940; // thiscall void (Game_SpyCraft* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a30718, COL offset +0x0; 13 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpyCraft_Read = 0x00437dc0; // thiscall void (Game_SpyReportDefences* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a313e8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpyReportDefences_Write = 0x00428ba0; // thiscall void (Game_SpyReportDefences* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a313e8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpyReportDefences_Read = 0x00428b10; // thiscall void (Game_SpyReportTechTree* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a318b4, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpyReportTechTree_Write = 0x00414ba0; // thiscall void (Game_SpyReportTechTree* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a318b4, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpyReportTechTree_Read = 0x00414b40; // thiscall void (Game_SpyReportTrade* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a313f8, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpyReportTrade_Write = 0x00428cf0; // thiscall void (Game_SpyReportTrade* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a313f8, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpyReportTrade_Read = 0x00428c20; // thiscall void (Game_SpyReportTradeRoute* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a3079c, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpyReportTradeRoute_Write = 0x00414aa0; // thiscall void (Game_SpyReportTradeRoute* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a3079c, COL offset +0x0; 4 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SpyReportTradeRoute_Read = 0x00438980; // layout sizeof(Game::SpyReportTradeRoute) -- container stride [verified] constexpr uint32_t sizeof_Game_SpyReportTradeRoute = 0x00000020; // thiscall void (Game_StrategyGameCreateParams* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a25574, COL offset +0x0; 25 member fields; 19/19 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StrategyGameCreateParams_Write = 0x0042ae40; // thiscall void (Game_StrategyGameCreateParams* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a25574, COL offset +0x0; 25 member fields; 19/19 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StrategyGameCreateParams_Read = 0x00432cc0; // thiscall void (Game_StrategyGameInfo* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a07ba0, COL offset +0x0; 13 member fields; 10/10 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StrategyGameInfo_Write = 0x00429960; // thiscall void (Game_StrategyGameInfo* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a07ba0, COL offset +0x0; 13 member fields; 10/10 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StrategyGameInfo_Read = 0x00475cb0; // thiscall void (Game_StrategyPlayerInfo* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a077d8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StrategyPlayerInfo_Write = 0x004298d0; // thiscall void (Game_StrategyPlayerInfo* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a077d8, COL offset +0x0; 2 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StrategyPlayerInfo_Read = 0x00429820; // layout sizeof(Game::StrategyPlayerInfo) -- container stride [verified] constexpr uint32_t sizeof_Game_StrategyPlayerInfo = 0x000000c4; // thiscall void (Game_StrategyServer* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a26084, COL offset +0x0; 38 member fields; 18/18 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StrategyServer_Write = 0x0039fa70; // thiscall void (Game_StrategyServer* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a26084, COL offset +0x0; 38 member fields; 18/18 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StrategyServer_Read = 0x003d27a0; // thiscall void (Game_StrategyTimerParams* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a07334, COL offset +0x0; 4 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StrategyTimerParams_Write = 0x004173e0; // thiscall void (Game_StrategyTimerParams* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a07334, COL offset +0x0; 4 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StrategyTimerParams_Read = 0x00420040; // thiscall void (Game_TacReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009e4600, COL offset +0x0; 17 member fields; 13/13 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_TacReport_Write = 0x00058e60; // thiscall void (Game_TacReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009e4600, COL offset +0x0; 17 member fields; 13/13 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_TacReport_Read = 0x00058c00; // thiscall void (Game_TacReportEvents* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009e45f0, COL offset +0x0; 7 member fields; 7/7 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_TacReportEvents_Write = 0x000565d0; // thiscall void (Game_TacReportEvents* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009e45f0, COL offset +0x0; 7 member fields; 7/7 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_TacReportEvents_Read = 0x00056540; // layout sizeof(Game::TacReportEvents) -- enumeration meets embedding [verified] constexpr uint32_t sizeof_Game_TacReportEvents = 0x00000020; // thiscall void (Game_TechOfferProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a31884, COL offset +0x0; 9 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_TechOfferProject_Write = 0x004535c0; // thiscall void (Game_TechOfferProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a31884, COL offset +0x0; 9 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_TechOfferProject_Read = 0x00453530; // thiscall void (Game_TechProject* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a31834, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_TechProject_Write = 0x00453390; // thiscall void (Game_TechProject* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a31834, COL offset +0x0; 6 member fields; 4/4 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_TechProject_Read = 0x00453280; // thiscall void (Game_TradeRoute* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2d848, COL offset +0x0; 8 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_TradeRoute_Write = 0x00419650; // thiscall void (Game_TradeRoute* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2d848, COL offset +0x0; 8 member fields; 5/5 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_TradeRoute_Read = 0x004208d0; // thiscall void (Game_ZuulInfestation* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f8cb0, COL offset +0x0; 5 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ZuulInfestation_Write = 0x001361e0; // thiscall void (Game_ZuulInfestation* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f8cb0, COL offset +0x0; 5 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ZuulInfestation_Read = 0x00136160; // thiscall void (Game_AICombatReport* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1a6f0, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_AICombatReport_Write = 0x002986e0; // thiscall void (Game_AICombatReport* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1a6f0, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_AICombatReport_Read = 0x0029c6d0; // thiscall void (Game_FieldTemplate_Point* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a05df8, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_FieldTemplate_Point_Write = 0x0019bfa0; // thiscall void (Game_FieldTemplate_Point* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a05df8, COL offset +0x0; 5 member fields; 3/3 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_FieldTemplate_Point_Read = 0x0019bf10; // layout sizeof(Game::FieldTemplate::Point) -- container stride [verified] constexpr uint32_t sizeof_Game_FieldTemplate_Point = 0x00000014; // thiscall void (Game_LocustFleetLogistics* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009f1f00, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_LocustFleetLogistics_Write = 0x00108e50; // thiscall void (Game_LocustFleetLogistics* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009f1f00, COL offset +0x0; 4 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_LocustFleetLogistics_Read = 0x00108d60; // thiscall void (Game_Objective* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2e0cc, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_Objective_Write = 0x004505b0; // thiscall void (Game_Objective* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2e0cc, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_Objective_Read = 0x00450430; // layout sizeof(Game::Objective) -- container stride [verified] constexpr uint32_t sizeof_Game_Objective = 0x00000044; // thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_ShipRequest* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fa614, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_ShipRequest_Write = 0x0015d120; // thiscall void (Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_ShipRequest* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fa614, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_ShipRequest_Read = 0x0015d040; // layout sizeof(Game::SVSOUpstartApes_EO_DEMANDS_SHIPS::ShipRequest) -- container stride [verified] constexpr uint32_t sizeof_Game_SVSOUpstartApes_EO_DEMANDS_SHIPS_ShipRequest = 0x00000020; // thiscall void (Game_ShipDesignDef_LegacyWeaponDef* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2df7c, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ShipDesignDef_LegacyWeaponDef_Write = 0x00413950; // thiscall void (Game_ShipDesignDef_LegacyWeaponDef* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2df7c, COL offset +0x0; 1 member fields; 1/1 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ShipDesignDef_LegacyWeaponDef_Read = 0x00446260; // thiscall void (Game_ShipWeapon* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x009fdc8c, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ShipWeapon_Write = 0x0016f450; // thiscall void (Game_ShipWeapon* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x009fdc8c, COL offset +0x0; 3 member fields; 2/2 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_ShipWeapon_Read = 0x00171130; // thiscall void (Game_SlotDef* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a2db6c, COL offset +0x0; 18 member fields; 15/15 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SlotDef_Write = 0x004276d0; // thiscall void (Game_SlotDef* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a2db6c, COL offset +0x0; 18 member fields; 15/15 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_SlotDef_Read = 0x00432790; // layout sizeof(Game::SlotDef) -- enumeration meets embedding [verified] constexpr uint32_t sizeof_Game_SlotDef = 0x000000bc; // thiscall void (Game_StarSystem_OutputRates* this, Mars::Stream* s) /* IStreamable slot 2, vftable 0x00a1f884, COL offset +0x0; 7 member fields; 7/7 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StarSystem_OutputRates_Write = 0x00345190; // thiscall void (Game_StarSystem_OutputRates* this, Mars::Stream* s) /* IStreamable slot 1, vftable 0x00a1f884, COL offset +0x0; 7 member fields; 7/7 field offsets agree between Read and Write */ [verified] constexpr uint32_t Game_StarSystem_OutputRates_Read = 0x003472a0; // layout sizeof(Game::StarSystem::OutputRates) -- enumeration meets embedding [verified] constexpr uint32_t sizeof_Game_StarSystem_OutputRates = 0x0000001c; // thiscall void (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) // the body of every `Player..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; // thiscall bool (Game::StrategyAIAgent::Streamable* this, Mars::Stream* s) [verified] constexpr uint32_t Game_StrategyAIAgent_Streamable_Read = 0x002c8fd0; // thiscall void (Game::StrategyAIAgent::DesignNameGen* this, Mars::Stream* s) // `dnnc` = (this+8 - this+4)/0x24, then dnnc x { dnid: StreamableHelper at elem+0, dnnm: WriteString at elem+8 }. Element stride 0x24 = ShipSectionID(8) + Mars::String(0x1c) [verified] constexpr uint32_t Game_StrategyAIAgent_DesignNameGen_Write = 0x002c77a0; // thiscall bool (Game::StrategyAIAgent::DesignNameGen* this, Mars::Stream* s) [verified] constexpr uint32_t Game_StrategyAIAgent_DesignNameGen_Read = 0x002ccc70; // thiscall void (Game::AISituation* this, Mars::Stream* s) // AISitSecs = AIWeightMap at this+0, AISitWeapFams = AIWeightMap> at this+0x10. sizeof >= 0x20 [verified] constexpr uint32_t Game_AISituation_Write = 0x0029a220; // thiscall bool (Game::AISituation* this, Mars::Stream* s) [verified] constexpr uint32_t Game_AISituation_Read = 0x0029a160; // thiscall void (Game::AISystem* this, Mars::Stream* s) // writes ONE `AISysSit` frame and nothing else, and the AISituation it writes is constructed empty on the stack -- `this` is never read. Every AISys body on disk is therefore two empty weight maps regardless of game state [verified] constexpr uint32_t Game_AISystem_Write = 0x002bda30; // thiscall bool (Game::AISystem* this, Mars::Stream* s) [verified] constexpr uint32_t Game_AISystem_Read = 0x002bd960; // thiscall void (Game::AttribMap* this, Mars::Stream* s) // count = *(this+8) (the tree's _Mysize; _Myhead is at this+4), then a red-black walk writing two NULL-named strings per node -- key at node+0x0c, value at node+0x28, _Isnil at node+0x45. So AttribMap is a std::map [verified] constexpr uint32_t Game_AttribMap_Write = 0x0019ee60; // thiscall bool (Game::AttribMap* this, Mars::Stream* s) [verified] constexpr uint32_t Game_AttribMap_Read = 0x001a8a60; // thiscall void (Game::AIWeightMap* this, Mars::Stream* s) // NULL-named count = span/0xc, then n x { NULL-named StreamableHelper frame at elem+0, NULL-named float at elem+8 } [verified] constexpr uint32_t Game_AIWeightMap_ShipSectionID_Write = 0x00296b40; // thiscall bool (Game::AIWeightMap* this, Mars::Stream* s) [verified] constexpr uint32_t Game_AIWeightMap_ShipSectionID_Read = 0x002bff00; // thiscall void (Game::AIWeightMap>* this, Mars::Stream* s) // NULL-named count = span>>3, then n x { NULL-named StreamableHelper> frame at elem+0, NULL-named float at elem+4 } [verified] constexpr uint32_t Game_AIWeightMap_StreamableEnumUInt_Write = 0x00296c50; // thiscall bool (Game::AIWeightMap>* this, Mars::Stream* s) [verified] constexpr uint32_t Game_AIWeightMap_StreamableEnumUInt_Read = 0x002b57c0; // thiscall void (Game::AIWeightMap>* this, Mars::Stream* s) // same wire shape as the uint instantiation; `AITurnPris` [verified] constexpr uint32_t Game_AIWeightMap_AIPurposeID_Write = 0x0029b590; // thiscall bool (Game::AIWeightMap>* this, Mars::Stream* s) [verified] constexpr uint32_t Game_AIWeightMap_AIPurposeID_Read = 0x002c01a0; // thiscall void (Game::AIWeightMap>* this, Mars::Stream* s) // same wire shape as the uint instantiation; `AISitWeapFams` [verified] constexpr uint32_t Game_AIWeightMap_WeaponFamilyID_Write = 0x0029b4c0; // thiscall bool (Game::AIWeightMap>* this, Mars::Stream* s) [verified] constexpr uint32_t Game_AIWeightMap_WeaponFamilyID_Read = 0x002bffe0; // thiscall void (Game::ShipSectionID* this, Mars::Stream* s) // two NULL-named ints, at this+0 and this+4 [verified] constexpr uint32_t Game_ShipSectionID_Write = 0x0016ed50; // thiscall bool (Game::ShipSectionID* this, Mars::Stream* s) [verified] constexpr uint32_t Game_ShipSectionID_Read = 0x0016ed10; // data void* [3] // { 0x004f7230 scalar-deleting dtor, 0x00697ae0 Read, 0x0054b6e0 Write } [verified] constexpr uint32_t Mars_StreamableHelper_StreamableEnumUInt_vftable = 0x005e24b8; // thiscall void (Mars::StreamableHelper>* this, Mars::Stream* s) // writes exactly ONE NULL-named int. So a StreamableEnum member is a FRAME on the wire whose body is a single int -- not a bare int. This is what makes a VectorHelper> a framed array of one-int frames [verified] constexpr uint32_t Mars_StreamableEnum_Write = 0x0014b6e0; // thiscall void (Mars::StreamableHelper>* this, Mars::Stream* s) // ReadInt through stream slot +0x10; stores only when the read succeeds [verified] constexpr uint32_t Mars_StreamableEnum_Read = 0x00297ae0; // data void* [3] // { 0x004f7230 dtor, 0x00431610 Read, 0x0042f7f0 Write } [verified] constexpr uint32_t Mars_VectorHelper_StreamableEnumUInt_vftable = 0x005e2438; // thiscall void (Mars::VectorHelper>* this, Mars::Stream* s) // NULL-named count = span>>2, then n NULL-named FRAMES each written by Mars_StreamableEnum_Write. Backs `nalat`, `SysMem` and `mts`, all of which have count 0 in every save we hold [verified] constexpr uint32_t Mars_VectorHelper_StreamableEnumUInt_Write = 0x0002f7f0; // data void* [3] // { 0x004f7230 dtor, 0x006b5600 Read, 0x0069b310 Write }; backs `prs2` [verified] constexpr uint32_t Mars_VectorHelper_AIPlayerRequestStamp_vftable = 0x0061a700; // thiscall void (Mars::VectorHelper* this, Mars::Stream* s) // NULL-named count = span>>3, then n NULL-named frames written by Game_AIPlayerRequestStamp_Write. Element stride 8 [verified] constexpr uint32_t Mars_VectorHelper_AIPlayerRequestStamp_Write = 0x0029b310; // data void* [3] // { 0x004f7230 dtor, 0x006953b0 Read, 0x00695400 Write } [verified] constexpr uint32_t Mars_StreamableHelper_AIPlayerRequestStamp_vftable = 0x0061a730; // thiscall void (Mars::StreamableHelper* this, Mars::Stream* s) // two named ints: `pid` at +0 and `trn` at +4. Game::AIPlayerRequestStamp is a POD with no RTTI class of its own, reached only through this specialised helper, so tools/serializers.py reports 'no serializer' for it and it has no entry in the generated wire table [verified] constexpr uint32_t Game_AIPlayerRequestStamp_Write = 0x00295400; // site site in StrategyServer::Write (0x0079fa70, already in addresses.json with the same address -- AGREEMENT, this lane's duplicate entry was dropped). THE DECISIVE EVIDENCE FOR THE ModCount / Frame NAMING: 0x0079fb2f `lea edx,[edi+0x08]; push "ModCount"` and 0x0079fb40 `lea eax,[edi+0x0c]; push "Frame"`, with edi = this. The frame is confirmed inside the same function by 0x0079fb90 `mov eax,[edi+0x16c]` under the tag "RNG", which is StrategyServer_off_RNGPtr (0x168 in the S+4 frame) -- so edi is S, not S+4. Tag order after the six id lists is ModCount, Frame, GameID ([edi+0x14] at 0x0079fb4f) [verified] constexpr uint32_t StrategyServer_Write_ModCountFrameTags = 0x0039fb2f; // field int ModCount, in the S+4 (Game::StrategySim sub-object) frame == S+0x8. THIS IS THE WIRE'S /Sim/ModCount. CORRECTION, and it matters: addresses.json's `StrategyServer_off_ModCount` puts that name on offset 0x8 of this frame (== S+0xc), which StrategyServer::Write tags "Frame". Lane T's `StrategyServer_off_PhaseCounter` -- 'nobody has named this one' -- is this word, and it IS ModCount. Written by an unconditional increment on ENTRY to every StrategySim command-application method (26 sites), plus once by StrategyServer::ProcessTurn's first instruction, once by OnAllCombatDone_Tail's first instruction, and once per call of the abandon/chaos check. It is a modification counter: not a turn number, not a phase counter, and not constant per turn (12-44 on measured turns) [verified] constexpr uint32_t StrategySim_off_ModCount = 0x00000004; // field int Frame, in the S+4 frame == S+0xc. THIS IS THE WIRE'S /Sim/Frame -- the turn number. Incremented once per turn by StrategyServer::BeginProcessTurn 0x007d990a; stamped into node.turnResearched by TechTree::SetResearched; used as minTurn by the EVENT_NO_RESEARCH sweep; and it is the KEY the turn-record archive is written under (FinalizeTurnRecords passes S->+0xc). The entry addresses.json calls `StrategyServer_off_ModCount` is this word, and the name is wrong on it [verified] constexpr uint32_t StrategySim_off_Frame = 0x00000008; // site site in StrategyServer::ProcessTurn, phase 4, 0x007dc871-0x007dc8c7, byte-for-byte: `for (i = 0; i < (Players.end - Players.begin) >> 2; ++i) { p = Players[i]; rec = p->+0x3d8; rec->+0x8 = 0; rec->+0x8 |= 1 << i; if (p->+0x168 != -1) rec->+0x8 |= p->+0x16c; }`. THREE separate stores to the same word (`mov [edx+8],ebx` with ebx=0, then two `or`), which is why it is an OR and not an assignment; the turn-record pointer is RELOADED from p->+0x3d8 before each. The shift is `mov edi,1; shl edi,cl` with cl = the loop counter, so the bit is the player's POSITION IN THE VECTOR and x86's 5-bit shift mask applies above 31 players. Loop guard is `test edx,0xfffffffc; jle` on the byte count [verified] constexpr uint32_t StrategyServer_ProcessTurn_AllianceMaskPass = 0x003dc871; // thiscall void (StrategySim* this /* S+4 */, Game::TurnCommands* first, int count) // applies a run of per-player command blocks: `last = first + count*0x1b4; for (b = first; b != last; b += 0x1b4) `. The 0x1b4 stride is sizeof(Game::TurnCommands) and independently confirms lane Q's layout (27 std::list members at 0x70..0x1a8, stride 0xc, so the class ends at 0x1b4). SIX command handlers are INLINED here and each bumps ModCount once (0x0088fe0a, 0x008902fe, 0x008903b9, 0x0089046c, 0x008905c8, 0x008907bc), each followed by `lea ecx,[base+0x80]; call HandleMap::Resolve` -- which is what proves the base is the StrategySim and not the block [verified] constexpr uint32_t StrategySim_ApplyTurnCommandBatch = 0x0048f9b0; // thiscall void (StrategyServer* this /* S frame */) // the End-Turn command flush: computes `count = (this->+0x178 - this->+0x174) / 0x1b4` (signed magic 0x964fda6d, sar 8 -- the reciprocal of 436) and calls StrategySim::ApplyTurnCommandBatch(this+4, this->+0x174, count). The `lea ecx,[esi+4]` at 0x0078f6de is a third independent sighting of the two-bases split. Called from StrategyNetworkClient::OnMessage 0x00784904, immediately before that handler calls StrategyServer::ProcessTurn at 0x0078491c [verified] constexpr uint32_t StrategyServer_ApplyAllTurnCommands = 0x0038f6a0; // field std::vector in the S frame (begin @+0x174, end @+0x178), element stride 0x1b4. One block per player; the End-Turn flush applies every command in every block, and each application bumps ModCount. This is the wire's `Player..TurnCommands_v5` custom-data block in memory [verified] constexpr uint32_t StrategyServer_off_TurnCommandQueue = 0x00000174; // field sizeof(Game::TurnCommands) = 436, recovered from the container stride in StrategyServer::ApplyAllTurnCommands (`imul ebx,ebx,0x1b4`) and from the signed-division reciprocal 0x964fda6d/sar 8 in the same function. Independent of, and agreeing with, lane Q's field walk, whose last list member sits at 0x1a8 with stride 0xc [verified] constexpr uint32_t TurnCommands_sizeof = 0x000001b4; // thiscall void (StrategyNetworkClient* this, Message* m) // vftable 0x00a229f4 slot 6. THE END-TURN DISPATCHER: it is the only direct caller of StrategyServer::ProcessTurn (0x0078491c), of OnAllCombatDone_Tail (0x00784d07) and of StrategyServer::ApplyAllTurnCommands (0x00784904), and it loads the server with `mov ecx,[esi+0x54]` before the first two. NOTE: this class's own `this` is NOT a StrategySim -- its entity hash is at +0x84, not +0x80 -- so the three `inc [reg+4]` sites in this body (0x007850d5, 0x0078514b, 0x00785224) are NOT ModCount and are listed as unresolved rather than counted [verified] constexpr uint32_t StrategyNetworkClient_OnMessage = 0x00384640; // field StrategyServer* in the S frame (the base ProcessTurn receives, not S+4). Read at 0x00784901 and 0x00784919 immediately before the command flush and the turn driver [verified] constexpr uint32_t StrategyNetworkClient_off_Server = 0x00000054; // thiscall void (StrategyServer* this /* S frame */, ServerSystem* sys, int mode, std::vector* out) // the abandon/chaos check called once per system from ProcessTurn phase 1. ITS FOURTH INSTRUCTION IS A ModCount BUMP: `inc [esi+0x8]` @0x007b9e20 with esi = this = S, so this is the only per-system ModCount writer in the turn. The phase-1 loop gate is `cmp BYTE [sys+0xc4],0; je` -- ServerSystem_off_Abdn -- so the call, and the bump, happen only for systems already flagged abandoned. Abdn is FALSE on all 28 systems of all 11 corpus saves, so this writer contributes 0 on every measured turn and is a rule-6 hypothesis for any turn where it does not [verified] constexpr uint32_t StrategyServer_AbandonChaosCheck = 0x003b9df0; // thiscall void (StrategySim* this /* S+4 */, ...) // the fleet-move command handler; logs "StrategySim: Fleet not found.", "StrategySim: Waypoint %d(id) not found." and "StrategySim: (see above) cannot move fleet %d(id)." Bumps ModCount at 0x008657aa. THE ONLY ModCount WRITER OTHER THAN THE ABANDON CHECK THAT IS DIRECT-CALL REACHABLE FROM EITHER TURN DRIVER (from OnAllCombatDone_Tail's 1369-function closure; not from ProcessTurn's 1382) [verified] constexpr uint32_t StrategySim_MoveFleetCommand = 0x00465780; // thiscall void __thiscall Game::StrategyApp::RunAI(int playerNetId, const char* aiCustomDataName, unsigned char aiPersonality, unsigned int rngSeed) -- RET 0x10, four stack args. The ONE-SHOT AI construction path, reached only from StrategyNetworkClient::OnMessage 0x00784640+0x96e, case SNMRunAI (net msg id 0x3d). Resolves the player through the handle registry at server+0x84; refuses on a human (`p->IsAI(+0xf9) == 0` -> "RunAI: Cannot create a StrategyClient/AI for a human player."); logs "RunAI: Creating AI client for %s using %08x for random seed."; operator_new(0x708) + StrategyClient ctor 0x00782ed0 WITH rngSeed; StrategyServer::InitGame; StrategyClient::CreateAI 0x007653c0(aiPersonality) which builds the Game::StrategyAIAgent into StrategyClient+0x12c ("RunAI: Failed to create AI for %s (player %d)."); if a save-game blob was supplied, "Loading AI custom data from save game..." through agent vt[5]/vt[6]; finally RaiseAIPrepareTurn 0x00815f20. NOT a per-turn entry point [verified] constexpr uint32_t StrategyApp_RunAI = 0x004706f0; // cdecl void (Game::StrategyServer* srv) with EBX = Game::StrategyAIAgent* (register-passed; both call sites set EBX before the call). Builds a stack Game::SEAIPrepareTurn (vftable 0x00a23c00) and invokes agent->vt[1](9, &ev) -- i.e. StrategyAIAgent::OnEvent with client event type 9. Also fires the SVScriptObject hooks at srv+0x1b4 with ids 9 and 0xa. Exactly two call sites: RunAI 0x008706f0+0x26c and StrategyApp::CreateGame 0x00888e80+0x3f5 -- both are game/AI construction, so SEAIPrepareTurn is NOT raised once per turn despite the name [verified] constexpr uint32_t StrategyApp_RaiseAIPrepareTurn = 0x00415f20; // thiscall void __thiscall Game::StrategyAIAgent::OnEvent(int clientEventType, Game::StrategyEvent** ev) -- RET 8. IStrategyAIAgent vtable slot 1 (vftable 0x00a1b244). Pushes a log scope on this->+0x10, then calls StrategyAIContext::OnStrategyEvent 0x006c2b90 with ECX = this->+0x94 (the context) and args (type, ev, &thunk 0x006d0ab0, this). The thunk forwards to StrategyAIAgent::OnAIPacket 0x006cf8a0. Every StrategyClient event handler in 0x00773xxx-0x00777xxx forwards through this slot when StrategyClient+0x12c is non-null [verified] constexpr uint32_t StrategyAIAgent_OnEvent = 0x002d0ad0; // thiscall void __thiscall Game::StrategyAIContext::OnStrategyEvent(int clientEventType, Game::StrategyEvent** ev, void (*cb)(void*, void*), Game::StrategyAIAgent* agent) -- RET 0x10. Registers {cb, agent, seq} on the pending-callback deque at this+0x58 (ring deque, buf@+0x5c cap@+0x60 head@+0x64 size@+0x68, 0x0c-byte nodes, push helper 0x0069e470 under a critical section). Then `switch (type - 6)` over 0..0x20 through the byte index table at 0x006c33a4 and jump table at 0x006c3360 (17 distinct cases), updating the AI world model and emitting INTERNAL AI packets {int code; ...} through StrategyAIContext::Broadcast 0x006b3840. Client event 9 (SEAIPrepareTurn) emits codes 1 then 2; client event 0x26 (SEResumePlaying) emits code 3. Tail: if the pending deque size is 1 it drains a second, separate queue at this+0x38 via 0x006a8690 [verified] constexpr uint32_t StrategyAIContext_OnStrategyEvent = 0x002c2b90; // thiscall void __thiscall Game::StrategyAIContext::Broadcast(const AIPacket* pkt) -- walks the listener red-black tree at this+0xc (std::set/map nodes; `_Isnil` at node+0x15) and calls listener->vt[3](pkt) on each -- that is the Game::AIObject event slot, implemented by AIPlayer (0x00723ed0), AISystem (0x006b3ae0), AIFleet (0x006b3970), AIBuildOrder and StrategyAIAgent (0x0069de50). Then, if the pending-callback deque at this+0x68 is non-empty, iterates it (0x0069e510 / 0x006a4ee0) and delivers the same packet to the queued {cb, this} pairs -- the hop that reaches StrategyAIAgent::OnAIPacket 0x006cf8a0. 19 call sites, all inside OnStrategyEvent 0x006c2b90 and 0x006c29c0 [verified] constexpr uint32_t StrategyAIContext_Broadcast = 0x002b3840; // thiscall void __thiscall Game::StrategyAIAgent::OnAIPacket(const AIPacket* pkt) -- 2008 bytes. `eax = pkt->code - 2; if (eax > 0xf) return; jmp [eax*4 + 0x006d0078]` -- a 16-entry jump table over internal packet codes 2..17. Code 2 = the PREPARE TURN body (0x006cf958, logs "====== AI Prepare Turn (%s) ======"); code 3 = the PROCESS TURN body (0x006cfabf, logs "====== AI Process Turn (%s) ======", ~30 phases, ends by calling cl_EndTurn 0x00579310). Codes 4/6/7/8/10/11/13 fall through to the no-op at 0x006d0058. this->+0x10 = the owning StrategyClient, this->+0x14 = the ClientPlayer (name std::string at +0x40), this->+0x94 = the StrategyAIContext. Reached only through the thunk at 0x006d0ab0 [verified] constexpr uint32_t StrategyAIAgent_OnAIPacket = 0x002cf8a0; // thiscall void __thiscall Game::StrategyClient::OnResumePlaying(Game::StrategyEvent** ev) -- the case-0x26 handler of StrategyClient::RaiseEvent 0x00783ee0 (jump table 0x00784200, 0x2c entries). At +0x9d: `if (this->+0x12c) agent->vt[1](0x26, ev)` -- THE per-turn AI trigger. SEResumePlaying is broadcast by StrategyServer::ResumePlaying 0x007ddc90 at the very start of a new turn, after OnAllCombatDone_Tail has written the pre-turn autosave [verified] constexpr uint32_t StrategyClient_OnResumePlaying = 0x00377480; // offset Game::StrategyAIAgent* -- non-null only on an AI client. Every StrategyClient event handler tests it before forwarding the event to the agent; SendEndTurn 0x00783980 reads agent->vt[8]() through it to fetch the AIEncounterFlags it appends to SNMEndTurn; cl_EndTurn 0x00579310 refuses to end the turn unless it is non-null [verified] constexpr uint32_t StrategyClient_off_AIAgent = 0x0000012c; // offset Mars::RNG* -- a PER-CLIENT generator, operator_new(0x9cc) + RNG_Seed(ctorArg) in the StrategyClient constructor 0x00782ed0+0x143..+0x181. For an AI client the seed is RunAI's 4th argument. THE ONLY GENERATOR THE STRATEGIC AI DRAWS FROM: all six direct NextInt sites in the AI module reach it, and so do the two façade helpers cl_Chance 0x00578cf0 and cl_RandRange 0x005798e0. It is distinct from the strategic generator at StrategyServer+0x16c and is NOT serialised anywhere in the save [verified] constexpr uint32_t StrategyClient_off_RNG = 0x00000134; // offset Game::TurnCommands -- the ACCUMULATING order queue. Every StrategyClient order method in 0x00762ca0..0x00763f60 does `lea ecx,[this+0x160]` and appends to one of its 27 lists (or sets one of its six gates). At End Turn, StrategyClient::BuildTurnCommands 0x00783780 does TurnCommands::operator=(this->+0x4d8, this->+0x160) and then overwrites the player id, research rate and fleet-move list from live state [verified] constexpr uint32_t StrategyClient_off_PendingTurnCommands = 0x00000160; // offset Game::TurnCommands -- the SEND buffer, the object that becomes the `Player..TurnCommands_v5` block on the wire. Written only by StrategyClient::BuildTurnCommands 0x00783780 (from +0x160) and read by SendEndTurn 0x00783980+0x96, which copies it into the SNMEndTurn message (vftable 0x00a229e0) with TurnCommands::operator= 0x007832b0 [verified] constexpr uint32_t StrategyClient_off_SendTurnCommands = 0x000004d8; // thiscall void __thiscall Game::StrategyClient::BuildTurnCommands(Game::TurnCommands* dst) -- called once from EndTurn 0x00783be0+0xee with dst = &this->+0x4d8. TurnCommands::Clear 0x00893f00(dst); TurnCommands::operator= 0x007832b0(dst, &this->+0x160); dst->playerId(+4) = this->+0x150->+4; TurnCommands::SetResearchRate 0x0080f2d0(dst, this->+0x150->+0xbc) -- which is why EVERY save's TurnCommands block has the research-rate gate set and the other five clear; then, if this->+0x6d0, walks the pending fleet-move vector at this->+0x6d4/+0x6d8 (stride 8) into the move list [verified] constexpr uint32_t StrategyClient_BuildTurnCommands = 0x00383780; // thiscall Game::TurnCommands& __thiscall Game::TurnCommands::operator=(const TurnCommands& src) -- member-by-member copy of the six gates and their payloads, then the 27 lists. 11 call sites, including SendEndTurn, StrategyServer::OnPlayerEndTurn 0x007d9af0+0x68 (the host storing an arriving block), BuildTurnEvents and LoadGame [verified] constexpr uint32_t TurnCommands_Assign = 0x003832b0; // cdecl void () -- the AI's turn-submission façade. `c = g_StrategyClients[g_CurrentClientIndex]; if (c && c->AIAgent(+0x12c) && !c->bTurnEnded(+0x15c)) StrategyClient::EndTurn(c, true);` -- note the +0x12c test: this entry point works ONLY for an AI client. Called from the AI Process Turn body at 0x006cfcd9. The other caller of StrategyClient::EndTurn is the human UI at 0x005e4f80+0x5f [verified] constexpr uint32_t cl_EndTurn = 0x00179310; // cdecl bool (float p) -- `c = g_StrategyClients[g_CurrentClientIndex]; return c ? RNG_Chance(c->RNG(+0x134), p) : false;`. One of the AI's two randomness façades; used by the AI Process Turn body at 0x006cfc24 for the surrender roll that follows the "Survival Outlook: Dead in %i turns (%5.2f%% chance to surrender this turn)" log line [verified] constexpr uint32_t cl_Chance = 0x00178cf0; // cdecl int (int lo, int hi) -- `c = g_StrategyClients[g_CurrentClientIndex]; if (!c) return 0; n = hi - lo; return lo + RNG_NextInt(&c->RNG(+0x134)->mt, &n);`. NOTE RNG_NextInt is INCLUSIVE of its bound (addresses.json), so the range is [lo, hi] inclusive. Eight AI-module call sites [verified] constexpr uint32_t cl_RandRange = 0x001798e0; // offset Game::StrategyClient* g_StrategyClients[] -- the client table the whole 0x00578cf0..0x005793xx façade family indexes with g_CurrentClientIndex (0x00ae4808). 40 functions reference it. The AI runs as the current client: everything it does goes through this indirection, which is how one process hosts the human client and N AI clients over the same API [mapped] constexpr uint32_t g_StrategyClients = 0x006e47e4; // offset int -- index into g_StrategyClients (0x00ae47e4). Selects which client the cl_* façade acts on. CORRECTED by lane AI2 2026-09-08: it is a STACK POINTER, not a plain index. The only two instructions in the image that write it are PushCurrentClient 0x00578020 (`g_StrategyClients[idx+1] = c; ++idx;`) and PopCurrentClient 0x00578040 (`--idx;`); the other 40 referencing functions only read `[idx*4 + 0x00ae47e4]`. StrategyAIAgent::OnEvent 0x006d0ad0 brackets the whole AI turn in Push(agent->+0x10)/Pop [verified] constexpr uint32_t g_CurrentClientIndex = 0x006e4808; // offset Mars::RNG -- a STATIC generator in .data, 0x9cc bytes. Its only static initialiser (0x009dc6e0) writes the Mars::IStreamable vftable 0x009e22bc, NOT the Mars::RNG vftable 0x009e9aec that RNG_Seed installs: none of the six RNG_Seed call sites in the image targets it, so its mt[624] is the zero-initialised BSS array and `left` is 0. An all-zero MT19937 state is a fixed point of the twist, so EVERY draw from it returns 0. Five consumers: SNMRunAI (the AI client seed, OnMessage+0x955), RunCombatRound 0x007cbe80+0x60f, 0x007c2fa0+0xc84, 0x0079ea90+0x73 (an RNG_Chance) and 0x005b9f00+0xc0 [verified] constexpr uint32_t g_GlobalRNG = 0x006f6e58; // thiscall void __thiscall -- loads Data/Strategy/AI/aitechmode.csv, aitechpri.csv and aitechgrp.csv through the Mars::ICSVRowParser subclasses Game::AIUserTechModeRowParser (vftable 0x00a1ae8c), AIUserTechPriRowParser (0x00a1ae7c) and AIUserTechGrpRowParser (0x00a1ae6c). Diagnostics: "%s, %i: tech %s does not exist.", "%s, %i: priority %i (%s) is out of range 0..255.", "%s, %i: bad research mode character: %s", "%s, %i: bad group: %s". Tech group ids are the AITG_* strings emitted by 0x006920a0 (ARMOR BALWEAP BEAM BIOWEAP NRGWEAP SHIELDS TORPS WARHEAD) [mapped] constexpr uint32_t AIRulesDB_LoadTechTables = 0x002c7980; // thiscall void __thiscall -- loads data/strategy/ai/stock_design_names.csv, stock_diplomacy_messages.csv and stock_player_names.csv through Game::AIPersonaDB::StockDesignNameRowParser (vftable 0x00a1b034), StockDiplomacyMessageRowParser (0x00a1b024) and StockPlayerNameRowParser (0x00a1b014). Diagnostics key on species and on a diplomatic event id; the id vocabulary is the 53 AIDIP_* strings returned by 0x00690960 [mapped] constexpr uint32_t AIPersonaDB_LoadStockTables = 0x002c6250; // thiscall void __thiscall -- loads data/strategy/ai/affinity_weapon.csv and affinity_section.csv ("ToAISectionRule: %s ship section not found: %s"). These are the ship-design affinity weights consumed by the design composer 0x006ad700 ("AIComposeShipBlueprint: SectionBlueprint::MAX_OPTIONS", "While AI for %s was designing a ship: Did not find any weapon to match %s, %s, bNoTrackingWeapons=%i."). Section-class rule selection is 0x0069cc30 / 0x0069cdb0 ("AISelectSectionClassRules: maxout", "AISelectSectionClassRulesMergeFallback: maxout"); the generic selector is 0x00695140 ("AIRulesDB::SelectRules_T: maxout") [mapped] constexpr uint32_t AIRulesDB_LoadAffinityTables = 0x002c63c0; // thiscall void __thiscall -- loads Data/Strategy/AI/weapon_replacements.csv through Game::StrategyAIContext::WeaponReplacementsRowParser (vftable 0x00a1a62c). Consumed by 0x00694f80 ("StrategyAIContext::GetWeaponReplacement: maxReplacements (%i)") [mapped] constexpr uint32_t StrategyAIContext_LoadWeaponReplacements = 0x002b4dc0; // thiscall void __thiscall Game::StrategyAIAgent::RebuildAndRunTasks() -- THE TASK SELECTION LOOP. Phase 20 of the AI Process Turn body (called from 0x006cfc94). Order: (1) 0x006b34f0(this, &this->+0x2f8) refreshes the per-fleet world model over client->+0x60..+0x64; (2) PruneTasks 0x006b3640(this); (3) `switch (client->+0x150->+0x5c)` over 0..6 through the 7-entry jump table at 0x006cf880 -- four distinct arms (case 0/2/3/6 -> 0x006cf665, case 1 -> 0x006cf6f8, case 5 -> 0x006cf75d, case 4 -> NOTHING) each calling a fixed, source-ordered list of per-task-family creators in 0x006ab6c0..0x006c0e60; (4) if this->+0x8, that object's vt[1](this); (5) PruneTasks again; (6) TaskList_SortByPriority 0x006bf9c0(&this->+0x31c, player->+0x5c) -- std::list::sort, STABLE, DESCENDING by IAITask::vt[10](); (7) RunTaskList 0x006b3320(this, &this->+0x31c, 0, &this->+0x2e8) then again with pass=1; (8) PruneTasks again; (9) if player->+0x2d8 in {1,2}, 0x006cf4c0 then 0x006cf590; (10) 0x006a8eb0(this) -- reaches client order method 0x00763a20; (11) if this->+0x124, cl_SetResearchRate(*(float*)0x009e2ea0) and clear the flag; (12) this->+0x128 = 0 [verified] constexpr uint32_t StrategyAIAgent_RebuildAndRunTasks = 0x002cf630; // thiscall void __thiscall std::list::sort(Pred) on the agent's task list -- the MSVC 7.1 binlist sort: eh_vector_constructor_iterator over 26 (0x1a) 0x0c-byte std::list bins, the `_Bin == 25` overflow branch, merge helper 0x006a9850. The Pred is a 4-byte functor carrying player->+0x5c, and the inlined comparison IGNORES it: the whole ordering key is IAITask::vt[10]() (see AITask_slot10_GetPriority). std::list::sort is STABLE, so ties keep creation order -- which makes the per-arm creator call order in RebuildAndRunTasks part of the answer, not an implementation detail [verified] constexpr uint32_t StrategyAIAgent_TaskListSortByPriority = 0x002bf9c0; // thiscall void __thiscall std::list::merge(list& right, Pred) -- RET 8. THE COMPARISON, inlined at 0x006a9879..0x006a9895: `a = A->vt[10](); b = B->vt[10](); if (a > b) splice A before B;` (`cmp [ebp-0x10],eax / jle` -- so a strictly-greater test, descending order, ties left alone). Both calls are __thiscall with no stack args, which pins IAITask::vt[10] as `int GetPriority(void)` [verified] constexpr uint32_t StrategyAIAgent_TaskListMerge = 0x002a9850; // cdecl void (Game::StrategyAIAgent* agent, std::list* tasks, int pass, std::vector* pending) -- THE TASK EXECUTION LOOP, run twice per turn with pass = 0 then 1. For each node of `tasks` in list order (i.e. priority order after the sort): task = node->value; erase task from `pending` (std::find 0x0069af70 + memmove compaction); push_back task onto the agent's active-task stack at agent->+0x12c/+0x130/+0x134 (growth helper 0x00483410, "vector too long"); call `task->vt[5](agent, pass)`; then if back() is still that task, pop_back. The push/pop bracket makes agent->+0x12c a task CALL STACK, which is how goal tasks (AITColonizeGoal, AITInvadeGoal, AITEscortGateInvadeGoal) nest sub-tasks [verified] constexpr uint32_t StrategyAIAgent_RunTaskList = 0x002b3320; // cdecl void (Game::StrategyAIAgent* agent) -- `for each node of agent->+0x31c: task = node->value; if (task->vt[6](agent)) { erase task from the vector agent->+0x2e8..+0x2ec; agent->RemoveTask(task) 0x006af900; }`. Called three times inside RebuildAndRunTasks: before creation, after creation, and after execution. This is what pins IAITask::vt[6] as `bool IsFinished(StrategyAIAgent*)` -- returning true destroys the task [verified] constexpr uint32_t StrategyAIAgent_PruneTasks = 0x002b3640; // thiscall void __thiscall Game::StrategyAIAgent::RemoveTask(Game::IAITask* task) -- unlinks the task from four containers: the master list at this+0x31c and the vector at this+0x2e8 (via 0x006ae930), the 0x0c-stride vector at this+0x1cc (via 0x006a95e0), and the 0x20-stride vector at this+0x208..+0x20c (find 0x0069b0e0 then a rep-movsd compaction of 0x20-byte records) [verified] constexpr uint32_t StrategyAIAgent_RemoveTask = 0x002af900; // cdecl int (int taskTypeId) -- the AI's whole task-ordering policy as one switch: `if ((unsigned)id > 0x20) return 0; jmp [id*4 + 0x00691ffc]`, 33 arms each a single `mov eax,imm32; ret`. Values (id -> priority): 0 AITSteamroll 1250, 1 AITExplore 600, 2 AITExploreInForce 550, 3 AITEscortGate 700, 4 AITEscortGateInvade 400, 5 AITEscortGateInvadeGoal 950, 6 AITDeployGateAt 1400, 7 AITColonize 900, 8 AITColonizeGoal 970, 9 AITColonizeAt 1300, 0xa AITInvade 500, 0xb AITInvadeGate 1000, 0xc AITInvadeGoal 930, 0xd (no class) 200, 0xe AITDefendColonyIncoming 1100, 0xf (no class) 300, 0x10 AITDefendGateIncoming 1200, 0x11 AITKillEasterEgg 800, 0x12 AITInterceptEnemy 850, 0x13 AITMining 350, 0x14 AITMiningReturn 375, 0x15 AITAttackBlockade 100, 0x16 AITAdvanceIdleShips 0, 0x17 AITStockFreighters 50, 0x18 AITRespondAttackSystem 980, 0x19 AITRespondDefendSystem 990, 0x1a AITNodeBore 1275, 0x1b AITBuildStations 910, 0x1c AITBuildPoliceShips 75, 0x1d AITBuildDeepScanShips 60, 0x1e AITRaid 399, 0x1f AITRetrieveArtifact 1, 0x20 AITReturnArtifact 2. The last two table entries are DEAD: both artifact classes override vt[10] with fixed 0x4ec/0x4ed (1260/1261). Ids 0xd and 0xf have priorities but no surviving class [verified] constexpr uint32_t AITask_PriorityForType = 0x00291f00; // thiscall int __thiscall Game::IAITask::GetPriority() -- vtable slot 10, the DEFAULT implementation, used by 21 of the 31 concrete tasks (5 more reach it through the thunk 0x00682650): `return AITask_PriorityForType(this->vt[1]());`. Overrides: AITInvade 0x00683670 and AITEscortGateInvade 0x006835e0 return the globals at 0x00a1795c / 0x00a17960 when `this->+0x4 & 1` is clear, else default; AITAttackBlockade 0x00685600 scans a 0xc-stride vector at this->+0x8->+0x1cc for a related task and filters on its type id (1, 2, 7, 0x11, ...); AITRetrieveArtifact 0x005465a0 returns 0x4ec and AITReturnArtifact 0x00546800 returns 0x4ed unconditionally [verified] constexpr uint32_t AITask_slot10_GetPriority = 0x00294220; // offset Game::IAITask vtable slot 1 (byte offset 4) -- `int GetTypeId(void)`, PURE in the interface (vftable 0x009fa354), and in all 31 concrete classes a single 16-byte `mov eax,imm32; ret` returning a value in 0..0x20. It is the key into AITask_PriorityForType and the discriminator every cross-task filter uses [verified] constexpr uint32_t AITask_vt_slot1_GetTypeId = 0x00000004; // offset Game::IAITask vtable slot 5 (byte offset 0x14) -- `void Execute(Game::StrategyAIAgent* agent, int pass)`, RET 8, PURE in the interface. THE task body: 27 distinct implementations across the 31 classes, 48..288+ bytes each, dispatched from StrategyAIAgent_RunTaskList 0x006b3320+0x167 (`mov edx,[task_vt+0x14]; push pass; push agent; mov ecx,task; call edx`). This is the only slot from which a client order method is ever reached [verified] constexpr uint32_t AITask_vt_slot5_Execute = 0x00000014; // offset Game::IAITask vtable slot 6 (byte offset 0x18) -- `bool IsFinished(Game::StrategyAIAgent* agent)`, RET 4, PURE in the interface. Called ONLY from StrategyAIAgent_PruneTasks 0x006b3640+0x2b; true means unlink and destroy. 27 distinct implementations; the two shared trivials are 0x005eda80 `return false` (AITAdvanceIdleShips, AITSteamroll -- never retire) and, e.g., AITBuildDeepScanShips 0x00682fe0 `return !0x0069a7f0(agent, 0x20, 0)` [verified] constexpr uint32_t AITask_vt_slot6_IsFinished = 0x00000018; // offset Game::IAITask vtable slot 8 (byte offset 0x20) -- `const char* GetTypeName(void)`, PURE in the interface; in all 31 classes a 16-byte `mov eax,; ret` returning the class's own unmangled name ("AITRaid", "AITColonizeGoal", ...). Pairing slot 1 with slot 8 across the 31 vtables yields the complete task-type enum with no gaps except ids 0x0d and 0x0f [verified] constexpr uint32_t AITask_vt_slot8_GetTypeName = 0x00000020; // offset Game::IAITask vtable slot 9 (byte offset 0x24) -- `void Describe(void)`, no args, PURE in the interface. Every implementation is a single log call of the form `Log(": %s -> %s\n", NameOf(vt2()), NameOf(vt3()))` (AITAdvanceIdleShips prints the literal "AITAdvanceIdleShips: n/a -> n/a\n"). It is what pins slots 2 and 3 as the task's source and destination target getters [verified] constexpr uint32_t AITask_vt_slot9_Describe = 0x00000024; // offset Game::IAITask vtable slot 7 (byte offset 0x1c) -- `void OnObjectDestroyed(void* obj)`, RET 4, NOT pure: the interface default 0x005f8ac0 is a bare `ret 4`. The dominant override 0x00682540 (17 of 31 classes) nulls whichever of this->+0xc and this->+0x8 holds an object whose +0x4 equals the argument -- i.e. it drops dangling target references [verified] constexpr uint32_t AITask_vt_slot7_OnObjectDestroyed = 0x0000001c; // cdecl void (Game::StrategyClient* c) -- `g_StrategyClients[g_CurrentClientIndex + 1] = c; ++g_CurrentClientIndex;` (written as `mov [eax*4+0x00ae47e8],ecx` with eax = the old index, then `inc [0x00ae4808]`). So 0x00ae47e4 is a STACK of client scopes and 0x00ae4808 is its stack pointer, not a plain index -- the whole cl_* family reads `[idx*4 + 0x00ae47e4]`, i.e. the top of stack. 18 callers; the AI-relevant one is StrategyAIAgent::OnEvent 0x006d0ad0, which brackets the ENTIRE AI turn in Push(agent->+0x10) / Pop. That is the mechanism by which every cl_* call the AI makes -- cl_Chance, cl_RandRange, cl_SetResearchRate, cl_EndTurn -- lands on that AI's own client and its own RNG at client+0x134 [verified] constexpr uint32_t PushCurrentClient = 0x00178020; // cdecl void () -- `--g_CurrentClientIndex;`, the two-instruction pop matching PushCurrentClient 0x00578020. 18 callers, the same set. No other instruction in the image writes 0x00ae4808: an image-wide absolute-reference scan finds 42 referencing functions and every one of the other 40 only READS it [verified] constexpr uint32_t PopCurrentClient = 0x00178040; // thiscall void __thiscall Game::StrategyApp::RunPendingAITurns() -- called EVERY FRAME from StrategyNetworkClient::Update 0x007842b0+0xf7 with ECX = the StrategyApp singleton 0x00b29f98. `if (this->+0x1c == this->+0x20) return;` (empty pending-AI-player-id vector). Otherwise: t0 = clock 0x008d0b70; show the Game::AIProcessingDialog at 0x00b1149c if it exists; then FOR EVERY entry of +0x1c..+0x20 IN INDEX ORDER, find the client in +0xc..+0x10 whose client->+0x148 matches, update the dialog with client->+0x150, and StrategyClient::RaiseEvent 0x00783ee0(client, 0x26 /*SEResumePlaying*/, &ev) -- which is what runs that AI player's whole turn. The loop has no early exit and no frame yield. After it, the pending vector is emptied, and only THEN: `remaining = this->+0x2c - (clock() - t0); if (remaining > 0) Sleep((int)(remaining * 1000));` before hiding the dialog. THE THROTTLE IS A TRAILING SLEEP, NOT A COMPUTE BUDGET: AIProcessMinTime cannot change a decision and cannot defer an AI turn across frames [verified] constexpr uint32_t StrategyApp_RunPendingAITurns = 0x00438c60; // offset float -- Game::StrategyApp+0x2c, in SECONDS. Set once in StrategyApp::CreateGame 0x00888e80+0x90: the GameOptions key "AIProcessMinTime" (string at 0x00a32e30) is read through 0x00898bc0, converted with the CRT string-to-long at 0x009dd320, `fild`ed and divided by the double 1000.0 at 0x009e22f8, then clamped at 0 before `fst [esi+0x2c]`. Its only consumer is the trailing Sleep in StrategyApp_RunPendingAITurns 0x00838c60+0x10e [verified] constexpr uint32_t StrategyApp_off_AIProcessMinTime = 0x0000002c; // offset std::vector -- Game::StrategyApp+0x1c.._+0x20, the queue of player net ids whose AI turn is due. Drained in index order by StrategyApp_RunPendingAITurns 0x00838c60, which is therefore the ONLY thing that decides in what order the AI players are stepped and hence the order their TurnCommands blocks reach the host. WHO PUSHES TO IT WAS NOT FOUND by this lane -- neither an absolute-reference scan for the singleton nor an enumeration of the methods called on it located the enqueue site [mapped] constexpr uint32_t StrategyApp_off_PendingAIPlayers = 0x0000001c; // thiscall bool __thiscall Game::StrategyClient::(void* fleetObj, int mode, bool flag) -- RET 0xc. `if (this->+0x15c) return false;` then builds the 12-byte record {i32 fleetId = fleetObj->+4, i32 mode, bool flag}, calls the local-apply/validate 0x00821cf0(this->+0x148 /*playerId*/, &rec), and on true appends it to the accumulating TurnCommands at this+0x160 via the LIST 14 adder 0x00842a00. THIS IS LANE Q'S UNEXPLAINED LIST 14. The AI reaches it through 0x006987e0, which calls it TWICE per fleet -- (fleet, 0, true) then (fleet, 1, true) -- and 0x00842a00 keys its insert-or-update on BOTH fleetId (node+0x8) and mode (node+0xc), so an AI fleet order deposits TWO list-14 elements. The UI path (0x005e6fa0) and OnResumePlaying 0x00777480 also call it [verified] constexpr uint32_t ClientOrder_FleetTask = 0x003634d0; // thiscall void __thiscall Game::TurnCommands::(const rec* r) -- operates on the std::list at this+0x10c, which is lane Q's LIST 14 (member 14 of 27, +0x70 + 14*0x0c - 0x0c = +0x10c). Scans for a node with node->+0x8 == r->fleetId AND node->+0xc == r->mode; if found, overwrites node->+0x8/+0xc/+0x10 in place; otherwise push_back via 0x00766c20. The node payload is exactly lane Q's observed element record {i32, i32, bool} [verified] constexpr uint32_t TurnCommands_AddList14 = 0x00442a00; // cdecl void* (Game::StrategyAIAgent* agent, std::vector* route, void* dest) -- the AI's single fleet-order bridge and the busiest AI->TurnCommands edge in the module. Pushes `dest` through cl_* helper 0x00578cd0, opens a route build with 0x0057b4a0, appends each element of `route` with 0x0057aa50, closes with 0x0057b4d0, resolves the resulting handle through 0x008f4b30, and if non-null calls ClientOrder_FleetTask 0x007634d0 twice: (obj, 0, true) then (obj, 1, true). Three callers -- 0x006b76a0, 0x006c15e0, 0x006c16c0 -- which between them are reached from the Execute (slot 5) body of 24 of the 31 task classes [verified] constexpr uint32_t AI_IssueFleetTask = 0x002987e0; // thiscall Game::StrategyClient order method appending to TurnCommands LIST 5 (+0xa0, the planetary-budget/system-rates list lane O observed in zuul-turn17-orders2.sav) through helper 0x008490b0, which is `add ecx,0xa0; call 0x00843fa0`. Called from the AI at 0x0069dd80 (AI Prepare Turn's one-shot NextInt scheduler) and from seven non-AI sites including the cl_* façade at 0x00579110. It is one of the five order methods lane AI1's 21-row table missed [verified] constexpr uint32_t ClientOrder_SetSystemRates = 0x00363270; // thiscall Game::StrategyClient order method appending to TurnCommands LIST 7 (+0xb8, lane O's `{i32 shipId, i32 w}` colonize list) through helper 0x00842890. Called from the AI at 0x006af790 -- phase 32 of the AI Process Turn body, i.e. AFTER cl_EndTurn -- and from ten non-AI sites, seven of which are the cl_* façade family 0x00578fc0..0x005790e0. Also missing from lane AI1's table [verified] constexpr uint32_t ClientOrder_Colonize = 0x00369640; // offset Game::TurnCommands -- the first of the 27 std::list members lane Q enumerated, stride 0x0c, so list N (1-based, as lane Q numbers them) is at +0x70 + (N-1)*0x0c and the last, list 27, is at +0x1a8. Recorded here because the order-method -> list mapping in ai-task-system.md#4 is expressed entirely in these offsets: the adder for list N is the function whose first `this`-relative access is +0x70 + (N-1)*0x0c [verified] constexpr uint32_t TurnCommands_off_ListBase = 0x00000070; // cdecl void __cdecl Game::StrategyApp::OnClientEvent(int netId, int eventId, void* ev) -- THE AI ENQUEUE SITE (lane AI2 §6.1 open item, closed). Operates on the STATIC StrategyApp at 0x00b29f98 (not a pointer -- `mov ecx,0xb29f98` at 0x007843a2 proves the object itself lives there). Body: (1) `if (*(void**)0x00b29f9c == 0) return` -- app+0x4, the StrategyServer; (2) `if (netId == 0) return`; (3) linear search of the client vector at app+0xc/+0x10 (absolutes 0x00b29fa4/0x00b29fa8) for `client->+0x148 == netId`; not found -> return; (4) `p = client->+0x150`; (5) `if (eventId == 0x26 (SEResumePlaying) && p->+0xf9 != 0 && p->+0xfa == 0)` then `if (!0x00438fe0(&pending, &netId)) 0x0059f1a0(&pending, &netId)` -- a DEDUPLICATED push_back onto the pending-AI vector at app+0x1c (absolute 0x00b29fb4), and RETURN; (6) otherwise `StrategyClient::RaiseEvent 0x00783ee0(client, eventId, ev)` inline. So event 0x26 for an AI player is the ONLY deferred event; everything else is delivered synchronously. Registered as StrategyServer+0x170 by CreateGame at 0x00889177; it has ZERO direct callers and no vtable slot (lane B6's third blind spot) [verified] constexpr uint32_t StrategyApp_OnClientEvent = 0x00438e10; // thiscall void __thiscall Game::StrategyServer::ResumePlaying() -- THE STEPPING ORDER. (1) `if (0x0080f4d0(&this->+0x4)) return`; (2) `if (++this->+0x168 == 1 && this->+0x1b4) { obs->vt[4](2,0); obs->vt[7](); }`; (3) 0x007dd230(&this->+0x174, 0) then 0x007dd230(&this->+0x174, playerCount) -- clear+resize; (4) FIRST walk of the player vector at this->+0x54..+0x58, IN INDEX ORDER: `if (!p->Elim /*+0xf8*/) p->Status /*+0x164*/ = 0` -- this is a Player.Status WRITER; (5) SECOND walk, again in index order: `if (p->Status == 0) { ev = {vptr 0x00a23bb8}; cb = this->+0x170; cb ? cb(p->+0x4 /*netId*/, 0x26, &ev) : Log(0x00a23c08); }`. The callback is StrategyApp::OnClientEvent 0x00838e10, so the pending-AI vector is filled in SERVER PLAYER INDEX ORDER -- i.e. save player order -- and RunPendingAITurns then walks it in index order. Both Elim and Status are save-visible ServerPlayer fields [verified] constexpr uint32_t StrategyServer_ResumePlaying = 0x003ddc90; // thiscall void __thiscall Game::StrategyServer::SetClientEventCallback(void (__cdecl* cb)(int netId, int eventId, void* ev)) -- RET 4. Two instructions: `this->+0x170 = arg`. The only registration in the image is CreateGame 0x00889177 installing StrategyApp::OnClientEvent 0x00838e10. Every server->client event in the game funnels through this one pointer; SynchronizePlayer 0x007c6220 dispatches through it at 0x007c6384, 0x007c65aa, 0x007c6889, 0x007c6a00, 0x007c6ae0 and more [verified] constexpr uint32_t StrategyServer_SetClientEventCallback = 0x003861f0; // data Game::StrategyApp -- the STATIC APP OBJECT ITSELF, not a pointer to one. Proved by `mov ecx,0xb29f98; call ` at 0x007842f6/0x007843a2 and by the absolute pair 0x00b29fa4/0x00b29fa8 being read where a method reads this->+0xc/this->+0x10. Layout confirmed this lane: +0x0 flag byte (bit 2 = 'a StrategyServer exists'), +0x4 StrategyServer*, +0xc/+0x10/+0x14 vector, +0x1c/+0x20/+0x24 vector pendingAITurns (absolutes 0x00b29fb4/b8/bc), +0x2c AIProcessMinTime (seconds, float). Lane AI2's scan for 'functions that load 0x00b29f98' missed the enqueue because MSVC folds the object base into the absolute address of the member: the enqueue writes 0x00b29fb4 directly and never materialises 0x00b29f98 [verified] constexpr uint32_t g_StrategyApp = 0x00729f98; // cdecl void __cdecl Game::StrategyAIAgent::AcquireFleetsForTask(StrategyAIAgent* agent, IAITask* task, double dA, double dB, void* targetA, void* targetB, vector* candidates, int pass, int flag, vector* out) -- THE HUB EVERY FLEET-SHAPED TASK GOES THROUGH, and where `pass` acquires its meaning. Real span 992 bytes to the next function start (Ghidra's size is short). Three blocks, each a `for (i = 0; i <= pass; ++i) gather(..., i, ...)` loop followed by an 0x00698960 sufficiency test: block A (0x006cef5a, entered when targetB->+0x14 == 0, or == 2 with task->GetTypeId() == 0x1e) gathers via 0x006abf80; block B (0x006cf029) gathers via 0x006b7c90; block C (0x006cf0ee, only if A and B both failed and targetA != 0) gathers via 0x006cb310, taking task->vt[11]() at 0x006cf10e as an argument. ALL THREE order-emitting exits are pass==1 only: 0x006bbd50 (0x006cefcf, 0x006cf180) returns immediately unless pass==1, and 0x006c16c0 (0x006cf198) takes the arm at 0x006c1791 only when pass==1. The result vector at [ebp-0x3c] has exactly two possible writers -- 0x006bbd50 and 0x006c16c0 -- enumerated from every `lea` of that slot in the body, so on pass 0 this function RETURNS AN EMPTY FLEET LIST AND WRITES NO TurnCommands. At 0x006cf1aa it special-cases task->GetTypeId() 0x17 (StockFreighters) and 0x1a (NodeBore), substituting 0.0 for dA [verified] constexpr uint32_t StrategyAIAgent_AcquireFleetsForTask = 0x002ceef0; // cdecl void __cdecl Game::StrategyAIAgent::GatherFleetsForTier(StrategyAIAgent* agent, float threshold, void* target, vector* candidates, int tier, vector* out) -- 112 bytes. Walks the 0x20-stride candidate vector in index order, calling 0x006abb00(agent, threshold, target, &cand[k], tier, out, &out[k]) for each. `tier` is the loop index i of AcquireFleetsForTask's `for (i = 0; i <= pass; ++i)`, so it takes the values 0..pass [verified] constexpr uint32_t StrategyAIAgent_GatherFleetsForTier = 0x002abf80; // cdecl void __cdecl Game::StrategyAIAgent::FillCandidateToTierQuota(StrategyAIAgent* agent, float threshold, void* target, Candidate32* cand, int tier, vector* out, Slot36* slot) -- 464 bytes. THE INSTRUCTION THAT DEFINES THE TWO PASSES, at 0x006abb1c..0x006abb4d: `have = 0x00695b90(&slot->+0x10) + 0x00698860(slot) + slot->+0x20; want = (tier == 0) ? cand->+0x10 : (tier == 1) ? cand->+0x14 : 0; if (have >= want) return;` -- a compiler-generated switch on tier with case 0 -> 0x006abb41 (cand->+0x10) and case 1 -> 0x006abb39 (cand->+0x14). So each candidate carries TWO quota fields and `pass` selects which one is in force: pass 0 fills the +0x10 quota, pass 1 re-runs tier 0 and then fills the larger +0x14 quota. Then a per-fleet filter loop rejecting on 0x0069c8c0, IsClaimedByAnotherTask 0x006a8d20 and 0x006ab900 [verified] constexpr uint32_t StrategyAIAgent_FillCandidateToTierQuota = 0x002abb00; // cdecl bool __cdecl Game::StrategyAIAgent::IssueRouteForFleets(StrategyAIAgent* agent, int pass, vector* fleets, void* target, vector* out) -- 192 bytes. `if (pass != 1) return;` at 0x006bbd78 (the MSVC `sub eax,0 / je / dec / jne` switch shape). Otherwise walks the 0x24-stride fleet vector, calling 0x0057aac0 per element to build a route, then 0x006b76a0(agent, &route, target, out), which is one of the three callers of AI_IssueFleetTask 0x006987e0. This is one of the two pass-1 gates that make pass 0 emit nothing [verified] constexpr uint32_t StrategyAIAgent_IssueRouteForFleets = 0x002bbd50; // cdecl void __cdecl Game::StrategyAIAgent::RequestBuildForTask(StrategyAIAgent* agent, IAITask* task, int pass, double, double, void* targetA, void* targetB, void* targetA2, vector* gathered) -- 304 bytes, AcquireFleetsForTask's LAST-RESORT arm: no fleet could be found, so build ships. Two gates in the prologue: (1) 0x006cea7b..0x006ceaa6 `if (agent->+0x10->+0x150->+0x2d8 /*plcy*/ == 0 && task->GetTypeId() != 0x1a /*NodeBore*/) return` -- a SECOND, independent consumer of the save-visible `plcy` field, beyond the two defence creators lane AI2 found; (2) 0x006ceaac `if (pass != 1) return`. Reaches list 3 (build orders) via 0x006ce460 -> 0x006ce360 -> 0x006ce190 -> 0x006bd790 -> 0x006b3bc0 -> 0x00762fd0 [verified] constexpr uint32_t StrategyAIAgent_RequestBuildForTask = 0x002cea50; // cdecl void __cdecl Game::StrategyAIAgent::AssignFleetsAndIssueOrders(StrategyAIAgent* agent, IAITask* task, int pass, vector* fleets, void* targetA, void* targetB, int flag) -- 3536 bytes, the busiest AI->TurnCommands function. `if (pass != 1) goto 0x006c241f` at 0x006c177e (the same `sub eax,0 / je / dec / jne` shape), so its ENTIRE working body -- including both calls to AI_IssueFleetTask 0x006987e0 at 0x006c1c86 and 0x006c1f78, and the two 0x00699fa0 -> list 8 paths -- runs on pass 1 only. Its only indirect call sites are four import thunks (0x009dd12c/0x009dd150), so its direct-call closure is complete: no vtable edge can escape it [verified] constexpr uint32_t StrategyAIAgent_AssignFleetsAndIssueOrders = 0x002c16c0; // thiscall bool __thiscall Game::StrategyAIAgent::IsClaimedByAnotherTask(void* obj) -- RET 4. NAMES IAITask VTABLE SLOT 12 (lane AI2 §10.2). Looks `obj->+4` up in the 8-byte-stride claim registry at agent->+0x2e8..+0x2ec (pairs of {IAITask* owner, int objectId}) and in the 4-byte set at agent->+0x2d8..+0x2dc; if the object is in neither, returns false (free). Otherwise `cur = back(agent->+0x12c /*the task call stack*/)`; with an empty stack or a null top it returns TRUE (claimed). Then at 0x006a8db3: `if (!cur->vt[12]()) return true;` -- and when slot 12 IS set, it returns false (i.e. lets the task take the object) only when the owner exists, `cur->GetTypeId() != owner->GetTypeId()`, and `cur->GetPriority() > owner->GetPriority()`. So SLOT 12 IS A PREEMPTION PERMISSION: 'this task may take an object already claimed by a strictly lower-priority task of a different type'. Default false; five classes set it. Caller 0x006abb00 skips the candidate when this returns true [verified] constexpr uint32_t StrategyAIAgent_IsClaimedByAnotherTask = 0x002a8d20; // thiscall int __thiscall Game::StrategyAIAgent::RangePenaltyForTask() -- the ONLY consumer of IAITask vtable slot 13 found in the image, dispatched at 0x00696630 on the `this` receiver. `budget = this ? this->vt[13]() : 15; n = max(1, agent->+0x10->+0x8 - 0x0080da80(player) + 1); if (n < budget) return 0;` else a 7-arm species switch on player->+0x5c through the byte index at 0x006966a8 = [0,0,0,0,2,1,0] and the table at 0x0069669c: species 0,1,2,3,4,6 and out-of-range -> 1000000 (0x000f4240), species 5 (Zuul) -> 0. So slot 13 is a RANGE/HOP BUDGET compared against a count, with a prohibitive penalty past it -- and the Zuul are exempt, a FOURTH independent cross-check on lane AI2's species reading (after Hiver gates, Zuul node-bore and NPC building nothing). The two 'Incoming' defence tasks return INT_MAX from slot 13, so they never take the penalty [verified] constexpr uint32_t StrategyAIAgent_RangePenaltyForTask = 0x00296620; // thiscall int __thiscall Game::IAITask::GetPriority_Default() -- 17 bytes: `return AITask_PriorityForType(this->vt[1]() /*GetTypeId*/);`, i.e. the vt[1] dispatch followed by a direct call to the 33-arm table at 0x00691f00. This is what the two tuned GetPriority overrides tail-jump to when their flag bit is CLEAR, so lane AI2's priority table stands with an extra hop in front of it [verified] constexpr uint32_t AITask_GetPriorityDefaultThunk = 0x00294220; // data int -- image-initialised value 650 (0x0000028a). AITInvade::GetPriority 0x00683670 is `movzx eax,byte [ecx+4]; not al; test al,1; je +5; jmp 0x00694220; mov eax,ds:0xa1795c; ret` -- so the tunable is returned when BIT 0 OF this->+0x4 IS SET, which is the OPPOSITE of lane AI2's stated `if (!(this->+0x4 & 1))`. It has EXACTLY ONE reference in the whole image (this load) and no writer anywhere: no loader, no CSV path. It is a code constant that happens to live in the writable data section. AITInvade's table priority is 500, so the flag raises it to 650 [verified] constexpr uint32_t g_AITInvadeUncommittedPriority = 0x0061795c; // data int -- image-initialised value 750 (0x000002ee), the twin of 0x00a1795c. AITEscortGateInvade::GetPriority 0x006835e0 has the identical shape and the identical inverted polarity: the tunable applies when bit 0 of this->+0x4 IS SET. Exactly one reference in the image, no writer. AITEscortGateInvade's table priority is 400, so the flag raises it to 750 [verified] constexpr uint32_t g_AITEscortGateInvadeUncommittedPriority = 0x00617960; // thiscall void (CombatResolveContext* this) // THE POST-BATTLE RETREAT PIPELINE. Exactly one caller: CombatResolver_Run 0x007d5af0, unconditionally, at 0x007d5be2. Real body 0x007d5a00..0x007d5abb; the only jcc in it is the operator-new null test whose false arm is a _CxxThrowException. It builds a ~0x2c-byte RetreatContext stack local from the resolver's ctx (rc->+0x00 = ctx->+0x00 = S; rc->+0x04 = ctx->+0x08 = enc; rc->+0x08 = ctx->+0x0c = res; a std::map at rc->+0x0c with an operator_new(0x18) head node at rc->+0x10 and _Mysize rc->+0x14; a std::vector at rc->+0x1c/+0x20/+0x24) and runs SIX unconditional this-calls in a straight line: FUN_0079bb90 (per-player destinations), FUN_0079bcd0 (build groups), FUN_007b0320 (whole vs partial), FUN_00790790 (split partial fleets), FUN_007d5650 (execute; EVENT_FLEET_RETREATED_VIA_TELEPORT), FUN_007a7cd0 (destructor). CORRECTS combat-resolver.md's characterisation of this as 'the per-phase combat pipeline': it is ONE subsystem, retreat, not six combat phases. DRAW-FREE: a 327-function closure (E8 calls plus E9 tail-call thunks) contains zero calls to the four RNG primitives and zero inlined MT tempering immediates [verified] constexpr uint32_t CombatResolve_Retreat = 0x003d5a00; // thiscall void (RetreatContext* this) // RETREAT PHASE 1. One loop over enc->members (stride 0x44, magic 0x78787879 / sar 5). Per member: FUN_00787210(&enc->+0x1c, enc->+0x0c, member->+0x00 /*ServerPlayer*/, &r1, &r2, &r3), then this->dest[player->PlyrIdx(+0x28)] = the FIRST NON-NULL of (r1, r2, r3) via std::map::operator[] 0x0076bce0. So the per-player retreat destination is: nearest system you own, else nearest system with no hostile presence, else nearest system at all [verified] constexpr uint32_t CombatRetreat_ChooseDestinations = 0x0039bb90; // thiscall void (RetreatContext* this, Mars::Vector3* battlePos, ServerSystem* excludeSys, ServerPlayer* p, ServerSystem** out1, ServerSystem** out2, ServerSystem** out3) // ret 0x18. THE RETREAT DESTINATION FORMULA. THREE INDEPENDENT nearest-system searches sharing one pass over S->Systems (S->+0x44/+0x48, 4-byte stride), each with its own best-so-far seeded to the .rdata FLT_MAX at 0x009e23a8 (0x7f7fffff): out1 = nearest sys with FUN_007437e0(sys) == p (owner); out2 = nearest sys with FUN_00744070(sys, p) FALSE (no hostile presence); out3 = nearest sys, unconditional. Both skips: sys == excludeSys, and FUN_007469e0(sys). THE INDEPENDENCE IS LOAD-BEARING -- each 'best' store is INSIDE its predicate arm (0x007872c6 / 0x007872ec), so a nearer rejected system does not spoil that search; a single-loop 'find nearest then filter' gives a different answer. Distance is SQUARED: the three component deltas are each fstp'd to float32 first, the products and sum accumulate on the x87 stack, the total narrows to float32 once at 0x007872a3 (a double accumulator with one final narrowing is BIT-IDENTICAL, since a float32 delta squares exactly in double). Comparison is a strict < (fld best; fcomp d; test ah,0x41; jne skip), so an exact tie goes to the EARLIER system in the vector [verified] constexpr uint32_t CombatRetreat_FindDestination = 0x00387210; // thiscall bool (ServerSystem* this) // 33 B. return this->Dstyd(+0xc5) != 0 || (this->vnh(+0xc6) != 0 && this->vnex3(+0xc8) == 0). A destroyed system, or one hosting a Von Neumann hive that has not been cleared out. All three are SAVED bools (struct-recovery.md section 1), which is what identifies the class as Game::ServerSystem rather than a bare StarSystem. Note CombatResolve_NodeCannon 0x007bb530 uses only the +0xc5 half for its own destination filter [verified] constexpr uint32_t ServerSystem_IsUnusableRetreatTarget = 0x003469e0; // thiscall ServerPlayer* (ServerSystem* this) // 7 B: mov eax,[ecx+0x100]; ret. this->PID, the saved owner handle [verified] constexpr uint32_t ServerSystem_GetOwner = 0x003437e0; // thiscall bool (ServerSystem* this, ServerPlayer* p) // ret 4. return (ServerPlayer_HostileMaskAt(p, this) & this->AFlags(+0xd4)) != 0 [verified] constexpr uint32_t ServerSystem_HasHostilePresence = 0x00344070; // thiscall uint32 (ServerPlayer* this, ServerSystem* sys) // ret 4. THE HOSTILITY MASK, and it carries a diplomacy rule that is nowhere in findings/subsystems/: ownerBit = (sys && sys->PID(+0x100) && sys->TAcq(+0x80) == S->Frame) ? (1 << sys->PID->PlyrIdx) : 0; return ~( (1 << this->PlyrIdx(+0x28)) | (this->CF(+0x174) & ~ownerBit) | this->NA(+0x170) | this->AL(+0x16c) ). A SYSTEM WHOSE OWNER CAPTURED IT ON THE CURRENT TURN LOSES THAT OWNER'S CEASEFIRE COVER. The current turn is reached as this->+0x8->+0x8, which is S->+0xc = Frame, because ServerPlayer+0x8 == S+4. The complement is over all 32 bits, harmless only because it is immediately ANDed with a presence mask [verified] constexpr uint32_t ServerPlayer_HostileMaskAt = 0x0040dff0; // thiscall void (RetreatContext* this) // RETREAT PHASE 2. One loop over res->+0x68/+0x6c -- a 0x14-STRIDE VECTOR OF RETREAT REQUESTS (magic 0x66666667 / sar 3), a Game::EncounterResults member not previously mapped: req->+0x04 ship handle, req->+0x08 mode word, req->+0x0c requested-destination handle, req->+0x10 second key word. Per request: ship = HandleMap::Resolve(S->+0x84, req->+0x04); skip if null, if ship->Dep(+0x50) != 0, if ship->EncID(+0x7c) == 0x15, if FUN_004f47f0(ship->EncID) (a DB-driven encounter-type mask), or if ship->PlrID(+0x10) == S->Players[S->+0x1c0] (the locally-controlled player). dest = CombatRetreat_ResolveShipDestination(ctx, req, ship); on 0 it logs 'Retreat: Could not find retreat destination for %s.' at level 2 and skips. Then, UNLESS (req->+0x08 == 1 && owner->Species(+0x5c) == 1), skip the ship if StarShip_IsGroundedByDamage(ship). Finally find-or-create the group via CombatRetreat_FindGroup and push_back the ship [verified] constexpr uint32_t CombatRetreat_BuildGroups = 0x0039bcd0; // thiscall ServerSystem* (RetreatContext* this, void* req, StarShip* ship) // ret 8. Returns 0 when ship is null or ship->+0x04 (its handle id) is 0. Else: if req->+0x0c != 0, HandleMap::Resolve(S->+0x84, req->+0x0c) and RETURN IT if found -- the explicitly ordered destination; if not found, log 'Retreat: %s cannot retreat to specified location. Location not found.' at level 2 with ship->PlrID->+0x40 (the player's name std::string) and fall through. Fall-through: return this->dest[ship->PlrID(+0x10)->PlyrIdx(+0x28)], the phase-1 default [verified] constexpr uint32_t CombatRetreat_ResolveShipDestination = 0x0039bc50; // thiscall RetreatGroup* (RetreatContext* this, StarShip* ship, ServerSystem* dest, int mode, int variant) // ret 0x10. Linear scan of this->groups (+0x1c/+0x20) for the group matching ALL FOUR key words: g->+0x00 == ship->PlrID(+0x10), g->+0x04 == dest, g->+0x2c == mode, g->+0x30 == variant. Returns 0 when absent [verified] constexpr uint32_t CombatRetreat_FindGroup = 0x003871c0; // thiscall RetreatGroup* (RetreatGroup* this) // 41 B; ENUMERATES the 0x34-byte heap record (operator new(0x34) at 0x0079bdee). Writes: +0x00 owner ServerPlayer* = 0; +0x04 dest ServerSystem* = 0; +0x08/+0x0c/+0x10 vector = 0 (allocator +0x14 never written, per earned-rule 5); +0x18/+0x1c/+0x20 vector = 0 (allocator +0x24); +0x28 BYTE partial = 0; +0x2c int mode = 0; +0x30 int variant = -1. Total 0x34 [verified] constexpr uint32_t RetreatGroup_Ctor = 0x0038f1c0; // thiscall bool (StarShip* this) // 46 B. design = this->DesID(+0x14); return (design->+0x74 != 0 || design->+0x78 != 0) && design->+0x12c < 2 && (FLT_EPSILON > this->+0x30). The .rdata float at 0x009e1ef8 is 0x34000000 = 1.1920929e-07, FLT_EPSILON -- the test is against an epsilon, NOT against zero, and it is strict, so a health exactly AT the epsilon is not grounded. this->+0x30 is the third float of the inline ShipHealth at 0x24..0x33 (R1 guesses drive). True means the ship CANNOT retreat. The species-1 / mode-1 gate path skips this test entirely [verified] constexpr uint32_t StarShip_IsGroundedByDamage = 0x00415090; // cdecl bool (int encounterType) // 67 B. DB-SOURCED, DO NOT HARD-CODE. Returns false for encounterType > 0x17. Otherwise lazily builds a bitmask once (flag at 0x00b0e988, cache at 0x00b0e984) by FUN_004f47b0, which ORs 1 << table[i] over a count at 0x00ae04e0 and an int array at 0x00adfe88 -- both .bss, filled from the game data files at load -- and returns (mask >> encounterType) & 1. The 0x17 bound is the same EncounterType range PickDominantEncounterType 0x004f4c40 returns over [verified] constexpr uint32_t IsBlockedEncounterType = 0x000f47f0; // thiscall void (RetreatContext* this) // RETREAT PHASE 3. Per group in this->groups: tally the group's ships by ship->FltID(+0x64) into a local 8-byte-stride vector<{StarFleet*, int}> (linear search, push {f,0} then ++count); then per distinct fleet f, if count == (f->+0xa8 - f->+0xa4)/4 (i.e. EVERY ship the fleet has is in this group) push f into g->fleets(+0x18), else set g->partial(+0x28) = 1. WARNING: the jb/jae pairs at 0x007b0504 and 0x007b0570 are MSVC's inlined vector::push_back aliasing check (_Inside(&_Val)), NOT a phase gate -- both arms converge on *_Mylast = value; _Mylast += 4 at 0x007b05ce. Draw-free, event-free, writes only the group records [verified] constexpr uint32_t CombatRetreat_ClassifyFleets = 0x003b0320; // thiscall void (RetreatContext* this) // RETREAT PHASE 4, and THE BIGGEST SAVED-STATE WRITER ON THE COMBAT PATH. Per group, gated on g->partial(+0x28) != 0: newFleet = StrategyServer_CreateFleet(S+4, IDMap_AllocateLocalID(S+4), g->owner, &enc->+0x1c, NULL) -- note the id mint is evaluated INSIDE the argument list, see IDMap_AllocateLocalID; push newFleet into g->fleets BEFORE the ship loop; then per ship in g->ships, skip it if ship->FltID(+0x64) is ALREADY in g->fleets (its whole fleet is retreating, leave it), else StarFleet_RemoveShip(old, ship), and if ((old->+0xa8 - old->+0xa4) & ~3) == 0 call StrategyServer_DestroyFleet(S+4, old, 0, 0), then StarFleet_AddShip(newFleet, ship). Finally if (enc->+0x0c) StarSystem_FleetArrives(enc->+0x0c, newFleet). PREDICTED ORIGINAL BUG: the fleet is created before the loop, so a group flagged partial creates a fleet even when every ship turns out to belong to a wholly-retreating fleet -- a zero-ship fleet in the save [verified] constexpr uint32_t CombatRetreat_SplitFleets = 0x00390790; // thiscall void (StarFleet* this, StarShip* ship) // ret 4. this->NShips(+0xa4).push_back(ship); SHIP->FltID(+0x64) = this (the saved handle); design = ship->DesID(+0x14); this->+0xb8 |= design->+0xb8 and this->+0xbc |= design->+0xbc (capability masks); this->+0xc0 (WORD, the fleet's gate-traffic cost) += FUN_0056f430(design->+0x12c); if bit 0x20000 of +0xb8 changed state, push_back this into (this->+0x10)->+0x114; FUN_00705c70(this) to recompute derived stats [verified] constexpr uint32_t StarFleet_AddShip = 0x003062e0; // thiscall void (StarFleet* this, StarShip* ship) // ret 4. Linear-search this->NShips(+0xa4/+0xa8) for ship; return if absent. If the fleet's flight plan is non-empty (+0xc8 != +0xcc) and IsGateTransitWaypoint(wpt->+0x08) 0x0056e6e0, this->PID(+0x58)->GTraf(+0x14c) -= (int16)this->+0xc0. Then SHIP->FltID(+0x64) = 0; erase the slot (memmove + _Mylast -= 4); FUN_00700090(this); FUN_00705070(this, ship); re-add the gate-traffic term under the same test; if bit 0x20000 of +0xb8 changed, find-and-erase this from (this->+0x10)->+0x114; if any ship remains, FUN_00705c70(this). GTraf is a SAVED ServerPlayer field [verified] constexpr uint32_t StarFleet_RemoveShip = 0x003063a0; // thiscall void (ServerSystem* this, StarFleet* f) // ret 4. f->Pos(+0x18/+0x1c/+0x20) = this->Pos (three raw dword copies); StarFleet_SetLocation(f, this); bit = 1 << f->PID(+0x58)->PlyrIdx(+0x28); this->FFlags(+0xd8) |= bit; this->+0x16c.push_back(f) (the SAVED NumFlts/Flt list); then recompute: if (FFlags & bit || GFlags(+0xdc) & bit || PID(+0x100) == owner) { AFlags(+0xd4) |= bit; VFlags(+0xcc) |= bit; } else AFlags &= ~bit -- on this path the else arm is unreachable because FFlags was just set [verified] constexpr uint32_t StarSystem_FleetArrives = 0x0034f240; // thiscall void (StarFleet* this, void* loc) // 16 B: this->LocID(+0xa0) = loc. Saved handle [verified] constexpr uint32_t StarFleet_SetLocation = 0x002fe2f0; // thiscall void* (StarFleet* this) // 19 B: eax = this->LocID(+0xa0); return (eax && eax->+0x14 == 0) ? eax : 0. The +0x14 word is a location-class tag; the sibling FUN_006fe320 returns it only when the tag is 2. The retreat path uses this one, whose class carries the fleet list at +0x16c and the name std::string at +0xa8 [verified] constexpr uint32_t StarFleet_GetLocationIfNode = 0x002fe300; // thiscall void (RetreatContext* this) // RETREAT PHASE 5. Two nested loops: over this->groups, then over g->fleets (whole fleets from phase 3 plus any new fleet from phase 4). Per fleet f: (a) if (enc->+0x0c && !ServerSystem_IsExploredBy(enc->+0x0c, f->PID(+0x58))) StrategyServer_GrantSystemIntel(S, S->+0x44[enc->+0x0c->Idx(+0x5c)], f->PID) -- RETREATING FROM AN UNEXPLORED SYSTEM REVEALS IT, writing the SAVED EFlags and PlayerView; (b) if CombatRetreat_UsesGate(g) && g->dest(+0x04) != 0, the GATE ARM: StarSystem_FleetDeparts(GetLocationIfNode(f), f), StarSystem_FleetArrives(g->dest, f), then two _snprintf'd strings from the .bss format-string pointers at 0x00aedf0c and 0x00aedf14 (destination Name(+0xa8), fleet FtName(+0x5c)) and EventStorage_PostEvent(ServerPlayer_GetEventStorage(f->PID), {msg, summary, f, 0, S->Frame, 'EVENT_FLEET_RETREATED_VIA_TELEPORT', 0}); (c) else the MOVE ARM: if (!StrategyServer_OrderFleetMove(S+4, f, &g->dest, 1)) log 'Retreat: Unable to set destination for retreat for %s.' at level 2 with f->PID->+0x40, else StarFleet_SetFlag(f, 2, true) and StarSystem_FleetDeparts(GetLocationIfNode(f), f) [verified] constexpr uint32_t CombatRetreat_Execute = 0x003d5650; // cdecl bool (RetreatGroup* g) // 48 B, ret 4 (ecx is set at the one call site but unused). return g != 0 && g->mode(+0x2c) == 1 && g->owner(+0x00) != 0 && g->owner->Species(+0x5c) == 1 && g->dest(+0x04) != 0. Species 1 is the gate-building species; this is the ONLY gate on EVENT_FLEET_RETREATED_VIA_TELEPORT, which is posted nowhere else in the image. The same (mode == 1 && species == 1) pair is what lets a ship with a dead drive past StarShip_IsGroundedByDamage in phase 2 [verified] constexpr uint32_t CombatRetreat_UsesGate = 0x003859f0; // thiscall bool (ServerSystem* this, ServerPlayer* p) // 34 B, ret 4: return (this->EFlags(+0xd0) & (1 << p->PlyrIdx(+0x28))) != 0. EFlags is a SAVED int [verified] constexpr uint32_t ServerSystem_IsExploredBy = 0x00343880; // thiscall void (ServerSystem* this, ServerPlayer* p, bool on) // 82 B, ret 8. bit = 1 << p->PlyrIdx(+0x28); wasSet = (this->EFlags(+0xd0) & bit) != 0; on ? EFlags |= bit : EFlags &= ~bit; then this->vft[0x1c](p, wasSet, on) -- AN INDIRECT EDGE, unresolved here, flagged for the vtable-inversion lane [verified] constexpr uint32_t ServerSystem_SetExploredBy = 0x003438b0; // thiscall void (StrategyServer* this /*the S frame*/, ServerSystem* sys, ServerPlayer* p) // 195 B, ret 8. Returns immediately if either pointer is null. ServerSystem_SetExploredBy(sys, p, true); FUN_006e4620(this->+0x154 /*the node-line/graph object*/, sys, p, 1); then builds a stack temp via FUN_00755ab0, fills it with FUN_007561d0(sys, &tmp, p), and applies it through FUN_007d1f00(this->+0x224, &sys->Idx(+0x5c), &p->PlyrIdx(+0x28), &tmp) -> FUN_0075cf50 -> FUN_00753430, destroying the temp with FUN_006bba00. this->+0x224 is INFERRED to be the per-(system, player) StarSystem::PlayerView map -- combat-done-tail.md phase 17 has its tree head at S+0x228 and its size word at S+0x22c, which puts the map object at S+0x224 -- but the final leg FUN_00753430 was NOT read, so the exact field written is a labelled hypothesis [hypothesis] constexpr uint32_t StrategyServer_GrantSystemIntel = 0x003d5080; // thiscall void (RetreatContext* this) // RETREAT PHASE 6, and it is only a destructor -- it writes no game state. FUN_00793090(this->+0x1c, this->+0x20) deletes every RetreatGroup; then vector::erase(begin,end) (a degenerate memmove of 0 bytes plus _Mylast = _Myfirst) and operator delete on the buffer, zeroing +0x1c/+0x20/+0x24; then _Tree::erase(head->_Left, head) via FUN_006a4e40 on the map at this->+0x0c and operator delete on its head node [verified] constexpr uint32_t CombatRetreat_ContextDtor = 0x003a7cd0; // thiscall StarFleet* (StrategyServer* this /*the S+4 frame*/, int oid, ServerPlayer* owner, Mars::Vector3* pos, const char* nameOverride) // 347 B, ret 0x10. operator new(0x120) -- SIZEOF(Game::StarFleet) = 0x120 BY ENUMERATION, agreeing with struct-recovery.md section 3 whose last member FtMS sits at +0x11c -- then ctor FUN_00706140; IDMap_Insert(this+0x80, obj, oid) which writes the id into obj->+0x04; obj->+0x10 = this (the S+4 pointer, NOT a ServerPlayer as it is on StarShip); obj->PID(+0x58) = owner; the name from FUN_00856310(owner) (-> FUN_00855f80 on owner+0x298) assigned into obj->FtName(+0x5c) unless nameOverride is given; pos copied to BOTH +0x18/+0x1c/+0x20 (Pos) and +0x4c/+0x50/+0x54 (PrvPos); StarFleet_SetFlag(obj, 0x400, true); push_back into the vector at this->+0x60 (= S+0x64), THE SAVED NumFlts/FltID/Flt MASTER LIST; then the virtual (*this)->vft[0x10](this, obj). Draw-free, including the name generator. DELEGATED instruction-level read; the call site is mine [verified] constexpr uint32_t StrategyServer_CreateFleet = 0x0045b340; // thiscall int (StrategyServer* this /*the S+4 frame*/) // AN 8-BYTE TAIL-CALL THUNK: sub ecx,0xffffff80 (ecx += 0x80) then jmp FUN_008b8b70, which is push [ecx+0x18] (the local node index); call IDMap_AllocateID; ret. IT TAKES NO STACK ARGUMENTS. At the one retreat call site 0x007907d8 three pushes sit immediately before it and belong to the LATER call to StrategyServer_CreateFleet -- the id mint is evaluated inside that call's argument list. Checking ret N against the push count is what catches this; see combat-retreat-pipeline.md section 7.3. The IDMap subobject therefore lives at (S+4)+0x80 [verified] constexpr uint32_t IDMap_AllocateLocalID = 0x0040f710; // thiscall int (IDMap* this, int nodeIndex) // ret 4. THE OBJECT-ID ALLOCATOR. Closes struct-recovery.md section 7's open item 'OID allocation (R2's x16)': R2's x16 is the shl 4 below. Layout: this->+0x08/+0x0c is a vector of STRIDE 0x14 (pinned twice -- FUN_008b8a70's 0x66666667 / sar 3 divide-by-20, and the lea eax,[edi+edi*4] with scale 4 here); NodeEntry is a std::map (0x10) followed by the counter at +0x10; this->+0x18 is the local node index. Body: returns 0 if nodeIndex == -1 (logs 'IDMap: Map not initialized.') or the node does not exist; else 0x008b8b16 'inc DWORD PTR [ecx+eax*4+0x10]' PRE-INCREMENTS the per-node counter, and on wrap to 0 logs 'IDMap: NextID wrapping for node %d.' and increments AGAIN (0x008b8b30), so 0 is never issued -- 0 is INVALID_NETWORK_ID; then id = (counter << 4) | (nodeIndex & 0xF) at 0x008b8b38/0x008b8b3e, with an overflow log if the counter no longer round-trips. THE COUNTER IS ALMOST CERTAINLY THE SAVE'S 'NMnx' TAG (StrategyServer block order: KeyPath, NMSz, NMLc, NMnx, ModCount, Frame) -- LABELLED HYPOTHESIS: StrategyServer::Write 0x0079fa70 was not read for it. DELEGATED instruction-level read [verified] constexpr uint32_t IDMap_AllocateID = 0x004b8ae0; // thiscall void (IDMap* this, void* obj, int id) // ret 8. Logs 'IDMap: Object already exists with id %d.' and continues if the id is taken; refuses id 0 with 'IDMap: Object with INVALID_NETWORK_ID not inserted.'; node = id & 0xF, bounds-checked; WRITES THE ID INTO obj->+0x04 (the NetworkObject handle slot every serialized object uses); then _Buynode + _Insert into the map at _Myfirst + node*0x14. DELEGATED instruction-level read [verified] constexpr uint32_t IDMap_Insert = 0x004b9350; // thiscall void (StrategyServer* this /*the S+4 frame*/, StarFleet* f, bool killRemainingShips, int shipDestroyArg) // 1352 B, ret 0xc. Called from the retreat split as (f, 0, 0), so the ship-massacre block 0x0088bbf1..0x0088bc82 is dead on that path. A REAL DELETE (vtable slot 0 with flag 1, the MSVC scalar deleting destructor, at 0x0088bea6). Order: (1) sweep the master fleet vector this->+0x60/+0x64 for any fleet whose flight plan (+0xc8/+0xcc, waypoint stride 0x1c) carries a waypoint whose field_0 is f's id -- for each, re-plan it via FUN_00707290 and POST 'EVENT_FLEET_INTERCEPT_ABORTED' to THAT fleet's owner, inlining player+0x29c rather than calling ServerPlayer::GetEventStorage; (2) optionally destroy remaining ships; (3) erase from the owner's fleet map at owner+0x2b4; (4) per player, FUN_00812110(player+0x29c, fleetId, -1) to purge queued events referencing it; (5) erase from this->+0x60 or log 'StrategySim: Can't delete %s (%i), not found in master list!'; (6) detach from its location; (7) vft[0x14](f), erase from this->+0x114, FUN_008b92a0(this+0x80, f); (8) delete. No RNG, no inlined MT draw, no SETurnResults stride. DELEGATED instruction-level read, full body to the next function start [verified] constexpr uint32_t StrategyServer_DestroyFleet = 0x0048b980; // thiscall bool (StrategyServer* this /*the S+4 frame*/, StarFleet* f, ServerSystem** dests, unsigned count) // 801 B, ret 0xc. Returns false if f is null, or if the path solver FUN_007066c0 sets any of bits 0x008 / 0x010 / 0x400 in its flag word (then logging 'StrategySim: %s (%s) move not permitted at this time.' at level 2) -- THE MEANING OF THOSE THREE BITS IS UNKNOWN, FUN_007066c0 was not opened. Before that gate it SNAPS the fleet's position onto its current system when they differ, tested by FUN_0080ec50 with fucompp EXACT IEEE EQUALITY and no epsilon; that snap is committed even on the false path. On success it builds a 0x1c-stride waypoint vector and commits it through FUN_00707080, which debits owner->GTraf(+0x14c) by (int16)f->+0xc0, writes f->+0xd8..+0xf8 and +0x100..+0x108 (cached source/destination positions and the leg record), then re-credits GTraf; finally it cancels conflicting ship actions via FUN_00849280. No RNG and no x87 in the function itself. DELEGATED instruction-level read, full body to the next function start [verified] constexpr uint32_t StrategyServer_OrderFleetMove = 0x004653c0; // thiscall void** (std::map* this, const int* key) // 125 B, ret 4. MSVC std::map::operator[]: _Lbound over the tree from this->_Myhead(+0x04)->_Parent, testing _Isnil at node+0x15 and the key at node+0x0c; if found returns &node->_Myval.second (node+0x10), else default-inserts the pair {key, 0} via _Buynode 0x008b91d0 + _Insert 0x0072b400 and returns the same. NODE IS 0x18 BY ENUMERATION from _Buynode's operator new(0x18): _Left +0x00, _Parent +0x04, _Right +0x08, pair at +0x0c/+0x10, and _Color/_Isnil written as ONE 16-bit store at +0x14/+0x15, plus 2 bytes padding. DELEGATED instruction-level read [verified] constexpr uint32_t Map_IntPtr_Subscript = 0x0036bce0; // site The byte AFTER Game::BuildQueue::ProcessTurn's last instruction (`ret 0x8` at 0x00891226, then int3 padding). Ghidra reports the function as 1230 bytes from 0x00890d50, i.e. ending at 0x0089121e -- INSIDE the epilogue, before the security cookie check. The body is 1264 bytes. Earned rule 17. Note that this address is ALSO the entry of SystemBuildQueue_AttachBuiltShip, the function's own slot-10 callee, which is why the fleet half of construction looked absent from the pass [verified] constexpr uint32_t BuildQueue_ProcessTurn_RealEnd = 0x00491240; // field ServerPlayer* -- the player that owns the queue. Read at 0x00890de9 (passed to the ship factory), 0x00890ec9 (passed to the system's post-build hook), 0x00890ef4 (the base of the ShipRecords update) and 0x00890f75 (the build-completed event's owner field) [verified] constexpr uint32_t BuildQueue_off_Owner = 0x0000000c; // field std::list head sentinel. The pass walks it as `node = *(head); while (node != head) node = *node`, so it is the MSVC circular list. Read at 0x00890d9a and re-read every iteration at 0x00890d9d / 0x00891020 [verified] constexpr uint32_t BuildQueue_off_OrderList = 0x00000010; // field int -- the list's element count. Used as the reserve hint for the removal sweep's scratch vector at 0x0089104a and decremented once per unlinked order at 0x008911b0 [verified] constexpr uint32_t BuildQueue_off_OrderCount = 0x00000014; // field int conleft, measured from the std::list NODE base (node+0x0 next, +0x4 prev, +0x8 the order's own vptr -- the list is polymorphic -- then +0xc desID, +0x10 con, +0x14 sav, +0x18 conleft, +0x1c ordID, +0x20 ShipDesign*). Compared against the remaining points at 0x00890db5, decremented at 0x008910c4, zeroed at 0x0089100c, and it is the removal sweep's predicate at 0x00891070 [verified] constexpr uint32_t ShipBuildOrder_off_ConLeft = 0x00000018; // field ShipDesign* -- from the same std::list node base as ShipBuildOrder_off_ConLeft. Loaded at 0x00890dbe and is the source of the money cost (+0xc0), the role flags (+0xb8) and the hull class (+0x12c) the completion reads [verified] constexpr uint32_t ShipBuildOrder_off_Design = 0x00000020; // vslot bool (StrategySim* sim, int64 cost) -- vtable slot 9. Called at 0x00890ddc only when design->+0xc0 > 0; a FALSE return SKIPS that order and the pass continues with the next one rather than stopping. Pure in Game::BuildQueue's own vtable (0x00a31328); Game::SystemBuildQueue (0x00a31358) binds 0x00809910 and Game::ShipBuildQueue (0x00a31388) binds 0x0091e480 [verified] constexpr uint32_t BuildQueue_vslot_ChargeMoney = 0x00000024; // vslot void (StrategySim* sim, StarShip* ship) -- vtable slot 10, called at 0x00890e20 immediately after the ship is created. Pure in the base vtable; Game::SystemBuildQueue binds 0x00891240, Game::ShipBuildQueue binds 0x0081c000. This is where join-an-existing-fleet vs create-a-new-one is decided [verified] constexpr uint32_t BuildQueue_vslot_AttachBuiltShip = 0x00000028; // __thiscall void (SystemBuildQueue* this, StrategySim* sim, StarShip* ship) /* RET 8. Game::SystemBuildQueue vtable slot 10. Routes the newly built hull by design role flags -- design->+0xb8 & 0x80000, design->+0xbc & 0x4, design->+0xb8 & 0x800000, design->+0xb8 & 0x400 each take their own handler -- and FALLS THROUGH at 0x00891328 to `if (ship->FltID == 0) ServerSystem_AttachShipToHomeFleet(sys, ship)`. NOTE the 0x400 tested at 0x00891311 is a DESIGN role bit and is NOT the fleet's FtFlg 0x400 */ [verified] constexpr uint32_t SystemBuildQueue_AttachBuiltShip = 0x00491240; // __thiscall void (ServerSystem* this, StarShip* ship) /* The join-or-create step. If this->+0x238 is non-null the cached home fleet is reused; otherwise an id is drawn from the object-id allocator (0x0074f500) and StrategyServer_CreateFleet (lane B5, 0x0085b340) builds a fleet at the system's position with a NULL name override, the result is cached in this->+0x238 (0x0074f513), FtFlg |= 0x20 (0x0074f519), and StarFleet_AddShip links the hull. ONE home fleet per system: every hull built at that system in later turns joins it */ [verified] constexpr uint32_t ServerSystem_AttachShipToHomeFleet = 0x0034f4d0; // field StarFleet* -- the cached fleet newly built hulls join. Tested at 0x0074f4d6 and written at 0x0074f513. NEW OFFSET: not in struct-recovery.md's ServerSystem table [verified] constexpr uint32_t ServerSystem_off_HomeFleet = 0x00000238; // __thiscall StarShip* (ObjectHost* this, int id, ServerPlayer* owner, ShipDesign* design) /* RET 0xc. operator new(0xb0) at 0x0086571c, ctor 0x00861280, then IDMap_Insert at 0x00865754 which is where the object id lands in ship->+0x4. Reached from exactly two places in the image: the build queue (through the 0x004f41a0 thunk) and the trade manager's encounter spawner */ [verified] constexpr uint32_t StarShip_Create = 0x004656f0; // __thiscall void (StarShip* this, ObjectHost* host, ShipDesign* design, ServerPlayer* owner) /* Zeroes the object through 0x0080c960 (which leaves +0x48, +0x60 and +0xac at -1), then writes +0xc host, +0x14 design (DesID), +0x10 owner (PlrID); allocates the ship-borne BuildQueue into +0x98 when design->+0xb8 & 0x400000, and three Population objects into +0x9c/+0xa0/+0xa4 when design->+0xb8 & 0x4000000; finally 0x00854680 copies the cached design stats -- Range +0x20 from design+0xe8, Health +0x24..+0x30 from design+0xec.., RefCap +0x6c, RepCap +0x70, ConCap +0x68 from design+0xd0. FltID (+0x64) is born NULL and is set by StarFleet_AddShip */ [verified] constexpr uint32_t StarShip_Ctor = 0x00461280; // field int tblt -- the turn the hull was completed. Born -1 in the constructor's default sweep (0x0080c9fb) and overwritten by the build queue at 0x00890e02 with the StrategySim's Frame word. MEASURED: the six hulls the zuul turn-16 -> turn-17 pair adds all carry tblt equal to the NEW turn number, which independently confirms that BeginProcessTurn's Frame increment happens BEFORE the spine, so a phase reading Frame during a turn sees the turn it is producing, not the one it started from [verified] constexpr uint32_t StarShip_off_TurnBuilt = 0x000000ac; // site `inc DWORD PTR [esi+edx*4+0x1b4]` with esi = the queue's owner and edx = design->+0x12c (the hull class). THE ONLY WRITER OF THE PER-CLASS BUILT COUNTER IN THE WHOLE IMAGE: a byte scan for the indexed-increment form at that displacement over all executable sections returns this one site. A hull that reaches the wire with the counter bumped came through the build queue and through nothing else [verified] constexpr uint32_t ShipRecords_BuiltCounterSite = 0x00490ef7; // field int built[3] -- the first of Game::ShipRecords' four parallel per-hull-class arrays (built, lost, killed, inService; wire tags srb/srl/srk/sri under the srnc count). SIZED BY ENUMERATION, not by what the code touches: three classes x four arrays x 4 bytes from 0x1b4 lands exactly on 0x1e4, the per-design vector, which is the next thing the same function reads. struct-recovery.md places the ShipRecords sub-object at 0x1b0, so 0x1b0 is its vptr [verified] constexpr uint32_t ServerPlayer_off_ShipRecordsBuilt = 0x000001b4; // field std::vector<{int srd; int src; int srb; int srl; int sri}> at +0x1e4/+0x1e8/+0x1ec, stride 0x14 (the wire's srbd section). The completion scans it linearly for a record whose first word equals the design's object id (0x00890f10), appends one when there is no hit (0x00890f4c) and increments the record's third word (0x00890f61). MEASURED: srd really is the design's save id -- the zuul corpus shows records keyed 608/576/114/816/18/34/130 against build orders naming exactly those desIDs [verified] constexpr uint32_t ServerPlayer_off_ShipRecordsByDesign = 0x000001e4; // __thiscall int (ServerTradeManagerImpl* this, int* spec) /* THE SECOND AND ONLY OTHER ROOT THAT CREATES SHIPS. Draws two ids from the object-id allocator, creates a StarFleet through StrategyServer_CreateFleet (lane B5, 0x0085b340) (0x0088f314) and then loops StarShip creation (0x0088f375) + StarFleet_AddShip (0x0088f381). Reached only from 0x008926ce, itself reached only from ServerTradeManagerImpl vtable slot 17 (0x008938a0), which picks its target with an RNG draw. It does NOT touch ModCount, and it does NOT touch ShipRecords -- so an encounter squadron is invisible to the per-class built counter, which is why that counter is a clean discriminator for player-built hulls */ [verified] constexpr uint32_t TradeManager_SpawnEncounterSquadron = 0x0048f070; // __thiscall void (ShipAction* this, ...) /* The construction-ship wrapper around BuildQueue::ProcessTurn (calls it at 0x00789551 with the ship's own queue at ship->+0x98 and its ConCap at ship->+0x68 as the point budget). REACHABILITY NOTE, and it is a new indirection class for the campaign: this function has ZERO call sites and is in NO vtable. Its address is written into a STACK-BUILT function-pointer table by the ship-action dispatcher (0x007b9c4b `mov eax,0x789500`, stored at 0x007b9c50), alongside five siblings. tools/vtable_map.py cannot see edges of this shape, so `no caller` and `no vtable caller` are BOTH lower bounds */ [verified] constexpr uint32_t ShipBorneBuildQueue_ProcessTurn = 0x00389500; // site CORRECTION to addresses.json's BuildQueue_ProcessTurn prototype, which reads `int (BuildQueue* this, ServerSystem* sys, int points)`. The FIRST STACK ARGUMENT IS NOT THE SYSTEM. At the only real call site the pushed value is `[sys+0x10] - 4` (0x0075257c `lea edi,[eax-0x4]` with eax = [esi+0x10], esi = the ServerSystem, ecx = [esi+0xa4] = the queue), i.e. the StrategyServer `S` frame -- the same object lane B5's StrategyServer_CreateFleet takes at S+4, one word higher. Two consequences the old prototype hides: the build-completed event is pushed onto a list at S+0x2b0, NOT onto the system; and the turn stamp written into the new hull at 0x00890e02 is S+0xc, which StrategyServer::Write tags `Frame`. AGREEMENT with lane T section 0 (the two bases four bytes apart) and with lane A2 (S+0xc is Frame, not ModCount) [verified] constexpr uint32_t BuildQueue_ProcessTurn_Arg0Correction = 0x00352589; // field ServerSystem* -- the system a SystemBuildQueue belongs to, read by the slot-10 attach handler at 0x00891246 as `(this->+0x4 == 0) ? this->+0x8 : 0`. Also read by the pass itself at 0x00890f89 for the build-completed event [verified] constexpr uint32_t BuildQueue_off_OwningSystem = 0x00000008; // thiscall int (ServerSystem* sys, int points, bool estimateOnly) // `ret 8`, real end 0x007517b5 (SEH frame, /GS cookie). Returns the points NOT consumed. Walks the system's fleets through the system's own vtable (slot 2 = count, slot 4 = element), keeps those whose +0x58 equals sys->PID and that pass 0x00813ab0(0,8), and inside each keeps the ships that pass 0x00814da0(0,8) with 0x00815180(ship,1) > 0. With estimateOnly it returns max(points - totalCost, 0) and touches nothing; without it, it distributes round-robin -- share = max(points / shipCount, 1) per pass, take = min(cost, share), applied by 0x008151c0 -- until no ship takes anything or the points run out. THE ROUND ROBIN IS EQUIVALENT TO points - min(points, totalCost): share is at least 1, so every ship with a positive cost takes at least one point per pass, and the only early exit needs every remaining cost to be zero. THIS IS THE SIDE EFFECT that makes ComputeOutputFromRates unsafe to call for its value (the B1 replace double-run defect): ComputeOutputFromRates passes estimateOnly = 0 [verified] constexpr uint32_t ServerSystem_RepairShipsInOrbit = 0x00351590; // thiscall int (ServerSystem* sys, double constructionShare) // `ret 8`, real end 0x00746883. Turns the ship-construction channel's rounded share into out[7]: with no owner it is ftol(share), else k = StationCount(sys, PID, 1) and b = STATION_BONUS_SHIPCON (slot 0x00af08ec) taken as 0 unless STRICTLY positive, returning ftol( k x (b x share) + share ). TRUNCATING, not rounding, and note the association -- neither is `share x (1 + b x k)` [verified] constexpr uint32_t ServerSystem_ConstructionPoints = 0x00346830; // thiscall int (BuildQueue* q) // 23 bytes, plain `ret`, no frame. Walks the std::list at q+0x10 from its sentinel and sums the dword at +0x18 of each node -- the order's `conleft`. This is the queue demand ComputeOutputFromRates charges against out[7] before anything cascades back to the money channel, and the ONE input of that function that a save can supply in full (Sys/BQ/ords/conleft is on the wire) [verified] constexpr uint32_t BuildQueue_TotalConstructionLeft = 0x004251e0; // thiscall double (ServerSystem* sys) // real end 0x00745dc0. The suitability the terraform channel aims at, and the value NormaliseOutputRates compares sys->Suit against with an exact ==. With no owner it returns sys->Suit itself, so an unowned system is always 'at its ideal'. Otherwise it starts from owner->IdealSuit (+0xb0), replaces that with StrategyServer::IdealSuit(sys->server, sys->indi->indsp) when the system carries an independence record (+0x1c8), and finally overrides both with sys->dsu (+0x118) whenever dsu differs from the float behind 0x00aeca6c. NOT the same source as CalcSuitMod's ideal, which is the server's per-species array unconditionally -- the two agree on every corpus save, so the difference is instruction-verified only. Every corpus system carries dsu = FLT_MAX, which is why the sentinel is READ AS FLT_MAX (inferred from the corpus, not from the data files) [verified] constexpr uint32_t ServerSystem_IdealSuitability = 0x00345d60; // custom int (/* ESI = ServerSystem* sys */) // real end 0x0074c80f. TAKES ITS `this` IN ESI, not ECX -- a compiler-local helper that inherits the register its caller holds; hooking or calling it as a __thiscall reads the wrong object. Returns 0 with no owner and 0 on an independent colony (+0x1c8), else derives a civilian-share ratio from three int64 population helpers (0x0074a870, 0x0074a8c0, 0x0074a920), scales CIVILIAN_RESOURCES_CONSUMED (slot 0x00ae2ea4) by it and floors the result at 1. Feeds only the resource ledger (out[1], out[2]); it is NOT on the path to out[3] [unverified] constexpr uint32_t ServerSystem_CivilianConsumption = 0x0034c6f0; // custom int (/* EBX = ServerSystem* sys */ ServerPlayer* owner) // real end 0x0074615a. TAKES THE SYSTEM IN EBX and the player on the stack -- the second compiler-local helper in this call graph with an inherited register. Returns 0 when either is null. Walks the system's fleets through the system's vtable (slot 2 = count, slot 3 = element), keeps those whose owner (0x0071e280) is the argument and whose +0x78 byte is set, and sums 0x00829180 over the ship vector at +0xa4..+0xa8. ComputeOutput stores the result in out[6]; nothing downstream of out[3] reads it [verified] constexpr uint32_t SystemRepairDemandForOwner = 0x003460b0; // thiscall void (TradeManager* mgr, ServerSystem* sys, ServerPlayer* owner, int* a, int* b) // `ret 0x10`. Returns immediately when `sys` is null, else forwards to 0x00833a10(owner, a, b, 0, sys) with the manager still in ECX. ComputeOutput reaches it as `mgr = server->vtbl[2]()` -- a zero-argument getter whose four argument pushes were scheduled BEFORE the call, which reads as a five-argument virtual call and is not one. Fills out[4] and out[5]; nothing downstream of out[3] reads them. The name is INFERRED from the callee's neighbourhood (the trade manager's difficulty multiplier lives at 0x00833938), not from a symbol [unverified] constexpr uint32_t TradeManager_SystemRouteIncome = 0x0043a5b0; // cdecl int (ServerSystem* sys, ServerPlayer* owner, int kind) // the station count both station bonuses read. ConstructionPoints 0x00746830 passes kind = 1 (shipyards); GroupOutput 0x0074b7a0 passes kind = 0 (the imperial output bonus). Body not read this lane; the argument order and the two kinds are read off the two call sites [unverified] constexpr uint32_t ServerSystem_StationCount = 0x00415c10; // thiscall int (StarShip* ship, int kind) // the per-ship repair demand the orbit repair pass sums and then spends against, always called with kind = 1. Body not read; this is the ONE input of ComputeOutput that no save can currently supply, and until it is read the engine takes the demand as 0 -- which reads a colony with a damaged fleet HIGH, because every point the repair pass would have taken is a point that comes back to the money channel instead [unverified] constexpr uint32_t Ship_RepairCost = 0x00415180; // thiscall void (StarShip* ship, int points) // the write half of the orbit repair pass. Body not read [unverified] constexpr uint32_t Ship_ApplyRepair = 0x004151c0; // thiscall bool (StarFleet* fleet, int a, int b) // the fleet-level gate of the orbit repair pass, called as (0, 8); the ship-level counterpart is 0x00814da0 with the same arguments. Body not read [unverified] constexpr uint32_t Fleet_TestFlags = 0x00413ab0; // thiscall void (Game_ShipDesignDef* this, Mars::Stream* s) // THE DESIGN SERIALIZER LANE D SAID DID NOT EXIST. Slot 1 of the ShipDesignDef vftable 0x009fef64. Writes, in DISK order: WriteBool 'FAIDes' this+0x4, WriteBool 'DHide' this+0x5, WriteBool 'DWep' this+0x6 (a BOOL, not an int -- the campaign schema had it as int; byte-neutral because a 4-char tag makes both items 12 bytes), WriteString 'DName' this+0x8, then THREE 'DSec' frames through StreamableHelper at this+0x4c, this+0x24, this+0x74 in that order. THREE sections, not five: the ctor 0x00874c70 runs eh_vector_constructor_iterator(this+0x24, stride 0x28, count 3). MEMORY ORDER != WRITE ORDER: the array is [+0x24, +0x4c, +0x74] and the wire is [+0x4c (command), +0x24 (mission), +0x74 (engine)] [verified] constexpr uint32_t Game_ShipDesignDef_Write = 0x00427390; // thiscall void (Game_ShipDesignDef* this, Mars::Stream* s) // slot 0 of vftable 0x009fef64. Mirrors Write field for field, same tags, same three DSec frames in the same order [verified] constexpr uint32_t Game_ShipDesignDef_Read = 0x00427240; // thiscall void (Game_ShipDesign* this, Mars::Stream* s) // CORRECTS 'Game::ShipDesign::Write (0x008747a0) makes no stream call at all': 0x008747a0 is in NO vftable and is not this class's writer. Game::ShipDesign derives from Game::ShipDesignDef (RTTI 0x00a894a8: ShipDesign, ShipDesignDef, IStreamable, RefCounted, NetworkObject; IStreamable sub-object at +0x9c, NetworkObject at +0xa0). Primary vftable 0x00a32720 slot 1; the +0x9c IStreamable vftable 0x00a32710 reaches it through an adjustor thunk at 0x00874de0. Body: direct call to ShipDesignDef::Write 0x00827390 (the base part), then WriteInt 'Dtc' this+0x134, WriteBool 'Dwgv' this+0x16c, and ONLY IF that flag is set a 'Dwg' frame through StreamableHelper at this+0x170. Dwgv is false in all 11 saves, so the Dwg branch is unexercised (rule 6) [verified] constexpr uint32_t Game_ShipDesign_Write = 0x004325e0; // thiscall void (Game_ShipDesign* this, Mars::Stream* s) // primary vftable 0x00a32720 slot 0. Calls ShipDesignDef::Read, then ReadInt 'DRefCnt' into a NULL destination (read and discarded; the WRITER never emits it, so it is a network-stream field the tag-addressed reader tolerates), then 'Dtc', 'Dwgv' and the conditional 'Dwg' [verified] constexpr uint32_t Game_ShipDesign_Read = 0x004324f0; // thiscall Game_ShipDesign* (Game_ShipDesign* this) // installs ShipDesignDef::vftable at +0, then IStreamable/NetworkObject at +0x9c/+0xa0 and the three ShipDesign vftables. Constructs the DName string at +0x8 and eh_vector_constructor_iterator(this+0x24, 0x28, 3) -- the three section records. Enumerates sizeof(Game_ShipDesignDef) == 0x9c [verified] constexpr uint32_t Game_ShipDesign_ctor = 0x00474c70; // fastcall void (Game_ShipDesign* this) // THE RECOMPUTE THAT PRODUCES BOTH CENSUS WORDS. 16 call sites; one of them stamps this+0x134 (Dtc) and calls straight in. (1) resolves each of the three section records this+0x24/+0x4c/+0x74 to a ShipSectionDef* through the catalog lookup 0x0056edd0 and caches them at this+0xac/+0xb0/+0xb4, IN MEMORY SLOT ORDER; (2) builds a 3-element context array (stride 0x124) and hands it to the aggregator 0x00826af0 together with this+0x130; (3) copies the aggregator's ~0x7c-byte output struct into the design: struct+0x10 -> this+0xb8 (role flags, low dword), struct+0x14 -> this+0xbc (high dword), struct+0x18..+0x64 -> this+0xc0..+0x118, struct+0x74 -> this+0x12c (HULL SIZE), struct+0x78 -> this+0x1a4, struct+0x00..+0x0c -> this+0x138..+0x144. Neither this+0xb8 nor this+0x12c is on the wire: they are rebuilt from the data files whenever a design changes or is loaded [verified] constexpr uint32_t Game_ShipDesign_UpdateDerivedStats = 0x0047e7c0; // cdecl void (DesignStatBlock* out, SectionContext ctx[3], void* techCtx, char flag) // walks the three section contexts (stride 0x124, ctx[i]+0 = the ShipSectionDef*, null = empty slot). out[4]/out[5] (the 64-bit role-flag word) |= ShipSectionDef::GetRoleFlags 0x0056ee80 per section -- an OR, so ONE flagged section flags the whole design. out[0x1d] (hull size) = sectionDef+0x260 -- an ASSIGNMENT, so the LAST resolved section in memory slot order wins; every shipped design is section_class-homogeneous so first-wins and last-wins agree on all 503 design records in the corpus. Also: out[6] x5 when any section carries tech 0x2756; out[4] &= ~0x80 for tech 0x2757; out[4] |= 0x100000 when EVERY section carries tech 0x2742. Default hull health (out[2]) is chosen by the SAME 0x400 bit when no section overrides it at +0x2ac: without 0x400 hull 0/1/2 -> 500/3000/15000, with 0x400 -> 100/500/1000 [verified] constexpr uint32_t Game_ShipDesign_AggregateSectionStats = 0x00426af0; // thiscall unsigned __int64 (Game_ShipSectionDef* this, void* ctx) // returns CONCAT(this+0x29c, this+0x298) -- the section's 64-bit role-flag word straight out of the parsed .shipsection -- with bit 0x20 of the low dword OR-ed in when ctx is non-null, ctx+0xfc is set and this+0x304 > 0. That conditional bit is the only part of the word that is not pure file data [verified] constexpr uint32_t Game_ShipSectionDef_GetRoleFlags = 0x0016ee80; // thiscall Game_ShipSectionDef* (SectionCatalog* this, Game_ShipSectionID* id) // linear scan of the vector at this+0x8/+0xc matching def+0x4 == id->species and def+0x8 == id->sectionId. Returns null for the (0,0) empty slot without scanning [verified] constexpr uint32_t Game_ShipSectionCatalog_FindByID = 0x0016edd0; // cdecl bool (int* out, const char* name) // THE HULL-SIZE DEFINITION. _stricmp against "Destroyer", "Cruiser", "Dreadnought" in that order and stores the index it stopped at: 0, 1, 2. Returns false without writing on no match [verified] constexpr uint32_t Game_ParseShipClassName = 0x0016e1c0; // cdecl bool (int* out, const char* name) // wraps 0x0056e1c0 for the `section_class` key; on failure logs " [%s] unrecognized ship class" and stores 0, so an unknown OR ABSENT section_class is a destroyer, not an error [verified] constexpr uint32_t Game_ShipSectionDef_ParseSectionClass = 0x0016e250; // cdecl void (unsigned __int64* flags, unsigned int loMask, unsigned int hiMask, bool value) // the .shipsection parser's flag setter. Every boolean role key in the section parser 0x005744e0 is one call to this with its own mask pair; `defence_platform` is (lo 0x400, hi 0) at 0x005749b7, `monitor` is (lo 0, hi 0x4), `refinery` 0x8, `mining_capacity` 0x10, `scanrange`/`rebelai_scanrange` 0x20, `gateship` 0x40, `ewar` 0x800, `ramscoop` 0x1000, `aicontrol` 0x2000, `command_quota` 0x10000, `node_bore` 0x20000, `prisoner_capacity` 0x40000, `freighter` 0x80000, `gravboat_bonus` 0x200000, `construction_capacity` 0x400000, `science` 0x1000000, `tradingpost` 0x2000000, `freighterQ` 0x8000000, `police` 0x10000000, `spy` 0x40000000, `spytender` 0x80000000, `refueling_capacity` 0x2, `repair_capacity` 0x4; high dword: `colony_trap` 0x1, `mining_trap` 0x2, `monitor` 0x4, `propaganda` 0x10 [verified] constexpr uint32_t Game_SetRoleFlagBit = 0x0016e780; // cdecl void (ServerPlayer* p, int out[8]) // THE CENSUS. Zeroes out[0..7], then walks the server's fleet vector (p+0x8 -> S, S+0x60/+0x64), keeps fleets whose owner (fleet+0x58) is p, and for every ship in fleet+0xa4/+0xa8 takes design = ship+0x14. If (design+0xb8 & 0x400) == 0: ++out[0] and ++out[2 + design->hullSize(+0x12c)]; else ++out[1] and ++out[5 + hullSize]. So out[0]/out[1] are the two grand totals (computed and DISCARDED by the caller), out[2..4] are ships by hull size 0/1/2 and out[5..7] are defence platforms by hull size. The six land at turnRecord+0x2a..+0x34 and reach the wire as the three `cls` groups' `shpt` and `satt` [verified] constexpr uint32_t Game_ServerPlayer_ShipCensusByHullClass = 0x00418a50; // cdecl int (Game_ShipDesign* design) // a SECOND classifier over the same two words, kept because it shows the flag word is a role set and not a single bit: returns -1 when the design lacks flag 0x80000 (`freighter`), else for hull size 1 returns 0 when 0x8000000 (`freighterQ`) is set and 1 otherwise, and 2 for any other hull size. 0x0082c7c0 is the matching counter over a fleet list. NOT the census -- neither reads 0x400 [unverified] constexpr uint32_t Game_ShipDesign_IsMobileWarshipClass = 0x0041a430; // offset unsigned int // low dword of the design's 64-bit role-flag word, the OR of its sections'. Bit 0x400 = `defence_platform`. High dword at +0xbc. NOT on the wire; rebuilt by 0x0087e7c0 [verified] constexpr uint32_t ShipDesign_off_RoleFlagsLow = 0x000000b8; // offset int // 0 destroyer / 1 cruiser / 2 dreadnought, from the last resolved section's `section_class`. NOT on the wire; rebuilt by 0x0087e7c0 [verified] constexpr uint32_t ShipDesign_off_HullSize = 0x0000012c; // offset int // the wire field `Dtc`, written by ShipDesign::Write and stamped by at least one caller immediately before it calls the recompute 0x0087e7c0 [unverified] constexpr uint32_t ShipDesign_off_Dtc = 0x00000134; // offset int // parsed `section_class`: 0 destroyer / 1 cruiser / 2 dreadnought, 0 when absent or unrecognised [verified] constexpr uint32_t ShipSectionDef_off_SectionClass = 0x00000260; // offset unsigned int // low dword of the section's 64-bit role-flag word (high dword at +0x29c), one bit per boolean role key in the .shipsection file [verified] constexpr uint32_t ShipSectionDef_off_RoleFlagsLow = 0x00000298; // thiscall double (ServerSystem* sys, int groupType) // `ret 4`, real end 0x0074d8e1. The income analogue of PopOutput 0x0074d8f0, and NOT the same law: it sums, over species 0..6, `(double)ftol( (double)GroupIncome(groupType, count) x moraleMod x addictionMod )` -- so the value truncates TWICE per (group, species) row, once inside GroupIncome and once after both factors. moraleMod is 0x00746910 (the same helper the output term uses) and applies to groupType 1 only; addictionMod is the float behind slot 0x00aeca48 when the system's int[7] addiction table at +0x1e4 has a non-zero entry for that species. For groupType 1 and the owner's own species on a non-independent system the count first gains the capacity surplus from two calls to 0x0074a6d0 (out slot 4, then out slot 6), max(0, B - A) [verified] constexpr uint32_t ServerSystem_PopIncome = 0x0034d760; // thiscall double (ServerSystem* sys) // plain `ret`, real end 0x0074b793. groupType 2 of the same loop as PopIncome, over SlaveCount(species) (0x0074b610): no morale factor and no capacity surplus, but the addiction factor still applies. Unexercised: slave counts are 0 on every call in the corpus [verified] constexpr uint32_t ServerSystem_SlaveIncome = 0x0034b700; // thiscall int64 (ServerSystem* sys, int species) // the slave-group population of one species; the group-2 counterpart of GroupPopulation 0x00747ba0 [unverified] constexpr uint32_t ServerSystem_SlaveCount = 0x0034b610; // thiscall void (ServerSystem* sys, int out[12]) // zeroes `out`, returns immediately when the caller's pointer is null or the system has no owner (+0x100), else calls ComputeOutputFromRates(out, &sys->Rts /*+0x88*/) -- the system's OWN rate sliders, not a max-mods vector. THE DISTINCTION THAT MATTERS: ComputeBudget's real (non-projected) per-system money is this function's out[3], while its projected mode and UpdateBankruptcyLimits use ComputeMaxIncome 0x007521c0. They are different numbers: this path funds the science, construction and terraform channels, so the repair pass runs and the unspent-industry and unspent-terraforming cascades into the money channel are live [verified] constexpr uint32_t ServerSystem_ComputeOutput = 0x00351fb0; // thiscall float (StrategyServer* srv, ServerPlayer* p) // `ret 4`, real end 0x0080f49f. Returns float32( float32(DifficultyMods_Select(p->diffMods /*+0x36c*/, p)[1]) x float32(srv->IncMod /*raw base +0xbc, the Sim block's `IncMod` tag*/) ), or just the server modifier when p is null. Every step is stored back through a 4-byte float. `ecx` here is the RAW StrategyServer base (ServerSystem+0x10), four bytes above the base the class's own methods get. THIS IS THE MISSING x1.1: at the difficulty level every corpus save carries (aidf == 1) the AI column of the table is 1.1f [verified] constexpr uint32_t StrategyServer_IncomeDifficultyMod = 0x0040f470; // thiscall float* (DifficultyMods* rec, ServerPlayer* p) // `ret 4`, real end 0x0059b4b3. Returns &rec->f[0] (the AI triple, at +0x04) when p is non-null AND p->[0xf9] (is-AI) is set AND p->NPC (+0xfb) is clear; otherwise &rec->f[3] (the non-AI triple, at +0x10). The three consumers read offset +0 (fleet maintenance divisor, ComputeBudget 0x0086338b), +4 (system and trade-route money, 0x0080f470 and 0x00833938) and +8 (research points bought with money, 0x0080e229 and 0x00863618) [verified] constexpr uint32_t DifficultyMods_Select = 0x0019b490; // cdecl void (int level, DifficultyMods* out) // real end 0x005a3a53 (Ghidra size 193 stops 2 bytes short). memcpy's the default {id 1, 1.0f x6} into `out` FIRST, then builds the table with BuildDifficultyTable 0x005a3870 and linear-searches it for id == level (stride 0x1c, from the 0x92492493 magic divide), copying the six floats on a hit. An out-of-range level therefore yields all ones rather than failing. Called from ServerPlayer::Read 0x008804d0 at 0x00880fa3, gated on 0 <= aidf < 3, which is also where ServerPlayer+0x368 (`aidf`) is stored [verified] constexpr uint32_t LoadDifficultyRow = 0x001a3990; // thiscall vector* (vector* out) // real end 0x005a3989. THE TABLE IS BUILT IN CODE, from .rdata float literals -- no data-file key, no GlobalConst slot, same shape as lane N's pop-type table. Three rows of {int id; float ai[3]; float other[3]} (0x1c): id 0 = ai {1,1,1} / other {1.5,1.5,1.5} (0x00a1b000); id 1 = ai {3.0 (0x00a0451c), 1.1 (0x009f957c), 1.5} / other {1,1,1}; id 2 = ai {1e6 (0x009ebd7c), 1.7 (0x009f9580), 2.0 (0x00a04518)} / other {1,1,1}. Read as: level 0 gives the break to the human player, levels 1 and 2 give it to the AI, and on level 2 the AI's fleet maintenance is divided by a million [verified] constexpr uint32_t BuildDifficultyTable = 0x001a3870; // thiscall float (ServerPlayer* p) // seven bytes: `fld DWORD [ecx+0x30c]; ret`. The save's per-player `IncMod` [verified] constexpr uint32_t ServerPlayer_GetIncMod = 0x0040dd10; // thiscall float (ServerPlayer* p) // `SpeciesDef(p->Species /*+0x5c*/)->+0x24`, the multiplier on the suitability MONEY cost (Zuul 0.7). A data-file value [verified] constexpr uint32_t ServerPlayer_GetSpeciesCostFactor = 0x0040dd20; // thiscall float (StrategyServer* srv, int species) // `ret 4`; one instruction of work: `fld DWORD [ecx + species*4 + 0xf8]`. The per-species ideal-suitability array on the RAW server base. IT IS ON THE WIRE: the Sim block's `ISsp`/`ISsu` pairs are this float[7] in species-index order, and the array is randomised per game by the map generator -- verified against every ServerPlayer's own `IdealSuit` field in all 11 corpus saves. CalcSuitMod reads THIS, not the player's field [verified] constexpr uint32_t StrategyServer_IdealSuit = 0x0040f4b0; // thiscall double (ServerSystem* sys) // real end 0x0074690d. Returns 0 with no owner, else float32(|IdealSuit(species) - sys->Suit|) / |owner->TerraMod (+0x134) x [0x00a1f928] / 20000|. The sign term (-1.0 when the planet's suitability is STRICTLY above the ideal) is multiplied in BEFORE the fabs at 0x00746906 and therefore cancels: the result is always >= 0. That is why the unspent-terraform cascade into the money channel is provably zero under the max-income rate vector [verified] constexpr uint32_t ServerSystem_TerraformPointsNeeded = 0x00346890; // thiscall void (ServerSystem* this, ServerPlayer* p, int encounterId) // 75 B, ret 8. THE NVE WRITER. if (!p) return; s = (int16)this->owner(+0x10)->Frame(+0x8); rec = NVE_map_at(&this->NVE(+0x284), &p->PlyrIdx(+0x28)); rec[0] = (s<<16)|s; rec[1] = encounterId. The map value is 8 bytes at node+0x10: an UNSERIALISED int16 touch stamp at +0, the saved ETS int16 at +2, the saved Eid int32 at +4 -- so ETS and the touch stamp are both set to the frame here, and only the writer at 0x007536a0 makes them differ. Sole caller is the tail's PlayerView-rebuild phase 0x007cf560, under the gate (AFlags >> PlyrIdx) & 1 [mapped] constexpr uint32_t ServerSystem_RecordObservation = 0x00356300; // thiscall void (ServerSystem* this, ServerPlayer* p, NveValue* src) // 70 B, ret 8. Intel sharing. rec = NVE_map_at(&this->NVE, &p->PlyrIdx(+0x28)); rec[0] = (hi16(src[0]) << 16) | (int16)this->owner(+0x10)->Frame(+0x8); rec[1] = src[1]. i.e. the receiver gets the DONOR's sighting turn (ETS) and encounter id unchanged, and only the unserialised touch stamp becomes the current frame. Never executed by any save in the corpus: no save has two players in an alliance [mapped] constexpr uint32_t ServerSystem_CopyObservationTo = 0x003536a0; // cdecl bool (ServerSystem* sys, ServerPlayer* from, ServerPlayer* to) // 96 B. Null-guards all three and from != to; a = FindObservation(from); b = FindObservation(to); if (a && (!b || b->ETS(+2) < a->ETS(+2))) { CopyObservationTo(to, a); return true; } return false. NEWER SIGHTING WINS, compared as a signed int16 [mapped] constexpr uint32_t ServerSystem_ShareObservation = 0x00354d90; // thiscall NveValue* (ServerSystem* this, ServerPlayer* p) // 58 B, ret 4. Map find on p->PlyrIdx(+0x28); returns node+0x10 (the 8-byte value) or null when the search ended at this->NVE head (+0x284) [mapped] constexpr uint32_t ServerSystem_FindObservation = 0x0034d360; // thiscall int (ServerSystem* this, ServerPlayer* p) // 64 B, ret 4. Same map find; returns node+0x14 (Eid) or -1. The extra `!= -0x10` guard is the null-node case reached through node+0x10 [mapped] constexpr uint32_t ServerSystem_LastSeenEncounterId = 0x0034f830; // thiscall bool (ServerSystem* this, ServerPlayer* p) // 34 B, ret 4. return ((1 << (p->PlyrIdx(+0x28) & 0x1f)) & this->AFlags(+0xd4)) != 0. THE gate on the observation record, on the explored sweep and on the PlayerView rebuild -- AFlags, the DERIVED non-sticky union, not VFlags. 19 callers [verified] constexpr uint32_t ServerSystem_IsVisibleTo = 0x00343fb0; // fastcall void (ServerSystem* this) // 41 B, WHOLE BODY. if (this->AFlags(+0xd4) != 0) this->ltis(+0x2c8) = this->owner(+0x10)->Frame(+0x8). This is `ltis`'s writer, which board.md recorded as unnamed; it is driver phase 29 (0x007dcbd6, per system). TShn's writer is still unnamed and is demonstrably a DIFFERENT rule: Spica in turn1-state has AFlags == 0 and its TShn moves anyway [mapped] constexpr uint32_t ServerSystem_UpdateLastObservedTurn = 0x00343ec0; // thiscall void (ServerSystem* this, ServerPlayer* p, bool wasSet, bool on) // 71 B, WHOLE BODY. Game::ServerSystem primary vftable 0x00a2044c SLOT 7 -- this RESOLVES the indirect edge lane B5 flagged in SetExploredBy's tail (vft[0x1c]). if (!wasSet && on && this->owner(+0x10)->Frame(+0x8) > 1) { FUN_00747a20(p->PlyrIdx(+0x28), 1); if (p->PlyrIdx < 0xf) this->+0x2a4 |= 1 << (PlyrIdx & 0x1f); } -- +0x2a4 sits past NVs and is NOT in the serialised table, so this edge writes no save state. Note the Frame > 1 guard: turn 1 is special-cased [mapped] constexpr uint32_t ServerSystem_OnExploredChanged = 0x003480b0; // cdecl bool (StarSystem* sys, int encounterId) // 129 B. Refuses unless the system is unowned (FUN_007437e0 == 0), has no planets, sys->+0x184 == -1 and two further tests pass; then sys->+0x184 = encounterId and ORs a mask from the encounter def into sys->+0x19c. sys->+0x184 is the field ServerSystem_RecordObservation copies into Eid. It is constructed to -1 (StarSystem ctor 0x00752ea0, member index 0x61) and IS NOT ON THE WIRE, so a reimplementation has to recover it from the encounter fleet's FtEnc -- which agrees on all six encounter fleets in the corpus and which no save can separate from the real field [mapped] constexpr uint32_t StarSystem_PlaceEncounter = 0x003887c0; // fastcall void (StrategyServer* S) // 948 B, tail phase 17. Four passes: (1) walk the per-(system, player) view tree at S+0x228 and drop entries whose player can no longer see the system, then clear the tree and reset S+0x22c = 0; (2) per player x per system, if IsVisibleTo, FUN_0075f550; (3) per player x per system, if IsVisibleTo, ServerSystem_RecordObservation(sys, player, sys->+0x184) -- byte-decoded at 0x007cf7a7..0x007cf7ce, this is the ONLY caller of the NVE writer outside intel sharing; (4) per system x per player, if IsExploredBy and a colony exists, build a PlayerView via 0x00755ab0/0x007561d0 and apply it. Draw-free [mapped] constexpr uint32_t StrategyServer_RebuildPlayerViews = 0x003cf560; // cdecl void (const char* gamename) /* GameSpy SDK gsAvailable. sprintf("%s.available.gamespy.com"), inet_addr/gethostbyname, UDP socket, sendto port 27900 (htons 0x6cfc) with '\x09\0\0\0\0' + gamename + NUL, len = strlen(gamename)+6. Overridable hostname buffer at 0x00b085b0. Leaves socket = -1 on DNS failure. */ [verified] constexpr uint32_t GameSpy_GSIStartAvailableCheck = 0x0000a060; // cdecl int (void) /* returns 0=waiting 1=available 2=unavailable 3=temporarily-unavailable. Socket==-1 (start failed) => returns 1. Retries once after 2000 ms then returns 1. */ [verified] constexpr uint32_t GameSpy_GSIAvailableCheckThink = 0x0000a210; // fastcall int (const uint8_t* pkt /*EAX*/, const sockaddr_in* from /*ECX*/, int len, uint32_t* out_status) /* requires len>=7, from.sin_addr/sin_port match, pkt[0..2]==FE FD 09, out = big-endian pkt[3..6]. Returns 0 on match, 1 otherwise. */ [verified] constexpr uint32_t GameSpy_AvailableCheckParseReply = 0x0000a1a0; // cdecl void (void) /* resolves natneg1.gamespy.com / natneg2.gamespy.com; UDP port 27901 (htons 0x6cfd) bound in FUN_00412200 */ [verified] constexpr uint32_t GameSpy_NatNeg_Resolve = 0x00012530; // cdecl int (SOCKET s, int cookie, int clientindex, void* progresscb, void* completedcb, void* userdata) /* only reachable from the server-browser join path and the host side; the direct-address join never calls it */ [verified] constexpr uint32_t GameSpy_NNBeginNegotiationWithSocket = 0x00012590; // cdecl void* (PEERCallbacks* cbs /* 0x74 bytes */) [verified] constexpr uint32_t GameSpy_peerInitialize = 0x00016be0; // cdecl int (void* peer, const char* title, const char* secretKey, const char* sbTitle, const char* sbSecretKey, int sbGameVersion, int sbMaxUpdates, int natNegotiate, const int pingRooms[3], const int crossPingRooms[3]) /* SOTS passes title=sbTitle="swordots", key=sbSecretKey="Z5gR9Z", sbGameVersion=[0x00b2d510], sbMaxUpdates=30, natNegotiate=1 */ [verified] constexpr uint32_t GameSpy_peerSetTitle = 0x00016cc0; // cdecl int (void* peer, SOCKET s, unsigned short port) [verified] constexpr uint32_t GameSpy_peerStartReportingWithSocket = 0x00016a50; // cdecl int (qr2_t* qrec, SOCKET s, int boundport, const char* gamename, const char* secret_key, int ispublic, int natnegotiate, void* serverkey_cb, void* playerkey_cb, void* teamkey_cb, void* keylist_cb, void* count_cb, void* adderror_cb, void* userdata) /* builds "%s.master.gamespy.com", UDP 27900 (htons 0x6cfc); also references 255.255.255.255 for LAN */ [verified] constexpr uint32_t GameSpy_qr2_init_socket = 0x0001d1a0; // cdecl void (int keyid, const char* name) [verified] constexpr uint32_t GameSpy_qr2_register_key = 0x0001e730; // cdecl void* (const char* queryForGamename, const char* queryFromGamename, const char* queryFromKey, int queryFromVersion, int maxConcUpdates, int queryVersion, int lanBrowse, void* callback, void* instance) [verified] constexpr uint32_t GameSpy_ServerBrowserNew = 0x00020060; // cdecl int (void* sb, int startSearch, unsigned short startSearchPort, unsigned short endSearchPort) [verified] constexpr uint32_t GameSpy_ServerBrowserLANUpdate = 0x00020160; // cdecl void (void* sb) [mapped] constexpr uint32_t GameSpy_ServerBrowserHalt = 0x00020110; // thiscall int (SBServerList* this) /* master index = (unsigned)(fold over tolower(c): h = c - h*0x63306ce7) % 20; sprintf("%s.ms%d.gamespy.com"), TCP port 28910 (htons 0x70ee). Override hostname global at 0x00b09440. For gamename "swordots" the index is 5. */ [verified] constexpr uint32_t GameSpy_SBServerListConnect = 0x00020840; // cdecl int (...) /* passes "peerchat.gamespy.com", TCP port 6667 (0x1a0b) to the socket-connect helper at 0x00408fe0 */ [verified] constexpr uint32_t GameSpy_chatConnectPeerchat = 0x00017ab0; // thiscall void (AutoJoin* this /*0x90 bytes*/, const void* params, const char* address) /* vftable 0x00a218f4; address std::string at +0x68 */ [verified] constexpr uint32_t Game_AutoJoin_ctor = 0x0036c6a0; // thiscall bool (AutoJoin* this) /* vftable slot 2. Runs GSIStartAvailableCheck/Think, stores availability at +5, then AutoJoin_InitServerBrowser */ [verified] constexpr uint32_t Game_AutoJoin_Think = 0x00378a90; // thiscall bool (AutoJoin* this) /* refuses when this->available (+5) == 0; peerInitialize + peerSetTitle("swordots","Z5gR9Z") */ [verified] constexpr uint32_t Game_AutoJoin_InitPeer = 0x003617f0; // thiscall bool (GameBrowserPanel* this) /* gated on this+0x8a (availability); peerInitialize + peerSetTitle */ [verified] constexpr uint32_t Game_GameBrowserPanel_InitPeer = 0x00378c40; // thiscall GameBrowserPanel* (GameBrowserPanel* this, ...) /* calls Game_RegisterQR2Keys then GSIStartAvailableCheck(gamename) */ [mapped] constexpr uint32_t Game_GameBrowserPanel_ctor_tail = 0x0037e210; // thiscall void (LANPage* this) /* vftable slot 18. ServerBrowserNew("swordots","swordots","Z5gR9Z", 0, 30, 1, lanBrowse=1, cb, this) into this+0xb9c. NO availability gate. */ [verified] constexpr uint32_t Game_GameBrowserPanel_LANPage_CreateBrowser = 0x0037dde0; // thiscall void (LANPage* this) /* ServerBrowserHalt then ServerBrowserLANUpdate(sb, 1, g_LanScanPort, g_LanScanPort + g_LanScanPortRange) */ [verified] constexpr uint32_t Game_GameBrowserPanel_LANPage_Refresh = 0x0037de50; // thiscall void (GameBrowserPanel* this) /* news a 0x166c-byte ManualJoinDialog (ctor 0x0077c790, vftable 0x00a22604) and binds Game_ManualJoin_OnAccept as its handler */ [verified] constexpr uint32_t Game_GameBrowserPanel_ShowManualJoinDialog = 0x0037e590; // cdecl void (void* sender, int action, GameBrowserPanel* self) /* validates the typed address with Game_ParseHostAddress then App_StartJoin(params, address, serverBrowser = NULL) -- the GameSpy-free join */ [verified] constexpr uint32_t Game_ManualJoin_OnAccept = 0x0036ef90; // cdecl bool (std::string* out_host, int* out_port, const char* text) /* sscanf "%d.%d.%d.%d:%d%1s" must yield 4 or 5 fields with every octet <= 255, then sscanf "%[^:]:%d"; out_port = -1 when absent. Dotted-quad only -- hostnames are rejected. */ [verified] constexpr uint32_t Game_ParseHostAddress = 0x004f64d0; // thiscall bool (App* this, const void* params, const char* address, void* serverBrowser) /* news Game::StrategyJoin (0xc8 bytes) into this+0x158; serverBrowser may be NULL */ [verified] constexpr uint32_t App_StartJoin = 0x00499e30; // thiscall StrategyJoin* (StrategyJoin* this, const void* params /*0x58 by value*/, const char* address, void* serverBrowser) /* vftable 0x00a21904. serverBrowser == NULL => state (+0x80) = 1, direct connect; else state 0, ServerBrowserAuxUpdateIP + NAT negotiation */ [verified] constexpr uint32_t Game_StrategyJoin_ctor = 0x00377b10; // thiscall bool (StrategyJoin* this) /* vftable slot 2; jump table at 0x00765948 for states 1..5 */ [verified] constexpr uint32_t Game_StrategyJoin_Think = 0x00365810; // thiscall bool (StrategyJoin* this, void* serverBrowser) /* ServerBrowserAuxUpdateIP-style direct query then NNBeginNegotiationWithSocket */ [verified] constexpr uint32_t Game_StrategyJoin_QueryViaServerBrowser = 0x00370e70; // thiscall bool (App* this, const char* address, const char* playerName) /* the direct-connect entry reached from StrategyJoin state 1; news a 0x130-byte client into this+0x14c and stamps the version word from 0x00b2d510 */ [verified] constexpr uint32_t App_ConnectToStrategyHost = 0x0049a7e0; // thiscall bool (GameBrowserPanel* this) /* picks private vs public SBServer address; logs "same NAT" / "behind NAT, but can still connect directly" / "requires NAT negotiation"; reads SBServerGetIntValue(server, "password", 0) */ [verified] constexpr uint32_t Game_OnJoinGame_ChooseAddress = 0x00372f60; // thiscall bool (StrategyHost* this) /* refuses when this+0x24e (availability) == 0; peerInitialize with a 0x74-byte callback table then peerSetTitle */ [verified] constexpr uint32_t Game_StrategyHost_InitPeer = 0x003c1ee0; // thiscall void (StrategyHost* this) /* if this+0x2c == 1 && this+0x30 != 0: qr2_init_socket(..., ispublic = 0) -- LAN reporting, no master heartbeat. Otherwise StrategyHost_InitPeer then peerStartReportingWithSocket. */ [verified] constexpr uint32_t Game_StrategyHost_StartReporting = 0x003c2dd0; // cdecl void (void) /* 16 qr2_register_key calls: 50..57 slot0..slot7, 58 numslots, 59 mapshape, 60 numsys, 61 turn, 62 scenario, 63 settings, 64 slot_, 65 ranks */ [verified] constexpr uint32_t Game_RegisterQR2Keys = 0x00498bf0; // thiscall void (Config* this) /* reads ini section [Network]: SyncCheckStrategy=True, SyncCheckCombat=True, SyncLogStrategy=False, SyncLogCombat=False, HostPort=3369, CombatHostPort=3370, LanScanPort=3369, LanScanPortRange=1, HeartbeatPeriod=15000, ConnectionTimeout=45000, MaxTxMessageSize=512, CombatLatency=1000 */ [verified] constexpr uint32_t Game_LoadNetworkConfig = 0x001a0610; // thiscall bool (App* this) /* GetCommandLineW + CommandLineToArgvW; matches the literal "/join", takes the next argv as the address and the one after as an optional second string, then constructs Game::AutoJoin into this+0x158 */ [verified] constexpr uint32_t Game_ParseJoinCommandLine = 0x0049d280; // data const char* /* -> "swordots" at 0x00a35c50 */ [verified] constexpr uint32_t g_GameSpyGameName = 0x00635cd4; // data const char* /* -> "Z5gR9Z" at 0x00a35c48 */ [verified] constexpr uint32_t g_GameSpySecretKey = 0x00635cd8; // data uint32_t /* packed build word; bits 16..23 = minor<<4 | major parsed from "1.8.1", byte 1 = edition flags, low nibble = build flavour. Passed as peerSetTitle sbGameVersion and stamped into the join handshake. */ [verified] constexpr uint32_t g_GameVersionWord = 0x0072d510; // data uint16_t /* [Network] LanScanPort, default 3369 */ [verified] constexpr uint32_t g_LanScanPort = 0x00713c6c; // data uint32_t /* [Network] LanScanPortRange, default 1; LAN sweep is [LanScanPort, LanScanPort+Range] */ [verified] constexpr uint32_t g_LanScanPortRange = 0x00713c70; // data char[] /* when non-empty, replaces ".available.gamespy.com" in the availability check */ [mapped] constexpr uint32_t g_GameSpyAvailableHostOverride = 0x007085b0; // data const char* /* when non-NULL, replaces ".ms.gamespy.com" in SBServerListConnect */ [mapped] constexpr uint32_t g_GameSpyMasterHostOverride = 0x00709440; // thiscall int (void* this, int, int) // the ONE callee through which Game::ServerSpyManager vftable 0x00a3073c slot 13 (0x008877b0) reaches the strategic generator. Exactly ONE incoming reference in the image: an unconditional call at 0x00887af2 inside that slot. Real body 0x008408e0..0x00840a59, 370 bytes. Contains three of lane V2's eight draw sites -- Chance 0x00840929, NextInt 0x008409c7, Chance 0x00840a3c -- all loading the generator as [reg+0x16c]. Named for the verified relationship (which slot reaches it, and that it draws), NOT for any semantics: its body was not read. Lane H hooks it with a register-transparent entry counter so that 'the spy path never fired' can be split into 'slot 13 was not entered' and 'slot 13 was entered and gated above this call' [mapped] constexpr uint32_t SpyManager_Slot13RngCallee = 0x004408e0; // thiscall uint (void* fleet, char) // first of the two callees through which Game::ServerTradeManagerImpl vftable 0x00a31b74 slot 13 (0x0088ef80) reaches the strategic generator. Exactly ONE incoming reference: an unconditional call at 0x0088f036. Real body 0x00820ca0..0x00820e53, 436 bytes, containing lane V2's NextFloat site 0x00820e18. Its own body loops the vector at arg+0xa4/+0xa8 testing per-element flags, and its boolean result is what gates the call to TradeManager_Slot13RngCalleeB one instruction later. THE CALLER'S LOOP IS OVER THE FLEETS VECTOR at GetServer()+0x64/+0x68, not over a trade-route list -- which is why 'the tail draws nothing because we have no trade routes' does not explain this slot [mapped] constexpr uint32_t TradeManager_Slot13RngCalleeA = 0x00420ca0; // thiscall void (void* this, int) // second of the two callees through which Game::ServerTradeManagerImpl slot 13 (0x0088ef80) reaches the strategic generator, called at 0x0088f042 ONLY when TradeManager_Slot13RngCalleeA returned non-zero. Exactly ONE incoming reference. Real body 0x0088b440..0x0088b976, 1323 bytes, containing lane V2's NextInt site 0x0088b613 (`mov ecx,[ecx+0x16c]; add ecx,4`). The two form a two-stage gate: A draws first and decides whether B runs, so a run where A fires and B does not is a MEASUREMENT, not a gap [mapped] constexpr uint32_t TradeManager_Slot13RngCalleeB = 0x0048b440; // thiscall uint32_t (Mars::RNG* this /*ecx = THE OBJECT, not &mt*/) // plain RET, no stack args. THE FOURTH DRAW ENTRY POINT. Whole 84-byte body read from the instruction stream: `cmp [ecx+0x9c8],0; push esi; lea esi,[ecx+4]; jne skip; mov ecx,esi; call RNG_Twist; skip: eax=[esi+0x9c0]; dec [esi+0x9c4]; ecx=*eax; eax+=4; [esi+0x9c0]=eax;` then the standard Mars temper (shr 11 / and 0xff3a58ad shl 7 / and 0xffffdf8c shl 15 / shr 18) and `ret`. EXACTLY ONE MT WORD, UNCONDITIONAL -- no rejection loop, no early-out, no branch except the lazy twist. Contrast RNG_NextFloat and RNG_NextInt, which are entered with ECX = &mt = obj+4; this one takes the object and does the +4 itself. Body ends 0x004f76c3 (Ghidra's 84 is correct here), then 12 int3 to 0x004f76d0. 11 callers image-wide; in StrategyServer::ProcessTurn's direct-call closure at DEPTH 4 via ProcessFleetMovement 0x007da9a0 -> MoveFleet 0x007d9ee0 -> ProbabilisticJump 0x007b6700 @0x007b67e7 [verified] constexpr uint32_t Mars_RNG_NextUInt = 0x000f7670; // thiscall float (Mars::RNG* this /*ecx = THE OBJECT*/, float lo, float hi) // RET 8. FIFTH DRAW ENTRY POINT, in no previous lane's primitive set. `add ecx,4; call RNG_NextFloat` then `lo + (float)((hi-lo) * unit)`, with the product STORED TO A FLOAT before the add and the sum stored to a float again -- two roundings, both must be reproduced. EXACTLY ONE MT WORD. In StrategyServer::ProcessTurn's closure at depth 3 via ServerPlayer::ProcessTurn -> 0x00889dc0 (call sites 0x0088a1bd, 0x0088a20f) [verified] constexpr uint32_t Mars_RNG_FloatRange = 0x0007d8a0; // cdecl int (Mars::RNG* rng /*STACK arg, the object*/, int lo, int hi) // plain RET. SIXTH DRAW ENTRY POINT. h = hi - lo; half = h/2 truncated toward zero (the `cdq; sub eax,edx; sar 1` idiom, so negative h rounds toward zero not down); returns lo + NextInt(half) + NextInt(h - half), both NextInt calls entered with ECX = rng+4 and both taking the bound BY POINTER. TRIANGULAR, not uniform. AT LEAST TWO MT WORDS -- each NextInt carries its own rejection loop. Not reachable from any turn driver by a direct call [verified] constexpr uint32_t Mars_RNG_IntRangeBell = 0x004e6d80; // cdecl int (Mars::RNG* rng /*STACK arg, the object*/, int lo, int hi, int mode) // plain RET. SEVENTH DRAW ENTRY POINT and the only one whose cost is UNBOUNDED. Box-Muller with rejection; BOTH draws are INLINED (temper chains at 0x008e6eb8 and 0x008e6f34), so no call-graph RNG sweep sees them and the only edges left are two bare RNG_Twist calls. half = (hi-lo)/2; mean = 2.1 * ((mode-lo)/half - 1). Per attempt: z = sqrt(-2 * ln(1 - (y1 + 0.5) * 2^-32)) * cos(2*pi * y2 * 2^-32) + mean; REJECT and redraw while z > 2.1 or z < -2.1 (back-edges 0x008e6f8d and 0x008e6fa0 -> 0x008e6e7f). Result = lo + ftol(((z + 2.1) / 4.2) * (hi - lo)). TWO MT WORDS PER ATTEMPT. NOTE THE DIVISOR: this path scales by 2^-32 (0x00a3b6f0), NOT the 1/(2^32-1) at 0x009e61b0 that RNG_NextFloat uses -- the two are different constants in the same image. Three callers (0x00786200, 0x00786230, 0x00798040); not reachable from any turn driver by a direct call [verified] constexpr uint32_t Mars_RNG_GaussianRange = 0x004e6e30; // thiscall void (EncounterDetectCtx* this, std::vector>* outBuckets, std::vector* detectors, std::vector* contacts) // RET 0xc. THE ONLY GAME FUNCTION WITH AN INLINED MT DRAW IN StrategyServer::ProcessTurn's CLOSURE (depth 4). this = {+0x00 StrategyServer* S, +0x04 TechDef* id 0x2729, +0x08 TechDef* id 0x2728}. For each contact (outer loop over `contacts`, index ebx) it walks `detectors` (inner loop, index edi) and draws ONE inlined NextFloat from S->rng (S+0x16c) PER (contact, detector) TRIAL, BEFORE the accept test. thresh = 0.25f (0x00a23a6c) if TechTree_HasTechComplete(detector->+0xf4, this->+0x8) or (detector->+0xf4, this->+0x4), else 0.0f; r = (float)unit; ACCEPT iff thresh >= r, and on accept the contact is pushed into outBuckets[detector] and the outer loop moves on. A detector with neither tech BURNS A WORD AND CAN NEVER ACCEPT (0.0f >= r only when the word is 0). WORDS PER CALL = sum over contacts of min(trials-to-first-accept, |detectors|); all-teched mean = |contacts| * 4 * (1 - 0.75^|detectors|), none-teched = |contacts| * |detectors| exactly. A vector 'tried' bitset at [ebp-0x38] (|contacts| words, bit = detector index) and a vector 'assigned' at [ebp-0x4c] are NEVER cleared, so the repeat-until-no-progress outer loop at 0x007aa583 cannot redraw a pair and always terminates after at most two passes. RULE 17: Ghidra reports 944 bytes (end 0x007aa5f0, mid-instruction); the real body ends at 0x007aa5f9 -- the std::vector length_error throw stub at 0x007aa5ee/0x007aa5f3 is outside the reported range [verified] constexpr uint32_t EncounterDetect_AssignContacts = 0x003aa240; // site site in EncounterDetect_AssignContacts: the INLINED Mars::RNG::NextFloat. `mov esi,[edx+0x16c]; cmp [esi+0x9c8],0; jne +8; lea ecx,[esi+4]; call RNG_Twist` -- the Twist call is the LAZY TWIST INSIDE NextFloat, not a bare Twist. The temper runs 0x007aa3e1..0x007aa407, the fild/+2^32/*1-over-(2^32-1) 0x007aa40c..0x007aa41d. THIS IS THE SITE RULE 16 WAS WRITTEN FOR: the only call-graph edge it leaves is EncounterDetect_AssignContacts -> RNG_Twist [verified] constexpr uint32_t EncounterDetect_AssignContacts_Draw = 0x003aa3b6; // site site in EncounterDetect_AssignContacts: `fcompp; fnstsw ax; test ah,5; jp 0x007aa45a`. st0 = thresh, st1 = r. ah&5 is 0 when thresh > r, 0 when thresh == r, 1 when thresh < r, 5 when unordered; PF is even for 0 and 5, so the jp is TAKEN (ACCEPT) iff thresh >= r or unordered, and falls through to the next-detector path iff thresh < r. EQUALITY ACCEPTS. Derive the branch from the ISA, not from the mnemonic (rule 10) [verified] constexpr uint32_t EncounterDetect_AssignContacts_AcceptTest = 0x003aa435; // site site in EncounterDetect_AssignContacts: `cmp BYTE [ebp-0xd],0; je 0x007aa2d0` -- the repeat-until-no-progress back-edge that rule 17 exists to make you look for. [ebp-0xd] is set to 1 at 0x007aa2da at the top of each pass and cleared at 0x007aa360 by any (contact, detector) pair that is evaluated. It is NOT an unbounded loop: the 'tried' bitset it consults is never reset, so the second pass evaluates nothing, clears nothing, and the flag stays 1. At most two passes, and no pair is ever drawn for twice [verified] constexpr uint32_t EncounterDetect_AssignContacts_OuterBackEdge = 0x003aa583; // site the REAL end of EncounterDetect_AssignContacts. Ghidra reports sizeInBytes 944, i.e. an end of 0x007aa5f0, which falls inside the 5-byte `push 0x9e1f90` at 0x007aa5ee. The body's last instruction is the `call ds:0x9dd150` (std::vector length_error throw) at 0x007aa5f3, ending 0x007aa5f9; int3 padding runs to the next function start 0x007aa600. Real size 953. Rule 17 [verified] constexpr uint32_t EncounterDetect_AssignContacts_RealEnd = 0x003aa5f9; // thiscall void (EncounterDetectCtx* this, TeamRecord* rec /*the 0x74-byte record StrategyServer::DetectEncounters builds*/) // RET 4. THE ONLY CALLER OF EncounterDetect_AssignContacts. Gate at 0x007ca671: 0x007892d0(rec) is true iff some entry of rec->(+0x28..+0x2c) (stride 0x44) has entry[0]->+0xfc != 0; false -> whole function is a no-op and NO WORD IS DRAWN. Then `detectors` = 0x007949b0(rec) (entries whose object has +0xfc == 0 AND +0xfb == 0) and `contacts` = 0x00791460(rec) (entries whose object has +0xfc != 0); either empty -> return, still no draw. Otherwise buckets = vector>(|detectors|) via 0x007b77c0, then the draw call at 0x007ca73a [verified] constexpr uint32_t EncounterDetect_ProcessTeamRecord = 0x003ca640; // thiscall EncounterDetectCtx* (EncounterDetectCtx* this, StrategyServer* S, std::vector* records) // RET 8, returns this in EAX. Constructs the 12-byte context {S, TechDef*(id 0x2729), TechDef*(id 0x2728)} -- the tech lookup is 0x0057d610(g_0x00b2d540->+0x110, id) -- then calls EncounterDetect_ProcessTeamRecord once per 0x74-byte record (the 0x8d3dcb09 / sar 6 divide-by-0x74 idiom). Called from StrategyServer::DetectEncounters 0x007d7f70 at 0x007d8470, i.e. inside the LAST phase of StrategyServer::ProcessTurn. This is the ONLY path by which a game-code inlined MT draw is reachable from ProcessTurn [verified] constexpr uint32_t EncounterDetect_Run = 0x003cb080; // thiscall bool (TechTree* this, TechDef* def) // RET 4. `if (!def) return false; node = this->+0x10[def->+0x00]; return node != NULL && node->+0x14 == 4;` -- state 4 is 'researched'. 28 callers image-wide. EncounterDetect_AssignContacts uses it to choose between the 0.25f detection threshold and 0.0f [verified] constexpr uint32_t TechTree_HasTechComplete = 0x0017d7e0; // site site in ProbabilisticJump: the SECOND draw, `mov ecx,[edx+0x16c]; call Mars_RNG_NextUInt`. Note ECX is the RNG OBJECT here while the NextFloat at 0x007b677c three dozen bytes earlier is entered at rng+4 -- two conventions on the same generator in one function. It is reached only when the arrival test at 0x007b67ae..0x007b67b5 FAILS (i.e. fleet->+0x58->+0x158 < float(NextFloat() * fleet->+0x58->+0x154)); on the arriving branch the function copies the destination position and never draws again. ONE EXTRA MT WORD. The word is handed to 0x008a6fa0, which returns a pointer to three floats used to scatter the fleet's position around the destination [verified] constexpr uint32_t ProbabilisticJump_NextUIntDraw = 0x003b67e7; // site NOT AN RNG SITE, recorded so nobody re-derives it. An image-wide scan for the tempering immediates reports 0x008cca30 as containing 0xffffdf8c, but the four bytes at 0x008cca90 are the rel32 displacement of `call 0x008caa20` (e8 8c df ff ff), not an `and r32,imm32`. FUN_008cca30 has no temper chain: no 0xff3a58ad, no `shr r32,0xb`, no shl 7/15/18. It appears in combat-resolver.md §0.1's list of 14 inlined-draw functions and must be struck from it [verified] constexpr uint32_t InlinedDrawScan_FalsePositive_008cca30 = 0x004cca8f; // constant 0xff3a58ad -- the first tempering mask AS THE IMAGE SPELLS IT, `y ^= (y & 0xff3a58ad) << 7`. This is the TEXTBOOK MT19937 tempering with the mask applied BEFORE the shift rather than after: (y & 0xff3a58ad) << 7 == (y << 7) & 0x9d2c5680, because the mask bits above bit 24 shift past bit 31 and are dont-cares (0x9d2c5680 >> 7 == 0x013a58ad == 0xff3a58ad & 0x01ffffff). Verified over 200,000 random words. Mars::RNG is stock MT19937 -- our mars::rng model is NOT wrong -- but a scan for the textbook constants finds NOTHING in this image, which is why an inlined-draw sweep must scan for THIS value at real instruction boundaries (rule 16). 33 occurrences inside decoded instructions image-wide, all genuine `and r32,imm32` in a temper chain [verified] constexpr uint32_t MT_TemperMask1 = 0xff3a58ad; // constant 0xffffdf8c -- the second tempering mask as the image spells it, `y ^= (y & 0xffffdf8c) << 15`, equal to the textbook `(y << 15) & 0xefc60000` (0xefc60000 >> 15 == 0x0001df8c == 0xffffdf8c & 0x0001ffff). Image-wide there are 34 occurrences of these bytes inside decoded instructions; 33 are genuine and ONE (0x008cca90) is the rel32 displacement of a call. Always require BOTH masks plus a preceding `shr r32,0xb` before calling a hit a draw [verified] constexpr uint32_t MT_TemperMask2 = 0xffffdf8c; // thiscall void (CombatResolveContext* this) // PLAIN RET, no stack args. THE COMBAT RESOLVER under phase 6 of StrategyServer::OnAllCombatDone_Tail. Exactly one caller: StrategyServer::ApplyEncounterResult 0x007d8920 at 0x007d8d24, on the full-battle path only (res->+0x4 == 0). REAL BODY IS 0x007d5af0..0x007d78c8 = 7641 B; Ghidra's 7499 stops mid-instruction at 0x007d783b. SHAPE, read from the instruction stream: (1) prologue + 16 unconditional this-calls 0x007d5b1e..0x007d5c02; (2) ONE loop over enc->members, stride 0x44, 0x007d5c30..0x007d779d -- 7021 of the 7641 bytes, with four inner loops and no other outer control flow; (3) five more unconditional this-calls 0x007d77a3..0x007d77cd; (4) a victor block gated on ctx->+0xa34 != -1; (5) FUN_0079c740 and the epilogue. ONLY EIGHT NON-STACK STORES IN THE WHOLE BODY and exactly ONE indirect call (inside a _CxxThrowException path): it composes and posts per-player events and delegates every state mutation to callees. Strings it composes: EVENT__FIGHT, EVENTSUM_, EVENTMSG_<...>, EVENT_TRADERAIDERS, EVENT_COMBAT_OBSERVED, EVENT_DEFEAT, EVENT_VICTORY, EVENT_ENGAGED, EVENT_STATION_KILLED, and the ENTITYVICTORY/ENTITYDEFEAT/UNRESOLVED outcome tokens. DRAWS NO RNG ITSELF -- see CombatResolve_NodeCannon and CombatResolve_SalvageBackEng [verified] constexpr uint32_t CombatResolver_Run = 0x003d5af0; // thiscall CombatResolveContext* (CombatResolveContext* this, StrategyServer* S /*the S frame*/, Encounter* enc, Game::EncounterResults* res) // built as a ~0xea0-byte STACK local at [ebp-0xea0] in StrategyServer::ApplyEncounterResult, immediately before the resolver call. Field assignment read from the instruction stream: this->+0x00 = S; this->+0x04 = operator new(0x5c) then FUN_005a13f0 (a per-player lookup object); this->+0x08 = enc; this->+0x0c = res; this->+0x10 = 0; this->+0x14 = 0 (byte); this->+0x18 = an empty std::string; this->+0x38 = FUN_00536890. Further fields the resolver uses: +0x290/+0x330/+0x430 per-player int arrays indexed by PlyrIdx*4; +0x7b0 + PlyrIdx*0x10 a per-player vector; +0x9b0 + PlyrIdx*4 the posted-event pointer; +0xa34 the winner PlyrIdx (-1 = none); +0xe7c = FUN_00787690(enc), set by the resolver's first act [verified] constexpr uint32_t CombatResolveContext_Ctor = 0x003b8460; // thiscall void (CombatResolveContext* this) // RNG SITE 1 OF 3 in the combat resolver. Exactly one caller (the resolver, unconditionally, at 0x007d5be2), so it runs ONCE PER RESOLVED BATTLE. Body: return with NO DRAW if res(ctx+0xc)->+0xa8 == +0xac (the flung-entity vector is empty); else build a candidate destination list = every StarSystem in S->Systems (S+0x44/+0x48) with sys->+0xc5 == 0 and sys != enc->+0xc, sort it by distance from enc->+0x1c..+0x24 (FUN_00796590 with a 16-byte functor), TRUNCATE TO 3 (FUN_00459f70); if the list is non-empty draw EXACTLY ONE RNG_NextInt at 0x007bb69b with ecx = S->+0x16c + 4 and bound n = count-1 passed BY POINTER. NextInt is inclusive on [0,n], so a normal galaxy gives n = 2, mask = 3, and a y&3 == 3 draw is rejected: 1 MT word with p = 3/4, 2 with p = 3/16, mean 4/3 words. Then per flung entity HandleMap::Resolve(S+0x84, h) and FUN_007bb420 groups by (destination, obj->+0x10) into a 0x18-stride vector; the tail posts EVENT_NODECANNON_FLINGS and EVENT_NODECANNON_KILLS with no further draw [verified] constexpr uint32_t CombatResolve_NodeCannon = 0x003bb530; // thiscall void (CombatResolveContext* this) // RNG SITES 2 AND 3 in the combat resolver. Exactly one caller (the resolver, unconditionally, at 0x007d77c8, AFTER the per-member loop), so it runs once per resolved battle -- but ITS OWN BODY IS A LOOP OVER THE ENCOUNTER MEMBERS, so the rolls are per combatant. WARNING: Ghidra sizes it 2670 B, ending at 0x007a89be, which CUTS OFF the outer back-edge at 0x007a89ab; the real body ends at 0x007a89cd and a dump that stops at Ghidra's size makes the whole outer loop read as straight-line code operating on members[0]. Structure: (1) 0x007a7f79..0x007a7f88 zero 32 slots of 0x10 at [ebp-0x310], a per-PlyrIdx 3-float salvage stat; (2) 0x007a7fc0..0x007a8071 per member, gate FUN_00787350(player), fill that player's slot via FUN_0078bcf0(ctx, player, slot), sticky flag [ebp-0x39d]; return with ZERO draws if the flag is clear (0x007a807e) or the member vector is empty (0x007a80a6); (3) OUTER LOOP 0x007a80ac..0x007a89ab over members, counter [ebp-0x3c4] -- per member: player = S->Players[m->+0x28], slot = [ebp-0x310] + PlyrIdx*0x10, skip with no draw if all three floats are 0.0 (0x007a8105), RESET the {void* def; float p} candidate vector [ebp-0x3d4]/[ebp-0x3d0]/[ebp-0x3cc] at 0x007a810d, rebuild it in the inner loop 0x007a8150..0x007a8406, then (4) ROLL LOOP 0x007a8452..0x007a8693 -- one INLINED Mars::RNG::NextFloat (see CombatResolve_SalvageBackEng_RollSite) per candidate whose def != 0, whose p > 0.0f, and for which FUN_0078f530(player, def, &out) is true; success is p >= roll (fcom + test ah,1, so equality succeeds); on success it composes "SPRJ_BACKENG_" + def->+0x20 and calls SpecialProject_UnlockRandomForPlayer, which draws one more word. No other RNG in the body: no direct call to RNG_NextInt/NextFloat/Chance and no second inlined draw anywhere in the 2782 real bytes [verified] constexpr uint32_t CombatResolve_SalvageBackEng = 0x003a7f30; // site AN INLINED Mars::RNG::NextFloat, byte-for-byte the body of RNG_NextFloat 0x0047d830: esi = S->+0x16c; if (esi->+0x9c8 /*left*/ == 0) RNG_Twist(esi+4) at 0x007a84cf; y = *esi->+0x9c4 /*next*/, next += 4, left--; temper with 0xff3a58ad and 0xffffdf8c; fild with the +2^32 fixup at 0x009e61b8, multiply by the 1/(2^32-1) double at 0x009e61b0, store as float32. THE CAMPAIGN'S RNG SWEEPS CANNOT SEE THIS: the only call-graph edge it leaves is FUN_007a7f30 -> RNG_Twist, which reads as a bare Twist and is not one. An image-wide instruction-boundary scan for the two tempering immediates finds FOURTEEN game functions with inlined MT draws besides the four RNG primitives: 0x004b1f20 (x4), 0x004f7670, 0x00507ac0 (x12), 0x005232a0, 0x006ec720, 0x006f65f0, 0x006f7890, 0x0079f7d0, 0x007a7f30, 0x007aa240, 0x007c2fa0 (x4), 0x007c4140, 0x008cca30, 0x008e6e30 (x2). Of those, 0x004f7670 and 0x007aa240 are in the direct-call closure of StrategyServer::ProcessTurn and 0x007a7f30 is in the closure of OnAllCombatDone_Tail -- three strategic-turn RNG sources that no call-graph accounting has counted [verified] constexpr uint32_t CombatResolve_SalvageBackEng_RollSite = 0x003a84bd; // thiscall std::string* (StrategyServer* S /*ecx, the S frame*/, std::string* outName, std::string* keyPrefix, ServerPlayer* player) // RET 0xc. If player == 0 it returns an empty string with NO DRAW; otherwise it forwards to SpecialProject_PickRandomAvailable(S->+0x160, outName, keyPrefix, player->PlyrIdx(+0x28), S->+0x16c). Reached only from CombatResolve_SalvageBackEng on a successful back-engineering roll, with keyPrefix = "SPRJ_BACKENG_" + the destroyed design's tag [verified] constexpr uint32_t SpecialProject_UnlockRandomForPlayer = 0x003a0540; // thiscall std::string* (void* projectMgr /*= StrategyServer+0x160*/, std::string* outName, std::string* keyPrefix, int plyrIdx, Mars::RNG* rng) // RET 0x10. RNG SITE 3 in the combat resolver's subtree. Builds a candidate vector via FUN_0059ec00 from keyPrefix; IF IT IS EMPTY it returns an empty string with NO DRAW; otherwise draws EXACTLY ONE RNG_NextInt at 0x00852ec7 (ecx = rng+4, bound n = count-1 by pointer, inclusive), marks the chosen element's per-player byte at elem[plyrIdx]++ and returns its name via FUN_008c97a0. Because the bound is inclusive and NextInt rejects on (y & mask) > n, the expected MT-word cost is 2^ceil(log2(count)) / count -- exactly 1 only when count is a power of two [verified] constexpr uint32_t SpecialProject_PickRandomAvailable = 0x00452d30; // thiscall Game::TacReport* (Game::CombatPlayerStats* this, int i) // RET 4. return (Game::TacReport*)(this->+0x04 + i * 0x94). Called by the combat resolver's inner loop B at 0x007d6f43 [verified] constexpr uint32_t CombatPlayerStats_TacReportAt = 0x00056c40; // thiscall int (Game::CombatPlayerStats* this) // plain RET. return (this->+0x08 - this->+0x04) / 0x94, by the 0xdd67c8a7 add-back / sar 7 reciprocal. Called by the combat resolver at 0x007d6f2b and 0x007d702d [verified] constexpr uint32_t CombatPlayerStats_TacReportCount = 0x00056c20; // layout sizeof(Game::TacReport) -- container stride, ENUMERATED TWICE and independently: the `imul eax,eax,0x94` in CombatPlayerStats_TacReportAt 0x00456c46, and the 0xdd67c8a7 add-back / sar 7 reciprocal divide in CombatPlayerStats_TacReportCount 0x00456c27. NOT sized by what the code touches. FLAG: objects/streams.json gives Game::TacReport 20 fields (two embedded Game::TacReportEvents of 0x20 each plus 18 scalars) which with a vptr accounts for at most 0x8c, so roughly 8 bytes are members the serializer never names -- carried, not named, in the sense of earned-rule 7. Not resolved here [verified] constexpr uint32_t sizeof_Game_TacReport = 0x00000094; // layout Game::CombatPlayerStats+0x04/+0x08/+0x0c = std::vector (stride 0x94), read off both accessors 0x00456c20 and 0x00456c40. sizeof(Game::CombatPlayerStats) is already verified at 0x24, and its stream schema writes RPBon/RPBonT/SavBonus/MaintHF BEFORE TacReports -- so this vector sits at +0x04, ahead of every scalar the serializer emits first. A live instance of lane Q's rule: OFFSET ORDER IS NOT WRITE ORDER; align against objects/streams.json, never against an offset-sorted view [verified] constexpr uint32_t Game_CombatPlayerStats_off_TacReports = 0x00000004; // site site in FUN_007b9df0, reached from the combat resolver as 0x007d5af0 -> FUN_007baef0 -> FUN_007b9df0 (depth 2), on the arm that also posts EVENT_INDSYS_SURRENDERS_COMBAT and writes the winner index. `mov ecx,[esi+0x28]; imul ecx,ecx,0x11c; mov edx,[ebp-0x14]; mov eax,[edx+0x2f4]; lea ecx,[ecx+eax*1+0x90]; call 0x007a6630` -- a push_back of a 0x20-byte record (vptr 0x00a23c54) into the SETurnResults ACCUMULATOR at StrategyServer+0x2f4, member +0x90, indexed by PlyrIdx*0x11c. COMBAT WRITES TURN RESULTS: lane K's combat-done-tail.md 5A attributes the phase-6 write to ApplyEncounterResult 0x007d8f9e at member +0x24; this is a SECOND member written from inside the resolver's subtree. FUN_007b9df0 has seven direct callers (0x007baef0, 0x007bd490, 0x007bd520, 0x007bd930, 0x007be870, 0x007d0580, StrategyServer::ProcessTurn), so it is not only a ProcessTurn-phase-1 function. Not serialized (SETurnResults has no Read/Write pair) but live in memory at autosave time and dispatched to the client as strategy-event 0x25 [verified] constexpr uint32_t CombatResolve_TurnResultsWriteSite = 0x003ba140; // layout Game::EncounterResults+0x98/+0x9c = a 4-byte-stride vector of participant handle ids. The combat resolver's per-member loop linear-scans it for the member's ServerPlayer->+0x04 at 0x007d5c96 and SKIPS THE WHOLE MEMBER when absent -- so a combatant present in the Encounter but not in this vector contributes nothing to the resolver. LABELLED HYPOTHESIS on the element type: it is a 4-byte scalar and matches the schema's `carr`, but the schema is in write order and no save observed here fixes the offset [hypothesis] constexpr uint32_t Game_EncounterResults_off_Participants = 0x00000098; // layout Game::EncounterResults+0x18/+0x1c = std::vector, stride 0x24 (verified sizeof), INDEXED BY THE ENCOUNTER MEMBER INDEX, not by PlyrIdx: the resolver computes base + i*0x24 with `lea edx,[eax+eax*8]; lea ebx,[eax+edx*4]` at 0x007d6f11 using the same i that drives the member loop, and skips the block when (+0x1c - +0x18)/0x24 <= i [verified] constexpr uint32_t Game_EncounterResults_off_PlayerStats = 0x00000018; // layout Game::EncounterResults+0xa8/+0xac = a 4-byte-stride vector of entity handles flung by a node cannon. Emptiness of THIS vector is the sole first gate on RNG site 1 of the combat resolver: CombatResolve_NodeCannon returns at 0x007bb575 with no draw when +0xa8 == +0xac. Each handle is resolved through the global HandleMap at GetGame()+0x84 (here reached as S+0x84) [verified] constexpr uint32_t Game_EncounterResults_off_NodeCannonFlung = 0x000000a8; // thiscall void (StrategyServer* this /*base S*/, std::vector* results) // RET 4. THE SECOND TURN DRIVER. Reached from exactly one caller: StrategyHost::OnMessage 0x00784640 at 0x00784d07, on the SNMAllCombatDone message, with this = host->+0x54 and results = msg+4. 36 phases, whole 1587-byte body read from the instruction stream. Base is S (NOT S+4): Players at [S+0x54/0x58], ServerTradeManager at [S+0x158], SVScriptObject at [S+0x1b4], encounters at [S+0x1e8]. STRAIGHT-LINE past 0x007d96bf -- every jcc from there on is a per-player loop bound or one of three null tests on S+0x1b4. Order: ++S->+0x8 / arity check / first contact over all ordered combatant pairs / sighting announce / battle tally / diplomacy stats / ApplyEncounterResult per encounter + resupply / encounters.clear() / script(8) / AIRebellion(1) / ProcessNodeSpaceTravel / node-line decay (RNG) / colony-loss drain / two morale passes / ProcessBankruptcy / colonizer resolve / PlayerView rebuild / warnings / infra-terra drain / script(0x14,0x15) / survey+stats / FUN_0078a7c0 / eight ServerTradeManager vtable calls / upkeep / sensors / script(0x1c) / view refresh / node-line sightings / intercept aborts / comm masks / UpdateBankruptcyLimits per player / incoming warnings / two more vtable calls / observed designs / player reports / turn records [verified] constexpr uint32_t StrategyServer_OnAllCombatDone_Tail = 0x003d92a0; // site site, phase 7: S->encounters.clear(). The bytes are `if (_Myfirst != _Mylast) { newEnd = FUN_007c5780(_Mylast,_Mylast,_Myfirst,c); FUN_00679c80(newEnd,_Mylast,&vec+0xc,c); _Mylast = newEnd; }`, MSVC's vector::erase(begin,end). FUN_007c5780 is std::_Uninit_move over 0x74-byte Encounters and is handed the EMPTY range [_Mylast,_Mylast), so it copies nothing and returns _Myfirst; FUN_00679c80 is std::_Destroy_range. THE IDENTICAL FOUR-ARGUMENT SHAPE appears at 0x007cd147/0x007cd15b inside FUN_007cd100 (vector::operator= taking the empty-source path), which is what identifies it. NO PREDICATE, NO FILTER: every encounter is erased. The `if` is the empty-vector guard erase always carries and both arms converge at 0x007d96bf [verified] constexpr uint32_t StrategyServer_OnAllCombatDone_Tail_ClearEncounters = 0x003d9690; // site site in FUN_007ae010 (phase 11 of OnAllCombatDone_Tail): `mov ecx,[esi+0x16c]; fld dword [0x009e2ea0] /*0.5f*/; push ecx; fstp [esp]; call 0x008e6dd0` = Mars::RNG::Chance(0.5f) on the strategic generator at S+0x16c. Chance early-outs WITHOUT a draw at p<=0 and p>=1 but takes neither at 0.5f, so this is EXACTLY ONE NextFloat PER EXPIRED NODE LINE PER TURN. State-dependent draw count, in the combat-done tail, BEFORE the autosave. Every RNG account in the repo assumes the strategic generator advances only inside StrategyServer::ProcessTurn; it also advances here, and again inside the combat resolver FUN_007d5af0 (RNG_NextInt on the node-cannon path, RNG_Twist + RNG_NextInt on the salvage path) [verified] constexpr uint32_t StrategyServer_OnAllCombatDone_Tail_NodeDecayRoll = 0x003ae095; // thiscall void (StrategyHost* this /*the global at 0x00b29f98*/, std::string* outName, bool endTurn) // THE AUTOSAVE. Exactly two call sites: SendEndTurn 0x007839d7 with endTurn=1 -> (Autosave EndTurn).sav, the PRE-turn state; StrategyHost::OnMessage 0x00784e59 with endTurn=0 -> (Autosave).sav, the POST-turn state. Body: null-check this->+0x4 (the strat game) -> log "Can't autosave- Strat game doesn't exist."; build FOUR paths as _snprintf(buf,0x3ff,"%s/%s.%s", dir, name, ext) with dir=FUN_007a05a0(game) ("SavedGames") and ext=FUN_007a0620(game) ("sav") and the four localized names registered at 0x009bed00..0x009bed7f (SOTS_GAME_AUTOSAVE @0xaf092c, _AUTOSAVEBACKUP @0xaf0934, _ENDTURN_AUTOSAVE @0xaf093c, _ENDTURN_AUTOSAVEBACKUP @0xaf0944); if (!IsSinglePlayerHost()) remove both ENDTURN files; pick (cur,bak) by endTurn; mkdir(dir); ROTATE remove(bak)+rename(cur,bak) ONLY WHEN endTurn==0 (the flag byte at [ebp-0x14a1] is set to 1 and the je at 0x00895266 SKIPS the store of 0 when the arg is zero); gate on (this->flags & 4) && this->+0x4; DETACH each player's connection at pl->+0x12c via conn->vft[0x14] and reattach via conn->vft[0x18] after; call SaveGame_WriteFile(this->+0x4, curPath, 1, &agentNames) at 0x0089595d [verified] constexpr uint32_t StrategyHost_Autosave = 0x00495210; // cdecl bool () // whole 27-byte body: `g = *(void**)0x00b2d540; net = g->+0x148; return net != 0 && net->+0x4 == 0;`. Gates SendEndTurn's pre-turn autosave and the AI-agent sidecar branch inside StrategyHost::Autosave; its NEGATION gates the deletion of the ENDTURN autosave pair (so the pre-turn autosave is a single-player-only feature) [verified] constexpr uint32_t StrategyHost_IsSinglePlayerHost = 0x00415fb0; // thiscall bool (void* this) // `return this->+0x148 != 0;`. Called on the global at 0x00b2d540 from StrategyHost::OnMessage 0x00784e3f -- this is the gate on the POST-turn autosave [verified] constexpr uint32_t StrategyHost_HasNetworkSession = 0x00498af0; // cdecl bool (void* game, const char* path, bool write, std::vector* agentNames) // the save-file ROOT that verify/save-reader/save_reader.py already models (its comment at line 694 names this address). Opens the stream with OpenSaveStream(path,&stream,write); with write=1 that is operator new(0x118) + ctor 0x008d10c0 + FUN_008d1090(path,"wb"), the gzip writer. Then writes four named top-level sections through stream->vft[0x28](tag,&ref): "Summary", "CreateParams", "Sim", "CDT"; then one "CD" record (tag at 0x00a2b9d4) per entry of the 0x20-stride agentNames vector whose +0x1c is non-null. NOTHING TIME-, NAME- OR MACHINE-DEPENDENT ENTERS THE PAYLOAD: the file NAME is built by StrategyHost::Autosave and never reaches here [verified] constexpr uint32_t SaveGame_WriteFile = 0x00477070; // cdecl bool (const char* path, Stream** out, bool write) // write -> operator new(0x118), ctor 0x008d10c0, open FUN_008d1090(path, "wb" @0x009e150c); read -> OpenFile(path, @0x00a2ec2c). Returns *out != 0. The "wb" is the gzip container the determinism note measured as header-deterministic (MTIME 0, XFL 0, OS 11) [verified] constexpr uint32_t OpenSaveStream = 0x00416510; // thiscall void (StrategyServer* this) // 122 BYTES, AND IT GENERATES NOTHING. (1) FUN_007c5610(&scratch, S->+0x304, S->+0x308) with ecx = &S->+0x304 -- erase-to-empty of the vector OUTBOX at S+0x304. (2) if (S->+0x244 != S->+0x248) S->+0x248 = S->+0x244 -- clear of a 0xc-stride vector; THE COPY LOOP AT 0x007dc680 IS DEAD CODE, `cmp edx,edx; je` at 0x007dc676 is unconditionally taken. (3) FUN_00792a20(S) prunes two intrusive lists at S+0x2d8 and S+0x2e4. (4) if (S->+0x128 & 4) BuildTurnEvents(S) -- normally FALSE. One caller: StrategyHost::OnMessage 0x00784e34 [verified] constexpr uint32_t StrategyServer_GenerateTurnEvents = 0x003dc640; // note MISNAMED. BuildTurnEvents 0x007db780 is NOT a per-turn turn-event builder: it is the FULL-STATE RESYNC PUSH for setup / load / rejoin. Its entire 3701-byte body is under `if (this->+0x12c != 0)`, and both that descriptor and bit 2 of +0x128 are set in exactly one place in the image -- FUN_007bd1b0 at 0x007bd204/0x007bd23a. It references NO EVENT_* string at all; its only string immediates are "vector too long" and "StrategyServer: OnEvent() called, but no callback function specified." It sends SEResetMap (0x29), SEAddPlayer (0x01), SEInitTrade (0x2a), SETurnEvents (0x28), SESyncDesign (0x19) and calls SynchronizePlayer. It CALLS FUN_0081b390 (the previous-turn snapshot) at 0x007dbc7c to ESTABLISH the baseline and never diffs against it. findings/control-flow/turn-spine.md reads as if this were a per-turn diff step -- it is not [verified] constexpr uint32_t StrategyServer_BuildTurnEvents_isResync = 0x003db780; // offset StrategyServer+0x2f4 (S frame) = std::vector ACCUMULATOR, stride 0x11c, one record per player indexed by PlyrIdx. Written during the turn by (at least) ApplyEncounterResult 0x007d8f9e, FUN_007ae010 0x007ae286/0x007ae3ce, FUN_007a4ff0 0x007a516f, FUN_007a4700, FUN_007b9df0, ProcessAid (3 sites) and ApplyEncounterResults itself -- found by a whole-image scan for `imul r32,r32,0x11c` / `add r32,0x11c` at real instruction boundaries. ApplyEncounterResults' tail (0x007d4fa0-0x007d505f) destroys S+0x304, SWAPS the two vector headers so this turn's accumulation becomes the outbox, then resize(0)+resize(nPlayers) here for the next turn [verified] constexpr uint32_t StrategyServer_off_TurnResultsPending = 0x000002f4; // offset StrategyServer+0x304 (S frame) = std::vector OUTBOX, stride 0x11c, filled by the swap in ApplyEncounterResults' tail. Read by SynchronizePlayer 0x007c865f: `if (size() == Players.size()) { r = base + i*0x11c; r->+0x20 = S->+0x1fc; OnEventCallback(netId, 0x25, r); r->+0x20 = 0; }`. Cleared by GenerateTurnEvents' first statement. SETurnResults is strategy-event id 0x25, unicast per player, and is NOT serialized -- its vtable 0x00a24b00 has no Read/Write pair and it appears in no save schema [verified] constexpr uint32_t StrategyServer_off_TurnResultsOutbox = 0x00000304; // thiscall void (SETurnResults* this) // the DEFAULT CONSTRUCTOR of Game::SETurnResults (Ghidra calls it Create; it is not a factory). sizeof == 0x11c, enumerated five ways: the 0x11c stride and its reciprocal 0xe6c2b449/sar 8 in vector::resize 0x007cd2a0, the `add esi,0x11c` in _Ufill 0x007c5850, the accessor 0x00788cb0 (base[PlyrIdx*0x11c]), the operator new[] in 0x0078b0c0, and this ctor closing at +0x118 (the _Alval of a vector member at +0x10c). Layout: +0x00 vptr; +0x04 bool; +0x08 EMBEDDED Game::EventStorage::TurnEvents (vptr +0x08, int EvTurn +0x0c, vector +0x10/+0x14/+0x18, _Alval +0x1c); +0x20 int stamped by SynchronizePlayer from S+0x1fc and cleared after; +0x24 vector (what ApplyEncounterResult publishes into); +0x34 byte with two bit-flags; strings at +0x38/+0x54/+0xa4/+0xc0/+0xdc; list at +0x70; vectors at +0x80/+0x90/+0xfc/+0x10c; two bools at +0xa0/+0xa1; int at +0xf8 [verified] constexpr uint32_t SETurnResults_ctor = 0x003a7ae0; // thiscall void (std::vector* this, int n) // MISNAMED as DispatchTurnResults: it dispatches nothing. std::vector::resize(n) -- shrink to _Erase 0x007c5610, grow to _Reserve 0x007cb340 + _Ufill 0x007c5850. Likewise 0x007c5850 ("SendTurnResultsToPlayers") is _Ufill: per element default-construct a stack temp with 0x007a7ae0, copy-construct into the destination with 0x007c24d0, destroy the temp with 0x0079ac10, dest += 0x11c. The ONLY send of an SETurnResults in the image is SynchronizePlayer 0x007c86d1 (push 0x25) [verified] constexpr uint32_t vector_SETurnResults_resize = 0x003cd2a0; // note THE GHIDRA SYMBOL NAMES ON 0x00825bb0 / 0x00825c40 ARE SWAPPED, and so is EventStorage_Read 0x00825cc0 (which is the Write). 0x00825c40 is the WRITE: it calls 0x008b9d50, which invokes stream vtable slot +0x24 and pushes the MEMBER'S VALUE -- identical in shape to the golden Game::ObservedTech::Write 0x00817cf0. 0x00825bb0 is the READ: it calls 0x008b9d20, which invokes slot +0x10 and passes a stack scratch as a DESTINATION. objects/layouts.json and objects/streams.json already have the direction right (write 0x825c40, read 0x825bb0); the Ghidra names and findings/subsystems/events.md repeat the swap. Wire schema of Game::EventStorage::TurnEvents, in order: "EvTurn" by WriteInt (FOUR BYTES ON THE WIRE, default -1) at this+0x04; then "Events" through slot +0x28 as a framed counted array of Game::EventStorage::Event bound via Mars::VectorHelper (vtable 0x00a2da7c) at this+0x08. sizeof == 0x18 by enumeration four ways: serializer span (0x08+0x10), the SETurnResults default ctor (subobject 0x08..0x1f, next member at +0x20), its copy ctor, and the 0x18 container stride in EventStorage::FindTurnBucket 0x00811f70 [verified] constexpr uint32_t TurnEvents_serializer_direction = 0x00425c40; // cdecl void* () // whole 10-byte body: `mov ecx,0x00b29f98; jmp 0x005f6450` and 0x005f6450 is `mov eax,[ecx+4]; ret`, i.e. `return *(void**)0x00b29f9c`. 0x00b29f98 is the SAME global StrategyHost::Autosave takes as its `this`, and +0x4 is the same strat-game pointer it null-checks and hands to SaveGame_WriteFile. GetGame()+0x84 is the game's global handle map. 670 xrefs [verified] constexpr uint32_t GetGame = 0x00178050; // thiscall void* (HandleMap* this, uint id) // `if (!id) return 0; slot = id & 0xF; if (slot >= (this->+0xc - this->+0x8)/0x14) return 0; b = this->+0x8 + slot*0x14; lower_bound(b, &it, &id); return it == b->+0x4 ? 0 : *(void**)(it + 0x10);`. A 16-BUCKET stdext::hash_map: vector at +0x8/+0xc/+0x10 with 0x14-byte stride, bucket index = id & 0xF, each bucket a red-black tree whose head is at bucket+0x4 (node layout _Left@0 _Parent@4 _Right@8 key@0xc value@0x10 _Color@0x14 _Isnil@0x15). NOTE the `this` at the call site is &bucketVector, i.e. map+0x84 on the game root, not the map object [verified] constexpr uint32_t HandleMap_Resolve = 0x004b9240; // thiscall void (ServerPlayer* this, ServerPlayer* other) // `if (other) this->HasEnc(+0x1a8) |= 1 << other->PlyrIdx(+0x28);`. Called twice symmetrically per ordered combatant pair in OnAllCombatDone_Tail phase 2 [verified] constexpr uint32_t ServerPlayer_MarkPlayerEncountered = 0x0040df10; // thiscall void (ServerPlayer* this, uint species) // `if (species < 7 && species != 4) this->HasDiscCl(+0x1a4) |= 1 << species;`. Species index 4 is permanently excluded. Called in OnAllCombatDone_Tail phase 2 as MarkSpeciesDiscovered(other->Species(+0x5c)) [verified] constexpr uint32_t ServerPlayer_MarkSpeciesDiscovered = 0x0040dee0; // cdecl int (uint typeMask) // A RANKER OVER GROUPS, NOT A FILTER OF ONE. Clears bit 0 (Standard); if the mask hits the boss group {SystemKiller 7, PuppetMaster 8, Locust 14, 21} the mask is RESTRICTED to that group; otherwise the ambient groups {Swarm 3, Derelict 4, Monitor 5, SlaversRefuel 9, CrowRuins 17}, {CrowsNest 12, GravTrap 13} and {GasCloud 11, Meteor 2, Pirate 6, TradeRaiders 18, 20, 23} are each dropped IF ANYTHING ELSE REMAINS. Returns the index of the lowest surviving set bit in [0,0x18), else 0. The four group masks are lazily built once into 0x00b0e96c..0x00b0e988. FUN_004f4970 is the id->name switch (Standard/VonNeumann/Meteor/Swarm/Derelict/Monitor/Pirate/SystemKiller/PuppetMaster/SlaversRefuel/SwarmQueen/GasCloud/CrowsNest/GravTrap/Locust/Berserker/CrowDefenders/CrowRuins/TradeRaiders), which fixes the return type as an EncounterType enum [verified] constexpr uint32_t PickDominantEncounterType = 0x000f4c40; // thiscall void (StrategyServer* this, Node* node /*= enc->+0xc*/, int encType /*= PickDominantEncounterType(enc->+0x38)*/) // GHIDRA'S DECOMPILE OF THIS FUNCTION IS UNUSABLE -- 19 'removing unreachable block' warnings delete the entire event-posting body; read it as instructions. mask = 0; if (!FUN_00788cd0(node, encType, &mask)) return; -- that gate is true only for VonNeumann(1), Meteor(2), Pirate(6), GasCloud(0xb), Berserker(0xf), 0x17, and fills mask with the players who can see it. Then per set player it composes the event key as the LITERAL "EVENT_" (0x00a24bf8, length 6) CONCATENATED WITH THE TYPE NAME -- EVENT_PIRATE, EVENT_TRADERAIDERS, ... -- and posts through ServerPlayer_GetEventStorage + EventStorage_PostEvent. Finally push_backs a 0x10-byte {system, encType, turn, turn+1} record into the vector at S+0x2c8/+0x2cc/+0x2d0. No RNG [verified] constexpr uint32_t StrategyServer_AnnounceEncounterSighting = 0x003a9db0; // thiscall void (StrategyServer* this, std::vector* encounters, std::vector* results) // two passes over ServerPlayer+0x230 vector, no RNG, no events. PASS A (dead homeworld): if the battle was at a player's own HomeSys(+0x2c), was a real battle (result->+0x4 == 0), had planet stats (result->+0x10c != 0) and the INT64 at result+0x120 is <= 0, then every participant that actually fought them gets deadhome(+0x20)++. PASS B (treaty betrayal): for every ordered pair with GetRelation < 1, a treaty slot signed within the last 3 turns and not yet betrayed since signing (`last != -1 && turn-last < 3 && (bty == -1 || bty < last)`), where the other side actually fought -- bty++ and lastXbty = turn, independently for NAP (+0x8/+0xa/+0xe), alliance (+0x10/+0x12/+0x16) and ceasefire (+0x18/+0x1a/+0x1e) [verified] constexpr uint32_t StrategyServer_UpdateDiplomacyStatsFromCombat = 0x00389d00; // thiscall void (StrategyServer* this, Encounter* enc, EncounterResults* res) // phase 6 of OnAllCombatDone_Tail. Dispatch on three result bytes: res->+0x4 != 0 makes the WHOLE FUNCTION A NO-OP (that flag means 'no battle happened', which is exactly what phase 3's sighting arm keys on); res->+0x6 != 0 -> FUN_007a06a0 (posts EVENT_PEACEFUL_ENCOUNTER); res->+0x7 != 0 -> FUN_007d3eb0 (posts EVENT_SYSTEM_SURRENDERED); otherwise the full path -- stamp StarShip+0x5c = turn on every participating ship; set the pairwise HasEng(+0x1ac) engagement bits; if enc->+0x3c stamp ServerPlayer+0x3d4 = turn; build a ~0xea0-byte combat report and run THE REAL RESOLVER FUN_007d5af0 (7499 B, UNREAD); append a Game::CombatReport to the std::list at S+0x1fc; fire the script hook vt[0x10](7,..)/vt[0x30](..). ALL branches then run a publication tail that push_backs a 0x30-byte Game::ClientEncounterResults into *(S+0x2f4) + PlyrIdx*0x11c + 0x24. DRAWS RNG through its subtree: FUN_007d5af0 -> FUN_007bb530 -> RNG_NextInt (node cannon), and -> FUN_007a7f30 -> RNG_Twist plus -> FUN_007a0540 -> FUN_00852d30 -> RNG_NextInt (salvage / back-engineering) [mapped] constexpr uint32_t StrategyServer_ApplyEncounterResult = 0x003d8920; // thiscall void (Node* this /*= enc->+0xc, the encounter's system*/, byte mask) // `if (!this->+0x100 /*owner*/) return; for each fleet at the node (vt[8] count, vt[0x10] get): if (owner->GetRelation(fleet->PID(+0x58)) == 3) for each ship in fleet->NShips(+0xa4/+0xa8) StarShip::RefreshFromDesign(ship, mask);`. FUN_00854680(ship,1) copies ship->+0x20 = design->+0xe8 and ship->+0x6c = design->+0xd8 then runs five recompute helpers -- a repair/refuel/stat refresh, not a movement step. strategic-turn-internals.md line 320 already calls it RefuelInOrbit(1); called from OnAllCombatDone_Tail phase 6 with mask = 1 [verified] constexpr uint32_t Node_ResupplyAlliedFleets = 0x003463f0; // thiscall void (StrategyServer* this) // the LAST call of OnAllCombatDone_Tail (0x007d98ba), and also called from LoadGame 0x007ddc40 -- so the per-player turn record is rebuilt at the end of every turn AND on load, and never has to survive a save round-trip. Per player, rec = P->+0x3d8: rec+0x0c (32) = P->Sav(+0x284) - P->PvSav(+0x188); rec+0x14 (32) = P->Sav; rec+0x18 (16, mov WORD) = (P->+0x34 - P->+0x30)>>2 owned systems; rec+0x28 (16) = completed-tech count from FUN_0057d980 over the tree's +0x10/+0x14 with state == 4; rec+0x20/+0x24 (int64, cdq/add/adc) = SUM over owned systems of (sys->+0x194 + sys->+0x18c) total population; rec+0x2a/+0x2c/+0x2e (16) = ship counts by hull size 0/1/2 for designs WITHOUT flag 0x400; rec+0x30/+0x32/+0x34 (16) = the same for designs WITH flag 0x400. The census comes from FUN_00818a50(P, int[8]) whose slots [0] and [1] (the grand totals) are computed and DISCARDED. Second loop: FUN_00894260(S->+0x200, i, S->+0xc, rec) archives the record by turn; the archive's copy-assign FUN_008712a0 deliberately does NOT copy +0x1c [verified] constexpr uint32_t StrategyServer_FinalizeTurnRecords = 0x0038a0e0; // thiscall int (ServerSystem* sys) // float mods[7] = {1.0f,0,..}; FUN_00747390(mods, sys, 0); float out[12] = {0}; ServerSystem::ComputeOutputRates(sys, out, mods); return max(*(int*)&out[3], 0). Slot 3 is the money/income rate. THE max() IS A `jg` -- the clamp at zero is what makes a loss-making colony contribute nothing to the bankruptcy limits rather than reducing them, which formula-gaps.md Q1 did not say. Only caller: ServerPlayer::UpdateBankruptcyLimits [verified] constexpr uint32_t ServerSystem_ComputeMaxIncome = 0x003521c0; // note Game::SNMAllCombatDone RTTI vtable, four slots (0x0079e590, 0x0082a100, 0x0082a170, 0x0079e500 -- the middle pair are the network Read/Write). Layout by enumeration from the two stack constructors and from every offset OnAllCombatDone_Tail reads: `struct SNMAllCombatDone { void* vptr; std::vector results; }`, 0x10 bytes -- which is why the handler passes msg+4 and not msg. Three construction sites: RunCombatRound 0x007cc847 (stack), the combat server FUN_007cfd00+0x541 = 0x007d0241 (stack; sends it to every player whose +0x44 is 4 or 5, then sets combatServer->+0x60 = 9; NOTE Ghidra sizes FUN_007cfd00 at 384 B but its real body runs to the ret at 0x007d02b9), and the deserialization factory 0x008663b0 (operator new(0x14) -- 4 bytes larger than the enumerated size, UNEXPLAINED) [verified] constexpr uint32_t SNMAllCombatDone_layout = 0x00624758; // thiscall double (ServerSystem* sys, double overHarvestRate) // `ret 8`, real end 0x007505a5. Returns 0 when the system has no owner (+0x100) or rbfl (+0x1dc) is non-zero. Otherwise the SUM of three terms -- the over-harvest demand x SpeciesDef+0x50, (TRes + available resources) x StripMineFraction x 0.9, and the population output of groups 0, 1 and 2 -- multiplied in one uninterrupted 80-bit chain by player OutMod, sys.OutMod, player +0x224, RebOutMod, ScOutMod, and finally by ADDICTION_OUTPUT_MOD when the addiction phase is >= 3. SIDE-EFFECT FREE: it and all seven callees were checked for stores to the game state (0x0074a6d0's only writes are through its int64 out-parameters), which is why this and not ComputeOutputFromRates is the compare target -- that one repairs ships in orbit [verified] constexpr uint32_t ServerSystem_ComputeTotalOutput = 0x00350480; // thiscall double (ServerSystem* sys, int groupType, int species, double count) // `ret 0x10`, real end 0x0074b871. THE population -> output term: returns 0 for count <= 0, else max(0, POPTYPE[groupType].outputMod x (stationFactor x 1.8) x moraleMod x (count / 500000)). stationFactor is 1 + stations x STATION_BONUS_IMPERIAL_OUTPUT and applies to groupType 0 of an owned system only (and only while that constant is > 0); moraleMod applies to groupType 1 only. So output points per head are typeOutputMod x 1.8 / 500000 -- exactly 3.6e-6 for an imperial population with no stations [verified] constexpr uint32_t ServerSystem_GroupOutput = 0x0034b7a0; // thiscall double (ServerSystem* sys, int groupType) // `ret 4`, real end 0x0074da01. Sums GroupOutput over species 0..6 using GroupPopulation(groupType, species). For groupType 1 and the owner's own species on a non-independent system it first adds a capacity surplus: two calls to 0x0074a6d0 with the int64 out-pointer in argument slot 4 and then in slot 6, surplus = max(0, B - A). The income analogue is 0x0074d760 [verified] constexpr uint32_t ServerSystem_PopOutput = 0x0034d8f0; // thiscall double (ServerSystem* sys) // real end 0x0074b901. Sums over species 0..6: max(0, POPTYPE[2].outputMod x 1.8 x (slaves / 500000)). Does NOT go through GroupOutput, so it carries neither the station factor nor the morale multiplier. Slave counts come from 0x0074b610 [verified] constexpr uint32_t ServerSystem_SlaveOutput = 0x0034b880; // thiscall float (ServerSystem* sys) // real end 0x00747ddd. R = clamp01(signed_cbrt((pbon + Pop) / 100) x 0.01); if 0.0001 + R >= 1 the infrastructure term SUBSTITUTES it outright (R := float32(ibon + Infra)); returns float32(min(R, float32(ibon + Infra))). The substitution is not a clamp: a colony with a pending infrastructure bonus can return a fraction above 1 [verified] constexpr uint32_t ServerSystem_StripMineFraction = 0x00347d30; // thiscall double (ServerSystem* sys, double overHarvestRate) // `ret 8`, real end 0x007484c4. CORRECTS strategic-turn-internals.md 3.3, which called this the population base-output term: its population is the RESOURCE stock (Res, plus MRes + ARes2 when the owner strip-mines), and the species constant at +0x4c is ADDED, not a cap -- the cap is the resource stock. B = rate > 0 ? max(rate x available x clamp01((pbon + Pop) x 1e-5), 1.0) : 0; return min(available, max(speciesBaseDemand + B, 0)). Called twice per output pass: once inside ComputeTotalOutput and once by ComputeOutputFromRates as the resource ledger's strip-mine demand [verified] constexpr uint32_t ServerSystem_OverHarvestDemand = 0x003483b0; // thiscall int64 (ServerSystem* sys, int groupType, int species) // groupType 0 returns (pbon + Pop) sign-extended, but ONLY for the system's effective species (indi->+4 when independent, else owner->Species) and 0 for every other; otherwise Population::Count(pbon2) + Population::Count(Pop2) for that (type, species) [verified] constexpr uint32_t ServerSystem_GroupPopulation = 0x00347ba0; // thiscall double (ServerSystem* sys, int species) // `ret 4`. Returns 1.0 when the system has no owner, is independent, or its Morale int[7] entry for the species is exactly 0 -- the zero guard matters, because 0 <= MORALE_DECREASE_OUTPUT would otherwise apply the penalty to every species with no record. Otherwise m >= MORALE_INCREASE_OUTPUT -> MORALE_INCREASE_OUTPUT_MOD, m <= MORALE_DECREASE_OUTPUT -> MORALE_DECREASE_OUTPUT_MOD, each used only when strictly positive, else 1.0 [verified] constexpr uint32_t ServerSystem_MoraleOutputMod = 0x00346910; // cdecl PopTypeRow* (int groupType) // table base 0x00b104e8, stride 0x30, three rows; groupType > 2 falls back to a lazily-initialised sentinel row at 0x00b10578 that no caller reaches. Row fields used here: +0x10 output modifier, +0x14 income modifier, +8 maximum population [verified] constexpr uint32_t PopTypeRow = 0x00135e00; // cdecl void () // builds the three-row population-type table IN CODE from x87 literals, rotating six values with fxch rather than storing them. Imperial {+4 1.0, maxpop 50000000, out 1.0, income 1.0, +0x18 1.0, +0x1c 1.0, +0x20 1.0}; civilian {+4 0.25, maxpop 20000000, out 0.33, income 0.33, +0x18 1.0, +0x1c 0.5, +0x20 2.0}; slaves {+4 0.0, maxpop 0, out SLAVES_OUTPUT_MOD, income SLAVES_INCOME_MOD, +0x18 SLAVES_REPAIR_MOD, +0x1c 0.0}. So the whole population -> output law is carried by the executable except the three slave modifiers [verified] constexpr uint32_t InitPopTypeTable = 0x00135ca0; // cdecl int (int groupType, int64 count) // ftol(POPTYPE[groupType].incomeModifier x (count / 14000.0)). The income analogue of GroupOutput; note the divisor is 14000, not the output law's 500000, and there is no 1.8 factor [verified] constexpr uint32_t GroupIncome = 0x00135e80; // cdecl SpeciesDef* (int species) // table base 0x00b10a00, stride 0x184, seven rows; species > 6 returns a lazily-constructed default at 0x00b105b0. Fields read by the output chain: +0x4c the base resource demand (an int) and +0x50 the resource output factor (a float). The table is .bss, so both come from the data files [verified] constexpr uint32_t SpeciesDefTable_Get = 0x00145cc0; // cdecl double (double x) // x >= 0 ? pow(x, 1/3) : -pow(-x, 1/3), with the exponent taken from the double 0.3333333333333333 at 0x00a3a7c8. Used by StripMineFraction [verified] constexpr uint32_t SignedCubeRoot = 0x004e5680; // offset offset float ScOutMod -- the last of the five output multipliers ComputeTotalOutput applies, and the innermost in the x87 chain [verified] constexpr uint32_t ServerPlayer_off_ScOutMod = 0x0000012c; // offset offset float -- the game-setup handicap OUTPUT multiplier, the sibling of SetupIncomeMult (+0x228) and SetupResearchMult (+0x22c). Read by ComputeTotalOutput as the third of its five multipliers [verified] constexpr uint32_t ServerPlayer_off_SetupOutputMult = 0x00000224; // data PopTypeRow[3] -- .bss, filled by InitPopTypeTable at startup. Stride 0x30 [verified] constexpr uint32_t PopTypeTable_base = 0x007104e8; // data SpeciesDef[7] -- .bss, filled from the data files. Stride 0x184 [verified] constexpr uint32_t SpeciesDefTable_base = 0x00710a00; // data float** -- pointer slot; storage 0x00af08f0, which unusually carries a value (0.1f) in the file image rather than being .bss [verified] constexpr uint32_t GlobalConst_slot_STATION_BONUS_IMPERIAL_OUTPUT = 0x006f08f4; // data int** -- pointer slot read by MoraleOutputMod; the threshold is an INT compared against the Morale int[7] entry [verified] constexpr uint32_t GlobalConst_slot_MORALE_INCREASE_OUTPUT = 0x006ec784; // data float** -- pointer slot read by MoraleOutputMod [verified] constexpr uint32_t GlobalConst_slot_MORALE_INCREASE_OUTPUT_MOD = 0x006ec78c; // data int** -- pointer slot read by MoraleOutputMod [verified] constexpr uint32_t GlobalConst_slot_MORALE_DECREASE_OUTPUT = 0x006ec794; // data float** -- pointer slot read by MoraleOutputMod [verified] constexpr uint32_t GlobalConst_slot_MORALE_DECREASE_OUTPUT_MOD = 0x006ec79c; // data float -- .bss storage reached through the slot at 0x00ae2e88; the output modifier of the slave row of the population-type table. SLAVES_INCOME_MOD is 0x00b0e9ac (slot 0x00ae2e84) and SLAVES_REPAIR_MOD is 0x00b0e9b4 (slot 0x00ae2e8c) [verified] constexpr uint32_t GlobalConst_storage_SLAVES_OUTPUT_MOD = 0x0070e9b0; // data const float 0.5f -- the progress-ratio threshold in ServerPlayer::ProcessTurn's `ResT != NULL && ResErrRoll != 0 && CONST < progressRatio` gate (events.md §3, window 0x008915ec-0x00891624). Read out of dumps/sots.exe: .rdata bytes at 0x00a2c788 are 00 00 00 3f (float 0.5); the following word 0x00a2c78c is float 100.0, so this is a float32, NOT the double an 8-byte read would suggest (that reads as 5.28e13). CONSEQUENCE, measured on the live game: ResErrRoll survives into ProcessResearch only while progress/cost <= 0.5 at the START of the turn, so the OnTechResearched draw (0x0088df20) can fire only when a single turn supplies more than half the target tech's remaining cost. See the board's `research_roll_pending save` row [verified] constexpr uint32_t ResearchRollProgressThreshold = 0x0062c788; // cdecl bool (StarFleet* fleet, MapObject* start, MapObject** dests, unsigned count, int* flagsOut, int* failIdxOut, int* typesOut, NodeRoute* routesOut) // 8 STACK ARGS, plain RET, esp cleaned by the caller (add esp,0x20 at both call sites) -- cdecl, NOT thiscall, even though ecx is loaded with the fleet for the range helpers. Real body 0x007066c0..0x00706907 then 8 int3 to the next start 0x00706910; Ghidra's 584 is correct. IT IS NOT A PATH FINDER: no frontier, no visited set, no relaxation, no recursion. It walks the caller's already-chosen destination list and calls ClassifyLeg 0x00703730 once per consecutive pair, accumulating flags (OR) and the index of the FIRST failing leg. RETURNS TRUE for every call with a non-null fleet and non-null start; all outcome information is in flagsOut/failIdxOut. Leading-destination drop at 0x00706722: if dests[0] is the fleet itself or the fleet's current SYSTEM (LocID with +0x14==0), dests is advanced and count decremented IN THE SOLVER'S OWN FRAME -- the caller's count is unchanged, so typesOut/routesOut end up shifted by one and the last element is never written. Three fuel figures: StarFleet_MinRange(fleet,0) for the pre-flight probe, FUN_00705d60(fleet,true) as the refuel reset, FUN_00705d60(fleet,false) as the running budget, clamped to >= 0 at the top of every leg. Per-leg draw-down uses an INLINED Mars_Vec3_Length (three f32 deltas, one narrowing on the sum of squares, one on the sqrt, one on the subtraction). The store of 0x009e22bc into the NodeRoute local on the back edge is the INLINED destructor (the Mars::IStreamable base vftable), not a branch and not a missing re-init -- the ctor runs again at the top of the next iteration. Two callers, both direct, none indirect: FUN_005e6d50 (UI, dry run: flags only, then a confirmation dialog if flags != 0) and StrategyServer_OrderFleetMove 0x008653c0 [verified] constexpr uint32_t PathSolver = 0x003066c0; // thiscall int (StarFleet* this, MapObject* from, MapObject* to, float* rangeInOut, int* flagsOut, NodeRoute* routeOut) // RET 0x14. Returns the WAYPOINT TYPE for one leg: 0 (no move possible), the owner's species drive type, 3 (node route), 4 or 5 (gate transit). Real body 0x00703730..0x00703bc9 then 6 int3 to 0x00703bd0. flagsOut may be null (a stack dummy is substituted and the whole flag block at 0x00703846 is skipped). Order of decision: (A) if `to` is a fleet, try to intercept it via FUN_00703650; (B) raise the flag bits; (C) if `to` is a deep-space point the player may not use, raise 0x400; (D) if the player has a gate at one end, check gate traffic against NGts*PrGtTrf and return 4 (gate->gate) or 5 (gate->gateless within CstR); (E) if the species drive type is not 3, return it unchanged -- every non-node race stops here with no range check and no route record; (F) otherwise solve the single node-line hop. Endpoint kinds from MapObject->+0x14: 0 system, 1 fleet, 2 deep-space point. The route record is written ONLY when the returned type is 3; for every other type it is left {nrp:-1, nrf:0, nrt:0} [verified] constexpr uint32_t StarFleet_ClassifyLeg = 0x00303730; // constant int // ClassifyLeg flag bit 0x001, set at 0x00703897. Some ship in the fleet is performing a cancellable action; OrderFleetMove cancels them all and proceeds. A WARNING, not a refusal -- the UI's dry run (flags != 0) shows it, the server's mask (flags & 0x418) ignores it [verified] constexpr uint32_t PathFlag_ShipActionsWillCancel = 0x00000001; // constant int // ClassifyLeg flag bit 0x002, ORed at 0x00703b10 from the errBits local seeded at 0x00703a0d. The leg's EXISTING node line is beyond the fleet's remaining fuel (LegInRange FUN_006ffa00 false). Not a refusal: OrderFleetMove installs the plan anyway [verified] constexpr uint32_t PathFlag_NodeLegOutOfRange = 0x00000002; // constant int // ClassifyLeg flag bit 0x004, set at 0x007039c5 when owner->GTraf(+0x14c) + fleet->+0xc0 would exceed owner->NGts(+0x144) * owner->PrGtTrf(+0x148). The leg then returns type 0. NOT one of OrderFleetMove's refusal bits, so a plan can be installed over gate capacity with a type-0 first waypoint. The fleet's own cost is zeroed first if its CURRENT waypoint is already a gate transit (it is already counted) [verified] constexpr uint32_t PathFlag_GateTrafficExceeded = 0x00000004; // constant int // ClassifyLeg flag bit 0x008, set at 0x00703859. THE FIRST OF OrderFleetMove's THREE REFUSAL BITS. The destination is a FLEET that is itself traversing a node route, and no interception point could be computed -- FUN_00703650 requires the mover to be sitting at one of the two ends of the target's node line and the whole line to be in range. Not raised when the target fleet is not node-travelling at all [verified] constexpr uint32_t PathFlag_CannotInterceptFleet = 0x00000008; // constant int // ClassifyLeg flag bit 0x010, set at 0x0070386d when ANY ship in the fleet satisfies StarShip_IsGroundedByDamage (destroyed drive). THE SECOND OF OrderFleetMove's THREE REFUSAL BITS. CLEARED again at 0x007039d3 on the successful gate-transit path -- a Hiver gate throw ignores dead drives, the same rule the retreat pipeline reaches from the other side via its species-1 bypass [verified] constexpr uint32_t PathFlag_FleetGrounded = 0x00000010; // constant int // ClassifyLeg flag bit 0x020, set at 0x00703b8b. No node line joins the two systems for this player and the fleet lacks the node-bore capability (StarFleet_HasFlagShips(fleet, 0x20000, 0) is false). Returns type 0. Not a refusal bit [verified] constexpr uint32_t PathFlag_NoNodeLineAndCannotBore = 0x00000020; // constant int // ClassifyLeg flag bit 0x040, ORed at 0x00703b10 from errBits after it is re-seeded at 0x00703b63. A node line was successfully bored but the leg is still out of fuel range. Distinguishes 'ran out of fuel on a line that already existed' (0x002) from 'ran out of fuel on a line we just made' (0x040) [verified] constexpr uint32_t PathFlag_BoredLineOutOfRange = 0x00000040; // constant int // ClassifyLeg flag bit 0x080, set at 0x00703b7d when FUN_006e4de0 (bore a node line between two systems) returns false. Not a refusal bit [verified] constexpr uint32_t PathFlag_NodeBoreFailed = 0x00000080; // constant int // ClassifyLeg flag bit 0x100, set at 0x00703a42. A node-drive leg from a deep-space POINT to a SYSTEM whose owner is neither the player nor a player with a positive relation (FUN_00817890). Not a refusal bit [verified] constexpr uint32_t PathFlag_DestSystemNotFriendly = 0x00000100; // constant int // ClassifyLeg flag bit 0x200, set at 0x00703a6a. The mirror of 0x100: a node-drive leg from a SYSTEM that is not friendly-owned to a deep-space POINT. Not a refusal bit [verified] constexpr uint32_t PathFlag_SourceSystemNotFriendly = 0x00000200; // constant int // ClassifyLeg flag bit 0x400. THE THIRD OF OrderFleetMove's THREE REFUSAL BITS. Two sites: 0x007038c5 (the destination point is in neither of the player's two per-point masks at point+0x8c and point+0x90, and the player is not species 4) and 0x00703ad3 (a node-drive leg to a point the player may not use). At the first site the leg then returns 0 for a gate or node drive and the plain drive type otherwise [verified] constexpr uint32_t PathFlag_DestPointNotPermitted = 0x00000400; // constant int // ClassifyLeg flag bit 0x800, set at 0x0070388a. The fleet contains a ship whose current action (ship+0x4c) is exactly 8. Singled out of the general 0x001 warning by masking bit 8 out of the action bitmask before the 0x001 test. A WARNING, not a refusal [verified] constexpr uint32_t PathFlag_ShipActionEight = 0x00000800; // constant int // 0x400|0x010|0x008. The literal in `test DWORD PTR [ebp-0x10],0x418` at 0x00865499 -- the only bits that make StrategyServer_OrderFleetMove refuse. On a hit it logs level 2 with the .rdata format at 0x00a31e44, "StrategySim: %s (%s) move not permitted at this time.", with the fleet's FtName(+0x5c) and the owner's name string (owner+0x40), both read through the MSVC std::string SSO test. Every other bit is either advisory or a route-quality complaint the server commits anyway. The UI dry run at 0x005e6da0 instead tests flags != 0, which is what surfaces the whole word to the player [verified] constexpr uint32_t PathFlag_OrderRefusalMask = 0x00000418; // cdecl int (int species) // 50 B. A 7-ENTRY JUMP TABLE at 0x0080c804, resolved byte by byte: Human(0)->3, Hiver(1)->0, Tarkas(2)->1, Liir(3)->2, _NPC(4)->0, Zuul(5)->3, Morrigi(6)->6; anything above 6 -> 0. THE ANSWER TO THE TYPE-2 QUESTION: waypoint type 2 is the LIIR drive, and it is unreachable for any node-drive race by construction. The value it returns IS the waypoint type for every leg the gate block and the node-route block decline, so a fleet's default waypoint type is a pure function of its owner's species -- no ship data, no terrain, no tech [verified] constexpr uint32_t DriveTypeOfSpecies = 0x0040c7d0; // thiscall int (StarFleet* this) // 118 B, no stack args. Returns 0 for an empty fleet, else DriveTypeOfSpecies(this->PID(+0x58)->Species(+0x5c)), else 0 if the fleet has more than one ship and any ship disagrees. ORIGINAL DEFECT: the disagreement loop at 0x006ff853 re-reads the FLEET's owner species on every iteration instead of indexing ship i, so the compared value is loop-invariant and the loop can never fail. As shipped it is dead code; reproduce it as written rather than 'fixing' it to read per-ship data [verified] constexpr uint32_t StarFleet_GetDriveType = 0x002ff810; // thiscall bool (ServerSystem* this, ServerPlayer* p) // 34 B, RET 4. return (this->GFlags(+0xdc) >> p->PlyrIdx(+0x28)) & 1. IDENTIFIES GFlags: combat-retreat-pipeline.md lists +0xdc as 'a second presence source (not read here)' -- it is the per-player GATE mask, and the whole waypoint-type-4/5 branch of ClassifyLeg is built on it [verified] constexpr uint32_t ServerSystem_HasGate = 0x00344010; // thiscall bool (ServerPlayer* this, ServerSystem* from, ServerSystem* to) // 150 B, RET 8. return from && to && 0.0f < this->CstR(+0x150) && ServerSystem_HasGate(from,this) && !ServerSystem_HasGate(to,this) && Mars_Vec3_Length(from->Pos - to->Pos) <= this->CstR. GIVES CstR A READER: strategic-turn-internals.md records it as unused; it is the GATE PROJECTION RADIUS -- how far past a gate a fleet can be thrown when the far end has no receiving gate. Its result is exactly the 4-vs-5 choice in ClassifyLeg (`add eax,4` after `setne`), so waypoint type 5 is a Hiver gate throw at a GATELESS system, not the Zuul node bore or Morrigi gravity casting. Length via Mars_Vec3_Length, so two float32 narrowings; the comparison is non-strict [verified] constexpr uint32_t ServerPlayer_GateProjectionReaches = 0x00418040; // thiscall int (ServerPlayer* this) // 14 B, no frame: `mov eax,[ecx+0x148]; imul eax,[ecx+0x144]` = this->PrGtTrf(+0x148) * this->NGts(+0x144) -- per-gate traffic times gate count, both saved ints. Compared against GTraf(+0x14c) + the fleet's own int16 cost at fleet+0xc0 [verified] constexpr uint32_t ServerPlayer_GateTrafficCapacity = 0x0040dc50; // thiscall float-free bool (StarFleet* this, MapObject* a, MapObject* b, float* rangeOpt) // 170 B, RET 0xc. THE ONLY FLOAT IN THIS SUBSYSTEM THAT DECIDES A FAILURE. dx,dy,dz each stored to a float32 slot; the sum of squares accumulated on the x87 stack and narrowed to float32 ONCE at 0x006ffa46; r = rangeOpt ? *rangeOpt : StarFleet_MinRange(this,0.0f); r = min(r, FUN_006ff710(this)) with both candidates read back from float32 slots; then `fmul st(0),st` computes r*r AND LEAVES IT IN THE REGISTER -- it is never stored. So the comparison is f32(sumsq) <= (double)r*(double)r, NOT f32(sumsq) <= f32(r*r). A reimplementation that narrows the square disagrees exactly at the boundary. Non-strict: equality returns true (test ah,0x41 then jp) [verified] constexpr uint32_t StarFleet_LegInRange = 0x002ffa00; // thiscall float (StarFleet* this) // 155 B, no stack args. Min over the fleet's ships of Ship_MaxRange 0x0080c820, seeded FLT_MAX from the .rdata word at 0x009e23a8, EXCEPT that it returns 0.0f (not FLT_MAX) for a fleet with no ships and EXITS EARLY the moment the running minimum is <= 0 -- so it is not a pure min if a zero-range ship precedes a negative one. Used only to cap the range in StarFleet_LegInRange [verified] constexpr uint32_t StarFleet_MinTankCapacity = 0x002ff710; // thiscall int (NodeGraph* this, ServerPlayer* p, ServerSystem* a, ServerSystem* b) // 303 B, RET 0xc. THE ONLY GRAPH STRUCTURE IN THE PATH SUBTREE, and it is a SINGLE-HOP ADJACENCY QUERY, never a search: it returns the path index of a node line joining a and b that player p has discovered, or -1. Rejects null args and a==b by system index (+0x5c). Gate: a TRIANGULAR adjacency array at this->+0x24 indexed (hi-1)*hi/2 + lo with one bit per player, so a pair the player has not discovered short-circuits to -1. Then it walks a hash bucket, accepting entries whose {+0xc,+0x10} pair matches in either order and whose +0x2c mask carries the player's bit, and returns entry->+0x8. ORIGINAL DEFECT: the ranking term FUN_006e2130((this->+0x4)->+0x8) depends only on `this`, so it is identical for every candidate; with best seeded at -1 the FIRST matching bucket entry always wins and every later one is dropped on the non-strict `score > best`. As shipped the tie-break is hash-bucket order [verified] constexpr uint32_t NodeGraph_FindNodeLine = 0x002e4eb0; // cdecl bool (MapObject* node, StarFleet* fleet) // 85 B. owner = MapObject_GetOwner(fleet); returns true if any fleet parked at the node and owned by that player carries a ship with capability mask 2 (the tanker bit), OR if the node has an owner whose relation to the fleet's owner is >= 3. NOTE the relation scale: strategic-turn-internals.md 5.2 records FUN_0080e050 as '1 ally, 2 NAP, 3 cease-fire', which would make this 'refuel at a cease-fire system but not at an ally's'; FUN_006d2050 was NOT read, and two call sites use the same scale with different thresholds (>= 3 here, > 0 in FUN_00817890), so 5.2's ordering should be re-checked. Called by PathSolver only when the destination's kind tag (+0x14) is 0, i.e. a system -- reaching one resets the running fuel budget to full tanks [verified] constexpr uint32_t StarFleet_CanRefuelAt = 0x00303c90; // thiscall ServerPlayer* (MapObject* this) // 26 B, no frame. switch on this->+0x14: 0 (system) -> this->PID(+0x100); 1 (fleet) -> this->PID(+0x58); anything else (2 = deep-space point) -> 0. Confirms the kind tag's three values from a third, independent site [verified] constexpr uint32_t MapObject_GetOwner = 0x0031e280; // thiscall MapObject* (MapObject* this) // 12 B, no frame: return (this->+0x14 != 0) ? 0 : this. A checked downcast to the kind-0 (system) case, written with the neg/sbb/not/and branchless idiom [verified] constexpr uint32_t MapObject_AsSystem = 0x0031e340; // register-live-in bool (/* ebx = StarFleet* mover, esi = StarFleet* target -- BOTH LIVE-IN, NEITHER WRITTEN */ float* rangeIn, MapObject** systemOut) // 214 B, cdecl stack frame but it TESTS ebx AND esi WITHOUT EVER WRITING THEM. Reading it as a plain two-argument cdecl function produces nonsense; its one caller (StarFleet_ClassifyLeg at 0x00703831) supplies both registers. Returns false unless the target has waypoints, its front waypoint is type 3, and FUN_00703520 accepts the geometry. On success *systemOut is the system to aim at: if the mover sits at the target's destination, aim at the target's node-transit ORIGIN; if it sits at the origin, aim at the destination; otherwise return true with *systemOut left 0. The transit origin is FUN_006ffab0, which resolves FlightPlan.pnd(+0xf8) through the entity hash at (fleet->galaxy(+0x10))+0x80 -- SO pnd IS THE NETWORK ID OF THE NODE TRANSIT'S ORIGIN OBJECT [verified] constexpr uint32_t StarFleet_SolveFleetIntercept = 0x00303650; // thiscall MapObject* (StarFleet* this) // 43 B, no frame. Returns 0 when the waypoint vector is empty, else IDMap resolve of this->FPlan.pnd(+0xf8) through (this->galaxy(+0x10))+0x80. Pairs with StarFleet_ResolveWaypoint 0x00701390, which resolves the front waypoint's Wpt id through the same map: origin and destination of the current node transit [verified] constexpr uint32_t StarFleet_GetNodeTransitOrigin = 0x002ffab0; // thiscall int (StarFleet* this) // 101 B, no stack args. OR of (1 << ship->+0x4c) over every ship satisfying FUN_0081f880, i.e. every ship whose current action is neither 0 nor 6 and is not action 8 with bit 3 of ship->+0x1c set. ship+0x4c IS THE SHIP'S CURRENT ACTION: OrderFleetMove open-codes the identical three-way predicate at 0x008655ed and calls the 'Ship leaving %s is still doing %s. Cancelling action.' cancel FUN_00849280 on every ship that passes it. The mask feeds ClassifyLeg's warning bits: bit 8 becomes 0x800, anything else becomes 0x001. Nothing bounds-checks the shift, so an action enum >= 32 would be UB [verified] constexpr uint32_t StarFleet_PendingShipActionMask = 0x002ff990; // thiscall bool (StarFleet* this) // 80 B, no stack args. True if ANY ship in the fleet satisfies StarShip_IsGroundedByDamage 0x00815090 (a destroyed drive, tested with FLT_EPSILON rather than zero). Sole producer of ClassifyLeg's 0x010 refusal bit [verified] constexpr uint32_t StarFleet_AnyShipGroundedByDamage = 0x00300240; // thiscall void (StarFleet* this, Waypoint* wpts, int count, int originId) // 514 B, RET 0xc. Real body 0x00707080..0x00707281 then 14 int3 to 0x00707290. EVERYTHING IT WRITES IS SAVED STATE: GTraf(+0x14c) debited by the int16 at fleet+0xc0 if the OLD front waypoint was a gate transit; FPlan.wpts assigned from a zeroed temp then the new list inserted; FPsp2(+0xd8) 0.0f then recomputed by FUN_00705c70; FPeta2(+0xdc) 0; FPogn2(+0xe0) zeroed then set to the fleet's Pos -- the position the order was given from; FPdpos(+0xec) zeroed then set to the FIRST waypoint target's Pos, resolved through the IDMap at (fleet->galaxy)+0x80 and left zero if it does not resolve; pnd(+0xf8) 0 then originId; FtTrans(+0xfc) = wpts[0].Tp, A SECOND SAVED COPY OF THE FIRST LEG'S WAYPOINT TYPE; FtOrig(+0x100) = the fleet's Pos; then GTraf re-credited if the NEW front waypoint is a gate transit. Checked on all 11 curated saves: FtTrans == wpts[0].Tp on 46 of 46 flight plans [verified] constexpr uint32_t StarFleet_SetFlightPlan = 0x00307080; // thiscall void (Waypoint* this, int Tp, const NodeRoute* r) // 34 B, RET 8: this->Tp(+0x8) = Tp; this->nrt.nrp(+0x10) = r->nrp(+0x4); this->nrt.nrf(+0x14) = r->nrf(+0x8); this->nrt.nrt(+0x18) = r->nrt(+0xc). Pins Waypoint = {vptr, int Wpt@+4, int Tp@+8, NodeRoute nrt@+0xc} at 0x1c bytes, cross-checked by the 0x92492493 divide-by-28 at 0x00865594 and the add edi,0x1c stride. The vptr of the destination is not touched [verified] constexpr uint32_t FlightPlan_Waypoint_Set = 0x003006e0; // thiscall NodeRoute* (NodeRoute* this) // 24 B, no frame, returns this in eax: vptr = 0x00a1cbdc (the Game::NodeRoute vftable), nrp = -1, nrf = 0, nrt = 0. THE DEFAULT nrp IS -1, NOT 0 -- and -1 is also what ClassifyLeg writes for a freshly bored node line, which is why the Zuul saves carry a mix of -1 and real path indices while the Human save carries only non-negative ones [verified] constexpr uint32_t NodeRoute_Construct = 0x002e1b20; // cdecl bool (StarFleet* f) // 90 B. FUN_006fe320(f) returns f->LocID(+0xa0) ONLY when the location's kind tag (+0x14) is 2, a DEEP-SPACE POINT -- never a system. Returns true iff any of the three position components differs by exact IEEE comparison (fucompp, test ah,0x44, jp), no epsilon. OrderFleetMove's opening snap is therefore point-only; combat-retreat-pipeline.md 2.5's 'snaps the fleet's position onto its current system' is corrected here -- a fleet parked at a system is never snapped [verified] constexpr uint32_t StarFleet_PosDiffersFromPointLocation = 0x0040ec50; // thiscall void __thiscall Game::TurnCommands::Write(Mars::IStream* s) -- the writer for the `Player..TurnCommands_v5` custom-data block (CDT id -> one CD frame). 764 bytes, no loops of its own. Two halves. (1) A PROLOGUE of six flag-gated groups, each a WriteBool on a member followed, only when that bool is set, by the group's payload; write order is NOT offset order, which is why the offset-sorted layout view cannot be aligned to the wire: bool@0x0c gates f32@0x08 (research rate); bool@0x14 gates i32@0x10 (research target tech); bool@0x20 gates i32@0x18 + f32@0x1c (research boost spend + fraction); bool@0x2c gates bool@0x24 + i32@0x28; bool@0x3c gates f32@0x30,0x34,0x38; bool@0x6c gates a StreamableHelper frame on the member at 0x40. i32@0x04 (player id) is written first and unconditionally. (2) TWENTY-SEVEN std::list members at 0x70..0x1a8, stride 0x0c ({_Myhead, _Mysize, _Alval}), each passed to its own free-function writer as helper(stream, &list). Every one is written unconditionally, so an empty list still costs one zero int: 8 prologue items + 27 zero counts = the 35-item block every no-orders save carries [verified] constexpr uint32_t TurnCommands_Write = 0x00442540; // thiscall void __thiscall Game::TurnCommands::Read(Mars::IStream* s) -- the reader paired with TurnCommands_Write (0x00842540). 1543 bytes; not decompiled by lane Q, listed so the pair is on the record [mapped] constexpr uint32_t TurnCommands_Read = 0x00492ae0; // cdecl void __cdecl (Mars::IStream* s, std::list* moves) -- writer for TurnCommands member +0xc4, the FLEET MOVE list. Per element: WriteInt(fleetId) then a NESTED counted int vector -- n = (v._Mylast - v._Myfirst) >> 2 written with WriteInt, then n ints. So a fleet move on the wire is {fleetId, hopCount, hopCount x systemId}, NOT a fixed {fleetId, 1, destSystemId, 0} quadruple: a multi-hop route is longer. Observed single-hop in human-turn2-orders.sav {1456, 1, [128]} and zuul-turn15-orders.sav {688, 1, [432]} [verified] constexpr uint32_t TurnCommands_WriteFleetMoveList = 0x0043e550; // cdecl void __cdecl (Mars::IStream* s, std::list* orders) -- writer for TurnCommands member +0x88, the BUILD ORDER list. Per element four WriteInts taken in DESCENDING member order (node+0x14, +0x10, +0xc, +0x8), so the wire order is {ordinal, designId, systemId, w}. Observed in zuul-turn15-orders.sav (5 orders, designs 608/576, system 384) and zuul-turn17-orders2.sav (20 orders, ordinals 6..25) [verified] constexpr uint32_t TurnCommands_WriteBuildOrderList = 0x00422870; // cdecl void __cdecl (Mars::IStream* s, std::list* cmds) -- writer for TurnCommands member +0xa0, the PLANETARY-BUDGET list. Per element WriteInt(systemId) then a StreamableHelper frame (helper vftable 0x00a1f884), which is the only NAMED sub-frame anywhere in the block (SRs SRt SRsc SRtf SRi SRoh SRnr). Observed once, in zuul-turn17-orders2.sav: system 384 with SRsc = 1.0 [verified] constexpr uint32_t TurnCommands_WriteSystemRatesList = 0x0042e3d0; // cdecl void __cdecl (Mars::IStream* s, std::list* orders) -- writer for TurnCommands member +0xb8, the COLONIZE list. Per element two WriteInts (node+0x8 then node+0xc): {shipId, w}. Observed once, in zuul-turn17-orders2.sav: three ships 2512/2544/2624, each with w = 1 [verified] constexpr uint32_t TurnCommands_WriteColonizeList = 0x00422960; // thiscall void (ServerPlayer* this, float dt) // RET 4. The per-player turn driver, called once per player from StrategyServer::ProcessTurn's player loop. THE dt ARGUMENT IS NEVER READ: the whole 1086-byte body contains zero [ebp+N] references (mechanical check over the full instruction decode), so a reimplementation may ignore it. Order: ComputeBudget -> Sav = SatAdd(Sav, net) -> record aid given -> ProcessSpecialProjects -> (ResT ? RollResearchAccident/ProcessResearch) -> research refund -> zero TRM/TRA/TRP -> RebAI decay -> timed-bonus sweep -> ResearchRollPending site -> EVENT_NO_RESEARCH -> PruneRaidTargets [verified] constexpr uint32_t ServerPlayer_ProcessTurn = 0x00491340; // site site in ServerPlayer::ProcessTurn: `if (this->ResT(+0x294) != 0) { if (!RollResearchAccident(&budget)) TechTree::ProcessResearch(this->TechTree(+0xf4), rng, &budget.researchAlloc, &overBudget); }`. Argument order read off the push order at 0x00891496-0x008914a5: pushes are (edx=&overBudget), (ecx=&allocVector), (eax=rng), so left-to-right the args are (RNG*, vector*, int*). The RNG is `*(ServerPlayer+8 - 4 + 0x16c)`. `overBudget` is a FRESH STACK LOCAL at [ebp-0x14], NOT Budget+0x64 [verified] constexpr uint32_t ServerPlayer_ProcessTurn_ResearchCall = 0x00491461; // site site in ServerPlayer::ProcessTurn, immediately after ProcessResearch returns: `if (overBudget > 0 && budget[17] > 0) Sav = SatAdd(Sav, ftol( (double)min(overBudget, budget[17]) / budget[17] * budget[16] ))`. budget[17] = researchPoints, budget[16] = researchMoney. Unspent research points are refunded to savings at the turn's own points-per-credit rate. PREVIOUSLY UNMODELLED - it is not in strategic-turn-internals.md §1.2. The min is selected by `cmp ecx,eax; lea eax,[ebp-0x18]; jl; lea eax,[ebp-0x10]`, then `fild; fidiv budget[17]; fimul budget[16]; call _ftol2` [verified] constexpr uint32_t ServerPlayer_ProcessTurn_ResearchRefund = 0x004914aa; // site site in ServerPlayer::ProcessTurn, AFTER ProcessResearch (ProcessTurn+0x2ac vs +0x165): `if (ResT(+0x294) && ResErrRoll(+0x3b4) && 0.5f < TechTree::ResearchProgressRatio(tree, ResT)) { RollResearchEvent(this); ResErrRoll = false; }`. THE CLEAR IS INSIDE THE BRANCH - when the ratio test fails the flag is left set, which is how it survives to the next turn. The threshold is a strict `<` (MSVC `fld 0.5f; fcomp ratio; fnstsw; test ah,5; jp skip`), constant 0x00a2c788 = 0.5f. This is the site campaign/board.md describes as running BEFORE ProcessResearch; it runs after [verified] constexpr uint32_t ServerPlayer_ProcessTurn_ResearchRollBlock = 0x004915ec; // thiscall void (ServerPlayer* this) // `mov byte [ecx+0x3b4], 1; ret` - the only ARMING write of ResErrRoll in the image other than the constructor. Referenced ONLY from the vtable word at 0x00a327bc (slot 6 of the 0x00a327a4 ServerPlayer vptr), so every caller is an indirect `call [vft+0x18]`. find-vtable-callers returns no genuine caller: all 18 hits are other classes' slot at byte offset 0x70 and pass arguments. WHO RE-ARMS THE FLAG DURING PLAY IS UNRESOLVED [verified] constexpr uint32_t ServerPlayer_SetResearchRollPending = 0x0040e300; // thiscall void (ServerPlayer* this) // `mov byte [ecx+0x3b4], 0; ret`. Vtable word 0x00a327c0, the slot after SetResearchRollPending [verified] constexpr uint32_t ServerPlayer_ClearResearchRollPending = 0x0040e310; // site site in the ServerPlayer constructor: `mov WORD PTR [esi+0x3b4], 1` - a two-byte store that sets ResErrRoll(+0x3b4) = 1 and +0x3b5 = 0. THE FLAG IS BORN TRUE. Consistent with (but not proof of) the hypothesis that ResErrRoll is armed once at construction and consumed once, never re-armed except through the vtable pair [verified] constexpr uint32_t ServerPlayer_ctor_ResearchRollPendingInit = 0x00480474; // thiscall float (TechTree* this, TechDef* def) // RET 4, result in st(0). node = this->nodes(+0x10)[def->id(+0x0)]; if (!node) return 0.0f; return (float)((double)node->progress(+0x1c) / (double)TechTree::Cost(node)). Whole 59-byte body read. The divide is done in double and narrowed once on the fstp. Called from exactly one place: the ResearchRollPending block in ServerPlayer::ProcessTurn [verified] constexpr uint32_t TechTree_ResearchProgressRatio = 0x0017e950; // thiscall TechDef* (TechTree* this) // plain RET. Walks this->nodes(+0x10/+0x14) in index order; for each non-null node resolves node->def(+0x0) and re-indexes nodes by that def's id (the same self-resolving idiom lane E corrected in ProcessResearch), and returns the FIRST def whose node state(+0x14) == 2 (available). NULL when none. Used only as the second half of the EVENT_NO_RESEARCH test [verified] constexpr uint32_t TechTree_FindFirstAvailableTech = 0x0017da90; // thiscall void (TechTree* this, std::vector* out, int minTurn, int maxTurn, bool sort) // RET 0x10. Clears *out, walks this->master(+0x4)->defs(+0x24/+0x28); SKIPS any def whose name (std::string at def+0x4) tail from its first '_' matches "_Root" case-insensitively; requires nodes[def->id]->state(+0x14) == 4 (researched) and minTurn <= node->turnResearched(+0x24) <= maxTurn; pushes {TechDef*, turnResearched, node->flag(+0x2c)} (12-byte record). When `sort` is true it finally calls 0x00582cd0(first, last, count, this). ServerPlayer::ProcessTurn calls it with (minTurn = server ModCount, maxTurn = INT_MAX, sort = true), i.e. "techs this player completed on THIS turn". strategic-turn-internals.md §1.2 step 9 described it as a list of AVAILABLE techs; it is the opposite [verified] constexpr uint32_t TechTree_CollectResearchedTechs = 0x00184e50; // field sizeof(ResearchedTechRecord) = { TechDef* def; int turnResearched; int flag } - the element TechTree::CollectResearchedTechs pushes. Stride confirmed by the vector-clear loop at 0x00584e75 (three dword copies, `add eax,0xc`) [verified] constexpr uint32_t ResearchedTechRecord_stride = 0x0000000c; // cdecl int (int a, int b) // saturating add. Overflow-checked: b>0 and a+b<=a -> +2000000000 (0x77359400); b<0 and a+b>=a -> -2000000000 (0x88ca6c00); otherwise clamp(a+b, -2000000000, +2000000000). Whole 94-byte body read. Used for every write to ServerPlayer::Sav in ProcessTurn (the net apply and the research refund) [verified] constexpr uint32_t ServerPlayer_SatAdd = 0x00417990; // cdecl int (ServerPlayer* p, int available) // the AI branch of ProcessTurn phase 4: a human player uses budget[11], which ComputeBudget fills only when p->IsAI(+0xf9)==0, and an AI recomputes it here. Body not read by lane T; the call shape (two pushes, `add esp,8`) is verified [mapped] constexpr uint32_t ServerPlayer_ConstructionSpend = 0x00417f90; // thiscall void (ServerPlayer* this, int constructionSpend, int available) // TWO stack arguments, pushed as (edi = budget[15] available) then (eax = construction spend), so the callee sees (spend, available). turn-spine.md called this "income/savings"; strategic-turn-internals.md §1.1 already corrected it to special projects. Body not read by lane T [mapped] constexpr uint32_t ServerPlayer_ProcessSpecialProjects = 0x00440fe0; // thiscall void (ServerPlayer* this) // the unconditional tail of ServerPlayer::ProcessTurn. Sweeps the vector at +0x338/+0x33c (0x20 stride) forward, erasing an element when: its inner range rec[0xc]==rec[0x10] (empty); or ModCount - rec[0x1c] > 20; or the entity lookup 0x008b9240(server EntityHash, rec[0x4]) fails; or the resolved object's +0x90 mask lacks bit (1 << this->PlyrIdx(+0x28)). Erase shifts the tail down 0x20 and calls the trailing element's vft[0] scalar destructor. Whole 239-byte body read [verified] constexpr uint32_t ServerPlayer_PruneRaidTargets = 0x00463cf0; // thiscall void (ServerTradeManager* this) // StrategyServer::ProcessTurn phase 2, called on StrategyServer+0x158 (S frame) / +0x154 (raw frame). 1494 bytes. Freighter -> route allocation; the formulas are in strategic-turn-internals.md §1.4 and have never been run against the game [mapped] constexpr uint32_t ServerTradeManager_ProcessTurn = 0x0046b300; // thiscall void (StrategyServer* this) // StrategyServer::ProcessTurn phase 3, 192 bytes. Every owned system whose owner has CnTrd registers NumTradeRoutesSupported routes [mapped] constexpr uint32_t StrategyServer_RegisterTradeSystems = 0x003adc80; // thiscall void (StrategyServer* this) // StrategyServer::ProcessTurn phase 28, 185 bytes. For every player, for every species index 0..6 except 4 and the player's own: if the species is known (0x0080de60) and SpeciesDef::Get(sp)->+0x78 is a tech id != 0xc5 whose def resolves and is not already researched, make it visible via 0x00586200(def, 0). The "you have met this race, its racial tech appears in your tree" rule. Draw-free, event-free, three callees [verified] constexpr uint32_t StrategyServer_UnlockMetSpeciesTechs = 0x0038aa70; // site site in StrategyServer::ProcessTurn: the per-system `ServerSystem::ProcessTurn` loop. `mov ecx,[Systems._Myfirst + i*4]; call 0x007598e0` - NO stack arguments and no returned value, confirming B4's correction that the decompile's `void* stream` parameter is a Ghidra guess. Loop bound recomputed every iteration as `(Systems._Mylast - _Myfirst) >> 2` [verified] constexpr uint32_t StrategyServer_ProcessTurn_SystemLoop = 0x003dc9c8; // site site in StrategyServer::ProcessTurn: the per-player `ServerPlayer::ProcessTurn` loop. `fld DWORD [ebp+8]; mov ecx,player; push ecx; fstp DWORD [esp]; call 0x00891340` - the driver's own float dt is forwarded as the callee's single stack argument (the `push ecx` only reserves the slot). Loop bound recomputed every iteration as `(Players._Mylast - _Myfirst) >> 2` [verified] constexpr uint32_t StrategyServer_ProcessTurn_PlayerLoop = 0x003dca00; // site site in StrategyServer::ProcessTurn: the first instruction of the end-of-turn tail (ProcessAid, ProcessSpecialProjects, ProcessSurrenders, the per-player 0x00818530 sweep, the two SVScriptObject hook pairs, 0x0086a8d0, 0x0078ab30, 0x00799380, UnlockMetSpeciesTechs, the per-system 0x00743ec0 sweep, 0x007b4c00 and the team/Status sweep). IT IS UNCONDITIONAL. turn-spine.md §2.4 step 11 says the tail is deferred when encounters are pending; that came from reading the INLINED std::vector DESTRUCTOR at 0x007dcb15 as a branch - `je 0x7dcb38` skips only the `operator delete`, and both arms converge here. There is no branch on the encounter snapshot anywhere in this function [verified] constexpr uint32_t StrategyServer_ProcessTurn_EndOfTurnTail = 0x003dcb38; // field THE TWO BASES, stated once. Let S = the `this` StrategyServer::ProcessTurn receives, which is also what ServerPlayer::GetServer (0x0080e320) returns. Then ServerPlayer+0x8 holds S+4, and EVERY `StrategyServer_off_*` entry in addresses.json except `StrategyServer_off_RNG` is expressed in the S+4 frame (Players 0x50 = S+0x54, Fleets 0x60 = S+0x64, EntityHash 0x80 = S+0x84, RNGPtr 0x168 = S+0x16c). `StrategyServer_off_RNG = 0x16c` is the same word in the S frame. Convert S-frame -> stored frame by subtracting 4. Reading the wrong base yields an empty Players vector and zero declared regions [verified] constexpr uint32_t StrategyServer_base_delta = 0x00000004; // field int, S+4 frame (== S+0x8). Incremented by the FIRST instruction of StrategyServer::ProcessTurn (`inc [esi+8]` @0x007dc6f0). This is a SECOND per-turn counter, distinct from StrategyServer_off_ModCount (0x8 in this frame == S+0xc), which BeginProcessTurn increments and which the research code stamps and reads. Both advance once per turn in different functions. Nobody has named this one [verified] constexpr uint32_t StrategyServer_off_PhaseCounter = 0x00000004; // field std::vector (begin @+0x40, end @+0x44) in the S+4 frame, i.e. S+0x44/S+0x48. Iterated three times per turn: the morale/abandon pre-pass, the ServerSystem::ProcessTurn loop, and the tail's 0x00743ec0 sweep [verified] constexpr uint32_t StrategyServer_off_Systems = 0x00000040; // field ServerTradeManager* in the S+4 frame (S+0x158). The `this` for ServerTradeManager::ProcessTurn, phase 2 of the turn [verified] constexpr uint32_t StrategyServer_off_TradeManager = 0x00000154; // field SVScriptObject* in the S+4 frame (S+0x1b4), null in a normal game. The end-of-turn tail calls two pairs on it when non-null: vft[0x10](6, 0) then vft[0x2c](), and later vft[0x10](0x1c, 0) then vft[0x78]() [verified] constexpr uint32_t StrategyServer_off_ScriptObject = 0x000001b0; // field std::vector in the S+4 frame (S+0x1e8), element stride 0x74. Rebuilt each turn by 0x007d7f70(this, &teamRecords) and then swept: for every member of every record, if (player->IsAI(+0xf9)==0 || player->+0xfa != 0) player->Status(+0x164) = 1. Strides verified from the signed-division magics: 0x8d3dcb09 with `sar 6` is /0x74, 0x78787879 with `sar 5` is /0x44 [verified] constexpr uint32_t StrategyServer_off_TeamRecords = 0x000001e4; // field sizeof(TeamRecord), the element of StrategyServer's team vector at +0x1e4 (S+4 frame) [verified] constexpr uint32_t TeamRecord_stride = 0x00000074; // field std::vector inside a TeamRecord (begin @+0x28, end @+0x2c), element stride 0x44, first word of each element a ServerPlayer* [verified] constexpr uint32_t TeamRecord_off_Members = 0x00000028; // field sizeof(TeamMember). Only its first word (ServerPlayer*) is read by the turn's Status sweep; the other 0x40 bytes are unread by that pass [verified] constexpr uint32_t TeamMember_stride = 0x00000044; // field void* to a per-player per-turn record. StrategyServer::ProcessTurn's player pre-pass writes rec->+0x8 = (1 << playerSlot) and ORs in the player's alliance mask AL(+0x16c) when ALid(+0x168) != -1; ServerPlayer::ProcessTurn then writes rec->+0x10 = budget[2] (trade income). NOTE the bit index is the player's POSITION in the server's Players vector, not PlyrIdx [verified] constexpr uint32_t ServerPlayer_off_TurnRecord = 0x000003d8; // field int, written by ServerPlayer::ProcessTurn from budget[19] (research points given away as aid). Not serialized. strategic-turn-internals.md §1.2 step 2 calls this a per-turn accumulator that is ZEROED; it is ASSIGNED [verified] constexpr uint32_t ServerPlayer_off_ResearchAidGiven = 0x000000c8; // field int, written by ServerPlayer::ProcessTurn from budget[14] (savings given away as aid). Not serialized [verified] constexpr uint32_t ServerPlayer_off_SavingsAidGiven = 0x000000cc; // field float RebOutMod. When RebAI(+0xfc) is set, ServerPlayer::ProcessTurn does RebOutMod = clamp(RebOutMod - 0.04f, 1.0f, 2.0f) each turn. The three constants are image floats: 0x00a17870 = 0.04f, 0x00a17868 = 1.0f, 0x00a1786c = 2.0f [verified] constexpr uint32_t ServerPlayer_off_RebOutMod = 0x00000128; // field int Status (serialized). 1 = playing, 4 = turn done (SNMSetPlayerStatus). Set back to 1 by the end-of-turn team sweep for every member with (IsAI==0 || +0xfa != 0) [verified] constexpr uint32_t ServerPlayer_off_Status = 0x00000164; // field int ALid, the first word of the PlayerAlliances block {ALid, AL, NA, CF} at +0x168. -1 means no alliance; the turn's per-player pre-pass tests it before OR-ing AL into the turn record's visibility mask [verified] constexpr uint32_t ServerPlayer_off_AllianceId = 0x00000168; // field int AL, the alliance member bitmask at PlayerAlliances+4. OR-ed into (player->+0x3d8)->+0x8 once per turn when ALid != -1 [verified] constexpr uint32_t ServerPlayer_off_AllianceMask = 0x0000016c; // field std::vector (begin @+0x338, end @+0x33c), element stride 0x20, save tags `rdtc` + n x `rdt`. Pruned every turn by ServerPlayer_PruneRaidTargets. Fields the prune reads: +0x4 entity id, +0xc/+0x10 an inner range, +0x1c the turn the record was created [verified] constexpr uint32_t ServerPlayer_off_RaidTargets = 0x00000338; // field sizeof(RaidTarget), the element of ServerPlayer+0x338. Confirmed by the prune's `add edi,0x20` step and its `sar ecx,5` count [verified] constexpr uint32_t RaidTarget_stride = 0x00000020; // field std::vector (begin @+0x3a4, end @+0x3a8), element {float PRm; int PRBt}, save tags `NumPR` + n x (`PRm`,`PRBt`). Swept once per turn by ServerPlayer::ProcessTurn: `for (i = count-1; i >= 0; --i) { TRM += e[i].PRm; if (--e[i].PRBt <= 0) erase(i); }`. THE ITERATION IS DESCENDING - float addition is not associative, so the order is load-bearing for bit-exactness [verified] constexpr uint32_t ServerPlayer_off_TimedResearchBonuses = 0x000003a4; // field sizeof(TimedResearchBonus) = { float PRm; int PRBt }. Confirmed by `sar edi,3` on the byte count and `[ecx+edi*8]` addressing [verified] constexpr uint32_t TimedResearchBonus_stride = 0x00000008; // thiscall bool (ServerPlayer* this) // the branch RollResearchEvent takes when odds > roll -- the one the campaign has never observed firing. Splits on the CURRENT RESEARCH TARGET: (a) ResT is one of five hard-coded plague-cure TechDefs (ids 0x273c..0x2740, resolved by 0x00535480) -> 0x00889bb0, which draws ONE FURTHER Mars::RNG::NextInt to pick a random element of this->OwnId (+0x30/+0x34) and posts EVENT_PLAGUE_OUTBREAK naming it; (b) ResT is in the aggression tech table (0x00690f70) AND this->+0x3b5 is set -> 0x008206a0, which allocates a Game::AIRebellionImpl into +0x3b8 if absent and then CANCELS the current research (progress reset 0x0057e7c0, state back to 2 via 0x0057e8d0, vtable hook, ResT = NULL), with no further RNG; (c) otherwise nothing. SO A FIRED ROLL COSTS 1 OR 2 RNG WORDS, not 1 -- every existing coverage note says 'exactly one NextFloat', which is the cost of REACHING here [mapped] constexpr uint32_t ServerPlayer_OnResearchRollSucceeded = 0x00489d60; // field bool `cta` -- the AI-rebellion gate of the succeeded-roll branch (0x00889d60 path b). The ServerPlayer constructor writes it in the SAME two-byte store that arms ResErrRoll: `mov WORD [esi+0x3b4], 1` sets +0x3b4 = 1 and +0x3b5 = 0. The two research-roll flags are adjacent and initialised together [mapped] constexpr uint32_t ServerPlayer_off_RebellionRollArmed = 0x000003b5; // thiscall void (StrategyServer* this, std::vector* actionTypes, bool force) // the ship-action dispatcher, run THREE TIMES per turn with different type sets -- instruction-verified at the three call sites 0x007dc92b, 0x007dc952, 0x007dcb0c. Walks the queued-ship-action list at S+0x78/0x7c and dispatches each action whose type is in `actionTypes` through a 12-entry function-pointer table indexed by type (targets include 0x00789500, the ship-borne BuildQueue::ProcessTurn wrapper); with `force` it validates (0x0083cbb0) and cancels (0x00849280) instead. Sets: {0..14} minus {2} before movement, {2} after movement, {0..14} with force at the end. Type 2 is the action that requires the fleet to have arrived [mapped] constexpr uint32_t StrategyServer_ProcessShipActions = 0x003b9b90; // cdecl std::vector* (std::vector* out) // fills *out with the fifteen ship-action type ids 0..14 and returns it. IT IS NOT AN ENCOUNTER SNAPSHOT -- turn-spine.md §2.4 steps 4 and 11 read it as one, which is where the 'the tail is deferred when encounters exist' error came from. Encounter detection is 0x007d7f70 at the very end of the turn [mapped] constexpr uint32_t StrategyServer_BuildShipActionTypeList = 0x00394ad0; // thiscall void (StrategyServer_secondBase* this) // phase 0, called on S+4. Per system copies current words into shadow words (+0x20c/0x210/0x214 <- +0x68/0x6c/0x70, +0x150.. <- +0x120.., +0xc9 <- +0xc8) via 0x00753530; per player +0x188 <- Sav(+0x284) and +0x18c <- +0x138. Also called from BuildTurnEvents, so it is the baseline 'changed since last turn' is diffed against. Draw-free, event-free [mapped] constexpr uint32_t StrategyServer_SnapshotPreviousTurn = 0x0041b390; // cdecl void (StrategyServer* server) // the end-of-turn fog-of-war update, timed with QueryPerformanceCounter and logged as "Sensors: %f seconds to update sensors for all players.". Wraps 0x0086a6d0: per player x per system (predicate 0x00850cf0) and per player x per fleet (predicate 0x0081e630), packing 2-bit visibility results into system+0x24 and fleet+0x54 for up to 15 players. Deterministic, no events [mapped] constexpr uint32_t StrategyServer_UpdateSensors = 0x0046a8d0; // thiscall void (ServerPlayer* this, bool flag) // no-ops unless this->AIRebellion(+0x3b8) is non-null; steps it (0x006d1fd0(flag)), and if it signals completion (0x00690ff0) calls its vft[0](1) and nulls +0x3b8. Called per player with flag=0 from StrategyServer::ProcessTurn's tail and with flag=1 from the deferred combat tail [mapped] constexpr uint32_t StrategyServer_StepAIRebellion = 0x00418530; // thiscall void (StrategyServer* this, std::vector* out) // the LAST phase of the turn: per system with combatants present (0x0078cb10), builds the pairwise 0x74-byte team records for fleet pairs whose relation (0x0080e050) is war, into StrategyServer+0x1e4 (S+4 frame). The turn's Status-back-to-playing sweep then walks the records it produced [mapped] constexpr uint32_t StrategyServer_DetectEncounters = 0x003d7f70; // data const float = 0.5f. The ONLY consumer is the ResearchRollPending block in ServerPlayer::ProcessTurn: the roll fires when 0.5f < progress/Cost, strictly. Not a registered config key - it is an image literal [verified] constexpr uint32_t g_flt_ResearchRollProgressThreshold = 0x0062c788; // data const float = 0.04f, subtracted from RebOutMod each turn for a RebAI player [verified] constexpr uint32_t g_flt_RebOutModDecay = 0x00617870; // data const float = 1.0f, the lower clamp of RebOutMod [verified] constexpr uint32_t g_flt_RebOutModMin = 0x00617868; // data const float = 2.0f, the upper clamp of RebOutMod [verified] constexpr uint32_t g_flt_RebOutModMax = 0x0061786c; // thiscall void (StrategyServer* S, vector* allianceBroken, vector* napBroken, vector* cfBroken) // 700 B, ret 0xc. THE DIPLOMACY LEDGER'S PER-TURN STAMP, and the only writer of DiplomacyStats on a turn with no combat and no diplomatic command. Pass A (0x00789920..0x007899c4): over every ORDERED pair (A,B) of the S-frame player vector at S+0x54/+0x58, skipping A==B by POINTER, rel = A->GetRelation(B) 0x0080e050; rel 1 -> slot 8, rel 2 -> slot 0, rel 3 -> slot 4, else skip; then ctor a DiplomacyStats on the stack, GetDipStat(&local,B) 0x008180e0, store (int16)S->Frame(+0xc) at local+8+slot*2, SetDipStat(&local,B) 0x00863950. Field mapping: rel 3 -> lastally(+0x10), rel 2 -> lastnap(+8), rel 1 -> lastcf(+0x18). Pass B (0x007899ca..) is the betrayal counter and indexes the three broken-mask arguments by the inner loop index, substituting a zero local when a vector's length != nPlayers -- with no command stream it is a no-op. SOLE CALLER: ApplyTurnCommands 0x007b18b0 at 0x007b2461, so this runs BEFORE both turn drivers and AFTER BeginProcessTurn's frame bump [mapped] constexpr uint32_t StrategyServer_StampTreatyTurns = 0x003898c0; // cdecl int (int myPlyrIdx, PlayerAlliances* a, int otherPlyrIdx) // 58 B. if (myPlyrIdx == otherPlyrIdx) return 3; bit = 1 << otherPlyrIdx (shl by cl, so masked to 5 bits); if (a->AL(+4) & bit) return 3; if (a->NA(+8) & bit) return 2; return (a->CF(+0xc) & bit) ? 1 : 0. THE RELATION CODES ARE 3 = ALLIED (and self), 2 = NON-AGGRESSION, 1 = CEASE-FIRE, 0 = WAR -- strategic-turn-internals.md section 5.2 had 1 and 3 the other way round. The bit is the INDEX FIELD, not the position in the player vector (the opposite of the shared-vision mask), and AL is tested with NO alliance-id guard [mapped] constexpr uint32_t PlayerAlliances_Relation = 0x002d2050; // thiscall int (ServerPlayer* this, ServerPlayer* other) // 33 B, ret 4. A thin forwarder: tail-calls the cdecl PlayerAlliances_Relation 0x006d2050 with (this->PlyrIdx(+0x28), &this->Alliances(+0x168), other->PlyrIdx(+0x28)). 60+ call sites across the image [mapped] constexpr uint32_t ServerPlayer_GetRelation = 0x0040e050; // thiscall DiplomacyStats* (DiplomacyStats* this) // 55 B. vptr = 0x00a21430; every field zeroed; then lastcf(+0x18) = lastnap(+8) = lastally(+0x10) = -1. So a fresh entry's three 'last in force' fields are -1, NOT 0, and every counter (lastnapbty/bkn*/bty*/deadhome) is 0. This is what distinguishes 'never' from 'on turn 0' in the ledger [mapped] constexpr uint32_t DiplomacyStats_ctor = 0x0040e7b0; // thiscall void (ServerPlayer* this, DiplomacyStats* out, ServerPlayer* other) // 294 B, ret 8. if (!out) return; re-initialise *out to the ctor's defaults IN PLACE (the vptr is not touched); out->other(+4) = other->+0x4 (the handle id, i.e. the wire's PlayerID); if (!other) return; then a LINEAR FIRST-MATCH scan of the 0x24-stride vector at this->dipstats(+0x230/+0x234) for entry.other == GetId(other) 0x0042bfb0, copying the entry's thirteen int16 fields (out+8..out+0x21) on a hit. Stride read as 0x38e38e39 / sar 3 [mapped] constexpr uint32_t ServerPlayer_GetDipStat = 0x004180e0; // thiscall void (ServerPlayer* this, const DiplomacyStats* src, ServerPlayer* other) // 328 B, ret 8. if (!src || !other) return; the same linear first-match scan; ON A MISS default-construct a DiplomacyStats on the stack and push_back it (0x0085bc40) so a NEW ENTRY IS APPENDED AT THE END, then back().other = other->+0x4; finally copy src's thirteen int16 fields into the entry and re-write other. The append order is therefore the order in which pairs are first stamped, which is player-vector order [mapped] constexpr uint32_t ServerPlayer_SetDipStat = 0x00463950; // thiscall bool (StrategyServer* this, int playerId) // 60 B, ret 4. p = HandleMap::Resolve(this + 0x80, playerId) 0x008b9240; if (!p) { Log(2, <0x00a2fb30>, playerId); return false; } p->Status(+0x164) = 4; return true. THE ONLY WRITER OF Player.Status = 4 IN THE IMAGE. Three callers, all End Turn SUBMISSION paths that run before the turn is processed: EndTurn 0x00783be0 (+0x70, passes the client's own id at client+0x148), EndTurnForced 0x00783d30 (+0x7b), OnPlayerEndTurn 0x007d9af0 (+0x35). The other two immediate stores to +0x164 in the image are ProcessTurn +0x5ca (value 1, inside the 0x44-stride encounter-member loop) and ResumePlaying +0xb1 (value 0, the load-path normalisation determinism-oracle.md recorded as 'Status resets 4 -> 0 on load'). There is NO writer between tail phase 31 and the autosave; backlog.md item 6 looks in the wrong place [mapped] constexpr uint32_t StrategyServer_MarkPlayerTurnEnded = 0x00421a40; // thiscall bool (TechTree* this, TechPrereqs* prereqs /* = TechDef + 0x88 */) // RET 4. An AND over groups, each group an OR over techs: a group is satisfied by any listed tech whose node exists in this->nodes AND whose state (+0x14) is 4. Zero groups -> TRUE (the function returns satisfied==total with both 0); a group with ZERO entries -> FALSE and the whole test fails, because the inner loop cannot break and the outer one then exits with that group uncounted. Reads only. Called from SetResearched twice: the unforced completion gate on the argument def, and the availability sweep on each node's self-resolved def [verified] constexpr uint32_t TechTree_PrereqsMet = 0x0017d8e0; // field TechPrereqs prereqs -- the block TechTree::PrereqsMet is called on. Two MSVC vectors back to back: the flat entry array at +0x00 and the group array at +0x10. SetResearched passes `def + 0x88` at both call sites [verified] constexpr uint32_t TechDef_off_Prereqs = 0x00000088; // field std::vector at TechPrereqs+0x00; only its _Myfirst is read (`*param_2`), because the groups carry the bounds. Element stride 8, the entry's TechDef* at +0x00; the second word is never read by PrereqsMet [verified] constexpr uint32_t TechPrereqs_off_Entries = 0x00000000; // field std::vector at TechPrereqs+0x10 (_Myfirst +0x10, _Mylast +0x14). Group count = (last - first) >> 3. Each group is {int start; int count} indexing the entry array: the OR-set is entries[start .. start+count) [verified] constexpr uint32_t TechPrereqs_off_Groups = 0x00000010; // field sizeof(TechPrereqEntry) -- the flat prerequisite entry, {TechDef* def; int unread} [verified] constexpr uint32_t TechPrereqs_entry_stride = 0x00000008; // field sizeof(TechPrereqGroup) -- {int start; int count} into the entry array [verified] constexpr uint32_t TechPrereqs_group_stride = 0x00000008; // field BYTE. Non-zero excludes the node from SetResearched's availability sweep entirely (`if ((char)def[0x2c] != 0) continue`, i.e. def+0xb0) -- it can still be completed by an explicit SetResearched and its cost/state are still lowered by the parent-edge sweep, but nothing ever moves it to state 2 and it never stamps turnAvailable, so it can never raise EVENT_TECHS_UNLOCKED. THE WRITE SITE WAS NOT READ: the name records what the byte does, not where it comes from. The tech-file keyword `unlock_explicitly` is the obvious candidate and matches the behaviour exactly, but MasterTechTree::ParseTech 0x0058b050 shows no reference to 0xb0 in its decompilation, so the link is a hypothesis and not a fact [verified] constexpr uint32_t TechDef_off_NoAutoAvailable = 0x000000b0; // field std::string name (0x1c bytes, _Mysize at +0x50, _Myres at +0x54: >= 0x10 selects the heap pointer). The substitution for every research event's %s, and the key RecordObservedTech de-duplicates the observed-tech vector on [verified] constexpr uint32_t TechDef_off_Name = 0x00000040; // constant flags bit 3 of SetResearched(def, flags): after the call, and after each recursive call from the availability sweep, run the refresh helper 0x00585ef0. Also enables the `node slot is NULL` re-lookup path at the head of the function (0x00580e30). No research-path call site sets it -- ProcessResearch passes 2 -- so neither behaviour is modelled [verified] constexpr uint32_t TechTree_SetResearched_flag_Refresh = 0x00000008; // site site inside TechTree::ProcessResearch: the tail loop that collects the newly available nodes for EVENT_TECHS_UNLOCKED. Runs only when tree->owner != 0, after the per-node loop AND after the decay sweep. Collects every node n with n != NULL, n->def != NULL, p = tree->nodes[n->def->techId] != NULL, p->state (+0x14) == 2, and n->turnAvailable (+0x20) == the owner's ModCount. Posts once if the collected vector is non-empty. NOTE the asymmetry: the state test is on the SELF-RESOLVED node p, the turn test on the iterated node n [verified] constexpr uint32_t TechTree_ProcessResearch_TechsUnlockedCollector = 0x00187cc3; // site site at the very head of ServerPlayer::OnTechResearched: RecordObservedTech is the FIRST statement, called unconditionally on every completion -- before the ResT/roll block and before the !silent event post. It de-duplicates by tech name, so the observed-tech vector grows by one 0x2c element per completion of a tech not already observed and by nothing otherwise [verified] constexpr uint32_t ServerPlayer_OnTechResearched_RecordObservedTech = 0x00491790; // site site in ServerPlayer::OnTechResearched, second statement: `if (this->ResT(+0x294) == def) { if (this->ResearchRollPending(+0x3b4)) RollResearchEvent(this); this->ResearchRollPending = 0; this->ResT = 0; }`. RollResearchEvent (0x0088df20) draws ONE NextFloat unconditionally and then enters ServerPlayer_OnResearchRollSucceeded (0x00889d60) only when roll < ResearchEventOdds -- the odds are 0 for every tech outside the plague and AI-rebellion families, so that branch is normally dead. CORRECTED BY LANE K 2026-09-08: that one word is the cost of REACHING the branch, not of a fired roll -- the plague path draws a SECOND word (NextInt) and posts EVENT_PLAGUE_OUTBREAK, the rebellion path cancels the research. A fired roll costs one or two words. This is the extra RNG a completion consumes, and clearing ResT means a second completion in the same pass consumes none [verified] constexpr uint32_t ServerPlayer_OnTechResearched_ResearchRollBlock = 0x00491790; // member Game::ServerSpyManager* StrategyServer::SpyManager, in lane T's S+4 frame (absolute StrategyServer+0x15c). StrategyServer ctor 0x007d78d0: `call 0x00832a30` (the ServerSpyManager ctor, identified by its store of vftable 0x00a3073c) then `mov [esi+0x15c],eax` at 0x007d7d8e. Corroborated independently by 0x007dcf90, which the RTTI inverse map shows is Game::StrategyServer vftable 0x00a26034 slot 14 at sub-object +4: it calls the same two ctors and stores at [esi+0x154] and [esi+0x158], exactly 4 lower than the base-frame 0x158/0x15c, as a +4 `this` requires. Sits immediately after StrategyServer_off_TradeManager (lane T, 0x154 in the same frame) [verified] constexpr uint32_t StrategyServer_off_SpyManager = 0x00000158; // data void* Game::ServerSpyManager::vftable[18] // sub-object +0, COL 0x00a87ed4, bases Game::IServerSpyManager / Game::ISpyManager / Mars::IStreamable. Unlike the trade manager there is no *Impl: ServerSpyManager is itself concrete (no purecall slots) and has no derived class. A second vftable 0x00a30728 sits at sub-object +4 with 3 slots [verified] constexpr uint32_t ServerSpyManager_vftable = 0x0063073c; // thiscall void (Game::ServerTradeManagerImpl* this) // vftable 0x00a31b74 slot 7 (+0x1c). Reached ONLY virtually, from 0x007d97ea in StrategyServer::OnAllCombatDone_Tail phase 23 (call 6 of 8). Zero direct call sites. Body not read; its direct+tail-jump closure is 189 functions and contains no RNG entry point and none of the eleven inlined-draw functions [mapped] constexpr uint32_t ServerTradeManagerImpl_vslot7 = 0x0048ad60; // thiscall void (Game::ServerTradeManagerImpl* this, int arg1) // vftable 0x00a31b74 slot 8 (+0x20). Reached ONLY virtually, from 0x007d97b6 in OnAllCombatDone_Tail phase 23 (call 2 of 8); the site pushes one argument. Closure 86 functions, draw-free [mapped] constexpr uint32_t ServerTradeManagerImpl_vslot8 = 0x0048e8d0; // thiscall void (Game::ServerTradeManagerImpl* this) // vftable 0x00a31b74 slot 9 (+0x24). Reached ONLY virtually, from 0x007d97dd (phase 23, call 5 of 8). Closure 185 functions, draw-free [mapped] constexpr uint32_t ServerTradeManagerImpl_vslot9 = 0x00468060; // thiscall void (Game::ServerTradeManagerImpl* this) // vftable 0x00a31b74 slot 11 (+0x2c). Reached ONLY virtually, from 0x007d97d0 (phase 23, call 4 of 8). Closure 188 functions, draw-free [mapped] constexpr uint32_t ServerTradeManagerImpl_vslot11 = 0x00448570; // thiscall void (Game::ServerTradeManagerImpl* this) // vftable 0x00a31b74 slot 12 (+0x30). Reached ONLY virtually, from 0x007d97c3 (phase 23, call 3 of 8). Closure 193 functions, draw-free [mapped] constexpr uint32_t ServerTradeManagerImpl_vslot12 = 0x0048e920; // thiscall void (Game::ServerTradeManagerImpl* this) // vftable 0x00a31b74 slot 13 (+0x34). Reached ONLY virtually, from 0x007d97f7 (phase 23, call 7 of 8). REACHES THE STRATEGIC GENERATOR: -> 0x00820ca0, NextFloat at 0x00820e18 with the generator loaded as [reg+0x16c] then `lea ecx,[ecx+4]`; and -> 0x0088b440, NextInt at 0x0088b613 with `mov ecx,[ecx+0x16c]; add ecx,4`. Neither was ever observed firing: lane Z measured 0 tail words on 8 turns [mapped] constexpr uint32_t ServerTradeManagerImpl_vslot13 = 0x0048ef80; // thiscall void (Game::ServerTradeManagerImpl* this) // vftable 0x00a31b74 slot 14 (+0x38). Reached ONLY virtually, from 0x007d97a7 (phase 23, call 1 of 8). Closure 143 functions, draw-free [mapped] constexpr uint32_t ServerTradeManagerImpl_vslot14 = 0x004590d0; // thiscall void (Game::ServerTradeManagerImpl* this) // vftable 0x00a31b74 slot 15 (+0x3c). Reached ONLY virtually, from 0x007d9804 (phase 23, call 8 of 8). REACHES THE STRATEGIC GENERATOR: Chance at 0x0082cdb8, generator loaded at 0x0082cda4 as `mov eax,[eax+0x16c]` then `mov ecx,eax`. Never observed firing [mapped] constexpr uint32_t ServerTradeManagerImpl_vslot15 = 0x0042cca0; // thiscall void (Game::ServerSpyManager* this) // vftable 0x00a3073c slot 13 (+0x34). Reached ONLY virtually, from 0x007d9811 in OnAllCombatDone_Tail phase 23 -- the ninth call of the block, and the only one whose receiver is StrategyServer+0x15c rather than +0x158. REACHES THE STRATEGIC GENERATOR: Chance at 0x00887c8a on `mov ecx,[ecx+0x16c]`, and through 0x008408e0 Chance at 0x00840929 and 0x00840a3c plus NextInt at 0x008409c7, all on [reg+0x16c]. Never observed firing [mapped] constexpr uint32_t ServerSpyManager_vslot13 = 0x004877b0; // thiscall void (Game::ServerSpyManager* this) // vftable 0x00a3073c slot 14 (+0x38). Reached ONLY virtually, from 0x007d989b in OnAllCombatDone_Tail phase 33 (call 1 of 2). REACHES THE STRATEGIC GENERATOR: Chance at 0x0088dc43 on `mov ecx,[eax+0x16c]`. Never observed firing [mapped] constexpr uint32_t ServerSpyManager_vslot14 = 0x0048db80; // thiscall void (Game::ServerSpyManager* this) // vftable 0x00a3073c slot 15 (+0x3c). Reached ONLY virtually, from 0x007d98a8 in OnAllCombatDone_Tail phase 33 (call 2 of 2). Closure 230 functions, draw-free [mapped] constexpr uint32_t ServerSpyManager_vslot15 = 0x00487f30; // thiscall void (void* this) // the do-nothing body every Game::SVScriptObject-derived class inherits in the hook slots it does not override. Occupies most of slots 4/11/25/27/30 across the 30 SVSO classes, which is what makes the non-stub overrides countable: 14 at slot 4, 7 at slot 11, 8 at slot 25, 1 at slot 30, 0 at slot 27 [mapped] constexpr uint32_t SVScriptObject_EmptyOverride = 0x0040c5a0; // cdecl Game::SVScriptObject* (int encID) // The EncObj factory. `dec eax; cmp eax,0x16; ja ; jmp dword [eax*4 + 0x0052bf60]` -- a 23-entry dword jump table indexed by encID-1. Live ids: 1 VonNeumann, 3 Swarm, 4 Derelict, 5 Monitor, 7 SystemKiller, 8 PuppetMaster, 9 SlaversRefuel, 10 SwarmQueen, 14 Locust, 17 CrowRuins, 20 Refugees, 21 Ortgay. Ids 2, 6, 11-13, 15, 16, 18, 19, 22, 23 and everything outside 1..23 return NULL. Class names read off the vftable store in each ctor [verified] constexpr uint32_t SVScriptObject_FactoryByEncID = 0x0012bf00; // data void* [23] // the jump table SVScriptObject_FactoryByEncID indexes with encID-1 [verified] constexpr uint32_t SVScriptObject_EncIDJumpTable = 0x0012bf60; // thiscall Game::SVScriptObject* (Game::SVSOSots* this, int encID) // scans the loaded (id, obj) pair vector at this+0x0C..0x10 first and only calls SVScriptObject_FactoryByEncID on a miss [verified] constexpr uint32_t SVSOSots_GetOrCreateEncObj = 0x001a4450; // cdecl Game::SVScriptObject* (const char* xscn) // the xsc factory: a flat four-way _stricmp chain, EXHAUSTIVE. "traps" -> Game::SVSOTraps (ctor 0x0052cbb0), "crowdefs" -> Game::SVSOCrowDefenders (0x0052b3c0), "indsys" -> Game::SVSOIndependentSystems (0x0075b530), "gmtrigger" -> Game::SVSOGrandMenaceTrigger (0x004f5630). Falls through with "Error creating extra script %s." [verified] constexpr uint32_t SVScriptObject_FactoryByScenarioName = 0x001a7050; // thiscall void (Game::SVSOSots* this) // registers exactly traps, crowdefs, indsys and -- only when ScnObj == NULL -- gmtrigger. Corroborates that the four scenario names are the whole set [verified] constexpr uint32_t SVSOSots_SeedNewGameScripts = 0x001a7d70; // thiscall void (Game::SVSOSots* this, Mars::IStream* s) // slot 2 of vftable 0x00A063C4. Emits ScnID, ScnObj (only when non-NULL), numx x (xscn, xsc), NEncObjs x (EncID, EncObj) [verified] constexpr uint32_t SVSOSots_Write = 0x0019ddf0; // thiscall void (Game::SVSOSots* this, Mars::IStream* s) // slot 1. Accepts two tags Write never emits -- NPCPlr (int, first) and hastraps (bool) with a following traps object -- read-only backward compatibility; a writer that omits them is correct [verified] constexpr uint32_t SVSOSots_Read = 0x001a7a40; // data Game::SVSOSots vftable (slot0 dtor, slot1 Read 0x005a7a40, slot2 Write 0x0059ddf0) [verified] constexpr uint32_t SVSOSots_vftable = 0x006063c4; // data Game::SVSOIndependentSystems vftable. slot0 dtor 0x0075afb0; slot1 Read AND slot2 Write are both 0x005f8ac0, the shared `ret 4` no-op stub -- so the "indsys" frame is genuinely empty on disk and the class's 0x1c4-byte body is runtime-only state [verified] constexpr uint32_t SVSOIndependentSystems_vftable = 0x00620314; // thiscall void (void*, Mars::IStream*) // `C2 04 00` -- a bare RET 4 shared as the inherited Read/Write for classes that serialize nothing [verified] constexpr uint32_t Streamable_NoOpStub = 0x001f8ac0; // thiscall void (Game::SVSOCrowDefenders* this, Mars::IStream* s) // sys; ndsys count then a loop writing dsys; ndes count then a loop writing des; drad. NOTE: `dsys` is INSIDE the ndsys loop -- objects/layouts.json records it as a plain member, which is wrong, and no save can settle it because both counts are 0 everywhere [verified] constexpr uint32_t SVSOCrowDefenders_Write = 0x000f8c90; // thiscall void (Game::SVSOMonitor* this, Mars::IStream* s) // calls SVSODerelict::Write (0x004fc2b0) as its first act -- Monitor derives from Derelict, which is why its tag run starts NDsn/DsnID/Dwght + NAsg/Eflt/Esys before nt/scnm/spwt/rsmd/dsgn [verified] constexpr uint32_t SVSOMonitor_Write = 0x000fd810; // thiscall void (Game::SVSODerelict* this, Mars::IStream* s) // NDsn count then a loop of (DsnID, Dwght); NAsg count then a loop of (Eflt, Esys). Two fields per iteration in each, confirmed by the 8-byte element strides [verified] constexpr uint32_t SVSODerelict_Write = 0x000fc2b0; // site `inc [esi+0x8]` with esi = S -- the FIRST instruction of StrategyServer::ProcessTurn's body bumps ModCount. OBSERVED LIVE by a DR0 4-byte write watchpoint on S+0x8 (lane W2): the trap reports EIP 0x007dc6f3, i.e. the instruction after a 3-byte `inc`, on both measured End Turns. This is the ordering marker for the whole ModCount question -- every command-application bump precedes it and the OnAllCombatDone_Tail bump follows it [verified] constexpr uint32_t StrategyServer_ProcessTurn_ModCountBump = 0x003dc6f0; // site ModCount bump at OnAllCombatDone_Tail + 0x2a (trap EIP 0x007d92cd). CORRECTS lane A2's prose, which called it 'OnAllCombatDone_Tail's first instruction': the address A2 predicted is exactly right, the offset is +0x2a and not +0. It is the LAST ModCount write of the turn and it lands AFTER StrategyServer::ProcessTurn has been entered, which refines A2's falsifier (c) -- 'hits after ProcessTurn is entered' is expected for this one site and only this one [verified] constexpr uint32_t StrategyServer_OnAllCombatDone_Tail_ModCountBump = 0x003d92ca; // site The turn-number increment: `inc [reg+0xc]` with reg = S, at BeginProcessTurn + 0x2a (trap EIP 0x007d990d). OBSERVED LIVE on DR1 watching S+0xc: EXACTLY ONE write per End Turn, value 3 -> 4 on the second measured turn, against TWELVE writes to S+0x8 in the same window. THIS SETTLES THE NAMING DISPUTE: S+0x8 is a modification counter (lane A2's `StrategySim_off_ModCount`) and S+0xc is the frame/turn number (lane A2's `StrategySim_off_Frame`). addresses.json's `StrategyServer_off_ModCount` (0x8 in the raw frame == S+0xc) carries the name on the wrong word, and lane T's `StrategyServer_off_PhaseCounter` (S+0x8) is ModCount [verified] constexpr uint32_t StrategyServer_BeginProcessTurn_FrameBump = 0x003d990a; // site Writes Player.Status(+0x164) = 1 for the LOCAL player only (not for every player), at StrategyServer::ProcessTurn + 0x5ca. Trap EIP 0x007dcc94, so the store is a 10-byte `mov dword ptr [reg+0x164], 1`. Confirms lane T2's static reading of the site AND narrows it: on a two-human-player save only player[0]'s Status moved; player[1]'s did not [verified] constexpr uint32_t StrategyServer_ProcessTurnTail_PlayerStatusOne = 0x003dcc8a; // site THE MISSING WRITER OF Player.Status = 4 between tail phase 31 and the post-turn autosave. This address is the instruction AFTER the store (the trap address); the store itself ends here. It lives in StrategyNetworkClient::OnMessage at +0xa15 -- the End-Turn dispatcher -- and it fires AFTER StrategyServer::ProcessTurn has returned and BEFORE the autosave, writing 4 to the local player's Status over the 1 the ProcessTurn tail had just written. CORRECTS lane T2's treaty-turn-stamp.md §3, which read StrategyServer::MarkPlayerTurnEnded 0x00821a40 as 'THE ONLY WRITER OF Player.Status = 4 IN THE IMAGE' and concluded 'there is NO writer between tail phase 31 and the autosave'. There is, and it was watched happening twice on two consecutive End Turns [verified] constexpr uint32_t StrategyNetworkClient_OnMessage_PlayerStatusFour = 0x00385055; // site The `p->Status(+0x164) = 4` store inside StrategyServer::MarkPlayerTurnEnded, at +0x35 (trap EIP 0x00821a75, so the store ends here). Called ONCE PER PLAYER at the START of an End Turn, from OnPlayerEndTurn 0x007d9af0 (return address 0x007d9b2a, i.e. the call is at +0x35) -- confirming lane T2's caller list live. It runs BEFORE the pre-turn autosave's successor and before ApplyAllTurnCommands, which is why the `(Autosave EndTurn)` file still carries Status 0 [verified] constexpr uint32_t StrategyServer_MarkPlayerTurnEnded_StatusStore = 0x00421a75; // site A ModCount bump observed live but NOT attributable to a named function: the trap EIP is 0x0086c3e9 and the nearest preceding known symbol is ServerTradeManager_ProcessTurn 0x0086b300, +0x10e9 away -- far too far to claim containment. Its return address is 0x0088fce2 (inside StrategySim::ApplyTurnCommandBatch 0x0088f9b0), so it IS one of lane A2's twenty command handlers; only the handler's identity is open. Recorded as an address to disassemble rather than dropped [mapped] constexpr uint32_t StrategySim_ModCountBump_unresolved_0086c3e6 = 0x0046c3e6; // site A ModCount bump observed live FOUR times per turn -- the single most frequent command handler on this save. Trap EIP 0x00821a87; nearest known symbol is StrategyServer_MarkPlayerTurnEnded 0x00821a40, but lane T2 measured that function at 60 bytes (ending 0x00821a7c), so this is the NEXT function and MarkPlayerTurnEnded's neighbour, not MarkPlayerTurnEnded. Return address 0x0088ffcb (StrategySim::ApplyTurnCommandBatch) [mapped] constexpr uint32_t StrategySim_ModCountBump_unresolved_00821a84 = 0x00421a84; // site A ModCount bump observed live once per turn; trap EIP 0x0084946e, nearest known symbol ServerTradeManagerImpl_vslot11 0x00848570 at +0xefe (not containment). Return address 0x008900a4 (StrategySim::ApplyTurnCommandBatch) [mapped] constexpr uint32_t StrategySim_ModCountBump_unresolved_0084946b = 0x0044946b; // site A ModCount bump observed live once per turn; trap EIP 0x0088bf01, nearest known symbol StrategyServer_DestroyFleet 0x0088b980 at +0x581 -- plausibly inside it, but unproven. Return address 0x008902b4 (StrategySim::ApplyTurnCommandBatch), so it is a command handler called from the batch applier rather than an inlined site [mapped] constexpr uint32_t StrategySim_ModCountBump_unresolved_0088befe = 0x0048befe; // thiscall 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. 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 [verified] constexpr uint32_t StrategyServer_NodeLineDecay = 0x003ae010; // site 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) [verified] constexpr uint32_t StrategyServer_NodeLineDecay_FleetSkipIsPostDraw = 0x003ae0b2; // thiscall 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 [verified] constexpr uint32_t NodePath_RemainingLife = 0x002e2130; // thiscall 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 [verified] constexpr uint32_t NodePath_DecayStage = 0x002e21b0; // thiscall 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 [mapped] constexpr uint32_t StrategyServer_ProcessNodeSpaceTravel = 0x003a0e20; // offset 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 [verified] constexpr uint32_t StrategyServer_off_NodeGraph = 0x00000150; // offset std::vector (_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 [verified] constexpr uint32_t ServerNodeGraph_off_Paths = 0x00000008; // thiscall 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 [verified] constexpr uint32_t ServerNodeGraph_FindPathById = 0x002e23d0; // thiscall 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 [verified] constexpr uint32_t StarFleet_HasFlagShips = 0x00303500; // cdecl 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 [verified] constexpr uint32_t StarFleet_PathRelation = 0x0038c360; // note NAME CORRECTION, from StrategyServer::Write's own wire tags. At 0x0079fb2f `lea edx,[edi+0x08]; push "ModCount"` and at 0x0079fb40 `lea eax,[edi+0x0c]; push "Frame"`, with edi = S (the same edi that indexes the players vector at +0x54). So in the S frame **S+0x8 is ModCount and S+0xc is Frame**, i.e. in the stored (S+4) frame +0x4 is ModCount and +0x8 is Frame. `StrategyServer_off_ModCount = 0x8` therefore carries the WRONG NAME: that word is Frame, the turn number. The word it names is the one lane T recorded as StrategyServer_off_PhaseCounter = 0x4 and lane K called 'never named' -- it has a name, and it is ModCount. CONFIRMED FROM THE SAVES, which is an independent instrument: Frame reads 1/2/3 on turn1/2/3-state, 16 on zuul-turn16, 23 on zuul-turn23, while ModCount reads 0/12/24/241/412. And CONFIRMED LIVE: lane Z measured S+0x8 advancing 12, 14, 12 per turn on the early Human game (the saves say +12/turn) and 16, 21, 44 on the Zuul one (the saves say ~24/turn average). A modification counter is exactly what those numbers look like, and it explains why only 2 of the 12-44 increments come from the two turn drivers. Integrator: reconcile StrategyServer_off_ModCount / StrategyServer_off_PhaseCounter rather than adding a third name [verified] constexpr uint32_t StrategyServer_wire_ModCount_vs_Frame = 0x0039fb2f; // thiscall void (void* rawBase /* = S+4 */) // the StrategyServer base-class ctor, called from StrategyServer::StrategyServer 0x007d78d0 at 0x007d7905 as `lea ecx,[esi+0x4]`. It zero-initialises FOUR CONSECUTIVE std::vectors as three-word triples with the fourth word skipped: raw +0x40/+0x44/+0x48, +0x50/+0x54/+0x58, +0x60/+0x64/+0x68, +0x70/+0x74/+0x78, then `lea ecx,[esi+0x80]` for the entity hash. That is the campaign's `{_Myfirst,_Mylast,_Myend,_Alval}` = 0x10 allocator-last shape (method rule 5) enumerated four times in a row, and it independently pins StrategyServer_off_Players = 0x50 and _off_Fleets = 0x60 in the raw frame WITHOUT any frame arithmetic -- the ctor is entered with ecx = S+4, so the players triple is literally {S+0x54, S+0x58, S+0x5c}. This is the enumeration that closes the 0x60-vs-0x64 question the campaign paid for once [verified] constexpr uint32_t StrategyServer_ctor_VectorBlock = 0x0045b120; // thiscall ServerPlayer* (StrategyServer* this /*S frame*/) // five sibling accessors at 0x00788de0, 0x00788e10, 0x00788e40, 0x00788e70, 0x00788ea0, one per NPC pseudo-player index word at S+0x1b8/0x1bc/0x1c0/0x1c4/0x1c8 (the five words the ctor sets to -1 at 0x007d79fe..0x007d7a16, and the save's NPCm/NPCo/NPCi/NPCv/NPCa). Each is `idx = this->+0x1b8; if (idx < 0) return 0; first = [this+0x54]; last = [this+0x58]; if (idx >= (last-first)>>2) return 0; return first[idx];` -- a bounds check against the players vector's size followed by an index off _Myfirst, which is a third independent confirmation that S+0x54/S+0x58 are _Myfirst/_Mylast. THE PLAYER VECTOR IS NOT THE LOBBY'S PLAYER LIST: it is #empires + one rebel-AI per distinct empire species + 4 NPC pseudo-players (Alien Menace, Peacekeeper Enforcer, Von Neumann, Independent Colony, all Species 4). Hence NumPlrs 8 on the Human saves (two species) and 7 on the Zuul ones (one species), against a lobby that says '2 Players' in both -- Summary.Players counts EMPIRE SLOTS and is also right [verified] constexpr uint32_t StrategyServer_NPCPlayerAccessors = 0x00388de0; // offset sizeof(Game::ServerPlayer) = 0x3e0, from the two `push 0x3e0` + operator new sites that precede the ctor call: 0x007865b3 (the bare factory reached through the class-registry word at 0x00a26078) and 0x0078a2f5 (the save loader, which also sets +0x8 = S+4 and inserts into the entity hash at S+0x84). The ctor itself is 0x008803d0 -- NOTE that findings/control-flow/turn-driver.md §3 cites 0x00880474 as 'the ServerPlayer constructor', which is an address INSIDE it; the instruction there is `mov WORD [esi+0x3b4],0x100`, a 16-bit store, so it sets ResErrRoll(+0x3b4) = 0 and cta(+0x3b5) = 1, not '+0x3b4 = 1' as that note reads [verified] constexpr uint32_t sizeof_Game_ServerPlayer = 0x000003e0; // thiscall void (ServerTradeManagerImpl* this, std::vector* records) // RET 4. THE DOMINANT STRATEGIC-RNG CONSUMER OF A TURN: 16 of every turn's 18-20 words on the reference save, measured live. `this+4` is the RAW StrategyServer base (GetServer 0x0080eb50 returns it minus 4). Loops over StrategyServer::Players (RAW+0x50/+0x54, 4-byte stride) -- 8 entries on the Human saves -- and per player rolls up to THREE Mars::RNG::Chance calls on the strategic generator at S+0x16c, each exactly one MT word because all three probabilities are strictly inside (0,1): +0x196 (0x00893426) Chance(TRADE_RAID_ODDS_PLAYER, image default 0.2f) -> kind 0; +0x283 (0x00893513) Chance(TRADE_RAID_ODDS_NPC, 0.05f) gated on 0.0f < S->+0x1a0, which is PLAYER-INDEPENDENT so the site is all-or-nothing per turn -> kind 1; +0x33e (0x008935ce) Chance(TRADE_RAID_ODDS_REFUGEE, 0.05f) gated on a subsystem manager being present -> kind 2. NO BACK-EDGE CONTAINS ANY OF THE THREE SITES, so the cost is a hard bound of one word per player per site. Two per-player skip gates exist (a visitedMask bit test at 0x00893302 and a `>2` pre-filter at 0x008933e0) and neither fired on ref-turn2. A SUCCESSFUL roll calls vslot 17 (ServerTradeManager_CreateRaidEncounter), which draws 0 or 1 FURTHER word. The record vector is the 0x74-stride TeamRecord vector at StrategyServer+0x1e8 -- the same one lane I's EncounterDetect_Run receives one instruction later. REAL SIZE 0x60a = 1546 BYTES, ending 0x0089389a; Ghidra reports 1532 and its end lands mid-instruction (method rule 17). Already named 'raid encounter generation' by findings/subsystems/strategic-turn-internals.md line 153 with these exact StrategyVars -- what was new is that it is where a turn's RNG goes [verified] constexpr uint32_t ServerTradeManager_GenerateTradeRaidEncounters = 0x00493290; // site site in StrategyServer::DetectEncounters 0x007d7f70, and THE CONCRETE FALSIFIER FOR 'not in the direct-call closure'. `mov ecx,[esi+0x158]; mov eax,[ecx]; mov edx,[eax+0x28]; push edi; call edx` -- a VIRTUAL dispatch through ServerTradeManagerImpl vftable slot 10 to ServerTradeManager_GenerateTradeRaidEncounters, passing the same TeamRecord vector that the DIRECT call at 0x007d8470 (lane I's EncounterDetect_Run) receives one instruction later. There are ZERO direct `call rel32` targets equal to 0x00893290 in the whole image, and exactly one dword 0x00893290 in .rdata, at 0x00a31b9c = vftable 0x00a31b74 + 0x28. The interface vftable Game::ServerTradeManager 0x00a311a4 has purecall in that slot, so dispatch is the only way in. Lane I's 22-site inventory of ProcessTurn's closure follows E8/E9 rel32 only and says so; this edge is `call edx`, so THE LARGEST SINGLE RNG CONSUMER OF A TURN HANGS OFF A VIRTUAL EDGE INSIDE A FUNCTION THE CLOSURE ALREADY CONTAINS [verified] constexpr uint32_t StrategyServer_DetectEncounters_TradeRaidVCall = 0x003d8469; // thiscall bool (ServerTradeManagerImpl* this, TeamRecord* out, ServerPlayer* p, int kind, std::vector* records) // ServerTradeManagerImpl vftable 0x00a31b74 slot 17, called from GenerateTradeRaidEncounters at 0x0089345e / 0x00893548 / 0x00893603 on each successful Chance roll. Real body 0x008938a0..0x00893af9. Draws `Mars::RNG::NextInt(&S->rng.mt, cands.size()-1)` at 0x008939ee to pick a raid target -- ONE FURTHER MT WORD -- but returns false at 0x0089391c WITHOUT DRAWING when the candidate vector from 0x0083b110 is empty. So a successful raid roll costs 0 or 1 extra word. It drew 0 on ref-turn2 turns 3-5, which is consistent with no roll succeeding (P ~= 0.8^8 * 0.95^8 ~= 11% on the image defaults) OR with an empty candidate list every time; the two are not distinguishable from a word count and this is the cheapest remaining experiment on this path [verified] constexpr uint32_t ServerTradeManager_CreateRaidEncounter = 0x004938a0; // note Game::ServerTradeManagerImpl offset-0 vftable, 22 slots, bases ServerTradeManagerImpl -> ServerTradeManager -> TradeManager -> Mars::IStreamable. SLOT 10 (+0x28, at 0x00a31b9c) = ServerTradeManager_GenerateTradeRaidEncounters 0x00893290; SLOT 17 = ServerTradeManager_CreateRaidEncounter 0x008938a0. The interface vftable Game::ServerTradeManager 0x00a311a4 has purecall in all 21 non-destructor slots. The instance is constructed by 0x00858f70 (writes vptrs 0x00a31b74 / 0x00a31b64) from 0x007d7d7b and 0x007dd1fd, and stored at StrategyServer+0x158 in the S frame (+0x154 in the stored frame, which addresses.json already calls StrategyServer_off_TradeManager). Lane K's combat-done-tail.md §9 tier 4 calls the eight end-of-turn vtable calls on S+0x158 'the largest blind spot in the map' -- this closes two of that class's slots [verified] constexpr uint32_t ServerTradeManagerImpl_vftable = 0x00631b74; } // namespace sots::addr