sots-re/findings/subsystems/spy-program-draws.md
alex f8ff1b6fa9 AP: predictions for the spy program's remaining draws, committed before the build
P's three sites are two mutually exclusive branches on the spy owner's Species;
only 0x00840a3c is reachable from a Human empire. Species table decoded from the
corpus. Predicted trajectory for the 20-turn wait, including the counter-mission
roll 0x0088dc43 firing at Frame 19, and the deploy NextFloat 0x0078c97f.
2026-09-09 00:25:11 -04:00

301 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# The spy program's remaining draws — `P`'s branch split, the counter-mission roll, and the deploy's own `NextFloat`
- **Type:** subsystem (static decode + live measurement)
- **Owner / date:** lane AP · 2026-09-09 · guest **VM144** (`sots-re-win10-144`, `re@192.168.10.144`)
- **Instrument:** lane Z's `draw_sites` return-address ledger at **`probes=8`** (the byte-neutral set;
never `probes=11` — lane H bisected 11 as not byte-neutral)
- **Continues:** lane AS's `findings/subsystems/spy-detection-roll.md`, whose §6 named this workload
from its own failed conjunct; lane AG's `findings/control-flow/gate-indexed-rng-audit.md` §3.2
- **Inputs:** `verify/results/saves/as-turn15-spydeployed.sav` (target 1),
`verify/results/saves/as-turn14-predeploy.sav` (target 2)
*(Sections 0–3 were written and committed **before** the build and before any measurement, in their
own commit. Sections 4 onward are the measurement. Rule 2.)*
---
## 0. Headline of the predictions
> **The brief that created this lane asks for "`P`'s three draws". There are three sites but they are
> two mutually exclusive branches, and the branch selector is the spy owner's *species*.** From
> `as-turn15-spydeployed.sav` **exactly one** of the three can ever fire — `0x00840a3c` — because the
> spy's owner is player 0 (`re`), `Species = 0` (Human). `0x00840929` and `0x008409c7` are gated on
> `spyOwner->Species == 6`, i.e. **Morrigi**, and are unreachable from this save, from every save in
> the 22-save corpus, and from any workload that does not start a *Morrigi* empire.
>
> Predicted, before the run: `0x00840a3c` fires once, for one word, on the End Turn that produces
> `Frame == sdet + 3`; and two sites nobody expected fire on the way — the spy **counter-mission**
> roll `0x0088dc43` (lane AG §3.2, corpus 0 of 22, never fired) starting at **Frame 19**, and the
> deploy's own `NextFloat` `0x0078c97f` on a separate one-turn run.
---
## 1. `P` decoded — `SpyManager::Slot13RngCallee` `0x008408e0`, complete
Program `sots` / "Sword of the Stars.exe", ImageBase `0x00400000`, all addresses VAs. Body
disassembled from its start **to the next function start** (rule 17): `0x008408e0 .. 0x00840a5a`,
followed by `int3` padding. The ReVa MCP server is down (`CONNECTION_CLOSED`) for this lane too, so
this is instruction stream only.
### 1.1 How `P` is reached, and that there is no further gate
The chain in `ServerSpyManager::vslot13` `0x008877b0` from lane AS's §1, extended past the point AS
stopped at. **Between the last gate and the call to `P` there is no branch at all** — the whole span
`0x00887897 .. 0x00887af2` is two `EVENT_SPY_DESTROYED_OWNER`-shaped string builds, a `push_back`,
and an event post:
```
0088781d je 0x887e92 G1 spy.deat (+0x10) == 0 -> next spy
00887856 je 0x887c5b G2 spy.sdet (+0x40) == -1 -> BRANCH D (the detection roll)
00887867 jl 0x887e92 G3 (server.Frame - sdet) < 3 -> next spy
0088787a je 0x887c4f G4 ServerSystem_GetOwner(sysA) == NULL -> sdet := -1, next spy
00887891 je 0x887c4f G5 ServerSystem_GetOwner(sysA) == ownerO -> sdet := -1, next spy
008878a1 call 0x59f1a0 push_back spy.sid into a LOCAL vector<int> at [ebp-0xdc]
008879dd call 0x8862b0 PostEvent "EVENT_SPY_DESTROYED_OWNER"
00887ae3 call 0x7437e0 eax = ServerSystem_GetOwner(sysA)
00887aee push eax ; arg2 = the TARGET system's owner
00887aef push edx ; arg1 = ownerO, the SPY's owner ([ebp-0xc0])
00887af0 mov ecx,edi ; this = the spy manager
00887af2 call 0x8408e0 P <-- the false-flag draw
00887af7 ...
00887b07 cmp eax,ebx
00887b09 je 0x887b5b ; P returned NULL -> the "unknown empire" event string
```
so **once `sdet` is stamped and three turns have passed, `P` is called unconditionally** (given a
target system that is owned and not owned by the spy's own empire). `P`'s arguments are two
`Game::ServerPlayer*`, not ids — `P` compares them by pointer against entries of the player vector.
### 1.2 The species branch — and why "three draws" is one draw
```
00840909 mov esi,[ebp+8] ; esi = arg1 = the SPY'S OWNER (a ServerPlayer*)
0084090c cmp dword ptr [esi+0x5c], 0x6 ; +0x5c = ServerPlayer::Species
00840910 jne 0x840a29 ; NOT species 6 -> BRANCH B
; ---- BRANCH A, species == 6 only --------------------------------------------------------------
00840919 fld dword ptr ds:0x9e5ac4 ; 0.75f (bytes 00 00 40 3f)
0084091f mov ecx,[eax+0x16c] ; THE STRATEGIC GENERATOR (the object, not +4)
00840929 call 0x8e6dd0 A1 ; Mars::RNG::Chance(strategic, 0.75f) ret 0x0084092e
00840930 je 0x840a45 ; roll failed -> return NULL
; build a candidate list over StrategyServer +0x54..+0x58 (the player vector):
; skip p if p->+0xfb != 0, p->+0xf8 != 0, p == arg1, or p == arg2
008409ac je 0x840a07 ; list EMPTY -> free it, return NULL (no NextInt)
008409ae eax = count - 1
008409b0 mov ecx,[...+0x16c]; add ecx,4 ; the +4 SUB-OBJECT (the NextInt convention)
008409c7 call 0x4271c0 A2 ; Mars::RNG::NextInt(&(count-1)) ret 0x008409cc
008409cf esi = list[eax] ; the blamed empire
return esi
; ---- BRANCH B, every other species ------------------------------------------------------------
00840a2c fld dword ptr ds:0x9e5ac0 ; 0.25f (bytes 00 00 80 3e)
00840a32 mov ecx,[edx+0x16c] ; the strategic generator
00840a3c call 0x8e6dd0 B1 ; Mars::RNG::Chance(strategic, 0.25f) ret 0x00840a41
00840a43 jne 0x8409f2 ; roll SUCCEEDED -> return esi == arg1 (the true owner)
00840a45 xor eax,eax ; else return NULL
```
Both float constants were read as the **four bytes in the image** (rule 23): `0x009e5ac0` is
`00 00 80 3e` = `0.25f` and `0x009e5ac4` is `00 00 40 3f` = `0.75f`. `0x009e5ac4` is the *same* word
lane AS read as the species-5 multiplier in `SpyCraft_AccumulateDetectionOdds` — one constant, two
uses.
**So `P` is the attribution roll for a destroyed spy, and it has two completely different shapes:**
| spy owner | draw sites reached | semantics |
|---|---|---|
| `Species == 6` | `0x00840929` (`Chance` 0.75f), then `0x008409c7` (`NextInt`) **only if the roll succeeds and the candidate list is non-empty** | 75 % of the time a **random third empire** is blamed — a false flag. The true owner is *never* returned. |
| every other species | `0x00840a3c` (`Chance` 0.25f) | 25 % of the time the **true owner** is returned; otherwise nobody is blamed. |
### 1.3 The species table, decoded from the corpus
`Game::ServerPlayer +0x5c` is the field `objects/layouts.md` names **`Species`** (Write
`0x008563e0`). The `sim/species` node lists seven `ISsp` names in index order, identical in every
save read:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Human | Hiver | Tarkas | Liir | **_NPC** | Zuul | **Morrigi** |
Cross-checked against lane V's `zuul-turn5-species5.sav`, whose three real players all carry
`Species = 5` — and lane V's hook independently reported `species=5` on the Zuul double-roll. The
four NPC empires (`Alien Menace`, `Peacekeeper Enforcer`, `Von Neumann`, `Independent Colony`) carry
`Species = 4` in every save.
That also **names lane AS's two unexplained multipliers**: `SpyCraft_AccumulateDetectionOdds`
multiplies the per-turn detection odds by `0.75f` for **Zuul** and `0.5f` for **Morrigi**, and by
`1.0f` for everyone else. Morrigi are the stealth species on both sites — half the detection odds,
and a false flag when they are caught.
### 1.4 What happens to the spy on that turn
The local `vector<int>` filled at `0x008878a1` is drained after the spy loop:
```
00887eb0..00887ee8 for each collected sid, back to front:
[[manager]+0x28](sid) ; vtable slot 10 = 0x00838480
```
and `0x00838480` looks the `SpyCraft` up by `sid`, calls **`ServerSystem::RemoveSpy 0x0074f550`** on
the `deat` system (which clears `spy.deat` and erases `sid` from `spies2`), clears the tender's
`atto` back-pointer (`spy+0xc`, `ship+0xa8`), erases the craft from the manager's vector at `+0x10`,
and invokes the scalar deleting destructor. **The spy is gone at the end of `vslot13`**, which is
phase 23 — *before* `vslot14` runs at phase 33.
### 1.5 The deploy's `NextFloat` — `0x0078c97f`, re-read
`SHIPACTION_DEPLOYSPY` handler `0x0078c930`, body to the next function start `0x0078c9e0`. One gate,
then the draw, with no branch between them:
```
0078c93d..0078c964 build a 12-byte stack query object (vtable 0x00a09648) and ask it about the ship
0078c969 test al,al
0078c96b jne 0x78c975 ; FALSE -> return 0 with NO DRAW
0078c975 mov ecx,[esi+0x16c]; add ecx,4 ; the strategic generator's +4 sub-object
0078c97f call 0x47d830 ; Mars::RNG::NextFloat ret 0x0078c984 <-- THE DRAW
0078c984 fld qword [0x009e21b0] ; 6.2831854820251465 == 2*pi
0078c98a fldz ; LO = 0.0
angle = LO + (HI - LO) * r ; the float-range idiom, stored through a float32 temp
0078c9ac call 0x0080c860 ; (ship, angle) -> eax
0078c9bb call edx ; [[esi+0x15c]+0x1c] = ServerSpyManager::DeploySpy slot 7
```
Lane AS read this as `cbh = NextFloat() * 2pi`; that is right in value (`LO` is literally `fldz`) and
the expanded form is worth recording because it is the campaign's standard `lerp(LO, HI, r)` shape,
not a bare multiply. **The draw is unconditional once the validator passes, so it is exactly one
strategic word per applied Deploy Spy order**, in the `ProcessTurn` bracket
(`ApplyAllTurnCommands`), not the tail's.
**Its predicate is on the command stream**, not on save fields: *"a `SHIPACTION_DEPLOYSPY` command in
this turn's command stream"*. Lane AG's gate-indexed audit has two kinds of cell — a measured firing,
or a predicate over the save — and this is neither.
### 1.6 A complete RNG scan of all four bodies
Every `call`/`jmp` in each body decoded to a real instruction boundary and matched against the seven
RNG entry points (`NextFloat 0x0047d830`, `NextInt 0x004271c0`, `Chance 0x008e6dd0`,
`Twist 0x00426e00`, `Seed 0x0049fdf0`, `0x005876c0`, `0x0057da00`):
| body | range | RNG sites |
|---|---|---|
| `ServerSpyManager::vslot13` | `0x008877b0 .. 0x00887f30` | `0x00887c8a` only |
| `SpyManager::Slot13RngCallee` (`P`) | `0x008408e0 .. 0x00840a5a` | `0x00840929`, `0x008409c7`, `0x00840a3c` |
| `SHIPACTION_DEPLOYSPY` handler | `0x0078c930 .. 0x0078c9e0` | `0x0078c97f` only |
| `ServerSpyManager::vslot14` | `0x0088db80 .. 0x0088dd2c` | `0x0088dc43` only |
No inlined draw is present in any of them (rule 16's tempering immediates do not appear).
---
## 2. The corpus and the state, counted before building (rule 28 practice 5)
`as-turn15-spydeployed.sav`, read with `verify/save-reader/save_reader.py`:
| field | value | consequence |
|---|---|---|
| `sim/Frame` | 15 | |
| spy `sid` | 1 | |
| spy `sown` | 16 | player 0, `re` |
| **`re`'s `Species`** | **0 (Human)** | **`P` takes branch B; `0x00840929` and `0x008409c7` are unreachable** |
| spy `deat` | 400 (Kepler) | G1 satisfied — lane AS's contribution |
| spy `sdet` | **−1** | **the failed conjunct; G2 sends us to branch D every turn** |
| spy `sdo` | 0.00839999970048666 | |
| spy `tdep` | **15** | `vslot14`'s `Frame − tdep ≥ 3` first holds at **Frame 18** |
| spy `cm`, `cmo`, `spyon`, `ncp` | all 0 | `vslot14` G4 fails today; it bootstraps itself at Frame 18 |
| spy `cbh` | 5.033599376678467 | `= 2π × 0.8011207…`, i.e. `0x0078c97f` already fired once, unmeasured |
| players | 7 — `re`(0), `Revenge Fleet`(0), `Spengler`(0), 4 × `_NPC`(4) | branch A's candidate list would be non-empty, if it were reachable |
**Corpus count for `P`, on the predicate `∃ spy with deat != 0 && sdet != -1 && Frame − sdet ≥ 3`:
0 of 22 before this lane, and 0 of 23 including `as-turn15-spydeployed.sav`.** The failed conjunct is
`sdet == -1`, exactly as lane AS handed it over — and it is not a conjunct any amount of clicking can
satisfy directly. It is satisfied by *waiting*, at a probability the code fixes.
**Corpus count for `0x0078c97f`:** not expressible over save fields at all (§1.5). The nearest
save-side witness is `cbh != 0`, which holds in **1 of 23** saves — `as-turn15-spydeployed.sav`,
because the site fired on the turn that produced it.
**No save needs to be manufactured for either target.** Lane AS built both states. This lane
generates no map and plays no new game.
---
## 3. Predictions
Committed before the build, before the deploy of any binary to VM144, and before any End Turn.
### 3.1 The static claims
| | prediction | how it is wrong |
|---|---|---|
| **PA1** | `P` reaches **at most one** draw site per call, selected by `spyOwner->Species (+0x5c) == 6`. From this save, only `0x00840a3c`. | a `draw_sites` row at `ret_rva 0x0044092e` or `0x004409cc` on any turn of this workload falsifies it outright |
| **PA2** | Species indices are `0 Human, 1 Hiver, 2 Tarkas, 3 Liir, 4 _NPC, 5 Zuul, 6 Morrigi`; therefore `AccumulateDetectionOdds`'s `0.75f` is **Zuul** and its `0.5f` is **Morrigi** | a save whose `sim/species` list is ordered differently, or a Zuul save with `Species != 5` |
| **PA3** | `0x00840a3c` is `Chance(0.25f)` — a widened `float`, not the decimal 0.25 read from a decompiler | the four bytes at `0x009e5ac0` are not `00 00 80 3e` |
| **PA4** | the spy is **destroyed** at the end of `vslot13` on the turn `P` runs: `nspy` 1 → 0, `spies2` at Kepler 1 → 0, and no spy record survives in the autosave | the post-`P` autosave still carries a `spy` record |
### 3.2 Target 1 — the wait, and the per-turn word trajectory
Let `T` be the `Frame` on which `sdet` is stamped. `sdo` grows by **0.0084 per turn** and the roll on
the End Turn producing `Frame f` uses `sdo = 0.0084 × (f − 14)`, because `AccumulateDetectionOdds`
runs *before* the gate. `Chance` costs one word for `0 < p < 1` (board row 367).
| | prediction |
|---|---|
| **PB1** | `sdo` in the autosave after the End Turn producing `Frame f` is exactly `0.0084 × (f − 14)` as a float32, for every `f` until detection: 0.0168, 0.0252, 0.0336, 0.0420, 0.0504, … |
| **PB2** | `OnAllCombatDone_Tail` costs **1** word on Frames 16, 17, 18 (`vslot13`'s detection roll alone) |
| **PB3** | **`0x0088dc43` fires for the first time in this campaign on Frame 19**, one call, one word, `ret_rva 0x0048dc48`, `strategic = true` — because `vslot14` bootstraps its counter-mission at Frame 18 (`Frame − tdep = 3`, `ncp` empty), setting `spyon`, `cm := 1`, `cmo := 0.2f`, **without drawing on Frame 18 itself** |
| **PB4** | from Frame 19 the tail costs **2** words per turn (`vslot13` 1 + `vslot14` 1), and drops to 1 on any turn where `cmo` has accumulated to ≥ 1.0 or where `cm` has just cycled back to 0 |
| **PB5** | save-side: `cm` moves off its corpus-constant 0 on **Frame 18**, `spyon` moves to the Kepler owner's id on Frame 18, and `cmo` starts stepping by `0.2f` per failed counter-mission roll from Frame 19 |
| **PB6** | on the detection turn `T`: `vslot13` still costs 1 word (the roll that *succeeds*), `sdet := T` |
| **PB7** | on `T+1` and `T+2`: **`vslot13` costs ZERO words** — `sdet != -1` skips branch D, and `Frame − sdet < 3` skips the destruction path. A zero at `0x00487c8f` on those two turns is a *positive* result about a third arm and must not be read as "the roll did not fire" (rule 28 practice 6) |
| **PB8** | on `T+3`: one row at `ret_rva 0x00440a41`, `entry = Chance`, `calls = 1`, `words = 1`, `no_draw_calls = 0`, `strategic = true`; `vslot13`'s own `0x00487c8f` row **absent**; `vslot14` contributes **0** because the spy was destroyed at the end of phase 23 |
| **PB9** | the entry probe on **`Game::SpyManager::Slot13RngCallee 0x008408e0` reads 1** on Frame `T+3` — its first non-zero in the campaign, and the complement of lane AS's demonstration that its zero said nothing about the subtree |
| **PB10** | cumulative detection probability: **11 % by +4 turns, 43 % by +10, 70 % by +15, 87 % by +20** — computed from `p_k = 0.0084(k+1)`. *(Lane AS's brief quoted 38/65/85; that is the same model evaluated one turn late, using `p_k = 0.0084k`. Minor, and stated so the difference is on the record before the run.)* |
| **PB11** | the tail bracket total over the whole run equals the per-site sum: **residual 0** on every turn |
**PB12 — the tail case, stated in advance.** At +20 turns there is a 13 % chance `sdet` is still −1.
If that happens, the result reported is the **`sdo` trajectory against PB1** plus the `0x0088dc43`
series, and `P` stays a decoded-not-measured site. That is a real measurement of the accumulator and
it will be reported as such rather than ground out.
### 3.3 Target 2 — the deploy order
From `as-turn14-predeploy.sav`, one Deploy Spy order, one End Turn, `probes=8`:
| | prediction |
|---|---|
| **PC1** | one `draw_sites` row at `ret_rva 0x0038c984`, `entry = NextFloat`, `calls = 1`, `words = 1`, `no_draw_calls = 0`, `strategic = true` |
| **PC2** | it lands in the **`ProcessTurn`** bracket, not `OnAllCombatDone_Tail`'s, because `ApplyAllTurnCommands` runs first |
| **PC3** | the same turn's `OnAllCombatDone_Tail` costs **exactly 1** — the first detection roll at `sdo = 0.0084` |
| **PC4** | the resulting `spy.cbh` reproduces lane AS's value **exactly**: `5.033599376678467`, i.e. `2π × 0.80112…`. The strategic generator is seeded from the save and the per-process AI client seed does not feed it, so the same input plus the same order must draw the same word. **This is the sharpest single prediction in the document**: it says the deploy draw is reproducible across processes, sessions and lanes |
| **PC5** | `spies2` at Kepler goes 0 → `[1]`, `deat` 0 → 400, `tdep := 15`, `sdo := 0` then `0.0084` — reproducing lane AS's §4.2 from a different process |
### 3.4 The control
| | prediction |
|---|---|
| **PD1** | two fresh `hooks=off` processes on the **same** input will produce a byte-identical *pre*-turn `(Autosave EndTurn).sav` and **differing** post-turn autosaves |
| **PD2** | the differences localise to **one AI player's research pick plus `Summary/Checksum` and the `turnstats` `tch` cell** — the per-process `StrategyClient` seed of `2026-09-08-ai-seed-per-process.md`. Nothing of my empire, the spy, `deat`, `sdet`, `sdo`, `cm`, `cmo` or `spies2` moves |
| **PD3** | therefore **this is not a calibration pair and must not enter `determinism-hashes.txt`** — stated in advance so it cannot be a post-hoc excuse (rule 26) |
| **PD4** | the leaves this lane's numbers depend on — the `spymgr` sub-tree and the `systems` sub-tree — **do** reproduce across the control pair, and that is the claim that will be checked by sub-tree rather than by whole-file hash |
### 3.5 How the whole model could be wrong
* **`countC` is not stable.** `sdo`'s per-turn increment is `0.7 × (0.01 + 0.001 × countC)` with
`countC = 2` inferred, not observed. If a fleet moves in or out of Kepler the increment changes and
PB1 breaks — **and that would be the most interesting failure available**, because it would
identify `countC` from the delta. Symptom: an `sdo` step that is not 0.0084.
* **`vslot14` might not bootstrap.** AG's `ncp` check is on a container this lane has only seen as
the integer 0. If `ncp` is a count and non-empty means something else, PB3 fires late or not at
all. Symptom: Frame 19 tail costs 1, not 2, and `cm` stays 0.
* **The stage machine consults `owner->+0xff` (`CnTrd`, FTL Economics) at 1 → 2/3.** This game has no
FTL Economics, so the 1 → 3 arm is the one that runs; that changes which stage sequence is
exercised but not any word count.
* **PB7's two zero turns are the polarity trap in a new place.** `vslot13` will be *entered* on
`T+1`/`T+2` (the probe reads 1) and cost 0. If this lane reported that as "the roll stopped
working", it would repeat exactly the mistake rule 28 exists for.
* **PC4 can fail benignly.** If AI orders applied before mine consume strategic words, `cbh` differs
and the deploy draw is still 1 word. That falsifies *reproducibility*, not the site.
---
*(End of the pre-registered section. Everything below is measurement.)*