diff --git a/campaign/board.md b/campaign/board.md index 52b0836..b5c97e5 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -29,7 +29,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | UI screen & flow map | meta | mapped | high | 100% | 2026-09-07 | findings/subsystems/ui-screen-map.md - 36 screens; screens are C++ on Mars controls (NOT data); turn state machine recovered | | `Game::StrategyServer` (sim block) | object | verified | high | 100% | 2026-09-07 | full member table (47 rows) confirmed vs real saves under strict parse incl. ID lists, GOWinPly, invasions, zdsc pairs, SvSctOb (present in all 3 saves) | | stream primitive API | subsystem | mapped | high | 100% | 2026-09-07 | IStreamable vft: +0x18 string, +0x1c bool, +0x20 float, +0x24 int, +0x28 nested, +0x30 raw; FUN_00816490 = NetworkObject handle id | -| real save for verification | verify | verified | high | 100% | 2026-09-07 | 6 saves, 3 distinct turn states in verify/results/saves/ (byte-deterministic per state). Game runs on VM140 via DXVK 3.1 + lavapipe; recipe findings/subsystems/running-the-game.md | +| real save for verification | verify | verified | high | 100% | 2026-09-07 | 6 saves, 3 distinct turn states in verify/results/saves/ (byte-deterministic per state). Game runs on VM140 via DXVK 3.1 + lavapipe; recipe findings/subsystems/running-the-game.md | **LANE O 2026-09-08.** Corpus is now **11 saves / 9 distinct states**: the 4 old ones plus 5 Zuul (turns 15-23) and 2 Human (turns 2-3) made this session, all `--strict` clean and all `coverage: PROVED` under `state_checksum --tree`. See the individual rows for what each unblocks. | save_reader.py | verify | verified | high | 100% | 2026-09-07 | --strict exit 0 on all 3 real saves; 0 resyncs / 0 hint-failures; only raw = RNG blob (2503 B). 29 tests. SAVE_FORMAT.md confirmed | | Ghidra type write-back | meta | verified | high | 100% | 2026-09-07 | structs saved in project (ServerSystem 87f, ServerPlayer 110f, StarFleet, StarShip, StrategyServer partial, 22 nested); 52 serializers + primitives + ~60 spine fns renamed; decompile shows field names | | strategic turn internals (economy/research/colony/movement/diplomacy) | subsystem | mapped | high | 90% | 2026-09-07 | findings/subsystems/strategic-turn-internals.md: budget/RP/trade/bankruptcy formulas, research rolls (unlisted race=1.0, Zuul x2), colony growth, movement, RNG=MT19937; ~600 g_KEY labels in Ghidra | @@ -44,7 +44,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | engine: mars/text | engine | verified | high | 100% | 2026-09-07 | flat-kv, id-manifest, csv; oracle 64/64 (Strings.csv 5722 rows); ctest green | | engine: game/sim formulas | engine | verified | high | 100% | 2026-09-08 | sim-pin merged: all 5 low-confidence formulas pinned (3.3 on protection limit, old-state bankruptcy decisions, hazard curve, money tail, pop bonus); tests 356->466; + game/effects (196 TechIds, 44 typed effects, 254 checks) | | engine: mars/stream + rng | engine | verified | high | 100% | 2026-09-07 | merging: 100% exact dump agreement on 3 saves; typed shapes round-trip byte-identical whole file; RNG = seed(RSeed)+2 twists confirmed; 4 tag-name fixes for SAVE_FORMAT | -| wire-schema channel (layouts -> engine) | engine | verified | high | 97% | 2026-09-08 | **Lane G.** Lane D's serializer recovery now reaches `sots-engine` as a GENERATED WIRE SCHEMA, not struct layouts: `tools/streams.py` -> `objects/streams.json` (386 classes / 2,042 items, disk order preserved, duplicate tags preserved, **every memory fact dropped** - no off/size/sizeof/gaps/strides) -> `tools/gen_stream_schema.py` -> `include/generated/sots_stream_schema.h`. WHY layouts.json was the wrong input: `build()` sorts fields by `off_abs` (**89 of 386 classes** have offset order != write order) and merges duplicate offsets into `alt_tags` (that IS the JewelsOfTheCrown double-tag trap). The table is a SPEC, not a program - the recovery is a linear pass so it cannot see `Write`'s branches and lists conditional fields (StarShip `BQ2`/`hbq`) unconditionally, and flattens container loops; a codec driven off it desynchronises. So hand-written `io()` stays the codec and a new `SchemaProbe` archive + `test_wire_schema.cpp` CHECK it: **56 shapes bound, 657 items matched, 0 mismatches**. FOUND 4 REAL DEFECTS present in BOTH `save_reader.py` and the engine, invisible to any round-trip test: `SystemParams` field 1 is a **string** not an int (empty string == 4 zero bytes == int 0, so it round-tripped by luck; a named planet would have desynced both readers); `ObservedTech/ObservedWeapon.odet` is a **bool** not an int (byte-safe only because a 4-char tag makes bool and int items both 12 bytes); `SpeciesRatios.nv` and `ShipRecords.srbd` are **counts**, not fields. TWO TRAPS RESOLVED: `ServerTradeManager`'s no-op Read/Write is real but the call is **virtual** - `ServerTradeManagerImpl` has the real serializer (`NumTradeSectors`/`TradeID`/`Trade`/`SctSize`), same shape resolves `IServerSpyManager` -> `ServerSpyManager`; general rule: **when an interface's serializer is the inherited stub, look for the concrete `*Impl`**. `ShipDesign::Write` really does make no stream call, but `ShipDesignDef::Section` is recovered so most of `Des` is typeable anyway. COVERAGE, measured by a new `CoverageArchive` that separates items a field NAMES from items a `Node` merely CARRIES (a byte-identical round trip is not a coverage claim): **37.9% -> 97.1%** typed on turn1 (97.2/97.2/97.6 on the others), round trip still byte-identical on all 4 saves. Typed this round: `TechTree` (both NumTechs sections - the 2nd is per-tech state, field list from `SpyReportTechTree` which streams the same record), `Events` (3 nesting levels), `ShipRecs`, `sprjs`, `civr`, `comms`, `spy2`, `spymgr`, `aid`, `Ojvs`, `AIEnf`, `FNG`, `trdmgr`, `Des` sections + gun banks. STILL OPAQUE: `CD` custom data 744 items (TurnCommands_v5 is a no-orders snapshot - **needs a save with issued turn commands**), `SvSctOb` 147 (8 EncObj variants), `DOpts` 94 (`read_elem` has no std::string branch), `spies2` 56, `RNG` 2 (correctly opaque). Ratchet at 95% so typing can't regress. ctest **34/34**, clean-room OK, `test_save` skips cleanly unset. `findings/objects/wire-schema-channel.md`; sots-engine branch `wip/layouts` | +| wire-schema channel (layouts -> engine) | engine | verified | high | 97% | 2026-09-08 | **Lane G.** Lane D's serializer recovery now reaches `sots-engine` as a GENERATED WIRE SCHEMA, not struct layouts: `tools/streams.py` -> `objects/streams.json` (386 classes / 2,042 items, disk order preserved, duplicate tags preserved, **every memory fact dropped** - no off/size/sizeof/gaps/strides) -> `tools/gen_stream_schema.py` -> `include/generated/sots_stream_schema.h`. WHY layouts.json was the wrong input: `build()` sorts fields by `off_abs` (**89 of 386 classes** have offset order != write order) and merges duplicate offsets into `alt_tags` (that IS the JewelsOfTheCrown double-tag trap). The table is a SPEC, not a program - the recovery is a linear pass so it cannot see `Write`'s branches and lists conditional fields (StarShip `BQ2`/`hbq`) unconditionally, and flattens container loops; a codec driven off it desynchronises. So hand-written `io()` stays the codec and a new `SchemaProbe` archive + `test_wire_schema.cpp` CHECK it: **56 shapes bound, 657 items matched, 0 mismatches**. FOUND 4 REAL DEFECTS present in BOTH `save_reader.py` and the engine, invisible to any round-trip test: `SystemParams` field 1 is a **string** not an int (empty string == 4 zero bytes == int 0, so it round-tripped by luck; a named planet would have desynced both readers); `ObservedTech/ObservedWeapon.odet` is a **bool** not an int (byte-safe only because a 4-char tag makes bool and int items both 12 bytes); `SpeciesRatios.nv` and `ShipRecords.srbd` are **counts**, not fields. TWO TRAPS RESOLVED: `ServerTradeManager`'s no-op Read/Write is real but the call is **virtual** - `ServerTradeManagerImpl` has the real serializer (`NumTradeSectors`/`TradeID`/`Trade`/`SctSize`), same shape resolves `IServerSpyManager` -> `ServerSpyManager`; general rule: **when an interface's serializer is the inherited stub, look for the concrete `*Impl`**. `ShipDesign::Write` really does make no stream call, but `ShipDesignDef::Section` is recovered so most of `Des` is typeable anyway. COVERAGE, measured by a new `CoverageArchive` that separates items a field NAMES from items a `Node` merely CARRIES (a byte-identical round trip is not a coverage claim): **37.9% -> 97.1%** typed on turn1 (97.2/97.2/97.6 on the others), round trip still byte-identical on all 4 saves. Typed this round: `TechTree` (both NumTechs sections - the 2nd is per-tech state, field list from `SpyReportTechTree` which streams the same record), `Events` (3 nesting levels), `ShipRecs`, `sprjs`, `civr`, `comms`, `spy2`, `spymgr`, `aid`, `Ojvs`, `AIEnf`, `FNG`, `trdmgr`, `Des` sections + gun banks. STILL OPAQUE: `CD` custom data 744 items (TurnCommands_v5 is a no-orders snapshot - **needs a save with issued turn commands**), `SvSctOb` 147 (8 EncObj variants), `DOpts` 94 (`read_elem` has no std::string branch), `spies2` 56, `RNG` 2 (correctly opaque). Ratchet at 95% so typing can't regress. ctest **34/34**, clean-room OK, `test_save` skips cleanly unset. `findings/objects/wire-schema-channel.md`; sots-engine branch `wip/layouts` | **LANE O 2026-09-08.** **`CD` `TurnCommands_v5` is no longer sample-starved.** Saves taken AFTER issuing orders and BEFORE End Turn grow the block from the universal empty snapshot (`CD[0]` digest `3df7d93164fb1d7d`, 35 leaves / 122 B in turn1/2/3, zuul-turn5 AND human-turn3) to 41 / 61 / 129 leaves. Field-by-field layout in `verify/save-reader/SAVE_FORMAT.md` **section 11**: playerId, researchRate float, per-command-type present-flags, then counted lists of build orders (ordinal, designId, systemId, 0), system-rate commands (systemId + a named `SRs SRt SRsc SRtf SRi SRoh SRnr` frame - the only NAMED sub-frame in the block), colonize orders (shipId, 1) and fleet moves (fleetId, 1, destSystemId, 0). Ids cross-checked against the same file's `Sys`/`Flt` records and against the UI (384 = Gallandro, 432 = Octans, 688 = the fleet the UI showed as Alpha Fleet with Dest: Octans, 216383 = the exact Imperial Savings drop the Boost Research panel caused). The block is the PENDING client->server queue: `zuul-turn15-orders.sav` still has `ResRate 0.25 / ResTNm '' / Sav 2,902,722` on the ServerPlayer while the block already carries rate 0.97 and the boost. Saves `verify/results/saves/zuul-turn15-orders.sav`, `zuul-turn17-orders2.sav`, `human-turn2-orders.sav` | engine: game/data catalogs | engine | verified | high | 100% | 2026-09-07 | merged: WeaponDef/ShipSectionDef/TurretTable/IdRegistry/TechTree/StringTable + cross_check; oracle 229,042 values 0 diffs; crosslink set reproduced exactly; 34 malformed shipped tokens pinned | | engine: mars/vfs (gob) | engine | verified | high | 100% | 2026-09-07 | merged: ZIP reader + native override; 8352+2035 entries = unzip -l; all 10268 files CRC-clean; byte-equal spot checks; ctest 11/11 | | determinism oracle | verify | verified | high | 100% | 2026-09-07 | BYTE-IDENTICAL across 5 runs incl. cross-process: (Autosave).sav 978041ac…, (Autosave EndTurn).sav bb4fd9ac…; gzip MTIME=0; only loaded-post-turn re-save differs (Player.Status 4->0, Summary.Checksum). findings/subsystems/determinism-oracle.md RE-CONFIRMED 2026-09-08 on engine `cef889e` (lane M's movement fix included) by lane F: `bb4fd9ac…` / `978041ac…` unchanged, and reproduced a further 3x under forced control words that leave the arithmetic alone (0x027f/0x127f/0x137f). | @@ -59,19 +59,19 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | P2-B3 ProcessResearch (behavioral, RNG) | phase2 | mapped | high | 85% | 2026-09-08 | LIVE, PARTIAL PASS: 15 calls compared, 13 zero-divergence; RNG post-state matched 14/15 incl. every roll (validates MT19937 + draw mapping + odds together). 2 divergences are the declared SetResearched boundary. ORACLE FAILS by exactly one item across 40,300: an unposted EVENT_RESEARCH_OVERBUDGET - compare was blind because the event list was never a declared region. fpu_cw=0x127f => 53-bit double, x87 question SETTLED. No Zuul in the save: double roll still disassembly-only . RECAPTURED WITH GUARDS 2026-09-08 (lane R): the oracle gap is now a COMPARE DIVERGENCE - `side.events.after.v.next_id orig=4 ours=3` on call 0, its only divergent field, with node[144] progress 2879->5768 and flag 1->2 both reproduced and the single RNG draw identical. 15 calls over 5 turns: 3 diverged, **RNG 15/15** (better than the original 14/15 - no tech-effect draw in this session). Guards on the two completion calls map SetResearched: ConMod[0..2]/OutMod/PopMod, ResTNm, TechTree+0x20 order counter, and the undeclared otch vector | . **LANE V 2026-09-08 (build `eventlive-dd38117-20260908T0916Z`): the events divergence is CLOSED on the reference turn** - 3 calls, 3 compared, **0 diverged, exit 0**. Over 5 turns 15 calls / **2** diverged (was 3), both short by exactly 1 `next_id` = the unmodelled `EVENT_TECHS_UNLOCKED`. **CORRECTION to lane R's 'RNG 15/15': that was WORKLOAD LUCK, not a property.** On lane V's call 9 the completed tech had `research_roll_pending` set, so the original drew one word `ours` did not (`left` 374 vs 375, `next_index` 250 vs 249) - the `OnTechResearched` callback draw, a DECLARED out-of-scope boundary (P-events-wiring §3 says so in as many words). Honest statement: the `rng` region reproduces bit-for-bit on every call that does not complete a roll-triggering tech, and is short by exactly one draw on every call that does. Also live: `sizeof(ObservedTech)` = **44**, measured twice (`observed_techs.bytes` 440->484 and 484->528). Zuul double roll now CLOSED on a species-5 save lane V made - see its own row | RNG signatures (Ghidra) | meta | verified | high | 100% | 2026-09-08 | Seed/Twist/NextFloat/NextInt verified; draw = y/(2^32-1); NextInt [0,n] inclusive; lazy twist; left@+0x9c4. RUNTIME CONFIRMED: fpu_cw=0x127f (53-bit double, round-nearest) - our next_float model is right, float_from_pc24 is an unused contingency | | engine: game/effects | engine | verified | high | 100% | 2026-09-08 | merged: TechId enum (196 slots @10000+i), 44 ids with typed strategic effects, species flag bits, ApplyTechEffect; 254 checks | -| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **O-workloads** (U-unlock released 2026-09-08 06:55 local; V-eventlive before it; F-fpucw before it, M-movefleet before that). QUEUE: empty. VM left at the MAIN MENU, `hooks=trace`, build `recap-7584bad-20260908T0615Z` restored from `C:\SOTS\shimdist-recap` (that dist also carries `shim.cfg.recap{trace,b3,b1,misc}`). Lane M also left `C:\SOTS\shimdist-mf` + `C:\SOTS\ui\mf{deploy,release}.ps1` in place - harmless, and a working template for the next lane. Windows Update DISABLED/paused on the VM. Non-holders build /srv/re-lab/build/sots-engine-, stage dist-, deploy C:\SOTS\shimdist-. GOTCHA (lane R): after `schtasks /Run /TN SOTS` the main menu can take >60 s - SCREENSHOT AND VERIFY before clicking, or the click path lands in Credits. GOTCHA (lane M): drive the load dialog ONE rui.ps1 CALL PER CLICK with a screenshot between - a single chained cmd.txt loses sync and silently ends up somewhere else. And the Load Game dialog does NOT pre-select Single Player on a fresh launch: the documented path really is Load Game (512,536) -> Single Player (512,290) -> OK (551,523) -> row -> OK (682,624) -> Launch (511,663). ref-turn2 row is at (400,436) GOTCHA (lane F, confirms lane R): the >60 s startup is REAL and cost a whole wasted run — do not sleep-and-click, **verify the main menu from a screenshot** (`verify/fpu-cw/` run scripts poll a screenshot until the Load Game / Exit buttons are bright red; 3 probes ≈ 25 s was typical). TIP (lane F): reset `SavedGames\` to a fixed file set before every run — the Load dialog row positions depend on how many files are listed, so a constant set means the click path never has to be re-derived (with the 4-file set ref-turn2 sits at (400,348), not (400,436)). PowerShell over SSH mangles quoting badly: send snippets base64 as `powershell -EncodedCommand`, or use `-ExecutionPolicy Bypass -File`. Lane F left `C:\SOTS\shimdist-fpu` + `C:\SOTS\ui\f{deploy,grab,fpu}.ps1` + `C:\SOTS\ui\preF\` (the pre-lane-F SavedGames snapshot, restored) in place. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, SavedGames back to the 7-file pre-F set, main menu verified by screenshot. GOTCHA (lane V, third confirmation): the >60 s startup is REAL - the Kerberos intro was still on screen at t+30 s and the main menu appeared at ~t+70 s. GOTCHA (lane V): `type ` via the click helper did **NOT** register in the game's text fields this session (neither the Create Game name nor the Save Game file name took SendKeys); the workaround is to accept the default name and **rename the `.sav` on disk** - the Load dialog lists files by FILENAME. SAVEDGAMES IS NOW **8 FILES**: the pre-existing 7 plus `zuul-turn5.sav` (a species-5 save, see the Zuul row). Row positions are UNCHANGED for existing lanes because `zuul-turn5` sorts last: **ref-turn2 is still (400,436)**, zuul-turn5 is the new row 8 at (400,465), rows 29 px apart from y=262 - verified by screenshot `verify/results/shim/eventlive/load-dialog-8files.png`. The three autosaves now belong to the Zuul game. Lane V left `C:\SOTS\shimdist-v` + `C:\SOTS\ui\v{deploy,release,click}.ps1`. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. LANE U (2026-09-08): left `C:\SOTS\shimdist-u` + `C:\SOTS\ui\u{deploy,release}.ps1` in place. SavedGames is STILL 8 FILES (the three autosaves now belong to the Zuul game at turn 15) - **ref-turn2 is still (400,436)**, zuul-turn5 still (400,465). A QEMU-level screenshot (feed `screendump /tmp/vm140.ppm` to `qm monitor 140` on spicy, then scp the .ppm) is more reliable than the click helper's `shot` action and does not need the game window. FOURTH confirmation of the >60 s startup: the Kerberos intro was still on screen at t+45 s, the main menu at ~t+85 s. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. | +| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **O-workloads** (U-unlock released 2026-09-08 06:55 local; V-eventlive before it; F-fpucw before it, M-movefleet before that). QUEUE: empty. VM left at the MAIN MENU, `hooks=trace`, build `recap-7584bad-20260908T0615Z` restored from `C:\SOTS\shimdist-recap` (that dist also carries `shim.cfg.recap{trace,b3,b1,misc}`). Lane M also left `C:\SOTS\shimdist-mf` + `C:\SOTS\ui\mf{deploy,release}.ps1` in place - harmless, and a working template for the next lane. Windows Update DISABLED/paused on the VM. Non-holders build /srv/re-lab/build/sots-engine-, stage dist-, deploy C:\SOTS\shimdist-. GOTCHA (lane R): after `schtasks /Run /TN SOTS` the main menu can take >60 s - SCREENSHOT AND VERIFY before clicking, or the click path lands in Credits. GOTCHA (lane M): drive the load dialog ONE rui.ps1 CALL PER CLICK with a screenshot between - a single chained cmd.txt loses sync and silently ends up somewhere else. And the Load Game dialog does NOT pre-select Single Player on a fresh launch: the documented path really is Load Game (512,536) -> Single Player (512,290) -> OK (551,523) -> row -> OK (682,624) -> Launch (511,663). ref-turn2 row is at (400,436) GOTCHA (lane F, confirms lane R): the >60 s startup is REAL and cost a whole wasted run — do not sleep-and-click, **verify the main menu from a screenshot** (`verify/fpu-cw/` run scripts poll a screenshot until the Load Game / Exit buttons are bright red; 3 probes ≈ 25 s was typical). TIP (lane F): reset `SavedGames\` to a fixed file set before every run — the Load dialog row positions depend on how many files are listed, so a constant set means the click path never has to be re-derived (with the 4-file set ref-turn2 sits at (400,348), not (400,436)). PowerShell over SSH mangles quoting badly: send snippets base64 as `powershell -EncodedCommand`, or use `-ExecutionPolicy Bypass -File`. Lane F left `C:\SOTS\shimdist-fpu` + `C:\SOTS\ui\f{deploy,grab,fpu}.ps1` + `C:\SOTS\ui\preF\` (the pre-lane-F SavedGames snapshot, restored) in place. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, SavedGames back to the 7-file pre-F set, main menu verified by screenshot. GOTCHA (lane V, third confirmation): the >60 s startup is REAL - the Kerberos intro was still on screen at t+30 s and the main menu appeared at ~t+70 s. GOTCHA (lane V): `type ` via the click helper did **NOT** register in the game's text fields this session (neither the Create Game name nor the Save Game file name took SendKeys); the workaround is to accept the default name and **rename the `.sav` on disk** - the Load dialog lists files by FILENAME. SAVEDGAMES IS NOW **8 FILES**: the pre-existing 7 plus `zuul-turn5.sav` (a species-5 save, see the Zuul row). Row positions are UNCHANGED for existing lanes because `zuul-turn5` sorts last: **ref-turn2 is still (400,436)**, zuul-turn5 is the new row 8 at (400,465), rows 29 px apart from y=262 - verified by screenshot `verify/results/shim/eventlive/load-dialog-8files.png`. The three autosaves now belong to the Zuul game. Lane V left `C:\SOTS\shimdist-v` + `C:\SOTS\ui\v{deploy,release,click}.ps1`. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. LANE U (2026-09-08): left `C:\SOTS\shimdist-u` + `C:\SOTS\ui\u{deploy,release}.ps1` in place. SavedGames is STILL 8 FILES (the three autosaves now belong to the Zuul game at turn 15) - **ref-turn2 is still (400,436)**, zuul-turn5 still (400,465). A QEMU-level screenshot (feed `screendump /tmp/vm140.ppm` to `qm monitor 140` on spicy, then scp the .ppm) is more reliable than the click helper's `shot` action and does not need the game window. FOURTH confirmation of the >60 s startup: the Kerberos intro was still on screen at t+45 s, the main menu at ~t+85 s. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. | **LANE O 2026-09-08.** **VM140 is FREE.** Holder was O-workloads; released 2026-09-08 after ~9 turns of Zuul play (turn 15 -> 23) and a fresh 3-turn Human game. **CORRECTION to lane V's `type` gotcha: the click helper's `type` DOES reach the game's text fields - you must CLICK THE FIELD FIRST.** `click 480 234` (the Save Game File Name box) then `type ` worked every time this session, and `click 500 363` + `type` worked for the Create Game name box; lane V's failure was almost certainly an unfocused field. Saves can therefore be named properly and no longer need renaming on disk (the file written is gameName + typedName). Coordinates confirmed this session at 1024x768: in-game menu = round button (1000,714) -> `Save` (937,679) / `Quit to Main Menu` (937,698) -> confirm OK (537,377); Save dialog `Save` (772,233), `Game Successfully Saved` OK (612,383), `Done` (772,542); map panel `Research` (55,124) `Design` (142,124) `Build` (221,124), `Manage Fleets` (60,464) `Move` (155,464) `Special` (229,464), `End Turn` (100,714); Build screen `Add To Queue` (125,714); **Esc leaves the Build and Research screens** - the bottom-left button on the Research screen is `Cancel research`, NOT back, and clicking it pops `Confirm Cancel`. Move mode: send `move X Y` FIRST to read the target's name / Range / ETA off the map, THEN `click`, because that readout is what tells you whether the route is a node route (2-5 turns) or a sublight crawl (37-86 turns). **Chained clicks in one `cmd.txt` DO work for repeated clicks on the SAME button** (`Add To Queue`) but drop about half at `sleep 250` and about 1 in 6 at `sleep 500` - count the result, never assume. End Turn takes 30-45 s under `hooks=trace` with ~25 ships. A mid-turn AI diplomacy popup can steal a scripted click sequence - screenshot after any batch that seems to have done nothing. **VM RESTORED and verified**: quit to the main menu (screenshot), `SavedGames` back to the ORIGINAL 8-file set with the three autosaves restored BYTE-IDENTICAL (sha256 prefixes 0630daaa / e0bf3485 / 1f0dc162), so `ref-turn2` is still (400,436) and `zuul-turn5` still (400,465); lane O's staging dir `C:\SOTS\o-stage` removed; `shim.cfg` still `hooks=trace`, build `recap-7584bad-20260908T0615Z` untouched. Lane O's 7 saves live in the REPO ONLY - push one back with `scp verify/results/saves/.sav re@192.168.10.139:C:/SOTS/SavedGames/` when a compare needs it, and remember that adding a file shifts the Load dialog rows (`MyGame*` sorts BEFORE `MyGameverify1*`) | Zuul double-roll (behavioural) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane V 2026-09-08.** No species-5 save existed, so lane V MADE one on VM140: a custom game with only Zuul left in the Available Species pool (both players Zuul), 4 End Turns under `shim.cfg.recapb3`. **8 calls, 8 compared, 0 diverged, exit 0.** All four researching calls have `species=5` and the generator advances by TWO, not one: `left` 540->538 / 522->520 / 504->502 / 485->483, `next_index` +2 each, `mt` hash unchanged; `ours` reproduced every post-state bit-for-bit. Save `verify/results/saves/zuul-turn5-species5.sav` (48559ab5b719b332, 59,131 B, turn 5, homeworld Gallandro) and on the VM as `C:\SOTS\SavedGames\zuul-turn5.sav`. NOT yet exercised by it: a Zuul tech COMPLETION (node 144 was at ~4,700 of 5,000 after 5 turns - one more End Turn gets there), so `SetResearched` and the Zuul boarding-pod grant are still untouched. Report `findings/subsystems/eventlive-verification.md` §5 **EXTENDED by lane U 2026-09-08: the double roll and the COMPLETION PATH together.** `zuul-turn5` + 10 more End Turns (turn 5 -> 15) under the same config: **20 calls, 20 compared, 0 diverged, exit 0**, with TWO Zuul completions. Call 2 (turn 7, alloc {144,1376}) advances the generator by two (`left` 449->447, `next_index` 175->177) AND runs the unlock cascade in the same call - node 144 -> state 4 / order 21 / turn_researched 7, nodes 132 and 136 -> state 2 with cost_rp 10000 / 16000 and turn_available 7, `next_id` 10->12, `observed_techs` 396->440. Note the ZUUL tree unlocks only 132 and 136 from tech 144 where the HUMAN tree also unlocks 142, so this is an independent instance of the cascade, not a repeat. Trace `verify/traces/unlock-b3-zuul.jsonl.gz` | -| budget tail coverage (expenses/aid/debt) | verify | backlog | — | 0% | 2026-09-08 | 8 ComputeBudget slots were always 0 in ref-turn2 (no sliders, no aid, no debt, no handicap). Need a save with expense sliders, a debtor and a research-aid treaty to exercise ExpenseTotal + the aid/bonus tail . CONFIRMED AND WORSE 2026-09-08 (lane R, 4284 calls): **13 of 22 slots are 0 on every call** - tradeIncome, shipCarriedPop, secondaryManager, bonusIncome, systemIncomeNeg, debtInterest, construction, expenses, researchMoneyGiven, savingsGiven, tra, researchPointsGiven, trp | +| budget tail coverage (expenses/aid/debt) | verify | backlog | — | 0% | 2026-09-08 | 8 ComputeBudget slots were always 0 in ref-turn2 (no sliders, no aid, no debt, no handicap). Need a save with expense sliders, a debtor and a research-aid treaty to exercise ExpenseTotal + the aid/bonus tail . CONFIRMED AND WORSE 2026-09-08 (lane R, 4284 calls): **13 of 22 slots are 0 on every call** - tradeIncome, shipCarriedPop, secondaryManager, bonusIncome, systemIncomeNeg, debtInterest, construction, expenses, researchMoneyGiven, savingsGiven, tra, researchPointsGiven, trp | **LANE O 2026-09-08.** **PARTIALLY unblocked, and three of the remaining slots are now shown to be UNREACHABLE through the 1.8 UI.** Delivered: `[11] construction` and a large `[8] maintenance` are live in `zuul-turn17-orders2.sav` (build queue of 20 DEs) and `zuul-turn23-fleet23.sav` (27 DEs in service, `Maint` 14,500 vs 3,000/1,000 in every earlier save); the empire research slider is off default (`ResRate` 0.97 vs 0.25) in all five Zuul saves. NOT delivered, with reasons. **(a) `[12] expenses`**: `Nexp` is `[]` for EVERY player in ALL 11 saves across turns 1-23, human and AI alike, and no screen in the 1.8 UI (map panel, Build, Research, Ranking, Intel Analysis, Empire Population Manager, in-game menu) exposes a per-category expense slider - the only economic sliders are the empire Savings/Research split and the per-system `SRs/SRt/SRsc/SRtf/SRi/SRoh` rates, which land in `Sys`, not `Nexp`. Working hypothesis: slot 12 is dead in 1.8. **(b) `[10] debtInterest` / `[7] systemIncomeNeg`**: a debtor cannot be manufactured quickly - a ship's savings cost is deducted at QUEUE time so the queue cannot be over-committed, and `Maint` counts only ships IN SERVICE, which arrive at construction-budget/ship-cost per turn (~4.5 DE/turn here). With income floored at 78,570 (Planetary Budget pushed fully to Construction; measured 124,055 -> 78,570 on the panel) that is ~157 destroyers, i.e. ~35 more End Turns. Reachable, just not cheap. **(c) `[13]/[14]/[19]` aid**: a treaty offer DOES occur (the AI offered a non-aggression pact on turn 3 of the Human game and it was accepted - `human-turn3-noderoute.sav`), but the accepted pact leaves the player's `aid` count 0, and SotS 1.8 has no player-initiated treaty UI - proposals are AI-driven events. Research aid needs a longer game and luck, not a click path | hook GetDifficultyMods | meta | backlog | — | 0% | 2026-09-08 | B1 derived the two difficulty rows from trace values (AI maintenance divisor 3, research x1.5) instead of snapshotting them; hook it properly so they stop being constants | | section-loader compare crash | verify | backlog | — | 0% | 2026-09-08 | SectionDictionary compare crashes the engine while the identical weapon path succeeds -> fault is in re-running LoadSection, not the manifest reader. Next boundary: hook LoadSection itself. docs/M2.md has 3 ranked leads | | P2-B4 colony + movement (behavioral) | phase2 | mapped | med | 70% | 2026-09-08 | LIVE: 36 calls compared, 0 divergences, tracecmp exit 0. Scout's headline: RNG left-delta 0 and mt hash identical on ALL 28 systems (only ProcessRebellion draws; none fired) - fpu_cw 0x127f confirmed. One real fleet move reproduced bit-for-bit; PlanFleetMovement's schedule matched. 3 prototypes + 6 helpers VERIFIED and written back to Ghidra; 22 formula corrections. THREE hook bugs found by reading the trace, not the verdict (stale args from describe_args-before-regions; StrategyServer has TWO bases 4 bytes apart; off_Fleets was a Ghidra-base number used as raw) - each would have given a clean compare that checked nothing. COVERAGE IS THIN: only 3 owned systems, 1 moving fleet, gate traffic all-zero; bats2, plague, rebellion, slaves, terraform, jumps, arrivals untested. No replace mode (input boundary). VM released: main menu, hooks=trace, build b4-fix2-20260908T0615Z . **QUALIFIED 2026-09-08 by lane R:** with 45 MoveFleet calls over 5 turns instead of 7 over 1, **8 of the 15 moving calls diverge by 1 ULP of position**. B4's clean verdict was a ONE-SAMPLE verdict - fleet 34 (its only mover) still matches bit-for-bit; the bug appears once other fleets move. ProcessTurn recaptured at 140 calls / 0 divergences, but only `ntdev` (15x) and `rcex` (7x) ever moved: everything else was byte-identical on all 140, so the verdict bounds two counters. Guard found the AI home system's fleet vector growing every turn (the build queue emitting a ship) | | harness gap: undeclared side-effect lists | verify | verified | high | 100% | 2026-09-08 | FIXED STRUCTURALLY (engine 3f0721f+): compile-time-required Coverage on every descriptor (a hook without one does not compile); Guard regions that catch AND localise undeclared writes (names player+0x2b0, not 'the hash moved'); replace mode now emits records; tracecmp prints coverage on every report + --strict-coverage. A hook claiming 'complete' while a guard caught an undeclared write now counts as a DIVERGENCE. Audit found 23 undeclared side effects: docs/harness-audit.md | -| RollResearchEvent draw (behavioural) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane R.** On turn 6 (`IND_TRKSTL`, tech 10094) `research_roll_pending` was true going in; the original drew exactly one word (rng `left` 375->374, `next_index` 249->250) and cleared the flag, and `ours` reproduced both bit-for-bit on the scratch generator. 0 divergences on that call. Needs 5 End Turns from ref-turn2 to reach - the reference turn has no completion at all | . RE-OBSERVED by lane V from the CALLER side 2026-09-08: on the B3 hook's own call 9 the same draw shows as an `rng` divergence (`left` 374 vs 375) plus guard spans `player+0x3b4` (`ResErrRoll`) and `player+0x196` (byte 2 of design mask B at `+0x194`). Still NO save that starts with `research_roll_pending` true - the flag has only ever been seen set and cleared inside one turn +| RollResearchEvent draw (behavioural) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane R.** On turn 6 (`IND_TRKSTL`, tech 10094) `research_roll_pending` was true going in; the original drew exactly one word (rng `left` 375->374, `next_index` 249->250) and cleared the flag, and `ours` reproduced both bit-for-bit on the scratch generator. 0 divergences on that call. Needs 5 End Turns from ref-turn2 to reach - the reference turn has no completion at all | . RE-OBSERVED by lane V from the CALLER side 2026-09-08: on the B3 hook's own call 9 the same draw shows as an `rng` divergence (`left` 374 vs 375) plus guard spans `player+0x3b4` (`ResErrRoll`) and `player+0x196` (byte 2 of design mask B at `+0x194`). Still NO save that starts with `research_roll_pending` true - the flag has only ever been seen set and cleared inside one turn **LANE O 2026-09-08.** **The 'no save starts with the flag true' gap is CLOSED, and the exact condition is now a number.** `verify/results/saves/zuul-turn17-rollpending.sav` has player 16 `ResErrRoll=True`, `ResTNm='WEP_GrnLas'`, and that node at `TResDone 1083 / TResCost 5000` = ratio 0.217; `zuul-turn16-noderoute.sav` is the same state at ratio 0.000. The gate constant at **0x00a2c788 is a float 0.5**, not a double (read out of `dumps/sots.exe` by PE section walk; an 8-byte read gives 5.28e13 garbage and 0x00a2c78c is float 100.0) - recorded as `ResearchRollProgressThreshold` in `ghidra/addresses.d/lane-o.json`. So `ResErrRoll` survives into `ProcessResearch` exactly while progress/cost <= 0.5 AT THE START of the turn, and the `OnTechResearched` draw can only fire when ONE turn supplies more than half the target's cost. Confirmed live end to end: target set turn 15 (ratio 0), funded 1,083 RP/turn at 97% research, flag still true entering turns 16 and 17, tech completed on turn 21 with the flag ALREADY CLEARED - the ProcessTurn roll had fired when the ratio crossed 0.5. To make the draw fire you need a tech whose remaining cost is under one turn's RP; on this empire (1 colony, income 78-124k, 1,083 RP/turn, cheapest available tech 5,000 RP) that is ~2.3x the available RP, so it needs a bigger empire or a cheaper tech, not a different click path. The in-game **Boost Research** panel (Research screen, `Boost Research` twisty) spends savings on the current target and IS recorded in `TurnCommands_v5`, but its per-turn pool is only ~7.5% of Imperial Savings and decays geometrically within the turn (197,271 then 17,551 then 1,561 out of 2.9M), so it cannot bridge the gap either | golden-trace recapture (post-guards) | verify | verified | high | 100% | 2026-09-08 | DONE on the live game, build `recap-7584bad-20260908T0615Z` (NO source change needed - the audit's machinery did all of it). **B3 ProcessResearch: the defect is VISIBLE** - `side.events.after.v.next_id orig=4 ours=3`, one divergent call of 3 on the reference turn and its ONLY divergent field; bit-for-bit the `EvNxID 4->3` that previously needed a 609 KB save diff. Over 5 turns 15 calls / 3 diverged, **RNG matched 15/15**, and the two completion calls miss TWO event ids each. **B1 ComputeBudget: verdict held** - 4284 compared, 0 diverged, exit 0, `budget_object` guard caught 0 undeclared writes (Budget+0x64 never changed value). **MoveFleet: 8 of 45 diverge by 1 ULP of position** (new; see its own row). First guarded captures for OnTechResearched (2 calls), ServerSystem::ProcessTurn (140 calls) and MoveFleet (45). Guards mapped SetResearched live (ConMod[0..2], OutMod, PopMod, ResTNm, TechTree+0x20 order counter) and found an UNDECLARED `vector otch` append at player+0x274. Oracle held on every run's first End Turn. Report `findings/subsystems/golden-trace-recapture.md`; engine `docs/R-recapture.md`; traces `verify/traces/recap-*`, reports `verify/results/compare/recap-*`. sots-engine branch `wip/recapture` e50d5e5 (merged with main 82ef52f; ctest 32/32, clean-room OK) | | MoveFleet position rounding (1 ULP) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane M.** Mechanism read off the instruction stream, not fitted: the engine's `Mars_Vec3_Normalize` (0x00422520, 123 callers) narrows to float32 FOUR times - `sumsq = f32(x*x+y*y+z*z)` (products/adds stay in 53-bit regs, only the SUM is stored), `len = f32(sqrt(sumsq))`, `inv = f32(1.0/len)` a RECIPROCAL that is MULTIPLIED through rather than three divides, and `dir.c = f32(delta.c*inv)`; and MoveFleet stores each `dest.c - pos.c` BACK TO A FLOAT32 SLOT before calling it, and takes the leg distance from that same call's return value. `ours` did all of it in double. The position tail (`f32(pos + f32(dir*move))`) was already right, which is exactly why the error was a constant ABSOLUTE ~1.2e-7. Confirmed OFFLINE first (an arrival copies the destination verbatim, so calls 115/155 hand you fleet 34's and fleet 50's exact float32 destinations = 8 fully determined legs; the 5-narrowing model reproduces the ORIGINAL bit-for-bit on all 8, the old double model reproduces `ours` on the 3 divergent ones), then LIVE: control run 8/45 diverged exit 1, fixed run **0/45 diverged exit 0**, with identical args, identical pos.before and identical ORIGINAL pos.after on all 45 calls. Report `findings/subsystems/movefleet-position-rounding.md`, engine `docs/M-movefleet.md`, branch `wip/movefleet` 2aa8cba | | undeclared ObservedTech append | verify | verified | high | 90% | 2026-09-08 | NEW (lane R). A tech completion grows `vector otch` at ServerPlayer+0x274 (all three vector words move = a realloc). Seen as an undeclared write by BOTH the ProcessResearch `player` guard and the OnTechResearched `player` guard. It is serialized ServerPlayer state and it is in NO coverage note anywhere - a third list append in the same neighbourhood as the event list. B3's replace oracle never saw it because turn 1 of ref-turn2 has no completion **MODELLED AND LIVE-VERIFIED by lane U 2026-09-08.** `ours` now models the append DECISION - it scans the owner's otch element NAMES pre-call (the original appends during the call, so a scan taken afterwards would find the tech present and "agree" with a count it never computed) and reproduces `RecordObservedTech`'s de-duplication by name, then moves the SCRATCH header's byte span by one 0x2c element per append. `observed_techs.bytes` now matches on every call in all three runs (440->484, 484->528 on the five-turn; 396->440, 440->484 on the Zuul). STILL NOT MODELLED: the element's own fields (turn_first/turn_last/detected/name/with) - no region can see them | **APPEND SITE NOW NAMED (lane X): `RecordObservedTech` 0x007ba1a0, called from OnTechResearched; stride 0x2c.** Still needs a declared region + a model in ours. . PARTLY ADDRESSED (lane P + lane V 2026-09-08): it IS now a declared **Result** region (`observed_techs`, ServerPlayer+0x274), so it has left the guards' undeclared list and appears in the diff instead - lane V measured the span growing by exactly 44 on both completion calls (440->484, 484->528) and `ours` short by that 44 each time. STILL OPEN: `ours` does not append, so the region diverges on every completion. The element and the append site are fully pinned (lane X/S: sizeof 0x2c, RecordObservedTech 0x007ba1a0, DE-DUPLICATES BY TECH NAME), so what is left is modelling it - and that belongs with whoever takes SetResearched | unnamed offsets from guard hits | verify | backlog | — | 0% | 2026-09-08 | NEW (lane R). Three spans the guards report every run and no addresses.json entry names: **ServerSystem+0xd8 (1 B)** and **ServerSystem+0x238 (4 B)** - written by the AI home system on every colony turn, alongside the fleet-vector growth; **StarFleet+0xdc (1 B)** - written on every moving MoveFleet call, just past Speed (FPsp2 @0xd8). Cheap wins for the contract | -| waypoint types 2-5 have no coverage | verify | backlog | — | 0% | 2026-09-08 | NEW (lane M, promoted from a coverage line to its own row because it is now the biggest gap in `MoveFleet`). Types 2 (node line), 3 (node route), 4 (gate teleport) and 5 (probabilistic jump) have NEVER fired in any capture, and the node-line step is WRONG BY CONSTRUCTION - `sim::NodeLineStep` and `sim::BuildStutterSegments` are written and host-tested but are NOT wired into the hook, which steps every waypoint type as `speed x dt`. **`ref-turn2` structurally cannot exercise them**: lane M held the VM and tried. The only mover in that save is the AI, which travels straight runs; the player that would travel a node line has `DE 00 CR 00 DN 00` at its home system (screenshot `verify/results/shim/mf-human-home-no-ships.png`), so Move/Manage Fleets are greyed out on every turn. Needs a ship built over several turns, or - much cheaper - a PURPOSE-BUILT SAVE with a fleet already in orbit next to a node line. Same save would unblock the gate-traffic and probabilistic-jump rows. Also owed on that path: `sim::Distance` is still plain double, and `Mars_Vec3_Length` (0x004224b0) says every vector length in the engine is float32-narrowed twice, so the stutter geometry is probably 1 ULP out the same way the position update was - deliberately left alone by lane M because there is no behavioural evidence to correct it against | +| waypoint types 2-5 have no coverage | verify | backlog | — | 0% | 2026-09-08 | NEW (lane M, promoted from a coverage line to its own row because it is now the biggest gap in `MoveFleet`). Types 2 (node line), 3 (node route), 4 (gate teleport) and 5 (probabilistic jump) have NEVER fired in any capture, and the node-line step is WRONG BY CONSTRUCTION - `sim::NodeLineStep` and `sim::BuildStutterSegments` are written and host-tested but are NOT wired into the hook, which steps every waypoint type as `speed x dt`. **`ref-turn2` structurally cannot exercise them**: lane M held the VM and tried. The only mover in that save is the AI, which travels straight runs; the player that would travel a node line has `DE 00 CR 00 DN 00` at its home system (screenshot `verify/results/shim/mf-human-home-no-ships.png`), so Move/Manage Fleets are greyed out on every turn. Needs a ship built over several turns, or - much cheaper - a PURPOSE-BUILT SAVE with a fleet already in orbit next to a node line. Same save would unblock the gate-traffic and probabilistic-jump rows. Also owed on that path: `sim::Distance` is still plain double, and `Mars_Vec3_Length` (0x004224b0) says every vector length in the engine is float32-narrowed twice, so the stutter geometry is probably 1 ULP out the same way the position update was - deliberately left alone by lane M because there is no behavioural evidence to correct it against | **LANE O 2026-09-08.** **TYPE 3 (NodeRoute) IS NOW COVERED - and type 2 is shown NOT to be produced by ordinary movement of EITHER node-drive race.** Waypoint census over the corpus (`Tp` values): turn1/turn2 none, turn3-state 1x type 1, `zuul-turn5-species5` **2x type 3** (already on disk since lane V - nobody had looked), and the new saves 6 / 8 / 11 / 11 / **16** x type 3 (`zuul-turn15-orders` through `zuul-turn23-fleet23`) plus 3x type 3 in `human-turn3-noderoute`. Includes the PLAYER'S OWN fleet: `zuul-turn16-noderoute.sav` has `Flt 688 HFPlan=True wpts` = 1 x (`Wpt 432`, `Tp 3`, `nrt` = (`nrp -1`, `nrf 384`, `nrt 432`)) - Gallandro to Octans, in nodespace - and `human-turn3-noderoute.sav` has (`Wpt 128`, `Tp 3`, `nrt` = (`nrp 37`, `nrf 224`, `nrt 128`)) where `nrp` is the `NdGr2` path id being ridden. **Type 2 negative result**: Zuul (rip-bore route over a natural node line) and Human (node drive over a single directly-connected node line, Downbelow->Delphi, and over multi-hop routes) BOTH emit type 3 - single-hop (`nrp -1`) and multi-hop alike, planned and in-transit alike, 20+ instances, zero type 2. So `WaypointKind::NodeLine` is not what a Move order produces for a node race; the remaining candidates are Hiver gate traffic (4/5) and some non-order path into node space. Types 4 and 5 still need a HIVER game. Mechanic worth knowing for the next lane: a Zuul fleet WITHOUT a Rip Bore is refused node travel outright (dialog: 'cannot reach X using nodes. Do you want to plot a sublight course?'), and node-connected destinations are identifiable from the Move-mode readout by a short ETA (2-5 turns) versus a 37-86 turn sublight crawl | ref-turn2 has no tech completion | meta | verified | high | 100% | 2026-09-08 | TRAP for anyone writing a workload (lane R). The documented one-End-Turn recipe produces **zero** `OnTechResearched` calls - an empty log that still passes. It takes 5 End Turns (to turn 7) to reach a completion. Also: only the FIRST End Turn is reproducible - it hashed to the oracle on all four runs and its research calls reproduce docs/B3.md exactly, but from turn 4 the AI picks a different research target than B3 recorded while the point totals stay nearly identical. Treat anything past turn 1 as *a* run, not *the* run | | B1 replace double-run | verify | backlog | — | 0% | 2026-09-08 | ComputeBudget replace mode runs the original a second time to harvest budget slots; ComputeOutput repairs ships in orbit as a side effect, so this is a real per-turn double effect on objects no region covers. Needs a design fix (harvest without re-running, or declare+revert) | | ReVa MCP link drop (workaround) | meta | verified | high | 100% | 2026-09-08 | The ReVa MCP client link dropped mid-session while the CT111 server stayed healthy (systemd active, :8080 listening, valid key -> 200). `tools/reva_call.py ''` calls the same server over plain HTTP (initialize -> notifications/initialized -> tools/call; replies are SSE with a leading `id:` line, initialize is plain JSON). Key is NEVER stored in the repo: $REVA_KEY, else ~/.claude.json, else ssh to the CT properties file. Use this whenever mcp__plugin_ReVa_ReVa__* is unavailable | @@ -83,7 +83,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | Summary.Checksum algorithm | objects | blocked | — | 0% | 2026-09-08 | Lane C RULED OUT two candidates so nobody repeats them: NOT a byte sum over the inflated stream, NOT a sum over the int leaves. Each is consistent with the -16 re-save delta but leaves no constant residual across turns | | event posting in ours | phase2 | in-progress | — | 0% | 2026-09-08 | Lane P: make ours actually post events so ProcessResearch's `side.events.after.v.next_id` 4->3 divergence closes. Converts harness-audit row 1 from known-defect to checked, and unbounds B2/B3 whose clean compares currently cover economy fields only | | LAB RULE: no `git add -A` in sots-re while lanes run | meta | verified | high | 100% | 2026-09-08 | MY error, caught by lane M: an integrator `git add -A` in the SHARED `sots-re` clone swept a running lane's in-progress files into commit 9d385a7 mid-run (remainder landed in f5b37c2). Nothing was lost, but authorship and atomicity were. RULE: while any lane is live, the integrator stages sots-re by explicit PATH only (`git add campaign/board.md campaign/DASHBOARD.md`), never `-A`. Lanes own their own subtrees. sots-engine is unaffected - lanes work in per-lane worktrees there, which is exactly why that repo has not had this problem . HAPPENED AGAIN 2026-09-08 (lane V): lane D's commit d7ea0a0 swept lane V's in-flight `ghidra/addresses.json` edit (the ObservedTech LIVE CONFIRMATION prose) into it. No harm this time - the content was correct and is now on main - but it is the same failure mode a third time, and it means a lane can find its own work already committed under another lane's message. Check `git log -- ` before assuming your edit is still unstaged. | -| MoveFleet waypoint types 2-5 | verify | backlog | — | 0% | 2026-09-08 | Still ZERO behavioural coverage after lane M. Not for lack of trying: the only mover in ref-turn2 is the AI (straight runs only), and the player that would travel a node line has DE/CR/DN all 00 at its home system, so Move/Manage Fleets are greyed out every turn - there is nothing to send along the node lines the map draws. Needs a ship built over several turns or a purpose-built save. The type-2 node-line step is still WRONG BY CONSTRUCTION (B4). Also: sim::Distance deliberately left in double (only stutter geometry uses it); Mars_Vec3_Length says it is probably 1 ULP out the same way, but there is zero behavioural evidence to correct it against - do not "fix" it blind | +| MoveFleet waypoint types 2-5 | verify | backlog | — | 0% | 2026-09-08 | Still ZERO behavioural coverage after lane M. Not for lack of trying: the only mover in ref-turn2 is the AI (straight runs only), and the player that would travel a node line has DE/CR/DN all 00 at its home system, so Move/Manage Fleets are greyed out every turn - there is nothing to send along the node lines the map draws. Needs a ship built over several turns or a purpose-built save. The type-2 node-line step is still WRONG BY CONSTRUCTION (B4). Also: sim::Distance deliberately left in double (only stutter geometry uses it); Mars_Vec3_Length says it is probably 1 ULP out the same way, but there is zero behavioural evidence to correct it against - do not "fix" it blind | **LANE O 2026-09-08.** Superseded in part - see the `waypoint types 2-5 have no coverage` row: **type 3 now has 16 instances in one save** (`zuul-turn23-fleet23.sav`) and the player's own fleet is a type-3 mover in `zuul-turn16-noderoute.sav` and `human-turn3-noderoute.sav`. Type 2 was ATTEMPTED as a Human node-drive move over a single directly-connected node line and still came back type 3, so B4's node-line step is not what a player Move order exercises | P2-P event posting in ours | phase2 | verified | high | 95% | 2026-09-08 | HOST-VERIFIED, VM RUN QUEUED (lane F holds VM140). next_id reaches 4 in a host reproduction of recap-b3 call 0, fixture rebuilt from raw bytes at the real 0x1c/0x18/0x74 strides and cross-checked against turn3-state.sav with lane C's state_checksum --tree. Count-only (lane E option a): ours never calls the game's PostEvent and REPLACE MODE WRITES NOTHING - a bumped EvNxID with no record behind it would corrupt the very save the oracle hashes. Three design points: the event scan is taken in describe_args BEFORE the original (taken after, ours would dedup against the original's own posts and agree for the wrong reason); dedup risk is MEASURED and reported as events_dedup_risk, not assumed; KeylessEventText resolves keys to "%s" so the shim carries no prose. VERIFIED from the instruction stream: SetResearched 0x00581e10 calls owner vft+0x10 with (flags>>2)&1 and ProcessResearch passes flags=2, so silent=false and the completion event IS posted - previously only inferable from "EvNxID moved by two". ctest 33/33, shim cross-builds on CT111 (lane P could only syntax-check) | . **LIVE-VERIFIED AGAINST AN ADVANCE PREDICTION (lane V 2026-09-08, build `eventlive-dd38117-20260908T0916Z`, main dd38117 unchanged).** Lane P wrote the expected numbers into `docs/P-events-wiring.md` §4 BEFORE the run; every one held. First End Turn: **3 calls, 3 compared, 0 diverged, exit 0** (was 1 diverged) with `turn=3`, `events_turn_bucket_exists=true`, `events_next_id_in=3`, `events_in_turn_bucket=1`, `events_dedup_risk=0`, no `events_scan_truncated`, `next_id` 3->4 both sides, `turns`/`turns_bytes` 2/48 unchanged, node[144] 2879->5768 + flag 1->2, rng identical. Five End Turns: exit 1 with **2 divergent calls instead of 3**, each short by EXACTLY 1 (`next_id` orig 7 ours 6, orig 12 ours 11) = the unmodelled `EVENT_TECHS_UNLOCKED`, exactly as forecast - 0 divergences there would have been suspicious, not good. §4.3 conversion held too: `player+0x274/0x278/0x27c` left the guard's undeclared list (13 spans in 2 calls -> 10) and appear in the diff as `observed_techs.bytes`. End-Turn oracle byte-identical to lane R's. clean_room_check OK, ctest 33/33 (separate commands). Report `findings/subsystems/eventlive-verification.md`; engine `docs/V-eventlive.md`; traces `verify/traces/eventlive-b3-*`, reports `verify/results/compare/eventlive-b3-*` | EVENT_TECHS_UNLOCKED / the unlock cascade | verify | verified | high | 100% | 2026-09-08 | Lane P FLAGGED RATHER THAN GUESSED. Trigger IS pinned (SetResearched's sweep sets state=2 + stamps turnAvailable sticky at -1; tail loop collects state==2 && turnAvailable==currentTurn) but evaluating it needs the unlock cascade ours deliberately does not run. The driver takes the unlock list as an INPUT and is handed nullptr ("no list") - deliberately distinct from an empty list ("computed, empty"). PREDICTED RESIDUAL: next_id short by exactly 1 on every completion call. Posting it "whenever something completed" would score on this save and be WRONG the first time a completion unlocks nothing - the exact false-pass shape this project keeps catching **CLOSED by lane U 2026-09-08 — by running the cascade, not by posting on completion.** `sots-engine src/game/sim/techgraph.{h,cpp}` implements `PrereqsMet` (0x0057d8e0, AND of ORs; zero groups = TRUE, an EMPTY group = FALSE), `SetResearched` (0x00581e10: stamps, child-cost sweep with a SIGNED min against an INT_MAX sentinel, sticky `turnAvailable`, zero-cost recursion) and the tail collector (0x00587cc3). Wired into the B3 hook in COMPARE MODE ONLY, over the scratch node copies. LIVE: **first End Turn 3/3/0 exit 0; five-turn 15/15/0 exit 0; Zuul 20/20/0 exit 0** — all 22 of lane V's divergent fields gone, `next_id` reaching 7 and 12, and the End-Turn oracle hashes UNCHANGED (`bb4fd9ac…` / `978041ac…`). The unlock list is still an INPUT and still `nullptr` when it could not be computed, so lane P's distinction survives. `findings/subsystems/unlock-cascade.md`; engine `docs/U-unlock.md` (§4 = the advance prediction, §5 = the outcome) | | sizeof(ObservedTech) unpinned | objects | verified | high | 100% | 2026-09-08 | **PINNED (lane X).** `sizeof(Game::ObservedTech) = 0x2c (44)` -- three independent proofs: the magic divide `0x2e8ba2e9 sar 3` (= /44, exact) at 0x0087239f, `imul reg,reg,0x2c` at 0x0087243a / 0x007b735b, and the search stride `add edi,0x2c` at 0x007ba257. **Append site = `RecordObservedTech+0xdf` (0x007ba27f): `lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320`** -- a de-duplicating append, direct callee of OnTechResearched 0x00891790; the realloc through 0x007b5820 is why all three vector words move. Element FULLY MAPPED (lane S, from ObservedTech::Write 0x00817cf0 / Read 0x00817c40): +0x00 vptr 0x00a2439c (RTTI `.?AVObservedTech@Game@@`), +0x04 uint16 `otnF`, +0x06 uint16 `otnL`, +0x08 **bool** `odet` (1 byte), +0x0c std::string `otch` (**0x1c**, so +0x24 is its _Alval, NOT a field), +0x28 int `owith` -- 0x2c exactly, nothing unaccounted. Same shape as Game::ObservedWeapon (Write 0x00817bc0, tag `owep`). Built the general tool the row asked for: `tools/x86disp.py`, an x86 displacement xref scanner (100% code coverage, 0.17% desync). `findings/subsystems/observedtech-append.md` | . **CONFIRMED LIVE (lane V 2026-09-08)**: on the running game the `observed_techs` Result region grew by **exactly 44 bytes** on each of the two tech-completion calls of the 5-End-Turn run (440->484, 484->528), and both non-researching players' vectors measured 880 = 20 x 44 and never moved. Behavioural confirmation of a purely static pin @@ -112,7 +112,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | VM click helper: `type` does not register | meta | verified | high | 100% | 2026-09-08 | The click helper's `type` action does not reach the game's text fields. WORKAROUND: accept the default name and rename the .sav afterwards. Costs a save-naming step in every workload-building lane | | wire-schema channel (engine) | engine | verified | high | 95% | 2026-09-08 | Lane G REJECTED the brief's premise for good reason. objects/layouts.json is a MEMORY-layout projection and was the wrong input: serializers.py's build() sorts fields by off_abs (89 of 386 classes have offset order != WRITE order) and merges duplicate offsets into alt_tags - which is exactly the JewelsOfTheCrown double-tag trap. Lab.layout() already computed true disk order internally and it was being thrown away. New `tools/streams.py` is a SECOND projection keeping order and repeats and DROPPING EVERY MEMORY FACT (no off/size/sizeof/gaps/strides) - so the original's ABI never enters our runtime types, which was the design constraint. Offsets still have their home as `offset` entries in addresses.json for the shim, which legitimately needs them. gen_stream_schema.py -> include/generated/sots_stream_schema.h (386 classes, 2,042 items). SUBTLETY: the on-disk primitive is NOT layouts.json's `kind` - it comes from the stream vftable slot/helper, and a member held as int16/int8 is written by WriteInt and is FOUR BYTES ON THE WIRE | | schema as CHECK not codec | engine | verified | high | 100% | 2026-09-08 | Lane G's key judgement: the table is a SPECIFICATION, not a program. The recovery is a LINEAR pass over Write, so it cannot see branches (StarShip's BQ2 is gated on hbq but listed unconditionally) and it flattens container loops - a codec driven off it DESYNCHRONISES ON THE FIRST BRANCH. So hand-written io() shapes stay the codec and a new SchemaProbe archive walks them WITH EVERY BRANCH TAKEN (the same view the recovery has), LCS-aligned against the table: 56 shapes bound, 657 items matched, 0 mismatches | -| NAMED coverage 38% -> 97% | engine | verified | high | 100% | 2026-09-08 | All four real saves round-tripped byte-identically BEFORE and AFTER - and lane G refused to call that a coverage claim, because `ar.any` bodies round-trip trivially by copying bytes nobody understands. New CoverageArchive separates items a FIELD NAMES from items a Node merely CARRIES: turn1/2/3 37.9/38.8/39.4% -> 97.1/97.2/97.2%; zuul-turn5 42.5% -> 97.6%. Ratchet at 95%. Newly typed: TechTree (both NumTechs sections), Events (3 nesting levels), ShipRecs, sprjs, civr, comms, spy2, spymgr, aid, Ojvs, AIEnf, FNG, trdmgr, Des section/gun-bank tree. STILL OPAQUE, honestly: CD custom data (744 items - TurnCommands_v5 is a NO-ORDERS snapshot, needs a save with issued orders), SvSctOb (147, eight EncObj variants), DOpts (94, read_elem lacks a std::string branch), spies2 (56, count 0 everywhere), RNG (2, correctly opaque) | +| NAMED coverage 38% -> 97% | engine | verified | high | 100% | 2026-09-08 | All four real saves round-tripped byte-identically BEFORE and AFTER - and lane G refused to call that a coverage claim, because `ar.any` bodies round-trip trivially by copying bytes nobody understands. New CoverageArchive separates items a FIELD NAMES from items a Node merely CARRIES: turn1/2/3 37.9/38.8/39.4% -> 97.1/97.2/97.2%; zuul-turn5 42.5% -> 97.6%. Ratchet at 95%. Newly typed: TechTree (both NumTechs sections), Events (3 nesting levels), ShipRecs, sprjs, civr, comms, spy2, spymgr, aid, Ojvs, AIEnf, FNG, trdmgr, Des section/gun-bank tree. STILL OPAQUE, honestly: CD custom data (744 items - TurnCommands_v5 is a NO-ORDERS snapshot, needs a save with issued orders), SvSctOb (147, eight EncObj variants), DOpts (94, read_elem lacks a std::string branch), spies2 (56, count 0 everywhere), RNG (2, correctly opaque) | **LANE O 2026-09-08.** The `CD` line is now actionable: three saves with issued orders exist (see the wire-schema row and `SAVE_FORMAT.md` section 11). `SvSctOb`, `DOpts`, `spies2` untouched | FOUR reader defects invisible to round-trip | verify | verified | high | 100% | 2026-09-08 | Present in BOTH readers, none catchable by a round-trip test. (1) SystemParams field 1 is a STRING, not an int - it is the empty string in every save, four zero bytes, byte-identical to int 0; it round-tripped BY LUCK and a named planet would have desynced both parsers. (2) ObservedTech/ObservedWeapon `odet` is a BOOL not an int (lane D's own golden table already said so); byte-safe only because a 4-char tag makes bool and int items both 12 bytes. (3) SpeciesRatios::nv and (4) ShipRecords::srbd are COUNTS, not fields. Lane G deliberately did NOT patch save_reader.py: every fix is byte-neutral on available saves, and quietly editing the oracle mid-campaign is worse than recording the divergence. THE ENGINE IS NOW THE CORRECTED REFERENCE; the Python oracle carries a logged open item | | RULE: interface stub -> look for the *Impl | objects | verified | high | 100% | 2026-09-08 | `trdmgr` RESOLVED, correcting the earlier "ServerTradeManager has no serializer" finding: the no-op Read/Write is real but THE CALL IS VIRTUAL - ServerTradeManagerImpl has the actual serializer. Same shape resolves IServerSpyManager -> ServerSpyManager. GENERAL RULE: when an interface's serializer is the inherited stub, look for the concrete *Impl. (Game::ShipDesign::Write genuinely still makes no stream call - but ShipDesignDef::Section IS recovered, so most of Des is typeable anyway) | | B3 hook: the completion path is now inside the compare | phase2 | verified | high | 95% | 2026-09-08 | NEW (lane U 2026-09-08). `Game::TechTree::ProcessResearch` in compare mode now reproduces not only what the function itself writes but the whole `SetResearched` completion path: the turn/order stamps, the child-cost sweep, the availability sweep, the newly-available collector that decides EVENT_TECHS_UNLOCKED, the de-duplicating ObservedTech append and the one RNG word RollResearchEvent draws. **35 compared calls across three workloads, 0 divergences, tracecmp exit 0 on all three.** FOUR pre-call reads make it work and each is a trap that reads back a PLAUSIBLE wrong answer if taken after the original: TechTree+0x20 (order counter, post-incremented -> every `order` off by one), ServerPlayer+0x294 (`ResT`, zeroed by the callback -> the extra RNG draw never modelled), +0x3b4 (pending roll, cleared in the same block), and the otch element names (already appended -> the dedup check 'agrees' with a count it never computed). All four are reported as ARGUMENTS so a run is auditable without trusting `ours`. COMPARE MODE ONLY: in replace mode applying half of OnTechResearched (the append and the roll, but not the ~90 tech-effect field writes) would leave the player in a state no code path produces | diff --git a/ghidra/addresses.d/lane-o.json b/ghidra/addresses.d/lane-o.json new file mode 100644 index 0000000..0d03f70 --- /dev/null +++ b/ghidra/addresses.d/lane-o.json @@ -0,0 +1,12 @@ +{ + "entries": [ + { + "name": "ResearchRollProgressThreshold", + "addr": "0x00a2c788", + "convention": "data", + "prototype": "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", + "status": "verified", + "source": "lane O 2026-09-08, PE section walk of dumps/sots.exe + turn-by-turn observation on VM140 (verify/results/saves/zuul-turn1[5-7]-*.sav)" + } + ] +} diff --git a/verify/results/saves/human-turn2-orders.sav b/verify/results/saves/human-turn2-orders.sav new file mode 100644 index 0000000..001acb8 Binary files /dev/null and b/verify/results/saves/human-turn2-orders.sav differ diff --git a/verify/results/saves/human-turn3-noderoute.sav b/verify/results/saves/human-turn3-noderoute.sav new file mode 100644 index 0000000..e61e0bc Binary files /dev/null and b/verify/results/saves/human-turn3-noderoute.sav differ diff --git a/verify/results/saves/zuul-turn15-orders.sav b/verify/results/saves/zuul-turn15-orders.sav new file mode 100644 index 0000000..2a62c8c Binary files /dev/null and b/verify/results/saves/zuul-turn15-orders.sav differ diff --git a/verify/results/saves/zuul-turn16-noderoute.sav b/verify/results/saves/zuul-turn16-noderoute.sav new file mode 100644 index 0000000..2fece38 Binary files /dev/null and b/verify/results/saves/zuul-turn16-noderoute.sav differ diff --git a/verify/results/saves/zuul-turn17-orders2.sav b/verify/results/saves/zuul-turn17-orders2.sav new file mode 100644 index 0000000..093363d Binary files /dev/null and b/verify/results/saves/zuul-turn17-orders2.sav differ diff --git a/verify/results/saves/zuul-turn17-rollpending.sav b/verify/results/saves/zuul-turn17-rollpending.sav new file mode 100644 index 0000000..ceaa194 Binary files /dev/null and b/verify/results/saves/zuul-turn17-rollpending.sav differ diff --git a/verify/results/saves/zuul-turn23-fleet23.sav b/verify/results/saves/zuul-turn23-fleet23.sav new file mode 100644 index 0000000..5492ca2 Binary files /dev/null and b/verify/results/saves/zuul-turn23-fleet23.sav differ diff --git a/verify/save-reader/SAVE_FORMAT.md b/verify/save-reader/SAVE_FORMAT.md index 2941b0a..d3e3218 100644 --- a/verify/save-reader/SAVE_FORMAT.md +++ b/verify/save-reader/SAVE_FORMAT.md @@ -268,3 +268,56 @@ resyncs / 0 hint-failures; the info count drops from 257/259/259 to 197 on turn1 60/62/62 `otnF` "read positionally" notes are gone, and every remaining info is a `"."` item. `test_save_reader.py::TagNameCorrectionsTest` pins each correction and fails a frame that uses the old spelling. + +## 11. `CD` `Player..TurnCommands_v5` — the issued-order block (lane O, 2026-09-08) + +Until now every save in the corpus carried the **same** empty `TurnCommands_v5` frame, so the +block looked structureless. `state_checksum --tree --depth 1` makes that concrete: `CD[0]` is +`3df7d93164fb1d7d`, **35 leaves / 122 B**, byte-for-byte identical in `turn1/2/3-state.sav`, +`zuul-turn5-species5.sav` and `human-turn3-noderoute.sav`. Saves made **after issuing orders and +before End Turn** grow it (all five below are `--strict` clean and `coverage: PROVED`): + +| save | CD[0] | orders issued that turn | +|---|---|---| +| any no-orders save | 35 leaves / 122 B | — | +| `human-turn2-orders.sav` | 41 / 143 B | 1 fleet move | +| `zuul-turn15-orders.sav` | 61 / 226 B | research target + research boost + 5 build + 1 fleet move | +| `zuul-turn17-orders2.sav` | 129 / 498 B | 20 build + 1 system-rates + 1 colonize (3 ships) | + +Body, as read off `zuul-turn15-orders.sav` / `zuul-turn17-orders2.sav` (all items `"."`-tagged, +so this is a positional record, not a named one): + +``` +int playerId (16 = the human player's PID; matches CDT's "Player.00000016.…") +bool ? (True in every sample, orders or not) +float researchRate (0.25 default -> 0.97 after the empire Savings/Research slider) +bool hasResearchTargetCmd ; if set, int techId follows (191 = WEP_GrnLas) +bool hasResearchBoostCmd ; if set, int money follows (216383 -- exactly the + Imperial Savings delta the boost produced) +float ? (0.9992… when a research command is present, absent otherwise) +bool x3 = False (three further command-present flags, never seen set) +int x2 = 0 +int nBuildOrders ; nBuildOrders x { int ordinal; int designId; int systemId; int 0 } + (ordinal is the running build-queue index, not 0-based per turn; + designId 608 = DE Colonizer, 576 = DE Armor; systemId 384 = the + home system that owns the queue) +int 0 +int nSystemRateCmds ; n x { int systemId; frame { float SRs; float SRt; float SRsc; + float SRtf; float SRi; float SRoh; int SRnr } } + (SRsc = 1.0 is the Planetary Budget slider pushed fully to + Construction; this is the only NAMED sub-frame in the block) +int 0 +int nColonizeOrders ; n x { int shipId; int 1 } +int nFleetMoveOrders ; n x { int fleetId; int 1; int destSystemId; int 0 } + … trailing zero words (the unused command lists) to the frame END +``` + +Cross-checks that pin the decode: `384`/`432` resolve to `Sys` **Gallandro**/**Octans** in the +same file, `688` to the `Flt` the UI showed as *Alpha Fleet* with `Dest: Octans`, and `216383` is +the exact `Imperial Savings` drop the Boost Research panel caused. **The commands are pending, not +applied**: in `zuul-turn15-orders.sav` the `ServerPlayer` still reads `ResRate 0.25`, `ResTNm ''`, +`Sav 2,902,722` while the block already carries rate 0.97 and the boost — the block is the +client→server queue that `ProcessTurn` drains. + +Not observed (the flags/lists that stayed zero in every sample): the three unset command-present +bools, and every list beyond the five above.