# The gate-indexed RNG audit — one row per statically reachable draw site, and every blank cell visible - **Type:** control-flow (static; no measurement taken, no VM used) - **Owner / date:** lane AG · 2026-09-09 · host + `dumps/sots.exe` only - **Instrument:** `objdump -b binary -m i386 -M intel` over `dumps/sots.exe`, decoded from real instruction boundaries **to the next function start, never to Ghidra's reported size** (rule 17); call graph rebuilt from scratch (`E8 rel32` + `E9 rel32` landing on a function start + lane V2's pinned virtual edges); corpus counted with `verify/save-reader/save_reader.py --dump` over the **22** saves in `verify/results/saves/` - **Created by:** `findings/resolutions/2026-09-09-tail-draws.md` §8 item 3 — *"the gate-indexed audit: one table, every reachable draw site, firing-or-predicate, corpus count"* - **Corrects (rule 11):** lane V2 §4.3 caution (a) — **closed**; lane AC §8's "three `rt` route records" — it is **four** in `ac-turn22-raider.sav`; lane Z §11.1 / lane H §4 on `CreateRaidEncounter`'s cost — a **success** costs far more than "0 or 1 further word" - **Ghidra is single-user and this lane did not enter it.** Everything here is read from the image on the host with the campaign's own tools; nothing was written back. The ReVa MCP server was down (`CONNECTION_CLOSED`) for the whole lane and the HTTP fallback was not needed. --- ## 0. What this table is, and what the old one was Lane Z's ledger is **indexed by firing**: a site enters it when a state we happened to run reaches it. Such a table grows only by luck, and it has now been qualified twice in two days (L1's hive draw, AC's raid roll). This table is **indexed by gate**: one row per site the *code* can reach, whether or not any state we own reaches it. Each row carries **either** a measured firing **or** a decoded predicate on save fields with the corpus counted against it. A row with neither is an **unread gate** and is listed as one in §6. **No total is reported.** Thirty-one rows are unread gates (thirty distinct draw sites — `0x0081351c` is reachable from both drivers and appears twice — plus one grouped row covering sixteen SVSO overrides whose sites were not individually enumerated). A "complete" number over this table would repeat exactly the mistake the lane exists to fix. **Three results the firing-indexed table could not have produced:** 1. **Six draw sites nobody had listed**, all behind `ServerTradeManager::CreateRaidEncounter`'s empty-candidate-list gate (§5.1). Lane H measured that function *entered* 3 times drawing 0 words; the ledger therefore recorded one site (`0x008939ee`) and could not see the five past it. 2. **A seventh unlisted site inside `Slot13RngCalleeB`'s own subtree** — `0x00820c1b`, a `NextFloat` in `FUN_00820af0` (§5.2). **Lane AD's committed prediction should say `B` costs 2 words on success, not 1.** 3. **The SVSO generator is named** (§4): all seventeen `SVScriptObject` overrides that can draw use `GetGame()->+0x16c` — the strategic generator, the one the save serialises. V2 §4.3's standing blank is closed. --- ## 1. Method, stated so it can be repeated and attacked ``` tools: scratch scripts over dumps/sots.exe + dumps/functions.json (41,089 starts) edges: every E8 rel32; every E9 rel32 whose target is a function start (lane V2 §5's rule); plus pinned virtual edges: OnAllCombatDone_Tail -> the eleven phase-23/33 vtable targets (V2 §3), DetectEncounters -> GenerateTradeRaidEncounters (slot 10, lane Z), GenerateTradeRaidEncounters | 0x007d5150 -> CreateRaidEncounter (slot 17, lane H) roots: StrategyServer::ProcessTurn 0x007dc6c0 (closure 1,541) StrategyServer::OnAllCombatDone_Tail 0x007d92a0 (closure 1,710) sites: every call whose target is one of the SEVEN RNG entry points (lane I §4) inlined: lane I's image-wide tempering-immediate scan, recall proven complete over all 67 immediate occurrences -- reused, not repeated ``` **Rule 16 is satisfied by reuse, not by a new sweep, and that is deliberate.** Lane I's scan is image-wide with *proven complete recall* (all 67 occurrences of both tempering immediates fall inside decoded instructions; a brute byte scan finds nothing extra). Re-running it could only reproduce it. What lane I's result says here is: **exactly two inlined draw sites lie in the two turn drivers' closures** — `0x007aa3b6` in `EncounterDetect_AssignContacts` (ProcessTurn, depth 4) and one in `0x007a7f30` (Tail, depth 3) — and the other twenty-six sites in nine functions belong to map setup, the lobby screen, the strategy network client/server and two combat encounters, reachable from neither driver (V2 §4.2 resolved all nine ancestries). **One correction to how the brief framed rule 16.** The spy detection roll at `0x00887c8a` is **not an inlined MT draw**. `0x008e6dd0` is `Mars::RNG::Chance`, a real entry point, and `0x00887c8a` is a plain `E8` call to it that any call-graph sweep sees. What is true — and is AC's actual point — is that the call sits in `ServerSpyManager::vslot13`'s **own body** rather than inside `P` (`0x008408e0`), so an *entry probe on `P`* cannot see it. That is an instrument-choice trap, not a recall trap, and the fix is lane Z's return-address ledger. Both traps are real; they are different. **Two site kinds excluded from every row, for the reason lane Z §11.2 gives.** `0x008e6e04` (the `NextFloat` inside `Chance`) and `0x0047d8a6` (the `NextFloat` inside `FloatRange`) are *helper-internal*: their words are already charged to the caller's row. Counting them is the double-count that made the first version of lane Z's site instrument report 44 words against a bracket of 18. **What the sweep cannot see, said plainly.** Indirect edges other than the pinned ones; `call [reg]` through function-pointer members; and any draw in a subtree entered only through an unpinned vtable slot. Every reachability claim below is therefore a **lower bound**, exactly as lane I's and V2's were. --- ## 2. The table Status key: **M** = measured firing with the configuration it was measured at · **P** = decoded predicate on save fields with the corpus counted · **U** = unread gate, the audit's own to-do list. "words" is per call unless stated. `NF`/`NI`/`Ch`/`NU`/`FR` = NextFloat / NextInt / Chance / NextUInt / FloatRange. ### 2.1 `StrategyServer::BeginProcessTurn` — outside both turn drivers | site | container | entry | st | firing, or predicate + corpus count | |---|---|---|---|---| | `0x0052770f` | `SVSOSwarmQueen::RegisterHives` | NI | **M** | **2 words** on `turn1-state.sav` turn 1→2 (two hives created), **0** on turn 2→3; lane L1, VM140, `shim.cfg.l1hive`, oracle reproduced. One word per new hive, *plus* an unmeasured rejection loop (bound 10 ⇒ mask 15 ⇒ E[1.45] words/hive) | ### 2.2 `StrategyServer::ProcessTurn` | site | container | entry | st | firing, or predicate + corpus count | |---|---|---|---|---| | `0x00893426` | `GenerateTradeRaidEncounters` | Ch | **M** | **8 calls / 8 words per turn** on `ref-turn2` (8 players); **7/7** on `zuul-turn23-fleet23` (7 players) — lane Z §11, lane H. One per entry of `StrategyServer::Players`, `TRADE_RAID_ODDS_PLAYER` 0.2f, no back edge | | `0x00893513` | same | Ch | **M** | same, `TRADE_RAID_ODDS_NPC` 0.05f, gated on `0.0f < S->+0x1a0` (player-independent) | | `0x008935ce` | same | Ch | **U** | **0 of 8 turns.** Gated on a subsystem manager being present (`TRADE_RAID_ODDS_REFUGEE`). Gate not decoded to a save field | | `0x0050329d` | `FUN_00503200` ← `DetectEncounters`→`0x007d5150` | NF | **M** | **1 word/turn** on both corpus saves — but lane H: *one word per player passing a per-player gate*, and exactly one player passes on both. **The gate itself is unread**; "1/turn" is a coincidence of two saves (rule 20) | | `0x007929a4` | `FUN_00792750`, same subtree | NI | **M** | same, same caveat | | `0x0088df4f` | `ServerPlayer::RollResearchEvent` | NF | **M** | **0–1 words/turn**, unconditional per player that reaches it (lane T); the 18↔20 spread lane Z reported is this site and `0x00587888` | | `0x00587888` | `TechTree::ProcessResearch` | NF | **M** | **0–1 words/turn** | | `0x005878bb` | same | NF | **U** | never in the ledger. Second research draw; branch condition not decoded | | `0x007aa3b6` | `EncounterDetect_AssignContacts` (**inlined**) | NF | **M** | **0 words, 5 End Turns, 2 saves** — and lane H found *why*: `EncounterDetect_ProcessTeamRecord` is called **zero** times, so the record vector `EncounterDetect_Run` receives is **empty**. The `+0xfc` gate is never evaluated. Cost is `Σ_c min(T_c,|detectors|)`, bounded by `\|contacts\|×\|detectors\|`, **and a trial burns a word even when `thresh = 0.0f` and cannot succeed** | | `0x0088a08f` | `ServerPlayer::RollResearchAccident` | NF | **U** | absent from the ledger on 3 turns. Top-of-function gates not decoded | | `0x00889e40` | same | NI | **U** | " | | `0x00889f86` | same | NI | **U** | " | | `0x00889fd6` | same | NI | **U** | " | | `0x0088a1bd` | same → `FloatRange` | FR | **U** | " (1 word; the two-narrowing float model is pinned in `sots-engine`) | | `0x0088a20f` | same → `FloatRange` | FR | **U** | " | | `0x00889c33` | `FUN_00889bb0` ← `RollResearchAccident` | NI | **U** | " | | `0x00758966` | `ServerSystem::ProcessRebellion` | Ch | **U** | absent from the ledger. Per system; gate not decoded | | `0x0074fe21` | `FUN_0074fbe0` ← rebellion | Ch | **U** | " | | `0x00753d87` | `FUN_00753c60` ← rebellion | Ch | **U** | " | | `0x007564b4` | `FUN_00756350` ← rebellion | Ch | **U** | " | | `0x00747fa8` | `FUN_00747f50` ← `MoveFleet` | NI | **U** | " | | `0x00747ffd` | same | NI | **U** | " | | `0x007b677c` | `ProbabilisticJump` | NF | **U** | unconditional *per call*, and the call is per fleet whose current waypoint is type 5. No corpus save is known to contain a node jump; **the waypoint-type predicate was not decoded here** | | `0x007b67e7` | same | NU | **U** | the *second* word, on the non-arrival branch: `[dest->+0x58->+0x158] < float(unit × [+0x154])`. Neither field is on the wire as far as this lane checked | | `0x0081351c` | `FUN_008134e0` (also in Tail) | Ch | **U** | absent from the ledger on every measured turn; gate not decoded | | `0x008939ee` | `CreateRaidEncounter` | NI | **M** | **entered 2× on Zuul turn 24, 1× on turn 25, 0 words every time** (lane H). The function returns at `0x008938fa` when the candidate vector `FUN_0083b110` builds is **empty** — that gate is *before* this draw. Rule 20's case study | | `0x0088f1eb` | `TradeManager_SpawnEncounterSquadron` ← `0x00892640` ← `CreateRaidEncounter` | NI | **U** | **NEW (§5.1).** Past the empty-list gate; never reached in any measurement | | `0x0088f356` | same | NI | **U** | **NEW.** " | | `0x0083b460` | `FUN_0083b1e0` ← `SpawnEncounterSquadron` | NI | **U** | **NEW.** " | | `0x00848fd9` | `FUN_00848e50` ← `0x00892640` | NI | **U** | **NEW.** " | | `0x00820c1b` | `FUN_00820af0` ← `0x0082ce00` ← `0x00892640` | NF | **U** | **NEW.** " — and the *same site* is reached from the Tail through `Slot13RngCalleeB` (§5.2) | ### 2.3 `StrategyServer::OnAllCombatDone_Tail` | site | container | entry | st | firing, or predicate + corpus count | |---|---|---|---|---| | `0x00820e18` | `TradeManager::Slot13RngCalleeA` ← trade `vslot13` | NF | **M** | **1 word/turn** on `ac-turn22-raider` lineage, **0** on lane L3's turn-15 control; lane AC, VM141, `probes=11` (**not byte-neutral** — the number is entry-attributed and stands, the turn totals do not). Predicate: a fleet parked on a `TradeSector` node with `Pos` bit-equal, owner's bit set in `tscr`, `tsct`/species-4 ⇒ `FtFlg & 0x800`. **Corpus: 1 of 22** (`ac-turn22-raider`; `ac-turn20-raider` has the fleet arriving) | | `0x0088b613` | `TradeManager::Slot13RngCalleeB` | NI | **P** | **§3.3.** Predicate: `A` succeeded **and** ∃ a route whose `trfr` endpoint resolves to the raider's own sector and whose `tro` owner stands at relation ≤ 0 to the raider. **Corpus: 0 of 22 — and the failed conjunct is named**: AC's raider sat on sector `TradeID 832`, whose member systems are indices 13/23/0/3; the four routes' `trfr` are 18/16/14/20, in sectors 816/816/768/752. `B` ran and returned before the draw | | `0x00820c1b` | `FUN_00820af0` ← `Slot13RngCalleeB` | NF | **P** | **NEW (§5.2).** Same predicate as `0x0088b613`, then `NOT (owner species == 1 OR arg-3 species test)`. **Corpus: 0 of 22.** Its existence changes `B`'s cost model from 1 word to **2** | | `0x007ae0a5` | `StrategyServer::NodeLineDecay` (phase 11) | Ch | **M** | **1 word** on Zuul turn 64 (lane Z §9) — one per node line whose lifetime expired, `p = 0.5f`, neither `Chance` early-out | | `0x007bb69b` | `FUN_007bb530` ← `ApplyEncounterResult` (phase 6) | NI | **U** | node-cannon path (`EVENT_NODECANNON_FLINGS`/`_KILLS`). Combat-dependent; **combat measured 0 strategic words** on the one battle ever instrumented (lane Z §10), which is a statement about that battle | | `0x00852ec7` | `FUN_00852d30` ← `0x007a0540` ← `0x007a7f30` (phase 6) | NI | **U** | salvage / back-engineering special project (`EVENT_SPRJBACKENG_UNLOCKED`). Same caveat | | *(inlined)* | `0x007a7f30` `CombatResolve_SalvageBackEng` (phase 6) | NF | **U** | lane I's site #8, the second confirmed rule-16 instance; combat-dependent | | `0x0082cdb8` | `ServerTradeManagerImpl::vslot15` | Ch | **P** | **§3.1 — this is the addiction-spread roll.** Predicate: ∃ an `rt` route with `trfrs == 0`, `trtos == 0`, whose `trfr` system has `nadct > 0`. **Corpus: 0 of 22** — `nadct` is 0 on **all 28 systems of all 22 saves** (616 system records). Words = Σ over such routes of that system's non-zero addiction slots | | `0x0088dc43` | `ServerSpyManager::vslot14` | Ch | **P** | **§3.2 — the spy counter-mission roll.** Predicate: ∃ a `spy` with `deat != 0`, target system owned, target not independent (`hindi` false), and `cm ∈ {1,2,3,4}`. **Corpus: 0 of 22** — the only three saves with a spy all read `deat 0`, `cm 0`, `cmo 0`. `p = cmo` and `Chance` costs **0 words once `cmo ≥ 1`** | | `0x00887c8a` | `ServerSpyManager::vslot13` (call in the caller's body) | Ch | **U** | AC decoded the *chain* around it: `sdet == -1` arm, gated on `FUN_00743f80(system) & 1`. Reached only after `deat != 0`. **Corpus: 0 of 22** by `deat`; the `0x00743f80` conjunct itself is **not** decoded to a save field | | `0x008409c7` | `SpyManager::Slot13RngCallee` (`P`) | NI | **U** | past `deat != 0`, `sdet != -1`, `turn − sdet ≥ 3`, system owned, owner ≠ spy's owner. **Corpus: 0 of 22.** The inner gate on the `NextInt` is unread | | `0x00840929` | same | Ch | **U** | " (the false-flag draw, `0.25f`/`0.75f`) | | `0x00840a3c` | same | Ch | **U** | " | | `0x0081351c` | `FUN_008134e0` (also in ProcessTurn) | Ch | **U** | as above | ### 2.4 SVSO script-object overrides — reachable from both drivers when `S+0x1b4` is non-null Seventeen overrides can reach a draw (V2 §4.3: slot 4 × 11, slot 11 × 4, slot 25 × 2). **All seventeen draw on `GetGame()->+0x16c`** — §4. The whole surface is guarded by `mov edi,[esi+0x1b4]; cmp edi,ebx; je`, and lane T and lane K both recorded `S+0x1b4` as **null in a normal game**. | row | st | firing, or predicate + corpus count | |---|---|---| | `SVSOSlaversRefuel::UpdateDifficultyTier` `0x00515820`, two `+0x16c` sites | **M** | **0 words on 50 consecutive End Turns** (lane L1 §5.1), entered every turn, two stores at frames 2 and 50 exactly as predicted. The draws sit behind the gate that never opened | | the other sixteen | **U** | **Predicate not decoded, corpus not counted.** The one save-visible conjunct that *is* known — `SvSctOb` carrying a live script object of that class — is where the value-domain census reports **16 constants under `Sim.SvSctOb.EncObj`: no corpus game ever triggered a random encounter.** So the corpus count is 0 by construction for any predicate resting on those fields, and the rows stay **unread** rather than negative | ### 2.5 Sites lane I found that are in neither driver Nine functions, twenty-six inlined sites: `0x004b1f20` (CombatNetworkClient slot 3), `0x00507ac0` (CrowRuinsEncounter slot 11, 12 sites), `0x005232a0` (SwarmEncounter slot 11), `0x006ec720` / `0x006f65f0` / `0x006f7890` (StrategyNetworkServer slots 3/4/7), `0x0079f7d0` and `0x007c4140` (StrategyNetworkClient slots 3/6), `0x007c2fa0` (StrategyLobbyScreen slot 3, 4 sites). Reachable from no turn driver over direct + tail-jump edges; V2 §4.2 named every ancestry. **Row status: outside the interval**, not unread — the interval this audit covers is `BeginProcessTurn → post-turn autosave`, and lane Z measured the generator as motionless outside it. --- ## 3. The three unread gates, decoded ### 3.1 `0x0082cdb8` — it is the **addiction spread roll**, and its seed path draws nothing `ServerTradeManagerImpl::vslot15` `0x0082cca0` — phase 23 call 8, dispatched at `0x007d9804`. **Ghidra's size 334 is right here; the body ends at `0x0082cdf0`.** Read in full: ``` routes = this->+0x3c .. +0x40 ; the SERIALISED route vector -- see below for i in 0 .. count-1: rt = routes[i] (bounds-checked, else 0) if (BYTE[rt+0x25] == 0) continue ; G1 pFrom = players[rt->trfow] pTo = players[rt->trtow] ; via [this+4]+0x50 = S+0x54 if (rt->trtos == 0 && pTo != pFrom): ; --- BLOCK 1, NO DRAW --- sysTo = systems[rt->trto] ; via GetGame()->+0x44 for k in 0..6: if ((pFrom->[0x348 + 4k] >> 4) & 1) BeginAddiction(sysTo, k) if (rt->trfrs != 0) continue ; G2 if (rt->trtos != 0) continue ; G3 sysFrom = systems[rt->trfr] sysTo = systems[rt->trto] for k in 0..6: ; --- BLOCK 2 --- if (sysFrom->addiction[k] != 0) ; G4 if (Chance(GetGame()->rng, *ADDICTION_SPREAD_ODDS)) ; <<< 0x0082cdb8, 1 WORD BeginAddiction(sysTo, k) ``` **How every tag was pinned, so this can be checked rather than believed.** * `this+0x3c/+0x40` is the **route** vector and `this+0x1c/+0x20` the **sector** vector, from `ServerTradeManagerImpl::Write` `0x0082cb60`, whose `this` is the sub-object at **+0x4c** (`vtable_map.py who 0x0082cb60` → `0x00a31b64 +0x4c slot 2`): it writes `NumTradeSectors` from `[this−0x30] = obj+0x1c`, `SctSize` from `[this−0x44] = obj+0x08`, and `rt` frames from `[this−0x10] = obj+0x3c`. The base ctor `0x00841bf0` zeroes four 0x10-byte vectors at `+0x0c/+0x1c/+0x2c/+0x3c` and loads `+0x08` from `TRADE_SECTOR_SIZE` (image default **7.0**). * **`BYTE[rt+0x25] != 0` is exactly the condition `Write` uses to decide whether to emit an `rt` record** (`0x0082cc3d`, byte-for-byte the same test as `0x0082ccd4`). So G1 is not merely satisfiable — it is satisfied by **every `rt` in every save, by construction.** * `Game::TradeRoute` field offsets are the campaign's own recovered layout (`objects/layouts.md`, grade *verified*): `+0x04 tro`, `+0x08 trfow`, `+0x0c trfr`, `+0x10 trfrs`, `+0x14 trtow`, `+0x18 trto`, `+0x1c trtos`, `+0x20 trtc`. `FUN_00841cd0` settles what `trfr`/`trfrs` mean: `if (rt->trfrs == 0) endpoint = GetGame()->systems[rt->trfr] else endpoint = registry_lookup(S+0x84, rt->trfrs)`. **`trfr` is a system index; `trfrs` is a non-system endpoint id, 0 meaning "use the index".** * `system+0x1e4 .. +0x1ff` is the **`int[7]` addiction table** serialised as `nadct` (count of non-zero entries) then sparse `(ads = index, adt = value)` pairs — `findings/objects/struct-recovery.md`. `FUN_00745e40(system, k)` returns `system->addiction[k] != 0` (its 0/1/2/3 phase computation from `ADDICTION_PHASE2_START` = 10 and `ADDICTION_PHASE3_START` = 15 is discarded by the `setne al` the caller reads). * `*ds:0x00af0c50 → 0x00af0c4c` is the StrategyVar **`ADDICTION_SPREAD_ODDS`**, image default **0.2f**, resolved from its `GlobalConst` static-init stub (the M1 loader, `loader-prototypes.md`). 0.2 takes neither `Chance` early-out, so the site costs **exactly one word per evaluation**. * `FUN_0074ef70` = `ServerSystem::BeginAddiction(k)`: refuses if the system is unowned, if bit 5 of `owner->[0x348+4k]` is set, or if the slot is already stamped; otherwise posts `EVENTSUM_ADDICTION_OUTBREAK` / `EVENTMSG_ADDICTION_OUTBREAK` and writes `system->addiction[k] = GetGame()->Frame`. **It draws nothing.** **Predicate on save fields, and the corpus counted against it** > Words = Σ over `rt` records with `trfrs == 0 ∧ trtos == 0` of `nadct` on the system at index `trfr`. | save | `rt` records | `trfrs`/`trtos` | systems with `nadct > 0` | words | |---|---|---|---|---| | `human-turn11-spytechs` | 1 | 0 / 0 | **0 of 28** | 0 | | `human-turn15-spyprogram` | 1 | 0 / 0 | **0 of 28** | 0 | | `ac-turn20-raider` | 3 | 0 / 0 | **0 of 28** | 0 | | `ac-turn22-raider` | **4** | 0 / 0 | **0 of 28** | 0 | | the other 18 saves | 0 | — | 0 of 28 | 0 | **Corpus count: 0 of 22.** The failed conjunct is **G4** — not the route container, which four saves have, and not `trfrs`/`trtos`, which are 0 on all nine route records. **No system in the corpus is addicted to anything.** **And the writer is found (rule 28 practice 3), which is the interesting part.** `ServerSystem::BeginAddiction` has **exactly two callers, both inside `vslot15` itself** — BLOCK 1 and BLOCK 2. So the *only* way any system becomes addicted is this one function, and **BLOCK 2 cannot run until BLOCK 1 has seeded it**. BLOCK 1 needs: * a route with `trtos == 0` whose `trfow != trtow` — an **inter-empire** trade route; every route in the corpus has `trfow == trtow == 1`, so BLOCK 1 has never run either; * the *from* owner holding **bit 4** of `player->[0x348 + 4k]`. `player+0x348` is written by `ServerPlayer::RebuildSpeciesTechFlags` `0x0082bf10` (already named in `addresses.json`): for each of the seven species `k` it reads eight tech ids from `SpeciesDef[k]+0x78 .. +0x94`, tests each with `TechTree_HasResearched`, and sets or clears the corresponding bit. `InitTable` `0x005453a0` fills `SpeciesDef+0x88` — **bit 4** — from `FUN_0053b620`, the per-species **addict tech** table, whose out-of-range arm logs `"Species %s: No addict tech."`: | species k | 0 | 1 | 2 | 3 | 4 | 5 | 6 | |---|---|---|---|---|---|---|---| | addict tech id | 0x2799 | 0x279a | 0x279b | 0x279c | **0xC5 = none** | **0xC5 = none** | 0x279d | (0xC5 = 197 is the image's "no tech" sentinel; these are the 10000-based `TechID` enum, **not** wire `techId`s, so `tools/techid_table.py` does not name them.) **Recipe for whoever holds a guest next, named from the failed conjunct and not from the nearest container:** research a species' addict tech (bit 4), then establish a trade route **between two different empires** (`trfow != trtow`) — BLOCK 1 then stamps the destination system's addiction slot with no draw, and on the *following* turn any route out of that system rolls `0x0082cdb8` once per addicted slot. Building more trade routes inside one empire, which is what the corpus has four of, can never do it. ### 3.2 `0x0088dc43` — the spy **counter-mission** roll, with an accumulating probability `ServerSpyManager::vslot14` `0x0088db80` — phase 33 call 10, dispatched at `0x007d989b`. **Rule 17 bites again: Ghidra reports 387 bytes, ending at `0x0088dd03`, which is inside the function. The real body ends at `0x0088dd2b`, followed by a five-entry jump table at `0x0088dd2c`.** ``` for each spy in this->+0x10 .. +0x14: if (spy->deat == 0) continue ; G1 sysT = registry_lookup(S+0x84, spy->deat) ; the TARGET system sysO = registry_lookup(S+0x84, spy->sown) if (ServerSystem_GetOwner(sysT) == NULL) continue ; G2 if (sysT->+0x1c8 != 0) continue ; hindi -- independent ; G3 if (spy->spyon != 0 && spy->spyon != ownerId(sysT)): ; spy->sdo = 0.0f; spy->sdet = -1 ; owner changed if (spy->cm != 0): if ((unsigned)(spy->cm - 1) > 3) continue ; G4: cm in {1,2,3,4} if (Chance(GetGame()->rng, spy->cmo)) ; <<< 0x0088dc43 else spy->cmo = (float)(spy->cmo + 0.2) ; ds:0x009e20d8, a WIDENED 0.2f (rule 23) else: if (spy->ncp is non-empty) continue if (GetGame()->Frame - spy->tdep < 3) continue spy->spyon = ownerId(sysT); spy->spyat = ... cm) 0->1, 1->(owner->CnTrd ? 2 : 3), 2->3, 3->4, 4->0; spy->cmo = (cm ? 0.2f : 0.0f)> ; ds:0x009e1fc0 = 0.2f ``` **Cost model, which matters more than the predicate.** `cmo` starts at `0.2f` and gains `0.2f` per failed turn. `Mars::RNG::Chance` **draws nothing when `p ≥ 1`** (lane I §4). So one counter-mission stage costs **at most 4 words** and then succeeds for free on the fifth turn: | turn of the stage | 1 | 2 | 3 | 4 | 5 | |---|---|---|---|---|---| | `cmo` | 0.2 | 0.4 | 0.6 | 0.8 | ≥ 1.0 | | words | 1 | 1 | 1 | 1 | **0** | A reimplementation that models `Chance` without the `p ≥ 1` early-out is one word out of step on every fifth turn of every counter-mission. **Predicate and corpus count** > ∃ a `spy` with `deat != 0` **and** `cm ∈ {1,2,3,4}`, whose `deat` system is owned and has > `hindi == false`. | field | across the corpus | |---|---| | `nspy` | 1 in `human-turn15-spyprogram`, `ac-turn20-raider`, `ac-turn22-raider`; **0** in the other 19 | | `deat` | **0** in all three spy records | | `cm` | **0** in all three | | `cmo` | **0** in all three | | `sdet` | **−1** in all three | | `hindi` | true on exactly **1 of 28** systems, in 10 of 22 saves — so G3 is *not* the binding conjunct | **Corpus count: 0 of 22.** The failed conjunct is **G1 (`deat == 0`)** and, one step behind it, **`cm == 0`**. This agrees exactly with the value-domain census, which lists `deat`, `cm`, `cmo`, `cbh`, `sdo` among the 234 fields that have only ever held one value; the count is therefore 0 **by construction**, not by inspection. **What lane AS needs, named from the failed conjunct.** `deat` is the spy's *target system handle* and it is not `spyon`. Both `spyon` and `cm` are written **by `vslot14` itself** once `deat` is set and `Frame − tdep ≥ 3` with `ncp` empty — so the whole counter-mission machine bootstraps from `deat` alone. That makes `deat`'s writer the single question worth answering before the workload is built; this lane did not find it (§7). **One caution for lane AS.** `cm`'s stage machine consults `owner->+0xff` — the field AC identified as **`CnTrd`** (FTL Economics) — when advancing 1 → 2 or 1 → 3. A spy program run without FTL Economics takes a different branch of the machine and reaches a different stage sequence. ### 3.3 `0x0088b613` — the raid **target pick**, and why `B` cost 0 words when it fired `TradeManager::Slot13RngCalleeB` `0x0088b440`, called at `0x0088f042` immediately after `A` returns true. Ghidra's 1,323 is right (body ends `0x0088b974`). ``` if (fleet == 0) return if (FUN_0080ec50(fleet)) fleet->Pos = sector->Pos ; snap sector = StarFleet_GetLocationAsTradeSector(fleet); if (!sector) return flag = FUN_0080ecf0(fleet) ; see below -- ALWAYS FALSE here cand = {} for each rt in this->+0x3c..+0x40: if (rt == 0) continue if (rt->trfr < 0 || rt->trfr >= count(this->+0x0c..+0x10)) continue ; G_B1a if (((void**)this->+0x0c)[rt->trfr] != sector) continue ; G_B1b if (flag) { if (rt->tro != fleet->owner->id) continue } ; unreachable arm else { owner = registry_lookup(S+0x84, rt->tro); if (!owner) continue ; G_B2 if (PlayerAlliances_Relation(raider->+0x28, &raider[0x168], owner->+0x28) > 0) continue ; G_B3 } cand.push_back(rt) if (cand.empty()) return ; G_B4 n = cand.size() bound = n - 1 target = cand[ RNG_NextInt(GetGame()->rng, &bound) ] ; <<< 0x0088b613, >= 1 WORD if (!FUN_00820af0(this, &out, target, fleet)) return ; <<< draws again, 0x00820c1b (§5.2) fleet->Pos = out if (!flag) for each player p with p->+0x5c != 4 and (sector->tssec >> p->+0x28) & 1: ``` **`RNG_NextInt` has no zero-bound early-out.** Read in full (`0x004271c0`, 139 bytes, Ghidra's size correct): it smears `*bound` down into a mask and then enters the draw loop **unconditionally**, rejecting while `(y & mask) > *bound`. With `*bound == 0` the mask is 0, the first word is accepted, and the call costs **exactly one word**. **So a single-candidate list still draws.** **That settles what lane AC measured.** AC saw `Slot13RngCalleeB` **entered once** on the turn `A` succeeded and the tail delta was **1 word**, all of it `A`'s. Since `B` cannot reach `0x0088b613` without spending a word, **`B` returned before the draw**, and the only early return past the sector lookup is **G_B4: the candidate list was empty.** That is the failed conjunct, and it is not a container that anyone had been staring at. **Why the `flag` arm is dead on this path.** `FUN_0080ecf0(fleet)` returns `fleet is at a TradeSector ∧ ¬(runtime flag 0x80000) ∧ TradeSector_PlayerTradesHere(sector, owner) ∧ ¬(FtFlg & 0x800)`. But `vslot13`'s G3 (`TradeSector_FleetMayRaid` `0x0080ed50`, AC §2.1) requires the *opposite* on the last two: if `PlayerTradesHere` is true it demands `FtFlg & 0x800`, and if it is false the `flag` computation returns false at its own second test. **Any fleet that reaches `B` through `vslot13` therefore has `flag == false`, deterministically** — so the diplomacy branch is the live one and the "raid your own routes" branch is unreachable from the tail. **Corpus count, and the arithmetic that names the next workload** Read out of `ac-turn22-raider.sav` — the only corpus state where `A` has ever succeeded: | sector `TradeID` | `tsct` | member systems (indices) | fleets present | |---|---|---|---| | 752 | 2 | 1, 10, 20, 24, 17 | `tsflt 6352` | | 768 | 2 | 2, 7, 14, 21, 22, 27 | `tsflt 5712` | | 784 | 0 | 4, 8, 12, 25 | — | | 800 | 0 | 6, 11, 5 | — | | 816 | 2 | 9, 15, **16**, **18**, 19, 26 | `tsflt 2736` (the AI's `Freighters`), `1010` | | **832** | **0** | **13, 23, 0, 3** | `tsflt 3744` — **AC's raider** | The four routes' `trfr` are **18, 16, 14, 20** → sectors **816, 816, 768, 752**. **None is 832.** So on the one state in which `A` has ever fired, `B`'s candidate list was empty *because the raider was parked on the one sector in the game that no trade route is sourced in*. **G_B3 passes on that state, so it is not competing for blame.** `PlayerAlliances_Relation` `0x006d2050` is already decoded in the campaign (lane T2): **3 = allied *and self*, 2 = non-aggression, 1 = cease-fire, 0 = war**, and the mask bit is the player's *index field*. `B` rejects a route when the relation is **> 0**, i.e. **only a route whose owner you are at war with may be raided** — and self returns 3, which is the second, independent reason the "raid your own routes" arm is dead. In `ac-turn22-raider.sav` both player 0 (the raider) and player 1 (`tro = 32`, the route owner) carry `AL = NA = CF = 0`, so the relation is **0 = war** and G_B3 passes. Only G_B1b failed. **Corpus count: 0 of 22.** **The next workload, named from the failed conjunct.** From `ac-turn22-raider.sav`, move the raider from sector 6 (`TradeID 832`) to **sector 5 (`TradeID 816`)**. Then: * `vslot13` G2 ✓ (fleet at a `TradeSector` node), G3b ✓ (`tscr = 253`, bit 0 = the human player), G3c ✓ — `tsct = 2` is bit 1, the *AI's* bit, so `TradeSector_PlayerTradesHere(sector, player 0)` is **false** and **no `FtFlg & 0x800` is required**, exactly as on sector 832; * G4 after arrival (`Pos` bit-equal, two End Turns); * `A` rolls; on success `B` finds **two** candidates (`trfr` 16 and 18 both lie in sector 816), draws `NextInt(bound = 1)` — mask 1, **no rejection possible, exactly 1 word** — then `FUN_00820af0` draws a `NextFloat` unless the species short-circuit fires (§5.2); * free rider: sector 816 also holds the AI's `Freighters` fleet, which AC read statically as **doubling** the raid odds — the first state that can test that arithmetic. **Committed prediction, so this is falsifiable:** on that state, `OnAllCombatDone_Tail` costs **1 word** on a turn `A` fails and **3 words** on a turn `A` succeeds (`A` 1 + `B` 1 + `0x00820af0` 1), with `B` entered exactly on the succeeding turns. **The one thing this decode rests on that was not proven.** `this+0x0c` is a 0x10-byte vector indexed by `rt->trfr`, which `FUN_00841cd0` proves is a **system index**, and whose elements `B` compares for pointer identity against a `TradeSector*`. The only coherent reading is a **per-system "containing trade sector" table**, and `TradeManager_SpawnEncounterSquadron`'s caller uses the same lookup and then reads `->+0x4`, which for a `TradeSector` is its `TradeID` (that is the field `Write` emits under the `TradeID` tag). **This lane did not find the code that fills `+0x0c`.** If it is instead a *sector*-indexed vector of six, then `trfr ∈ {14,16,18,20}` fails the bounds check G_B1a on every corpus route and `0x0088b613` is **unreachable in principle**. The two readings are distinguished by exactly the experiment above: parking on sector 816 makes the draw fire under the first reading and not under the second. **Whoever runs it should report the outcome as a verdict on this paragraph, not only as a word count.** --- ## 4. The SVSO generator — named, and it is the strategic one Lane V2 §4.3 left this open: *"the draws are at depth 2–4 and the generator arrives as an argument, so **which** generator these use is not established."* **It is `GetGame()->+0x16c` — `StrategyServer`'s own generator, the one the save serialises — for all seventeen overrides V2 lists as reaching a draw.** Method: rebuild the direct+tail-jump closure of each of the seventeen slot-4/11/25 targets, find every `mov r32,[r32+0x16c]` in it, and read back where the base register came from. | the seventeen | how the generator arrives | |---|---| | CrowDefenders s11, CrowRuins s11, Ortgay s11, Swarm s4, SwarmQueen s25, SlaversRefuel s4 | `call GetGame` (`0x00578050`) immediately before the `+0x16c` load, in `0x005108c0` / `0x00518340` / `0x004fb280` / `0x00515c20` / `0x0051ad70` / `0x00515820` | | HiverInvasion s4, HolyLands s4, JewelsOfTheCrown s4, ProgressionWars s25, PuppetMaster s4, Sots s4, SystemKiller s4, UpstartApes s4, VonNeumann s11 | through the two global draw wrappers `FUN_00579500` (`NextInt`) and `FUN_00579ae0` (`NextFloat`), which both do `mov ecx,0xb29f98; call 0x005f6450` — the inlined body of `GetGame` — then `[eax+0x16c]` | | Tournament s4 | `FUN_0054a550`, whose `esi` carries `+0x44/+0x48` (the systems vector) — the `StrategyServer` itself | | CivilWar s4 | `FUN_00554f40` → `call GetGame; mov ecx,eax; call 0x00786200 / 0x00786230`, and those read `[ecx+0x16c]` before `Mars_RNG_GaussianRange` | `GetGame` `0x00578050` is `mov ecx,0xb29f98; jmp 0x005f6450` and `0x005f6450` is `mov eax,[ecx+4]; ret` — i.e. `*(void**)0x00b29f9c`, a singleton whose `+0xc` is the frame and `+0x16c` the generator. Lane L1 measured `SVSOSwarmQueen::RegisterHives` — which reaches its generator by the same `GetGame()->+0x16c` — moving the **strategic** word position, and the values it produced matched two historical saves. That is an independent live confirmation of this static reading. **Two false positives worth recording so the next lane does not repeat them.** A `+0x16c` load is **not** evidence of a generator on its own: `FUN_006a8650` and `FUN_0074d1c0` use `+0x16c`/`+0x170` as a `std::vector` begin/end pair (`sub eax,[esi+0x16c]; sar 2`). Image-wide, 145 functions contain a `[reg+0x16c]` load; the generator claim has to come from what the pointer is *used for*. --- ## 5. Sites the firing-indexed table structurally could not contain ### 5.1 Five draw sites behind `CreateRaidEncounter`'s empty-candidate-list gate `ServerTradeManager::CreateRaidEncounter` `0x008938a0` builds a candidate vector with `FUN_0083b110` and returns `false` at `0x008938fa` when it is **empty** — before every draw in the function. Lane H measured it **entered 2× on Zuul turn 24 and 1× on turn 25, 0 words each time**, which is the campaign's own case study for rule 20. What the ledger could then record was one site (`0x008939ee`). Past the gate there are **five more**, in a subtree nobody has enumerated: ``` CreateRaidEncounter 0x008938a0 @0x008939ee NextInt (known) @0x00893a25 -> 0x00892640 -> TradeManager_SpawnEncounterSquadron 0x0088f070 @0x0088f1eb NextInt @0x0088f356 NextInt -> 0x0083b1e0 @0x0083b460 NextInt -> 0x00848e50 @0x00848fd9 NextInt -> 0x0082ce00 -> FUN_00820af0 @0x00820c1b NextFloat ``` Each `NextInt` carries its own rejection loop, so the cost of one *successful* raid encounter is **unbounded above** and is certainly not "0 or 1 further word". **Correction, rule 11.** `tail-rng-ledger.md` §11.1 says *"a success costs 0 or 1 further word"*, and lane H's insert reads *"a successful raid roll costs 0 further words on our corpus"*. The second is right about the corpus and neither is right about the code: **a success whose candidate list is non-empty costs at least six draws**, and the standalone must model that before the first raid encounter is ever created. Both sentences should gain a pointer here. ### 5.2 `0x00820c1b` — a second draw inside `Slot13RngCalleeB`'s own subtree `FUN_00820af0(TradeManager*, Vector3* out, TradeRoute* rt, ...)` computes the interception point along the raided route: ``` owner = registry_lookup(S+0x84, rt->tro) a = systems[rt->trfr]->Pos b = systems[rt->trto]->Pos dist = |b - a| - 0.0 if (owner->+0x5c == 1) frac = 1.0 ; species 1 **[POLARITY CORRECTED 2026-09-09 by lane AZ (rule 11): the short-circuit arm sets `frac = 0.0`, NOT 1.0 (`fldz; fstp [ebp+0xc]` at 0x00820bf8); the DRAW arm computes `0.0 + (1.0-0.0)*NextFloat()`. So a Human/Zuul/Hiver interception happens at the raided route's SOURCE end, not its destination. Word counts unaffected; the sentence was wrong. Rule 28 practice 6 - read the polarity before the count.]** else if (FUN_0081a2d0(arg3) == 1) frac = 1.0 else if (FUN_0053baf0(SpeciesDef(...))) frac = 1.0 else frac = 0.0 + (1.0 - 0.0) * NextFloat() ; <<< 0x00820c1b, 1 WORD out = a + dir * (frac*dist + ) ``` It is called from **two** places: `0x0082cf65` (inside the `CreateRaidEncounter` subtree, §5.1) and **`0x0088b62c` — `Slot13RngCalleeB`, immediately after the target pick**. So `B`'s cost on success is `1 (NextInt) + 1 (NextFloat)`, minus the species short-circuit. **This should change lane AD's committed predictions before its build.** The resolution's §4 lists *"`B` costs 0 on every success"* as a prediction to commit; on this reading `B` costs **2** on a success with a non-empty candidate list, and 0 only because AC's list was empty. AD's probe is worth running exactly as briefed — it just needs the third hypothesis in the sheet. --- ## 6. The audit's own to-do list — the blank cells, listed **Thirty-one rows are unread gates** — thirty distinct draw sites plus the grouped SVSO row. They are the value of this table; the firing-indexed one could not show them. | group | rows | what is missing | |---|---|---| | `ServerPlayer::RollResearchAccident` `0x00889dc0` and its subtree | 7 (`0x0088a08f`, `0x00889e40`, `0x00889f86`, `0x00889fd6`, `0x0088a1bd`, `0x0088a20f`, `0x00889c33`) | lane I recorded "all behind top-of-function gates" and nobody has decoded them. **Highest-value unread group** — it is per player, in `ProcessTurn`, and would fire on an ordinary save | | the rebellion subtree | 4 (`0x00758966`, `0x0074fe21`, `0x00753d87`, `0x007564b4`) | per system; no corpus save has ever had a rebellion | | `MoveFleet` / `ProbabilisticJump` | 4 (`0x00747fa8`, `0x00747ffd`, `0x007b677c`, `0x007b67e7`) | needs a fleet on a **node line** (waypoint type 5). `human-turn3-noderoute` and `zuul-turn16-noderoute` exist and were never bracketed for this | | `CreateRaidEncounter` past the gate | 5 (§5.1) | needs a **non-empty raid candidate list** — the same workload as §3.3 | | the spy chain | 4 (`0x00887c8a`, `0x008409c7`, `0x00840929`, `0x00840a3c`) | corpus 0 by `deat`; the inner gates are unread. Lane AS's ground | | SVSO overrides other than SlaversRefuel | 1 grouped row (16 targets) | `S+0x1b4` null in a normal game; `SvSctOb.EncObj` has 16 census constants | | the second research draw `0x005878bb` | 1 | branch condition inside `TechTree::ProcessResearch` | | the refugee raid roll `0x008935ce` | 1 | "a subsystem manager is present" — not yet a save field | | `FUN_008134e0` `0x0081351c` | 1 site, listed under both drivers | in **both** drivers, never in the ledger, gate not decoded | | combat phase 6 | 3 (`0x007bb69b`, `0x00852ec7`, and the inlined site in `0x007a7f30`) | combat-dependent; one battle has ever been instrumented | Three rows are **P with corpus count 0 and a recipe attached** (§3.1, §3.2, §3.3) — rule 6 with an instruction sheet, which is a different thing from a negative. --- ## 7. What this lane did not settle * **`ServerTradeManager+0x0c`'s writer was not found.** §3.3's last paragraph states the consequence and the experiment that decides it. This is the one load-bearing inference in the document and it is flagged as one. * **`SpyCraft.deat`'s writer was not found.** It is the single conjunct that gates the entire spy half, and per rule 28 practice 3 a field that reads 0 in all 22 saves is a corpus coincidence until its writer is named. Lane AS should find it before building anything. * **`FUN_00743f80(system) & 1`** — the conjunct on AC's inline detection roll `0x00887c8a` — was not decoded to a save field. * **The diplomacy conjunct is decoded but never exercised.** `PlayerAlliances_Relation` was already read by lane T2 and G_B3 resolves to "at war"; the corpus satisfies it only because nobody has ever signed anything (`Sim.Player.dipstats` holds **11 census constants** — no corpus game reached a treaty). So the §3.3 workload gets G_B3 for free today and would *lose* it the moment a lane builds the treaty workload. Worth stating in AD's sheet as a conjunct that currently passes by accident. * **Which of `RebuildSpeciesTechFlags`' eight bits is bit 5** (the one that blocks `BeginAddiction`) was not pinned; only bit 4 was, through `InitTable`'s `SpeciesDef+0x88` slot. `EVENTSUM_ADDICTION_TEMPERENCE` exists in the image and is the obvious lead. * **Nothing here was measured.** Every "M" row is another lane's measurement, cited with the configuration it was taken at. In particular every AC number is `probes=11`, which lane H bisected as **not** byte-neutral; the tail delta is entry-attributed and stands, the turn totals are not calibration data, and no AC autosave is an oracle pair. * **The corpus is 22 saves, not 20.** The brief says twenty; `verify/results/saves/` holds twenty-two `.sav` files and the value-domain census counts the same twenty-two. Every count in this document is over those twenty-two, listed by name in §3.1's table plus the eighteen with no `rt` record. --- ## 8. Artefacts | what | where | |---|---| | this document | `findings/control-flow/gate-indexed-rng-audit.md` | | new addresses (12 entries, no name collides with an existing one) | `ghidra/addresses.d/ag.json` | | nothing was written to Ghidra, no VM was used, no build directory was created | — | --- ## Proposed board rows New row: ``` | GATE-INDEXED RNG AUDIT: 31 unread-gate rows, 6 new draw sites, the SVSO generator named | control-flow | verified | high | 100% | 2026-09-09 | **Lane AG**, host only, no VM, no Ghidra (`findings/control-flow/gate-indexed-rng-audit.md`). The resolution's §8 item 3, delivered: one row per STATICALLY REACHABLE draw site in `BeginProcessTurn` u `ProcessTurn` u `OnAllCombatDone_Tail` over direct + tail-jump + pinned-virtual edges, each carrying a MEASURED firing or a DECODED PREDICATE on save fields with the 22-save corpus counted against it. **NO TOTAL IS REPORTED - 31 rows (30 distinct sites + one grouped SVSO row) are UNREAD GATES**, and that visible blank column is the deliverable. THREE GATES DECODED. (1) `0x0082cdb8` is the **ADDICTION SPREAD ROLL**: `ServerTradeManagerImpl::vslot15` iterates the SERIALISED route vector (its G1, `BYTE[rt+0x25]!=0`, is byte-for-byte the test `Write` uses to emit an `rt`), and for each route with `trfrs==0 && trtos==0` rolls `Chance(ADDICTION_SPREAD_ODDS=0.2f)` once per species slot the FROM system is addicted to. Corpus **0 of 22**: `nadct` is 0 on all 28 systems of all 22 saves. WRITER FOUND (rule 28 practice 3): `ServerSystem::BeginAddiction 0x0074ef70` has EXACTLY TWO CALLERS, both inside vslot15 - so BLOCK 1 (an INTER-EMPIRE route, `trfow != trtow`, plus bit 4 of `player+0x348+4k`, which `RebuildSpeciesTechFlags` sets from the per-species ADDICT TECH table `FUN_0053b620`) must seed it before the roll can ever happen. Every corpus route is intra-AI (`trfow==trtow==1`), so neither block has run. (2) `0x0088dc43` is the SPY COUNTER-MISSION roll, `p = spy->cmo` starting at 0.2f and gaining 0.2f per failure - so `Chance` costs 0 WORDS from the fifth turn on (`p>=1` early-out), a stage costs at most 4 words. Predicate `deat != 0 AND cm in {1..4}`; corpus **0 of 22** by construction (both fields are among the census's 234 constants). (3) `0x0088b613` is the RAID TARGET PICK, and **`RNG_NextInt` HAS NO ZERO-BOUND EARLY-OUT** (read in full: mask smear then an unconditional draw loop) - so a single-candidate list still costs a word, which PROVES lane AC's `B` returned BEFORE the draw. The failed conjunct is named: AC's raider sat on sector `TradeID 832`, member systems 13/23/0/3, while the four `rt` records' `trfr` are 18/16/14/20, lying in sectors 816/816/768/752. **NEXT WORKLOAD: park the raider on sector 816 instead of 832** - two candidates, `NextInt(bound=1)` cannot reject, `tsct=2` is the AI's bit so no `FtFlg&0x800` is needed, and the AI's Freighters fleet is already there to test the doubled odds. SIX NEW DRAW SITES the firing-indexed table structurally could not hold: five behind `CreateRaidEncounter`'s empty-candidate-list gate (`0x0088f1eb`, `0x0088f356`, `0x0083b460`, `0x00848fd9`, `0x00820c1b` via `0x00892640`) and `0x00820c1b` AGAIN from `Slot13RngCalleeB` at `0x0088b62c`. **SVSO GENERATOR CLOSED**: all 17 overrides that can draw use `GetGame()->+0x16c` = `*(void**)0x00b29f9c` -> +0x16c, the STRATEGIC generator - V2 §4.3 caution (a) retired. Rule 17 bit twice more (`ServerSpyManager::vslot14` real end 0x0088dd2b vs Ghidra's 0x0088dd03, jump table past it) | ``` Edits to existing rows: - **Row 186** (`RNG LEDGER CLOSED - zero residual`) — append: `GATE-INDEXED SUCCESSOR 2026-09-09 (lane AG): the site set is now enumerated by GATE rather than by firing - 43 entry-point call sites plus 2 inlined sites inside the two turn drivers' closures over direct + tail-jump + pinned-virtual edges, of which 30 distinct sites are UNREAD GATES. Six were not in any earlier inventory. No per-turn total is claimed.` - **Row 207** (V2's eight indirect sites) — append: `THREE OF THE FOUR UNREAD GATES ARE NOW DECODED (lane AG): 0x0082cdb8 = addiction spread (corpus 0 of 22, `nadct` 0 everywhere, and its seed path needs an INTER-EMPIRE route plus a species addict tech); 0x0088dc43 = spy counter-mission, p = cmo accumulating 0.2f/turn so Chance costs 0 words once p>=1 (corpus 0 of 22 by `deat`/`cm`); 0x0088b613 = raid target pick, and NextInt has no zero-bound early-out so B provably returned before it - the failed conjunct is the raider's SECTOR, not any container. Still unread: 0x00887c8a's `FUN_00743f80 & 1` conjunct and the three sites inside P.` - **Row 399** — append: `AND lane AC's "three `rt` route records" is FOUR in ac-turn22-raider.sav (three in ac-turn20-raider); corrected by direct read, lane AG.` - **The `CreateRaidEncounter` clause of row 186 / `tail-rng-ledger.md` §11.1** — `"a success costs 0 or 1 further word"` is wrong about the CODE (right about the corpus): a success with a non-empty candidate list reaches FIVE more draw sites through 0x00892640, each NextInt with its own rejection loop. Lane AG §5.1. - **Lane AD's brief** (resolution §4/§7) — one prediction to add before the build: **`B` costs 2 words on a success with a non-empty candidate list** (`NextInt 0x0088b613` + `NextFloat 0x00820c1b` in `FUN_00820af0`, unless the species short-circuit fires), not 0. AC's 0 was an empty list, not a free success.