Two results from the same VM session, both of which needed a workload the
corpus cannot supply.
CDiff, played forward. ref-turn2 loaded and 49 End Turns driven through the UI
helper to frame 51, with the entry hook emitting one record per turn:
50 calls, frames 2..51, EXACTLY TWO STORES
frame 2 -1 -> 0
frame 50 0 -> 1 <- the modelled tier transition, at the modelled frame
and 47 turns between them on which the writer ran and wrote NOTHING
predict_path was computed at entry from the transcribed threshold table on all
fifty turns and agreed with the cdiff region on every one. Frame >= 100 is still
a code read and is not claimed as a measurement.
The AI seed probe (asked for by the coordinator, ranked above CDiff). Hooks on
Mars::RNG::Seed and StrategyApp::RunAI, two launches from turn1-state, load
only. Every AI client seed differs between processes -- net 32, 496 and 512 all
move -- while the record structure is identical and one Seed call with seed=0
produces a byte-identical state in both runs.
So the turn1-state -> turn2 nondeterminism is a SEED effect, not the ordering
effect predicted, and the 'one of three varies' observation is explained by two
of the three empires having a research pick that is robust to the stream. This
falsifies lane AI1's 'every draw from the static generator returns 0', which
that lane had explicitly flagged as arithmetic rather than measurement.
Where the seed comes from is NOT established; the finding names the one hook
that would settle it and the six values it must reproduce.
VM140 left as found: the 8-file save set with its oracle bytes intact.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
409 lines
24 KiB
Markdown
409 lines
24 KiB
Markdown
# Hive creation draws, inside `BeginProcessTurn` — lane Z's zero was the workload, not the code
|
||
|
||
- **Type:** control-flow (live measurement)
|
||
- **Status:** **verified** — five nested boundary hooks on the running game, reconciled against a
|
||
return-address draw-site ledger that shares no code path with them
|
||
- **Confidence:** high on the RNG result (two instruments agree word for word on two consecutive
|
||
turns, and the *values* the draws produce match two corpus saves exactly); see §7 for what is
|
||
not settled
|
||
- **Owner / date:** lane L1 · 2026-09-08 · VM140
|
||
- **Instrument:** `sots-engine` `wip/l1`, `src/shim/hooks/tail_rng.{h,cpp}` (five new hooks),
|
||
build `0117495-dirty-20260908T2113Z`, config `shim.cfg.l1hive`
|
||
- **Predictions, committed before the build:** `sots-engine/docs/L1-predictions.md`
|
||
- **Corrects:** `findings/control-flow/tail-rng-ledger.md` §0/§2 (qualified, not overturned) and
|
||
`findings/objects/svsctob-writers.md` §4.3 (one address)
|
||
|
||
---
|
||
|
||
## 0. The answer
|
||
|
||
**`SVSOSwarmQueen::RegisterHives` draws one strategic-generator word per new hive, inside
|
||
`StrategyServer::BeginProcessTurn`, which runs between the pre-turn autosave and
|
||
`StrategyServer::ProcessTurn` — outside both turn drivers and before either.** Measured on the End
|
||
Turn taken from `turn1-state.sav`, where the swarm's two hives are created:
|
||
|
||
| turn | `BeginProcessTurn` | `ProcessTurn` | tail | turn total | residual outside the drivers |
|
||
|---|---|---|---|---|---|
|
||
| 1 → 2 (**hives created**) | **2** | 20 | 0 | **22** | **2** |
|
||
| 2 → 3 (hives exist) | **0** | 19 | 0 | **19** | **0** |
|
||
|
||
Lane SV predicted this from the instruction stream and could not run it. It is right.
|
||
|
||
**Read the two rows' evidence differently, because they are not equally strong.** Row 2 is a closed
|
||
bracket: `Autosave(endTurn=1)` at word 192, `Autosave(endTurn=0)` at 211, subtotals 0 + 19 + 0,
|
||
residual 0 by subtraction. Row 1's bracket is **incomplete by construction** — it is the first End
|
||
Turn after a load, so the pre-turn autosave hook has no server pointer yet and records
|
||
`words: null`, the same limitation `tail-rng-ledger.md` reports for its turn 3. Its total of 22 comes
|
||
from the **independent per-call-site ledger** (§2.1), which sums to 22 with nothing unattributed and
|
||
carries the hive draws as their own row. So row 1's residual is *attributed*, not *subtracted* — and
|
||
row 2, which is subtracted, is the one that shows the residual returning to 0.
|
||
|
||
**What this does to `tail-rng-ledger.md`.** Its headline — *"a strategic turn advances the strategic
|
||
generator by 18–22 words, all of it inside `StrategyServer::ProcessTurn`, and the residual outside
|
||
the two turn drivers is exactly zero"* — is **correct for every turn it measured and false as a
|
||
statement about the code**. All eight of its turns, and the twelve more it added later, were turns on
|
||
which the hives already existed. On the one turn in this campaign's corpus where they do not, the
|
||
residual is 2 and it is outside `ProcessTurn`.
|
||
|
||
That is the same shape as "the tail draws nothing", which was true of eight turns and false on
|
||
turn 64. Rule 20's distinction, one level up: **a ledger that closes to zero is a statement about the
|
||
states that occurred.**
|
||
|
||
**What it means for the standalone.** A reimplementation that models `ProcessTurn` and the tail
|
||
perfectly writes a correct autosave on every turn of the corpus and is **two generator words out of
|
||
step on the turn the hives appear**, after which every later turn diverges. The interval it must
|
||
reproduce starts at `BeginProcessTurn`, not at `ProcessTurn`.
|
||
|
||
---
|
||
|
||
## 1. The instrument, and why it could see this when lane Z's could not
|
||
|
||
Lane Z's six hooks bracket `Autosave(endTurn=1) → ProcessTurn → combat → tail → Autosave(endTurn=0)`.
|
||
`BeginProcessTurn` runs **inside that bracket** and **outside every subtotal**, so a draw there was
|
||
always visible as *residual* — and lane Z measured the residual as 0, correctly, because on its turns
|
||
there was nothing to see. Nothing had ever been hooked in that interval.
|
||
|
||
This lane adds five nested hooks, each declaring the live `Mars::RNG` at `S+0x16c` as a region whose
|
||
`describe` reports an absolute word position (lane Z's `RngLedger`):
|
||
|
||
```
|
||
Autosave(endTurn=1) ....................... 192 <- lane Z
|
||
StrategyServer::BeginProcessTurn ........ NEW <- the interval nobody had hooked
|
||
SVSOSwarmQueen::OnTurnBegin ........... NEW (evt 0x13, vtable slot +0x60)
|
||
SVSOSwarmQueen::RegisterHives ....... NEW THE DRAW
|
||
SVSOSwarmQueen::TickHives ........... NEW the NextQ slip; 0 words
|
||
StrategyServer::ProcessTurn ............. 19 <- lane Z
|
||
StrategyServer::OnAllCombatDone_Tail .... 0 <- lane Z
|
||
SVSOSlaversRefuel::UpdateDifficultyTier NEW (evt 0x14, tail phase 20)
|
||
Autosave(endTurn=0) ....................... 211
|
||
```
|
||
|
||
Each new hook carries a **model evaluated at entry** — the hive count and the two config constants,
|
||
the per-hive `NextQ` values and the three gate constants, the whole `CDiff` threshold-table scan — so
|
||
the record can disagree with the model instead of merely agreeing with itself.
|
||
|
||
## 2. The hive-creation turn, measured
|
||
|
||
Workload: `turn1-state.sav` (Frame 1, `SvSctOb / EncID 10 / Hives` count **0**, two systems with
|
||
`EggScio == 3`), pushed to VM140 as `l1-turn1.sav`, loaded, **End Turn**.
|
||
|
||
```
|
||
[d2] SVSOSwarmQueen::RegisterHives words 170 -> 172 DELTA 2
|
||
scenario_tag = 3 enc_id = 10 hives_in = -1 (empty) systems = 28
|
||
systems_tagged = 2 predict_new_hives = 2 predict_min_words = 2
|
||
hive_nextq_lo = {ptr 0x0158e0a0, value 20}
|
||
hive_nextq_hi = {ptr 0x0158e0a4, value 30}
|
||
[d2] SVSOSwarmQueen::TickHives words 172 -> 172 DELTA 0
|
||
hives_in = 2
|
||
hives = [{sys 0x0e46e788, egg_scio 3, next_q 30, queen 0},
|
||
{sys 0x0e46dbe8, egg_scio 3, next_q 28, queen 0}]
|
||
gate_frame_floor = 10 gate_queen_age = 5 spawn_list_cap = 3
|
||
predict_gate1_fails = true predict_words = 0
|
||
[d1] SVSOSwarmQueen::OnTurnBegin words 170 -> 172 DELTA 2
|
||
[d0] StrategyServer::BeginProcessTurn words 170 -> 172 DELTA 2 side[frame] 1 -> 2
|
||
[d0] StrategyServer::ProcessTurn words 172 -> 192 DELTA 20
|
||
[d0] OnAllCombatDone_Tail words 192 -> 192 DELTA 0
|
||
```
|
||
|
||
`predict_new_hives` is computed at entry by transcribing the original's own two predicates over the
|
||
live `Systems` vector — `sys->EggScio(+0x184) == queen->scenarioTag(+0x4)` and "no existing hive
|
||
references this system". It said **2**; the generator moved **2**. The prediction and the measurement
|
||
were produced by different code paths and neither was fitted to the other.
|
||
|
||
### 2.1 A third instrument agrees, and it is keyed on the return address
|
||
|
||
The per-call-site ledger (lane Z's `draw_sites`, seven detoured generator entry points recording
|
||
`__builtin_return_address(0)`) reported, on the same turn:
|
||
|
||
| return VA | entry point | calls | words | site |
|
||
|---|---|---|---|---|
|
||
| **0x00527714** | **`RNG_NextInt`** | **2** | **2** | **`RegisterHives` — a site no previous run has ever recorded** |
|
||
| 0x0058788d | `NextFloat` | 1 | 1 | `TechTree::ProcessResearch`+0x1c8 |
|
||
| 0x0088df54 | `NextFloat` | 1 | 1 | `ServerPlayer::RollResearchEvent`+0x2f |
|
||
| 0x0089342b | `Chance` | 8 | 8 | `GenerateTradeRaidEncounters`+0x196 |
|
||
| 0x00893518 | `Chance` | 8 | 8 | `GenerateTradeRaidEncounters`+0x283 |
|
||
| 0x005032a2 | `NextFloat` | 1 | 1 | `DetectEncounters` subtree (lane H) |
|
||
| 0x007929a9 | `NextInt` | 1 | 1 | `DetectEncounters` subtree (lane H) |
|
||
| 0x008e6e09 | `NextFloat` | 16 | 16 | *inside `Chance`'s own body — the helper-internal double count* |
|
||
|
||
38 raw, minus the 16 helper-internal, = **22** = 2 (`BeginProcessTurn`) + 20 (`ProcessTurn`), with
|
||
nothing left over. On the next turn the same table reads 35 − 16 = **19** and **the 0x00527714 row is
|
||
absent entirely**.
|
||
|
||
0x00527714 is the return address of the `call 0x004271c0` at **0x0052770f**. Lane SV's document puts
|
||
that call at 0x0052770c; 0x0052770c is the `mov [ebp-0x14],esi` that stores the bound. Corrected in
|
||
`ghidra/addresses.d/lane-l1.json`.
|
||
|
||
### 2.2 The draws produced the historical values, which is the strongest check available
|
||
|
||
`NextQ = frame + LO + NextInt(HI − LO)`. Live: `LO = 20`, `HI = 30`, `frame = 2`.
|
||
|
||
| | hive on system A | hive on system B |
|
||
|---|---|---|
|
||
| after `RegisterHives` (measured) | `next_q = 30` ⇒ draw 8 | `next_q = 28` ⇒ draw 6 |
|
||
| after `TickHives`' slip, same turn | **31** | **29** |
|
||
| `turn2-state.sav` (historical) | **31** | **29** |
|
||
| after the next turn's slip (measured) | **32** | **30** |
|
||
| `turn3-state.sav` (historical) | **32** | **30** |
|
||
|
||
Two draws, two systems, four numbers, all matching saves captured in a different process on a
|
||
different day. **The generator trajectory is identical to the historical game's**, which is a much
|
||
stronger statement about instrument neutrality than a word count could be — and it is corroborated by
|
||
the absolute positions: this run reaches word **192** after turn 2 and **211** after turn 3, and
|
||
`tail-rng-ledger.md` §2.1 records `211` as the `ProcessTurn` entry position of turn 4 on the same
|
||
game.
|
||
|
||
## 3. `LO` and `HI` — the leaf lane SV declared blocked
|
||
|
||
> *"blocked on one MT draw and two data-file constants … behind pointers at 0x00ae0204/0x00ae0208
|
||
> that no `.text` or `.data` reference initialises in a form this lane could follow. Fitting `LO` and
|
||
> `HI` from a single two-hive observation would have been fitting, not derivation."*
|
||
|
||
Refusing to fit was right, and reading them live costs one `LogF`-shaped line:
|
||
|
||
| pointer | value | meaning |
|
||
|---|---|---|
|
||
| `*(int*)0x00ae0204` | **20** | `LO` — the low end of the queen-countdown window |
|
||
| `*(int*)0x00ae0208` | **30** | `HI` — the high end; `RNG_NextInt`'s bound is `HI − LO = 10`, inclusive |
|
||
| `*(int*)0x00ae0210` | **10** | `TickHives` gate 1: no queen may spawn while `Frame <= 10` |
|
||
| `*(int*)0x00ae0228` | **5** | `TickHives` gate 3: a minimum age in turns |
|
||
| `*(int*)0x00ae0220` | **3** | a cap applied inside the (never-executed) spawn arm |
|
||
|
||
So a new hive's countdown is `frame + 20 + U{0..10}`, and it then slips +1 per turn for as long as
|
||
any gate fails. The leaf is closed: a standalone needs these five integers and the two predicates,
|
||
not the data files.
|
||
|
||
## 4. The `NextQ` slip, live
|
||
|
||
Lane SV recovered the slip statically — a single `inc [hive+8]` at **0x0052785a** that four
|
||
gate-failure edges converge on — and reproduced 31/29 → 32/30 with no draw and no fitting. Confirmed:
|
||
|
||
* `TickHives` was **entered on both turns** and cost **0 words** both times.
|
||
* On turn 1→2 it received `next_q` 30 / 28 and the save carries 31 / 29.
|
||
* On turn 2→3 it received 31 / 29 and the save carries 32 / 30.
|
||
* `predict_gate1_fails` was **true** on both turns, and now has a number behind it:
|
||
`Frame <= 10`, so on this save **no queen can spawn before turn 11 whatever the countdown says**.
|
||
Every hive slips for at least the first ten turns of its life.
|
||
|
||
**The spawn arm still has never executed** (rule 6). No hive in any corpus save has a queen, and
|
||
`queens_in` was empty on both turns. What the gate constants now let anyone state is *why*, and how
|
||
far away it is, rather than "we never saw it".
|
||
|
||
## 5. `CDiff` — both edges, and one of them settled without a workload
|
||
|
||
`SVSOSlaversRefuel::UpdateDifficultyTier` 0x00515820 was entered **once per End Turn**, at depth 1
|
||
inside the tail (event 0x14, through the class's seven-instruction generic handler 0x0051a800), and
|
||
cost **0 words** both times.
|
||
|
||
| turn | `frame` | `cdiff_in` | `predict_path` | region `cdiff` before → after |
|
||
|---|---|---|---|---|
|
||
| 1 → 2 | 2 | **−1** | `store` | **−1 → 0** |
|
||
| 2 → 3 | 3 | 0 | `no_store_unchanged` | 0 → 0 |
|
||
|
||
The second row is the point. **"Entered and stored nothing" is now distinguishable from "did not
|
||
run"** — which is exactly what a save-file comparison cannot do, and why every corpus pair after
|
||
frame 2 shows `CDiff` motionless without saying whether the writer ran.
|
||
|
||
The table itself was re-read from the instruction stream, independently of lane SV, at real
|
||
instruction boundaries:
|
||
|
||
```
|
||
515868 [ebp-0x34] = 1 [ebp-0x30] = 1 [ebp-0x2c] = 1
|
||
515877 [ebp-0x28] = 0x32 [ebp-0x24] = 2 [ebp-0x20] = 3
|
||
515885 [ebp-0x1c] = 0x64 [ebp-0x18] = 2 [ebp-0x14] = 5
|
||
515893 xor eax,eax ; lea ecx,[ebp-0x34]
|
||
515898 cmp [ecx],edx ; jg 0x5158c2 ; first threshold GREATER than the frame
|
||
51589c add eax,ebx ; add ecx,0xc ; cmp eax,3 ; jl 0x515898
|
||
<falls through to 0x5158a6, which IS THE EPILOGUE -- no store>
|
||
5158c2 test eax,eax ; jle 0x5158a6 ; index 0 -> no store
|
||
5158cc cmp [edi+0x38],eax ; je 0x5158a6 ; unchanged -> no store
|
||
5158d1 mov [edi+0x38],eax ; CDiff = index - 1
|
||
```
|
||
|
||
Only the **threshold** column is ever loaded; the `(1,1) (2,3) (2,5)` payload columns are dead in
|
||
this function. So:
|
||
|
||
| frame | scan index | stored |
|
||
|---|---|---|
|
||
| ≤ 0 | 0 | nothing |
|
||
| 1 … 49 | 1 | `CDiff = 0` |
|
||
| 50 … 99 | 2 | `CDiff = 1` |
|
||
| ≥ 100 | walks off the end | **nothing** |
|
||
|
||
**`CDiff` can only ever hold 0 or 1. Tier 2 is unreachable at any frame**, and lane SV's reading of
|
||
that off-by-one is confirmed by a second independent disassembly. The class ctor 0x0051a820 stores
|
||
`[eax+0x38] = -1`, which is precisely the `CDiff = −1` that `turn1-state.sav` carries and the reason
|
||
frame ≤ 0 never matters: `Frame` starts at 1 and the tail does not run before the first End Turn.
|
||
|
||
### 5.1 The frame-50 crossing, played forward and measured
|
||
|
||
The corpus cannot reach frame 50 — its deepest save is turn 23 — so the game was played forward.
|
||
`ref-turn2.sav` loaded, then **49 End Turns** driven through the UI helper, with the entry hook
|
||
emitting one record per turn:
|
||
|
||
```
|
||
50 UpdateDifficultyTier calls, frames 2 .. 51
|
||
frame= 2 cdiff_in=-1 store -1 -> 0 <== STORE
|
||
frame= 49 cdiff_in= 0 no_store_unchanged 0 -> 0
|
||
frame= 50 cdiff_in= 0 store 0 -> 1 <== STORE
|
||
frame= 51 cdiff_in= 1 no_store_unchanged 1 -> 1
|
||
```
|
||
|
||
**Fifty consecutive turns, fifty entries, exactly two stores, at exactly the two predicted frames.**
|
||
Between them sit 47 turns on which the function ran and wrote nothing — which is the part a save-file
|
||
comparison can never show, and the reason the hook prints `predict_path` rather than only a value.
|
||
|
||
`predict_path` was computed at entry from the transcribed table on every one of the fifty turns and
|
||
agreed with the `cdiff` region on every one, including both transitions. The 47 quiet turns check
|
||
nothing by themselves and are reported that way; the two that moved are the test.
|
||
|
||
**Frame ≥ 100 was not reached** and is not claimed as a measurement. It is ~50 more End Turns at
|
||
~30 s each, and the answer is already a twice-read property of a three-entry table scan rather than a
|
||
question about state: at frame 100 the loop exits with index 3 and falls into the epilogue. The hook
|
||
is in the tree and prints `predict_path = no_store_ran_off_end_frame_ge_100` when it happens, so
|
||
whoever plays a game past frame 100 gets that edge for free.
|
||
|
||
## 6. Instrument neutrality (rule 19) — checked, and it needed checking twice
|
||
|
||
Five new MinHook detours were installed for this lane, on top of lane Z's six template hooks, seven
|
||
draw-site detours and twelve entry probes. Lane H's finding is that **one** detour on a clean
|
||
prologue boundary changed the game's output by four bytes, so this was a real prediction, not a
|
||
formality.
|
||
|
||
**The oracle reproduces, exactly.** Same build, same `shim.cfg.l1hive`, load `ref-turn2.sav`, one
|
||
End Turn:
|
||
|
||
| file | size | sha256 prefix | historical |
|
||
|---|---|---|---|
|
||
| `(Autosave EndTurn).sav` | 66,732 | `bb4fd9ac89f41e3b` | **identical** |
|
||
| `(Autosave).sav` | 67,219 | `978041acd168b56e` | **identical** |
|
||
|
||
That run's trace is the second half of the same story: `RegisterHives` entered with the two hives
|
||
already present and `NextQ` 31 / 29, drew **0**, `BeginProcessTurn` cost **0**, `ProcessTurn` cost
|
||
**19**, position 192 → 211. So the hooks that measured 2 words on the creation turn measure 0 on a
|
||
turn that produces a byte-identical historical autosave.
|
||
|
||
### 6.1 The creation turn is NOT a byte oracle — and the reason is a live nondeterminism, not the hooks
|
||
|
||
Loading `turn1-state.sav` and ending one turn does **not** reproduce `turn2-state.sav`
|
||
(`ab4ac2d7…`); it produces a 66,739-byte save with a different hash. Reported here rather than
|
||
buried, because it is the kind of thing that looks like instrument perturbation and is not:
|
||
|
||
* The **generator trajectory is identical** to the historical game's — same word positions (192 after
|
||
turn 2, 211 after turn 3), and the two hive draws produce `NextQ` 31 / 29 and then 32 / 30, exactly
|
||
the values `turn2-state.sav` and `turn3-state.sav` carry.
|
||
* The **pre-turn** autosave round-trips byte-identically (`a3f9dc4b…` in, `a3f9dc4b…` out).
|
||
* The whole difference, at schema level, is **one field on one player**:
|
||
|
||
| player | historical `turn2-state.sav` | this run |
|
||
|---|---|---|
|
||
| 3 | `ResTNm = BIO_GnMod` | `ResTNm = XNC_TrnsLir2` |
|
||
|
||
Players 0, 1 and 2 agree exactly (`""`, `IND_Waldo`, `DRV_PlsFiss`), as do `Status`, `EvNxID`,
|
||
`NumOwn` and every other field checked. The two `TechTree/St` bytes that also move are that choice's
|
||
consequence, and `Summary/Checksum` follows.
|
||
|
||
The `hooks=off` control was then run from the same input, and it produced a **third** answer.
|
||
|
||
**Lane L5 found the same thing independently on VM146 the same day**, from the other direction — as a
|
||
by-product of its own rule-19 control — and owns the write-up:
|
||
`findings/subsystems/turn1-to-turn2-nondeterminism.md`. This lane's two runs are corroboration and
|
||
they widen its spread, so the union is recorded here and the analysis is not repeated.
|
||
|
||
| run | instrument | `(Autosave).sav` | that player's `ResTNm` |
|
||
|---|---|---|---|
|
||
| the original 2026-09-07 session (live game, never loaded) | phase-2 shim, `hooks=trace` | `ab4ac2d7e2977260` | `BIO_GnMod` |
|
||
| lane L5 run B | `ComputeBudget=compare` | `ab4ac2d7e2977260` | `BIO_GnMod` |
|
||
| lane L5 run A | same hooks, different constant in `ours` only | `7fc9e6ab46b47794` | `XNC_TrnsHvr2` |
|
||
| lane L5 control | `hooks=off` | `62061918176e3441` | `XNC_TrnsLir2` |
|
||
| **L1** | full L1 hook set | `62061918176e3441` | `XNC_TrnsLir2` |
|
||
| **L1 control** | **`hooks=off`** | **`e43ec1d2b443c101`** | **`XNC_TrnsMorr2`** |
|
||
|
||
**The two rows in bold are what this lane adds, and the second one sharpens lane L5's conclusion.**
|
||
L5 argued from "the `hooks=off` control is a *third* value, not one of the other two". This lane ran
|
||
`hooks=off` again and got a **fourth** value — so **`hooks=off` is not self-consistent with itself
|
||
across processes**, which is a stronger statement than "no hook is responsible" and rules out the
|
||
whole class of explanations in which some particular instrument configuration selects the pick.
|
||
Four distinct outcomes over six runs, and this lane's instrumented run happens to coincide with
|
||
L5's uninstrumented one, which is the clearest possible demonstration that the hooks are not the
|
||
variable.
|
||
|
||
Players 0, 1 and 2 pick identically in every run (`""`, `IND_Waldo`, `DRV_PlsFiss`). Only the
|
||
shadow-empire player moves. In this lane's instrumented run the strategic generator's trajectory was
|
||
**identical to the historical game's** — same word positions, and the hive draws produced the
|
||
historical `NextQ` values — so whatever chooses here does not draw from it.
|
||
|
||
### 6.2 It is bounded, and the strategic generator is not what varies
|
||
|
||
**No claim in §0–§5 rests on an autosave diff.** The hive result is a *generator word position*
|
||
measured by two instruments that share no code path — a region ledger reading `left` around a phase
|
||
boundary, and a set of detours keyed on return address — plus the entry-side prediction those
|
||
numbers were checked against. Diffing `turn1-state → turn2` bytes is not part of it, and would not
|
||
have been able to answer the question either way.
|
||
|
||
The variation is small and exactly located. Every pairwise leaf comparison over the three post-turn
|
||
saves this lane holds gives **4 differing leaves out of 35,032**, with the leaf count and the whole
|
||
tree structure identical:
|
||
|
||
```
|
||
/Summary/Checksum (derived)
|
||
/Sim/Player/TechTree/St 2 <-> 3 (the node it stopped researching)
|
||
/Sim/Player/TechTree/St 3 <-> 2 (the node it started researching)
|
||
/Sim/Player/ResTNm BIO_GnMod | XNC_TrnsLir2 | XNC_TrnsMorr2 | XNC_TrnsHvr2
|
||
```
|
||
|
||
so the reference pair remains usable for everything except one shadow empire's research selection.
|
||
This matches lane L5's characterisation exactly, on a fourth sample.
|
||
|
||
**And it is not the strategic generator.** This is the part this lane can add that a save diff
|
||
cannot: in the instrumented run the strategic generator's trajectory was **identical to the
|
||
historical game's** — the two hive draws produced the historical `NextQ` 31 / 29, the next turn's
|
||
slip produced 32 / 30, and the absolute word positions 192 and 211 are the ones
|
||
`tail-rng-ledger.md` §2.1 records for the same game. A pick drawn from `S+0x16c` would have moved
|
||
that trajectory. It did not.
|
||
|
||
There is an obvious next place to look, and this lane's own draw-site table points at it: **8 words
|
||
per turn are drawn from a *different* `Mars::RNG` in the same process** — the `StrategyClient`'s at
|
||
`client+0x134`, from `0x00578cf0`, `0x005798e0` and `0x0069dbb0` (`tail-rng-ledger.md` §11.2 first
|
||
identified these and correctly excluded them). That object is **not serialised**, so if the AI's
|
||
pick draws from it, its seed is not in the save and a per-process variation is exactly what one
|
||
would expect — which is lane AI1's "seeded from an unseeded global" reading, arriving from a
|
||
different direction. Two rows of that table appear on every turn of both saves measured here:
|
||
`draw_site_words_other_rng = 8`, `draw_site_calls_other_rng = 8`.
|
||
|
||
**The experiment that would settle it** is a boundary hook on the AI research-selection site with a
|
||
region over `client+0x134` rather than `S+0x16c`, on two fresh processes: if the two runs enter it
|
||
with different generator state, the cause is found. That is one hook and one afternoon, and it is
|
||
the difference between "the reference pair has four unstable leaves" and "we know why".
|
||
|
||
## 7. What this does NOT settle (rule 15)
|
||
|
||
* **One hive-creation event, on one save.** `turn1-state.sav` is the only save in the corpus whose
|
||
swarm queen has zero hives *and* whose next turn creates some; the six Zuul saves sit at zero hives
|
||
for eighteen turns and never transition. So "one word per new hive" is confirmed for **two** hives
|
||
created in **one** call and extrapolated beyond that.
|
||
* **`RNG_NextInt`'s rejection loop never fired.** Bound 10 ⇒ mask 15, so a draw is rejected with
|
||
probability 5/16 and the *expected* cost is 16/11 ≈ 1.45 words per hive, not 1. Two hives costing
|
||
exactly 2 is a ~47% outcome. **A reimplementation must model the rejection loop, not the constant.**
|
||
This run cannot tell the two apart and does not claim to.
|
||
* **The spawn arm, and everything past it.** No queen has ever spawned under any instrument. Its
|
||
callees 0x0050dfc0 and 0x004fe810 are unread and may draw.
|
||
* **`CDiff` at frame 50 and frame 100** — modelled and predicted, not observed (§5).
|
||
* **`BeginProcessTurn`'s other work is unmodelled.** Its two container walks (three calls per system,
|
||
one per fleet) and its event post are not declared by anything. On these two turns their cost is
|
||
zero *by subtraction* — `BeginProcessTurn` and `OnTurnBegin` agree to the word on both turns — but
|
||
that is a measurement on 28 systems and one player's fleets, not a proof.
|
||
* **No game-state region is declared by any of these hooks.** They measure the generator and nothing
|
||
else. A clean run here says the RNG accounting is right and says nothing about whether the turn was
|
||
computed correctly.
|
||
* **Event 0x15 and the alliance events remain unreachable** from any save we hold — unchanged from
|
||
lane SV.
|
||
|
||
## 8. Files
|
||
|
||
- Traces: `verify/results/shim/l1/trace-et1.jsonl` (End Turn 1, hive creation),
|
||
`trace-et2.jsonl` (both End Turns; the second is the control turn)
|
||
- Report tool used to read them: `verify/results/shim/l1/rep.py`
|
||
- Saves produced: `verify/results/shim/l1/`
|
||
- Addresses: `ghidra/addresses.d/lane-l1.json` (10 entries)
|
||
- Instrument + predictions: `sots-engine` `wip/l1`, `docs/L1-predictions.md`
|