One row per statically reachable draw site in BeginProcessTurn u ProcessTurn u
OnAllCombatDone_Tail, each carrying a measured firing or a decoded predicate on
save fields plus the 22-save corpus count. No total is reported: 31 rows (30
distinct sites plus one grouped SVSO row) are unread gates, and that visible
blank column is the deliverable.
Three gates decoded:
0x0082cdb8 = the addiction spread roll (Chance ADDICTION_SPREAD_ODDS 0.2f);
corpus 0 of 22 -- nadct is 0 on all 28 systems of all 22 saves. Writer found:
ServerSystem_BeginAddiction has exactly two callers, both inside vslot15, so
an INTER-EMPIRE route plus a species addict tech must seed it first.
0x0088dc43 = the spy counter-mission roll, p = cmo accumulating 0.2f per turn,
so Chance costs 0 words from the fifth turn on; corpus 0 of 22 by deat/cm.
0x0088b613 = the raid target pick. RNG_NextInt has no zero-bound early-out, so
a single-candidate list still draws -- which proves lane AC's B returned
before the draw. Failed conjunct named: the raider sat on sector 832 while
all four rt records source in 816/816/768/752. Next workload: sector 816.
Six draw sites no firing-indexed table could hold: five behind
CreateRaidEncounter's empty-candidate-list gate, and 0x00820c1b again from
Slot13RngCalleeB -- so B costs 2 words on a success, not 0.
SVSO generator closed: all 17 drawing overrides use GetGame()->+0x16c.
Rule 17 again: ServerSpyManager_vslot14 really ends at 0x0088dd2b, not 0x0088dd03.
630 lines
48 KiB
Markdown
630 lines
48 KiB
Markdown
# 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)):
|
||
<reset>; 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
|
||
<mission complete: FUN_00888870, advance stage>
|
||
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 = ...
|
||
<fall through to the stage machine>
|
||
<stage machine: switch(spy->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: <post news>
|
||
```
|
||
|
||
**`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
|
||
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 + <double @0x009e20a0>)
|
||
```
|
||
|
||
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.
|