Compare commits

...

5 commits

Author SHA1 Message Date
alex
fdea0fde08 lanes CR and DW merged: research stays compared (16 leaves, all OnTechResearched); writer byte-exact on 43 saves; gate locked; exhaustive-config blind spot 2026-09-09 10:13:49 -04:00
alex
d4e9b58d4d plan: ruling on event text — game data via the VFS, not a caveat; CR verdict on 1a 2026-09-09 10:12:19 -04:00
alex
1df4ffb463 lane DW: the writer is byte-exact on all 43 saves; make the gate honest
Track 0 items 0a and 0b.  The finding has the full account; the two things
that matter for anyone reading the gate afterwards:

gate.sh was taking TAIL's exit code from `ctest ... | tail -15`, so it printed
"GATE ok host-ctest" directly above "2 tests failed out of 59".  The same
masking was on the host build and on the shim cross-build.  All three now
capture the real status and trim the output afterwards.

The corpus-skip check counted every "unset, skipped" line, two of which are not
corpus tests, so it could never reach zero.  It is now scoped to the tests gated
on the corpus, SOTS_DATA_DIR is set so game_design_census actually runs against
it (43 saves, 2728 designs, 0 mismatched -- a test that had been skipping), and
every remaining skip is printed by name so none can be invisible again.
2026-09-09 10:10:40 -04:00
alex
7dcc66bc66 lane CR: our code ran instead of ProcessResearch on a real completion; the oracle missed by 16 leaves, all of them OnTechResearched's
Replace mode was tried live on a turn that actually completes a tech, with a
two-process hooks=off oracle established first on that exact (save, procedure,
route). Verdict: game/sim/research stays compared.

What displaced: all 13 tech-tree leaves the turn moves -- 2 from the pass itself
and 11 from the SetResearched cascade -- produced by our code in live game memory,
with the original's ProcessResearch never executing.

What did not: 16 leaves, every one written by ServerPlayer::OnTechResearched.
Five player tech-effect fields (OutMod, ConMod[0..2], ResTNm), one ObservedTech
element, two event records plus EvNxID, and five derived leaves behind them.

Also: ref-turn2 + one End Turn does NOT complete a tech, so every research oracle
before this one was taken on a quiet turn; and a config that names all 27 registered
hooks off and passes check_shim_configs.py still installs six detours, because the
M0 stub and the FPU module's four sampling detours have no hook. key.
2026-09-09 10:05:48 -04:00
alex
ea7b771c76 lane DT: T34 RecordObservedDesigns -- mechanism read from the instruction stream, prediction committed before the build
The phase is FUN_007c2350 (tail phase 34): for every player, over every ship of
every fleet, gated on a two-bit-per-player field at Ship+0x54 that is NOT on the
wire, call RecordObservedDesign(player, ship.design). That function dedups on
odid alone, and on a hit it ERASES and PUSH_BACKS rather than updating in place --
so the list is ordered by last observation and otnF survives the move -- then caps
the list at 20 entries per DESIGN-OWNER, counted from the back, after every single
record call. sizeof(ObservedDesign) = 0x10.

Predictions, computed from a Python simulation of that mechanism over the corpus,
committed before any engine code is written:
  P1 rich turn odes: 55 -> 21, i.e. 34 closed / 0 regressed
  P2 otch+owep (24 leaves): 0 closed / 0 regressed, blocked (see the finding)
  P3 rich-turn total 1092 -> 1058
  P4 canonical pair: 1 closed / 0 regressed
  P5 the NPC-owner guard is worth 4 leaves of regression on the canonical pair
2026-09-09 10:03:48 -04:00
26 changed files with 5579 additions and 28 deletions

View file

@ -1,17 +1,17 @@
# SotS RE campaign — coverage dashboard
Generated 2026-09-09 13:40 UTC · `sots-re` @ c4ea347,2026-09-09 · `sots-engine` @ e7e2bd6,2026-09-09 (245 commits) · regenerate with `tools/dashboard.py`
Generated 2026-09-09 14:13 UTC · `sots-re` @ d4e9b58,2026-09-09 · `sots-engine` @ 52db23c,2026-09-09 (250 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)
447 targets · mapped-or-better **399/447** `[█████████░] 89%` · verified **356/447** `[████████░░] 80%`
453 targets · mapped-or-better **404/453** `[█████████░] 89%` · verified **361/453** `[████████░░] 80%`
| Status | Count | % |
|---|---:|---:|
| verified | 356 | 80% |
| mapped | 43 | 10% |
| verified | 361 | 80% |
| mapped | 43 | 9% |
| in-progress | 5 | 1% |
| backlog | 41 | 9% |
| blocked | 2 | 0% |
@ -22,16 +22,16 @@ Generated 2026-09-09 13:40 UTC · `sots-re` @ c4ea347,2026-09-09 · `sots-engine
| control-flow | 41 | 2 | 1 | 0 | 0 | 44 |
| subsystems | 4 | 8 | 0 | 2 | 1 | 15 |
| engine | 33 | 1 | 0 | 0 | 0 | 34 |
| verify | 105 | 15 | 3 | 35 | 0 | 158 |
| phase2 | 13 | 3 | 1 | 0 | 0 | 17 |
| meta | 94 | 6 | 0 | 1 | 0 | 101 |
| other | 20 | 2 | 0 | 0 | 0 | 22 |
| verify | 106 | 15 | 3 | 35 | 0 | 159 |
| phase2 | 15 | 3 | 1 | 0 | 0 | 20 |
| meta | 95 | 6 | 0 | 1 | 0 | 102 |
| other | 21 | 2 | 0 | 0 | 0 | 23 |
## 2. Binary understanding
- RTTI type descriptors: **1,924** (`Game::` 1,404, `Mars::` 194; serializable types 179)
- Classes with recovered member layouts: **384** / 1,598 named classes `[██░░░░░░░░] 24%` — `objects/layouts.json` (serializer recovery) plus classes recovered by hand in `struct-recovery.md` + `schema-gaps-resolved.md`. Note 179 types are *serializable*; the recovery also reaches non-serializable ones, so this is not a subset of that
- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **1298**, verified **1167** `[█████████░] 90%`
- Functions: **41,411** (parsed from `01-fingerprint.md`); named/annotated in the **address contract** (`ghidra/addresses.json`, not Ghidra's full rename count): **1310**, verified **1179** `[█████████░] 90%`
## 3. Data layer
@ -58,13 +58,13 @@ Generated 2026-09-09 13:40 UTC · `sots-re` @ c4ea347,2026-09-09 · `sots-engine
| `game/sim` | 4,780 | 14 | 1038 | yes | game-sim.md |
| `mars/parse` | 875 | 12 | 277 | yes | mars-parse.md |
| `mars/rng` | 273 | 0 | 0 | yes | mars-rng.md |
| `mars/stream` | 5,834 | 8 | 329 | yes | mars-stream.md |
| `mars/stream` | 6,021 | 8 | 331 | yes | mars-stream.md |
| `mars/text` | 899 | 8 | 245 | yes | mars-text.md |
| `mars/vfs` | 788 | 9 | 140 | yes | mars-vfs.md |
| `shim` | 16,709 | 0 | 0 | direct (WIN32) | H-probes.md |
| `shim/hooks` | 13,593 | 0 | 0 | direct (WIN32) | L1-predictions.md |
| `shim` | 16,777 | 0 | 0 | direct (WIN32) | H-probes.md |
| `shim/hooks` | 13,658 | 0 | 0 | direct (WIN32) | L1-predictions.md |
| `shim/trace` | 2,258 | 9 | 273 | direct (WIN32) | shim-trace.md |
| **total** | **59,514** | **122** | **4257** | | |
| **total** | **59,834** | **122** | **4259** | | |
Board `engine:` rows: verified **33**, mapped 1, in flight 0 (of 34) — verified & merged `[██████████] 97%`
@ -133,11 +133,11 @@ Most recent open:
## 9. Delta since previous dashboard
- verified targets: 354 → 356 (+2) · mapped-or-better: 396 → 399 (+3)
- engine LOC: 59,514 → 59,514 (+0) · test files: 122 → 122 (+0) · checks: 4,257 → 4,257 (+0)
- addresses verified: 1,163 → 1,167 (+4) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0)
- verified targets: 356 → 361 (+5) · mapped-or-better: 399 → 404 (+5)
- engine LOC: 59,514 → 59,834 (+320) · test files: 122 → 122 (+0) · checks: 4,257 → 4,259 (+2)
- addresses verified: 1,167 → 1,179 (+12) · recovered layouts: 384 → 384 (+0) · open questions: 26 → 26 (+0)
- standalone leaves closed: 45 → 45 (+0) · leaves still diverging: 63 → 63 (+0)
---
warnings: board.md: unknown types subsystems, tooling; 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": 356, "mapped_plus": 399, "targets": 447, "loc": 59514, "tests": 122, "checks": 4257, "addr_verified": 1167, "addr_total": 1298, "layouts": 384, "open_q": 26, "sa_closed": 45, "sa_left": 63} -->
warnings: board.md line 459: unknown status 'open' for '**A replace of `ProcessResearch` is gated on `ServerPlayer::OnTechResearched`, and partly on a policy question**'; board.md: unknown types engine + gate, subsystems, tooling; 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": 361, "mapped_plus": 404, "targets": 453, "loc": 59834, "tests": 122, "checks": 4259, "addr_verified": 1179, "addr_total": 1310, "layouts": 384, "open_q": 26, "sa_closed": 45, "sa_left": 63} -->

View file

@ -154,11 +154,22 @@ mode and the **same hashes**, with a count from the hook proving the path execut
| # | module | today | evidence for the attempt | target |
|---|---|---|---|---|
| 1a | `TechTree::ProcessResearch` + unlock cascade | compared | 35 calls / 3 workloads / 0 div; advance prediction held on a changed workload | **replaced** — lane CR running |
| 1a | `TechTree::ProcessResearch` + unlock cascade | compared | 35 calls / 3 workloads / 0 div; advance prediction held on a changed workload | **stays `compared`** (lane CR, 2026-09-09): our code displaced all 13 tech-tree leaves live, oracle missed by **16**, every one written by `OnTechResearched` — a different function. Next: the write-back |
| 1b | `ServerSystem::ComputeTotalOutput` + `GroupOutput` | compared | 24,357 calls, 0 undeclared writes — but **13 distinct states** (rule 23) | replaced, after widening the state set |
| 1c | `ServerSystem::ProcessTurn` (colony) | compared | 36 calls 0 div; 3 owned systems, gates all zero | replaced, on a save with gate traffic |
| 1d | `game/nav`, `game/design`, `game/combat` retreat, `mars/vfs` | modelled, **never hooked** | host-tested only | compared — one lane each |
**Ruling (2026-09-09, on lane CR's policy question).** Reaching `replaced` on research needs two
event *records* whose text comes from the game's string table. That text is **game data**, not
engine code: the engine already reads every catalogue, tech name and design from the user's own
install through `$SOTS_DATA_DIR` / the VFS, and a string table is no different. So the answer is
**not** "call the game's `PostEvent` and take a QUALIFIED caveat" — it is **load the string table
through the same VFS path everything else uses** and construct the record clean. We ship no
strings; the user's copy supplies them. That is the same clean-room posture as every other data
dependency, and it turns the last research residual into ordinary engine work rather than a
policy exception. Track 1a, next lane: `OnTechResearched` write-back (~90 tech-effect fields) +
`ObservedTech` element + the two event records via the VFS string table.
### Track 2 — Rung B worklist, ranked against a real rich turn (lane CV, residual 0)
| leaves | subsystem | note |

File diff suppressed because one or more lines are too long

View file

@ -249,3 +249,305 @@ OK; `tools/check_shim_configs.py` OK with 27 registered hooks and all four CR co
## 4. Results
*(added after the runs; nothing above this line is edited)*
### 4.0 The five runs, in order
All five on **VM145**, held by this lane, each a **fresh process**, `SavedGames` reset to exactly
`turn3-state.sav` before every launch, `C:\SOTS\shimdist-cr\binkw32.dll`
(25,274,723 B, sha256 `d2ad56b32c1b5f6b…`, `BUILD_ID cr-618ccb1-20260909T131556Z`, exports 66/66
identical to the real `binkw32.dll`), one config per run and nothing else changed. Every screen —
main menu, Load-Game chooser, the one-row file list, the **lobby**, the loaded map at "Turn 3", the
post-turn map at "Turn 4" — was verified from a live `qm monitor` screendump before the next click.
No run was driven by sleeping.
| run | config | mode | `research.replace_cascade` | `(Autosave EndTurn).sav` | `(Autosave).sav` |
|---|---|---|---|---|---|
| **C1** | `croff` | `hooks=off` | – | 67,212 `e00eed0c…` | 67,811 `79df5047…` |
| **C2** | `croff` | `hooks=off` | – | 67,212 **`e00eed0c…`** | 67,811 **`79df5047…`** |
| **N** | `crcompare` | compare | off | 67,212 **`e00eed0c…`** | 67,811 **`79df5047…`** |
| **R0** | `crreplace0` | **replace** | off | 67,212 `e00eed0c…` | 67,511 **`6b51db99…`** |
| **R1** | `crreplace1` | **replace** | **on** | 67,212 `e00eed0c…` | 67,537 **`8a4309ee…`** |
`(Autosave EndTurn).sav` is the *pre*-turn resave and is identical in all five runs, as it must be —
nothing has run yet when it is written. The verdict is carried entirely by `(Autosave).sav`.
### 4.1 P0 held: the oracle
**C1 and C2 agree byte-for-byte in two fresh processes.** The pair, in the standing
`certified-pairs.md` format:
| input | procedure | route | `(Autosave EndTurn)` | `(Autosave)` | processes | evidence |
|---|---|---|---|---|---:|---|
| `turn3-state.sav` `978041ac…` | one End Turn | **load** | `e00eed0c…` | `79df5047…` | **3** | lane CR ×2 `hooks=off`, ×1 compare-instrumented |
**Masks that must be on the line.** Measured, not assumed —
`state_checksum.py turn3-state.sav <(Autosave EndTurn)>` gives **exactly 5 leaves**:
`/Summary/Checksum` and `Player.Status 4 → 0` on each of the four live players (16 `re`,
32 `Fane Lao`, 496 and 512 `Singularity`). **There is no `/CD[1]/NPrvVa` term on this state** —
`CD[1]`'s diplomacy block is early-game and the leaf does not move — so the `--mask resave` rule
holds here in the form the docs originally stated, and the lane BQ exception does not apply.
**Exposure facts next to the hashes** (certified-pairs standing rule 4): §1.3's table — no player
with an AI client enters the turn with `ResTNm == ''`; `NumDes` does not move; the two ships that
complete join existing fleets; the four factions at 528–576 carry the empty-`ResTNm` signature
inertly (`Status 0`, no client). The turn does create fleets (`Flt[50]`, `Flt[1808]`) and retire one
(`Flt[1776]`) — that is a fleet-assignment shape, and it agreed anyway. **Which is the point of
rule 26(c)'s retraction: the screen said "likely fine" and only the two-process control decided it.**
### 4.2 P3 held: the instrument is neutral, and it counts the completion itself
Run **N** reproduced the control's two hashes exactly, so rule 19 is satisfied on *this* save and
*this* route and everything below is read from runs that passed their own check.
`tracecmp verify/traces/cr-N.jsonl.gz`: **3 calls, 3 compared, 0 diverged, exit 0**, coverage verdict
`partial`, 8 unmodelled notes — and the undeclared-write set is **exactly the six spans predicted**:
```
player+0x10c:3 player+0x110:3 player+0x114:3 player+0x124:3 player+0x294:4 tree_header+0x20:1
```
The hook's own per-call line, which is the instrument saying a completion fired rather than the save
being asked to imply it:
```
research: mode=compare steps=1 completions=1 overbudget=1166 cascade_possible=1 ok=1
cascade_completions=1 unlocked=3 otch_appends=1 roll_draws=0 failures=0 depth=0
research: mode=compare steps=1 completions=0 … (×2, all zero)
```
**A correction to the campaign's read of the detour count, and it goes the other way from the
brief's warning.** My config names all **27** registered template hooks and
`check_shim_configs.py` passes it as `# exhaustive` — and the shim still installed **six** detours,
not one:
| detour | source | named by a `hook.` key? |
|---|---|---|
| `Mars::Application::Initialize` | the M0 asm stub, installed unconditionally whenever `hooks != off` | **no** |
| `Game::TechTree::ProcessResearch` | the one template hook | yes |
| `StrategyClient::EndTurn`, `StrategyServer::BeginProcessTurn`, `StrategyServer::ProcessTurn`, `DemoApp::OnTick` | the **FPU-force module**, which installs four *sampling* detours by default (`fpu: module init … force=off value=0x0000 sample_ticks=on`, `sample_turn=on`) | **no** |
`Shim::SelfTest::Fill` additionally emits one `trace` record at startup; it is an in-shim self-test,
not a detour on the game. So **`# exhaustive` is exhaustive over the template-hook set only**, and
`tools/check_shim_configs.py` cannot see the other five. It was the *neutrality check* (N identical
to C1/C2), not the config check, that made this run safe — which is worth saying plainly, because a
lane reading "exhaustive, therefore one detour" would be wrong by five.
For the record, `fpu_cw = 0x027f` (53-bit, round-to-nearest) in every CR run.
### 4.3 P1 and P2 held exactly, on the LOAD route
The load route reproduced the continuation's turn-4 call in every particular — allocations, the
completion, the unlock set, the arithmetic. From `cr-N.jsonl.gz` (compare) and confirmed identically
in `cr-R1.jsonl.gz` (replace):
| call | owner | species | alloc | what moved |
|---|---|---|---|---|
| 1 | `Player[32 "Fane Lao"]` | 2 | `{144, 2898}` | the completion (below) |
| 2 | `Player[496 "Singularity"]` | 0 | `{90, 0}` | **nothing at all** |
| 3 | `Player[512 "Singularity"]` | 2 | `{9, 0}` | **nothing at all** |
Call 1, node **144 `IND_Waldo`**: `state 3 → 4`, `progress 5768 → 7500`, `turn_researched −1 → 4`,
`order −1 → 22`; `overbudget 0 → 1166`; three nodes unlocked —
**132 `IND_OrbFound` @ 10000**, **136 `IND_RefCoat` @ 16000**, **142 `IND_TrkStl` @ 8000**, each
`state 0 → 2`, `cost_rp INT_MAX → …`, `turn_available −1 → 4`; `events.next_id 5 → 7`;
`observed_techs 440 → 484 bytes`.
P2's hand arithmetic is confirmed to the unit: `cost 5000`, `lo 2500`, `hi 7500`,
`spent = min(2898, 1732) = 1732`, `progress = 7500` **exactly on the ceiling**,
`overbudget = 2898 − 1732 = 1166`; `progress < hi` false so **no draw**; `ratio = 1.5`, not below
`0.800000011920929`, so **`flag` unchanged**. The `rng` region **did not move on any of the three
calls** in any run.
That last fact is a coverage hole, not a success — see §4.6.
### 4.4 P4 and P5 held: replace diverges, and the cascade closes exactly eleven leaves
`state_checksum.py cr-oracle-autosave.sav <replace autosave>`:
| run | diverging leaves | file size |
|---|---:|---|
| **R0** — replace, cascade **off** (the shipped behaviour) | **27** | 67,511 B |
| **R1** — replace, cascade **on** | **16** | 67,537 B |
| (the turn itself, `turn3-state` → oracle, for scale) | 128 | – |
**The eleven leaves the cascade closes — R0 has them, R1 does not.** All eleven are `TechTree`:
```
Player[32]/TechTree/St[94] TResCost[94] TUnlck[94] (node 132 IND_OrbFound, 10000)
Player[32]/TechTree/St[98] TResCost[98] TUnlck[98] (node 136 IND_RefCoat, 16000)
Player[32]/TechTree/St[104] TResCost[104] TUnlck[104] (node 142 IND_TrkStl, 8000)
Player[32]/TechTree/TAcq[106] TiAcq[106] (node 144, turn 4 / order 22)
```
The tree is serialised as parallel arrays indexed by **tree slot**, not tech id — slots 94/98/104/106
are tech ids 132/136/142/144 — and the pass's own two words, `St[106] 3 → 4` and
`TResDone[106] 5768 → 7500`, are correct in **both** replace runs because
`ProcessResearchTurn` writes them without the cascade. So of the **13 tech-tree leaves this turn
moves, our code produced all 13 in live memory with the original's `ProcessResearch` never
executing** — 2 from the pass, 11 from `SetResearched`.
**One prediction I cannot test and must retract as written.** P4 item 2 said the completion-order
counter would be "left at 22" in R0. `TechTree+0x20` **is not a save leaf** — the counter's value
surfaces only through the per-node `TiAcq` stamp — so the oracle cannot see it either way. What is
observable is the trace: R1's only guard hit is `tree_header+0x20:1` (ours writing 22 → 23, the same
byte the original moves in compare mode) and R0 has **0 undeclared writes in 0 calls**. The counter
matters for the *next* completion, not for this save.
### 4.5 P6 held: the residual is `ServerPlayer::OnTechResearched`, entirely
R1's **16** leaves, every one of them named, with nothing left over:
| leaf | what it is | modelled by `ours`? |
|---|---|---|
| `Player[32]/OutMod` `1.25 → 1.1` | a tech effect | no — B2's milestone |
| `Player[32]/ConMod[0..2]` `0.9 → 1.0` (×3) | tech effects | no — B2's milestone |
| `Player[32]/Events/EvNxID` `7 → 5` | the two events not posted | decision modelled, **write is compare-only** |
| `…/Events/.[EvTurn=4]/Events/.[EvEID=5]`, `.[EvEID=6]` `only-in-A` | the two event records | text comes from the game's string table |
| `…/Events/.[EvTurn=4]/Events/.[0]` `3 → 1` | that turn's event count | ditto |
| `Player[32]/otch/.[11]` `only-in-A`, `otch/.[0]` `11 → 10` | the `ObservedTech` element | append **decided** (`otch_appends=1`), element not constructed |
| `Player[32]/ResTNm` `'' → 'IND_Waldo'` | `ResT` never cleared | inside the callback |
| `Player[32]/BnkPr`, `BnkEl` | bankruptcy projection | **downstream of `OutMod`** |
| `Sys[288 "Ke'Dolarra"]/RepCur`, `RepMax` `421640 → 371040` | repair capacity | **downstream of `ConMod`** |
| `/Summary/Checksum` | derived | derived |
So the residual decomposes into **5 primary player fields** (`OutMod`, `ConMod[0..2]`, `ResTNm`),
**1 `ObservedTech` element**, **2 event records + their id counter**, and **5 derived leaves** that
follow from those. Every single one is written by `ServerPlayer::OnTechResearched`, none of them by
`TechTree::ProcessResearch` or by `SetResearched`.
That also closes the loop with §4.2's guard: the four `player` spans the compare reported as
undeclared (`+0x10c`, `+0x110`, `+0x114`, `+0x124`, all three bytes wide — float writes whose top
byte did not change) plus `+0x294` (`ResT`, four bytes) are **five** writes, and the save shows
**five** primary player fields. The guard was reporting exactly what the oracle later billed us for.
**The input class that breaks it is a completion, and only a completion.** Both replace runs are
byte-perfect on the two null calls and on every other leaf of the 128 the turn moves. A replace run
over a turn where research does not complete would be byte-identical — and would prove nothing
(rule 1), which is why this lane refused to run it on `ref-turn2`.
### 4.6 Coverage, reported as loudly as the result (rules 15 and 23)
* **1 completion. 1 distinct tech (144 `IND_Waldo`). 3 unlocked nodes. 3 calls.**
* **2 of the 3 calls allocate zero points and write nothing at all**, in any mode. Their entire
contribution to "3 calls, 0 diverged" is that two null calls stayed null.
* **1 of 4 tech trees is exercised.** Player 16's tree is never processed (`ResTNm == ''`); players
496 and 512 are the null calls.
* **The RNG region did not move on any call in any run.** The spend cap bound exactly, so the
odds/roll branch was skipped, and `roll_pending_in` was false on the completing call, so
`RollResearchEvent` drew nothing (`roll_draws=0`). **`region:rng` — the single strongest check in
this hook's compare — compared "unchanged against unchanged" on this workload and established
nothing.** The generator parity evidence for this module is entirely lane U's and lane V's,
on other turns.
* **Branches that did not execute here:** the Zuul double roll (species 2, not 5); the
completed-early flag (`ratio 1.5`); the over-budget notification (`flag` already 2 from turn 3);
`RollResearchEvent`'s draw and, behind it, the plague / AI-rebellion paths; `SetResearched`'s
zero-cost recursion; the `def+0xb0` `NoAutoAvailable` skip; an empty prerequisite group; a
re-observed tech (the dedup's negative case); and the decay sweep, which ran over every node and
**changed nothing** because no other `Available` node had non-zero progress.
* **The event model is count-only by construction** and stayed compare-only in replace mode by
design; the two missing records are two of the sixteen residual leaves.
* **`TechTree::Cost` is the original's.** `ours` calls the game's read-only `Cost` for every cost it
needs, in both modes. The effective-cost formula is **not** displaced, and any claim about this
module inherits that dependency.
### 4.7 Verdict
**No. `game/sim/research` does not move from `compared` to `replaced`.**
The bar is "our code ran instead of the original's **and a byte-level oracle held afterwards**". Our
code did run instead — `mode=replace`, `completions=1`, `unlocked=3`, the original's
`ProcessResearch` never executed, and the game finished the turn and wrote a save. The oracle did
**not** hold: 16 leaves in the best configuration. There is no qualified reading that rescues it,
because the failure is not a rounding residual — it is a set of writes nobody has implemented.
What the lane did establish, and it is worth more than the rung would have been:
1. **The research pass and the entire `SetResearched` cascade are displaceable and were displaced.**
All 13 tech-tree leaves the turn moves were produced by our code in live game memory, on a turn
with a real completion and a real three-node unlock cascade — which is a strictly stronger
statement than the 35 compared calls the board already carried, because in a compare the
original's code still did the work.
2. **The blocking boundary is named and measured, not guessed:** `ServerPlayer::OnTechResearched`,
5 player fields + 1 `ObservedTech` element + 2 events, and 5 derived leaves behind them.
`ProcessResearch` **cannot** reach `replaced` on any workload containing a completion until
`OnTechResearched` is displaced — and that is B2's milestone and its own `compared` board row,
not a defect in the research model.
3. **A new certified pair** on a turn that exercises the completion path, which the campaign did not
have: `ref-turn2` + one End Turn is a *quiet* turn for research, and every oracle the module had
been checked against was that one.
The cheapest route to the rung, now that the boundary is priced: implement the ~90-field
`ApplyTechEffect` write-back live (B2 already has `game/effects/tech_effects` host-tested), construct
the `ObservedTech` element, and decide what to do about the two event records — whose *text* comes
from the game's string table and therefore cannot be produced clean-room at all. **The event text is
a hard stop for a byte-identical oracle on any completion turn**, and that should be settled as a
policy question (call the game's `PostEvent`, and accept the `ComputeBudget`-shaped QUALIFIED
caveat) before anyone spends another lane on it.
---
## 5. Proposed board rows
**I have not edited `campaign/board.md`.** I *have* added the certified pair to
`verify/results/saves/certified-pairs.md`, which the brief pointed at as the standing format and
whose four standing rules for adding a row are all satisfied (two fresh `hooks=off` processes; the
control run before anything was read from an instrumented run; not an extension of an existing pair
but its own agreement; exposure facts recorded beside the hashes).
### 5.1 `tools/displacement.py` — the rung does NOT move
The verdict is **no**, so `"compared"` stays. What I do propose is replacing the evidence and caveat
strings, which currently understate what is known and do not name the gate:
```python
("TechTree::ProcessResearch + unlock cascade", "compared",
"35 calls across 3 workloads, 0 divergences, tracecmp exit 0; advance prediction held on a "
"changed workload (unlock costs no earlier report contained); REPLACE ATTEMPTED live (lane "
"CR): ours ran instead of the original on a real completion and produced all 13 tech-tree "
"leaves the turn moves, but the save oracle missed by 16 leaves",
"compare only. The replace attempt failed on ServerPlayer::OnTechResearched, not on the "
"research model: 5 player tech-effect fields, 1 ObservedTech element and 2 event records, "
"plus 5 derived leaves. Gated on B2. Thin: 1 completion, 1 tech, 2 of 3 calls allocate zero "
"points, and the RNG region did not move at all on the replace workload"),
```
### 5.2 Board rows to add
| row | class | status | conf | cov | date | evidence |
|---|---|---|---|---|---|---|
| **`ProcessResearch` replace: our code ran instead of the original's, and the oracle missed by 16 leaves — all of them `OnTechResearched`'s** | phase2 | verified | high | 95% | 2026-09-09 | **Lane CR, VM145**, `findings/subsystems/research-replace.md`. Predictions committed before the build (`sots-re` 4b3cc82); engine `wip/cr` 618ccb1 adds `research.replace_cascade=on\|off` (default off) so the shipped and extended behaviours differ by **a config line, not a binary**. New certified pair `turn3-state.sav` → one End Turn, **load** route, `e00eed0c…`/`79df5047…`, 2 `hooks=off` processes + 1 compare. Compare run **3/3/0 exit 0** and byte-identical to the control (rule 19 satisfied), undeclared writes exactly the 6 predicted spans. **Replace, cascade on: 16 diverging leaves; cascade off: 27.** The 11-leaf delta is the whole `SetResearched` cascade and it is **ours**; with the pass's own 2 words that is **13 of 13 tech-tree leaves the turn moves, produced live by our code**. The 16 residual leaves are `OutMod`, `ConMod[0..2]`, `ResTNm`, one `ObservedTech` element, two event records + `EvNxID`, and 5 derived. **Verdict: stays `compared`.** |
| **`ref-turn2` + one End Turn does NOT exercise a research completion** | verify | verified | high | 100% | 2026-09-09 | Lane CR. The campaign's most-reproduced oracle is a **quiet turn for the completion path**: `unlock-b3-t1.md` reports `0 undeclared write(s) in 0 call(s)` and `unlock-shim.log`'s first three lines read `completions=0`. It *is* a real workload for the pass (one RNG word, `flag 1 → 2`, one over-budget event) — but a replace-mode oracle taken there would be rule 1's green verdict on a hook comparing nothing. The completing turn is the **next** one, from `turn3-state.sav`. |
| **`# exhaustive` is exhaustive over the 27 template hooks only — six detours are installed, not one** | phase2 | verified | high | 100% | 2026-09-09 | Lane CR. A config naming all 27 registered hooks `off` except one, passing `tools/check_shim_configs.py`, still installs **6** detours: the M0 `Application::Initialize` asm stub (unconditional whenever `hooks != off`) and the **FPU-force module's four sampling detours** (`force=off value=0x0000 sample_ticks=on sample_turn=on`, on by default). `check_shim_configs.py` cannot see either group — no `hook.` key names them. What made lane CR's runs safe was the **neutrality check** (compare run byte-identical to two `hooks=off` controls), not the config check. A lane reading "exhaustive, therefore one detour" is wrong by five. |
| **A replace of `ProcessResearch` is gated on `ServerPlayer::OnTechResearched`, and partly on a policy question** | phase2 | open | high | – | 2026-09-09 | Lane CR. To reach `replaced`, three things are needed: the ~90-field tech-effect write-back applied live (B2 has `game/effects/tech_effects` host-tested), the `ObservedTech` element constructed (`ours` already decides the append), and the two research event **records** written. The third is not an implementation gap: their `EvDsc`/`EvMsg` text comes from the game's string table, which the engine must not carry, so a byte-identical oracle on any completion turn requires calling the game's own `PostEvent` and accepting a `ComputeBudget`-shaped **QUALIFIED** `replaced`. Settle that before spending a lane. |
### 5.3 Artefacts
| what | where |
|---|---|
| predictions commit (before the build) | `sots-re` `4b3cc82` |
| engine change | `sots-engine` worktree `wip/cr`, `618ccb1` — `research.replace_cascade`, a mode-independent completion counter, four `shim.cfg.cr*` configs |
| oracle + replace saves | `verify/results/saves/cr/cr-{oracle-endturn,oracle-autosave,replace0-autosave,replace1-autosave}.sav` |
| traces | `verify/traces/cr-{N,R0,R1}.jsonl.gz` |
| `tracecmp` reports | `verify/results/compare/cr-{compare,replace0,replace1}.{md,json}` |
| shim logs (the per-call counters) | `verify/results/shim/cr/cr-{N,R0,R1}.log` |
| certified pair | `verify/results/saves/certified-pairs.md`, row 4 |
No `ghidra/addresses.d/cr.json`: this lane read no new address. No game data, save, disassembly or
`FUN_xxxxxxxx` name reached `sots-engine`; `tools/clean_room_check.sh` OK.
### 5.4 VM145 released
Held for this lane only; **VM140 untouched**. Restored and verified byte-for-byte to as-found:
```
binkw32.dll 15,527,327 B 903527F4A698EEA9FBE25F3A6236657C7D8348F994EEF394696F187E5E554B97 (= shimdist-recap)
shim.cfg 0AE410CC72FE155837D711C78543E14B1F5743310CD023298EB1E5BBD5457E71 (= shimdist-recap\shim.cfg.recaptrace)
SavedGames the same 9 files, every size and sha256 identical to the as-found survey
game not running (as found)
```
Left behind: `C:\SOTS\shimdist-cr\`, `C:\SOTS\ui\cr\` (the input save, the four run outputs and the
restore set) and `C:\SOTS\ui\cr{deploy,click,grab,snap,restore,find}.ps1`. `click_helper.ps1` is
unmodified. **One thing not restored:** my deploy helper *deletes* `C:\SOTS\shim.log` rather than
rotating it to `.prev` the way lane BQ's does, so whatever log was there when I took the guest is
gone. Logs are not simulation state and no measurement depends on it, but the next lane should
rotate rather than delete.

View file

@ -0,0 +1,248 @@
# `T34 RecordObservedDesigns` — the phase, its gate, and what it closes
- **Type:** subsystem / engine implementation
- **Owner / date:** lane DT · 2026-09-09 · **HOST ONLY**. No VM was touched; VM140/141/145/146
were not approached, not pinged and not logged into. The corpus was read, never written.
- **Trigger:** `campaign/backlog.md` §2/§3 Track 2, and
`findings/subsystems/rung-b-rich-turn.md` §5 rank 4 — lane CV's ranked worklist names
`T34 RecordObservedDesigns` (stub) as "79 leaves, no upstream dependency", the largest
independently closable item measured against a rich turn.
- **Consumes:** CV's measurement (`verify/results/standalone/cv/*`), lane BR's deep block
`verify/results/turncommands/br2-turn27-deep.tcb`, lane X/S's `observedtech-append.md`
(the sibling `ObservedTech` record and its 0x2c element), lane PL's `players-residual.md`
§M3, lane T's `combat-done-tail.md` phase table.
- **Touches:** `sots-engine`: `src/game/sim/observed.{h,cpp}` (new),
`src/app/observed_phase.{h,cpp}` (new), `src/app/turn.cpp`, `src/app/phase_catalog.cpp`,
`tests/game_sim/test_observed.cpp` (new), two `CMakeLists.txt`.
`sots-re`: this file, `ghidra/addresses.d/dt.json`, `verify/results/standalone/dt/*`.
**`src/mars/stream/` was not touched** — that is lane DW's file set this round.
---
## 1. The prediction, written and committed before the engine was built (rule 2)
This is a *computed* prediction, not a guess: the mechanism below was read out of the
instruction stream first, then simulated in Python against the save corpus, and the numbers
below are that simulation's output. What the build then tests is whether the C++ in
`sots_turn` — reading the save through the engine's own typed shapes, in the engine's own
iteration order — reproduces the simulation. Any disagreement is an engine/order defect, and
that is exactly the thing this prediction is for.
| # | prediction | why |
|---|---|---|
| **P1** | On the rich turn the **`odes` sub-group closes 34 of its 55 leaves and regresses 0**; 21 remain. | The mechanism is fully determined by the wire (§2). The 21 that remain are the shadow of two designs — 1826 and 1522 — whose **first ships are built during turn 27→28**; this engine builds no ship, so it cannot observe them, and their absence changes which entries the 20-per-owner cap evicts. |
| **P2** | The **`otch` + `owep` sub-group (24 leaves) closes 0 and regresses 0.** | Not implemented: the design→tech and design→weapon lists the phase feeds to `RecordObservedTech`/`RecordObservedWeapon` are built by three `std::set` builders in the original that are not decoded, and the wire's own `DOpts` list covers only 13 of the 18 tech names this turn touches (§4). Named as a dependency, not attempted. |
| **P3** | Rich-turn total: **1092 → 1058**. | P1 + P2, with nothing else moving. |
| **P4** | On the **canonical pair the phase closes 1 leaf and regresses 0** (`Player[32]/odes/.[1]/otnL: 2 → 3`). | The whole canonical-pair `odes` residual is that one stamp; the simulation reproduces it exactly, including the eight players whose lists must stay empty. |
| **P5** | The **NPC guard is load-bearing and its absence is a 4-leaf regression on the canonical pair**, not on the rich turn. | Without it, player 528 ("Alien Menace", NPC) gains four `odes` entries the target does not have. This is the one place the two pairs disagree about which rule is needed, and it is why both pairs are run. |
**Falsifiers, each with its symptom.**
1. *The engine's fleet/ship iteration order is not the save's order.* Symptom: the surviving
`odid` sequence is a different permutation and the rich-turn residual is **worse than 21**.
2. *A player observes ships it does not own.* The gate in the original is a two-bit-per-player
field on the ship that is **not on the wire**; this implementation stands in for it with
ownership (§3.3). Symptom: entries appear for foreign designs — a regression, on either pair.
3. *`sim.frame` is not the post-increment turn at tail time.* Symptom: every `otnL` is off by
one and the residual **grows** rather than shrinks.
4. *The cap is not 20, or is not per design-owner.* Symptom: the list length is wrong, which
is a `LEN` leaf plus four per extra element.
---
## 2. What the phase is — read from the instruction stream
**`T34` is `0x007c2350`**, 222 bytes, called from `OnAllCombatDone_Tail` `0x007d92a0` at
`0x007d98aa` (lane T already had it as tail phase 34: "record observed designs into
`ServerPlayer+0x254/+0x258`"). Its two other callers are `BuildTurnEvents` `0x007db780` and
`LoadGame` `0x007dd530`.
### 2.1 The phase body — a triple loop with one gate
```
for pi in 0 .. |S->Players|: ; S+0x54 / S+0x58
p = S->Players[pi]
for fi in 0 .. |S->Fleets|: ; S+0x64 / S+0x68
f = S->Fleets[fi]
for si in 0 .. |f->ships|: ; f+0xa4 / f+0xa8
sh = f->ships[si]
if (p->PlyrIdx(+0x28) >= 15) continue ; 0x007c23bf cmp eax,0xf ; jge
cl = 2 * p->PlyrIdx
if (((sh->+0x54 >> cl) & 3) != 3) continue ; 0x007c23ce and eax,[edx+0x54]
RecordObservedDesign(S, p, sh->design(+0x14)) ; 0x007c23e2
```
Two things to carry away. The **outer** loop is players and the **inner** two are the whole
fleet list, so the phase is O(players × ships) and every player is offered every ship in the
game. And the gate is a **two-bit-per-player field at `Ship+0x54`** — index `2*PlyrIdx`, both
bits required. `Ship+0x54` is **not serialised**: the ship's on-disk field list runs
`… +0x4c Dep, +0x4d Atq, +0x5c LCT, +0x60 tsd …` with `0x54` and `0x58` absent
(`objects/layouts.json`, `Game::StarShip`). The cap at 15 players is the same cap
`src/game/sim/visibility.h` already records for the runtime companion of the system masks.
### 2.2 `RecordObservedDesign` — `0x007be340`, `ret 8`
`this` is the `StrategyServer`; arg 0 is the observing `ServerPlayer*`, arg 1 the `Design*`.
```
if (!player || !design) return;
owner = design->+0x130 ; the design's owning ServerPlayer
if (owner->+0xfb != 0 && owner->+0xfc == 0) return; ; 0x007be389 / 0x007be392
if (!Design::vt-ish 0x0080baf0(design)) return; ; 0x007be3a1
if (Design::HasFlag(design, 0x400, 0)) return; ; 0x007be3b7 -> 0x00813ab0
; --- dedup, on odid ALONE (not on the (odid, opid) pair) ---
for (e = player->odes._Myfirst; e != _Mylast; e += 0x10) ; *** stride 0x10 = 16 ***
if (e->+0x8 == design->id) break; ; +0x8 is `odid`
if (e == _Mylast) { ; APPEND 0x007be3f5
push_back(ObservedDesign{}); ; ctor 0x0080da30, push_back 0x00799d80
back->+0x4 = S->turn(+0xc); ; otnF (16-bit)
back->+0x6 = S->turn; ; otnL (16-bit)
back->+0x8 = design->+0xa0->+0x4; ; odid
back->+0xc = owner->+0x4; ; opid
if (owner != player) <notify list at S->+0x284 + 0x10*player->PlyrIdx>
} else { ; MOVE-TO-BACK 0x007be618
tmp = *e; ; otnF/otnL/odid/opid copied out
odes.erase(e); ; 0x00795c40
odes.push_back(tmp); ; 0x00799d80
}
back->+0x6 = S->turn; ; otnL, common to both paths
; --- the cap: 20 entries per DESIGN-OWNER, counted from the BACK 0x007be67f ---
kept = 0
for (i = count-1; i >= 0; --i)
if (odes[i].opid == owner->+0x4) {
if (kept >= 0x14) { <notify if odes[i].opid != player id>; erase odes[i]; }
else ++kept;
}
```
`sizeof(Game::ObservedDesign) = 0x10`, and the element is
`{vptr @+0x0, uint16 otnF @+0x4, uint16 otnL @+0x6, int odid @+0x8, int opid @+0xc}` — the
wire record `otnF otnL odid opid` exactly, with the vptr accounting for the difference between
12 on disk and 16 in memory. (Compare its sibling `Game::ObservedTech` at `0x2c`, whose extra
0x1c is the embedded tech-name `std::string` — `observedtech-append.md` §4.)
**Three facts here are the whole finding, and none of them is guessable from the wire:**
- **the dedup key is `odid` alone.** A design id is globally unique in this game, so this only
matters if two owners could ever share one — but it is what the code does, and modelling it
as a `(odid, opid)` pair would be a different program.
- **re-observation is `erase` + `push_back`, not an in-place update.** The list is therefore
ordered by *last* observation, and `otnF` survives the move. This is why the target's list is
a permutation of the input's rather than an append.
- **the list is capped at 20 entries per design-owner**, counted from the most recent end, and
the cap is enforced *after every single record call*, not once per turn. On a player with more
than 20 designs in service this thrashes: entries are evicted and re-created inside one sweep,
and each re-creation resets `otnF` to the current turn. That is measured, not inferred — see
§3.2.
### 2.3 The tech and weapon arms
The same function then builds three lists off the design and feeds two of them to
`RecordObservedTech` `0x007ba1a0` (lane X's function) and one to `RecordObservedWeapon`
`0x007be1b0`:
```
0x007be4af FUN_00862c90(&v, design) -> for each: RecordObservedTech(player, owner, x) 0x007be4e1
0x007be50d FUN_008629b0(&v, design) -> for each: RecordObservedTech(player, owner, x) 0x007be535
0x007be561 FUN_008626a0(&v, design) -> for each: RecordObservedWeapon(player, owner, x) 0x007be591
```
All three are ~600–700-byte `std::set` builders (`operator new 0x14` red-black nodes) walking
the design's section array at `Design+0x2c`/`+0xac` and `+0x3c`. They are **not decoded**, and
they are the dependency §4 names.
---
## 3. The gate, and the two guards the corpus forced
### 3.1 The visibility gate is unexercised on both reference pairs — say so out loud (rule 6, rule 28)
`Ship+0x54` is not on the wire, so the phase's real gate cannot be read from a save. It does not
have to be, on these two pairs, because **no player observes another player's ship on either of
them**:
- rich turn: player 16's twelve `opid = 32` records all keep `otnL = 27`, and player 32's one
`opid = 16` record keeps `otnL = 25`. Every record that moves is a player's own design.
- canonical pair: the single moving record is player 32's own design 18 (lane PL's M3).
So this implementation stands the gate in for **`ship.PlrID == player.id`**, and that is a
**hypothesis with a named falsifier**: the first save in which one empire's fleet stands in
sensor range of another's and a foreign `odes` record moves. It is not a reading of the gate and
must not be quoted as one. What it *is* is the smallest rule that is correct on every state the
corpus contains — and lane PL's M3 said the same thing from the other end ("not an intel pass").
### 3.2 The NPC guard — `owner->+0xfb && !owner->+0xfc`
The two bytes are consecutive `ServerPlayer` bools next to `RebAI`, which lane T pinned at
`+0xfc` (`ProcessTurn` phase 8: `if (RebAI(+0xfc)) RebOutMod = clamp(...)`). The wire writes
`Elim, NPC, RebAI, ReqCL` consecutively, so `+0xfb` is `NPC`. Read as
**"a design owned by an NPC that is not a rebel AI is never recorded"**, and the corpus agrees:
| save | player | kind | own designs | own `odes` records |
|---|---|---|---:|---:|
| `ad-turn27` | 32 "The Eternal Empire" | not NPC | 46 | 20 |
| `ad-turn27` | 16 "re" | not NPC | 6 | 1 |
| `ad-turn27` | 528 "Alien Menace" | **NPC, not rebel** | 19 | **0** |
| `ad-turn27` | 576 "Independent Colony" | **NPC, not rebel** | 1 | **0** |
Every `odes` record in the corpus, on every player, names a design owned by 16 or 32 — the two
non-NPC empires. The four NPC factions own 26 designs between them and not one of them has ever
been observed by anybody.
**Without this guard the canonical pair regresses by 4 leaves** (player 528 gains four records),
which is P5 and is the reason the canonical pair is not a formality here.
### 3.3 What is *not* modelled
`0x0080baf0(design)` and `Design::HasFlag(design, 0x400)` are two further early returns that
this implementation does not evaluate — the first is an unnamed predicate on the design, the
second a design flag. Both are **assumed false** for every design in the corpus, on the evidence
that the simulation reproduces the target list without them. That is an argument from a
sufficient model, not from having read the flags, and it is recorded as such.
---
## 4. Named dependency: the tech and weapon arms are NOT independently closable
CV's ranking says rank 4 has "no upstream dependency". That is right for the `odes` half and
**wrong for the other 24 leaves in the group**, in two separate ways. Both are named here and
neither is attempted (scope discipline).
**(a) `otch` / `owep` need three undecoded set builders.** The obvious wire candidate is the
per-section `DOpts` list, which `sots-engine`'s own `game::design::applied_techs` already
documents as "exactly the save's `DOpts` list: hull-class tech, `requires_tech` in file order,
then the chosen option". Measured against the reference turn, the union of `DOpts` over the
designs player 32 observes covers **13 of the 18 tech names that move** and misses five —
`WEP_GrnLas`, `WEP_PlsmCan`, `WEP_Dsrptr` (three weapon techs), `CCC_BtlCmp`, and the one
**newly appended** record, `BIO_TerBac`. So `DOpts` is one of the three lists and not the whole
of them; the other two (`0x008629b0`, `0x008626a0`) are `std::set<std::string>` builders over
the design's weapon banks and would have to be decoded, and their **set ordering is
load-bearing** because it decides the append order of any new `otch` element.
**(b) The `odes` half itself is capped by ship construction.** Two of the designs the target
observes — 1826 "Egg Thief Mk 3" and 1522 "Bravestar Mk 2" — have **zero ships in the input and
their first ships in the target** (4 and 1). No phase in this engine builds a ship (CV rank 8=,
`M2` in `players-residual.md`), so those two observations cannot happen here, and because the
list is capped at 20 per owner their absence also stops the two evictions (546 "Warrior Mk 6",
770 "Beaten Shield Mk 2") that the target performs. That is the whole of the 21-leaf residual.
**The oracle run proves the mechanism is not the residual.** Feeding the same implementation the
**target's** post-turn fleet/ship state instead of the input's leaves **1 leaf, not 21** —
`Player[32]/odes/.[18]/otnF`, where this model writes 28 (the entry was evicted and re-created
inside the sweep) and the target holds 27. So on the rich turn the mechanism is exact on 20 of
21 records and the gap is ship construction, not `T34`.
---
## 5. Results
*(filled in after the run — see §5.1 onward)*
---
## 6. Reproducing
*(filled in after the run)*

File diff suppressed because one or more lines are too long

View file

@ -58,8 +58,13 @@ ROWS = [
"every call; only 20 distinct states across 4,437 calls"),
("TechTree::ProcessResearch + unlock cascade", "compared",
"35 calls across 3 workloads, 0 divergences, tracecmp exit 0; advance prediction held on a "
"changed workload (unlock costs no earlier report contained)",
"compare only, never replaced"),
"changed workload (unlock costs no earlier report contained); REPLACE ATTEMPTED live (lane "
"CR): ours ran instead of the original on a real completion and produced all 13 tech-tree "
"leaves the turn moves, but the save oracle missed by 16 leaves",
"compare only. The replace attempt failed on ServerPlayer::OnTechResearched, not on the "
"research model: 5 player tech-effect fields, 1 ObservedTech element and 2 event records, "
"plus 5 derived leaves. Gated on B2. Thin: 1 completion, 1 tech, 2 of 3 calls allocate zero "
"points, and the RNG region did not move at all on the replace workload"),
("ServerPlayer::OnTechResearched", "compared",
"3 completions, 0 div; float32 confirmed bit-for-bit on the game",
"compare only; RollResearchEvent's branch has fired once in four sessions"),

View file

@ -15,11 +15,21 @@
# Usage: tools/gate.sh [--fresh] --fresh: rm -rf the remote build dirs first (rule 24)
set -u
# One gate at a time. Lane DW saw up to FOUR `gate.sh --fresh` runs concurrently, all rsyncing
# into the same remote tree and rm -rf'ing its build dirs -- rule 24's shape with no lock, and a
# way to report a build that is half someone else's. Every lane runs from this host, so a local
# flock is sufficient. Waits up to 30 min, then fails loudly rather than running unlocked.
if [ -z "${SOTS_GATE_LOCKED:-}" ]; then
exec env SOTS_GATE_LOCKED=1 flock -w 1800 /tmp/sots-gate.lock "$0" "$@" \
|| { echo "GATE FAIL could not acquire /tmp/sots-gate.lock within 30 min"; exit 2; }
fi
ENGINE="${ENGINE:-$HOME/sots-engine}"
RE="${RE:-$HOME/sots-re}"
CT=111
REMOTE_TREE=/srv/re-lab/build/sots-engine
REMOTE_CORPUS=/srv/re-lab/saves-corpus
REMOTE_DATA=/srv/re-lab/gob-extract # the extracted asset tree; game_design_census needs it
FRESH=0
[ "${1:-}" = "--fresh" ] && FRESH=1
@ -49,15 +59,25 @@ ssh spicy "pct exec $CT -- mkdir -p $REMOTE_CORPUS && tar -C /tmp/sots-corpus -c
NCORPUS=$(ct "ls $REMOTE_CORPUS | grep -c .sav")
echo "GATE info corpus on CT111: $NCORPUS saves"
# 5. host build + tests, WITH the corpus
ct "cd $REMOTE_TREE && cmake -S . -B build-host -DCMAKE_BUILD_TYPE=Release >/dev/null 2>&1 && cmake --build build-host -j8 2>&1 | tail -1"; report host-build $?
ct "cd $REMOTE_TREE/build-host && SOTS_SAVES_DIR=$REMOTE_CORPUS ctest --output-on-failure 2>&1 | tail -15"; report host-ctest $?
# and prove the corpus tests RAN rather than skipped
SKIPPED=$(ct "cd $REMOTE_TREE/build-host && SOTS_SAVES_DIR=$REMOTE_CORPUS ctest -V 2>&1 | grep -c 'unset, skipped'")
# 5. host build + tests, WITH the corpus AND the extracted data tree
#
# `cmd | tail -1` returns TAIL's status, not the command's -- so for a day this
# script printed "GATE ok host-ctest" over a run with two FAILING tests, which is
# rule 1's failure wearing this script's own clothes. Every step below captures
# the real exit code first and only then trims the output.
ct "cd $REMOTE_TREE && cmake -S . -B build-host -DCMAKE_BUILD_TYPE=Release >/tmp/gate-cfg.log 2>&1 && cmake --build build-host -j8 >/tmp/gate-build.log 2>&1; rc=\$?; tail -1 /tmp/gate-build.log; exit \$rc"; report host-build $?
ct "cd $REMOTE_TREE/build-host && SOTS_SAVES_DIR=$REMOTE_CORPUS SOTS_DATA_DIR=$REMOTE_DATA ctest --output-on-failure >/tmp/gate-ctest.log 2>&1; rc=\$?; tail -15 /tmp/gate-ctest.log; exit \$rc"; report host-ctest $?
# and prove the corpus tests RAN rather than skipped. The check is scoped to the
# tests gated on the CORPUS (they name SOTS_SAVES_DIR when they skip); every other
# skip is listed below it by name, because a skip nobody reads is how this started.
SKIPPED=$(ct "cd $REMOTE_TREE/build-host && SOTS_SAVES_DIR=$REMOTE_CORPUS SOTS_DATA_DIR=$REMOTE_DATA ctest -V 2>&1 | grep -c SOTS_SAVES_DIR")
if [ "${SKIPPED:-1}" -ne 0 ]; then echo "GATE FAIL corpus tests skipped ($SKIPPED) -- the gate is hollow"; status=1; else echo "GATE ok corpus tests ran (0 skipped)"; fi
ct "cd $REMOTE_TREE/build-host && SOTS_SAVES_DIR=$REMOTE_CORPUS SOTS_DATA_DIR=$REMOTE_DATA ctest -V 2>&1 | grep skipped" | sed 's/^/GATE info skip: /'
# 6. shim cross-build
ct "cd $REMOTE_TREE && cmake -S . -B build-shim -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-mingw-i686.cmake -DCMAKE_BUILD_TYPE=Release >/dev/null 2>&1 && cmake --build build-shim -j8 2>&1 | tail -1 && ls -la build-shim/binkw32.dll"; report shim-cross-build $?
# 6. shim cross-build -- same exit-code discipline as step 5. The `ls` at the end
# is a second, independent check (the artefact exists), not the build's verdict:
# without --fresh a DLL from an earlier run would satisfy it on its own.
ct "cd $REMOTE_TREE && cmake -S . -B build-shim -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-mingw-i686.cmake -DCMAKE_BUILD_TYPE=Release >/tmp/gate-shim-cfg.log 2>&1; rc=\$?; cmake --build build-shim -j8 >/tmp/gate-shim.log 2>&1 || rc=1; tail -1 /tmp/gate-shim.log; ls -la build-shim/binkw32.dll || rc=1; exit \$rc"; report shim-cross-build $?
echo "== gate: $([ $status -eq 0 ] && echo GREEN || echo RED)"
exit $status

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
## tracecmp report: N.trace.jsonl
- build: cr-618ccb1-20260909T131556Z started: 2026-09-09T13:33:54Z inline_max: 256
- calls: 4 compared: 3 diverged: 0 invalid records: 0 warnings: 0
- coverage: 3 guarded call(s), 6 undeclared write(s) in 1 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | 3 | compare:3 | 3 | 0 | 0 |
| Shim::SelfTest::Fill | 1 | trace:1 | 0 | 0 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | partial | events, node[0], node[105], node[106], node[107], node[108], +271 | player, tree_header | 6 in 1 call(s) | 8 |
| Shim::SelfTest::Fill | complete | buf | - | not watched | 0 |
#### Game::TechTree::ProcessResearch — not checked by this run
- (medium) posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage: ours reproduces the decision and the id sequence, so region:events compares next_id, but the composed EvDsc/EvMsg text is not reproduced and no region can see it — text comes from the game's string table, which the engine must not carry; ours posts into its own EventStorage and writes only the counts into the scratch copy, so no live byte moves and replace mode posts nothing at all [region:events]
- (low) composes EVENT_TECHS_UNLOCKED's message from the unlocked techs' names — the trigger and the list are modelled (SetResearched's availability sweep plus the tail collector, both read off the instruction stream), so region:events compares next_id; the names come from the game's string table, so the message is composed from node indices instead and is not the game's text [region:events]
- (high) TechTree::SetResearched in REPLACE mode: only its TechTree half runs, and only when research.replace_cascade=on — with the flag OFF (the default) nothing of the cascade runs, so a replace run leaves the completed node unstamped and no tech unlocked. With it ON, the four TechNode words (costRP, turnAvailable, turnResearched, order) and the tree's completion-order counter are written live, and the ServerPlayer half is still not: no event is posted, no ObservedTech element is appended and no tech effect is applied. Neither setting is a full displacement of the completion path; the pair measures where the boundary is [guard:player, guard:tree_header]
- (high) ServerPlayer::OnTechResearched's tech effects: the ~90 hard-coded ServerPlayer field writes, the plague-cure masks, the design-option bitmasks and the species tech flags — B2's milestone. `ours` models only the two parts of the callback this hook's regions can see -- the observed-tech append and the RNG word RollResearchEvent draws before its branch (one word on a missed roll, two on a fired plague roll) -- and the rest is what the player guard reports [guard:player]
- (high) the research-event branch RollResearchEvent takes when its roll beats the odds (ServerPlayer::OnResearchRollSucceeded: the plague and AI-rebellion event paths) — RollResearchEvent draws one NextFloat unconditionally and that draw IS modelled -- but that is only the cost of REACHING the branch. A FIRED roll costs one or two words: the plague path draws a SECOND word (NextInt) to pick an owned system and posts EVENT_PLAGUE_OUTBREAK, while the rebellion path allocates an AIRebellion at ServerPlayer+0x3b8 and CANCELS the current research (no further draw). The branch is entered only for the plague and AI-rebellion tech families, whose odds are 0 everywhere else, and it has never been observed firing in three sessions -- which is why every earlier note in this repo said 'exactly one NextFloat' and nothing caught it. If it is ever entered, region:rng is the check [region:rng]
- (medium) constructs the ObservedTech element it appends to ServerPlayer+0x274 — `ours` models the append DECISION -- RecordObservedTech de-duplicates by tech name, so it decides whether the vector grows -- and moves the scratch header's byte span by one 0x2c element per append. The element's own fields (turn_first, turn_last, detected, the name string, `with`) are not built, and no region can see them [region:observed_techs]
- (low) the tree's completion-order counter (TechTree+0x20) is read pre-call, not modelled as a region — the per-node `order` word IS compared, and it is stamped from a counter `ours` seeds from the pre-call read and advances itself; the counter's own final value is only seen by the tree_header guard [guard:tree_header]
- (low) writes a completion line to the game log — log text is not simulation state
- guard hits in compare mode: player+0x10c:3, player+0x110:3, player+0x114:3, player+0x124:3, player+0x294:4, tree_header+0x20:1

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
## tracecmp report: R0.trace.jsonl
- build: cr-618ccb1-20260909T131556Z started: 2026-09-09T13:51:31Z inline_max: 256
- calls: 4 compared: 0 diverged: 0 invalid records: 0 warnings: 0
- coverage: 3 guarded call(s), 0 undeclared write(s) in 0 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | 3 | replace:3 | 0 | 0 | 0 |
| Shim::SelfTest::Fill | 1 | trace:1 | 0 | 0 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | partial | events, node[0], node[105], node[106], node[107], node[108], +271 | player, tree_header | 0 | 8 |
| Shim::SelfTest::Fill | complete | buf | - | not watched | 0 |
#### Game::TechTree::ProcessResearch — not checked by this run
- (medium) posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage: ours reproduces the decision and the id sequence, so region:events compares next_id, but the composed EvDsc/EvMsg text is not reproduced and no region can see it — text comes from the game's string table, which the engine must not carry; ours posts into its own EventStorage and writes only the counts into the scratch copy, so no live byte moves and replace mode posts nothing at all [region:events]
- (low) composes EVENT_TECHS_UNLOCKED's message from the unlocked techs' names — the trigger and the list are modelled (SetResearched's availability sweep plus the tail collector, both read off the instruction stream), so region:events compares next_id; the names come from the game's string table, so the message is composed from node indices instead and is not the game's text [region:events]
- (high) TechTree::SetResearched in REPLACE mode: only its TechTree half runs, and only when research.replace_cascade=on — with the flag OFF (the default) nothing of the cascade runs, so a replace run leaves the completed node unstamped and no tech unlocked. With it ON, the four TechNode words (costRP, turnAvailable, turnResearched, order) and the tree's completion-order counter are written live, and the ServerPlayer half is still not: no event is posted, no ObservedTech element is appended and no tech effect is applied. Neither setting is a full displacement of the completion path; the pair measures where the boundary is [guard:player, guard:tree_header]
- (high) ServerPlayer::OnTechResearched's tech effects: the ~90 hard-coded ServerPlayer field writes, the plague-cure masks, the design-option bitmasks and the species tech flags — B2's milestone. `ours` models only the two parts of the callback this hook's regions can see -- the observed-tech append and the RNG word RollResearchEvent draws before its branch (one word on a missed roll, two on a fired plague roll) -- and the rest is what the player guard reports [guard:player]
- (high) the research-event branch RollResearchEvent takes when its roll beats the odds (ServerPlayer::OnResearchRollSucceeded: the plague and AI-rebellion event paths) — RollResearchEvent draws one NextFloat unconditionally and that draw IS modelled -- but that is only the cost of REACHING the branch. A FIRED roll costs one or two words: the plague path draws a SECOND word (NextInt) to pick an owned system and posts EVENT_PLAGUE_OUTBREAK, while the rebellion path allocates an AIRebellion at ServerPlayer+0x3b8 and CANCELS the current research (no further draw). The branch is entered only for the plague and AI-rebellion tech families, whose odds are 0 everywhere else, and it has never been observed firing in three sessions -- which is why every earlier note in this repo said 'exactly one NextFloat' and nothing caught it. If it is ever entered, region:rng is the check [region:rng]
- (medium) constructs the ObservedTech element it appends to ServerPlayer+0x274 — `ours` models the append DECISION -- RecordObservedTech de-duplicates by tech name, so it decides whether the vector grows -- and moves the scratch header's byte span by one 0x2c element per append. The element's own fields (turn_first, turn_last, detected, the name string, `with`) are not built, and no region can see them [region:observed_techs]
- (low) the tree's completion-order counter (TechTree+0x20) is read pre-call, not modelled as a region — the per-node `order` word IS compared, and it is stamped from a counter `ours` seeds from the pre-call read and advances itself; the counter's own final value is only seen by the tree_header guard [guard:tree_header]
- (low) writes a completion line to the game log — log text is not simulation state

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
## tracecmp report: R1.trace.jsonl
- build: cr-618ccb1-20260909T131556Z started: 2026-09-09T13:42:14Z inline_max: 256
- calls: 4 compared: 0 diverged: 0 invalid records: 0 warnings: 0
- coverage: 3 guarded call(s), 1 undeclared write(s) in 1 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | 3 | replace:3 | 0 | 0 | 0 |
| Shim::SelfTest::Fill | 1 | trace:1 | 0 | 0 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | partial | events, node[0], node[105], node[106], node[107], node[108], +271 | player, tree_header | 1 in 1 call(s) | 8 |
| Shim::SelfTest::Fill | complete | buf | - | not watched | 0 |
#### Game::TechTree::ProcessResearch — not checked by this run
- (medium) posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage: ours reproduces the decision and the id sequence, so region:events compares next_id, but the composed EvDsc/EvMsg text is not reproduced and no region can see it — text comes from the game's string table, which the engine must not carry; ours posts into its own EventStorage and writes only the counts into the scratch copy, so no live byte moves and replace mode posts nothing at all [region:events]
- (low) composes EVENT_TECHS_UNLOCKED's message from the unlocked techs' names — the trigger and the list are modelled (SetResearched's availability sweep plus the tail collector, both read off the instruction stream), so region:events compares next_id; the names come from the game's string table, so the message is composed from node indices instead and is not the game's text [region:events]
- (high) TechTree::SetResearched in REPLACE mode: only its TechTree half runs, and only when research.replace_cascade=on — with the flag OFF (the default) nothing of the cascade runs, so a replace run leaves the completed node unstamped and no tech unlocked. With it ON, the four TechNode words (costRP, turnAvailable, turnResearched, order) and the tree's completion-order counter are written live, and the ServerPlayer half is still not: no event is posted, no ObservedTech element is appended and no tech effect is applied. Neither setting is a full displacement of the completion path; the pair measures where the boundary is [guard:player, guard:tree_header]
- (high) ServerPlayer::OnTechResearched's tech effects: the ~90 hard-coded ServerPlayer field writes, the plague-cure masks, the design-option bitmasks and the species tech flags — B2's milestone. `ours` models only the two parts of the callback this hook's regions can see -- the observed-tech append and the RNG word RollResearchEvent draws before its branch (one word on a missed roll, two on a fired plague roll) -- and the rest is what the player guard reports [guard:player]
- (high) the research-event branch RollResearchEvent takes when its roll beats the odds (ServerPlayer::OnResearchRollSucceeded: the plague and AI-rebellion event paths) — RollResearchEvent draws one NextFloat unconditionally and that draw IS modelled -- but that is only the cost of REACHING the branch. A FIRED roll costs one or two words: the plague path draws a SECOND word (NextInt) to pick an owned system and posts EVENT_PLAGUE_OUTBREAK, while the rebellion path allocates an AIRebellion at ServerPlayer+0x3b8 and CANCELS the current research (no further draw). The branch is entered only for the plague and AI-rebellion tech families, whose odds are 0 everywhere else, and it has never been observed firing in three sessions -- which is why every earlier note in this repo said 'exactly one NextFloat' and nothing caught it. If it is ever entered, region:rng is the check [region:rng]
- (medium) constructs the ObservedTech element it appends to ServerPlayer+0x274 — `ours` models the append DECISION -- RecordObservedTech de-duplicates by tech name, so it decides whether the vector grows -- and moves the scratch header's byte span by one 0x2c element per append. The element's own fields (turn_first, turn_last, detected, the name string, `with`) are not built, and no region can see them [region:observed_techs]
- (low) the tree's completion-order counter (TechTree+0x20) is read pre-call, not modelled as a region — the per-node `order` word IS compared, and it is stamped from a counter `ours` seeds from the pre-call read and advances itself; the counter's own final value is only seen by the tree_header guard [guard:tree_header]
- (low) writes a completion line to the game log — log text is not simulation state
- guard hits in replace mode: tree_header+0x20:1

View file

@ -20,6 +20,7 @@ those columns is an observation, not a certification.
| `ref-turn2.sav` `ab4ac2d7…` | one End Turn | load | `bb4fd9ac…` | `978041ac…` | 5 + 1 | runs A–E; reproduced by lane H after everything changed |
| `ar-turn37-816raiders.sav` `b6f4e05f…` | End Turn, **auto-resolve peacefully**, End Turn | load | `15b99255…` | `7a8b3d5e…` | **4** | lane AR ×3 `hooks=off`, lane BS ×1 instrumented |
| `ar-oracle-A-pre.sav` `15b99255…` | one End Turn | **load** | `33e30092…` | `4c356f59…` | **3** | lane BQ ×2 `hooks=off`, lane BS ×1 instrumented |
| `turn3-state.sav` `978041ac…` | one End Turn | **load** | `e00eed0c…` | `79df5047…` | **3** | lane CR ×2 `hooks=off`, ×1 compare-instrumented |
**The second and third rows are the same lineage and they do not agree with each other.** Row 2's
second turn reached by continuation gives `7a8b3d5e…`; the identical turn reached by *loading* row 2's
@ -29,6 +30,22 @@ boundary, and it is why the route column exists.
**Row 3 is the standalone's first trade-raid pair** — one End Turn, no encounter, four raid rolls at
one word each.
**Row 4 is the first pair on a turn that completes a research tech** (lane CR, 2026-09-09).
`turn3-state.sav` is the *output* of row 1 — the `(Autosave).sav` that `ref-turn2` + one End Turn
produces — and the turn it starts is the one where `Player[32 "Fane Lao"]` completes tech 144
`IND_Waldo` and the cascade unlocks three nodes. Row 1's own turn does **not** complete anything
(`unlock-b3-t1.md`: `0 undeclared write(s) in 0 call(s)`), so every research oracle before this row
was taken on a turn where the completion path did not run. Exposure at entry: none of the three
players with an AI client (32, 496, 512) has `ResTNm == ''`; `NumDes` does not move; two ships
complete and join existing fleets; the four factions at 528–576 carry the empty-`ResTNm` signature
inertly (`Status 0`, no client). Two ships completing *and* three fleets changing did **not** expose
this turn — a reminder that 26(c) is a screen and the control is the decision.
Masks for row 4, measured against the input: **exactly `/Summary/Checksum` and `Player.Status 4 → 0`
on the four live players. No `/CD[1]/NPrvVa` term** — this state's `CD[1]` diplomacy block is
early-game and the leaf does not move, so the documented `--mask resave` rule holds here in its
original form.
### Masks that must be on the line
- `Player.Status` (4 → 0 on load) and the derived `Summary.Checksum`.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -37,3 +37,11 @@ b6f4e05ff226eabd1695003b36293553642553cea96ef417364393e2332a7094 verify/results
1c8baa27680809d585ab1728391e82729642d32aaedffd5bf95b616f0a11a436 verify/results/saves/ad-turn27-two-raiders.sav
e913ff41188211c02d536da616337544e64133996cd7cc7a6521a8df1960226f verify/results/saves/bp-pinA-turn28.sav
724528ffde9a7514b69ab9e82ed3b8c8ade63d622c0720de30503a73a92df11e verify/results/saves/bp-pinB-turn28.sav
# Lane CR, 2026-09-09 -- VM145. Input turn3-state.sav (= the (Autosave).sav of certified-pairs
# row 1), one End Turn, LOAD route. Build cr-618ccb1-20260909T131556Z, shimdist-cr.
# C1/C2 hooks=off (two fresh processes), N compare, R0/R1 replace.
e00eed0c03a31d27a81b7470a9dcc9ba08a2ac48c20baeee4f34749164743e3f 67212 (Autosave EndTurn).sav C1 == C2 == N == R0 == R1
79df50475a7b83afa927d992b9f030dcf45710f4bda0133b8b1fa4800a72e420 67811 (Autosave).sav C1 == C2 == N [THE ORACLE]
6b51db992b158caa5424d71b2dccf72924af7198b4165bfcfc870e0d438fb6d5 67511 (Autosave).sav R0 replace, research.replace_cascade=off -> 27 leaves
8a4309ee4fe0b3177a2820600b5016c7c256d0f51b065df469ecd0b4f2342235 67537 (Autosave).sav R1 replace, research.replace_cascade=on -> 16 leaves

View file

@ -0,0 +1,118 @@
09:33:54.650 [tid 880] ==== sots-engine shim (binkw32 proxy) build cr-618ccb1-20260909T131556Z ====
09:33:54.650 [tid 880] exe: C:\SOTS\Sword of the Stars.exe
09:33:54.650 [tid 880] exe base=0x00f40000 (link-time image base 0x00400000, ASLR delta +11796480) pid=20996 shim=71d70000
09:33:54.650 [tid 880] addresses: Source: sots-re ghidra/addresses.json @ aa8d3fb, generated 2026-09-09 by tools/gen_addresses.py
09:33:54.650 [tid 880] config: hooks=trace
09:33:54.650 [tid 880] config: hook.Game::EncounterDetect::AssignContacts=off
09:33:54.650 [tid 880] config: hook.Game::EncounterDetect::ProcessTeamRecord=off
09:33:54.650 [tid 880] config: hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off
09:33:54.650 [tid 880] config: hook.Game::SVSOSwarmQueen::OnTurnBegin=off
09:33:54.650 [tid 880] config: hook.Game::SVSOSwarmQueen::RegisterHives=off
09:33:54.650 [tid 880] config: hook.Game::SVSOSwarmQueen::TickHives=off
09:33:54.650 [tid 880] config: hook.Game::SectionDictionary::SectionDictionary=off
09:33:54.650 [tid 880] config: hook.Game::ServerPlayer::ComputeBudget=off
09:33:54.650 [tid 880] config: hook.Game::ServerPlayer::OnTechResearched=off
09:33:54.650 [tid 880] config: hook.Game::ServerPlayer::ProcessTurn=off
09:33:54.650 [tid 880] config: hook.Game::ServerSystem::ComputeTotalOutput=off
09:33:54.650 [tid 880] config: hook.Game::ServerSystem::GroupOutput=off
09:33:54.650 [tid 880] config: hook.Game::ServerSystem::ProcessTurn=off
09:33:54.650 [tid 880] config: hook.Game::StrategyApp::RunAI=off
09:33:54.650 [tid 880] config: hook.Game::StrategyHost::Autosave=off
09:33:54.650 [tid 880] config: hook.Game::StrategyServer::ApplyEncounterResult=off
09:33:54.650 [tid 880] config: hook.Game::StrategyServer::BeginProcessTurn=off
09:33:54.650 [tid 880] config: hook.Game::StrategyServer::MoveFleet=off
09:33:54.650 [tid 880] config: hook.Game::StrategyServer::NodeLineDecay=off
09:33:54.650 [tid 880] config: hook.Game::StrategyServer::OnAllCombatDone_Tail=off
09:33:54.650 [tid 880] config: hook.Game::StrategyServer::ProcessFleetMovement=off
09:33:54.650 [tid 880] config: hook.Game::StrategyServer::ProcessNodeSpaceTravel=off
09:33:54.650 [tid 880] config: hook.Game::StrategyServer::ProcessTurn=off
09:33:54.650 [tid 880] config: hook.Game::WeaponDictionary::Init=off
09:33:54.650 [tid 880] config: hook.Mars::GlobalConsts::LoadFile=off
09:33:54.650 [tid 880] config: hook.Mars::RNG::Seed=off
09:33:54.650 [tid 880] config: hook.Game::TechTree::ProcessResearch=compare
09:33:54.650 [tid 880] config: research.replace_cascade=off
09:33:54.650 [tid 880] config: trace.path=C:\SOTS\shim.trace.jsonl
09:33:54.650 [tid 880] config: trace.inline_max=256
09:33:54.650 [tid 880] config: trace.flush=always
09:33:54.728 [tid 880] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always)
09:33:54.728 [tid 880] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=013e0e50
09:33:54.728 [tid 880] hook: MH_Initialize -> MH_OK
09:33:54.728 [tid 880] hook: MH_CreateHook -> MH_OK (trampoline=00a30fe0)
09:33:54.759 [tid 880] hook: MH_EnableHook -> MH_OK
09:33:54.759 [tid 880] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295)
09:33:54.759 [tid 880] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed)
09:33:54.759 [tid 880] dict: dictionaries hook ready (crt new=73e7232b delete=73e70174)
09:33:54.759 [tid 880] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed)
09:33:54.759 [tid 880] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed)
09:33:54.759 [tid 880] research: ProcessResearch hook ready (Cost=010bda00, node=0x34, rng=0x9cc, fpu_cw=0x027f, replace_cascade=off)
09:33:54.759 [tid 880] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010c76c0 MH_CreateHook -> MH_OK (trampoline=00a30fc0)
09:33:54.775 [tid 880] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=compare
09:33:54.775 [tid 880] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f)
09:33:54.775 [tid 880] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::ServerSystem::GroupOutput rva=0x0034b7a0 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::ServerSystem::ComputeTotalOutput rva=0x00350480 mode=off (not installed)
09:33:54.775 [tid 880] player_turn: ServerPlayer::ProcessTurn hook armed (ratio helper at 010be950)
09:33:54.775 [tid 880] hook: Game::ServerPlayer::ProcessTurn rva=0x00491340 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::StrategyHost::Autosave rva=0x00495210 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::StrategyServer::ProcessTurn rva=0x003dc6c0 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::StrategyServer::OnAllCombatDone_Tail rva=0x003d92a0 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::StrategyServer::ApplyEncounterResult rva=0x003d8920 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::StrategyServer::NodeLineDecay rva=0x003ae010 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::StrategyServer::ProcessNodeSpaceTravel rva=0x003a0e20 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::EncounterDetect::AssignContacts rva=0x003aa240 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::EncounterDetect::ProcessTeamRecord rva=0x003ca640 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::StrategyServer::BeginProcessTurn rva=0x003d98e0 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::SVSOSwarmQueen::OnTurnBegin rva=0x00129930 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::SVSOSwarmQueen::RegisterHives rva=0x00127630 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::SVSOSwarmQueen::TickHives rva=0x00127770 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::SVSOSlaversRefuel::UpdateDifficultyTier rva=0x00115820 mode=off (not installed)
09:33:54.775 [tid 880] hook: Mars::RNG::Seed rva=0x0009fdf0 mode=off (not installed)
09:33:54.775 [tid 880] hook: Game::StrategyApp::RunAI rva=0x004706f0 mode=off (not installed)
09:33:54.806 [tid 880] drawsite: Mars::RNG::NextFloat rva=0x0007d830 -> va=00fbd830 create=MH_OK enable=MH_OK
09:33:54.822 [tid 880] drawsite: Mars::RNG::NextInt rva=0x000271c0 -> va=00f671c0 create=MH_OK enable=MH_OK
09:33:54.853 [tid 880] drawsite: Mars::RNG::Chance rva=0x004e6dd0 -> va=01426dd0 create=MH_OK enable=MH_OK
09:33:54.869 [tid 880] drawsite: Mars::RNG::NextUInt rva=0x000f7670 -> va=01037670 create=MH_OK enable=MH_OK
09:33:54.884 [tid 880] drawsite: Mars::RNG::FloatRange rva=0x0007d8a0 -> va=00fbd8a0 create=MH_OK enable=MH_OK
09:33:54.916 [tid 880] drawsite: Mars::RNG::IntRangeBell rva=0x004e6d80 -> va=01426d80 create=MH_OK enable=MH_OK
09:33:54.931 [tid 880] drawsite: Mars::RNG::GaussianRange rva=0x004e6e30 -> va=01426e30 create=MH_OK enable=MH_OK
09:33:54.931 [tid 880] probe: installing 12 of 12 (probes= in shim.cfg)
09:33:54.947 [tid 880] probe: Game::ServerSpyManager::vslot13 rva=0x004877b0 -> va=013c77b0 create=MH_OK enable=MH_OK
09:33:54.963 [tid 880] probe: Game::ServerSpyManager::vslot14 rva=0x0048db80 -> va=013cdb80 create=MH_OK enable=MH_OK
09:33:54.994 [tid 880] probe: Game::ServerTradeManagerImpl::vslot13 rva=0x0048ef80 -> va=013cef80 create=MH_OK enable=MH_OK
09:33:55.009 [tid 880] probe: Game::ServerTradeManagerImpl::vslot15 rva=0x0042cca0 -> va=0136cca0 create=MH_OK enable=MH_OK
09:33:55.025 [tid 880] probe: Game::SpyManager::Slot13RngCallee rva=0x004408e0 -> va=013808e0 create=MH_OK enable=MH_OK
09:33:55.041 [tid 880] probe: Game::TradeManager::Slot13RngCalleeA rva=0x00420ca0 -> va=01360ca0 create=MH_OK enable=MH_OK
09:33:55.072 [tid 880] probe: Game::TradeManager::Slot13RngCalleeB rva=0x0048b440 -> va=013cb440 create=MH_OK enable=MH_OK
09:33:55.088 [tid 880] probe: Game::ServerTradeManager::CreateRaidEncounter rva=0x004938a0 -> va=013d38a0 create=MH_OK enable=MH_OK
09:33:55.103 [tid 880] probe: Game::ServerTradeManager::GenerateTradeRaidEncounters rva=0x00493290 -> va=013d3290 create=MH_OK enable=MH_OK
09:33:55.134 [tid 880] probe: Game::ServerSpyManager::vslot15 [control] rva=0x00487f30 -> va=013c7f30 create=MH_OK enable=MH_OK
09:33:55.150 [tid 880] probe: Game::ServerTradeManagerImpl::vslot14 [control] rva=0x004590d0 -> va=013990d0 create=MH_OK enable=MH_OK
09:33:55.166 [tid 880] probe: Game::EncounterDetect::Run [control] rva=0x003cb080 -> va=0130b080 create=MH_OK enable=MH_OK
09:33:55.166 [tid 880] watch: disabled (watch=off)
09:33:55.166 [tid 880] aiorders: disabled (aiorders=off)
09:33:55.166 [tid 880] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=off value=0x0000 sample_ticks=on
09:33:55.166 [tid 880] fpu: sample_turn=on (off releases StrategyServer::ProcessTurn for another hook)
09:33:55.166 [tid 880] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=012c3be0 MH_CreateHook -> MH_OK (trampoline=00a30d40)
09:33:55.197 [tid 880] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK
09:33:55.197 [tid 880] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=013198e0 MH_CreateHook -> MH_OK (trampoline=00a30d20)
09:33:55.213 [tid 880] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK
09:33:55.213 [tid 880] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0131c6c0 MH_CreateHook -> MH_OK (trampoline=00a30d00)
09:33:55.228 [tid 880] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK
09:33:55.228 [tid 880] fpu: DemoApp::OnTick rva=0x0049a640 -> va=013da640 MH_CreateHook -> MH_OK (trampoline=00a30ce0)
09:33:55.259 [tid 880] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK
09:33:55.259 [tid 880] selftest: Shim::SelfTest::Fill mode=trace checksum=075ef0c3 records=1
09:33:55.275 [tid 880] Application::Initialize called (this=00a88128)
09:33:56.166 [tid 880] fpu: TICK BASELINE at OnTick (this=00a88128): cw=0x127f 53bit-double/nearest
09:39:38.180 [tid 880] fpu: sample at StrategyClient::EndTurn (this=1e0711e8): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:39:41.289 [tid 880] fpu: sample at StrategyClient::EndTurn (this=32c5e628): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:39:41.336 [tid 880] fpu: sample at StrategyClient::EndTurn (this=32c58038): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:39:41.383 [tid 880] fpu: sample at StrategyClient::EndTurn (this=32c5d798): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:39:42.305 [tid 880] fpu: sample at StrategyServer::BeginProcessTurn (this=0d2908c8): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:39:42.305 [tid 880] fpu: sample at StrategyServer::ProcessTurn (this=0d2908c8): cw=0x127f 53bit-double/nearest
09:39:42.336 [tid 880] research: mode=compare steps=1 completions=1 overbudget=1166 cascade_possible=1 ok=1 cascade_completions=1 unlocked=3 otch_appends=1 roll_draws=0 failures=0 depth=0 name_unreadable=0
09:39:42.383 [tid 880] research: mode=compare steps=1 completions=0 overbudget=0 cascade_possible=1 ok=1 cascade_completions=0 unlocked=0 otch_appends=0 roll_draws=0 failures=0 depth=0 name_unreadable=0
09:39:42.398 [tid 880] research: mode=compare steps=1 completions=0 overbudget=0 cascade_possible=1 ok=1 cascade_completions=0 unlocked=0 otch_appends=0 roll_draws=0 failures=0 depth=0 name_unreadable=0

View file

@ -0,0 +1,118 @@
09:51:31.009 [tid 19724] ==== sots-engine shim (binkw32 proxy) build cr-618ccb1-20260909T131556Z ====
09:51:31.009 [tid 19724] exe: C:\SOTS\Sword of the Stars.exe
09:51:31.009 [tid 19724] exe base=0x00f40000 (link-time image base 0x00400000, ASLR delta +11796480) pid=20348 shim=71d70000
09:51:31.009 [tid 19724] addresses: Source: sots-re ghidra/addresses.json @ aa8d3fb, generated 2026-09-09 by tools/gen_addresses.py
09:51:31.009 [tid 19724] config: hooks=trace
09:51:31.009 [tid 19724] config: hook.Game::EncounterDetect::AssignContacts=off
09:51:31.009 [tid 19724] config: hook.Game::EncounterDetect::ProcessTeamRecord=off
09:51:31.009 [tid 19724] config: hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off
09:51:31.009 [tid 19724] config: hook.Game::SVSOSwarmQueen::OnTurnBegin=off
09:51:31.009 [tid 19724] config: hook.Game::SVSOSwarmQueen::RegisterHives=off
09:51:31.009 [tid 19724] config: hook.Game::SVSOSwarmQueen::TickHives=off
09:51:31.009 [tid 19724] config: hook.Game::SectionDictionary::SectionDictionary=off
09:51:31.025 [tid 19724] config: hook.Game::ServerPlayer::ComputeBudget=off
09:51:31.025 [tid 19724] config: hook.Game::ServerPlayer::OnTechResearched=off
09:51:31.025 [tid 19724] config: hook.Game::ServerPlayer::ProcessTurn=off
09:51:31.025 [tid 19724] config: hook.Game::ServerSystem::ComputeTotalOutput=off
09:51:31.025 [tid 19724] config: hook.Game::ServerSystem::GroupOutput=off
09:51:31.025 [tid 19724] config: hook.Game::ServerSystem::ProcessTurn=off
09:51:31.025 [tid 19724] config: hook.Game::StrategyApp::RunAI=off
09:51:31.025 [tid 19724] config: hook.Game::StrategyHost::Autosave=off
09:51:31.025 [tid 19724] config: hook.Game::StrategyServer::ApplyEncounterResult=off
09:51:31.025 [tid 19724] config: hook.Game::StrategyServer::BeginProcessTurn=off
09:51:31.025 [tid 19724] config: hook.Game::StrategyServer::MoveFleet=off
09:51:31.025 [tid 19724] config: hook.Game::StrategyServer::NodeLineDecay=off
09:51:31.025 [tid 19724] config: hook.Game::StrategyServer::OnAllCombatDone_Tail=off
09:51:31.025 [tid 19724] config: hook.Game::StrategyServer::ProcessFleetMovement=off
09:51:31.025 [tid 19724] config: hook.Game::StrategyServer::ProcessNodeSpaceTravel=off
09:51:31.025 [tid 19724] config: hook.Game::StrategyServer::ProcessTurn=off
09:51:31.025 [tid 19724] config: hook.Game::WeaponDictionary::Init=off
09:51:31.025 [tid 19724] config: hook.Mars::GlobalConsts::LoadFile=off
09:51:31.025 [tid 19724] config: hook.Mars::RNG::Seed=off
09:51:31.025 [tid 19724] config: hook.Game::TechTree::ProcessResearch=replace
09:51:31.025 [tid 19724] config: research.replace_cascade=off
09:51:31.025 [tid 19724] config: trace.path=C:\SOTS\shim.trace.jsonl
09:51:31.025 [tid 19724] config: trace.inline_max=256
09:51:31.025 [tid 19724] config: trace.flush=always
09:51:31.088 [tid 19724] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always)
09:51:31.088 [tid 19724] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=013e0e50
09:51:31.088 [tid 19724] hook: MH_Initialize -> MH_OK
09:51:31.088 [tid 19724] hook: MH_CreateHook -> MH_OK (trampoline=017e0fe0)
09:51:31.119 [tid 19724] hook: MH_EnableHook -> MH_OK
09:51:31.119 [tid 19724] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295)
09:51:31.119 [tid 19724] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed)
09:51:31.119 [tid 19724] dict: dictionaries hook ready (crt new=73e7232b delete=73e70174)
09:51:31.119 [tid 19724] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed)
09:51:31.119 [tid 19724] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed)
09:51:31.119 [tid 19724] research: ProcessResearch hook ready (Cost=010bda00, node=0x34, rng=0x9cc, fpu_cw=0x027f, replace_cascade=off)
09:51:31.119 [tid 19724] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010c76c0 MH_CreateHook -> MH_OK (trampoline=017e0fc0)
09:51:31.134 [tid 19724] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=replace
09:51:31.134 [tid 19724] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f)
09:51:31.134 [tid 19724] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::ServerSystem::GroupOutput rva=0x0034b7a0 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::ServerSystem::ComputeTotalOutput rva=0x00350480 mode=off (not installed)
09:51:31.134 [tid 19724] player_turn: ServerPlayer::ProcessTurn hook armed (ratio helper at 010be950)
09:51:31.134 [tid 19724] hook: Game::ServerPlayer::ProcessTurn rva=0x00491340 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::StrategyHost::Autosave rva=0x00495210 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::StrategyServer::ProcessTurn rva=0x003dc6c0 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::StrategyServer::OnAllCombatDone_Tail rva=0x003d92a0 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::StrategyServer::ApplyEncounterResult rva=0x003d8920 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::StrategyServer::NodeLineDecay rva=0x003ae010 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::StrategyServer::ProcessNodeSpaceTravel rva=0x003a0e20 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::EncounterDetect::AssignContacts rva=0x003aa240 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::EncounterDetect::ProcessTeamRecord rva=0x003ca640 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::StrategyServer::BeginProcessTurn rva=0x003d98e0 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::SVSOSwarmQueen::OnTurnBegin rva=0x00129930 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::SVSOSwarmQueen::RegisterHives rva=0x00127630 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::SVSOSwarmQueen::TickHives rva=0x00127770 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::SVSOSlaversRefuel::UpdateDifficultyTier rva=0x00115820 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Mars::RNG::Seed rva=0x0009fdf0 mode=off (not installed)
09:51:31.134 [tid 19724] hook: Game::StrategyApp::RunAI rva=0x004706f0 mode=off (not installed)
09:51:31.166 [tid 19724] drawsite: Mars::RNG::NextFloat rva=0x0007d830 -> va=00fbd830 create=MH_OK enable=MH_OK
09:51:31.181 [tid 19724] drawsite: Mars::RNG::NextInt rva=0x000271c0 -> va=00f671c0 create=MH_OK enable=MH_OK
09:51:31.213 [tid 19724] drawsite: Mars::RNG::Chance rva=0x004e6dd0 -> va=01426dd0 create=MH_OK enable=MH_OK
09:51:31.228 [tid 19724] drawsite: Mars::RNG::NextUInt rva=0x000f7670 -> va=01037670 create=MH_OK enable=MH_OK
09:51:31.275 [tid 19724] drawsite: Mars::RNG::FloatRange rva=0x0007d8a0 -> va=00fbd8a0 create=MH_OK enable=MH_OK
09:51:31.291 [tid 19724] drawsite: Mars::RNG::IntRangeBell rva=0x004e6d80 -> va=01426d80 create=MH_OK enable=MH_OK
09:51:31.306 [tid 19724] drawsite: Mars::RNG::GaussianRange rva=0x004e6e30 -> va=01426e30 create=MH_OK enable=MH_OK
09:51:31.306 [tid 19724] probe: installing 12 of 12 (probes= in shim.cfg)
09:51:31.338 [tid 19724] probe: Game::ServerSpyManager::vslot13 rva=0x004877b0 -> va=013c77b0 create=MH_OK enable=MH_OK
09:51:31.353 [tid 19724] probe: Game::ServerSpyManager::vslot14 rva=0x0048db80 -> va=013cdb80 create=MH_OK enable=MH_OK
09:51:31.369 [tid 19724] probe: Game::ServerTradeManagerImpl::vslot13 rva=0x0048ef80 -> va=013cef80 create=MH_OK enable=MH_OK
09:51:31.400 [tid 19724] probe: Game::ServerTradeManagerImpl::vslot15 rva=0x0042cca0 -> va=0136cca0 create=MH_OK enable=MH_OK
09:51:31.416 [tid 19724] probe: Game::SpyManager::Slot13RngCallee rva=0x004408e0 -> va=013808e0 create=MH_OK enable=MH_OK
09:51:31.431 [tid 19724] probe: Game::TradeManager::Slot13RngCalleeA rva=0x00420ca0 -> va=01360ca0 create=MH_OK enable=MH_OK
09:51:31.463 [tid 19724] probe: Game::TradeManager::Slot13RngCalleeB rva=0x0048b440 -> va=013cb440 create=MH_OK enable=MH_OK
09:51:31.478 [tid 19724] probe: Game::ServerTradeManager::CreateRaidEncounter rva=0x004938a0 -> va=013d38a0 create=MH_OK enable=MH_OK
09:51:31.494 [tid 19724] probe: Game::ServerTradeManager::GenerateTradeRaidEncounters rva=0x00493290 -> va=013d3290 create=MH_OK enable=MH_OK
09:51:31.525 [tid 19724] probe: Game::ServerSpyManager::vslot15 [control] rva=0x00487f30 -> va=013c7f30 create=MH_OK enable=MH_OK
09:51:31.541 [tid 19724] probe: Game::ServerTradeManagerImpl::vslot14 [control] rva=0x004590d0 -> va=013990d0 create=MH_OK enable=MH_OK
09:51:31.572 [tid 19724] probe: Game::EncounterDetect::Run [control] rva=0x003cb080 -> va=0130b080 create=MH_OK enable=MH_OK
09:51:31.572 [tid 19724] watch: disabled (watch=off)
09:51:31.572 [tid 19724] aiorders: disabled (aiorders=off)
09:51:31.572 [tid 19724] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=off value=0x0000 sample_ticks=on
09:51:31.572 [tid 19724] fpu: sample_turn=on (off releases StrategyServer::ProcessTurn for another hook)
09:51:31.572 [tid 19724] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=012c3be0 MH_CreateHook -> MH_OK (trampoline=017e0d40)
09:51:31.588 [tid 19724] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK
09:51:31.588 [tid 19724] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=013198e0 MH_CreateHook -> MH_OK (trampoline=017e0d20)
09:51:31.619 [tid 19724] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK
09:51:31.619 [tid 19724] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0131c6c0 MH_CreateHook -> MH_OK (trampoline=017e0d00)
09:51:31.634 [tid 19724] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK
09:51:31.634 [tid 19724] fpu: DemoApp::OnTick rva=0x0049a640 -> va=013da640 MH_CreateHook -> MH_OK (trampoline=017e0ce0)
09:51:31.666 [tid 19724] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK
09:51:31.666 [tid 19724] selftest: Shim::SelfTest::Fill mode=trace checksum=075ef0c3 records=1
09:51:31.681 [tid 19724] Application::Initialize called (this=03938128)
09:51:32.556 [tid 19724] fpu: TICK BASELINE at OnTick (this=03938128): cw=0x127f 53bit-double/nearest
09:57:42.520 [tid 19724] fpu: sample at StrategyClient::EndTurn (this=0dc22d40): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:57:45.614 [tid 19724] fpu: sample at StrategyClient::EndTurn (this=345fdee8): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:57:45.645 [tid 19724] fpu: sample at StrategyClient::EndTurn (this=345fabf0): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:57:45.692 [tid 19724] fpu: sample at StrategyClient::EndTurn (this=345fb338): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:57:46.598 [tid 19724] fpu: sample at StrategyServer::BeginProcessTurn (this=0dc25ab8): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:57:46.598 [tid 19724] fpu: sample at StrategyServer::ProcessTurn (this=0dc25ab8): cw=0x127f 53bit-double/nearest
09:57:46.614 [tid 19724] research: mode=replace steps=1 completions=1 overbudget=1166 cascade_possible=0 ok=0 cascade_completions=0 unlocked=0 otch_appends=0 roll_draws=0 failures=0 depth=0 name_unreadable=0
09:57:46.629 [tid 19724] research: mode=replace steps=1 completions=0 overbudget=0 cascade_possible=0 ok=0 cascade_completions=0 unlocked=0 otch_appends=0 roll_draws=0 failures=0 depth=0 name_unreadable=0
09:57:46.629 [tid 19724] research: mode=replace steps=1 completions=0 overbudget=0 cascade_possible=0 ok=0 cascade_completions=0 unlocked=0 otch_appends=0 roll_draws=0 failures=0 depth=0 name_unreadable=0

View file

@ -0,0 +1,118 @@
09:42:14.649 [tid 19172] ==== sots-engine shim (binkw32 proxy) build cr-618ccb1-20260909T131556Z ====
09:42:14.649 [tid 19172] exe: C:\SOTS\Sword of the Stars.exe
09:42:14.649 [tid 19172] exe base=0x00f40000 (link-time image base 0x00400000, ASLR delta +11796480) pid=20592 shim=71d70000
09:42:14.649 [tid 19172] addresses: Source: sots-re ghidra/addresses.json @ aa8d3fb, generated 2026-09-09 by tools/gen_addresses.py
09:42:14.649 [tid 19172] config: hooks=trace
09:42:14.649 [tid 19172] config: hook.Game::EncounterDetect::AssignContacts=off
09:42:14.649 [tid 19172] config: hook.Game::EncounterDetect::ProcessTeamRecord=off
09:42:14.649 [tid 19172] config: hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off
09:42:14.649 [tid 19172] config: hook.Game::SVSOSwarmQueen::OnTurnBegin=off
09:42:14.649 [tid 19172] config: hook.Game::SVSOSwarmQueen::RegisterHives=off
09:42:14.649 [tid 19172] config: hook.Game::SVSOSwarmQueen::TickHives=off
09:42:14.649 [tid 19172] config: hook.Game::SectionDictionary::SectionDictionary=off
09:42:14.649 [tid 19172] config: hook.Game::ServerPlayer::ComputeBudget=off
09:42:14.649 [tid 19172] config: hook.Game::ServerPlayer::OnTechResearched=off
09:42:14.649 [tid 19172] config: hook.Game::ServerPlayer::ProcessTurn=off
09:42:14.649 [tid 19172] config: hook.Game::ServerSystem::ComputeTotalOutput=off
09:42:14.649 [tid 19172] config: hook.Game::ServerSystem::GroupOutput=off
09:42:14.649 [tid 19172] config: hook.Game::ServerSystem::ProcessTurn=off
09:42:14.649 [tid 19172] config: hook.Game::StrategyApp::RunAI=off
09:42:14.649 [tid 19172] config: hook.Game::StrategyHost::Autosave=off
09:42:14.649 [tid 19172] config: hook.Game::StrategyServer::ApplyEncounterResult=off
09:42:14.649 [tid 19172] config: hook.Game::StrategyServer::BeginProcessTurn=off
09:42:14.649 [tid 19172] config: hook.Game::StrategyServer::MoveFleet=off
09:42:14.649 [tid 19172] config: hook.Game::StrategyServer::NodeLineDecay=off
09:42:14.649 [tid 19172] config: hook.Game::StrategyServer::OnAllCombatDone_Tail=off
09:42:14.649 [tid 19172] config: hook.Game::StrategyServer::ProcessFleetMovement=off
09:42:14.649 [tid 19172] config: hook.Game::StrategyServer::ProcessNodeSpaceTravel=off
09:42:14.649 [tid 19172] config: hook.Game::StrategyServer::ProcessTurn=off
09:42:14.649 [tid 19172] config: hook.Game::WeaponDictionary::Init=off
09:42:14.649 [tid 19172] config: hook.Mars::GlobalConsts::LoadFile=off
09:42:14.649 [tid 19172] config: hook.Mars::RNG::Seed=off
09:42:14.649 [tid 19172] config: hook.Game::TechTree::ProcessResearch=replace
09:42:14.649 [tid 19172] config: research.replace_cascade=on
09:42:14.649 [tid 19172] config: trace.path=C:\SOTS\shim.trace.jsonl
09:42:14.649 [tid 19172] config: trace.inline_max=256
09:42:14.649 [tid 19172] config: trace.flush=always
09:42:14.742 [tid 19172] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always)
09:42:14.742 [tid 19172] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=013e0e50
09:42:14.742 [tid 19172] hook: MH_Initialize -> MH_OK
09:42:14.742 [tid 19172] hook: MH_CreateHook -> MH_OK (trampoline=00cd0fe0)
09:42:14.773 [tid 19172] hook: MH_EnableHook -> MH_OK
09:42:14.773 [tid 19172] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295)
09:42:14.773 [tid 19172] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed)
09:42:14.773 [tid 19172] dict: dictionaries hook ready (crt new=73e7232b delete=73e70174)
09:42:14.773 [tid 19172] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed)
09:42:14.773 [tid 19172] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed)
09:42:14.773 [tid 19172] research: ProcessResearch hook ready (Cost=010bda00, node=0x34, rng=0x9cc, fpu_cw=0x027f, replace_cascade=on)
09:42:14.773 [tid 19172] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010c76c0 MH_CreateHook -> MH_OK (trampoline=00cd0fc0)
09:42:14.805 [tid 19172] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=replace
09:42:14.805 [tid 19172] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f)
09:42:14.805 [tid 19172] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::ServerSystem::GroupOutput rva=0x0034b7a0 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::ServerSystem::ComputeTotalOutput rva=0x00350480 mode=off (not installed)
09:42:14.805 [tid 19172] player_turn: ServerPlayer::ProcessTurn hook armed (ratio helper at 010be950)
09:42:14.805 [tid 19172] hook: Game::ServerPlayer::ProcessTurn rva=0x00491340 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::StrategyHost::Autosave rva=0x00495210 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::StrategyServer::ProcessTurn rva=0x003dc6c0 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::StrategyServer::OnAllCombatDone_Tail rva=0x003d92a0 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::StrategyServer::ApplyEncounterResult rva=0x003d8920 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::StrategyServer::NodeLineDecay rva=0x003ae010 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::StrategyServer::ProcessNodeSpaceTravel rva=0x003a0e20 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::EncounterDetect::AssignContacts rva=0x003aa240 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::EncounterDetect::ProcessTeamRecord rva=0x003ca640 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::StrategyServer::BeginProcessTurn rva=0x003d98e0 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::SVSOSwarmQueen::OnTurnBegin rva=0x00129930 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::SVSOSwarmQueen::RegisterHives rva=0x00127630 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::SVSOSwarmQueen::TickHives rva=0x00127770 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::SVSOSlaversRefuel::UpdateDifficultyTier rva=0x00115820 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Mars::RNG::Seed rva=0x0009fdf0 mode=off (not installed)
09:42:14.805 [tid 19172] hook: Game::StrategyApp::RunAI rva=0x004706f0 mode=off (not installed)
09:42:14.820 [tid 19172] drawsite: Mars::RNG::NextFloat rva=0x0007d830 -> va=00fbd830 create=MH_OK enable=MH_OK
09:42:14.852 [tid 19172] drawsite: Mars::RNG::NextInt rva=0x000271c0 -> va=00f671c0 create=MH_OK enable=MH_OK
09:42:14.867 [tid 19172] drawsite: Mars::RNG::Chance rva=0x004e6dd0 -> va=01426dd0 create=MH_OK enable=MH_OK
09:42:14.883 [tid 19172] drawsite: Mars::RNG::NextUInt rva=0x000f7670 -> va=01037670 create=MH_OK enable=MH_OK
09:42:14.916 [tid 19172] drawsite: Mars::RNG::FloatRange rva=0x0007d8a0 -> va=00fbd8a0 create=MH_OK enable=MH_OK
09:42:14.945 [tid 19172] drawsite: Mars::RNG::IntRangeBell rva=0x004e6d80 -> va=01426d80 create=MH_OK enable=MH_OK
09:42:14.961 [tid 19172] drawsite: Mars::RNG::GaussianRange rva=0x004e6e30 -> va=01426e30 create=MH_OK enable=MH_OK
09:42:14.961 [tid 19172] probe: installing 12 of 12 (probes= in shim.cfg)
09:42:14.992 [tid 19172] probe: Game::ServerSpyManager::vslot13 rva=0x004877b0 -> va=013c77b0 create=MH_OK enable=MH_OK
09:42:15.008 [tid 19172] probe: Game::ServerSpyManager::vslot14 rva=0x0048db80 -> va=013cdb80 create=MH_OK enable=MH_OK
09:42:15.039 [tid 19172] probe: Game::ServerTradeManagerImpl::vslot13 rva=0x0048ef80 -> va=013cef80 create=MH_OK enable=MH_OK
09:42:15.070 [tid 19172] probe: Game::ServerTradeManagerImpl::vslot15 rva=0x0042cca0 -> va=0136cca0 create=MH_OK enable=MH_OK
09:42:15.086 [tid 19172] probe: Game::SpyManager::Slot13RngCallee rva=0x004408e0 -> va=013808e0 create=MH_OK enable=MH_OK
09:42:15.118 [tid 19172] probe: Game::TradeManager::Slot13RngCalleeA rva=0x00420ca0 -> va=01360ca0 create=MH_OK enable=MH_OK
09:42:15.133 [tid 19172] probe: Game::TradeManager::Slot13RngCalleeB rva=0x0048b440 -> va=013cb440 create=MH_OK enable=MH_OK
09:42:15.148 [tid 19172] probe: Game::ServerTradeManager::CreateRaidEncounter rva=0x004938a0 -> va=013d38a0 create=MH_OK enable=MH_OK
09:42:15.180 [tid 19172] probe: Game::ServerTradeManager::GenerateTradeRaidEncounters rva=0x00493290 -> va=013d3290 create=MH_OK enable=MH_OK
09:42:15.211 [tid 19172] probe: Game::ServerSpyManager::vslot15 [control] rva=0x00487f30 -> va=013c7f30 create=MH_OK enable=MH_OK
09:42:15.226 [tid 19172] probe: Game::ServerTradeManagerImpl::vslot14 [control] rva=0x004590d0 -> va=013990d0 create=MH_OK enable=MH_OK
09:42:15.258 [tid 19172] probe: Game::EncounterDetect::Run [control] rva=0x003cb080 -> va=0130b080 create=MH_OK enable=MH_OK
09:42:15.258 [tid 19172] watch: disabled (watch=off)
09:42:15.258 [tid 19172] aiorders: disabled (aiorders=off)
09:42:15.258 [tid 19172] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=off value=0x0000 sample_ticks=on
09:42:15.258 [tid 19172] fpu: sample_turn=on (off releases StrategyServer::ProcessTurn for another hook)
09:42:15.258 [tid 19172] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=012c3be0 MH_CreateHook -> MH_OK (trampoline=00cd0d40)
09:42:15.273 [tid 19172] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK
09:42:15.273 [tid 19172] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=013198e0 MH_CreateHook -> MH_OK (trampoline=00cd0d20)
09:42:15.305 [tid 19172] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK
09:42:15.305 [tid 19172] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0131c6c0 MH_CreateHook -> MH_OK (trampoline=00cd0d00)
09:42:15.320 [tid 19172] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK
09:42:15.320 [tid 19172] fpu: DemoApp::OnTick rva=0x0049a640 -> va=013da640 MH_CreateHook -> MH_OK (trampoline=00cd0ce0)
09:42:15.351 [tid 19172] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK
09:42:15.351 [tid 19172] selftest: Shim::SelfTest::Fill mode=trace checksum=075ef0c3 records=1
09:42:15.367 [tid 19172] Application::Initialize called (this=03548128)
09:42:16.336 [tid 19172] fpu: TICK BASELINE at OnTick (this=03548128): cw=0x127f 53bit-double/nearest
09:48:17.041 [tid 19172] fpu: sample at StrategyClient::EndTurn (this=0d6dd1e0): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:48:20.150 [tid 19172] fpu: sample at StrategyClient::EndTurn (this=32e7aed0): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:48:20.181 [tid 19172] fpu: sample at StrategyClient::EndTurn (this=32e7f7a0): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:48:20.228 [tid 19172] fpu: sample at StrategyClient::EndTurn (this=32e7a040): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:48:21.134 [tid 19172] fpu: sample at StrategyServer::BeginProcessTurn (this=0d66ade8): cw=0x127f 53bit-double/nearest [no fpu.force configured]
09:48:21.134 [tid 19172] fpu: sample at StrategyServer::ProcessTurn (this=0d66ade8): cw=0x127f 53bit-double/nearest
09:48:21.150 [tid 19172] research: mode=replace steps=1 completions=1 overbudget=1166 cascade_possible=1 ok=1 cascade_completions=1 unlocked=3 otch_appends=1 roll_draws=0 failures=0 depth=0 name_unreadable=0
09:48:21.181 [tid 19172] research: mode=replace steps=1 completions=0 overbudget=0 cascade_possible=1 ok=1 cascade_completions=0 unlocked=0 otch_appends=0 roll_draws=0 failures=0 depth=0 name_unreadable=0
09:48:21.197 [tid 19172] research: mode=replace steps=1 completions=0 overbudget=0 cascade_possible=1 ok=1 cascade_completions=0 unlocked=0 otch_appends=0 roll_draws=0 failures=0 depth=0 name_unreadable=0

BIN
verify/traces/cr-N.jsonl.gz Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.