{ "entries": [ { "name": "RecordObservedDesignsSweep", "addr": "0x007c2350", "convention": "thiscall", "prototype": "void __thiscall Game::StrategyServer::RecordObservedDesigns() -- TAIL PHASE 34 (called at 0x007d98aa from OnAllCombatDone_Tail 0x007d92a0; also from BuildTurnEvents 0x007db780 at 0x007dbc98 and from LoadGame 0x007dd530 at 0x007ddc2b). 222 bytes. Triple loop: for each player p in Players (this+0x54/0x58), for each fleet f in Fleets (this+0x64/0x68), for each ship sh in f->ships (f+0xa4/0xa8) -- so the recorder is called once per SHIP, not once per design, and the repetition is load-bearing because the recorder moves a re-observed record to the back of the list. TWO GATES: `p->PlyrIdx(+0x28) >= 15` skips the player (0x007c23bf `cmp eax,0xf; jge`), and `((sh->+0x54 >> (2*PlyrIdx)) & 3) != 3` skips the ship (0x007c23c4-0x007c23d6, `lea ecx,[eax+eax]; mov eax,3; shl eax,cl; and eax,[edx+0x54]; shr eax,cl; cmp eax,3`). Ship+0x54 is a TWO-BIT-PER-PLAYER visibility word and is NOT serialised -- the ship's on-disk field list runs +0x4c Dep, +0x4d Atq, +0x5c LCT with 0x54 and 0x58 absent. Then `RecordObservedDesign(p, sh->design(+0x14))` at 0x007c23e2", "status": "verified", "source": "findings/subsystems/t34-observed-designs.md (lane DT, 2026-09-09, own disassembly pass over dumps/sots.exe with tools/x86disp.py)" }, { "name": "RecordObservedDesign", "addr": "0x007be340", "convention": "thiscall", "prototype": "void __thiscall Game::StrategyServer::RecordObservedDesign(Game::ServerPlayer* observer, Game::Design* design) -- RET 8. Four early returns: null observer, null design, `design->owner(+0x130)->+0xfb != 0 && ->+0xfc == 0` (0x007be389/0x007be392 -- NPC and RebAI, so a design owned by an NPC that is not a rebel AI is never observed by anyone), `!0x0080baf0(design)` (0x007be3a1) and `0x00813ab0(design, 0x400, 0)` (0x007be3b7, a design flag test). DEDUP ON odid ALONE: linear scan of observer->odes (+0x254/+0x258) comparing element+0x8, stride 0x10 (0x007be3c4-0x007be3ef). MISS -> ObservedDesign ctor 0x0080da30 on the stack, push_back 0x00799d80, then otnF/otnL = server->turn(+0xc) as WORDS at +0x4/+0x6, odid = design->+0xa0->+0x4 at +0x8, opid = owner->+0x4 at +0xc; if the owner is not the observer, the design id is also appended to a per-player vector at server->+0x284 + 0x10*observer->PlyrIdx. HIT -> the element's four fields are COPIED OUT, the element is ERASED (vector::erase 0x00795c40) and a copy is PUSHED ON THE BACK (0x007be618-0x007be66d): a re-observation is a MOVE-TO-BACK, not an in-place update, so the list is in last-observation order and otnF survives. Both paths then set back->+0x6 = turn. Finally the TRIM at 0x007be67f: walk from the last element to the first, count elements whose opid equals this design's owner id, keep the first 20 (`cmp [ebp-0x10],0x14; jge erase`) and erase the rest -- a CAP OF 20 RECORDS PER DESIGN OWNER, applied after EVERY record call, not once per sweep. Tail: three set builders feed the tech and weapon lists -- 0x00862c90 and 0x008629b0 into RecordObservedTech 0x007ba1a0 (at 0x007be4e1 and 0x007be535), 0x008626a0 into RecordObservedWeapon 0x007be1b0 (at 0x007be591)", "status": "verified", "source": "findings/subsystems/t34-observed-designs.md (lane DT, 2026-09-09)" }, { "name": "RecordObservedWeapon", "addr": "0x007be1b0", "convention": "thiscall", "prototype": "void __thiscall Game::StrategyServer::RecordObservedWeapon(Game::ServerPlayer* observer, Game::ServerPlayer* designOwner, ) -- the owep sibling of RecordObservedTech 0x007ba1a0 (lane X). Its single call site is RecordObservedDesign+0x251 (0x007be591), fed from the design's weapon set built by 0x008626a0. Named from that call site and from ObservedWeapon::Write 0x00817bc0; its body is NOT read", "status": "hypothesis", "source": "findings/subsystems/t34-observed-designs.md (lane DT, 2026-09-09) -- call-site attribution only" }, { "name": "vector_ObservedDesign_push_back", "addr": "0x00799d80", "convention": "thiscall", "prototype": "void __thiscall std::vector::push_back(const ObservedDesign&) -- both call sites are inside RecordObservedDesign with ecx = ServerPlayer+0x254 (0x007be40b append, 0x007be65c move-to-back)", "status": "verified", "source": "findings/subsystems/t34-observed-designs.md (lane DT, 2026-09-09)" }, { "name": "vector_ObservedDesign_erase", "addr": "0x00795c40", "convention": "thiscall", "prototype": "iterator __thiscall std::vector::erase(iterator) -- called at 0x007be64d with ecx = ServerPlayer+0x254, on the move-to-back path only. The trim at the end of RecordObservedDesign does its own inline element shift (0x007be7de, four moves of 0x10 bytes per element) rather than calling this", "status": "verified", "source": "findings/subsystems/t34-observed-designs.md (lane DT, 2026-09-09)" }, { "name": "ObservedDesign_ctor", "addr": "0x0080da30", "convention": "thiscall", "prototype": "Game::ObservedDesign::ObservedDesign() -- called at 0x007be3f8 on a stack temporary that is immediately pushed back. Writes the vptr 0x00a23c64 (the same constant the move-to-back path stores into its stack copy at 0x007be62e)", "status": "verified", "source": "findings/subsystems/t34-observed-designs.md (lane DT, 2026-09-09)" }, { "name": "ObservedDesign_sizeof", "convention": "constant", "prototype": "sizeof(Game::ObservedDesign) = 0x10 (16). Measured three ways inside RecordObservedDesign: the dedup scan advances `add eax,0x10` (0x007be3e6), the trim indexes `shl edx,4` / `sar eax,4` (0x007be6a1, 0x007be692) and the erase shift steps 0x10 per element (0x007be7fc). Layout {vptr @+0x0, uint16 otnF @+0x4, uint16 otnL @+0x6, int odid @+0x8, int opid @+0xc} -- fully accounted, no padding slack. That is the wire record otnF otnL odid opid plus the vptr, and it is the SMALL sibling of Game::ObservedTech (0x2c), whose extra 0x1c is the embedded tech-name std::string", "status": "verified", "source": "findings/subsystems/t34-observed-designs.md (lane DT, 2026-09-09); cf. findings/subsystems/observedtech-append.md", "offset": "0x10" }, { "name": "ObservedDesign_vftable", "addr": "0x00a23c64", "convention": "data", "prototype": "Game::ObservedDesign vftable. Stored at +0x0 by ObservedDesign_ctor 0x0080da30 and written directly into the stack copy on RecordObservedDesign's move-to-back path (0x007be62e `mov [ebp-0x50], 0xa23c64`)", "status": "verified", "source": "findings/subsystems/t34-observed-designs.md (lane DT, 2026-09-09)" }, { "name": "ServerPlayer_off_ObservedDesigns", "convention": "offset", "prototype": "std::vector Game::ServerPlayer::odes at +0x254 (_Myfirst +0x254, _Mylast +0x258, _Myend +0x25c, _Alval +0x260). Already in objects/layouts as the odes vector; recorded here with its WRITER, which was not previously attributed: RecordObservedDesign 0x007be340 is the only function that appends to, erases from or trims it. Its sibling ServerPlayer_off_ObservedTechs is +0x274 (lane X)", "status": "verified", "source": "findings/subsystems/t34-observed-designs.md (lane DT, 2026-09-09)", "offset": "0x254" }, { "name": "ServerPlayer_off_IsNPC", "convention": "offset", "prototype": "bool Game::ServerPlayer::NPC at +0xfb. Read by RecordObservedDesign at 0x007be389 as the first half of the observability guard `if (owner->+0xfb && !owner->+0xfc) return`. The identification is by ADJACENCY, not by a direct read: lane T pinned RebAI at +0xfc (ProcessTurn phase 8, 0x00891517), and the wire writes Elim, NPC, RebAI, ReqCL consecutively, so +0xfb is NPC. CORROBORATED BY THE WHOLE CORPUS: every observation record in every save names a design owned by one of the two non-NPC empires, and the four NPC factions' 26 designs appear in nobody's list; and removing the guard from the reimplementation regresses the canonical pair by 9 leaves (four spurious records for the NPC 'Alien Menace') while costing nothing on the rich turn", "status": "hypothesis", "source": "findings/subsystems/t34-observed-designs.md (lane DT, 2026-09-09) -- adjacency plus a corpus-wide and a differential check, not a direct read of the writer", "offset": "0xfb" } ] }