The observed-design list is a MOVE-TO-BACK vector capped at 20 records per
DESIGN OWNER, and that is why lane CV's 55 odes leaves looked unrelated: a turn's
output is a permutation of its input, not an append.
rich turn ad-turn27 + BR's deep tcb vs bp-pinB-turn28 1092 -> 1058
36 closed / 2 regressed vs CV's run; 116 / 8 vs the 1166 baseline
canonical turn2-state + CB's tcb vs turn3-state 62 -> 61
1 closed / 0 regressed, set-differenced by leaf PATH
All five committed predictions held; P5 held in direction and was wrong in
magnitude (9 leaves, not 4 -- I predicted spurious records, not leaves).
CV's "no upstream dependency" is right for the design half and wrong for the
other 24 leaves of the 79, two separate ways, and both are named rather than
attempted: the otch/owep arms need three undecoded design->tech/weapon set
builders (the wire's own DOpts list covers 13 of the 18 tech names the turn
moves), and the design half is itself capped by SHIP CONSTRUCTION -- two designs
get their first ships that turn. Fed the true post-turn ship list the same code
leaves 1 leaf of 55 instead of 21, so the mechanism is not the residual.
Control, run: with the NPC-owner guard compiled out the canonical pair goes
61 -> 70 and the rich turn stays at 1058. The guard is load-bearing on one pair
and inert on the other, which is why both were run.
Coverage, stated as loudly as the divergence: 8 players swept, TWO produce any
record; the canonical pair offers 16 (player, ship) pairs of which the guard
drops 15, leaving one record call on one design. No foreign observation moves on
either pair, so the visibility gate -- a two-bit-per-player word at Ship+0x54
that is not on the wire, stood in for with ownership -- is entirely unexercised
and is labelled a hypothesis.
ghidra/addresses.d/dt.json: 10 entries (the sweep 0x007c2350, the recorder
0x007be340, RecordObservedWeapon, the vector helpers, sizeof(ObservedDesign) =
0x10, the vftable, and ServerPlayer +0x254 / +0xfb). gen_addresses.py merges it
clean at 1320 entries, no duplicate name or address.
84 lines
8.4 KiB
JSON
84 lines
8.4 KiB
JSON
{
|
|
"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, <weapon>) -- 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<Game::ObservedDesign>::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<Game::ObservedDesign>::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::ObservedDesign> 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"
|
|
}
|
|
]
|
|
}
|