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.
20 KiB
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_sitesreturn-address ledger atprobes=8(the byte-neutral set; neverprobes=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'sfindings/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. Fromas-turn15-spydeployed.savexactly one of the three can ever fire —0x00840a3c— because the spy's owner is player 0 (re),Species = 0(Human).0x00840929and0x008409c7are gated onspyOwner->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:
0x00840a3cfires once, for one word, on the End Turn that producesFrame == sdet + 3; and two sites nobody expected fire on the way — the spy counter-mission roll0x0088dc43(lane AG §3.2, corpus 0 of 22, never fired) starting at Frame 19, and the deploy's ownNextFloat0x0078c97fon 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
countCis not stable.sdo's per-turn increment is0.7 × (0.01 + 0.001 × countC)withcountC = 2inferred, 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 identifycountCfrom the delta. Symptom: ansdostep that is not 0.0084.vslot14might not bootstrap. AG'sncpcheck is on a container this lane has only seen as the integer 0. Ifncpis a count and non-empty means something else, PB3 fires late or not at all. Symptom: Frame 19 tail costs 1, not 2, andcmstays 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.
vslot13will be entered onT+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,
cbhdiffers and the deploy draw is still 1 word. That falsifies reproducibility, not the site.
(End of the pre-registered section. Everything below is measurement.)