Lane SV recovered the script-object subsystem statically and predicted that SVSOSwarmQueen::RegisterHives takes one strategic-generator word per new hive inside StrategyServer::BeginProcessTurn, which runs inside lane Z's autosave bracket and outside both turn drivers. Nothing had ever been hooked in that interval. Measured on VM140, and it is right. turn 1 -> 2 (hives created) BeginProcessTurn 2 ProcessTurn 20 residual 2 turn 2 -> 3 (hives exist) BeginProcessTurn 0 ProcessTurn 19 residual 0 Two instruments that share no code path agree: the region ledger reads 170 -> 172 across RegisterHives, and the return-address draw-site table gains a row at 0x00527714 (NextInt, 2 calls / 2 words) which is absent on the next turn. The draws produce NextQ 30/28, TickHives' slip takes them to 31/29 -- exactly turn2-state.sav -- and the next turn's slip gives 32/30, exactly turn3-state.sav. So tail-rng-ledger.md's "all of it inside ProcessTurn, residual exactly zero" is correct for every turn it measured and false as a statement about the code. The interval a standalone must reproduce starts at BeginProcessTurn. Also closed or corrected: * LO/HI read live rather than fitted: NextQ = frame + 20 + NextInt(10), inclusive; the TickHives gates are 10 (a frame floor), 5 and 3, so no queen can spawn before frame 11 on any save. * CDiff's threshold scan re-read independently: the tier can only ever be 0 or 1, and "entered and stored nothing" was observed live and distinguished from "did not run" (method rule 20). * SnLv measured for the first time: Spica reads AFlags 0 with SnLv 0x200, so the sensor branch is what refreshes it -- nvo-tshn-visible-owner.md §6 goes [H] to [V]. ComputeContactLevel's documented "else 1" names one of four return tails; the observed non-visible level is 2. * Rule 19: the oracle reproduced byte for byte with all five new detours live, and again with four watchpoints armed on top. * turn1-state -> turn2 is not deterministic (lane L5 owns this). Two more runs here make it four distinct outcomes over six runs, including two DIFFERENT hooks=off results, and the strategic generator is provably not what varies. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
385 lines
23 KiB
Markdown
385 lines
23 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.
|
||
|
||
What is **not** yet measured is the 50 and 100 crossings live. §7 says what that costs and why it was
|
||
not bought with the VM time available; the entry hook is in the tree and prints `predict_path` on
|
||
every turn, so whoever plays a game past frame 50 gets both edges 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`
|