diff --git a/docs/U-unlock.md b/docs/U-unlock.md index 9348b4f..041c663 100644 --- a/docs/U-unlock.md +++ b/docs/U-unlock.md @@ -282,4 +282,149 @@ together. ## 5. Outcome -To be filled in from the run. +Build `unlock-405ba41-20260908T1026Z`, cross-built on CT111 +(`/srv/re-lab/build/sots-engine-u`, exports byte-identical to the real `binkw32.dll`), staged +`C:\SOTS\shimdist-u` on VM140. Recipe `shim.cfg.recapb3`, unchanged. + +**The residual is gone. All three runs are 0 divergent, `tracecmp` exit 0.** + +| run | calls | compared | diverged | exit | +|---|---|---|---|---| +| first End Turn (`ref-turn2` → Launch → End Turn) | 3 | 3 | **0** | 0 | +| five-turn continuation (turn 2 → 7) | 15 | 15 | **0** | 0 | +| Zuul (`zuul-turn5` → 10 End Turns, turn 5 → 15) | 20 | 20 | **0** | 0 | + +Reports `verify/results/compare/unlock-b3-{t1,t1-5,zuul}.md` in the RE repo; traces +`verify/traces/unlock-b3-*`; shim log `verify/results/shim/unlock-shim.log`. + +### 5.1 The oracle first + +`(Autosave EndTurn).sav` = `bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948`, +`(Autosave).sav` = `978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921` — **the same two +hashes lane R and lane V recorded**. The cascade does not perturb the running game. That check +comes first because a clean compare from a build that moved the game would be worthless. + +### 5.2 First End Turn — §4.1 held field for field + +3 calls, 0 divergent, exit 0, and every predicted argument: + +| | predicted | observed | +|---|---|---| +| `observed_techs_in` | 10 / 20 / 20 | **10 / 20 / 20** ✓ | +| `roll_pending_in` (call 0) | false | **false** ✓ | +| `order_counter_in` (call 0) | ≥ 0, not −1 | **22** ✓ | +| `research_target` | non-null | `0x049812f8` ✓ | +| `observed_scan_failed` / `_truncated` | absent | absent ✓ | +| shim log, all 3 calls | `ok=1 completions=0 unlocked=0 otch_appends=0 roll_draws=0 failures=0 depth=0 name_unreadable=0` | exactly that ✓ | + +The collector ran on all three and came back empty, which is the §3.2 check: it is only silent +because nothing became available, not because it was not asked. + +### 5.3 Five turns — §4.2 held on the deterministic half, and the rest was better than predicted + +Call 3 is exactly the call lane P and lane V pinned, and every number matched: + +| call 3 | predicted | observed | +|---|---|---| +| `events.next_id` | 5 → **7** | 5 → **7** ✓ | +| `node[132]` state / cost_rp / turn_available | 0→2 / INT_MAX→10000 / −1→4 | ✓ | +| `node[136]` | 0→2 / INT_MAX→16000 / −1→4 | ✓ | +| `node[142]` | 0→2 / INT_MAX→8000 / −1→4 | ✓ | +| `node[144]` state / turn_researched / order | 3→4 / 4 / **22** | ✓ | +| `observed_techs.bytes` | 440 → **484** | ✓ | +| `order_counter_in` | **22** | **22** ✓ | +| `roll_pending_in` | **false** | **false** ✓ | +| `observed_techs_in` | **10** | **10** ✓ | +| shim log | `completions=1 unlocked=3 otch_appends=1 roll_draws=0` | exactly that ✓ | + +**Call 9 was not the call I predicted, and that is the more interesting result.** From turn 5 the +AI picked a different research target than in lane V's session — lane R's documented trap #2, and +the reason the board calls the continuation *a* run and not *the* run. Lane V's call 9 completed +tech 142 and unlocked one node; mine completed **tech 9** and unlocked **three**: + +``` +node[9] state 3->4 progress 3064->6000 turn_researched -1->6 order -1->23 +node[3] state 0->2 cost_rp INT_MAX->13000 turn_available -1->6 +node[12] state 0->2 cost_rp INT_MAX->35000 turn_available -1->6 +node[18] state 0->2 cost_rp INT_MAX->4000 turn_available -1->6 +``` + +`next_id` 10 → **12** and `observed_techs.bytes` 484 → **528** as predicted, but the three costs — +13000, 35000, 4000 — appear in no earlier report and were not predicted by anyone. The model +reproduced them with zero divergences on a case it had never seen. An unrehearsed instance is +worth more than a rehearsed one, and it is the answer to "did the model just memorise lane V's +numbers": it cannot have, because lane V's numbers are not what happened. + +Call 12 then allocates to node 18 — the 4000-cost tech the cascade had just unlocked — which is +the cascade's output feeding the next turn's budget. + +### 5.4 One prediction missed: the guard count + +Predicted **10 undeclared writes in 2 calls**, observed **9 in 2 calls**, and the span list is not +the same one: + +``` +lane V: player+0x10c:3 +0x110:3 +0x114:3 +0x124:3 +0x294:4 +0x196:1 +0x3b4:1 tree_header+0x20:1 +lane U: player+0x10c:3 +0x110:3 +0x114:3 +0x124:3 +0x294:4 +0x130:3 tree_header+0x20:1 +``` + +This is the workload difference, not the code. `player+0x3b4` is gone because the pending-roll +byte was **already 0** when the completing call ran, so the original never wrote it (see §5.5). +`+0x196` (design-option mask B) is replaced by `+0x130` (`PopMod`) because a different tech +completed and different tech effects fired. Both are `OnTechResearched`'s writes and both are +declared unmodelled. `tree_header+0x20` is still there and still undeclared, exactly as §4.3 said +it would be: `ours` seeds the counter and advances its own copy, it does not model the live word. + +Coverage verdict `partial` with **8** unmodelled notes, as predicted. + +### 5.5 The `RollResearchEvent` draw — modelled, inside the compare, but NOT exercised + +This is the honest limit of the run, and it should not be read as more than it is. + +`roll_draws` was **0 on all 35 compared calls**. Not one completion in three runs had the pending +byte still set. The reason is visible in the trace and it is a real finding: `ResearchRollPending` +is normally consumed by `ServerPlayer::ProcessTurn` *before* `ProcessResearch` runs, because that +call site fires when the progress ratio crosses a threshold — which is precisely the turns +approaching completion. In the Zuul run `roll_pending_in` is `true` on the funded call for turns +8–12 and flips to **false** on turn 13, the turn before the tech completes on turn 14. + +So lane V's call-9 draw was the *rare* case (a tech that jumped past the threshold to completion +in one turn), not the normal one — which also explains lane R's "RNG matched 15 of 15". Three +sessions, three different answers: 0 extra draws, 1, and 0. + +What can be claimed: the draw is implemented at the right point in the stream (inside the owner +callback, between this entry's roll and the next entry's), its two inputs are read pre-call and +**reported in every record** (`research_target`, `roll_pending_in`), and `region:rng` compared +clean on all 35 calls — so the model is not drawing a word the original does not. What cannot be +claimed: that the branch has been seen to fire live. **The boundary is inside the compare, but the +compare has not yet had the chance to test it.** It needs a workload where a tech goes from below +the ProcessTurn threshold to complete in a single turn. + +### 5.6 Zuul — the double roll and the completion path, together + +20 calls, 0 divergent, exit 0, turn 5 → 15, **two** Zuul completions. Call 2 is the one lane V's +save was one End Turn short of: + +``` +call 2 turn 7 species 5 alloc {144, 1376} left 449 -> 447 next_index 175 -> 177 + node[144] state 3->4 turn_researched 7 order 21 + node[132] state 0->2 cost_rp INT_MAX->10000 turn_available 7 + node[136] state 0->2 cost_rp INT_MAX->16000 turn_available 7 + events.next_id 10 -> 12 observed_techs 396 -> 440 +``` + +The generator advances by **two** (the species-5 double roll) *and* the completion cascade runs, +in the same call, and `ours` reproduces the post-state bit for bit. Note the Zuul tree unlocks +only 132 and 136 from tech 144 where the Human tree also unlocks 142 — a per-species tree, so this +is an independent instance of the cascade and not a repeat of §5.3. A second completion follows on +call 16 (turn 14, `next_id` 26 → 28, `observed_techs` 440 → 484). + +### 5.7 What is now owed + +* The `RollResearchEvent` draw needs a workload that fires it (§5.5). +* `TechTree::GetProgressRatio` (0x0057e950) — the COMPLETE/UNDERBUDGET split is still analogy. + Count-neutral, so no run can see it. +* `def+0xb0`'s write site. The byte is read and honoured; where it comes from is not read. +* The `0x00889d60` branch behind the research-event roll, the temperance sweep, `PruneOldTurns`, + and replace mode on this hook — all still untouched. +* The ObservedTech element's own fields. `ours` decides the append; it does not build the element. diff --git a/include/generated/sots_addresses.h b/include/generated/sots_addresses.h index d0091f3..c9d7628 100644 --- a/include/generated/sots_addresses.h +++ b/include/generated/sots_addresses.h @@ -1,5 +1,5 @@ // GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1). -// Source: sots-re ghidra/addresses.json @ 545c715, generated 2026-09-08 by tools/gen_addresses.py +// Source: sots-re ghidra/addresses.json @ ae00053, generated 2026-09-08 by tools/gen_addresses.py // Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated). #pragma once #include