45 KiB
The spy detection roll — decoded, its gate turned into a save predicate, and measured
- Type: subsystem (static decode + live measurement)
- Owner / date: lane AS · 2026-09-09 · guest VM144 (
sots-re-win10-144,re@192.168.10.144) - Instrument: lane Z's
draw_sitesreturn-address ledger atprobes=8, shim buildas-c172c99-20260909T0205Z, distC:\SOTS\shimdist-as - Predictions, committed before the build:
sots-enginedocs/AS-predictions.md(commitc172c99, addendum61875a4) - Answers: the resolution
2026-09-09-tail-draws.md§8 item 2 (the spy half of the tail gate) - Corrects: the resolution's "the detection roll is inline in
vslot13" (it is a direct call); lane L3 §6's "spies2is not the spy list"; board row 399's V2 misquote is not touched here
(Sections 1–3 were written and committed before any measurement. Sections 4 onward are the measurement.)
0. Headline
The detection roll fires, it costs one word, and its gate is an asteroid belt.
Mars::RNG::Chanceat0x00887c8a— in the body ofServerSpyManager::vslot13, phase 23 call 9 of 9 ofOnAllCombatDone_Tail— rolls once per deployed, undetected spy per turn, on the strategic generator, at a probabilitysdothat starts at 0.0084 as measured and accumulates by the same amount every turn.OnAllCombatDone_Tailwent 363 → 364: one word, against 0 on every turn any lane has ever measured.The predicate is
spy.deat != 0 && spy.sdet == -1 && sys(deat).ARes2 > 0 && (sys(deat).TerrFl & 1). Corpus count: 0 of 22. The failed conjunct isdeat != 0— every spy this campaign has ever built was still docked to its tender. Not trade routes, not freighters, notspyon, and not the asteroid belt: all 22 corpus saves already had a usable belt somewhere, and two of them already had four AI-owned ones.And the polarity is the opposite of the intuitive one: when the belt is gone the roll does not happen and the spy is detected anyway. A zero at this site is two different results.
1. The chain, decoded from the instruction stream
Program sots / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs. Every body
below was disassembled from its start to the next function start (rule 17); no Ghidra size was
used. ReVa/CT111's MCP endpoint was down for this session (CONNECTION_CLOSED), so nothing was read
in the decompiler — this is all instruction stream, which for these bodies is a feature.
Game::ServerSpyManager::vslot13 = 0x008877b0 (lane V2's name), dispatched at 0x007d9811
from OnAllCombatDone_Tail on receiver S+0x15c slot 13. Body 0x008877b0 .. 0x00887f30.
Throughout: ebx = 0, edi = this (the manager), esi = the current Game::SpyCraft. The loop is
over the vector at this+0x10 .. this+0x14 — the same vector lane L3 watched grow to 1 at
Build Spy.
0088781d je 0x887e92 G1 spy.deat (+0x10) == 0 -> next spy
0088782d call 0x8b9240 sysA = registry(server+0x84, spy.deat) -> [ebp-0xbc]
00887845 call 0x8b9240 ownerO = registry(server+0x84, spy.sown) -> [ebp-0xc0]
00887856 je 0x887c5b G2 spy.sdet (+0x40) == -1 -> BRANCH D (detection)
00887867 jl 0x887e92 (server.turn - sdet) < 3 -> next spy
0088787a je 0x887c4f sysA.PID == 0 -> sdet := -1, next spy
00887891 je 0x887c4f sysA.PID == ownerO -> sdet := -1, next spy
00887af2 call 0x8408e0 <-- P, the false-flag draw (three sites inside)
Branch D — the detection roll:
00887c5b push eax ; ownerO
00887c62 push [ebp-0xbc] ; sysA
00887c63 push esi ; the spy
00887c66 call 0x81f570 D1 SpyCraft_AccumulateDetectionOdds -> writes spy.sdo (+0x3c)
00887c6b mov ecx,[ebp-0xbc]
00887c71 call 0x743f80 D2 t = ServerSystem_BeltUsableFlags(sysA)
00887c76 test al,1
00887c78 je 0x887c97 (t & 1) == 0 -> SKIP THE ROLL, straight to D4
00887c7a mov ecx,[edi+8] ; the StrategyServer
00887c7d fld [esi+0x3c] ; spy.sdo
00887c80 mov ecx,[ecx+0x16c] ; THE STRATEGIC GENERATOR
00887c86 push ecx
00887c87 fstp [esp]
00887c8a call 0x8e6dd0 D3 Mars::RNG::Chance(strategic, sdo) <-- THE DETECTION ROLL
return address 0x00887c8f
00887c8f test al,al
00887c91 je 0x887e92 roll failed -> next spy, still undetected
00887c97 mov edx,[edi+8]
00887c9a mov eax,[edx+0xc] ; the current turn
00887c9d mov [esi+0x40],eax D4 spy.sdet := turn (DETECTED; then the event strings)
1.1 It is a direct call, not an inlined draw — the instrument advice was right for the wrong reason
The resolution that created this lane says the roll is inline in vslot13. It is not. The bytes
are e8 41 f1 05 00 — a plain E8 rel32 to the Chance entry point 0x008e6dd0. Nothing
resembling rule 16's inlined tempering sequence is present.
What is true, and is the whole reason draw_sites is the right instrument, is that the call sits
in the caller rather than in SpyManager::Slot13RngCallee 0x008408e0. An entry probe on
0x008408e0 therefore reads zero whether or not the roll fires — but that is instrument choice,
not inlining, and the distinction matters because an inlined draw would be invisible to
draw_sites too, and this one is not. Lane AG read this independently and reached the same
conclusion; the correction is recorded in both places.
1.2 The gate is an asteroid belt, and it is sixteen bytes
00743f80 cmp dword ptr [ecx + 0x6c], 0
00743f84 mov eax, dword ptr [ecx + 0x19c]
00743f8a jg 0x743f8f
00743f8c and eax, 0xfffffffe ; clear bit 0 when [ecx+0x6c] <= 0
00743f8f ret
On Game::ServerSystem (objects/layouts.md): +0x6c = ARes2, +0x19c = TerrFl,
+0x100 = PID (which ServerSystem_GetOwner 0x007437e0, used three times above, returns). So
bit 0 of the return =
(TerrFl & 1) AND (ARes2 > 0)— "this system still has a usable asteroid belt".
ARes2 is already the field lane AC identified as the asteroid-belt resource (values 1500–3000,
matching SYSTEM_MIN/MAX_ASTEROID_RESOURCES). TerrFl bit 0 is its flag; §2 checks that reading
against the corpus, which is the only place a static reading of an unnamed bit can be checked
without a debugger.
The polarity, stated because everyone's intuition gets it backwards. The branch is a je that
jumps past the roll. Belt usable ⇒ roll. Belt gone ⇒ no draw at all, and sdet := turn
unconditionally. The spy hides in the belt; if the belt is mined out it has nowhere to hide and is
spotted for free. So at this site, a draw_sites row with calls = 0 and a sdet that has moved
is a positive result about a different arm, and must never be reported as "the roll did not fire".
1.3 sdo — where the probability comes from, and why the roll should cost a word
0x0081f570 (SpyCraft_AccumulateDetectionOdds(spy, sys, spyOwner), __stdcall, ret 0xc, 512
bytes) is called unconditionally on branch D, immediately before the gate, and writes
spy.sdo (+0x3c):
- if
sys.PID == 0(unowned):fldz; fstp [esi+0x3c]—sdo := 0.0and return. A spy at an unowned system rolls at p = 0, which costs no word. - otherwise it walks the system's fleet vector accumulating three counters (
[ebp-4],[ebp-8],[ebp-0xc]) and computes, with every literal read as the four bytes in the image (rule 23 — these are widened floats, not decimals):
p = 0.01f (0x009e31c0)
p += (countA > 0) ? 0.01f : 0 (0x009e3e14)
p += (countB > 0) ? 0.02f : 0 (0x00a1d048)
p += 0.001 * countC (0x009e23c0)
p *= (spyOwner->[0x5c] == 5) ? 0.75f (0x009e5ac4)
: (spyOwner->[0x5c] == 6) ? 0.5f (0x00a2c788)
: 1.0f
p *= 0.7 (0x009e5df8)
sdo = min(sdo + p, 1.0) (0x009e1ef0)
Two properties matter more than the arithmetic. It accumulates onto the previous sdo, and it
is clamped at 1.0. Mars::RNG::Chance costs 0 words at p <= 0 and at p >= 1 (board row
367). So:
- a fresh deploy at a quiet foreign system gives
sdo = 0.7 × 0.01 = 0.007— strictly inside (0, 1), so the roll costs exactly one word; sdoclimbs by ~0.007/turn, so a spy left in place for ~143 turns reachesp >= 1, at which point the site costs nothing and always succeeds. That is a second way a zero at this site is not a negative, and it is the same shape as lane AG'scmoearly-out.
(Measured afterwards: 0.0084, i.e. 0.7 × (0.01 + 0.001 × 2) — countC = 2 rather than the
0 assumed here. The one-word conclusion is unaffected and the form of the expression reproduced
exactly. §4.2a.)
1.4 What a deploy actually writes — and one draw site nobody had
ServerSpyManager::DeploySpy = 0x00887410, vftable 0x00a3073c slot 7, zero direct call
sites, reached only from the SHIPACTION_DEPLOYSPY handler 0x0078c930 through
[[dispatcher+0x15c]+0x1c] — the stack-built ship-action function-pointer table that lane B6
identified as a distinct indirection class. On the success path it writes:
spy->Reset() (0x00838070)
spy->sdo (+0x3c) := 0.0f
spy->sdet (+0x40) := -1
spy->cbh (+0x18) := <the float argument>
spy->tdep (+0x14) := server->[+0xc] ; the current turn
spy->atto (+0xc) := 0 ; carrierShip->[+0xa8] := 0 ; detach from the tender
ServerSystem_AddSpy(system, spy) at 0x008874d4
and AddSpy is where deat is born (§1.5). Because the deploy leaves sdet == -1 and sdo == 0,
the detection branch is entered on the very next OnAllCombatDone_Tail — which is the same End
Turn that applied the order, since ApplyAllTurnCommands runs before the tail.
The float argument is computed in the handler, and it is a draw:
0078c975 mov ecx,[esi+0x16c] ; esi = StrategyServer -- THE STRATEGIC GENERATOR
0078c97c add ecx,4 ; the +4 sub-object, same one the trade-raid roll uses
0078c97f call 0x47d830 ; Mars::RNG::NextFloat <-- 1 word, ret 0x0078c984
0078c984 fld qword [0x009e21b0] ; 6.283185482025146 == 2*pi
cbh = NextFloat() * 2*pi
0x0078c97fis a strategic-generator draw site in no existing inventory. It is not one of lane V2's eight, and it is not in lane I's 22, because it hangs off the ship-action table and no call-graph sweep reaches it. It fires once per Deploy Spy order applied, insideApplyAllTurnCommands— so it lands in theProcessTurnbracket, not the tail's. Any gate-indexed audit (lane AG) should carry it, and its predicate is "aSHIPACTION_DEPLOYSPYcommand in the turn's command stream", which is a stream predicate rather than a save predicate — a category the audit does not yet have a column for.
1.5 deat and spies2 are written by the same two instructions — spies2 is closed
ServerSystem::AddSpy = 0x007514c0, 48 bytes, complete:
007514c3 mov edx,[ebp+8] ; the spy
007514c6 test edx,edx / je ret
007514ca test ecx,ecx / jne
007514ce xor eax,eax ; this == NULL -> store 0
007514d2 mov eax,[ecx+4] ; the system's own registry handle
007514d5 mov [edx+0x10],eax ; *** spy->deat = system handle ***
007514d8 add edx,4 ; &spy->sid
007514db push edx
007514dc add ecx,0x1cc ; &system->spies2
007514e2 call 0x59f1a0 ; push_back (lane AI3's wrapper)
and its exact mirror ServerSystem::RemoveSpy 0x0074f550 sets spy->deat = 0 and erases
spy->sid from the same +0x1cc vector. ServerSystem +0x1cc is spies2 in objects/layouts.md.
AddSpy has exactly one caller (0x008874d4, inside DeploySpy) and no vtable slot, so this is
the only way deat ever becomes non-zero at runtime.
spies2is the per-system vector of DEPLOYED spy ids, anddeatis its reciprocal back-pointer. They are written by the same function, two instructions apart.
- Lane L3 §6: "
spies2is therefore not the spy list" — half right and worth correcting in place. It is not the spy manager's craft list; it is the system's deployed-spy list. L3's inference from "this save has a spy andspies2is still empty" was sound about whatspies2is not, and the alternative it offered second ("or it only fills for a deployed spy") is the right one.- Lane AC's P4 ("fills only for a deployed spy") is confirmed, and now from the writer rather than by elimination.
This retires one third of lane W's rule-6 flag on
spies2/SysMem/mtsstatically, and §5 exercises it live.
1.6 vslot14's roll is a different gate — and it excludes independent colonies
For the record, because target choice depends on it. ServerSpyManager::vslot14 0x0088db80
(phase 33) loops the same vector and requires spy.deat != 0, sys.PID != 0, and
sys.indi (+0x1c8) == 0 (ServerSystem_IsIndependent 0x00743fa0) — not an independent
colony — plus spy.cm (+0x24) in 1..4, before its Chance at 0x0088dc43. Lane AG has decoded
that site fully; the consequence here is only that an AI-empire target exercises strictly more
than an Independent Colony, which is why this lane went looking for an AI-owned belt rather than
taking the easy NPC one.
2. The corpus, counted before measuring (rule 28b)
verify/save-reader/save_reader.py --json over all 22 saves in verify/results/saves/.
| count | |
|---|---|
saves with any spymgr.spy record |
3 (L3's turn-15, AC's turn-20 and turn-22) |
of those, deat != 0 |
0 |
| saves satisfying the full detection-roll predicate | 0 of 22 |
| the failed conjunct | spy.deat != 0 |
The integrator's value-domain census (value-domain-census.md) reaches the same 0 independently and
more strongly: deat, sdet, sdo, cbh, cm, cmo are all in the 234 fields that have only
ever held one value, so the count is 0 by construction.
Two things fell out of the same pass, and the first is the load-bearing one.
ARes2 > 0andTerrFl & 1agree on every one of 616 system records (22 saves × 28 systems), with no exception in either direction. That is a save-side confirmation of §1.2's reading of an unnamed flag bit, obtained without a debugger, over a sample large enough to be worth something. It got stronger on the state this lane built (§4): there,TerrFlalso takes the values 2 and 3, andARes2 > 0still coincides exactly withTerrFl & 1— so bit 1 is a second, independent terrain feature and bit 0 is not merely "TerrFl is non-zero".- The belt was never the missing conjunct. All 22 corpus saves have at least one system
with a usable belt (5 to 15 of 28), and two of them —
human-turn2-orders.savandhuman-turn3-noderoute.sav— already have four AI-owned belt systems. Lane AC's map had none, and the campaign generalised from it. Those two saves arePTech 0 / ResM 1.0, so the spy tech chain is tens of turns away and they were not a usable shortcut; but "no AI colony can have a belt" was a fact about lane L3's one map, exactly the rule-28 practice-3 trap.
3. Predictions
Committed to sots-engine docs/AS-predictions.md at c172c99 before the build, with the
addendum at 61875a4 before the measurement. Summarised here; the falsification table is in that
file.
| prediction | |
|---|---|
| P1 | an AI-owned belt system exists within ≤ 5 map regenerations at lane L3's custom settings |
| P2 | the predicate of §2; corpus count 0 of 22; failed conjunct deat != 0 |
| P3 | the roll fires: one draw_sites row at ret_rva 0x00887c8f, Chance, calls = 1, strategic = true |
| P4 | it costs 1 word; no_draw_calls = 0 |
| P5 | the tail bracket goes 0 → 1; the trade half contributes 0 (tscr 252, no fleet at a sector node) |
| P6 | sdet most likely stays −1 (p ≈ 0.993); sdo moves off its corpus-constant 0 to ≈ 0.007, and that is the save-side confirmation |
| P7 | spies2 non-empty in the target system only |
| P8 | the hooks=off pair reproduces byte-identically in two fresh processes |
| P9 | probes=8 is byte-neutral on this workload too |
4. The workload — built from a cold main menu in one session
Shim build as-c172c99-20260909T0205Z, cross-built on CT111 in this lane's own directory
/srv/re-lab/build/sots-engine-as (rm -rf build-host build-shim before configure — rule 24),
staged to /srv/re-lab/shim/dist-as, deployed to C:\SOTS\shimdist-as on VM144. Exports checked
identical to the real binkw32.dll (66 names).
Guest adoption check, first, before anything else. ref-turn2.sav, one End Turn, hooks=off,
this lane's own build:
(Autosave EndTurn).sav bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 66,732
(Autosave).sav 978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921 67,219
Both identical to the published oracle in verify/results/saves/determinism-hashes.txt. The
binary and the guest are sane.
4.1 The map — zero regenerations were needed
New custom game at lane L3's settings (28 stars, 5 ly, 2 players Human vs Tarkas, treasury 9,850,000, 10 initial colonies, 15 initial technologies, econ 148 %, research 146 %), species set explicitly to Human on the launch card (it defaults to Random — L3's note holds).
The first map generated had two AI-owned belt systems. The brief expected several regenerations; P1's estimate of ≤ 5 was never tested because none were needed.
| SysID | name | PID | ARes2 |
TerrFl |
note |
|---|---|---|---|---|---|
| 400 | Kepler | 32 (AI) | 2435 | 1 | the target — 3.5 ly from my colony Midway |
| 224 | Delta Pavonis | 32 (AI) | 1537 | 1 | the spare |
| 272 | Solaris | 0 | 2773 | 1 | unowned — sdo would be forced to 0 here (§1.3) |
| 160 / 480 | Piscium / Pascal | 16 (me) | 2448 / 2522 | 1 | mine |
| 336 | Copernicus | 0 | 2359 | 3 | bit 1 also set |
| 128 | Downbelow | 0 | 1965 | 1 | |
| 384 / 448 / 464 | Nunki / Mu Bootis / Jinx | — | 0 | 2 | bit 1 set, bit 0 clear, no belt |
That last row is the one that matters for §1.2. In the whole 22-save corpus TerrFl had only ever
been 0 or 1, so "bit 0" and "non-zero" were indistinguishable. This map has three systems at
TerrFl = 2 with ARes2 = 0 and one at TerrFl = 3 with ARes2 = 2359. ARes2 > 0 still
coincides exactly with TerrFl & 1, and now it is bit 0 specifically, not "TerrFl is set".
4.2 The path to a deployed spy, with what each turn cost
The four techs came faster than lane L3's recipe suggested because CCC_FTLBrdB was in the
15-technology roll again (it appears to be common) and because the top-of-screen
Savings ↔ Research slider at its right end turns a 2-turn tech into a 1-turn tech.
| turn | what |
|---|---|
| 2 | new game; research CCC_SpyBm (CCC_FTLBrdB already researched from the initial roll) |
| 4 | CCC_SpyBm done; research IND_OrbFound |
| 6 | done; research IND_CruisCon (2 turns → 1 with the research slider at max) |
| 7 | done; research IND_SlvgTech |
| 8 | all four techs; design a Cruiser with the Repair and Salvage mission section; queue it |
| 9 | the tender exists; fleet panel's Special → Build Spy → Select All → OK |
| 10 | the spy craft exists: nspy 1, deat 0, sdet -1, sdo 0, atto = the tender — the corpus state |
| 10 | Move the fleet to Kepler (Dest: Kepler, 4 Turns) |
| 14 | fleet arrived: Flt.LocID = 400, read from the autosave, not the panel |
| 14 | Special → Deploy Spy — the menu entry is present, which is itself the belt gate answering |
| 15 | order applied. deat = 400, sdet = -1, sdo = 0.0084, spies2 = [1] at Kepler |
Two things about turn 15 are worth stating separately, because they are results rather than steps.
(a) sdo = 0.0084, and that is §1.3's formula evaluated, not fitted. The prediction was 0.007
for a quiet system (0.7 × 0.01). The measured value is 0.7 × 0.012 = 0.0084, i.e. countC = 2
in the third term. So the form — a 0.7 multiplier over a 0.01 base plus 0.001 per unit of a
counter — reproduces exactly, at a value no run of this campaign has ever produced, on a state
nobody had built. The identity of countC is not established (it is 2 at a system holding one
foreign colony and my one-ship fleet); the constant and the shape are.
(b) spies2 filled, in exactly the system the writer says. Reading all 28 systems of the
turn-15 autosave: spies2 is count = 0 in 27 of them and count = 1, element = 1 at Kepler,
where 1 is the spy's sid. This is the first non-empty spies2 in the corpus and it matches
ServerSystem::AddSpy instruction for instruction (§1.5).
5. The measurement
All three runs load the same file, MyGameas1spydep.sav
(sha256 854a10fa1ea602f0f7909f9bf9154942e56ea5fa1db4cce47d6d32bf53952e08, 76,705 B, turn 15,
--strict clean, 0 errors / 0 warnings), in a freshly launched process, and press End Turn
once. Repo copy: verify/results/saves/as-turn15-spydeployed.sav.
5.1 The oracle pair (rule 26) — and what it says honestly
| run | config | (Autosave EndTurn).sav (the input, re-saved) |
(Autosave).sav (post-turn) |
|---|---|---|---|
| A | hooks=off |
98e45d3745b91450… 76,706 |
262f8bda97c9511d… 77,685 |
| B | hooks=off, fresh process |
98e45d3745b91450… identical |
e34775a757e6fb3d… 77,649 differs |
| M | probes=8 |
98e45d3745b91450… identical |
262f8bda97c9511d… identical to A |
The pre-turn save is byte-identical in all three, which proves the input state — including any pending order — was the same in every run. The post-turn saves are not.
A vs B differs in exactly 22 leaves, and every one of them is inside one AI player.
state_checksum --no-audit localises them:
/Summary/Checksum 985948837 -> 985948925
/Sim/turnstats/history/hist[1]/stats[15]/tch 46 -> 45
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/St[62] 2 -> 3
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TResDone[62] 0 -> 22469
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/St[64] 4 -> 2
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TResDone[64] 22469 -> 0
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TAcq[64] 16 -> -1
/Sim/players/Player[32 "Revenge Fleet"]/TechTree/TiAcq[64] 45 -> -1
/Sim/players/Player[32 "Revenge Fleet"]/ResTNm '' -> 'DRV_NodFoc'
… plus that player's Events/EvNxID/otch bookkeeping for the same pick
In A the AI completed Overthrusting; in B it is part-way through DRV_NodFoc. Nothing else
moved — not one leaf of my empire, not deat, sdet, sdo, spies2, trdmgr or any fleet.
That is precisely the mechanism 2026-09-08-ai-seed-per-process.md established: each AI
StrategyClient's generator is seeded with a word that differs in every process, so its research
pick is a per-process outcome. Board row 326 recorded the same signature on
turn1-state → turn2 — "one shadow empire's research pick plus the derived checksum".
So the control agrees with itself modulo the known non-deterministic leaves, which is the bar the brief set — and it is a weaker bar than byte-identity, which must be said plainly. What is established is: on this input, in two fresh un-instrumented processes, every leaf outside one AI player's research bookkeeping reproduced exactly. What is not established is byte-identity, and this workload cannot establish it until the client seeds are pinned (the resolution's Rung C).
And the instrumented run M came out byte-identical to A. Read carefully, that is:
- strong for the thing that matters —
probes=8changed no leaf of the sim, the spy, or my empire, on the same input, which is the byte-neutrality claim P9 makes; - and, for the AI-pick leaf specifically, a coincidence of size 1/k (rule 26's corollary): M happened to draw the same AI seed outcome as A. It is not evidence about the instrument in that one leaf class, and I am not treating it as such.
5.2 The instrument was armed, and one probe reading zero is the point
shim.log: seven draw-site detours create=MH_OK enable=MH_OK; probe: installing 8 of 12; all
eight MH_OK; no COVERAGE: line. draw_site_overflow = 0.
| probe | entries this turn |
|---|---|
Game::ServerSpyManager::vslot13 0x008877b0 |
1 |
Game::ServerSpyManager::vslot14 0x0088db80 |
1 |
Game::ServerTradeManagerImpl::vslot13 0x0088ef80 |
1 |
Game::ServerTradeManagerImpl::vslot15 0x0082cca0 |
1 |
Game::SpyManager::Slot13RngCallee 0x008408e0 |
0 |
Game::TradeManager::Slot13RngCalleeA 0x00820ca0 |
0 |
Game::TradeManager::Slot13RngCalleeB 0x0088b440 |
0 |
Game::ServerTradeManager::CreateRaidEncounter 0x008938a0 |
0 |
indices 8–11 (GenerateTradeRaidEncounters, three controls) |
NOT INSTALLED — probes=8 installs 0–7 |
That table is the lane's methodological point in one line. The detection roll fired, and the entry probe on
SpyManager::Slot13RngCalleeread 0 on the same turn. Four lanes read that zero and concluded the spy subtree was draw-free. It was never a statement about the subtree; it was a statement about which function the call sits in.
5.3 The detection roll — it fires, and it costs one word
draw_sites, the post-turn autosave marker, the row verbatim:
entry = Chance
ret_rva = 0x0048_7c8f (VA 0x00887c8f — the instruction after `call 0x8e6dd0` at 0x00887c8a)
calls = 1
words = 1
no_draw_calls = 0
strategic = true
and the same site in the per-call-site report, with the owner resolved:
* STRAT Chance call 0x00887c8a <ServerSpyManager::vslot13>+0x4da calls=1 words=1
The boundary ledger for the same turn:
BeginProcessTurn 345 -> 345 0
ProcessTurn 345 -> 363 18
OnAllCombatDone_Tail 363 -> 364 1
Autosave 364 -> 364 0
OnAllCombatDone_Tailcosts 1 word, against 0 on every turn any lane has ever measured, and the whole of it is the detection roll.
Turn total 19 words; the per-site sum after removing helper-internal rows is 19; residual 0.
The tail's one word is the only site in the tail that fired: no row at 0x00820e18 or 0x0088b613
(trade 13 — tscr is 252 in this game and no fleet stands on a sector node, so lane AC's predicate
says 0 and it is 0), none at 0x0082cdb8 (trade 15), none at 0x0088dc43 (spy 14 — cm = 0, lane
AG's failed conjunct), and none of P's three.
Predictions, scored. P1 held trivially (0 regenerations, not ≤ 5). P2 held. P3 held
exactly. P4 held exactly — one word, no_draw_calls = 0, and the reasoning behind it (four
widened-float literals plus Chance's cost at interior p) was written down before the run.
P5 held — 0 → 1. P6 held in both halves: sdet stayed −1 (the roll failed, as a 1.68 %
roll should), and sdo moved from its corpus-constant 0 to 0.0084 on the deploy turn and to
0.0168 after the measured turn — an increment of exactly 0.0084 both times, which is the
accumulator running. P7 held. P8 held only modulo the AI-seed leaves (§5.1). P9 held.
5.4 The second instrument agreed — which is what makes this more than one number
The coordinator asked for a save-visible confirmation independent of the bracket. There are two:
sdomoved and by the right amount. 0 → 0.0084 → 0.0168. Only branch D writessdo, and it writes it immediately before the gate, so a movingsdoproves the branch ran; the equal increments prove the accumulator, not a one-off.spies2filled in exactly one system. That is the deploy half rather than the roll, but it confirms thedeatreading the whole predicate rests on.
sdet did not move, and per P6 that was the predicted outcome, not a failure.
6. Coverage — what this did not touch, said as loudly as what it did
- One measured turn, one spy, one target system, one fleet of one ship. The cost is 1 word on that turn. The loop is over the spy vector, so the cost should be one word per deployed, undetected spy at a system with a usable belt — that is a reading, not a measurement. Rule 20: do not fit a constant to one observation. A second spy at a second belt system separates per-spy from per-turn and costs one more deploy.
- The
sdoarithmetic is validated only in its first two terms.countA,countBand the species multiplier were 0/0/1.0 here, andcountC = 2is inferred from the value, not observed. A Zuul or Liir owner (+0x5c= 5 or 6) would exercise the 0.75/0.5 branches; a system with enemy ships present would exercisecountA/countB. - The auto-detect arm was never taken.
(TerrFl & 1) == 0at the target has not been reached — it would need a belt to be mined out from under a deployed spy. So §1.2's polarity claim rests on the instruction stream plus the deploy-time refusal, not on a measured firing. It is the single most load-bearing unmeasured branch in this document. P(0x008408e0) and its three draws stay unfired, and the failed conjunct is nowsdet, notdeat. From this save the recipe is arithmetic rather than a guess:sdogrows 0.0084/turn, soP(detected)is 12 % after 5 more End Turns, 38 % after 10, 65 % after 15, 85 % after 20 — thenPruns 3 turns aftersdetis stamped, provided the system is still foreign-owned. About 20 End Turns fromas-turn15-spydeployed.sav, no clicking.0x0078c97fwas decoded and not measured (§6.1).probes=11was not used anywhere in this lane, and no number here is comparable to lane AC's turn totals, which were taken at that configuration.- No
Guardregion is declared by any hook in this family, the same gap lanes Z and H reported:undeclared = 0in this trace is vacuous. - One control run was lost to the harness and is reported rather than hidden. A third
hooks=offrun mis-detected the post-turn End Turn button as "turn still running" and clicked End Turn three times, advancing the line to turn 18. It was discarded; nothing in §5 uses it. The defect was in this lane's own click automation (the brightness test had the wrong polarity: after a completed turn the button is brighter, not dimmer), which is rule 19 pointing at the operator rather than at the detour. Its lesson is worth carrying: verify the turn number changed, not a button's colour.
6.1 A draw site nobody has — the deploy's own NextFloat
Decoded here and not measured, so it is a rule-6 hypothesis with a hook site attached:
0078c975 mov ecx,[esi+0x16c] ; esi = StrategyServer -- the STRATEGIC generator
0078c97c add ecx,4
0078c97f call 0x0047d830 ; Mars::RNG::NextFloat -- 1 word, return address 0x0078c984
0078c984 fld qword [0x009e21b0] ; 6.283185482025146 == 2*pi
spy.cbh = NextFloat() * 2*pi
0078c9bb call edx ; -> ServerSpyManager::DeploySpy, vtable slot 7
inside the SHIPACTION_DEPLOYSPY handler 0x0078c930, which is entry 9 of the stack-built
ship-action function-pointer table (lane B6's indirection class — no direct callers, no vtable
slot, invisible to every call-graph sweep this campaign has run).
It fires once per Deploy Spy order applied, inside ApplyAllTurnCommands, so it lands in the
ProcessTurn bracket, not the tail's. Its predicate is not a predicate on save fields at all —
it is "a SHIPACTION_DEPLOYSPY command in this turn's command stream", a stream predicate,
which lane AG's gate-indexed audit has no column for and should grow one.
To measure it costs one run: load verify/results/saves/as-turn14-predeploy.sav under probes=8,
select Kepler, Special → Deploy Spy → <ship> → OK, End Turn, and read draw_sites at
ret_rva 0x0078c984. This lane attempted it and lost the run to the map UI (Kepler and Midway sit
within four pixels of each other at the zoom the map opens at, and Midway wins the hit test), then
chose to report the site honestly rather than rush a fourth process. Prediction, committed here for
whoever takes it: one call, one word, strategic = true, in the ProcessTurn bracket, and the
same turn's tail still costs exactly 1 — the deploy and the first detection roll happen on the same
End Turn, because ApplyAllTurnCommands runs before OnAllCombatDone_Tail.
7. VM144 as left
Restored and verified by screenshot at the main menu, profile re, 2026-09-09 00:03 local.
C:\SOTS\binkw32.dllrestored to lane L3's build fromC:\SOTS\shimdist-l3\binkw32.dll(sha256479B8614D2417603…, byte-identical to what this lane found in place).C:\SOTS\shim.cfgrestored fromC:\SOTS\ui\preAS-shim.cfg(L3'sshim.cfg.l3probe).- The three autosaves restored byte-identical from
C:\SOTS\ui\preAS-SavedGames(1985E6F4…/24B2E072…/5EC80C1E…). C:\SOTS\shim.trace.jsonlremoved.
SavedGames is now 15 files, not 13, so every Load-dialog row position has moved again —
screenshot the dialog, do not reuse a remembered row. The two additions are this lane's, and they
are worth leaving because they are the only deployed-spy states in existence:
| file | turn | why it is worth keeping |
|---|---|---|
MyGameas1predeploy.sav |
14 | fleet + tender + docked spy standing at an AI belt colony, one click from a deploy — the input for §6.1 |
MyGameas1spydep.sav |
15 | the deployed spy: deat 400, sdet -1, sdo 0.0084, spies2 = [1] at Kepler |
At the 15-file set, verified by screenshot this session: MyGameas1predeploy is at (400, 347)
and MyGameas1spydep at (400, 376); OK is unmoved at (682, 624).
Also left in place, all harmless: C:\SOTS\shimdist-as\ (build
as-c172c99-20260909T0205Z, 64 files), C:\SOTS\ui\asgo.ps1, C:\SOTS\ui\ashash.ps1,
C:\SOTS\ui\preAS-shim.cfg, C:\SOTS\ui\preAS-SavedGames\ (13 files),
C:\SOTS\ui\asfetch.sav / asgrab.jsonl / asgrab.log (scratch). click_helper.ps1 was not
touched — this lane needed no new verbs, which is the first VM lane in a while that did not.
7.1 Guest notes worth carrying forward
- The map's hover readout works with a plain
moveon VM144 — lane AC'sjmovejiggle was not needed here. Enter Move mode first; the readout renders at the hovered star, not at the cursor. - Finding a named system on the star map is solvable arithmetically instead of by hunting.
Hover four systems, read their names, and least-squares-fit an affine map from the save's
Posvectors to screen coordinates; the residuals came out ≤ 8 px on 5 points, which is enough to click a specific star first time. The script is in this lane's scratch notes and the method is the reusable part. Its failure mode is systems that project within a few pixels of each other (Kepler and Midway), where the nearer one wins the hit test. - The End Turn button is BRIGHTER after a turn completes, not dimmer. Any "is the turn still running?" test built on that button's colour has the polarity backwards. Poll the autosave mtime and the turn number in the ticker, never the button.
- Accepting the AI's non-aggression offer (turn 6 here) stopped the combat encounters that were interrupting every third End Turn, and cost nothing this lane needed.
8. Corrections to earlier findings (rule 11)
2026-09-09-tail-draws.md§0.7 and §7, and lane AC §2.2: "the detection roll is inline invslot13". It is a directE8call to theChanceentry point sitting invslot13's body. The instrument advice that followed from it is right; the stated reason is not. §1.1.- Lane L3 §6: "
spies2is therefore not the spy list". Half right. It is not the spy manager's craft list; it is the system's deployed-spy id vector, written byServerSystem::AddSpyin the same two instructions that setdeat. L3's own second alternative was the correct one. §1.5. - Lane AC §5: "the textbook target — an enemy colony with a belt — does not exist in this game." True of that map and read across the campaign as if it were true of the game. Two corpus saves already had four AI-owned belt systems, and the first map this lane generated had two. §2.
- Lane V2 §3.1's spy-13 row listed
0x00887c8acorrectly and is vindicated, like its trade-13 row before it. Its "never observed firing" note is now discharged for this one site. - Ghidra's size for
0x008877b0is 1,869 bytes; the next function starts at0x00887f30, i.e. 1,920. The detection roll at0x00887c8ais inside both, but the 51-byte shortfall is rule 17 again, on the very function this lane exists to read. - This lane's own pre-registered §1.1, corrected here rather than edited there (rule 12).
docs/AS-predictions.mdcalls0x0083ce50"the deploy validator" and says its0x1000000refusal bit is theCan't deploy spy: Spy requires an asteroid belt to hide.message. Read properly afterwards,0x0083ce50is a shared placement validator with ~21 refusal bits (colonise, mine, dump ore, too many things present, …), reached from0x00760ce0and the order path0x00849460, and it is not onServerSpyManager::DeploySpy's call chain at all — that chain isSHIPACTION_DEPLOYSPY 0x0078c930→ vtable slot 70x00887410→AddSpy, and it never calls0x00743f80. What survives, and is all §1.2 needs, is that0x00743f80bit 0 gates a belt-shaped refusal there and the detection roll here; which action that refusal belongs to was over-claimed and is not established. The empirical half is unaffected:Deploy Spywas offered and accepted at a belt system, and the corpus agreement ofARes2 > 0withTerrFl & 1is 616 of 616.
9. Artifacts
| what | where |
|---|---|
the deployed-spy save — first deat != 0 and first non-empty spies2 in the corpus |
verify/results/saves/as-turn15-spydeployed.sav (76,705 B, --strict 0 errors / 0 warnings, sha256 854a10fa1ea602f0…) |
| the pre-deploy save, one click from a deploy at an AI belt colony | verify/results/saves/as-turn14-predeploy.sav (75,908 B, sha256 53986f8511cbf992…) |
the instrumented trace (draw_sites + probe_entries + the boundary ledger) |
verify/traces/as-probes8-turn15-turn16.jsonl.gz |
| shim log for that run (probe and detour install status) | verify/results/shim/as/as-probes8-run.shim.log |
| addresses this lane mints | ghidra/addresses.d/as.json (7 entries; 6 verified, 1 mapped) |
| predictions, committed before the build | sots-engine docs/AS-predictions.md (c172c99, addendum 61875a4) |
| instrument | sots-engine src/shim/shim.cfg.hp8 and shim.cfg.hoff, both unchanged — this lane wrote no engine code |
9.1 The hashes, for the record
input MyGameas1spydep.sav 854a10fa1ea602f0f7909f9bf9154942e56ea5fa1db4cce47d6d32bf53952e08
A/B/M (Autosave EndTurn).sav 98e45d3745b914506fc4c409ad5a5ad0938a2003ab4c845421a99a8065149c3a
A (Autosave).sav 262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe
B (Autosave).sav e34775a757e6fb3df647d7a589e8b1819b90bea094196a9e3cc819e51ed63257
M (Autosave).sav 262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe
This is not a calibration pair for the standalone, and it must not be entered in
determinism-hashes.txt as one. A ≠ B, in the AI-research-pick leaf class, so the turn is
reproducible only once the client seeds are pinned. What it is is a reproducible input and a
turn whose every non-AI leaf reproduces across three processes and two configurations.
Proposed campaign/board.md rows
New row:
| THE SPY DETECTION ROLL FIRES - 1 word, gated on an asteroid belt; `spies2` and `deat` closed | verify | verified | high | 100% | 2026-09-09 | **Lane AS, VM144, build `as-c172c99-20260909T0205Z`, `probes=8` (never 11).** `Mars::RNG::Chance` at **0x00887c8a**, in the BODY of `ServerSpyManager::vslot13` (tail phase 23 call 9), fired **1 call / 1 word / no_draw_calls=0 / strategic=true** at `ret_rva 0x00887c8f`; `OnAllCombatDone_Tail` went **363 -> 364**, against 0 on every turn any lane has ever measured. Turn total 19, per-site sum 19, **residual 0**. THE ENTRY PROBE ON `SpyManager::Slot13RngCallee 0x008408e0` READ **0 ON THE SAME TURN** - that zero was never a statement about the subtree, only about which function the call sits in (rule 28 practice 4, demonstrated rather than argued). **CORRECTION: the roll is NOT inline** (the resolution and AC §2.2 both say so) - it is a plain `E8` to the `Chance` entry point in the caller. **THE PREDICATE**, decoded to save fields: `spy.deat != 0 && spy.sdet == -1 && sys(deat).ARes2 > 0 && (sys(deat).TerrFl & 1)`; **corpus count 0 of 22, failed conjunct `deat != 0`**. `ServerSystem_BeltUsableFlags 0x00743f80` (16 bytes) returns `TerrFl` with bit 0 cleared when `ARes2 <= 0`; `ARes2>0` and `TerrFl&1` agree on all 616 corpus system records AND on the new map, which also has `TerrFl = 2` and `3`, so bit 0 is the belt specifically. **POLARITY: belt gone => NO DRAW AND THE SPY IS DETECTED ANYWAY** (`je` past the roll to `sdet := turn`) - a zero here is two different results. Cost: `sdo` starts at 0, `SpyCraft_AccumulateDetectionOdds 0x0081f570` ACCUMULATES `0.7*(0.01 + [cA]0.01 + [cB]0.02 + 0.001*cC) * species(1.0/0.75/0.5)` and CLAMPS AT 1.0, so `Chance` costs 1 word until sdo reaches 1 and 0 thereafter. Measured `sdo` 0 -> 0.0084 -> 0.0168 (= 0.7*0.012 exactly, a value no run had produced). **`spies2` IS CLOSED**: `ServerSystem::AddSpy 0x007514c0` sets `spy.deat = system handle` and pushes `spy.sid` into `ServerSystem+0x1cc` in the same two instructions - `spies2` is the per-system DEPLOYED-spy id vector; L3's "not the spy list" was half right, AC's P4 was right. Measured: `count=1 element=1` at Kepler, 0 in the other 27. **NEW DRAW SITE NOBODY HAS**: `0x0078c97f` NextFloat in the SHIPACTION_DEPLOYSPY handler `0x0078c930` (`cbh = NextFloat()*2pi`), strategic generator, in the ProcessTurn bracket - decoded, NOT measured, and its predicate is on the COMMAND STREAM not the save, a column the gate-indexed audit lacks. Oracle: input byte-identical in 3 processes; A vs B differ in **22 leaves, all inside one AI player's research pick** (the known per-process client seed, resolution 2026-09-08) - so this is NOT a calibration pair and must not enter `determinism-hashes.txt`. `probes=8` run came out byte-identical to control A. Saves: `as-turn15-spydeployed.sav`, `as-turn14-predeploy.sav`; findings `findings/subsystems/spy-detection-roll.md` |
Edits to existing rows:
- Row 62 (guest holders) —
VM144 = FREE (lane AS released 2026-09-09; restored to L3's build + shim.cfg + the three pre-AS autosaves byte-identical, main menu verified by screenshot). SavedGames is now 15 FILES - AS added MyGameas1predeploy (400,347) and MyGameas1spydep (400,376), so ROW POSITIONS HAVE MOVED AGAIN; screenshot the dialog. AS left C:\SOTS\shimdist-as + ui\as{go,hash}.ps1 + ui\preAS-*; click_helper.ps1 untouched. - Row 207 (V2's eight sites) — append:
FIRED 2026-09-09: 0x00887c8a (spy 13), lane AS, 1 word, behind a deployed spy at a foreign colony with a usable asteroid belt. V2 vindicated a second time. Five of the eight still unfired: P's three (0x00840929 / 0x008409c7 / 0x00840a3c, gate nowsdet != -1and turn-sdet>=3, ~20 End Turns from as-turn15-spydeployed.sav), 0x0088dc43 (spy 14, gatecm in 1..4, lane AG), 0x0082cdb8 (trade 15). AND THE INVENTORY IS SHORT ONE: 0x0078c97f in the SHIPACTION_DEPLOYSPY handler draws a NextFloat on the strategic generator and is in no sweep, because it hangs off the stack-built ship-action table. - Row 373 / the
spies2rule-6 flag —CLOSED 2026-09-09 by lane AS, from the writer and then live. spies2 (ServerSystem+0x1cc) is the per-system vector of DEPLOYED spy ids: ServerSystem::AddSpy 0x007514c0 sets SpyCraft+0x10 (deat) to the system handle and push_backs SpyCraft+0x4 (sid) into +0x1cc; RemoveSpy 0x0074f550 is the exact mirror. Measured non-empty for the first time in the corpus: count=1, element=1, in the deploy system only.SysMemandmtsare untouched and their flags stay up. - Row 186 /
tail-rng-ledger.md— append to the existing qualification:THIRD site outside the firing-indexed table has now fired: the spy detection roll (tail T23c9, lane AS). The pattern is now three for three - hives, the raid roll, the detection roll - and each fired on the first state built for it. The gate-indexed audit is the fix, not a longer table.