roadmap: rewrite around the engine as the product; milestone split into Rung A (modulo command stream) and Rung B (needs AI); 13 stale board rows marked superseded
This commit is contained in:
parent
7f4d5c5b89
commit
c4f1c2407a
3 changed files with 130 additions and 74 deletions
|
|
@ -1,31 +1,31 @@
|
|||
# SotS RE campaign — coverage dashboard
|
||||
|
||||
Generated 2026-09-08 16:52 UTC · `sots-re` @ 90fba30,2026-09-08 · `sots-engine` @ 0592104,2026-09-08 (145 commits) · regenerate with `tools/dashboard.py`
|
||||
Generated 2026-09-08 17:04 UTC · `sots-re` @ 7f4d5c5,2026-09-08 · `sots-engine` @ 0592104,2026-09-08 (145 commits) · regenerate with `tools/dashboard.py`
|
||||
|
||||
> **North star:** A functional reimplementation of the engine — behavior-equivalent, NOT byte-for-byte
|
||||
|
||||
## 1. Map coverage (campaign/board.md)
|
||||
|
||||
227 targets · mapped-or-better **195/227** `[█████████░] 86%` · verified **162/227** `[███████░░░] 71%`
|
||||
227 targets · mapped-or-better **207/227** `[█████████░] 91%` · verified **171/227** `[████████░░] 75%`
|
||||
|
||||
| Status | Count | % |
|
||||
|---|---:|---:|
|
||||
| verified | 162 | 71% |
|
||||
| mapped | 33 | 15% |
|
||||
| in-progress | 2 | 1% |
|
||||
| backlog | 28 | 12% |
|
||||
| verified | 171 | 75% |
|
||||
| mapped | 36 | 16% |
|
||||
| in-progress | 1 | 0% |
|
||||
| backlog | 17 | 7% |
|
||||
| blocked | 2 | 1% |
|
||||
|
||||
| Type | verified | mapped | in-progress | backlog | blocked | total |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| objects | 29 | 6 | 0 | 2 | 1 | 38 |
|
||||
| control-flow | 14 | 2 | 0 | 1 | 0 | 17 |
|
||||
| subsystems | 3 | 7 | 1 | 3 | 1 | 15 |
|
||||
| control-flow | 15 | 2 | 0 | 0 | 0 | 17 |
|
||||
| subsystems | 4 | 8 | 0 | 2 | 1 | 15 |
|
||||
| engine | 20 | 0 | 0 | 0 | 0 | 20 |
|
||||
| verify | 42 | 8 | 0 | 18 | 0 | 68 |
|
||||
| phase2 | 10 | 3 | 1 | 2 | 0 | 16 |
|
||||
| verify | 45 | 10 | 1 | 12 | 0 | 68 |
|
||||
| phase2 | 13 | 3 | 0 | 0 | 0 | 16 |
|
||||
| meta | 37 | 6 | 0 | 1 | 0 | 44 |
|
||||
| other | 7 | 1 | 0 | 1 | 0 | 9 |
|
||||
| other | 8 | 1 | 0 | 0 | 0 | 9 |
|
||||
|
||||
## 2. Binary understanding
|
||||
|
||||
|
|
@ -73,8 +73,8 @@ Board `engine:` rows: verified **20**, mapped 0, in flight 0 (of 20) — verifie
|
|||
| P2-M0 shim bootstrap | ✅ verified | 100% | sots-engine 5f5bc41: proxy binkw32.dll (66 exports identical), MinHook, asm-stub trace hoo |
|
||||
| P2-M1 flat-KV config loader | ✅ verified | 100% | LIVE: GlobalConsts::LoadFile hooked; trace 19 calls/1088 regions (tracecmp 0); compare 19/ |
|
||||
| P2-M2 manifest/id registry | ✅ verified | 80% | LIVE: trace 22 calls (tracecmp 0); weapons compare 1/1 0 divergences; REPLACE+oracle byte- |
|
||||
| P2-M3 Mars brace-block parser | ⬜ backlog | 0% | Mars::Script pull tokenizer (Open 0x008cd7d0, ReadToken 0x008cd2f0, Next 0x008cd3e0, SkipB |
|
||||
| P2-M4 gobio VFS read | ⬜ backlog | 0% | choke point: bool __cdecl gobio::ReadFile(const char*, IBuffer**) 0x008d5140; FileSystemSe |
|
||||
| P2-M3 Mars brace-block parser | ✅ verified | 0% | SUPERSEDED (engine mars/parse, oracle 1531/1531). Mars::Script pull tokenizer (Open 0x008c |
|
||||
| P2-M4 gobio VFS read | ✅ verified | 0% | SUPERSEDED (engine mars/vfs, 10268 files CRC-clean). choke point: bool __cdecl gobio::Read |
|
||||
|
||||
## 6. Standalone (`src/app`) — distance to the byte-match
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ Detail: `verify/results/standalone/report.txt`.
|
|||
|
||||
## 8. Open questions
|
||||
|
||||
Open **26** · resolved/parked 11 · backlog items: Now 4, Next 3, Later 2, Breadth queue 7, Parked 1, From the RE how-to 4, Behavioral slice 4
|
||||
Open **26** · resolved/parked 11 · backlog items: 1. The engine today 0, 2. The milestone, restated 2, 3. Engine work to Rung A 0, 4. Future engine modules 5, 5. Workloads to manufacture 7, 6. Multiplayer revival 4, 7. Method and infrastructure 5, Parked 2
|
||||
|
||||
Most recent open:
|
||||
|
||||
|
|
@ -131,11 +131,11 @@ Most recent open:
|
|||
|
||||
## 9. Delta since previous dashboard
|
||||
|
||||
- verified targets: 146 → 162 (+16) · mapped-or-better: 177 → 195 (+18)
|
||||
- engine LOC: 42,377 → 43,313 (+936) · test files: 101 → 106 (+5) · checks: 3,304 → 3,480 (+176)
|
||||
- addresses verified: 838 → 940 (+102) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0)
|
||||
- verified targets: 162 → 171 (+9) · mapped-or-better: 195 → 207 (+12)
|
||||
- engine LOC: 43,313 → 43,313 (+0) · test files: 106 → 106 (+0) · checks: 3,480 → 3,480 (+0)
|
||||
- addresses verified: 940 → 940 (+0) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0)
|
||||
- standalone leaves closed: 5 → 5 (+0) · leaves still diverging: 204 → 204 (+0)
|
||||
|
||||
---
|
||||
warnings: board.md: unknown types subsystems; mars-rng.md: no oracle total row parsed; mars-stream.md: no oracle total row parsed; mars-vfs.md: no oracle total row parsed
|
||||
<!-- dashboard-metrics {"verified": 162, "mapped_plus": 195, "targets": 227, "loc": 43313, "tests": 106, "checks": 3480, "addr_verified": 940, "addr_total": 986, "layouts": 384, "open_q": 26, "sa_closed": 5, "sa_left": 204} -->
|
||||
<!-- dashboard-metrics {"verified": 171, "mapped_plus": 207, "targets": 227, "loc": 43313, "tests": 106, "checks": 3480, "addr_verified": 940, "addr_total": 986, "layouts": 384, "open_q": 26, "sa_closed": 5, "sa_left": 204} -->
|
||||
|
|
|
|||
|
|
@ -1,51 +1,107 @@
|
|||
# Backlog (prioritized)
|
||||
# Roadmap
|
||||
|
||||
Object model + control flow are mapped concurrently. Priority favors targets that unblock the
|
||||
most other work. Groomed by **re-quartermaster**.
|
||||
Groomed 2026-09-08. Replaces the phase-0/phase-2 backlog, all of which shipped. The board
|
||||
(`board.md`) is the ledger of findings; this is the plan. **The product is `sots-engine` — our
|
||||
code.** RE findings are inputs to it, and the measure of progress is how much of the game runs in
|
||||
our engine, verified against the original. Every number here is measured; the source is named.
|
||||
|
||||
## Now
|
||||
1. **Data readers first (reimpl lever)** — implement the Mars brace-block parser + flat-KV +
|
||||
CSV readers. Unlocks tech tree, weapons (207), ship sections (875), races, AI tables,
|
||||
scenarios, tuning — nearly free. Also becomes the first `verify/parsers/`. See
|
||||
`findings/subsystems/data-model.md`.
|
||||
1. **RTTI class inventory** — pull the full class/vftable list from ReVa; this seeds the real
|
||||
object-model target names (currently placeholders on the board).
|
||||
2. **empire/player object** — the root of the game state; cross-ref the save-editor structs.
|
||||
3. **entry → main loop** — establish the control-flow spine in parallel with the object model.
|
||||
## 1. The engine today (`sots-engine` main `0592104`, 31,980 LOC, ctest 45/45, clean-room, MIT)
|
||||
|
||||
## Next
|
||||
4. star system / fleet / ship / tech-tree objects (fan-out once class names are known).
|
||||
5. D3D9 device init → present loop (renderer anchor).
|
||||
6. save load/write path (ties object model to on-disk format; feeds verify/ parsers).
|
||||
| module | LOC | what it is | verification |
|
||||
|---|---:|---|---|
|
||||
| `mars/parse`, `mars/text` | 1,774 | brace-block + flat-KV + CSV readers (the `Mars::Script` pull tokenizer, first-wins keys) | oracle 1,531/1,531 + 64/64 |
|
||||
| `mars/vfs` | 788 | `.gob` ZIP + native-override VFS (the `./Mods` mechanism) | 10,268 files CRC-clean |
|
||||
| `mars/stream` | 5,407 | save reader/writer, 386-class wire schema, `SchemaProbe`, `CoverageArchive` | **100% named coverage, byte-identical round-trip, 11 saves** |
|
||||
| `mars/rng` | 244 | MT19937 + all seven entry points (`NextFloat/NextInt/Chance/NextUInt/FloatRange/IntRangeBell/GaussianRange`) | live: every draw of a turn attributed |
|
||||
| `game/data`, `game/design`, `game/config` | 3,835 | weapons/sections/tech/strings catalogs, design rules, hull class, tuning | 229,042 values 0 diffs; 127/127 designs |
|
||||
| `game/effects`, `game/events` | 1,531 | tech effects, `EventStorage` (turn-bucketed, dedup) | live: research slice 35 calls / 0 div |
|
||||
| `game/sim` | 3,382 | economy (`ComputeBudget`, output term), research + unlock cascade, colony, movement | live: budget 4,437 / output 24,357 / research 35 / movement 45 calls — all 0 div on declared regions |
|
||||
| `game/nav`, `game/combat` | 1,163 | route classifier + failure bits; retreat **planner** | offline vs 58 waypoints / 46 plans, 0 failures; retreat never instrumented |
|
||||
| `app` (`sots_turn`) | 1,754 | **the standalone**: load save → walk all three drivers' phase order → write save; divergence + completion metric | 11/11 saves run; ref pair **209 → 204** leaves, closed 5 / regressed 0 |
|
||||
| `shim` | 769 + hooks | proxy `binkw32.dll` + MinHook trace/compare/replace harness — **scaffolding, to be displaced** | it is the instrument, not the product |
|
||||
|
||||
## Later
|
||||
7. battle-load path (primary bug) — needs the object model + main loop mapped first.
|
||||
8. .gob/DDS/model asset parsers (interop tools).
|
||||
What the engine can do end to end today: read every shipped asset and every save byte-for-byte,
|
||||
run the economy/research/movement/output pieces to the original's numbers, and step a turn with
|
||||
**14 of 44** driver phases and **2 of 37** tail phases modelled (8 committed). It cannot yet
|
||||
reproduce a turn.
|
||||
|
||||
## Breadth queue (next rounds, one Ghidra user at a time; non-Ghidra items can run in parallel)
|
||||
- Combat sim: `GameCombatSim`/`Mars::CombatSim` tick, `CombatCommand*` dispatch (31 types), weapon flight per `weaponclass` (Ghidra)
|
||||
- Renderer: D3D9 init → `Mars::DrawDevice`/`Camera` → present loop; `.fx` shader usage (Ghidra)
|
||||
- Networking: `SNM*`/`FNM*` protocol from the 82 `NetMsgReg_*` factories; lockstep sync + host migration (Ghidra)
|
||||
- AI: `AIPlayer`, fuzzy `IAIRule`, `TacAISquad*`; how the AI CSV tables are consumed (Ghidra)
|
||||
- `Mars::Buffer::gobio` VFS + NativeFileSystem override order (Ghidra, small)
|
||||
- Ship design rules: section sockets/banks → `ShipDesignDef` validation (data + small Ghidra)
|
||||
- Reader/verify: fold strategic-internals formulas into harness checks against the 3 real saves (Python)
|
||||
## 2. The milestone, restated
|
||||
|
||||
## Parked (not on the critical path — resume only with a clear reason)
|
||||
- **Battle-load bug** — one cheap Ghidra question when convenient: does the combat loader use the job API (`0x8fc160`/`0x8fa5b0`) whose completion poll is `TryEnterCriticalSection → Sleep(1000)` @`0x0071ea60`? Dynamic resume: add HDA audio (`qm set 140 -audio0 device=ich9-intel-hda,driver=none`), build Tankers + Extended-Range DEs, move to Markab/Junction, then `verify/harness/profiling/battle-run.sh`.
|
||||
> `sots_turn` loads a save, runs one strategic turn, and writes an autosave that byte-matches
|
||||
> the original's.
|
||||
|
||||
## From the RE how-to (guides/re-windows-2000s-howto.md) — adopt
|
||||
- **Replay + per-turn state-checksum regression harness** (OpenRCT2 model): record multi-turn input scripts, checksum sim state each turn, run in CI; decide the float-parity policy now (x87 vs SSE: i686 shim vs future x64 standalone) — `-mfpmath`/strict or tolerant compare. Extends our single-step End-Turn hash oracle. (verify lane)
|
||||
- **Automated class recovery**: run Ghidra `RecoverClassesFromRTTIScript` (with Decompiler Parameter ID) and/or CMU OOAnalyzer on the 1,924 RTTI classes to build class structs + method-to-class assignment; import VS2010 FID databases for CRT/STL triage. (Ghidra lane, after tech-effects)
|
||||
- **apitrace whole-run D3D9 capture** of the original as the golden trace for the eventual `Mars::DrawDevice` reimpl (RenderDoc can't capture D3D9); **WinDbg TTD** (x86 supported) for data-flow questions like 'who writes ServerPlayer+0x164'. (VM lane)
|
||||
- **Pin game/sim low-confidence formulas** from `findings/subsystems/formula-gaps.md` (bankruptcy limits/stamping, linear hazard curve, money tail, pbon, slider request, [6] net, no speed clamp, decay incl. current) and wire the code-defined tech effects (`findings/subsystems/tech-effects.md`) into game/sim + game/data. (engine lane, next slot)
|
||||
Today showed the milestone as first stated is **underdetermined on the reference pair**:
|
||||
`ModCount` = 2 + one per command applied from every player's block, and on `turn1-state.sav` the
|
||||
human block is empty yet 10 non-driver bumps occur — **the AI's orders are generated during the
|
||||
turn, after the autosave**. `Summary.Checksum` is fed by `ModCount`. So, two rungs:
|
||||
|
||||
## Behavioral slice (the pivot from data to logic — user directive 2026-09-08)
|
||||
Data loaders are done/underway; `game/sim` has never been compared against the original. Verify behavior
|
||||
function-by-function on the End-Turn workload (`ref-turn2.sav`, deterministic, 8 players x 28 systems):
|
||||
1. **B1 ComputeBudget** (pure, no RNG) — the cleanest first brick.
|
||||
2. **B2 OnTechResearched** (tech effects) — needs sim-pin's effects layer.
|
||||
3. **B3 ProcessResearch** (RNG) — snapshot MT19937 pre-call, run ours from a copy; validates RNG+odds+Zuul at once.
|
||||
4. Then, once signatures are verified: ServerSystem::ProcessTurn (colony growth), MoveFleet, and finally
|
||||
StrategyServer::ProcessTurn as a whole-turn compare.
|
||||
Gate: RNG_* and StrategyServer::ProcessTurn signatures are `unverified` — Ghidra pass first.
|
||||
- **Rung A — byte-match modulo the command stream.** Every leaf except `ModCount`,
|
||||
`Summary.Checksum`, and what the AI's own orders touch. **No AI needed.** Near target.
|
||||
- **Rung B — full byte-match.** Needs AI order generation in our engine (§4.1), or recorded AI
|
||||
command blocks fed as input. Rung B is where the engine becomes the game.
|
||||
|
||||
## 3. Engine work to Rung A (ranked by leaves closed per lane)
|
||||
|
||||
| # | engine deliverable | module | RE input (done unless noted) | closes |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **income chain**: `TradePointsToMoney` (`incomeModifier/14000`) + AI difficulty ×1.1 | `game/sim` | lane N/Y; 25-record `BnkEl` oracle, 6/25 match now; **multiplier not on the wire — one Ghidra read** | P01/P02/P03/P05/P06/T31, ~82 of 204 leaves, 2 RNG words |
|
||||
| 2 | **wire `ShipCensus` + alliance mask into `BuildTurnRecord`**, commit T36 | `app` | lanes D2/A2, 480+560 fields 0 mismatch | +24 turnstats leaves, clean |
|
||||
| 3 | **encounter-detection draws** in the generator model | `game/sim` + `app` | lane I bound; **lane H measuring now** | last 2 RNG words |
|
||||
| 4 | **`nve` visibility record** | `game/sim` | one mechanism ×8, unread | 32 leaves |
|
||||
| 5 | **post events into the save's turn bucket** (P11 + tail event phases) | `app` + `game/events` | model exists; wiring only | events subtree |
|
||||
| 6 | **`Player.Status` restore** (S31) | `app` | needs the writer between tail 31 and autosave — **watchpoint** | S31 regression |
|
||||
| 7 | **tail phases** — bankruptcy decision half, turn results outbox swap, per-player sync | `game/sim` + `app` | lane K map; 34/37 stubs | tail subtree |
|
||||
| 8 | Rung B: **AI order generation** | new `game/ai` | §4.1 | `ModCount`, Checksum |
|
||||
|
||||
Rules that hold for every item: formula-held-inputs-missing is **evaluated and reported, not
|
||||
written**; `closed` and `regressed` reported separately, never netted; `verified` in the phase
|
||||
table means compared against the live game and the build fails if it drifts silently.
|
||||
|
||||
## 4. Future engine modules (breadth, ranked by what it unblocks)
|
||||
|
||||
1. **`game/ai`** — `AIPlayer`, fuzzy `IAIRule`, `StrategyAIAgent`. Essentially unread; **proven
|
||||
to gate Rung B**. Known: `AISystem::Write` emits nothing; the 3 `AIAgent` save blocks are cache
|
||||
state (typed, 10 element layouts unexercised).
|
||||
2. **`game/combat`** beyond the planner — resolver (7,641 B, orchestration only) and retreat are
|
||||
mapped; the six pipeline callees and the tanker fold are unread; **nothing here has run under
|
||||
an instrument**. Combat *simulation* (`GameCombatSim`) stays parked.
|
||||
3. **trade + spy managers** — all 11 virtual targets named; four can draw RNG and none has fired
|
||||
(no trade routes / spy program in the corpus).
|
||||
4. **`net/`** — the lockstep protocol. Direct IP works in the original without GameSpy; our engine
|
||||
inherits determinism to the word, so a lockstep peer is mostly serialization we already own.
|
||||
5. **UI** and **renderer** — not on any path; DXVK carries rendering for the original.
|
||||
|
||||
## 5. Workloads to manufacture (an unexercised path is a hypothesis)
|
||||
|
||||
- **orders save → End Turn → autosave** — the Rung-A reference (human commands in-file).
|
||||
- **trade routes + spy program** — the four tail draw sites; `spies2`/`SysMem`/`mts` elements.
|
||||
- **a cruiser** (2 census leaves), **a DN platform** (the third).
|
||||
- **a Liir fleet** — waypoint type 2 is the Liir drive, not a node line; **a Hiver game** — types 4/5.
|
||||
- **a battle with `res->+4` clear and a partial retreat** — resolver + retreat instrumented; tests
|
||||
the `NMnx` prediction and the predicted zero-ship-fleet bug in the original.
|
||||
- `ResErrRoll` true entering `ProcessResearch` — save exists; needs one turn funding > ½ a tech.
|
||||
- expense sliders / debtor / aid — likely dead in 1.8; debtor ≈ 35 turns. Low priority.
|
||||
|
||||
## 6. Multiplayer revival (lane G2)
|
||||
|
||||
- **Tier 0 (hours):** `/concurrent /join 127.0.0.1:3369` — two clients on one VM, no server.
|
||||
Predictions written; queued behind lane H for VM140.
|
||||
- **Tier 1:** availability responder, byte-specified; probably a no-op (fails open on DNS failure).
|
||||
- **Tier 3 (config, not code):** self-hosted OpenSpy/UniSpy with the `swordots`/`Z5gR9Z`/1381
|
||||
row; fix queryport 3369 (not 6500) and add the 16 custom QR2 keys (ids 50–65).
|
||||
- No CD-key code exists; 333networks ruled out (v0 only).
|
||||
|
||||
## 7. Method and infrastructure
|
||||
|
||||
- `guides/method-rules.md` (18 rules) is the contract for every lane brief. Rule 18 — *measure
|
||||
first, the lab exists* — is why the RNG ledger closed in one VM session after three static lanes.
|
||||
- Reachability claims older than lane V2 are **lower bounds**; re-check "no caller" findings with
|
||||
`tools/vtable_map.py`. Never trust Ghidra's function end (five lanes bitten).
|
||||
- Regenerate the determinism-oracle autosaves on VM140 (lane N overwrote them; lane H doing it).
|
||||
- Board rows superseded by later rows are marked `SUPERSEDED` at integration — keep doing it.
|
||||
- Displace the shim: each hook that reaches `replace` with 0 divergences is code the original no
|
||||
longer runs. Track that count; it is the honest "how much of the game is ours" number.
|
||||
|
||||
## Parked
|
||||
- Battle-load bug (thread contention) — resume only with a reason.
|
||||
- Renderer — after the sim byte-matches.
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| Mars entity chain | object | mapped | high | 100% | 2026-09-07 | EntityBase@0x00a36fd0 (10) -> PhysicalEntity@0x00a3710c (12) -> TargetableEntity -> Actor@0x009e2114 (36); mixins RigidBody/Body/NetworkObject/HandleObject/IScriptable |
|
||||
| `Game::GameCombatSim` + `CombatCommand*` | subsystem | backlog | — | 0% | 2026-09-07 | tactical combat (~40 command types) |
|
||||
| D3D9 device init | subsystem | backlog | — | 0% | 2026-09-07 | `Mars::DrawDevice`/`Camera` → Direct3DCreate9/CreateDevice |
|
||||
| `Mars::Buffer::gobio` | subsystem | in-progress | — | 0% | 2026-09-07 | .gob I/O; anchors the verify/ parser |
|
||||
| `Mars::Buffer::gobio` | subsystem | verified | — | 0% | 2026-09-08 | SUPERSEDED (see engine: mars/vfs row). .gob I/O; anchors the verify/ parser |
|
||||
| serializable-types vs save structs | verify | mapped | high | 100% | 2026-09-07 | findings/objects/save-editor-structs.md (~130 structs, exact order); verifier: cross-check vs binary next |
|
||||
| battle-load path | subsystem | mapped | med | 60% | 2026-09-07 | mitigation = affinity pin FUN_0089ee70; sim+load on main thread; see thread-contention row (parked) |
|
||||
| class hierarchy + key vftables | meta | mapped | high | 100% | 2026-09-07 | findings/objects/ghidra-recon.md - RTTI Base_Class_Array read directly; vftables for 8 core classes |
|
||||
| data-model (.gob data files) | subsystem | mapped | high | 100% | 2026-09-07 | findings/subsystems/data-model.md - tech/weapons/sections/races/AI all data-driven |
|
||||
| string / config intel | meta | mapped | high | 100% | 2026-09-07 | findings/subsystems/strings-and-config.md |
|
||||
| Mars brace-block parser | subsystem | verified | high | 100% | 2026-09-07 | verify/parsers/ (mars_data, flat_kv, manifest, effect_txt, verify.py): 1595/1595 files parse, 0 dangling cross-links; catalogs in verify/results/data-catalogs/ |
|
||||
| networking (SNM/FNM + GameSpy) | subsystem | backlog | — | 0% | 2026-09-07 | ~60 SNM strategy msgs, FNM file-xfer/host-migration; lockstep sim |
|
||||
| networking (SNM/FNM + GameSpy) | subsystem | mapped | — | 0% | 2026-09-08 | SUPERSEDED (lane G2: direct IP joins without GameSpy; identity + hosts list recovered). ~60 SNM strategy msgs, FNM file-xfer/host-migration; lockstep sim |
|
||||
| battle-load: thread contention | subsystem | blocked | med | 60% | 2026-09-07 | PARKED (not on reimpl critical path). Profile: findings/subsystems/battle-load-profile.md. Combat not reached (no ships/range/encounters); audio thread inert w/o audio device; candidate = TryEnterCriticalSection->Sleep(1000) job poll @0x0071ea60 + 15.6ms timer. Resume recipe + tools in verify/harness/profiling/ |
|
||||
| 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) |
|
||||
|
|
@ -36,8 +36,8 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| P2-M0 shim bootstrap | phase2 | verified | high | 100% | 2026-09-07 | sots-engine 5f5bc41: proxy binkw32.dll (66 exports identical), MinHook, asm-stub trace hook on Application::Initialize; menu reached; evidence verify/results/shim/. Initialize signature unverified -> asm stubs only |
|
||||
| P2-M1 flat-KV config loader | phase2 | verified | high | 100% | 2026-09-08 | LIVE: GlobalConsts::LoadFile hooked; trace 19 calls/1088 regions (tracecmp 0); compare 19/19, 0 divergences; REPLACE loaded all 19 files and End-Turn autosaves hashed exactly to the oracle; offline replay 0. 3 extra parsers found (string/vec3/rect), pi/180 is a double. sots-engine 8b231c0, docs/M1.md; traces verify/traces/m1-* |
|
||||
| P2-M2 manifest/id registry | phase2 | verified | high | 80% | 2026-09-08 | LIVE: trace 22 calls (tracecmp 0); weapons compare 1/1 0 divergences; REPLACE+oracle byte-identical with our weapon loader feeding the game. Proves 123 weapon ids == manifest, DELETED lines are comments, dict sorted by _stricmp on name, +0x14 layout fix correct. GAP: section dictionary is TRACE-ONLY - ours crashes the engine in compare mode (leads in docs/M2.md; hooking LoadSection is the next boundary) |
|
||||
| P2-M3 Mars brace-block parser | phase2 | backlog | — | 0% | 2026-09-07 | Mars::Script pull tokenizer (Open 0x008cd7d0, ReadToken 0x008cd2f0, Next 0x008cd3e0, SkipBlock 0x008cd4b0); whitespace-only delimiting, quotes " ' ` no escapes, EOF ends, stray } ignored, LAST PAIR W/O TRAILING NEWLINE DROPPED; consumers = _stricmp if/else chains (WeaponDef::ParseScript 0x00599070, SectionDef::ParseScript 0x005744e0, MasterTechTree::ParseTech 0x0058b050). .effect = Mars::TextFileStream (Open 0x008cfb90) |
|
||||
| P2-M4 gobio VFS read | phase2 | backlog | — | 0% | 2026-09-07 | choke point: bool __cdecl gobio::ReadFile(const char*, IBuffer**) 0x008d5140; FileSystemSet ctor 0x008d6d60 pushes NativeFileSystem(".") FIRST then sots.ini [Modules] Mount<N> (fallback sots.gob, sots_local_en.gob); zip lookup case+slash-insensitive 0x008d4a50 -> matches mars/vfs NativeFirst |
|
||||
| P2-M3 Mars brace-block parser | phase2 | verified | — | 0% | 2026-09-08 | SUPERSEDED (engine mars/parse, oracle 1531/1531). Mars::Script pull tokenizer (Open 0x008cd7d0, ReadToken 0x008cd2f0, Next 0x008cd3e0, SkipBlock 0x008cd4b0); whitespace-only delimiting, quotes " ' ` no escapes, EOF ends, stray } ignored, LAST PAIR W/O TRAILING NEWLINE DROPPED; consumers = _stricmp if/else chains (WeaponDef::ParseScript 0x00599070, SectionDef::ParseScript 0x005744e0, MasterTechTree::ParseTech 0x0058b050). .effect = Mars::TextFileStream (Open 0x008cfb90) |
|
||||
| P2-M4 gobio VFS read | phase2 | verified | — | 0% | 2026-09-08 | SUPERSEDED (engine mars/vfs, 10268 files CRC-clean). choke point: bool __cdecl gobio::ReadFile(const char*, IBuffer**) 0x008d5140; FileSystemSet ctor 0x008d6d60 pushes NativeFileSystem(".") FIRST then sots.ini [Modules] Mount<N> (fallback sots.gob, sots_local_en.gob); zip lookup case+slash-insensitive 0x008d4a50 -> matches mars/vfs NativeFirst |
|
||||
| loader prototypes (Ghidra) | meta | verified | high | 100% | 2026-09-07 | findings/subsystems/loader-prototypes.md: 80 entries (79 verified) in ghidra/addresses.json; Initialize = bool(this, AppStartup*) RET 4 (M0 crash explained); 93 fns renamed + 9 structs in Ghidra |
|
||||
| compare harness | verify | mapped | high | 100% | 2026-09-07 | verify/harness/compare/: TRACE_FORMAT.md, tracecmp.py, replay, oracle bridge |
|
||||
| engine: mars/parse | engine | verified | high | 100% | 2026-09-07 | sots-engine: brace-block + .effect readers; oracle 1531/1531; cross-compiles i686; wired into host+shim builds |
|
||||
|
|
@ -71,7 +71,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| 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<ObservedTech> 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 | **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
|
||||
| waypoint types 2-5 have no coverage | verify | mapped | — | 0% | 2026-09-08 | SUPERSEDED (superseded by lane P2 species table). 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 <tool> '<json>'` 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 |
|
||||
|
|
@ -81,9 +81,9 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| ObservedTech append (undeclared) | verify | verified | high | 90% | 2026-09-08 | Lane R's guards caught a vector<ObservedTech> append at `player+0x274` during SetResearched. It is SERIALIZED state and appears in NO coverage note anywhere - found only because guards localise rather than just flag a moved hash. Needs a declared region + a model in ours **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 | . 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
|
||||
| fpu_cw sensitivity experiment | verify | verified | high | 100% | 2026-09-08 | **DONE (lane F): 53-bit vs 64-bit x87 makes NO difference — the SSE port has no double-rounding budget to preserve, `STATE_CHECKSUM.md` §3.5 CLOSED.** 7 End Turns from ref-turn2, 6 control words, whole-state checksum on each. stock / 0x027f / 0x127f / **0x137f (64-bit)** all give `978041ac…` identical across all 35,394 leaves. Two settings DO move state, each reproduced on a repeat run: **0x007f (true 24-bit)** -> `/Sim/systems/Sys[112 "Gamma Cephei"]/Pop2/PopG/PopC 540000000->540000002` (+derived Summary/Checksum); **0x1a7f (53-bit, round-UP)** -> `/Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[0]` and `/Pos/.[2]`, 1 ULP each. So the port must hold intermediates at 53 bits (never compute a chain in `float`) and use round-to-nearest — both SSE defaults, now measured. **THE BRIEFED TRIPLE WAS UNDER-POWERED: 0x027f is 53-bit (differs from 0x127f only in bit 12, infinity control, ignored since the 387) and 0x137f is 64-bit, not a rounding change** — run as written, all three come back identical and would have 'proved' something false. PC=bits 8-9, RC=bits 10-11. Setting PROVEN to hold: read-back at each force + 38 in-pipeline hook samples per run spanning turn phases 4/6/8, all at the forced value (`verify/results/fpu-cw/cw-census.txt`). `Mars::Application::Run` calls `_controlfp(0x50000,0x3070300)` at 0x0089f606 EVERY FRAME = 0x127f, so forcing at EndTurn is wiped before BeginProcessTurn; BeginProcessTurn is the point that works. TRAP: under 24-bit the CRT's own %g rendering degrades, so trace TEXT is not a comparison surface — use `verify/fpu-cw/trace_bitdiff.py`. findings/subsystems/fpu-precision-sensitivity.md |
|
||||
| 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 |
|
||||
| event posting in ours | phase2 | verified | — | 0% | 2026-09-08 | SUPERSEDED (lanes P/V/U: prediction held, cascade closed). 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 -- <path>` 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 | **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
|
||||
| MoveFleet waypoint types 2-5 | verify | mapped | — | 0% | 2026-09-08 | SUPERSEDED (lane P2: type 2 = LIIR drive (naming error), 3 covered by lane O, 4/5 need a Hiver game). 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
|
||||
|
|
@ -140,7 +140,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| ResearchRollPending SETTLED | verify | verified | high | 100% | 2026-09-08 | Lane T, by full displacement scan of +0x3b4 - only TWO consumers in the image. Site A ServerPlayer::ProcessTurn+0x2ac runs AFTER ProcessResearch (+0x165), NOT before as the board previously said: `if (ResT && ResErrRoll && 0.5f < progress/Cost) { roll; ResErrRoll = false; }` - threshold is a STRICT `<` against 0.5f at 0x00a2c788, and THE CLEAR IS INSIDE THE BRANCH, so a failed ratio test leaves the flag set. Site B OnTechResearched+0x51 clears unconditionally given ResT==def. So the flag survives into the next turn IFF progress/Cost <= 0.5 at the end of this one - which is exactly why three sessions gave 0/1/0. SAVE RECIPE: ResErrRoll is a serialized tag right after ResTNm so it can be EDITED INTO a save - set it true, point ResTNm at a high-cost tech with near-zero progress, fund research to >= 1.5x cost |
|
||||
| REPO-WIDE CORRECTION: roll branch costs ONE OR TWO RNG words | verify | verified | high | 100% | 2026-09-08 | EVERY coverage note in the repo says the research-event roll draws "exactly one NextFloat". Lane T: RollResearchEvent itself draws one, but the branch it fires (ServerPlayer_OnResearchRollSucceeded 0x00889d60) draws a SECOND NextInt on the plague path and posts EVENT_PLAGUE_OUTBREAK; the rebellion path allocates an AIRebellion and CANCELS the research. Any RNG accounting that assumes one word is wrong the first time that branch fires - and it has never fired in three sessions, so nothing has caught it |
|
||||
| ResErrRoll arming mechanism UNKNOWN | objects | backlog | — | 0% | 2026-09-08 | Lane T flagged rather than papered over: NOTHING was found that ARMS the flag during play. The only writes are the two clears, ServerPlayer::Read, the constructor (`mov WORD [esi+0x3b4],1` - born TRUE), and a virtual setter pair with no locatable caller. "Born true, consumed once" fits every observation but IS A HYPOTHESIS. The save recipe does not depend on it |
|
||||
| unread driver: OnAllCombatDone_Tail | control-flow | backlog | — | 0% | 2026-09-08 | THE BIGGEST UNREAD BLOCK IS NOT IN ProcessTurn AT ALL. No bankruptcy, no turn results, no turn events, no autosave run in the turn driver - they are all in StrategyServer::OnAllCombatDone_Tail 0x007d92a0, a SECOND driver no lane has read. Highest-value next control-flow target |
|
||||
| unread driver: OnAllCombatDone_Tail | control-flow | verified | — | 0% | 2026-09-08 | SUPERSEDED (lane K read all 36 phases). THE BIGGEST UNREAD BLOCK IS NOT IN ProcessTurn AT ALL. No bankruptcy, no turn results, no turn events, no autosave run in the turn driver - they are all in StrategyServer::OnAllCombatDone_Tail 0x007d92a0, a SECOND driver no lane has read. Highest-value next control-flow target |
|
||||
| player_turn hook prepared (boundary narrowed) | phase2 | mapped | high | 70% | 2026-09-08 | Lane T built the descriptor with compile-time coverage() (9 notes), Result regions for the three phases needing nothing but the player, roll_flags + rng as observations ours never writes, guards over the whole ServerPlayer and the TechTree header. DELIBERATELY NARROWED: phases 2/3/6 (savings, aid records, refund) depend on ComputeBudget's slots and ProcessResearch's overBudget, both STACK LOCALS of the original. The three ways to reach them are calling ComputeBudget ourselves (repairs ships - audit #6), reading the nested hooks (audit #5, SELF-FULFILLING), or inferring from the Sav delta. They are GUARDED, NOT CHECKED; formulas written and unit-tested but not in the verdict. docs/T-turn-driver.md carries the prediction incl. which checks are WEAK BY CONSTRUCTION on the reference save |
|
||||
| INTEGRATOR NOTE: two gate failures caught on lane T merge | meta | verified | high | 100% | 2026-09-08 | Both gates failed on merge and both were real, which is the argument for running them SEPARATELY rather than &&-chained: (1) clean-room exit 1 - a raw FUN_00889d60 identifier had reached docs/ in the public-capable engine repo; fixed by using the name the lane itself had already put in addresses.json (ServerPlayer_OnResearchRollSucceeded). (2) The shim CROSS-BUILD failed -Werror=unused-function on player_turn.cpp - the file is WIN32-only and the lane could not build it on the notes host, exactly as it flagged. Host ctest passed both times and would have hidden both. A lane that cannot cross-build its own shim TU must say so, and the integrator must run the CT111 shim build before pushing |
|
||||
| guides/method-rules.md | meta | verified | high | 100% | 2026-09-08 | The 15 rules this campaign PAID FOR, each traced to the finding that produced it, ranked by damage caused. Written because the integrator was pasting the same hard-won constraints into every lane brief, which invites drift and omission. Lane briefs now REFERENCE it instead of restating it. Covers: green verdict != evidence; predict before the run; static vs behavioural find different bugs; inlined dtor looks like a branch; size by enumeration; unexercised path = hypothesis; round-trip != coverage; two checks sharing an assumption are one check; rankers never filter; derive experiment params from the ISA; correct the record; fix the oracle openly; separate gates; never hand-resolve a generated header; report thin coverage as loudly as divergences |
|
||||
|
|
@ -151,15 +151,15 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| budget tail: partial, three slots shown UNREACHABLE | verify | mapped | high | 60% | 2026-09-08 | DELIVERED: [11] construction and a large [8] maintenance (Maint 14,500 in zuul-turn23-fleet23.sav, 27 DEs, vs 1,000-3,000 in every earlier save); ResRate off default. NOT DELIVERED, WITH REASONS - this is the useful half: [12] expenses - `Nexp` is [] for every player in ALL 11 saves, turns 1-23, human and AI, and NO 1.8 screen exposes an expense slider (map panel, Build, Research, Ranking, Intel Analysis, Empire Population Manager, in-game menu all checked); LIKELY DEAD IN 1.8. [10] debtInterest / [7] systemIncomeNeg - savings cost is deducted at QUEUE time so the queue cannot be over-committed, and Maint counts only in-service ships arriving at ~4.5 DE/turn; income FLOORS at 78,570 (measured 124,055->78,570), so a debtor needs ~157 destroyers = ~35 more End Turns. Reachable, not cheap. aid [13]/[14]/[19] - the AI DID offer a non-aggression pact (accepted, in human-turn3-noderoute.sav) but it leaves aid count 0, and 1.8 has no player-initiated treaty UI |
|
||||
| waypoint type 2: NEGATIVE RESULT | verify | verified | high | 100% | 2026-09-08 | Lane O: type 3 is now covered and TYPE 2 APPEARS NOT TO EXIST FOR MOVE ORDERS. Census: turn3-state 1x type 1; zuul-turn5-species5 ALREADY HAD 2x type 3 on disk (nobody had looked); lane O's saves have 6/8/11/11/16 type 3 plus 3 in the Human game. The PLAYER'S OWN fleet is a mover: Flt 688, Wpt 432, Tp 3, nrt{nrp -1, nrf 384, nrt 432} (Gallandro->Octans, in nodespace). A fresh HUMAN game was built specifically to chase type 2 - Human node drive, single directly-connected node line (Downbelow->Delphi) - and it came back Tp 3, nrt{nrp 37, nrf 224, nrt 128}. Across BOTH node-drive races, single-hop and multi-hop, natural and rip-bored lines, planned and in-transit: 20+ instances, ZERO type 2. WaypointKind::NodeLine is NOT what a Move order produces. Types 4/5 still need a Hiver game |
|
||||
| OnAllCombatDone_Tail mapped | control-flow | verified | high | 90% | 2026-09-08 | Lane K: 36 phases, WHOLE 1587-byte body read from the instruction stream. Base is S (checked against FOUR independent reads). Signature `void __thiscall (S, vector<EncounterResults>*)`, ret 4, called from EXACTLY ONE site - StrategyHost::OnMessage 0x00784d07 on SNMAllCombatDone with msg+4. Strides enumerated from division magics: EncounterResults 0x178, Encounter 0x74, member record 0x44; the arity check between them LOGS AND CONTINUES, no early return. TRAP 1 AVOIDED AND ITS CONVERGE POINT FOUND: phase 7 is encounters.clear(), not a filter - the `if` is the empty-vector guard erase always carries, both arms meet 3 instructions later at 0x007d96bf, and the IDENTICAL four-argument shape appears inside vector<Encounter>::operator= on its empty-source path. Past that point the function is STRAIGHT-LINE: every jcc in phases 8-36 is a loop bound or one of three null tests. Three functions run TWICE per turn across the two drivers. UNREAD (the lane boundary, stated): FUN_007d5af0, 7499 B, the combat resolver under phase 6 |
|
||||
| CRITICAL for the standalone milestone: the tail advances the RNG | verify | backlog | — | 0% | 2026-09-08 | LANE K'S MOST IMPORTANT FINDING. The tail draws from the strategic generator and NOTHING MODELS IT: phase 11 draws one NextFloat PER EXPIRED NODE LINE (instruction-verified at 0x007ae095) and phase 6's subtree draws NextInt/Twist. BOTH RUN BEFORE THE AUTOSAVE. So any reimplementation that reproduces ProcessTurn exactly STILL DIVERGES the first turn a node line expires. This directly threatens the "standalone runs one turn and byte-matches the autosave" milestone and must be modelled before that is attempted |
|
||||
| CRITICAL for the standalone milestone: the tail advances the RNG | verify | verified | — | 0% | 2026-09-08 | SUPERSEDED (lane Z: ledger closed, zero residual). LANE K'S MOST IMPORTANT FINDING. The tail draws from the strategic generator and NOTHING MODELS IT: phase 11 draws one NextFloat PER EXPIRED NODE LINE (instruction-verified at 0x007ae095) and phase 6's subtree draws NextInt/Twist. BOTH RUN BEFORE THE AUTOSAVE. So any reimplementation that reproduces ProcessTurn exactly STILL DIVERGES the first turn a node line expires. This directly threatens the "standalone runs one turn and byte-matches the autosave" milestone and must be modelled before that is attempted |
|
||||
| autosave mechanism read | subsystems | verified | high | 100% | 2026-09-08 | The mechanism our determinism ORACLE rests on, finally read from bytes. StrategyHost::Autosave 0x00895210, called with endTurn=1 from SendEndTurn (pre-turn file) and endTurn=0 from the message handler (post-turn file). The (Autosave Backup) rotation fires ONLY on the post-turn call - the flag byte is (endTurn==0) because the je at 0x00895266 SKIPS the store of 0. FOUR PROPERTIES MAKE IT REPRODUCIBLE: a fixed point in the message sequence; NOTHING IDENTIFYING REACHES THE WRITER (the filename is built here and never passed on); per-player connection state is DETACHED via conn->vft[0x14] before the write and reattached after; and a deterministic gzip container. SaveGame_WriteFile 0x00877070 writes Summary/CreateParams/Sim/CDT + per-agent CD - exactly the root save_reader.py models |
|
||||
| bankruptcy: 3 formula corrections | subsystems | verified | high | 100% | 2026-09-08 | ProcessBankruptcy 0x007c0a50 at phase 15, per-player UpdateBankruptcyLimits at phase 31 - which INSTRUCTION-VERIFIES formula-gaps.md Q1's ordering claim. Three corrections banked: the divisor is the double -0.15000000596046448 (= (double)(float)-0.15f), NOT -0.15; the per-system income term is max(rate,0) BEFORE summing; and 3.3 IS NOT A BINARY CONSTANT - it lives in .bss and is DB-loaded |
|
||||
| turn results: half of lane T's note was wrong | control-flow | verified | high | 100% | 2026-09-08 | The records ARE filled here (phases 6, 11, 18 write S+0x2f4[PlyrIdx], found by a whole-image scan for the 0x11c stride) - but ApplyEncounterResults' tail then SWAPS S+0x2f4 into the outbox S+0x304 and refills the accumulator; SynchronizePlayer dispatches S+0x304[i] as strategy event 0x25; GenerateTurnEvents clears it. sizeof(SETurnResults)=0x11c, enumerated five ways. ALSO: DispatchTurnResults and SendTurnResultsToPlayers ARE MISNOMERS - they are vector::resize and _Ufill; NOTHING in that group sends anything |
|
||||
| turn events are NOT per-turn | control-flow | verified | high | 100% | 2026-09-08 | GenerateTurnEvents is 122 bytes and GENERATES NOTHING (its copy loop at 0x007dc680 is provably dead). BuildTurnEvents is MISNAMED: its whole body is gated on a setup descriptor set in one place on the LOAD/REJOIN path, and it references NO EVENT_* string whatsoever. Also found: TurnEvents_Write/TurnEvents_Read are SWAPPED IN GHIDRA (layouts.json is right); sizeof(TurnEvents)=0x18 enumerated four ways |
|
||||
| CORRECTION: S+0x8 advances TWICE per turn | control-flow | verified | high | 100% | 2026-09-08 | Lane K corrects lane T: OnAllCombatDone_Tail's FIRST INSTRUCTION increments the same word ProcessTurn does, so lane T's "lockstep" conclusion about the two per-turn counters does not hold |
|
||||
| A CORRECTION WITHDRAWN BEFORE PUBLISHING | meta | verified | high | 100% | 2026-09-08 | Worth recording as method, not trivia. Lane K DRAFTED an entry claiming off_RNG was wrongly flagged as a frame exception, then checked and WITHDREW it: addresses.json already carries BOTH frames (off_RNG=0x16c for S, off_RNGPtr=0x168 for S+4), which is exactly lane T's rule, and a second call site confirmed them. The duplicate entry is gone and the near-miss is recorded in the finding. Also: lane K fixed the "exactly one NextFloat" claim across 4 notes + 4 engine files but DELIBERATELY LEFT the captured verify/results/compare/* artefacts alone - they are RUN RECORDS of what the hook said at the time, and editing them would falsify the record |
|
||||
| SNMAllCombatDone on no-combat turns is an INFERENCE | verify | backlog | — | 0% | 2026-09-08 | Lane K's stated caveat: that the message is delivered on turns with NO combat is inferred from the determinism note (the post-turn autosave appears on every End Turn and this handler is its only reachable caller), NOT from the instruction stream. One VM run settles it |
|
||||
| trade manager: nine unidentified virtual calls | subsystems | backlog | — | 0% | 2026-09-08 | Lane K's stated blind spot (its tier 4): phase 23 is NINE CONSECUTIVE VIRTUAL CALLS on the trade manager and NONE is identified |
|
||||
| SNMAllCombatDone on no-combat turns is an INFERENCE | verify | verified | — | 0% | 2026-09-08 | SUPERSEDED (lane Z: tail ran 8/8 turns incl. no-combat). Lane K's stated caveat: that the message is delivered on turns with NO combat is inferred from the determinism note (the post-turn autosave appears on every End Turn and this handler is its only reachable caller), NOT from the instruction stream. One VM run settles it |
|
||||
| trade manager: nine unidentified virtual calls | subsystems | verified | — | 0% | 2026-09-08 | SUPERSEDED (lane V2 named all eleven). Lane K's stated blind spot (its tier 4): phase 23 is NINE CONSECUTIVE VIRTUAL CALLS on the trade manager and NONE is identified |
|
||||
| TurnCommands_v5 typed - 100% NAMED COVERAGE | engine | verified | high | 95% | 2026-09-08 | Lane Q. All ELEVEN saves 99.7-99.9% -> 100.0%; opaque 37/40/43/63/132 -> 2 EVERYWHERE (only the MT19937 blob, correctly opaque). Ratchet 99.8 -> 99.99. Verified by the integrator with SOTS_SAVES_DIR set: 11 saves, `round trip: tree identical, typed identical`, `resyncs 0, hint-failures 0`. THE RECONCILIATION - lane W's arithmetic AND conclusion were right but the disagreement had THREE causes, only one a branch: (1) the sequence lane W aligned against was OFFSET-SORTED, not write-ordered - the writer is flag-then-payload with each gate bool AFTER the payload it gates, so TurnCommands is one of the 89 classes where offset order != write order, and "item 4 is 8 bytes, which can only be a bool where the recovery says i32" is an ARTIFACT OF THE SORTED VIEW. ALIGN SAVES AGAINST streams.json, NEVER layouts.md. (2) The tail is 27 container CALL SITES, not 27 scalars: twenty-seven std::list<T> members at 0x70..0x1a8 stride 0xc allocator-last, each writing WriteInt(size) then records; the linear recovery keeps one item per call site and DROPS THE COUNT WORD, so 44 = 17 members + 27 call sites and lane W's "27 trailing ints, 22 i32 slots" is the same 27 seen from both sides. (3) All 27 lists are ALWAYS written, so an empty one still costs a zero int: 8 prologue + 27 counts = 35, the bit-identical block every earlier save carried. Item arithmetic closes to the unit on all five workloads (35/38/41/61/123) |
|
||||
| CORRECTION to lane O: fleet-move element | objects | verified | high | 100% | 2026-09-08 | Lane Q: the element is {fleetId, nHops, nHops x systemId} - a COUNTED ROUTE VECTOR - not {fleetId, 1, destSystemId, 0}. Only the three-item reading balances: a four-item element needs 26 lists on human-turn2 and 28 on zuul-turn15, and the writer has 27. ALSO: NEITHER noderoute save carries a fleet move, so the node-route UI does NOT queue through that list. SAVE_FORMAT 11 corrected in place |
|
||||
| SECOND defect found at main: NVs PID tag | verify | verified | high | 100% | 2026-09-08 | Rule 6 exactly, and it was live at main. zuul-turn23-fleet23.sav is the FIRST save with a non-empty NVs list; its element's leading id had been typed positionally as "." when the real tag is PID. The typed round trip on that save DIFFERED AT 0x89c14 at engine c883a32, BEFORE any of lane Q's changes - i.e. a real round-trip defect that eleven saves' worth of green tests had never exposed because no earlier save populated the list. Independently corroborated: the recovered table for Game::ServerSystem names that item PID, and the Sys row moves 102 matched/1 wire-only/3 shape-only -> 103/0/2. ALSO: zuul-turn17-orders2 was BELOW the old coverage ratchet, so that gate was already failing on lane O's largest save |
|
||||
|
|
@ -175,7 +175,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| Mars::RNG::NextUInt read completely | objects | verified | high | 100% | 2026-09-08 | 0x004f7670, 84 bytes, EVERY INSTRUCTION READ: `if (left==0) Twist(&mt); y=*next++; --left; temper(y); return y`. EXACTLY ONE WORD, unconditional, no loop, no branch but the lazy twist, no scaling with game state. Ghidra's 84 is CORRECT here (body ends 0x004f76c3, 12 int3 to 0x004f76d0) - worth stating, since rule 17 is about not TRUSTING the size, not about it always being wrong. CONVENTION TRAP: ECX is the RNG OBJECT, not &mt - it does `lea esi,[ecx+4]` itself, where NextFloat/NextInt are entered at object+4, and ProbabilisticJump uses BOTH conventions 0x6b bytes apart |
|
||||
| EncounterDetect_AssignContacts + "a doomed roll still moves the generator" | verify | verified | high | 95% | 2026-09-08 | FUN_007aa240 (Ghidra says 944; REAL BODY IS 953 - Ghidra's end lands inside a `push` at 0x007aa5ee, real end 0x007aa5f9; nine hidden bytes, only a throw stub, no draw-count change, but the range is wrong). Draws ONE INLINED NextFloat PER (contact, detector) TRIAL, BEFORE THE ACCEPT TEST. Threshold 0.25f if the detector's TechTree has tech 0x2728/0x2729 else 0.0f; accept iff thresh >= r, derived from fcompp/test ah,5/jp - EQUALITY ACCEPTS. words = sum over contacts of min(trials-to-first-accept, |detectors|); with no detector teched that is |contacts| x |detectors| EXACTLY and NOTHING IS EVER ASSIGNED - **a roll that can never succeed still advances the generator**. The outer repeat-until-no-progress back-edge (which a truncated dump HIDES) cannot redraw a pair: the tried-bitset is filled ABOVE the back-edge target and never cleared, so at most two passes |
|
||||
| RNG entry points: SEVEN, not three | objects | verified | high | 100% | 2026-09-08 | Lane I: NextFloat, NextInt, Chance, plus NextUInt, FloatRange 0x0047d8a0 (1 word, NARROWS TWICE, in ProcessTurn's closure at depth 3 with two call sites), IntRangeBell 0x008e6d80 (triangular, >=2 words) and GaussianRange 0x008e6e30 (2 words PER ATTEMPT, UNBOUNDED, both draws inlined). AND IT SCALES BY 2^-32 WHERE NextFloat SCALES BY 1/(2^32-1) - TWO DIVISORS IN ONE IMAGE. GaussianRange documented but deliberately NOT modelled |
|
||||
| RNG residual: 18-20 words STILL UNEXPLAINED | verify | backlog | — | 0% | 2026-09-08 | THE HONEST HEADLINE. Lane J predicted the two inlined functions would explain lane Z's per-turn gap. **NOT CONFIRMED.** Both new sources are gated and neither has been measured: 0x007aa240 contributes 0..(|contacts|x|detectors|) and whether its +0xfc gate passed is not knowable statically; ProbabilisticJump's second word is 0 unless a type-5 waypoint fails its arrival test. RESIDUAL: 18-20 words, essentially ALL of it. WHAT IS NOW PROVABLE IS THE NEGATIVE: there is NO TWENTY-THIRD MECHANISM - the complete draw-site inventory of the ProcessTurn closure (1,426 functions) is 22 sites (21 entry-point calls + 1 inlined), so the 18-20 words are distributed among exactly those. THE SEARCH SPACE CLOSES; THE COUNT DOES NOT. One extra bracket on EncounterDetect_ProcessTeamRecord with |contacts|/|detectors| in the argument record turns the formula into a one-turn test |
|
||||
| RNG residual: 18-20 words STILL UNEXPLAINED | verify | verified | — | 0% | 2026-09-08 | SUPERSEDED (lane Z: every word attributed; 16 = trade-raid generation behind a virtual edge). THE HONEST HEADLINE. Lane J predicted the two inlined functions would explain lane Z's per-turn gap. **NOT CONFIRMED.** Both new sources are gated and neither has been measured: 0x007aa240 contributes 0..(|contacts|x|detectors|) and whether its +0xfc gate passed is not knowable statically; ProbabilisticJump's second word is 0 unless a type-5 waypoint fails its arrival test. RESIDUAL: 18-20 words, essentially ALL of it. WHAT IS NOW PROVABLE IS THE NEGATIVE: there is NO TWENTY-THIRD MECHANISM - the complete draw-site inventory of the ProcessTurn closure (1,426 functions) is 22 sites (21 entry-point calls + 1 inlined), so the 18-20 words are distributed among exactly those. THE SEARCH SPACE CLOSES; THE COUNT DOES NOT. One extra bracket on EncounterDetect_ProcessTeamRecord with |contacts|/|detectors| in the argument record turns the formula into a one-turn test |
|
||||
| CORRECTION: no Mars MT19937 variant | objects | verified | high | 100% | 2026-09-08 | Lane I correcting ITSELF (rule 11). It had written into Ghidra that 0xff3a58ad/0xffffdf8c are a Mars variant of MT19937. THEY ARE NOT - they are the textbook masks applied BEFORE the shift: (y & 0xff3a58ad) << 7 == (y << 7) & 0x9d2c5680, verified over 200k words. `mars::rng` in sots-engine was NEVER WRONG. Corrected in place in Ghidra and in the fragment. NOTE FOR FUTURE SCANS: a scan for the TEXTBOOK constants finds NOTHING in this image, which is exactly why rule 16's scan must use these pre-shift values |
|
||||
| STANDALONE SCAFFOLD RUNS | engine | verified | high | 100% | 2026-09-08 | THE NORTH STAR MADE CONCRETE. `sots_turn SAVE --roundtrip --phases --out POST.sav --metric m.json`: loads a real .sav through mars::stream, PROVES THE FOUNDATION FIRST (re-serialises the untouched parse and byte-compares against the inflated stream; if that fails the run STOPS), walks the published phase order of ALL THREE drivers (host steps, StrategyServer::ProcessTurn's 32 with ServerPlayer::ProcessTurn's 12 nested at phase 13, then OnAllCombatDone_Tail's 37), PRINTS EVERY PHASE IT DOES NOT RUN, writes post-turn state back through write_save + gzip, emits a completion metric. Driven over all 11 saves: 11/11 load, run, re-serialise and re-read cleanly. Integrator-verified on CT111 with real saves: ctest 38/38, and the trace output labels hypotheses INLINE (e.g. "3 judged stable by the owned/not-abandoned stand-in (HYPOTHESIS -- the original asks a callee)") |
|
||||
| standalone divergence vs the oracle | verify | mapped | high | 100% | 2026-09-08 | Leaves localised by state_checksum.py, coverage PROVED on every save in every comparison. turn1->turn2: baseline 209 diverge -> 204, CLOSED 5, REGRESSED 0. turn2->turn3: 108 -> 103, closed 5, regressed 0. Closed the same five on both pairs: /Summary/Turn, /Sim/Frame, and ntdev on Gamma Cephei / Ke'Dolarra / Koa'Vo. Remaining 204: /Sim/players 82, /Sim/systems 80, /Sim/turnstats 24, /Sim/SvSctOb 8, plus 10 singletons (ModCount, RNG, Checksum, NMnx, cmbtid, four id lists, NumFlts). BYTE MATCH: NO, AND IT CANNOT BE YET - 0 of the 18-20 per-turn generator words are modelled and the generator is saved state |
|
||||
|
|
@ -193,7 +193,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| BnkEl INVERTS: a per-save oracle for lane N's blocker | verify | verified | high | 100% | 2026-09-08 | THE HIGHEST-LEVERAGE FIND OF THE LANE. BnkEl is injective in maxIncome (slope ~6.67) so it inverts: 25 player-records recovered across the corpus (e.g. turn1-state player 16 -> 238592). That value IS `sum of max(ComputeMaxIncome(s),0)` - the exact population->output term lane N is chasing - available as a PER-SAVE ORACLE NEEDING NO VM, read from bytes the game itself wrote. Tool: tools/max_income_oracle.py. The same inversion independently recovers BANKRUPTCY_PROTECTION_LIMIT_FACTOR = 3.3 from saves (all 25 in [3.299994751, 3.299999872]), corroborating lane K's "3.3 is DB-loaded, not a binary constant" WITHOUT READING THE DATA FILES. Routed to lane N mid-flight |
|
||||
| CORRECTION: the -0.15 divisor defect is much worse than noted | verify | verified | high | 100% | 2026-09-08 | The repo characterised the wrong bankruptcy divisor as roughly "one ulp on large empires". THAT UNDERSTATEMENT IS WRONG - and lane Y corrected its OWN prediction which had repeated it. `-0.15` and the real `-0.15000000596046448` differ for EVERY maxIncome DIVISIBLE BY 3, from m=3 (-20 vs -19), and the rate climbs: 33.3% below ~1.2M, 66.7% at ~1.4M, **100% above 3M**. SIX OF THE 25 CORPUS RECORDS would get a different BnkEl from the engine's current constant than the game wrote. One-line fix in economy.cpp wanting a test at maxIncome=3. Lane Y reported it and did NOT touch the file - it is lane N's |
|
||||
| re-ranked path to the byte-match | meta | mapped | high | 100% | 2026-09-08 | Lane Y's ranking, superseding lane S2's now that the RNG ledger is closed: (1) PER-SYSTEM MONEY OUTPUT - now has a per-save oracle - unblocks P01/P02/P05/P06/T31 and 5 of 11 record fields, ~82 of 204 leaves and 2 of the 4 missing words; (2) S04 alliance mask - 8 of the 17 leaves stopping T36; (3) encounter detection - the last non-research generator residual, HOOK-SHAPED; (4) ship-design catalogue (hull size + 0x400) - census, ship records, part of T24; (5) ModCount - ONE leaf, but it feeds Summary.Checksum, so NO BYTE-MATCH WITHOUT IT |
|
||||
| three hook-shaped questions for the VM holder | verify | backlog | — | 0% | 2026-09-08 | Lane Y raised these per rule 18: bracket DetectEncounters with contact/detector counts (settles the last non-research generator residual); hook CreateRaidEncounter to separate "no roll succeeded" from "candidate list empty" (a word count CANNOT); and run one ZUUL End Turn - THE STANDALONE PREDICTS 14 WORDS THERE, NOT 16, which tests what the raid loop actually iterates over. All three routed to lane N mid-flight |
|
||||
| three hook-shaped questions for the VM holder | verify | in-progress | — | 0% | 2026-09-08 | SUPERSEDED (lane H holds VM140). Lane Y raised these per rule 18: bracket DetectEncounters with contact/detector counts (settles the last non-research generator residual); hook CreateRaidEncounter to separate "no roll succeeded" from "candidate list empty" (a word count CANNOT); and run one ZUUL End Turn - THE STANDALONE PREDICTS 14 WORDS THERE, NOT 16, which tests what the raid loop actually iterates over. All three routed to lane N mid-flight |
|
||||
| CORRECTION: FUN_007d5a00 is RETREAT, not a combat pipeline | control-flow | verified | high | 95% | 2026-09-08 | Lane B5 corrects lane J's characterisation. It is the RETREAT pipeline - and THE LARGEST WRITER OF SAVED STATE ANYWHERE UNDER THE COMBAT RESOLVER. Lane J's "the resolver's own body writes nothing outside its stack context" STANDS; this is where a large part of "its callees write plenty" lives. Seven saved fields/containers move, ALL `verified` in struct-recovery.md so the autosave oracle sees all of them: StarShip.FltID (ships of a partly-retreating fleet re-parented); the StarFleet master list off S+0x64 (one fleet CREATED per partial group, every emptied fleet DESTROYED); **NMnx**, the object-id counter, `inc [ecx+eax*4+0x10]` at 0x008b8b16 once per created fleet; NShips/FtFlg (new fleet born with flag 0x400, retreaters flag 2); LocID/Pos/PrvPos/inline FlightPlan (gate retreat TELEPORTS, normal retreat installs a flight plan); ServerSystem VFlags/EFlags/AFlags/FFlags + NumFlts - AND RETREATING FROM AN UNEXPLORED SYSTEM MARKS IT EXPLORED FOR YOU; ServerPlayer.GTraf + per-system NVs/PlayerView debited and re-credited around every membership and plan change |
|
||||
| OID allocation CLOSED - R2's x16 is a shl 4 | objects | verified | high | 100% | 2026-09-08 | struct-recovery.md 7's long-open "OID allocation (R2's x16)" is answered. The allocator is IDMap::AllocateID 0x008b8ae0; the counter is PER NETWORK NODE at IDMap->+0x08 + node*0x14 + 0x10, pre-incremented, never issues 0; an id is `(counter << 4) | (node & 0xF)`. R2's x16 IS THE shl 4. Found ONLY by following E9 TAIL-CALL THUNKS - an 8-byte jmp thunk hid the whole chain from the first closure pass |
|
||||
| retreat destination rule + a predicted ORIGINAL BUG | control-flow | verified | high | 90% | 2026-09-08 | All six phases instruction-verified, dumped to the next function start. destinations -> group -> classify whole/partial -> split -> execute -> destroy. THE DESTINATION RULE: three INDEPENDENT nearest-system searches in ONE pass - nearest owned, nearest with no hostile presence, nearest at all - first non-null wins. The independence is LOAD-BEARING (each `best` store sits INSIDE its predicate arm); a single-loop version gives a different system. Squared distance, float32 deltas, strict `<` so exact ties go to the EARLIER system. New diplomacy rule: A SYSTEM CAPTURED ON THE CURRENT TURN LOSES ITS OWNER'S CEASEFIRE COVER. FALSIFIABLE PREDICTION WRITTEN BEFORE ANY RUN: on the first turn a battle produces a partial retreat, post-turn NMnx rises by exactly the number of groups with a split fleet - one per GROUP, not per ship or per fleet. PLUS A PREDICTED ORIGINAL BUG: the fleet is created BEFORE the ship loop, so a group flagged partial creates a fleet even when every ship turns out to belong to a wholly-retreating one - A ZERO-SHIP FLEET IN THE SAVE |
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue