board: lane L5 - reference pair non-deterministic; interest literals verified with a failing control; verified column held at 0 on principle

This commit is contained in:
alex 2026-09-08 17:55:47 -04:00
parent ea7881acb3
commit e4195d47d1
20 changed files with 895 additions and 2 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,344 @@
# 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 is the reason this section is worth
reading. **It produced a third answer.**
| run | instrument | `(Autosave).sav` | size | player 3's `ResTNm` |
|---|---|---|---|---|
| the original 2026-09-07 session (live game, never loaded) | phase-2 shim, `hooks=trace` | `ab4ac2d7e2977260` | 66,739 | `BIO_GnMod` |
| this lane | full L1 hook set | `62061918176e3441` | 66,739 | `XNC_TrnsLir2` |
| this lane, control | **`hooks=off` — nothing installed at all** | `e43ec1d2b443c101` | **66,740** | `XNC_TrnsMorr2` |
Players 0, 1 and 2 pick identically in all three (`""`, `IND_Waldo`, `DRV_PlsFiss`). Only player 3
moves, and it moves **with the instrument removed as well**.
**So the turn-1 → turn-2 transition of this game is not deterministic across processes, and no hook
is responsible.** The three picks are three different tech ids, two of them species-tagged variants
of the same node (`XNC_TrnsLir2` / `XNC_TrnsMorr2`), which is the signature of a selection over a
container whose order is not fixed — a pointer-keyed map or an unstable tie-break — rather than of a
generator draw. The strategic generator's trajectory was **identical to the historical game's** in
the instrumented run, so whatever chooses here does not draw from it.
Two consequences, both of which matter more than this lane:
* **`turn1-state.sav` must not be used as a determinism-oracle input.** `determinism-oracle.md`
establishes byte-identity for `ref-turn2 → turn 3` and was never run on turn 1; that claim is
intact and this is a different pair.
* **The standalone milestone has a target that the original itself does not always hit.** A
reimplementation cannot be held to a byte match on a turn where the original disagrees with itself.
Which turns those are is now an open question with one known member.
None of this touches §0–§5: the RNG numbers were taken on a run whose generator trajectory matches
the historical game word for word, and the same hook set reproduces the oracle exactly on the
adjacent turn.
## 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`

View file

@ -15,6 +15,24 @@ outside the two turn drivers is **exactly zero**. The tail's cost on these turns
**latent**: it will bite the first turn a node line expires or a real battle resolves, and our saves reach
neither.
> **QUALIFIED BY LANE L1, 2026-09-08 — the residual is zero for the turns measured, not for the code.**
> (`findings/control-flow/hive-creation-rng.md`.) **`StrategyServer::BeginProcessTurn` runs inside this
> document's autosave bracket and outside every one of its subtotals, and nothing had ever been hooked
> there.** On every turn of both saves measured here the swarm hives already existed, so it cost 0 and the
> residual closed. On the one corpus turn that *creates* them — `turn1-state.sav`, Frame 1 → 2 —
> `SVSOSwarmQueen::RegisterHives` takes **one `RNG_NextInt` per new hive** at 0x0052770f, and the measured
> split is `BeginProcessTurn` **2** + `ProcessTurn` **20** = 22, i.e. a **residual of 2**. The very next
> turn it is 0 again (`BeginProcessTurn` 0, `ProcessTurn` 19, bracket 192 → 211), and lane Z's
> instrument reproduces this document's own positions exactly.
>
> Nothing in the tables below is wrong and no number moves. What changes is the scope of the headline:
> **"all of it inside `ProcessTurn`" is a statement about eight turns on which no script object created
> anything.** §11's per-site table gains one row on such a turn — return address **0x00527714**, entry
> point `NextInt`, 2 calls / 2 words — and the same table on the following turn does not carry it.
>
> For the standalone this changes one thing: **the interval to reproduce starts at `BeginProcessTurn`,
> not at `ProcessTurn`.**
---
## 0. Where to start if you are building the standalone

View file

@ -264,6 +264,27 @@ the gates open. The gates read config pointers at 0x00ae0210, 0x00ae0228, 0x00ae
The `NextQ` **slip** is not blocked and is exact: on `turn2 -> turn3`, where the hives already exist,
the modelled rule reproduces both target turns (31 -> 32, 29 -> 30) with no draw and no fitting.
> **RUN AND CONFIRMED BY LANE L1, 2026-09-08** (`findings/control-flow/hive-creation-rng.md`). Every
> claim in §4 and §5 below was put under an instrument on VM140 and held, with two corrections of
> detail and one leaf closed:
>
> * **Hive creation draws, and lane Z's zero is the workload.** On the `turn1-state.sav` End Turn,
> `RegisterHives` was entered once inside `BeginProcessTurn` and cost **exactly 2 words** for 2 new
> hives; `BeginProcessTurn`'s total was 2 and the residual outside the two drivers was **2**, not 0.
> On the next turn all of those are 0. The prediction in §5 was right.
> * **The `NextQ` slip is exact, live.** `TickHives` cost 0 words on both turns; the hives left
> `RegisterHives` at `NextQ` 30 / 28, the save carries 31 / 29, and the next turn's save carries
> 32 / 30 — reproducing `turn2-state.sav` and `turn3-state.sav` from a draw plus an `inc`.
> * **`LO` and `HI` are closed, by reading them live rather than fitting them**: `*(int*)0x00ae0204 =
> **20**`, `*(int*)0x00ae0208 = **30**`, so `NextQ = frame + 20 + NextInt(10)` (inclusive). The
> `TickHives` gates read 10 (a frame floor), 5 and 3 — so no queen can spawn before frame 11 on any
> save, which is why the spawn arm has never run.
> * **`CDiff`'s two invisible edges are confirmed** — the table scan was re-read independently and
> `CDiff` can only ever hold 0 or 1 — and the "stored nothing" case was observed live and
> distinguished from "did not run": the writer is entered every turn and stored only on frame 2.
> * **One address corrected:** the draw call in §4.3 is at **0x0052770f** (return 0x00527714), not
> 0x0052770c, which is the `mov [ebp-0x14],esi` that stores the bound.
**An RNG claim this lane did not measure.** Lane Z measured a strategic turn at 18–22 generator words,
*all* inside `ProcessTurn`, with the residual outside the two turn drivers **exactly zero** — on turns
6 and 64, where the swarm hives already existed. Hive creation draws **inside `BeginProcessTurn`**,

View file

@ -5,6 +5,22 @@ Question: "load the same save, press End Turn, and the resulting autosave is byt
game processes, with and without an intervening manual save, and also identical to the autosave the
*original* (non-loaded, freshly created) game wrote in the earlier session.
> **SCOPED BY LANE L1, 2026-09-08 — the oracle is a property of THIS PAIR, not of End Turns.**
> (`findings/control-flow/hive-creation-rng.md` §6.1.) Every run below starts from the **turn-2**
> state. Lane L1 ran the same experiment one turn earlier — load `turn1-state.sav`
> (`a3f9dc4b…`, which is this game's turn-1 pre-turn autosave), one End Turn — three times, and got
> **three different post-turn autosaves**: `ab4ac2d7…` (the original live session), `62061918…` (full
> hook set) and `e43ec1d2…` (**`hooks=off`, nothing installed at all**). The whole difference is one
> field: player 3's research target `ResTNm`, which reads `BIO_GnMod` / `XNC_TrnsLir2` /
> `XNC_TrnsMorr2` across the three. Players 0–2 pick identically every time, and in the instrumented
> run the strategic generator's trajectory matched the historical game word for word.
>
> So: **the turn-1 → turn-2 transition of this game is not deterministic across processes, and no
> hook is responsible.** Nothing below is wrong — but "load the same save, press End Turn, and the
> autosave is byte-identical" is verified for `ref-turn2 → turn 3` and **falsified for
> `turn1-state → turn 2`**. Anyone building a byte oracle must pick the pair deliberately, and the
> pairs in this document plus `verify/results/shim/tailrng/` are the ones that hold.
## Verdict
**byte-identical.** No masking is needed for the End Turn oracle when both sides start from the same

View file

@ -218,6 +218,18 @@ with the entire tech tree.
first is false. Both reduce to the same one-line model, so nothing downstream is blocked, but the
claim "the sensor branch is what refreshes Spica" is **[H]**, not [V]: no measurement of `SnLv`
exists. One arming line (`sys+0x24` in slot 1 instead of `_Mysize`) settles it on the next VM run.
> **SETTLED FOR SPICA BY LANE L1, 2026-09-08** (`findings/subsystems/snlv-measured.md`). The
> arming line was added (`watch.mode=snlv`) and run. Spica reads `AFlags = 0x0` with
> **`SnLv = 0x00000200`**, i.e. contact level **2** for player index 4 — the player its `NVO`
> record is keyed to. The `AFlags` term is false, so the sensor term is the only way `IsKnownTo`
> can be true: **"the sensor branch is what refreshes Spica" is now [V]**. Two further facts came
> free: level 3 occurs on exactly the eight (system, player) pairs whose `AFlags` bit is set, and
> level **2** — never 1 — occurs where it is not, which corrects
> `SensorPhase_ComputeContactLevel`'s prototype (it has four return tails, `{0,1}`, `{2,3}`, a
> hard 2 and a hard 3; the `else 1` reading names only one of them). **Bismol is still open** —
> it is not on this map. The same config on `zuul-turn23-fleet23.sav` answers it in one End Turn,
> because the arming sweep prints every system's `SnLv`.
- **The 54 "sensor branch" rows are unobserved in one conjunct each.** They are consistent, not
proven. The honest statement of the corpus result is: *no record contradicts the model, and the
one record that contradicted `AFlags` alone is explained.*

View file

@ -0,0 +1,141 @@
# `SnLv` measured — the sensor branch is what refreshes Spica, and the contact level is 2, not 1
- **Type:** subsystem (live measurement)
- **Status:** **verified** for Spica; `nvo-tshn-visible-owner.md` §6's first bullet moves **[H] → [V]**
- **Confidence:** high. Hardware data-write watchpoint on the word itself, with the canary self-test
passing and the determinism oracle reproduced byte for byte in the same run.
- **Owner / date:** lane L1 · 2026-09-08 · VM140
- **Instrument:** `src/shim/hooks/watchpoints.cpp`, new `watch.mode=snlv`, config `shim.cfg.l1snlv`,
build `0117495-dirty-20260908T2113Z`
- **Answers:** `findings/subsystems/nvo-tshn-visible-owner.md` §6 — *"One arming line
(`sys+0x24` in slot 1 instead of `_Mysize`) settles it on the next VM run."* It did.
---
## 0. The result
`ServerSystem::IsKnownTo` is `AFlags bit || (PlyrIdx < 15 && ((SnLv >> 2*PlyrIdx) & 3) != 0 &&
HasResearched(CCC_AdvSens))`. Lane W3 could not decide whether Spica's `NVO` record is refreshed
through the `AFlags` term or the sensor term, because **`SnLv` is not serialised** and had never been
measured.
On `ref-turn2.sav`, at the arming point, **Spica has `AFlags = 0x0` and `SnLv = 0x00000200`**, i.e.
contact level **2** for player index 4 — the very player its single `NVO` record is keyed to.
`AFlags = 0` makes the first term false. The sensor term is therefore the only way `IsKnownTo` can
return true for that pair, and the record is refreshed. **The sensor branch is what refreshes Spica**,
measured rather than inferred.
## 1. The whole map, because it costs no debug register
Printing `SnLv` and its decoded per-player 2-bit fields for every system during the arming sweep is
free. All 28 systems of `ref-turn2.sav`, at `ApplyAllTurnCommands` entry on the End Turn from turn 2
(only the non-zero rows shown; the other 20 systems read `SnLv = 0`):
| system | `AFlags` | `SnLv` | non-zero levels |
|---|---|---|---|
| Hyperion | 0x10 | 0x00000300 | player 4 → **3** |
| Gamma Cephei | 0x01 | 0x00000003 | player 0 → **3** |
| **Spica** | **0x00** | **0x00000200** | **player 4 → 2** |
| Ke'Dolarra | 0x02 | 0x0000000c | player 1 → **3** |
| Koa?Vo | 0x80 | 0x0000c000 | player 7 → **3** |
| Kaa?Vaalu | 0x10 | 0x00000300 | player 4 → **3** |
| Markab | 0x10 | 0x00000300 | player 4 → **3** |
| Kea?Pono | 0x10 | 0x00008300 | player 4 → **3**, player 7 → **2** |
| Ko'Rorkor | 0x10 | 0x00000300 | player 4 → **3** |
Two regularities fall out and neither was available from any save:
1. **Level 3 occurs exactly where the `AFlags` bit is set**, on all eight rows — which is the
`IsVisibleTo` early-out of `SensorPhase_ComputeContactLevel` doing what lane W3 read.
2. **Level 2 occurs exactly where it is not** — Spica for player 4, Kea'Pono for player 7. So the
non-visible contact state on this save is **2**, and **level 1 was never observed**.
## 2. A correction: `ComputeContactLevel` does not return 1 there
`ghidra/addresses.d/lane-w3.json` records
`SensorPhase_ComputeContactLevel` as *"3 if IsVisibleTo (AFlags), else 1 if inside p's sensor
envelope, else 0"*. The function has **four** return tails, read at instruction boundaries:
```
00850d18 mov eax,3 ; IsVisibleTo -> 3
00850e2f mov eax,ecx ; ecx = setne on an FPU range compare -> 0 or 1
00850e47 lea eax,[edx+2] ; edx = setne on the same compare -> 2 or 3
00850e52 mov eax,2 ; -> 2
```
which pair is taken is selected by `cl` and by `[ebx+0x10]`. So the value set is `{0,1,2,3}` and the
`{0,1}` pair is one of three tails, not the only alternative to 3. The measurement above lands on a
`2` tail. **`IsKnownTo` only tests `!= 0`, so nothing downstream changes** — but the constant in the
prototype is wrong and would mislead anyone reimplementing the sensor phase.
## 3. Who writes it, and how often
DR1 watched Spica's `sys+0x24` for the whole End Turn: **20 writes**, every one from the same
instruction.
| trap EIP (Ghidra VA) | store site | value written | count |
|---|---|---|---|
| 0x0086a78c | **0x0086a789** `mov [edi+0x24],edx` | 0x00000200 every time | **20** |
0x0086a789 is exactly the pack site lane W3 named from static reading — the trap reports the
instruction after the store, and 0x0086a789 + 3 = 0x0086a78c. Confirmed to the byte. The value never
changed across the turn: the recompute is idempotent on this workload, which is why the *absence* of
`SnLv` from the wire costs nothing on these saves and why no save could ever have shown it.
The enclosing loop at 0x0086a761..0x0086a79b walks `GetGame()->Systems` and calls
`ComputeContactLevel` once per system, masking `3 << (2*idx)` out and OR-ing the result in, guarded
by `[ebp-0x10] >= 0xf` — the `PlyrIdx < 15` test lane W3 read in `IsKnownTo`, here on the writing
side. Twenty full sweeps of the systems vector happen in one End Turn.
## 4. `TShn` refreshed, in the same run, twice
DR0 watched Spica's `NVO` record at `root+0x10` (`{int16 touched, int16 TShn}`): **4 writes**, in two
pairs, all with a `SetVisibleOwner` frame:
| seq | trap EIP (VA) | word after the store | reading |
|---|---|---|---|
| 8 | 0x0075b95a | `0x00020003` | `touched` := 3 (the new `Frame`), `TShn` still 2 |
| 9 | 0x0075b965 | `0x00030003` | `TShn` := 3 |
| 20 | 0x0075b95a | `0x00030003` | same pair again |
| 21 | 0x0075b965 | `0x00030003` | |
Return addresses 0x0075bd4a / 0x0075bdc3 with 0x0086a806 on the stack put the caller chain in the
sensor phase, next to the `SnLv` writer. This confirms three things lane W3 stated as a model:
`SetVisibleOwner` stamps `touched` and `TShn` in **two separate stores**; it runs **twice per turn**
for one (system, player) pair; and on this turn both are stamped with the new `Frame`.
## 5. Coverage — what this does NOT settle (rule 15)
* **Bismol is not on this map.** `nvo-tshn-visible-owner.md` §6's open question — *which* conjunct
fails at Bismol — belongs to `zuul-turn23-fleet23.sav`, a different game with a different star
list. This run settles the Spica half of that bullet and leaves the Bismol half open. The same
config on the Zuul save answers it: the arming sweep prints every system's `SnLv`, so one launch
and one End Turn is the whole experiment.
* **One system watched, 28 printed.** The 20 `SnLv` writes and the 4 `TShn` writes are Spica's. Every
other system's row above is a **read at arm time**, not a watched write.
* **`SnLv` never changed value** during the turn, so nothing here says what a *transition* looks like
or when a contact is lost.
* **Level 1 was never observed.** §2's four-tail reading is static; only 0, 2 and 3 have been seen.
* **Per-thread blind spot.** Debug registers are per-thread and the arm is on the turn thread only.
* **`ShareVisibleOwner` still has never executed** — no corpus save has an alliance (unchanged).
## 6. Instrument neutrality (rule 19)
The armed run **is** the oracle run: `ref-turn2.sav`, one End Turn, four watchpoints live and ~24
exception deliveries, plus the five lane-L1 hooks and lane Z's ledger:
| file | size | sha256 prefix | historical |
|---|---|---|---|
| `(Autosave EndTurn).sav` | 66,732 | `bb4fd9ac89f41e3b` | **identical** |
| `(Autosave).sav` | 67,219 | `978041acd168b56e` | **identical** |
Canary self-test: `writes=1 traps=1 dr7=0xdddd0055 PASS`.
## 7. Files
- Arming log (all 28 systems with `SnLv` decoded, the base probe, the canary):
`verify/results/shim/l1/watch-arm.txt`
- Hit log: `verify/results/shim/l1/watch-hits.txt`
- Instrument: `sots-engine` `wip/l1`, `src/shim/hooks/watchpoints.cpp` (`watch.mode=snlv`),
configs `shim.cfg.l1snlv` / `shim.cfg.l1control`

View file

@ -0,0 +1,84 @@
{
"entries": [
{
"name": "SVSOSwarmQueen_RegisterHives_DrawSite",
"addr": "0x0052770f",
"convention": "site",
"prototype": "call RNG_NextInt (0x004271c0) inside SVSOSwarmQueen_RegisterHives, ECX = GetGame()->RNG(+0x16c) + 4, bound = HI - LO passed BY POINTER at [ebp-0x14]; return address 0x00527714, then `add eax,edi` where edi = frame + LO. ONE STRATEGIC-GENERATOR WORD PER NEW HIVE, taken inside StrategyServer::BeginProcessTurn -- outside BOTH turn drivers and before either. Corrects svsctob-writers.md, which put the call at 0x0052770c (that is the `mov [ebp-0x14],esi` storing the bound)",
"status": "verified",
"source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08); disassembled from dumps/sots.exe and measured live on turn1-state.sav"
},
{
"name": "SVSOSwarmQueen_TickHives_SlipSite",
"addr": "0x0052785a",
"convention": "site",
"prototype": "`inc DWORD PTR [esi+0x8]` -- the NextQ slip. FOUR gate-failure edges converge here: 0x005277e3 (frame <= *SwarmQueen_GateFrameFloor_ptr), 0x005277fe (queens count >= the per-turn allowance), 0x00527848 (age < *SwarmQueen_GateQueenAge_ptr), 0x0052784f (hive's system handle is 0). Consumes NO generator word on any of them; TickHives calls no RNG primitive anywhere in its body (0x00527770..0x005279a1 read whole)",
"status": "verified",
"source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)"
},
{
"name": "SwarmQueen_HiveNextQOffsetLo_ptr",
"addr": "0x00ae0204",
"convention": "data",
"prototype": "int* -- pointer to the LOW end of the new-hive queen-countdown offset. RegisterHives computes nextQ = frame + *this + RNG_NextInt(*SwarmQueen_HiveNextQOffsetHi_ptr - *this). Behind a pointer, so no .text or .data reference initialises it in a form a static reader can follow; the value is read live by the shim at RegisterHives entry",
"status": "mapped",
"source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)"
},
{
"name": "SwarmQueen_HiveNextQOffsetHi_ptr",
"addr": "0x00ae0208",
"convention": "data",
"prototype": "int* -- pointer to the HIGH end of the new-hive queen-countdown offset. The RNG_NextInt bound is (*this - *SwarmQueen_HiveNextQOffsetLo_ptr) and NextInt is INCLUSIVE of its bound, so the countdown lands in [frame+LO, frame+HI]",
"status": "mapped",
"source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)"
},
{
"name": "SwarmQueen_GateFrameFloor_ptr",
"addr": "0x00ae0210",
"convention": "data",
"prototype": "int* -- TickHives gate 1: `if (GetGame()->Frame(+0xc) <= *this) slip`. No queen can spawn at or below this frame",
"status": "mapped",
"source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)"
},
{
"name": "SwarmQueen_SpawnListCap_ptr",
"addr": "0x00ae0220",
"convention": "data",
"prototype": "int* -- read in TickHives' SPAWN arm (0x005278d8), after a queen has been created: when *this <= 0 the vector at queen+0x30 is trimmed to 0 entries, otherwise to *this. Never exercised on any corpus save (no hive has ever had a queen)",
"status": "mapped",
"source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)"
},
{
"name": "SwarmQueen_GateQueenAge_ptr",
"addr": "0x00ae0228",
"convention": "data",
"prototype": "int* -- TickHives gate 3: an age in turns, compared against `GetGame()->Frame - resolvedQueen->+0xc`; below it the hive slips instead of spawning",
"status": "mapped",
"source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)"
},
{
"name": "SVSOSwarmQueen_off_Hives",
"offset": "0x10",
"convention": "field",
"prototype": "std::vector<HiveInfo> -- {_Myfirst 0x10, _Mylast 0x14, _Myend 0x18, _Alval 0x1c}, element stride 0x10: {void* vptr = 0x009f1a68, StarSystem* sys(+4), int nextQ(+8), Queen* queen(+0xc)}. The WIRE order is HiveID / QueenID / NextQ, which is NOT the member order",
"status": "verified",
"source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)"
},
{
"name": "SVSOSwarmQueen_off_Queens",
"offset": "0x20",
"convention": "field",
"prototype": "std::vector<T> at +0x20..+0x24, element stride 0xc -- TickHives' second gate divides the byte span by 12. Empty on every corpus save",
"status": "verified",
"source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)"
},
{
"name": "SVSOSlaversRefuel_off_CDiff",
"offset": "0x38",
"convention": "field",
"prototype": "int -- the difficulty tier, wire tag \"CDiff\". Set to -1 by the ctor 0x0051a820 and written ONLY by SVSOSlaversRefuel_UpdateDifficultyTier, which stores (scanIndex - 1) and only when it differs. Reachable values are 0 (frame 1..49) and 1 (frame 50..99) ONLY: frame <= 0 exits at index 0 and frame >= 100 walks off the end of the 3-entry threshold table, so BOTH edges store nothing and the tier can never reach 2",
"status": "verified",
"source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08); threshold table read off the stores at 0x00515868..0x0051588c"
}
]
}

View file

@ -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 @ 2e935b8, generated 2026-09-08 by tools/gen_addresses.py
// Source: sots-re ghidra/addresses.json @ ea7881a, generated 2026-09-08 by tools/gen_addresses.py
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
#pragma once
#include <cstdint>
@ -1913,6 +1913,50 @@ constexpr uint32_t StrategyServer_FinalizeTurnRecords = 0x0038a0e0;
constexpr uint32_t ServerSystem_ComputeMaxIncome = 0x003521c0;
// note Game::SNMAllCombatDone RTTI vtable, four slots (0x0079e590, 0x0082a100, 0x0082a170, 0x0079e500 -- the middle pair are the network Read/Write). Layout by enumeration from the two stack constructors and from every offset OnAllCombatDone_Tail reads: `struct SNMAllCombatDone { void* vptr; std::vector<EncounterResults> results; }`, 0x10 bytes -- which is why the handler passes msg+4 and not msg. Three construction sites: RunCombatRound 0x007cc847 (stack), the combat server FUN_007cfd00+0x541 = 0x007d0241 (stack; sends it to every player whose +0x44 is 4 or 5, then sets combatServer->+0x60 = 9; NOTE Ghidra sizes FUN_007cfd00 at 384 B but its real body runs to the ret at 0x007d02b9), and the deserialization factory 0x008663b0 (operator new(0x14) -- 4 bytes larger than the enumerated size, UNEXPLAINED) [verified]
constexpr uint32_t SNMAllCombatDone_layout = 0x00624758;
// site call RNG_NextInt (0x004271c0) inside SVSOSwarmQueen_RegisterHives, ECX = GetGame()->RNG(+0x16c) + 4, bound = HI - LO passed BY POINTER at [ebp-0x14]; return address 0x00527714, then `add eax,edi` where edi = frame + LO. ONE STRATEGIC-GENERATOR WORD PER NEW HIVE, taken inside StrategyServer::BeginProcessTurn -- outside BOTH turn drivers and before either. Corrects svsctob-writers.md, which put the call at 0x0052770c (that is the `mov [ebp-0x14],esi` storing the bound) [verified]
constexpr uint32_t SVSOSwarmQueen_RegisterHives_DrawSite = 0x0012770f;
// site `inc DWORD PTR [esi+0x8]` -- the NextQ slip. FOUR gate-failure edges converge here: 0x005277e3 (frame <= *SwarmQueen_GateFrameFloor_ptr), 0x005277fe (queens count >= the per-turn allowance), 0x00527848 (age < *SwarmQueen_GateQueenAge_ptr), 0x0052784f (hive's system handle is 0). Consumes NO generator word on any of them; TickHives calls no RNG primitive anywhere in its body (0x00527770..0x005279a1 read whole) [verified]
constexpr uint32_t SVSOSwarmQueen_TickHives_SlipSite = 0x0012785a;
// data int* -- pointer to the LOW end of the new-hive queen-countdown offset. RegisterHives computes nextQ = frame + *this + RNG_NextInt(*SwarmQueen_HiveNextQOffsetHi_ptr - *this). Behind a pointer, so no .text or .data reference initialises it in a form a static reader can follow; the value is read live by the shim at RegisterHives entry [mapped]
constexpr uint32_t SwarmQueen_HiveNextQOffsetLo_ptr = 0x006e0204;
// data int* -- pointer to the HIGH end of the new-hive queen-countdown offset. The RNG_NextInt bound is (*this - *SwarmQueen_HiveNextQOffsetLo_ptr) and NextInt is INCLUSIVE of its bound, so the countdown lands in [frame+LO, frame+HI] [mapped]
constexpr uint32_t SwarmQueen_HiveNextQOffsetHi_ptr = 0x006e0208;
// data int* -- TickHives gate 1: `if (GetGame()->Frame(+0xc) <= *this) slip`. No queen can spawn at or below this frame [mapped]
constexpr uint32_t SwarmQueen_GateFrameFloor_ptr = 0x006e0210;
// data int* -- read in TickHives' SPAWN arm (0x005278d8), after a queen has been created: when *this <= 0 the vector at queen+0x30 is trimmed to 0 entries, otherwise to *this. Never exercised on any corpus save (no hive has ever had a queen) [mapped]
constexpr uint32_t SwarmQueen_SpawnListCap_ptr = 0x006e0220;
// data int* -- TickHives gate 3: an age in turns, compared against `GetGame()->Frame - resolvedQueen->+0xc`; below it the hive slips instead of spawning [mapped]
constexpr uint32_t SwarmQueen_GateQueenAge_ptr = 0x006e0228;
// field std::vector<HiveInfo> -- {_Myfirst 0x10, _Mylast 0x14, _Myend 0x18, _Alval 0x1c}, element stride 0x10: {void* vptr = 0x009f1a68, StarSystem* sys(+4), int nextQ(+8), Queen* queen(+0xc)}. The WIRE order is HiveID / QueenID / NextQ, which is NOT the member order [verified]
constexpr uint32_t SVSOSwarmQueen_off_Hives = 0x00000010;
// field std::vector<T> at +0x20..+0x24, element stride 0xc -- TickHives' second gate divides the byte span by 12. Empty on every corpus save [verified]
constexpr uint32_t SVSOSwarmQueen_off_Queens = 0x00000020;
// field int -- the difficulty tier, wire tag "CDiff". Set to -1 by the ctor 0x0051a820 and written ONLY by SVSOSlaversRefuel_UpdateDifficultyTier, which stores (scanIndex - 1) and only when it differs. Reachable values are 0 (frame 1..49) and 1 (frame 50..99) ONLY: frame <= 0 exits at index 0 and frame >= 100 walks off the end of the 3-entry threshold table, so BOTH edges store nothing and the tier can never reach 2 [verified]
constexpr uint32_t SVSOSlaversRefuel_off_CDiff = 0x00000038;
// thiscall void __thiscall Game::AITColonize::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITColonizeGoal::Execute is the same address. 48 bytes; a forwarder to the parameterised worker 0x0068b280 called as worker(ecx = agent, this, pass, this->+0x8, &this->+0x20, &this->+0x10) with `edi = this->+0xc` passed as an IMPLICIT REGISTER ARGUMENT (whole-program-optimised custom convention; a reimplementation that ports only the stack arguments passes garbage). Reaches the colonize order (list 7) at depth 4 via 0x0068b280 -> 0x006930f0 -> 0x00578ff0 -> 0x00769640 [unverified]
constexpr uint32_t AITColonize_Execute = 0x0028b400;
// thiscall void __thiscall Game::AITEscortGateInvade::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITEscortGateInvadeGoal::Execute is the same address. 80 bytes; forwards to 0x0068c5d0 as __fastcall(ecx = this->+0xc, edx = agent) plus 8 stack arguments. Reaches list 7 at depth 4 [unverified]
constexpr uint32_t AITEscortGateInvade_Execute = 0x0028c7c0;
// thiscall void __thiscall Game::AITInvade::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITInvadeGoal::Execute is the same address. 160 bytes; forwards to 0x0068d460 with 11 arguments, then maintains this->+0x38 and this->+0x39. +0x39 is computed at 0x0068d80f-0x0068d82a as (0x006a6380(agent, this->+0xc) < 2 * 0x006a6260(agent, this->+0xc)) and is what IAITask slot 11 returns for this class. Reaches list 14 at depth 5 [unverified]
constexpr uint32_t AITInvade_Execute = 0x0028d7a0;
// thiscall void __thiscall Game::AITNodeBore::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Does its setup (0x00685810) and its finaliser (0x0068e090) only when pass == 0, and forwards `pass` to 0x0068a520 in both passes. Reaches list 14 at depth 5 via 0x0068a520 -> 0x006ceef0 -> 0x006c16c0 -> 0x006987e0 -> 0x007634d0. Zuul-flavoured task (the node-bore is a Zuul mechanic), so on a non-Zuul board it may be absent from the task list entirely [unverified]
constexpr uint32_t AITNodeBore_Execute = 0x0028e590;
// thiscall void __thiscall Game::AITRaid::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. THE ONE TASK WHOSE PASS-0 BEHAVIOUR IS NOT SETTLED: at 0x0068e89e it calls 0x006b76a0 and then at 0x0068e8b8 loops the list-16 order method 0x007635f0(client, fleetId, 1) over the returned fleets, and NEITHER the call nor the loop carries a `pass` guard of its own. AI3 §2.3's argument says the fleet vector at [ebp-0x28] is empty on pass 0 because it is downstream of the same hub, but [ebp-0x28] was never traced to closure. This address exists as an address so that a zero at 0x007635f0 can be read: entered-and-silent and never-entered are opposite answers (method rule 20) [unverified]
constexpr uint32_t AITRaid_Execute = 0x0028e670;
// thiscall void __thiscall Game::AITAdvanceIdleShips::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Whole body is pass-1 only: `if (pass != 1) return` at 0x0068f25a. Table priority 0, so it is always the LAST task in the descending-priority sweep -- the AI sweeps up whatever is still idle only after every other task has taken both its minimum and its desired force, which is what the two-tier quota model predicts [unverified]
constexpr uint32_t AITAdvanceIdleShips_Execute = 0x0028f230;
// thiscall void __thiscall Game::AITBuildDeepScanShips::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Reaches the BUILD order (list 3) at depth 6 via 0x006ce460 -> 0x006ce360 -> 0x006ce190 -> 0x006bd790 -> 0x006b3bc0 -> 0x00762fd0, the same chain as AITBuildPoliceShips [unverified]
constexpr uint32_t AITBuildDeepScanShips_Execute = 0x002901a0;
// thiscall void __thiscall Game::AITBuildPoliceShips::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Reaches the BUILD order (list 3) at depth 6, identical chain to AITBuildDeepScanShips. One of the two candidate producers of the single list-3 element the reference AI emits on turn 1 [unverified]
constexpr uint32_t AITBuildPoliceShips_Execute = 0x00290380;
// thiscall bool __thiscall Game::StrategyClient::OrderList16(int objectId, bool flag) -- RET 8. THE LIST-16 PRODUCER. Opens with `cmp byte [this+0x15c], 0` -- the End-Turn latch StrategyClient::EndTurn 0x00783be0 sets at 0x00783c59 before BuildTurnCommands runs -- so like every other order method it refuses once the turn has been submitted. Element record for list 16 is {i32, bool} (lane Q), matching the (objectId, flag) pair; list 16 is in the PAYING half of the ModCount table (bump at 0x00821e23 in its applier 0x00821e20). Reached from AITRaid::Execute at depth 1. The method is named for the list it fills, NOT for what the flag means: no save has ever carried a list-16 element and nothing has been observed [unverified]
constexpr uint32_t StrategyClient_OrderList16 = 0x003635f0;
// offset int aidf -- the difficulty level (0..2) stored by ServerPlayer::Read 0x008804d0 at 0x00880fa3, the selector LoadDifficultyRow 0x005a3990 uses to pick the row it copies for this player. VERIFIED LIVE on VM146: read at ComputeBudget entry on every one of the 8 players of turn1-state across 3,895 calls, it is 1 on all eight, which is the difficulty level the whole corpus carries and the level whose AI column holds the 1.1x income modifier [verified]
constexpr uint32_t ServerPlayer_off_Aidf = 0x00000368;
// offset DifficultyMods* -- a POINTER to the per-player difficulty record, NOT the record inline. This entry exists to correct a lane L5 mistake and to save the next lane the run. addresses.json already records DifficultyMods_Select 0x0059b490 being called as Select(p->diffMods /*+0x36c*/, p), and lane L5 read the 0x1c bytes AT +0x36c expecting {int id; float ai[3]; float other[3]}. They are not that: the first dword is 0x0da0ec20..0x0da13360 on the eight players of turn1-state -- eight distinct heap addresses within 18 KB of each other and of the ServerPlayer allocations themselves (the player object was at 0x0da08600) -- and the floats behind it decode as denormal garbage (1.7e-38, 1.4e-43). So the field is a pointer and the record is one dereference further on. HYPOTHESIS, not verified: this run did not follow the pointer, so it is consistent with +0x36c being a pointer to the DifficultyMods record and equally consistent with its being some other per-player sub-object pointer. What IS established is the thing the reading was for: the difficulty row is reachable from a ServerPlayer, so compute_budget.h's old coverage note ('not reachable from a ServerPlayer, so the two relevant entries are fitted constants') is wrong as written. The fitted pair {3.0, 1.5} / {1.0, 1.0} remains UNVERIFIED against the running game; the next run should dereference this field behind a readability guard and compare the selected triple against those constants [hypothesis]
constexpr uint32_t ServerPlayer_off_DiffModsPtr = 0x0000036c;
// data double 0x3FEB333340000000 = 0.85000002384185791 = (double)0.85f -- the research-yield factor, multiplied in by ComputeBudget at 0x00863601 as a QWORD operand. THE WIDTH IS THE FINDING, and what makes it a rule-23 case rather than a curiosity is its two NEIGHBOURS in the same product: 1.15 at 0x00a1a4b8 and 0.5 at 0x009e20a0 are stored as EXACT doubles, while this one is a widened float. Three literals, one expression, two widths -- so a C++ port that writes all three as decimals is wrong in exactly one place. The exact-decimal product (money/50 x 1.15 x 0.5 x 0.85) is integral when researchMoney is a multiple of 40,000, and the image's larger constant lands one ABOVE it there: at 40,000 the game gives 391 research points and the decimal gives 390. NOT VISIBLE ON THE CORPUS, and that is now measured rather than argued: the live compare run presented 9 distinct researchMoney values across 3,895 calls and NONE of them is a multiple of 40,000. Read live out of the running process as well, so nothing patched or relocated it [verified]
constexpr uint32_t g_ResearchYieldFactor = 0x0062d818;
// thiscall double (ServerSystem* sys, double overHarvestRate) // `ret 8`, real end 0x007505a5. Returns 0 when the system has no owner (+0x100) or rbfl (+0x1dc) is non-zero. Otherwise the SUM of three terms -- the over-harvest demand x SpeciesDef+0x50, (TRes + available resources) x StripMineFraction x 0.9, and the population output of groups 0, 1 and 2 -- multiplied in one uninterrupted 80-bit chain by player OutMod, sys.OutMod, player +0x224, RebOutMod, ScOutMod, and finally by ADDICTION_OUTPUT_MOD when the addiction phase is >= 3. SIDE-EFFECT FREE: it and all seven callees were checked for stores to the game state (0x0074a6d0's only writes are through its int64 out-parameters), which is why this and not ComputeOutputFromRates is the compare target -- that one repairs ships in orbit [verified]
constexpr uint32_t ServerSystem_ComputeTotalOutput = 0x00350480;
// thiscall double (ServerSystem* sys, int groupType, int species, double count) // `ret 0x10`, real end 0x0074b871. THE population -> output term: returns 0 for count <= 0, else max(0, POPTYPE[groupType].outputMod x (stationFactor x 1.8) x moraleMod x (count / 500000)). stationFactor is 1 + stations x STATION_BONUS_IMPERIAL_OUTPUT and applies to groupType 0 of an owned system only (and only while that constant is > 0); moraleMod applies to groupType 1 only. So output points per head are typeOutputMod x 1.8 / 500000 -- exactly 3.6e-6 for an imperial population with no stations [verified]

View file

@ -0,0 +1,16 @@
# Lane L1 — the turn-begin RNG run, 2026-09-08
Instrument: `sots-engine` `wip/l1`, build `0117495-dirty-20260908T2113Z`. Findings:
`findings/control-flow/hive-creation-rng.md` and `findings/subsystems/snlv-measured.md`.
| file | what |
|---|---|
| `trace-et1.jsonl` | `l1-turn1.sav` (= `verify/results/saves/turn1-state.sav`) + **one** End Turn — the hive-creation turn. `SVSOSwarmQueen::RegisterHives` words 170 → 172. |
| `trace-et2.jsonl` | the same session after a **second** End Turn; the second bracket is 192 → 211 with a residual of 0. Supersedes `trace-et1.jsonl`, which is kept because it is the record as it stood before the control turn existed. |
| `trace-oracle.jsonl` | `ref-turn2.sav` + one End Turn with the same build and config. This run reproduced the determinism oracle byte for byte (`bb4fd9ac…` / `978041ac…`), so it is the rule-19 control for everything in the other two. |
| `watch-arm.txt` | the `watch.mode=snlv` arming sweep: all 28 systems with `AFlags`, `SnLv` and the decoded per-player 2-bit contact levels, plus the canary self-test. |
| `watch-hits.txt` | 24 hardware watchpoint hits — 20 writes to Spica's `SnLv` (all from 0x0086a789) and 4 to its `NVO` record's `{touched, TShn}` pair. |
| `saves/l1-t1t2-post.sav` | post-turn autosave of the hive-creation turn, **full L1 hook set**. |
| `saves/l1-t1t2-post-hooksoff.sav` | the same turn with **`hooks=off`, nothing installed**. It differs from the line above AND from the historical `turn2-state.sav`, in one field: player 3's `ResTNm`. See the finding's §6.1 — this pair is why `turn1-state.sav` must not be used as a determinism oracle. |
| `saves/l1-t2t3-pre.sav`, `saves/l1-t2t3-post.sav` | the second bracket's two autosaves. |
| `rep.py` | the reader used for every table in the finding: prints each record's hook, depth, `rng` word delta and non-pointer arguments. `uv run python3 rep.py trace-et2.jsonl`. |

View file

@ -0,0 +1,36 @@
import json, sys
SKIP = {'rng','rng_words_in','rng_left_in','server','host','out_name','results',
'queen','slavers','server_cached','host_plus_0x54','sites','thresholds'}
def val(x):
if isinstance(x, dict) and 't' in x:
v = x.get('v')
if x['t'] == 'struct' and isinstance(v, dict):
return {k: val(w) for k, w in v.items()}
if x['t'] == 'list' and isinstance(v, list):
return [val(w) for w in v]
return v
return x
for path in sys.argv[1:]:
print("="*72); print(path)
for ln in open(path):
r = json.loads(ln)
if 'hook' not in r:
print("meta:", r.get('build_id') or list(r)[:6]); continue
hook = r['hook']; depth = r.get('depth')
side = r.get('side') or {}
rng = side.get('rng') or {}
b = val(rng.get('before')) or {}; a = val(rng.get('after')) or {}
bw, aw = b.get('words'), a.get('words')
d = (aw - bw) if isinstance(aw, int) and isinstance(bw, int) else None
print(f"[d{depth}] {hook:52s} words {bw} -> {aw} DELTA {d}")
for arg in (r.get('args') or []):
n = arg.get('n')
if n in SKIP: continue
print(f" {n} = {json.dumps(val(arg))}")
for nm, s in side.items():
if nm == 'rng': continue
print(f" side[{nm}] {json.dumps(val(s.get('before')))} -> {json.dumps(val(s.get('after')))}")
if r.get('err'): print(" ERR", r['err'])

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,77 @@
17:31:12.313 [tid 3828] watch: VEH=01848448 text=[0x00e81000,0x0145ca00) out=C:\SOTS\shim.watch.txt
17:31:12.329 [tid 3828] watch: arm hook StrategyServer::ApplyAllTurnCommands rva=0x0038f6a0 va=0120f6a0 create=MH_OK
enable=MH_OK
17:34:46.960 [tid 3828] watch: systems vector @0x1f2b20ac first=0x0e5e46c8 last=0x0e5e4738 end=0x0e5e4738 count=28
(expect 28 on the reference game)
17:34:46.960 [tid 3828] watch: NVO header probe -- ServerSystem+0x274 validated on 9 systems, +0x26c on 0; using
+0x274 (pointer delta 0)
17:34:46.960 [tid 3828] watch: sys[0] @0x0e5bd0b0 'Caladan' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[1] @0x0e587d80 'Hyperion' AFlags=0x10 SnLv=0x00000300 lv[0..14]=0 0 0 0 3 0 0 0 0
0 0 0 0 0 0 NVO=1 NVE=1 root=0x0e5ca6d0 key=4 ok=1
17:34:46.960 [tid 3828] watch: sys[2] @0x0e588060 'Tully' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[3] @0x0e588340 'Foo' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[4] @0x0e588620 'Gamma Cephei' AFlags=0x1 SnLv=0x00000003 lv[0..14]=3 0 0 0 0 0 0 0
0 0 0 0 0 0 0 NVO=1 NVE=1 root=0x1f309038 key=0 ok=1
17:34:46.960 [tid 3828] watch: sys[5] @0x0e588900 'Biter' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[6] @0x0e588be0 'Flinx' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[7] @0x0e588ec0 'Procyon' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[8] @0x0e5891a0 'Kor?Voth' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[9] @0x0e589480 'Ke'Rassak' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[10] @0x0e589760 'Koa?Tuun' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[11] @0x0e5dc6c8 'Spica' AFlags=0x0 SnLv=0x00000200 lv[0..14]=0 0 0 0 2 0 0 0 0 0 0
0 0 0 0 NVO=1 NVE=0 root=0x1f309888 key=4 ok=1
17:34:46.960 [tid 3828] watch: sys[12] @0x0e5dc9a8 'Junction' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[13] @0x0e5dcc88 'Nemesis' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[14] @0x0e5dcf68 'Keu?Loko' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[15] @0x0e5dd248 'Ke'Dolarra' AFlags=0x2 SnLv=0x0000000c lv[0..14]=0 3 0 0 0 0 0 0
0 0 0 0 0 0 0 NVO=1 NVE=1 root=0x1f3090d0 key=1 ok=1
17:34:46.960 [tid 3828] watch: sys[16] @0x0e5dd528 'Koa?Vo' AFlags=0x80 SnLv=0x0000c000 lv[0..14]=0 0 0 0 0 0 0 3 0 0
0 0 0 0 0 NVO=1 NVE=1 root=0x1f3096c0 key=7 ok=1
17:34:46.960 [tid 3828] watch: sys[17] @0x1f2e7890 'Delphi' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[18] @0x1f2e8718 'Kaa?Vaalu' AFlags=0x10 SnLv=0x00000300 lv[0..14]=0 0 0 0 3 0 0 0
0 0 0 0 0 0 0 NVO=1 NVE=1 root=0x1f309330 key=4 ok=1
17:34:46.960 [tid 3828] watch: sys[19] @0x1f2e6fd8 'Mu Bootis' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[20] @0x1f2e7e60 'Kor?Hakaan' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[21] @0x1f2e72c0 'Serpens' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[22] @0x1f2e8148 'Markab' AFlags=0x10 SnLv=0x00000300 lv[0..14]=0 0 0 0 3 0 0 0 0 0
0 0 0 0 0 NVO=1 NVE=1 root=0x1f309920 key=4 ok=1
17:34:46.960 [tid 3828] watch: sys[23] @0x1f2e75a8 'Pascal' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[24] @0x1f2e7b78 'Kao?Lolto' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[25] @0x1f2e8430 'Kea?Pono' AFlags=0x10 SnLv=0x00008300 lv[0..14]=0 0 0 0 3 0 0 2 0
0 0 0 0 0 0 NVO=2 NVE=1 root=0x1f309168 key=4 ok=1
17:34:46.960 [tid 3828] watch: sys[26] @0x1f2e8a00 'Gienah' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0
17:34:46.960 [tid 3828] watch: sys[27] @0x1f2eea08 'Ko'Rorkor' AFlags=0x10 SnLv=0x00000300 lv[0..14]=0 0 0 0 3 0 0 0
0 0 0 0 0 0 0 NVO=1 NVE=1 root=0x1f309ae8 key=4 ok=1
17:34:46.960 [tid 3828] watch: trade routes -- manager=0x0e5c0818 (S+4+0x154) vector@mgr+0x3c first=0x00000000
last=0x00000000 end=0x00000000 count=0
17:34:46.960 [tid 3828] watch: spy programs -- manager=0x1f2c8218 (S+4+0x158) vector@mgr+0x10 first=0x00000000
last=0x00000000 end=0x00000000 count=0
17:34:46.960 [tid 3828] watch: SELFTEST canary writes=1 traps=1 dr7=0xdddd0055 PASS
17:34:46.960 [tid 3828] watch: slot 0 -> 'Spica' NVO root+0x10 {touched:i16,TShn:i16} key=player 4 = 0x1f309898
17:34:46.960 [tid 3828] watch: slot 1 -> 'Spica' SnLv (sys+0x24) = 0x00000200 at arm; key player 4 level 2 =
0x0e5dc6ec
17:34:46.960 [tid 3828] watch: slot 2 -> trade routes vector _Mylast (mgr+0x40, count 0 at arm) = 0x0e5c0858
17:34:46.960 [tid 3828] watch: slot 3 -> spy programs vector _Mylast (mgr+0x14, count 0 at arm) = 0x1f2c822c
17:34:46.960 [tid 3828] watch: ARMED on tid 3828 dr7=0xdddd0055, S=1f2b2068 (ApplyAllTurnCommands this)

View file

@ -0,0 +1,24 @@
watchhit seq=0 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x00fc5a84,0x014187a6,0x012ea916,0x014052f8
watchhit seq=1 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a
watchhit seq=2 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a
watchhit seq=3 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a
watchhit seq=4 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a
watchhit seq=5 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a
watchhit seq=6 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a
watchhit seq=7 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a
watchhit seq=8 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db95a value=131075(0x00020003) tid=3828 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806
watchhit seq=9 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db965 value=196611(0x00030003) tid=3828 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806
watchhit seq=10 mark=1 slot=1 dr6=0xffff0ff2 eip=0x0129e78b value=512(0x00000200) tid=3828 ebpret=0x0129e99e ebpret2=0x012ea927 scan=0x0129e99e,0x012ea927,0x014052f8,0x00fa677a
watchhit seq=11 mark=1 slot=1 dr6=0xffff0ff2 eip=0x0129e78b value=512(0x00000200) tid=3828 ebpret=0x0129e99e ebpret2=0x012ea927 scan=0x0129e99e,0x012ea927,0x014052f8,0x00fa677a
watchhit seq=12 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x00fc5a84,0x014187a6,0x012ea916,0x012e2696
watchhit seq=13 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73
watchhit seq=14 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73
watchhit seq=15 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73
watchhit seq=16 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73
watchhit seq=17 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73
watchhit seq=18 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73
watchhit seq=19 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73
watchhit seq=20 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db95a value=196611(0x00030003) tid=3828 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806
watchhit seq=21 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db965 value=196611(0x00030003) tid=3828 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806
watchhit seq=22 mark=1 slot=1 dr6=0xffff0ff2 eip=0x0129e78b value=512(0x00000200) tid=3828 ebpret=0x0129e99e ebpret2=0x012ea927 scan=0x0129e99e,0x012ea927,0x012e2696,0x0120ae73
watchhit seq=23 mark=1 slot=1 dr6=0xffff0ff2 eip=0x0129e78b value=512(0x00000200) tid=3828 ebpret=0x0129e99e ebpret2=0x012ea927 scan=0x0129e99e,0x012ea927,0x012e2696,0x0120ae73