diff --git a/campaign/board.md b/campaign/board.md index f7dcebe..bd9693a 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -96,3 +96,6 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | std::string 0x18 vs 0x1c CONTRADICTION - RESOLVED | objects | verified | high | 100% | 2026-09-08 | 0x1c WAS RIGHT ALL ALONG; lane X mis-attributed ObservedTech+0x24, which is the string's trailing empty-allocator word. ONE layout binary-wide: _Bx@0 (16-byte SSO union), _Mysize@0x10, _Myres@0x14, _Alval@0x18. Proof by three COMPLETE ENUMERATIONS of ObservedTech (Write 0x00817cf0, ctor 0x008562a0, inlined copy ctor 0x0079a184) each of which skips +0x24, then generalised by new `tools/strfootprint.py`: 65 std::string members off a non-stack base, ZERO with a sibling inside the 0x1c span, 51 of 52 measurable inter-member gaps exactly 0x1c (the one 0x20 is StrategyServer::KeyPath, 0x1c on its own Read side - the WRITER skips a member). No 0x18 instantiation, no EBO variant, no custom allocator, no game-local string class. BLAST RADIUS: 65 layouts audited, ZERO were wrong - including ServerPlayer::pswd, the row lane X flagged. Only PROSE carried the 0x18 number. Separately found+fixed: struct-recovery.md 0 had _Mysize/_Myres TRANSPOSED while every table in the same file used the correct offsets | | ObservedTech on-disk mapping | objects | verified | high | 100% | 2026-09-08 | READ, NOT GUESSED, from ObservedTech::Write 0x00817cf0 / Read 0x00817c40: +0x00 vptr, +0x04 u16 otnF, +0x06 u16 otnL, +0x08 bool odet (ONE byte, WriteBool), +0x0c std::string otch (0x1c), +0x28 int owith = 0x2c exactly, nothing unaccounted. Lane X's flagged-as-hypothesis first-seen/last-seen pair CONFIRMED by the tag names. Game::ObservedWeapon (0x00817bc0/0x00817b10) is the identical element with tag owep. Matches save_reader.py's on-disk order exactly - independent agreement between disassembly and the save oracle. `odet` typed int in the reader is BENIGN (for a 4-char tag a bool item and an int item are both 12 bytes, same value); all 60/61/61 real-save values are 00000000 so the SAVES do not discriminate - the binary does | | MY BRIEFING ERROR: fpu_cw triple was under-powered | meta | verified | high | 100% | 2026-09-08 | I specified the experiment as 0x027f / 0x127f / 0x137f. WRONG ON BOTH AXES: 0x027f is 53-bit (it differs from 0x127f only in bit 12, infinity control, ignored since the 387) and 0x137f is 64-bit EXTENDED, not a rounding change. Run literally it returns "all three identical" - TRUE, but it tests single precision not at all and rounding not at all, so the conclusion drawn would have been wrong. Lane F caught it and added the genuine probes 0x007f (24-bit) and 0x1a7f (round-up). RULE: derive experiment parameters from the ISA definition, never from assumed mnemonics - and brief lanes to challenge the parameters, not just execute them | +| serializer struct recovery (automated) | objects | verified | high | 90% | 2026-09-08 | **Lane D: struct recovery is now MECHANICAL.** `tools/serializers.py` decodes the `Mars::IStreamable` `Write` idiom for the whole binary in **0.35 s**; `tools/rtti_map.py` walks type-descriptor <- COL <- vftable[-1] (1,924 TDs = the inventory's count, 2,172 COLs/vftables) to attribute each serializer to its class and its COL this-adjustment. **VALIDATION RUN BEFORE ANY NEW CLAIM, four ways: (A) 305/307 field offsets+kinds exact across 17 already-known classes, ZERO WRONG** vs struct-recovery.md 1-4 (ServerSystem 79/79, StarShip 23/23, StarFleet 16/16, ServerPlayer 101/103, PlayerView 13/13, DiplomacyStats 14/14 - including every trap: ObservedTech+0x24 correctly NOT a field, Bats2/rcex int64, TShn/ETS/DipStats int16, ltis int, pswd string, TRM/CstR/E/T/shrm/RefCap/RepCap/PlayerView.Infra float); **(B) sizeof read off the VectorHelper element-stride divide** (invert MSVC's magic `M = ceil(2^(32+k)/s)`) reproduces ObservedTech **0x2c** independently of lane X's three proofs, plus MoraleEvent 0x50, PlayerReport 0x30, DiplomacyStats 0x24, ObservedDesign 0x10; **(C) 22 of save_reader.py's shapes, recovered tag order identical, 22 agree / 0 disagree** (Sys 78 tags, Player 104, CreateParams 25, Ship 22, Slot 18); **(D) automated Read/Write cross-check on every class, 437/437 field offsets agree**. SCALE: **386 classes with a Write, 1,682 member fields** - verified 87 (542 fields) / clean 77 (328) / unnamed 176 (471) / partial 31 / empty 15; **58 sizeofs corroborated** by a second line of evidence, the rest reported as LOWER BOUNDS and labelled as such (a serializer enumerates only *serialised* members - a non-streamed tail is invisible). ~10x more classes with a known layout, reproducible from the exe in under a second. FOUR ENABLERS each worth 10-170 classes: (1) the RTTI **ClassHierarchyDescriptor** is the only honest "is this an IStreamable" test - a 3-slot vftable also matches TacAISquadRule_*, the CSV row parsers and ~100 others, and using it dropped a bogus 116-class "empty" bucket to 11; (2) **mod=0 memory operands** carry no displacement so `tools/x86disp.py` cannot index them - every field at offset 0 was invisible (OutputRates.SRt, every container element read through a bare iterator); (3) the **member->id pointer idiom** behind every handle field (HomeSys/PlrID/DesID/FltID/SrnTo) - and the base's this-ness must be read BEFORE retiring the destination, because `mov edi,[edi+0x1c0]` overwrites the this-register with the member it is reading; (4) **sub-writers**, base-class (StarMapNode inside ServerSystem = `Pos`) and private-same-class (StrategyServer's six id lists live in FUN_00794cd0 - without it PlayerIDs/DesignIDs/SystemIDs/FleetIDs/ShipIDs/TradeIDs are simply absent), spliced at the call site so disk order survives. FAILURE CLASSES, enumerated: 176 anonymous-tag classes (writer passes NULL, so offsets+types are complete but there are NO on-disk names - a hard limit on names, not on layout; combat commands + network messages); 19 unsized nested types; 9 map/list-node-only; 3 untyped sub-writers; 2 fields lost to a value assembled ACROSS A BRANCH (`tech ? tech->name : ""`, `max(v,1)`) - **left unrecovered on purpose rather than patched with a backward-search heuristic that has no oracle on the other 1,600 classes**; std::map/list members recovered only as their `_Mysize` int; 1 runtime-built tag (Mars::ParticleSystem). Everything but the first is a bounded mechanical fix. WRITE-BACK: 288 structures + 328 labels into Ghidra (0 failures), +201 addresses.json entries, header regenerated with gen_addresses.py. `findings/objects/serializer-struct-recovery.md`, `objects/layouts.{json,md,h}` | +| Game::ShipDesign is NOT streamed through IStreamable | objects | mapped | high | 100% | 2026-09-08 | Lane D, from the serializer sweep: `ShipDesign`'s IStreamable slots are inert - `Write` 0x008747a0 makes no stream call at all. Same for `Game::ServerTradeManager` (Read and Write are BOTH the inherited no-op 0x924fb0) and 13 others incl. the whole `Mars::Particle*` family and `Mars::DecalFile`. So designs persist by some other path (`ShipDesignDef` is already verified separately and may be the whole answer) - worth confirming before anyone goes looking for a ShipDesign serializer that does not exist | +| SVSOJewelsOfTheCrown writes a DUPLICATE tag | objects | mapped | high | 100% | 2026-09-08 | Lane D: `SVSOJewelsOfTheCrown::Write` emits the tag `JEWELLOCATIONID` TWICE, at +0x8 and +0x10. Not a tool error - the Write really does it. Any save reader matching that shape BY NAME will bind the wrong field. Same shape as ServerPlayer's two `Team` members (int @0xac, PlayerAlliances @0x168), which is why the Read/Write cross-check has to exclude duplicated tags rather than report them as conflicts |