diff --git a/campaign/board.md b/campaign/board.md index c36d773..9ecd6f3 100644 --- a/campaign/board.md +++ b/campaign/board.md @@ -59,7 +59,7 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | P2-B3 ProcessResearch (behavioral, RNG) | phase2 | mapped | high | 85% | 2026-09-08 | LIVE, PARTIAL PASS: 15 calls compared, 13 zero-divergence; RNG post-state matched 14/15 incl. every roll (validates MT19937 + draw mapping + odds together). 2 divergences are the declared SetResearched boundary. ORACLE FAILS by exactly one item across 40,300: an unposted EVENT_RESEARCH_OVERBUDGET - compare was blind because the event list was never a declared region. fpu_cw=0x127f => 53-bit double, x87 question SETTLED. No Zuul in the save: double roll still disassembly-only . RECAPTURED WITH GUARDS 2026-09-08 (lane R): the oracle gap is now a COMPARE DIVERGENCE - `side.events.after.v.next_id orig=4 ours=3` on call 0, its only divergent field, with node[144] progress 2879->5768 and flag 1->2 both reproduced and the single RNG draw identical. 15 calls over 5 turns: 3 diverged, **RNG 15/15** (better than the original 14/15 - no tech-effect draw in this session). Guards on the two completion calls map SetResearched: ConMod[0..2]/OutMod/PopMod, ResTNm, TechTree+0x20 order counter, and the undeclared otch vector | . **LANE V 2026-09-08 (build `eventlive-dd38117-20260908T0916Z`): the events divergence is CLOSED on the reference turn** - 3 calls, 3 compared, **0 diverged, exit 0**. Over 5 turns 15 calls / **2** diverged (was 3), both short by exactly 1 `next_id` = the unmodelled `EVENT_TECHS_UNLOCKED`. **CORRECTION to lane R's 'RNG 15/15': that was WORKLOAD LUCK, not a property.** On lane V's call 9 the completed tech had `research_roll_pending` set, so the original drew one word `ours` did not (`left` 374 vs 375, `next_index` 250 vs 249) - the `OnTechResearched` callback draw, a DECLARED out-of-scope boundary (P-events-wiring §3 says so in as many words). Honest statement: the `rng` region reproduces bit-for-bit on every call that does not complete a roll-triggering tech, and is short by exactly one draw on every call that does. Also live: `sizeof(ObservedTech)` = **44**, measured twice (`observed_techs.bytes` 440->484 and 484->528). Zuul double roll now CLOSED on a species-5 save lane V made - see its own row | RNG signatures (Ghidra) | meta | verified | high | 100% | 2026-09-08 | Seed/Twist/NextFloat/NextInt verified; draw = y/(2^32-1); NextInt [0,n] inclusive; lazy twist; left@+0x9c4. RUNTIME CONFIRMED: fpu_cw=0x127f (53-bit double, round-nearest) - our next_float model is right, float_from_pc24 is an unused contingency | | engine: game/effects | engine | verified | high | 100% | 2026-09-08 | merged: TechId enum (196 slots @10000+i), 44 ids with typed strategic effects, species flag bits, ApplyTechEffect; 254 checks | -| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **VM140=L1, VM141=L2, VM144=L3, VM145=L4, VM146=L5** (five parallel investigations 2026-09-08). VM140 remains the reference/oracle guest. Was: FREE (W3 released 2026-09-08, oracle bytes verified; **AND THE LAB IS NOW FIVE GUESTS** - VM141/144/145/146 are ZFS clones for probes and workloads, VM140 remains the exclusive reference; W2 released 2026-09-08, VM restored + oracle bytes verified; H released 2026-09-08, oracle reproduced + VM restored; N released 2026-09-08; Z released 2026-09-08; O-workloads released 2026-09-08 08:30 local, see the LANE O note at the end of this row; U-unlock released 2026-09-08 06:55 local; V-eventlive before it; F-fpucw before it, M-movefleet before that). QUEUE: empty. VM left at the MAIN MENU, `hooks=trace`, build `recap-7584bad-20260908T0615Z` restored from `C:\SOTS\shimdist-recap` (that dist also carries `shim.cfg.recap{trace,b3,b1,misc}`). Lane M also left `C:\SOTS\shimdist-mf` + `C:\SOTS\ui\mf{deploy,release}.ps1` in place - harmless, and a working template for the next lane. Windows Update DISABLED/paused on the VM. Non-holders build /srv/re-lab/build/sots-engine-, stage dist-, deploy C:\SOTS\shimdist-. GOTCHA (lane R): after `schtasks /Run /TN SOTS` the main menu can take >60 s - SCREENSHOT AND VERIFY before clicking, or the click path lands in Credits. GOTCHA (lane M): drive the load dialog ONE rui.ps1 CALL PER CLICK with a screenshot between - a single chained cmd.txt loses sync and silently ends up somewhere else. And the Load Game dialog does NOT pre-select Single Player on a fresh launch: the documented path really is Load Game (512,536) -> Single Player (512,290) -> OK (551,523) -> row -> OK (682,624) -> Launch (511,663). ref-turn2 row is at (400,436) GOTCHA (lane F, confirms lane R): the >60 s startup is REAL and cost a whole wasted run — do not sleep-and-click, **verify the main menu from a screenshot** (`verify/fpu-cw/` run scripts poll a screenshot until the Load Game / Exit buttons are bright red; 3 probes ≈ 25 s was typical). TIP (lane F): reset `SavedGames\` to a fixed file set before every run — the Load dialog row positions depend on how many files are listed, so a constant set means the click path never has to be re-derived (with the 4-file set ref-turn2 sits at (400,348), not (400,436)). PowerShell over SSH mangles quoting badly: send snippets base64 as `powershell -EncodedCommand`, or use `-ExecutionPolicy Bypass -File`. Lane F left `C:\SOTS\shimdist-fpu` + `C:\SOTS\ui\f{deploy,grab,fpu}.ps1` + `C:\SOTS\ui\preF\` (the pre-lane-F SavedGames snapshot, restored) in place. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, SavedGames back to the 7-file pre-F set, main menu verified by screenshot. GOTCHA (lane V, third confirmation): the >60 s startup is REAL - the Kerberos intro was still on screen at t+30 s and the main menu appeared at ~t+70 s. GOTCHA (lane V): `type ` via the click helper did **NOT** register in the game's text fields this session (neither the Create Game name nor the Save Game file name took SendKeys); the workaround is to accept the default name and **rename the `.sav` on disk** - the Load dialog lists files by FILENAME. SAVEDGAMES IS NOW **8 FILES**: the pre-existing 7 plus `zuul-turn5.sav` (a species-5 save, see the Zuul row). Row positions are UNCHANGED for existing lanes because `zuul-turn5` sorts last: **ref-turn2 is still (400,436)**, zuul-turn5 is the new row 8 at (400,465), rows 29 px apart from y=262 - verified by screenshot `verify/results/shim/eventlive/load-dialog-8files.png`. The three autosaves now belong to the Zuul game. Lane V left `C:\SOTS\shimdist-v` + `C:\SOTS\ui\v{deploy,release,click}.ps1`. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. LANE U (2026-09-08): left `C:\SOTS\shimdist-u` + `C:\SOTS\ui\u{deploy,release}.ps1` in place. SavedGames is STILL 8 FILES (the three autosaves now belong to the Zuul game at turn 15) - **ref-turn2 is still (400,436)**, zuul-turn5 still (400,465). A QEMU-level screenshot (feed `screendump /tmp/vm140.ppm` to `qm monitor 140` on spicy, then scp the .ppm) is more reliable than the click helper's `shot` action and does not need the game window. FOURTH confirmation of the >60 s startup: the Kerberos intro was still on screen at t+45 s, the main menu at ~t+85 s. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. | **LANE O 2026-09-08.** **VM140 is FREE.** Holder was O-workloads; released 2026-09-08 after ~9 turns of Zuul play (turn 15 -> 23) and a fresh 3-turn Human game. **CORRECTION to lane V's `type` gotcha: the click helper's `type` DOES reach the game's text fields - you must CLICK THE FIELD FIRST.** `click 480 234` (the Save Game File Name box) then `type ` worked every time this session, and `click 500 363` + `type` worked for the Create Game name box; lane V's failure was almost certainly an unfocused field. Saves can therefore be named properly and no longer need renaming on disk (the file written is gameName + typedName). Coordinates confirmed this session at 1024x768: in-game menu = round button (1000,714) -> `Save` (937,679) / `Quit to Main Menu` (937,698) -> confirm OK (537,377); Save dialog `Save` (772,233), `Game Successfully Saved` OK (612,383), `Done` (772,542); map panel `Research` (55,124) `Design` (142,124) `Build` (221,124), `Manage Fleets` (60,464) `Move` (155,464) `Special` (229,464), `End Turn` (100,714); Build screen `Add To Queue` (125,714); **Esc leaves the Build and Research screens** - the bottom-left button on the Research screen is `Cancel research`, NOT back, and clicking it pops `Confirm Cancel`. Move mode: send `move X Y` FIRST to read the target's name / Range / ETA off the map, THEN `click`, because that readout is what tells you whether the route is a node route (2-5 turns) or a sublight crawl (37-86 turns). **Chained clicks in one `cmd.txt` DO work for repeated clicks on the SAME button** (`Add To Queue`) but drop about half at `sleep 250` and about 1 in 6 at `sleep 500` - count the result, never assume. End Turn takes 30-45 s under `hooks=trace` with ~25 ships. A mid-turn AI diplomacy popup can steal a scripted click sequence - screenshot after any batch that seems to have done nothing. **VM RESTORED and verified**: quit to the main menu (screenshot), `SavedGames` back to the ORIGINAL 8-file set with the three autosaves restored BYTE-IDENTICAL (sha256 prefixes 0630daaa / e0bf3485 / 1f0dc162), so `ref-turn2` is still (400,436) and `zuul-turn5` still (400,465); lane O's staging dir `C:\SOTS\o-stage` removed; `shim.cfg` still `hooks=trace`, build `recap-7584bad-20260908T0615Z` untouched. Lane O's 7 saves live in the REPO ONLY - push one back with `scp verify/results/saves/.sav re@192.168.10.139:C:/SOTS/SavedGames/` when a compare needs it, and remember that adding a file shifts the Load dialog rows (`MyGame*` sorts BEFORE `MyGameverify1*`) +| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **VM140=L1, VM141=L2, VM144=L3, VM145=L4, VM146=FREE (L5 released; left running l5A-widened with hooks=off)** (five parallel investigations 2026-09-08). VM140 remains the reference/oracle guest. Was: FREE (W3 released 2026-09-08, oracle bytes verified; **AND THE LAB IS NOW FIVE GUESTS** - VM141/144/145/146 are ZFS clones for probes and workloads, VM140 remains the exclusive reference; W2 released 2026-09-08, VM restored + oracle bytes verified; H released 2026-09-08, oracle reproduced + VM restored; N released 2026-09-08; Z released 2026-09-08; O-workloads released 2026-09-08 08:30 local, see the LANE O note at the end of this row; U-unlock released 2026-09-08 06:55 local; V-eventlive before it; F-fpucw before it, M-movefleet before that). QUEUE: empty. VM left at the MAIN MENU, `hooks=trace`, build `recap-7584bad-20260908T0615Z` restored from `C:\SOTS\shimdist-recap` (that dist also carries `shim.cfg.recap{trace,b3,b1,misc}`). Lane M also left `C:\SOTS\shimdist-mf` + `C:\SOTS\ui\mf{deploy,release}.ps1` in place - harmless, and a working template for the next lane. Windows Update DISABLED/paused on the VM. Non-holders build /srv/re-lab/build/sots-engine-, stage dist-, deploy C:\SOTS\shimdist-. GOTCHA (lane R): after `schtasks /Run /TN SOTS` the main menu can take >60 s - SCREENSHOT AND VERIFY before clicking, or the click path lands in Credits. GOTCHA (lane M): drive the load dialog ONE rui.ps1 CALL PER CLICK with a screenshot between - a single chained cmd.txt loses sync and silently ends up somewhere else. And the Load Game dialog does NOT pre-select Single Player on a fresh launch: the documented path really is Load Game (512,536) -> Single Player (512,290) -> OK (551,523) -> row -> OK (682,624) -> Launch (511,663). ref-turn2 row is at (400,436) GOTCHA (lane F, confirms lane R): the >60 s startup is REAL and cost a whole wasted run — do not sleep-and-click, **verify the main menu from a screenshot** (`verify/fpu-cw/` run scripts poll a screenshot until the Load Game / Exit buttons are bright red; 3 probes ≈ 25 s was typical). TIP (lane F): reset `SavedGames\` to a fixed file set before every run — the Load dialog row positions depend on how many files are listed, so a constant set means the click path never has to be re-derived (with the 4-file set ref-turn2 sits at (400,348), not (400,436)). PowerShell over SSH mangles quoting badly: send snippets base64 as `powershell -EncodedCommand`, or use `-ExecutionPolicy Bypass -File`. Lane F left `C:\SOTS\shimdist-fpu` + `C:\SOTS\ui\f{deploy,grab,fpu}.ps1` + `C:\SOTS\ui\preF\` (the pre-lane-F SavedGames snapshot, restored) in place. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, SavedGames back to the 7-file pre-F set, main menu verified by screenshot. GOTCHA (lane V, third confirmation): the >60 s startup is REAL - the Kerberos intro was still on screen at t+30 s and the main menu appeared at ~t+70 s. GOTCHA (lane V): `type ` via the click helper did **NOT** register in the game's text fields this session (neither the Create Game name nor the Save Game file name took SendKeys); the workaround is to accept the default name and **rename the `.sav` on disk** - the Load dialog lists files by FILENAME. SAVEDGAMES IS NOW **8 FILES**: the pre-existing 7 plus `zuul-turn5.sav` (a species-5 save, see the Zuul row). Row positions are UNCHANGED for existing lanes because `zuul-turn5` sorts last: **ref-turn2 is still (400,436)**, zuul-turn5 is the new row 8 at (400,465), rows 29 px apart from y=262 - verified by screenshot `verify/results/shim/eventlive/load-dialog-8files.png`. The three autosaves now belong to the Zuul game. Lane V left `C:\SOTS\shimdist-v` + `C:\SOTS\ui\v{deploy,release,click}.ps1`. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. LANE U (2026-09-08): left `C:\SOTS\shimdist-u` + `C:\SOTS\ui\u{deploy,release}.ps1` in place. SavedGames is STILL 8 FILES (the three autosaves now belong to the Zuul game at turn 15) - **ref-turn2 is still (400,436)**, zuul-turn5 still (400,465). A QEMU-level screenshot (feed `screendump /tmp/vm140.ppm` to `qm monitor 140` on spicy, then scp the .ppm) is more reliable than the click helper's `shot` action and does not need the game window. FOURTH confirmation of the >60 s startup: the Kerberos intro was still on screen at t+45 s, the main menu at ~t+85 s. VM RESTORED: recap build `recap-7584bad-20260908T0615Z`, `hooks=trace`, main menu verified by screenshot. | **LANE O 2026-09-08.** **VM140 is FREE.** Holder was O-workloads; released 2026-09-08 after ~9 turns of Zuul play (turn 15 -> 23) and a fresh 3-turn Human game. **CORRECTION to lane V's `type` gotcha: the click helper's `type` DOES reach the game's text fields - you must CLICK THE FIELD FIRST.** `click 480 234` (the Save Game File Name box) then `type ` worked every time this session, and `click 500 363` + `type` worked for the Create Game name box; lane V's failure was almost certainly an unfocused field. Saves can therefore be named properly and no longer need renaming on disk (the file written is gameName + typedName). Coordinates confirmed this session at 1024x768: in-game menu = round button (1000,714) -> `Save` (937,679) / `Quit to Main Menu` (937,698) -> confirm OK (537,377); Save dialog `Save` (772,233), `Game Successfully Saved` OK (612,383), `Done` (772,542); map panel `Research` (55,124) `Design` (142,124) `Build` (221,124), `Manage Fleets` (60,464) `Move` (155,464) `Special` (229,464), `End Turn` (100,714); Build screen `Add To Queue` (125,714); **Esc leaves the Build and Research screens** - the bottom-left button on the Research screen is `Cancel research`, NOT back, and clicking it pops `Confirm Cancel`. Move mode: send `move X Y` FIRST to read the target's name / Range / ETA off the map, THEN `click`, because that readout is what tells you whether the route is a node route (2-5 turns) or a sublight crawl (37-86 turns). **Chained clicks in one `cmd.txt` DO work for repeated clicks on the SAME button** (`Add To Queue`) but drop about half at `sleep 250` and about 1 in 6 at `sleep 500` - count the result, never assume. End Turn takes 30-45 s under `hooks=trace` with ~25 ships. A mid-turn AI diplomacy popup can steal a scripted click sequence - screenshot after any batch that seems to have done nothing. **VM RESTORED and verified**: quit to the main menu (screenshot), `SavedGames` back to the ORIGINAL 8-file set with the three autosaves restored BYTE-IDENTICAL (sha256 prefixes 0630daaa / e0bf3485 / 1f0dc162), so `ref-turn2` is still (400,436) and `zuul-turn5` still (400,465); lane O's staging dir `C:\SOTS\o-stage` removed; `shim.cfg` still `hooks=trace`, build `recap-7584bad-20260908T0615Z` untouched. Lane O's 7 saves live in the REPO ONLY - push one back with `scp verify/results/saves/.sav re@192.168.10.139:C:/SOTS/SavedGames/` when a compare needs it, and remember that adding a file shifts the Load dialog rows (`MyGame*` sorts BEFORE `MyGameverify1*`) | Zuul double-roll (behavioural) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane V 2026-09-08.** No species-5 save existed, so lane V MADE one on VM140: a custom game with only Zuul left in the Available Species pool (both players Zuul), 4 End Turns under `shim.cfg.recapb3`. **8 calls, 8 compared, 0 diverged, exit 0.** All four researching calls have `species=5` and the generator advances by TWO, not one: `left` 540->538 / 522->520 / 504->502 / 485->483, `next_index` +2 each, `mt` hash unchanged; `ours` reproduced every post-state bit-for-bit. Save `verify/results/saves/zuul-turn5-species5.sav` (48559ab5b719b332, 59,131 B, turn 5, homeworld Gallandro) and on the VM as `C:\SOTS\SavedGames\zuul-turn5.sav`. NOT yet exercised by it: a Zuul tech COMPLETION (node 144 was at ~4,700 of 5,000 after 5 turns - one more End Turn gets there), so `SetResearched` and the Zuul boarding-pod grant are still untouched. Report `findings/subsystems/eventlive-verification.md` §5 **EXTENDED by lane U 2026-09-08: the double roll and the COMPLETION PATH together.** `zuul-turn5` + 10 more End Turns (turn 5 -> 15) under the same config: **20 calls, 20 compared, 0 diverged, exit 0**, with TWO Zuul completions. Call 2 (turn 7, alloc {144,1376}) advances the generator by two (`left` 449->447, `next_index` 175->177) AND runs the unlock cascade in the same call - node 144 -> state 4 / order 21 / turn_researched 7, nodes 132 and 136 -> state 2 with cost_rp 10000 / 16000 and turn_available 7, `next_id` 10->12, `observed_techs` 396->440. Note the ZUUL tree unlocks only 132 and 136 from tech 144 where the HUMAN tree also unlocks 142, so this is an independent instance of the cascade, not a repeat. Trace `verify/traces/unlock-b3-zuul.jsonl.gz` | | budget tail coverage (expenses/aid/debt) | verify | backlog | — | 0% | 2026-09-08 | 8 ComputeBudget slots were always 0 in ref-turn2 (no sliders, no aid, no debt, no handicap). Need a save with expense sliders, a debtor and a research-aid treaty to exercise ExpenseTotal + the aid/bonus tail . CONFIRMED AND WORSE 2026-09-08 (lane R, 4284 calls): **13 of 22 slots are 0 on every call** - tradeIncome, shipCarriedPop, secondaryManager, bonusIncome, systemIncomeNeg, debtInterest, construction, expenses, researchMoneyGiven, savingsGiven, tra, researchPointsGiven, trp | **LANE O 2026-09-08.** **PARTIALLY unblocked, and three of the remaining slots are now shown to be UNREACHABLE through the 1.8 UI.** Delivered: `[11] construction` and a large `[8] maintenance` are live in `zuul-turn17-orders2.sav` (build queue of 20 DEs) and `zuul-turn23-fleet23.sav` (27 DEs in service, `Maint` 14,500 vs 3,000/1,000 in every earlier save); the empire research slider is off default (`ResRate` 0.97 vs 0.25) in all five Zuul saves. NOT delivered, with reasons. **(a) `[12] expenses`**: `Nexp` is `[]` for EVERY player in ALL 11 saves across turns 1-23, human and AI alike, and no screen in the 1.8 UI (map panel, Build, Research, Ranking, Intel Analysis, Empire Population Manager, in-game menu) exposes a per-category expense slider - the only economic sliders are the empire Savings/Research split and the per-system `SRs/SRt/SRsc/SRtf/SRi/SRoh` rates, which land in `Sys`, not `Nexp`. Working hypothesis: slot 12 is dead in 1.8. **(b) `[10] debtInterest` / `[7] systemIncomeNeg`**: a debtor cannot be manufactured quickly - a ship's savings cost is deducted at QUEUE time so the queue cannot be over-committed, and `Maint` counts only ships IN SERVICE, which arrive at construction-budget/ship-cost per turn (~4.5 DE/turn here). With income floored at 78,570 (Planetary Budget pushed fully to Construction; measured 124,055 -> 78,570 on the panel) that is ~157 destroyers, i.e. ~35 more End Turns. Reachable, just not cheap. **(c) `[13]/[14]/[19]` aid**: a treaty offer DOES occur (the AI offered a non-aggression pact on turn 3 of the Human game and it was accepted - `human-turn3-noderoute.sav`), but the accepted pact leaves the player's `aid` count 0, and SotS 1.8 has no player-initiated treaty UI - proposals are AI-driven events. Research aid needs a longer game and luck, not a click path | hook GetDifficultyMods | meta | backlog | — | 0% | 2026-09-08 | B1 derived the two difficulty rows from trace values (AI maintenance divisor 3, research x1.5) instead of snapshotting them; hook it properly so they stop being constants | @@ -323,3 +323,8 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`). | CORRECTION: layouts.md ServerSystem NVO rows are wrong | objects | backlog | — | 0% | 2026-09-08 | Lane W3: `Game::ServerSystem` rows `PID@0x274` and `NVO@0x278` are wrong - **+0x274 is _Myhead, +0x278 is _Mysize, and the "PID" tag belongs to the NODE KEY**. Same shape for the NVE/NVs rows. Documented but not fixed because it is a GENERATED file - the fix belongs in the recovery, not the output | | gen_addresses now detects SAME-ADDRESS duplicates | meta | verified | high | 100% | 2026-09-08 | Lane SV caught by hand what the tooling could not see: it reached 0x004271c0 independently and would have filed `RNG_NextIntInclusive`, but addresses.json already carries it as **RNG_NextInt** - and **the merger only checked duplicate NAMES**, so it would have merged silently and **forked the vocabulary for the campaign's most-used RNG primitive**. Fixed: gen_addresses.py now errors on same-address-different-name too (offsets excluded - they are only meaningful per owning struct). IT IMMEDIATELY FOUND **13 MORE across 8 lane fragments**, including two AI lanes naming one function differently, a boundary marker colliding with the function it bounds, and several lane-suffixed aliases of existing entries. All resolved with the agreement recorded in each fragment's _note. **AND THE SWEEP BROKE THE SHIM BUILD** - the engine referenced a dropped alias - which the CT111 cross-build caught and the host build could not. Rule 13 earning its keep again | | standalone: 209 -> 124, closed 85, regressed 0 | engine | verified | high | 100% | 2026-09-08 | Corrected figure after finding a stale-build error of mine (rule 24). Lanes PL and SV had each independently reported 126 from a 128 baseline while main also read 128 - arithmetically impossible, which is what exposed it. Both lanes' work was in the tree all along; the binary I measured predated both merges because the gate rsync excludes `build*` and rsync preserves mtimes, so cmake relinked nothing. **NO ERROR, NO WARNING, AND A PLAUSIBLE NUMBER** - the same shape as the build-dir race earlier today that looked exactly like engine nondeterminism. Fresh build dir per measurement from now on | +| THE REFERENCE PAIR IS NOT DETERMINISTIC | verify | verified | high | 100% | 2026-09-08 | **Lane L5, unlooked for, and it qualifies the metric every standalone number is measured against.** Three runs of `turn1-state -> turn2` on VM146 gave **three different post-turn autosaves, differing in EXACTLY FOUR LEAVES** - one Singularity shadow empire's research pick plus the derived checksum. **RULE 19 IS DISCHARGED**: two of the three runs carried IDENTICAL hooks and still differed, and the hooks=off run was a THIRD value - so this is the game, not the instrument. The determinism oracle **stands for ref-turn2 -> turn3** (the byte-identical hashes, reproduced many times today including in two fresh processes for AI1's P2) but **DOES NOT GENERALISE TO ITS NEIGHBOUR**. Most likely locus: lane AI4 established there are THREE AI players including the two dormant Singularity empires, all of which pick a target on turn 1, and lane AI1's reading has the AI's per-client RNG seeded from an unseeded global - never confirmed live because this build does not emit the log line. Routed to lane L1, which is on this exact pair | +| interest literals VERIFIED LIVE - with a control that FAILED | verify | verified | high | 100% | 2026-09-08 | Lane L5 did what rule 23 asks. Prediction: the game pays **499, not 500**, on a treasury of exactly 50,000. HELD - the shipped widened-float build ran **3,895 calls, 0 diverged, 0 undeclared writes**. **AND THE CONTROL BUILD FAILED AS REQUIRED**: exact decimals gave **1,359 divergences of 2,718 calls**, every one on a treasury that is a multiple of 100, while the three non-boundary treasuries diverged ZERO times. **Coverage: 5 distinct treasury states, 2 ON THE BOUNDARY** - versus the green run this replaces, 4,437 calls over 20 states, NONE on a boundary. Fewer calls, far more evidence. BLAST RADIUS IS BIGGER THAN G3 STATED: the one-money error propagates into `available` and `researchMoney` on 1,317 of the 1,359 calls, so it moves the player's RESEARCH SPEND, not just the treasury. The control also resolved a contradiction L5 flagged BEFORE running: slot 5 IS diffed and the harness CAN see it, so the old green run was thin workload, not a broken instrument | +| "widen every literal" is ITSELF a defect | verify | verified | high | 100% | 2026-09-08 | Two MORE rule-23 defects found by operand sweep in ComputeBudget, both corrected and both **marked unverified because no reference turn can see them**: the research-yield factor is a widened 0.85f (needs research money a multiple of 40,000 - the run presented 9 distinct values, NONE is) and the three research modifiers are summed in SINGLE precision (needs shrm/TRM non-zero; the corpus has both at 0). Both pinned by tests that fail with decimals. **CRITICALLY: three constants BESIDE them are genuinely exact doubles** - so blanket-widening is its own bug. Read the four bytes, every time | +| BANKRUPTCY factor measured, not assumed | objects | verified | high | 100% | 2026-09-08 | = 3.29999995 = (float)3.3. **Its file image is ZERO - the loader fills it at run time** - so lane PL had read the operand width and been forced to ASSUME the value. Now measured on the running game: the assumption was right and the narrowing is real. **No static reading could have produced this** | +| L5 kept `verified` at 0, and defended it | meta | verified | high | 100% | 2026-09-08 | The column has read 0 all campaign and L5 held it there ON PURPOSE. Every phase its compare touches is `Partial` for reasons UPSTREAM of the compare: P01's repair demand is taken as 0, P02 needs the AI's research orders, T31's BnkPr needs an unwired tuning constant. **None is fixable by a VM.** Promoting one because part of it was checked is exactly the drift app_test_catalog asserts against - the ladder's two conditions, COMPLETE and COMPARED, are blocked by different things and the column measures the conjunction. Catalog notes now carry the live evidence and its coverage instead of the older call counts. Also two predictions of its own falsified and recorded: the difficulty record is a HEAP POINTER at ServerPlayer+0x36c not an inline record (correcting the hook's standing "not reachable" note; the fitted pair stays a hypothesis, deliberately not dereferenced), and it predicted 2 divergences and got 1,359 because it counted players where the harness counts calls | diff --git a/findings/control-flow/hive-creation-rng.md b/findings/control-flow/hive-creation-rng.md new file mode 100644 index 0000000..40b8625 --- /dev/null +++ b/findings/control-flow/hive-creation-rng.md @@ -0,0 +1,344 @@ +# Hive creation draws, inside `BeginProcessTurn` — lane Z's zero was the workload, not the code + +- **Type:** control-flow (live measurement) +- **Status:** **verified** — five nested boundary hooks on the running game, reconciled against a + return-address draw-site ledger that shares no code path with them +- **Confidence:** high on the RNG result (two instruments agree word for word on two consecutive + turns, and the *values* the draws produce match two corpus saves exactly); see §7 for what is + not settled +- **Owner / date:** lane L1 · 2026-09-08 · VM140 +- **Instrument:** `sots-engine` `wip/l1`, `src/shim/hooks/tail_rng.{h,cpp}` (five new hooks), + build `0117495-dirty-20260908T2113Z`, config `shim.cfg.l1hive` +- **Predictions, committed before the build:** `sots-engine/docs/L1-predictions.md` +- **Corrects:** `findings/control-flow/tail-rng-ledger.md` §0/§2 (qualified, not overturned) and + `findings/objects/svsctob-writers.md` §4.3 (one address) + +--- + +## 0. The answer + +**`SVSOSwarmQueen::RegisterHives` draws one strategic-generator word per new hive, inside +`StrategyServer::BeginProcessTurn`, which runs between the pre-turn autosave and +`StrategyServer::ProcessTurn` — outside both turn drivers and before either.** Measured on the End +Turn taken from `turn1-state.sav`, where the swarm's two hives are created: + +| turn | `BeginProcessTurn` | `ProcessTurn` | tail | turn total | residual outside the drivers | +|---|---|---|---|---|---| +| 1 → 2 (**hives created**) | **2** | 20 | 0 | **22** | **2** | +| 2 → 3 (hives exist) | **0** | 19 | 0 | **19** | **0** | + +Lane SV predicted this from the instruction stream and could not run it. It is right. + +**Read the two rows' evidence differently, because they are not equally strong.** Row 2 is a closed +bracket: `Autosave(endTurn=1)` at word 192, `Autosave(endTurn=0)` at 211, subtotals 0 + 19 + 0, +residual 0 by subtraction. Row 1's bracket is **incomplete by construction** — it is the first End +Turn after a load, so the pre-turn autosave hook has no server pointer yet and records +`words: null`, the same limitation `tail-rng-ledger.md` reports for its turn 3. Its total of 22 comes +from the **independent per-call-site ledger** (§2.1), which sums to 22 with nothing unattributed and +carries the hive draws as their own row. So row 1's residual is *attributed*, not *subtracted* — and +row 2, which is subtracted, is the one that shows the residual returning to 0. + +**What this does to `tail-rng-ledger.md`.** Its headline — *"a strategic turn advances the strategic +generator by 18–22 words, all of it inside `StrategyServer::ProcessTurn`, and the residual outside +the two turn drivers is exactly zero"* — is **correct for every turn it measured and false as a +statement about the code**. All eight of its turns, and the twelve more it added later, were turns on +which the hives already existed. On the one turn in this campaign's corpus where they do not, the +residual is 2 and it is outside `ProcessTurn`. + +That is the same shape as "the tail draws nothing", which was true of eight turns and false on +turn 64. Rule 20's distinction, one level up: **a ledger that closes to zero is a statement about the +states that occurred.** + +**What it means for the standalone.** A reimplementation that models `ProcessTurn` and the tail +perfectly writes a correct autosave on every turn of the corpus and is **two generator words out of +step on the turn the hives appear**, after which every later turn diverges. The interval it must +reproduce starts at `BeginProcessTurn`, not at `ProcessTurn`. + +--- + +## 1. The instrument, and why it could see this when lane Z's could not + +Lane Z's six hooks bracket `Autosave(endTurn=1) → ProcessTurn → combat → tail → Autosave(endTurn=0)`. +`BeginProcessTurn` runs **inside that bracket** and **outside every subtotal**, so a draw there was +always visible as *residual* — and lane Z measured the residual as 0, correctly, because on its turns +there was nothing to see. Nothing had ever been hooked in that interval. + +This lane adds five nested hooks, each declaring the live `Mars::RNG` at `S+0x16c` as a region whose +`describe` reports an absolute word position (lane Z's `RngLedger`): + +``` +Autosave(endTurn=1) ....................... 192 <- lane Z + StrategyServer::BeginProcessTurn ........ NEW <- the interval nobody had hooked + SVSOSwarmQueen::OnTurnBegin ........... NEW (evt 0x13, vtable slot +0x60) + SVSOSwarmQueen::RegisterHives ....... NEW THE DRAW + SVSOSwarmQueen::TickHives ........... NEW the NextQ slip; 0 words + StrategyServer::ProcessTurn ............. 19 <- lane Z + StrategyServer::OnAllCombatDone_Tail .... 0 <- lane Z + SVSOSlaversRefuel::UpdateDifficultyTier NEW (evt 0x14, tail phase 20) +Autosave(endTurn=0) ....................... 211 +``` + +Each new hook carries a **model evaluated at entry** — the hive count and the two config constants, +the per-hive `NextQ` values and the three gate constants, the whole `CDiff` threshold-table scan — so +the record can disagree with the model instead of merely agreeing with itself. + +## 2. The hive-creation turn, measured + +Workload: `turn1-state.sav` (Frame 1, `SvSctOb / EncID 10 / Hives` count **0**, two systems with +`EggScio == 3`), pushed to VM140 as `l1-turn1.sav`, loaded, **End Turn**. + +``` +[d2] SVSOSwarmQueen::RegisterHives words 170 -> 172 DELTA 2 + scenario_tag = 3 enc_id = 10 hives_in = -1 (empty) systems = 28 + systems_tagged = 2 predict_new_hives = 2 predict_min_words = 2 + hive_nextq_lo = {ptr 0x0158e0a0, value 20} + hive_nextq_hi = {ptr 0x0158e0a4, value 30} +[d2] SVSOSwarmQueen::TickHives words 172 -> 172 DELTA 0 + hives_in = 2 + hives = [{sys 0x0e46e788, egg_scio 3, next_q 30, queen 0}, + {sys 0x0e46dbe8, egg_scio 3, next_q 28, queen 0}] + gate_frame_floor = 10 gate_queen_age = 5 spawn_list_cap = 3 + predict_gate1_fails = true predict_words = 0 +[d1] SVSOSwarmQueen::OnTurnBegin words 170 -> 172 DELTA 2 +[d0] StrategyServer::BeginProcessTurn words 170 -> 172 DELTA 2 side[frame] 1 -> 2 +[d0] StrategyServer::ProcessTurn words 172 -> 192 DELTA 20 +[d0] OnAllCombatDone_Tail words 192 -> 192 DELTA 0 +``` + +`predict_new_hives` is computed at entry by transcribing the original's own two predicates over the +live `Systems` vector — `sys->EggScio(+0x184) == queen->scenarioTag(+0x4)` and "no existing hive +references this system". It said **2**; the generator moved **2**. The prediction and the measurement +were produced by different code paths and neither was fitted to the other. + +### 2.1 A third instrument agrees, and it is keyed on the return address + +The per-call-site ledger (lane Z's `draw_sites`, seven detoured generator entry points recording +`__builtin_return_address(0)`) reported, on the same turn: + +| return VA | entry point | calls | words | site | +|---|---|---|---|---| +| **0x00527714** | **`RNG_NextInt`** | **2** | **2** | **`RegisterHives` — a site no previous run has ever recorded** | +| 0x0058788d | `NextFloat` | 1 | 1 | `TechTree::ProcessResearch`+0x1c8 | +| 0x0088df54 | `NextFloat` | 1 | 1 | `ServerPlayer::RollResearchEvent`+0x2f | +| 0x0089342b | `Chance` | 8 | 8 | `GenerateTradeRaidEncounters`+0x196 | +| 0x00893518 | `Chance` | 8 | 8 | `GenerateTradeRaidEncounters`+0x283 | +| 0x005032a2 | `NextFloat` | 1 | 1 | `DetectEncounters` subtree (lane H) | +| 0x007929a9 | `NextInt` | 1 | 1 | `DetectEncounters` subtree (lane H) | +| 0x008e6e09 | `NextFloat` | 16 | 16 | *inside `Chance`'s own body — the helper-internal double count* | + +38 raw, minus the 16 helper-internal, = **22** = 2 (`BeginProcessTurn`) + 20 (`ProcessTurn`), with +nothing left over. On the next turn the same table reads 35 − 16 = **19** and **the 0x00527714 row is +absent entirely**. + +0x00527714 is the return address of the `call 0x004271c0` at **0x0052770f**. Lane SV's document puts +that call at 0x0052770c; 0x0052770c is the `mov [ebp-0x14],esi` that stores the bound. Corrected in +`ghidra/addresses.d/lane-l1.json`. + +### 2.2 The draws produced the historical values, which is the strongest check available + +`NextQ = frame + LO + NextInt(HI − LO)`. Live: `LO = 20`, `HI = 30`, `frame = 2`. + +| | hive on system A | hive on system B | +|---|---|---| +| after `RegisterHives` (measured) | `next_q = 30` ⇒ draw 8 | `next_q = 28` ⇒ draw 6 | +| after `TickHives`' slip, same turn | **31** | **29** | +| `turn2-state.sav` (historical) | **31** | **29** | +| after the next turn's slip (measured) | **32** | **30** | +| `turn3-state.sav` (historical) | **32** | **30** | + +Two draws, two systems, four numbers, all matching saves captured in a different process on a +different day. **The generator trajectory is identical to the historical game's**, which is a much +stronger statement about instrument neutrality than a word count could be — and it is corroborated by +the absolute positions: this run reaches word **192** after turn 2 and **211** after turn 3, and +`tail-rng-ledger.md` §2.1 records `211` as the `ProcessTurn` entry position of turn 4 on the same +game. + +## 3. `LO` and `HI` — the leaf lane SV declared blocked + +> *"blocked on one MT draw and two data-file constants … behind pointers at 0x00ae0204/0x00ae0208 +> that no `.text` or `.data` reference initialises in a form this lane could follow. Fitting `LO` and +> `HI` from a single two-hive observation would have been fitting, not derivation."* + +Refusing to fit was right, and reading them live costs one `LogF`-shaped line: + +| pointer | value | meaning | +|---|---|---| +| `*(int*)0x00ae0204` | **20** | `LO` — the low end of the queen-countdown window | +| `*(int*)0x00ae0208` | **30** | `HI` — the high end; `RNG_NextInt`'s bound is `HI − LO = 10`, inclusive | +| `*(int*)0x00ae0210` | **10** | `TickHives` gate 1: no queen may spawn while `Frame <= 10` | +| `*(int*)0x00ae0228` | **5** | `TickHives` gate 3: a minimum age in turns | +| `*(int*)0x00ae0220` | **3** | a cap applied inside the (never-executed) spawn arm | + +So a new hive's countdown is `frame + 20 + U{0..10}`, and it then slips +1 per turn for as long as +any gate fails. The leaf is closed: a standalone needs these five integers and the two predicates, +not the data files. + +## 4. The `NextQ` slip, live + +Lane SV recovered the slip statically — a single `inc [hive+8]` at **0x0052785a** that four +gate-failure edges converge on — and reproduced 31/29 → 32/30 with no draw and no fitting. Confirmed: + +* `TickHives` was **entered on both turns** and cost **0 words** both times. +* On turn 1→2 it received `next_q` 30 / 28 and the save carries 31 / 29. +* On turn 2→3 it received 31 / 29 and the save carries 32 / 30. +* `predict_gate1_fails` was **true** on both turns, and now has a number behind it: + `Frame <= 10`, so on this save **no queen can spawn before turn 11 whatever the countdown says**. + Every hive slips for at least the first ten turns of its life. + +**The spawn arm still has never executed** (rule 6). No hive in any corpus save has a queen, and +`queens_in` was empty on both turns. What the gate constants now let anyone state is *why*, and how +far away it is, rather than "we never saw it". + +## 5. `CDiff` — both edges, and one of them settled without a workload + +`SVSOSlaversRefuel::UpdateDifficultyTier` 0x00515820 was entered **once per End Turn**, at depth 1 +inside the tail (event 0x14, through the class's seven-instruction generic handler 0x0051a800), and +cost **0 words** both times. + +| turn | `frame` | `cdiff_in` | `predict_path` | region `cdiff` before → after | +|---|---|---|---|---| +| 1 → 2 | 2 | **−1** | `store` | **−1 → 0** | +| 2 → 3 | 3 | 0 | `no_store_unchanged` | 0 → 0 | + +The second row is the point. **"Entered and stored nothing" is now distinguishable from "did not +run"** — which is exactly what a save-file comparison cannot do, and why every corpus pair after +frame 2 shows `CDiff` motionless without saying whether the writer ran. + +The table itself was re-read from the instruction stream, independently of lane SV, at real +instruction boundaries: + +``` +515868 [ebp-0x34] = 1 [ebp-0x30] = 1 [ebp-0x2c] = 1 +515877 [ebp-0x28] = 0x32 [ebp-0x24] = 2 [ebp-0x20] = 3 +515885 [ebp-0x1c] = 0x64 [ebp-0x18] = 2 [ebp-0x14] = 5 +515893 xor eax,eax ; lea ecx,[ebp-0x34] +515898 cmp [ecx],edx ; jg 0x5158c2 ; first threshold GREATER than the frame +51589c add eax,ebx ; add ecx,0xc ; cmp eax,3 ; jl 0x515898 + +5158c2 test eax,eax ; jle 0x5158a6 ; index 0 -> no store +5158cc cmp [edi+0x38],eax ; je 0x5158a6 ; unchanged -> no store +5158d1 mov [edi+0x38],eax ; CDiff = index - 1 +``` + +Only the **threshold** column is ever loaded; the `(1,1) (2,3) (2,5)` payload columns are dead in +this function. So: + +| frame | scan index | stored | +|---|---|---| +| ≤ 0 | 0 | nothing | +| 1 … 49 | 1 | `CDiff = 0` | +| 50 … 99 | 2 | `CDiff = 1` | +| ≥ 100 | walks off the end | **nothing** | + +**`CDiff` can only ever hold 0 or 1. Tier 2 is unreachable at any frame**, and lane SV's reading of +that off-by-one is confirmed by a second independent disassembly. The class ctor 0x0051a820 stores +`[eax+0x38] = -1`, which is precisely the `CDiff = −1` that `turn1-state.sav` carries and the reason +frame ≤ 0 never matters: `Frame` starts at 1 and the tail does not run before the first End Turn. + +What is **not** yet measured is the 50 and 100 crossings live. §7 says what that costs and why it was +not bought with the VM time available; the entry hook is in the tree and prints `predict_path` on +every turn, so whoever plays a game past frame 50 gets both edges for free. + +## 6. Instrument neutrality (rule 19) — checked, and it needed checking twice + +Five new MinHook detours were installed for this lane, on top of lane Z's six template hooks, seven +draw-site detours and twelve entry probes. Lane H's finding is that **one** detour on a clean +prologue boundary changed the game's output by four bytes, so this was a real prediction, not a +formality. + +**The oracle reproduces, exactly.** Same build, same `shim.cfg.l1hive`, load `ref-turn2.sav`, one +End Turn: + +| file | size | sha256 prefix | historical | +|---|---|---|---| +| `(Autosave EndTurn).sav` | 66,732 | `bb4fd9ac89f41e3b` | **identical** | +| `(Autosave).sav` | 67,219 | `978041acd168b56e` | **identical** | + +That run's trace is the second half of the same story: `RegisterHives` entered with the two hives +already present and `NextQ` 31 / 29, drew **0**, `BeginProcessTurn` cost **0**, `ProcessTurn` cost +**19**, position 192 → 211. So the hooks that measured 2 words on the creation turn measure 0 on a +turn that produces a byte-identical historical autosave. + +### 6.1 The creation turn is NOT a byte oracle — and the reason is a live nondeterminism, not the hooks + +Loading `turn1-state.sav` and ending one turn does **not** reproduce `turn2-state.sav` +(`ab4ac2d7…`); it produces a 66,739-byte save with a different hash. Reported here rather than +buried, because it is the kind of thing that looks like instrument perturbation and is not: + +* The **generator trajectory is identical** to the historical game's — same word positions (192 after + turn 2, 211 after turn 3), and the two hive draws produce `NextQ` 31 / 29 and then 32 / 30, exactly + the values `turn2-state.sav` and `turn3-state.sav` carry. +* The **pre-turn** autosave round-trips byte-identically (`a3f9dc4b…` in, `a3f9dc4b…` out). +* The whole difference, at schema level, is **one field on one player**: + + | player | historical `turn2-state.sav` | this run | + |---|---|---| + | 3 | `ResTNm = BIO_GnMod` | `ResTNm = XNC_TrnsLir2` | + + Players 0, 1 and 2 agree exactly (`""`, `IND_Waldo`, `DRV_PlsFiss`), as do `Status`, `EvNxID`, + `NumOwn` and every other field checked. The two `TechTree/St` bytes that also move are that choice's + consequence, and `Summary/Checksum` follows. + +The `hooks=off` control was then run from the same input, and it is the reason this section is worth +reading. **It produced a third answer.** + +| run | instrument | `(Autosave).sav` | size | player 3's `ResTNm` | +|---|---|---|---|---| +| the original 2026-09-07 session (live game, never loaded) | phase-2 shim, `hooks=trace` | `ab4ac2d7e2977260` | 66,739 | `BIO_GnMod` | +| this lane | full L1 hook set | `62061918176e3441` | 66,739 | `XNC_TrnsLir2` | +| this lane, control | **`hooks=off` — nothing installed at all** | `e43ec1d2b443c101` | **66,740** | `XNC_TrnsMorr2` | + +Players 0, 1 and 2 pick identically in all three (`""`, `IND_Waldo`, `DRV_PlsFiss`). Only player 3 +moves, and it moves **with the instrument removed as well**. + +**So the turn-1 → turn-2 transition of this game is not deterministic across processes, and no hook +is responsible.** The three picks are three different tech ids, two of them species-tagged variants +of the same node (`XNC_TrnsLir2` / `XNC_TrnsMorr2`), which is the signature of a selection over a +container whose order is not fixed — a pointer-keyed map or an unstable tie-break — rather than of a +generator draw. The strategic generator's trajectory was **identical to the historical game's** in +the instrumented run, so whatever chooses here does not draw from it. + +Two consequences, both of which matter more than this lane: + +* **`turn1-state.sav` must not be used as a determinism-oracle input.** `determinism-oracle.md` + establishes byte-identity for `ref-turn2 → turn 3` and was never run on turn 1; that claim is + intact and this is a different pair. +* **The standalone milestone has a target that the original itself does not always hit.** A + reimplementation cannot be held to a byte match on a turn where the original disagrees with itself. + Which turns those are is now an open question with one known member. + +None of this touches §0–§5: the RNG numbers were taken on a run whose generator trajectory matches +the historical game word for word, and the same hook set reproduces the oracle exactly on the +adjacent turn. + +## 7. What this does NOT settle (rule 15) + +* **One hive-creation event, on one save.** `turn1-state.sav` is the only save in the corpus whose + swarm queen has zero hives *and* whose next turn creates some; the six Zuul saves sit at zero hives + for eighteen turns and never transition. So "one word per new hive" is confirmed for **two** hives + created in **one** call and extrapolated beyond that. +* **`RNG_NextInt`'s rejection loop never fired.** Bound 10 ⇒ mask 15, so a draw is rejected with + probability 5/16 and the *expected* cost is 16/11 ≈ 1.45 words per hive, not 1. Two hives costing + exactly 2 is a ~47% outcome. **A reimplementation must model the rejection loop, not the constant.** + This run cannot tell the two apart and does not claim to. +* **The spawn arm, and everything past it.** No queen has ever spawned under any instrument. Its + callees 0x0050dfc0 and 0x004fe810 are unread and may draw. +* **`CDiff` at frame 50 and frame 100** — modelled and predicted, not observed (§5). +* **`BeginProcessTurn`'s other work is unmodelled.** Its two container walks (three calls per system, + one per fleet) and its event post are not declared by anything. On these two turns their cost is + zero *by subtraction* — `BeginProcessTurn` and `OnTurnBegin` agree to the word on both turns — but + that is a measurement on 28 systems and one player's fleets, not a proof. +* **No game-state region is declared by any of these hooks.** They measure the generator and nothing + else. A clean run here says the RNG accounting is right and says nothing about whether the turn was + computed correctly. +* **Event 0x15 and the alliance events remain unreachable** from any save we hold — unchanged from + lane SV. + +## 8. Files + +- Traces: `verify/results/shim/l1/trace-et1.jsonl` (End Turn 1, hive creation), + `trace-et2.jsonl` (both End Turns; the second is the control turn) +- Report tool used to read them: `verify/results/shim/l1/rep.py` +- Saves produced: `verify/results/shim/l1/` +- Addresses: `ghidra/addresses.d/lane-l1.json` (10 entries) +- Instrument + predictions: `sots-engine` `wip/l1`, `docs/L1-predictions.md` diff --git a/findings/control-flow/tail-rng-ledger.md b/findings/control-flow/tail-rng-ledger.md index 4bcb1e0..9e153c3 100644 --- a/findings/control-flow/tail-rng-ledger.md +++ b/findings/control-flow/tail-rng-ledger.md @@ -15,6 +15,24 @@ outside the two turn drivers is **exactly zero**. The tail's cost on these turns **latent**: it will bite the first turn a node line expires or a real battle resolves, and our saves reach neither. +> **QUALIFIED BY LANE L1, 2026-09-08 — the residual is zero for the turns measured, not for the code.** +> (`findings/control-flow/hive-creation-rng.md`.) **`StrategyServer::BeginProcessTurn` runs inside this +> document's autosave bracket and outside every one of its subtotals, and nothing had ever been hooked +> there.** On every turn of both saves measured here the swarm hives already existed, so it cost 0 and the +> residual closed. On the one corpus turn that *creates* them — `turn1-state.sav`, Frame 1 → 2 — +> `SVSOSwarmQueen::RegisterHives` takes **one `RNG_NextInt` per new hive** at 0x0052770f, and the measured +> split is `BeginProcessTurn` **2** + `ProcessTurn` **20** = 22, i.e. a **residual of 2**. The very next +> turn it is 0 again (`BeginProcessTurn` 0, `ProcessTurn` 19, bracket 192 → 211), and lane Z's +> instrument reproduces this document's own positions exactly. +> +> Nothing in the tables below is wrong and no number moves. What changes is the scope of the headline: +> **"all of it inside `ProcessTurn`" is a statement about eight turns on which no script object created +> anything.** §11's per-site table gains one row on such a turn — return address **0x00527714**, entry +> point `NextInt`, 2 calls / 2 words — and the same table on the following turn does not carry it. +> +> For the standalone this changes one thing: **the interval to reproduce starts at `BeginProcessTurn`, +> not at `ProcessTurn`.** + --- ## 0. Where to start if you are building the standalone diff --git a/findings/objects/svsctob-writers.md b/findings/objects/svsctob-writers.md index 98c34dd..d98de2a 100644 --- a/findings/objects/svsctob-writers.md +++ b/findings/objects/svsctob-writers.md @@ -264,6 +264,27 @@ the gates open. The gates read config pointers at 0x00ae0210, 0x00ae0228, 0x00ae The `NextQ` **slip** is not blocked and is exact: on `turn2 -> turn3`, where the hives already exist, the modelled rule reproduces both target turns (31 -> 32, 29 -> 30) with no draw and no fitting. +> **RUN AND CONFIRMED BY LANE L1, 2026-09-08** (`findings/control-flow/hive-creation-rng.md`). Every +> claim in §4 and §5 below was put under an instrument on VM140 and held, with two corrections of +> detail and one leaf closed: +> +> * **Hive creation draws, and lane Z's zero is the workload.** On the `turn1-state.sav` End Turn, +> `RegisterHives` was entered once inside `BeginProcessTurn` and cost **exactly 2 words** for 2 new +> hives; `BeginProcessTurn`'s total was 2 and the residual outside the two drivers was **2**, not 0. +> On the next turn all of those are 0. The prediction in §5 was right. +> * **The `NextQ` slip is exact, live.** `TickHives` cost 0 words on both turns; the hives left +> `RegisterHives` at `NextQ` 30 / 28, the save carries 31 / 29, and the next turn's save carries +> 32 / 30 — reproducing `turn2-state.sav` and `turn3-state.sav` from a draw plus an `inc`. +> * **`LO` and `HI` are closed, by reading them live rather than fitting them**: `*(int*)0x00ae0204 = +> **20**`, `*(int*)0x00ae0208 = **30**`, so `NextQ = frame + 20 + NextInt(10)` (inclusive). The +> `TickHives` gates read 10 (a frame floor), 5 and 3 — so no queen can spawn before frame 11 on any +> save, which is why the spawn arm has never run. +> * **`CDiff`'s two invisible edges are confirmed** — the table scan was re-read independently and +> `CDiff` can only ever hold 0 or 1 — and the "stored nothing" case was observed live and +> distinguished from "did not run": the writer is entered every turn and stored only on frame 2. +> * **One address corrected:** the draw call in §4.3 is at **0x0052770f** (return 0x00527714), not +> 0x0052770c, which is the `mov [ebp-0x14],esi` that stores the bound. + **An RNG claim this lane did not measure.** Lane Z measured a strategic turn at 18–22 generator words, *all* inside `ProcessTurn`, with the residual outside the two turn drivers **exactly zero** — on turns 6 and 64, where the swarm hives already existed. Hive creation draws **inside `BeginProcessTurn`**, diff --git a/findings/subsystems/determinism-oracle.md b/findings/subsystems/determinism-oracle.md index 5990828..d8749f2 100644 --- a/findings/subsystems/determinism-oracle.md +++ b/findings/subsystems/determinism-oracle.md @@ -5,6 +5,22 @@ Question: "load the same save, press End Turn, and the resulting autosave is byt game processes, with and without an intervening manual save, and also identical to the autosave the *original* (non-loaded, freshly created) game wrote in the earlier session. +> **SCOPED BY LANE L1, 2026-09-08 — the oracle is a property of THIS PAIR, not of End Turns.** +> (`findings/control-flow/hive-creation-rng.md` §6.1.) Every run below starts from the **turn-2** +> state. Lane L1 ran the same experiment one turn earlier — load `turn1-state.sav` +> (`a3f9dc4b…`, which is this game's turn-1 pre-turn autosave), one End Turn — three times, and got +> **three different post-turn autosaves**: `ab4ac2d7…` (the original live session), `62061918…` (full +> hook set) and `e43ec1d2…` (**`hooks=off`, nothing installed at all**). The whole difference is one +> field: player 3's research target `ResTNm`, which reads `BIO_GnMod` / `XNC_TrnsLir2` / +> `XNC_TrnsMorr2` across the three. Players 0–2 pick identically every time, and in the instrumented +> run the strategic generator's trajectory matched the historical game word for word. +> +> So: **the turn-1 → turn-2 transition of this game is not deterministic across processes, and no +> hook is responsible.** Nothing below is wrong — but "load the same save, press End Turn, and the +> autosave is byte-identical" is verified for `ref-turn2 → turn 3` and **falsified for +> `turn1-state → turn 2`**. Anyone building a byte oracle must pick the pair deliberately, and the +> pairs in this document plus `verify/results/shim/tailrng/` are the ones that hold. + ## Verdict **byte-identical.** No masking is needed for the End Turn oracle when both sides start from the same diff --git a/findings/subsystems/nvo-tshn-visible-owner.md b/findings/subsystems/nvo-tshn-visible-owner.md index d639e15..cf702aa 100644 --- a/findings/subsystems/nvo-tshn-visible-owner.md +++ b/findings/subsystems/nvo-tshn-visible-owner.md @@ -218,6 +218,18 @@ with the entire tech tree. first is false. Both reduce to the same one-line model, so nothing downstream is blocked, but the claim "the sensor branch is what refreshes Spica" is **[H]**, not [V]: no measurement of `SnLv` exists. One arming line (`sys+0x24` in slot 1 instead of `_Mysize`) settles it on the next VM run. + + > **SETTLED FOR SPICA BY LANE L1, 2026-09-08** (`findings/subsystems/snlv-measured.md`). The + > arming line was added (`watch.mode=snlv`) and run. Spica reads `AFlags = 0x0` with + > **`SnLv = 0x00000200`**, i.e. contact level **2** for player index 4 — the player its `NVO` + > record is keyed to. The `AFlags` term is false, so the sensor term is the only way `IsKnownTo` + > can be true: **"the sensor branch is what refreshes Spica" is now [V]**. Two further facts came + > free: level 3 occurs on exactly the eight (system, player) pairs whose `AFlags` bit is set, and + > level **2** — never 1 — occurs where it is not, which corrects + > `SensorPhase_ComputeContactLevel`'s prototype (it has four return tails, `{0,1}`, `{2,3}`, a + > hard 2 and a hard 3; the `else 1` reading names only one of them). **Bismol is still open** — + > it is not on this map. The same config on `zuul-turn23-fleet23.sav` answers it in one End Turn, + > because the arming sweep prints every system's `SnLv`. - **The 54 "sensor branch" rows are unobserved in one conjunct each.** They are consistent, not proven. The honest statement of the corpus result is: *no record contradicts the model, and the one record that contradicted `AFlags` alone is explained.* diff --git a/findings/subsystems/snlv-measured.md b/findings/subsystems/snlv-measured.md new file mode 100644 index 0000000..f437611 --- /dev/null +++ b/findings/subsystems/snlv-measured.md @@ -0,0 +1,141 @@ +# `SnLv` measured — the sensor branch is what refreshes Spica, and the contact level is 2, not 1 + +- **Type:** subsystem (live measurement) +- **Status:** **verified** for Spica; `nvo-tshn-visible-owner.md` §6's first bullet moves **[H] → [V]** +- **Confidence:** high. Hardware data-write watchpoint on the word itself, with the canary self-test + passing and the determinism oracle reproduced byte for byte in the same run. +- **Owner / date:** lane L1 · 2026-09-08 · VM140 +- **Instrument:** `src/shim/hooks/watchpoints.cpp`, new `watch.mode=snlv`, config `shim.cfg.l1snlv`, + build `0117495-dirty-20260908T2113Z` +- **Answers:** `findings/subsystems/nvo-tshn-visible-owner.md` §6 — *"One arming line + (`sys+0x24` in slot 1 instead of `_Mysize`) settles it on the next VM run."* It did. + +--- + +## 0. The result + +`ServerSystem::IsKnownTo` is `AFlags bit || (PlyrIdx < 15 && ((SnLv >> 2*PlyrIdx) & 3) != 0 && +HasResearched(CCC_AdvSens))`. Lane W3 could not decide whether Spica's `NVO` record is refreshed +through the `AFlags` term or the sensor term, because **`SnLv` is not serialised** and had never been +measured. + +On `ref-turn2.sav`, at the arming point, **Spica has `AFlags = 0x0` and `SnLv = 0x00000200`**, i.e. +contact level **2** for player index 4 — the very player its single `NVO` record is keyed to. + +`AFlags = 0` makes the first term false. The sensor term is therefore the only way `IsKnownTo` can +return true for that pair, and the record is refreshed. **The sensor branch is what refreshes Spica**, +measured rather than inferred. + +## 1. The whole map, because it costs no debug register + +Printing `SnLv` and its decoded per-player 2-bit fields for every system during the arming sweep is +free. All 28 systems of `ref-turn2.sav`, at `ApplyAllTurnCommands` entry on the End Turn from turn 2 +(only the non-zero rows shown; the other 20 systems read `SnLv = 0`): + +| system | `AFlags` | `SnLv` | non-zero levels | +|---|---|---|---| +| Hyperion | 0x10 | 0x00000300 | player 4 → **3** | +| Gamma Cephei | 0x01 | 0x00000003 | player 0 → **3** | +| **Spica** | **0x00** | **0x00000200** | **player 4 → 2** | +| Ke'Dolarra | 0x02 | 0x0000000c | player 1 → **3** | +| Koa?Vo | 0x80 | 0x0000c000 | player 7 → **3** | +| Kaa?Vaalu | 0x10 | 0x00000300 | player 4 → **3** | +| Markab | 0x10 | 0x00000300 | player 4 → **3** | +| Kea?Pono | 0x10 | 0x00008300 | player 4 → **3**, player 7 → **2** | +| Ko'Rorkor | 0x10 | 0x00000300 | player 4 → **3** | + +Two regularities fall out and neither was available from any save: + +1. **Level 3 occurs exactly where the `AFlags` bit is set**, on all eight rows — which is the + `IsVisibleTo` early-out of `SensorPhase_ComputeContactLevel` doing what lane W3 read. +2. **Level 2 occurs exactly where it is not** — Spica for player 4, Kea'Pono for player 7. So the + non-visible contact state on this save is **2**, and **level 1 was never observed**. + +## 2. A correction: `ComputeContactLevel` does not return 1 there + +`ghidra/addresses.d/lane-w3.json` records +`SensorPhase_ComputeContactLevel` as *"3 if IsVisibleTo (AFlags), else 1 if inside p's sensor +envelope, else 0"*. The function has **four** return tails, read at instruction boundaries: + +``` +00850d18 mov eax,3 ; IsVisibleTo -> 3 +00850e2f mov eax,ecx ; ecx = setne on an FPU range compare -> 0 or 1 +00850e47 lea eax,[edx+2] ; edx = setne on the same compare -> 2 or 3 +00850e52 mov eax,2 ; -> 2 +``` + +which pair is taken is selected by `cl` and by `[ebx+0x10]`. So the value set is `{0,1,2,3}` and the +`{0,1}` pair is one of three tails, not the only alternative to 3. The measurement above lands on a +`2` tail. **`IsKnownTo` only tests `!= 0`, so nothing downstream changes** — but the constant in the +prototype is wrong and would mislead anyone reimplementing the sensor phase. + +## 3. Who writes it, and how often + +DR1 watched Spica's `sys+0x24` for the whole End Turn: **20 writes**, every one from the same +instruction. + +| trap EIP (Ghidra VA) | store site | value written | count | +|---|---|---|---| +| 0x0086a78c | **0x0086a789** `mov [edi+0x24],edx` | 0x00000200 every time | **20** | + +0x0086a789 is exactly the pack site lane W3 named from static reading — the trap reports the +instruction after the store, and 0x0086a789 + 3 = 0x0086a78c. Confirmed to the byte. The value never +changed across the turn: the recompute is idempotent on this workload, which is why the *absence* of +`SnLv` from the wire costs nothing on these saves and why no save could ever have shown it. + +The enclosing loop at 0x0086a761..0x0086a79b walks `GetGame()->Systems` and calls +`ComputeContactLevel` once per system, masking `3 << (2*idx)` out and OR-ing the result in, guarded +by `[ebp-0x10] >= 0xf` — the `PlyrIdx < 15` test lane W3 read in `IsKnownTo`, here on the writing +side. Twenty full sweeps of the systems vector happen in one End Turn. + +## 4. `TShn` refreshed, in the same run, twice + +DR0 watched Spica's `NVO` record at `root+0x10` (`{int16 touched, int16 TShn}`): **4 writes**, in two +pairs, all with a `SetVisibleOwner` frame: + +| seq | trap EIP (VA) | word after the store | reading | +|---|---|---|---| +| 8 | 0x0075b95a | `0x00020003` | `touched` := 3 (the new `Frame`), `TShn` still 2 | +| 9 | 0x0075b965 | `0x00030003` | `TShn` := 3 | +| 20 | 0x0075b95a | `0x00030003` | same pair again | +| 21 | 0x0075b965 | `0x00030003` | | + +Return addresses 0x0075bd4a / 0x0075bdc3 with 0x0086a806 on the stack put the caller chain in the +sensor phase, next to the `SnLv` writer. This confirms three things lane W3 stated as a model: +`SetVisibleOwner` stamps `touched` and `TShn` in **two separate stores**; it runs **twice per turn** +for one (system, player) pair; and on this turn both are stamped with the new `Frame`. + +## 5. Coverage — what this does NOT settle (rule 15) + +* **Bismol is not on this map.** `nvo-tshn-visible-owner.md` §6's open question — *which* conjunct + fails at Bismol — belongs to `zuul-turn23-fleet23.sav`, a different game with a different star + list. This run settles the Spica half of that bullet and leaves the Bismol half open. The same + config on the Zuul save answers it: the arming sweep prints every system's `SnLv`, so one launch + and one End Turn is the whole experiment. +* **One system watched, 28 printed.** The 20 `SnLv` writes and the 4 `TShn` writes are Spica's. Every + other system's row above is a **read at arm time**, not a watched write. +* **`SnLv` never changed value** during the turn, so nothing here says what a *transition* looks like + or when a contact is lost. +* **Level 1 was never observed.** §2's four-tail reading is static; only 0, 2 and 3 have been seen. +* **Per-thread blind spot.** Debug registers are per-thread and the arm is on the turn thread only. +* **`ShareVisibleOwner` still has never executed** — no corpus save has an alliance (unchanged). + +## 6. Instrument neutrality (rule 19) + +The armed run **is** the oracle run: `ref-turn2.sav`, one End Turn, four watchpoints live and ~24 +exception deliveries, plus the five lane-L1 hooks and lane Z's ledger: + +| file | size | sha256 prefix | historical | +|---|---|---|---| +| `(Autosave EndTurn).sav` | 66,732 | `bb4fd9ac89f41e3b` | **identical** | +| `(Autosave).sav` | 67,219 | `978041acd168b56e` | **identical** | + +Canary self-test: `writes=1 traps=1 dr7=0xdddd0055 PASS`. + +## 7. Files + +- Arming log (all 28 systems with `SnLv` decoded, the base probe, the canary): + `verify/results/shim/l1/watch-arm.txt` +- Hit log: `verify/results/shim/l1/watch-hits.txt` +- Instrument: `sots-engine` `wip/l1`, `src/shim/hooks/watchpoints.cpp` (`watch.mode=snlv`), + configs `shim.cfg.l1snlv` / `shim.cfg.l1control` diff --git a/ghidra/addresses.d/lane-l1.json b/ghidra/addresses.d/lane-l1.json new file mode 100644 index 0000000..39f50f9 --- /dev/null +++ b/ghidra/addresses.d/lane-l1.json @@ -0,0 +1,84 @@ +{ + "entries": [ + { + "name": "SVSOSwarmQueen_RegisterHives_DrawSite", + "addr": "0x0052770f", + "convention": "site", + "prototype": "call RNG_NextInt (0x004271c0) inside SVSOSwarmQueen_RegisterHives, ECX = GetGame()->RNG(+0x16c) + 4, bound = HI - LO passed BY POINTER at [ebp-0x14]; return address 0x00527714, then `add eax,edi` where edi = frame + LO. ONE STRATEGIC-GENERATOR WORD PER NEW HIVE, taken inside StrategyServer::BeginProcessTurn -- outside BOTH turn drivers and before either. Corrects svsctob-writers.md, which put the call at 0x0052770c (that is the `mov [ebp-0x14],esi` storing the bound)", + "status": "verified", + "source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08); disassembled from dumps/sots.exe and measured live on turn1-state.sav" + }, + { + "name": "SVSOSwarmQueen_TickHives_SlipSite", + "addr": "0x0052785a", + "convention": "site", + "prototype": "`inc DWORD PTR [esi+0x8]` -- the NextQ slip. FOUR gate-failure edges converge here: 0x005277e3 (frame <= *SwarmQueen_GateFrameFloor_ptr), 0x005277fe (queens count >= the per-turn allowance), 0x00527848 (age < *SwarmQueen_GateQueenAge_ptr), 0x0052784f (hive's system handle is 0). Consumes NO generator word on any of them; TickHives calls no RNG primitive anywhere in its body (0x00527770..0x005279a1 read whole)", + "status": "verified", + "source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)" + }, + { + "name": "SwarmQueen_HiveNextQOffsetLo_ptr", + "addr": "0x00ae0204", + "convention": "data", + "prototype": "int* -- pointer to the LOW end of the new-hive queen-countdown offset. RegisterHives computes nextQ = frame + *this + RNG_NextInt(*SwarmQueen_HiveNextQOffsetHi_ptr - *this). Behind a pointer, so no .text or .data reference initialises it in a form a static reader can follow; the value is read live by the shim at RegisterHives entry", + "status": "mapped", + "source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)" + }, + { + "name": "SwarmQueen_HiveNextQOffsetHi_ptr", + "addr": "0x00ae0208", + "convention": "data", + "prototype": "int* -- pointer to the HIGH end of the new-hive queen-countdown offset. The RNG_NextInt bound is (*this - *SwarmQueen_HiveNextQOffsetLo_ptr) and NextInt is INCLUSIVE of its bound, so the countdown lands in [frame+LO, frame+HI]", + "status": "mapped", + "source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)" + }, + { + "name": "SwarmQueen_GateFrameFloor_ptr", + "addr": "0x00ae0210", + "convention": "data", + "prototype": "int* -- TickHives gate 1: `if (GetGame()->Frame(+0xc) <= *this) slip`. No queen can spawn at or below this frame", + "status": "mapped", + "source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)" + }, + { + "name": "SwarmQueen_SpawnListCap_ptr", + "addr": "0x00ae0220", + "convention": "data", + "prototype": "int* -- read in TickHives' SPAWN arm (0x005278d8), after a queen has been created: when *this <= 0 the vector at queen+0x30 is trimmed to 0 entries, otherwise to *this. Never exercised on any corpus save (no hive has ever had a queen)", + "status": "mapped", + "source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)" + }, + { + "name": "SwarmQueen_GateQueenAge_ptr", + "addr": "0x00ae0228", + "convention": "data", + "prototype": "int* -- TickHives gate 3: an age in turns, compared against `GetGame()->Frame - resolvedQueen->+0xc`; below it the hive slips instead of spawning", + "status": "mapped", + "source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)" + }, + { + "name": "SVSOSwarmQueen_off_Hives", + "offset": "0x10", + "convention": "field", + "prototype": "std::vector -- {_Myfirst 0x10, _Mylast 0x14, _Myend 0x18, _Alval 0x1c}, element stride 0x10: {void* vptr = 0x009f1a68, StarSystem* sys(+4), int nextQ(+8), Queen* queen(+0xc)}. The WIRE order is HiveID / QueenID / NextQ, which is NOT the member order", + "status": "verified", + "source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)" + }, + { + "name": "SVSOSwarmQueen_off_Queens", + "offset": "0x20", + "convention": "field", + "prototype": "std::vector at +0x20..+0x24, element stride 0xc -- TickHives' second gate divides the byte span by 12. Empty on every corpus save", + "status": "verified", + "source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08)" + }, + { + "name": "SVSOSlaversRefuel_off_CDiff", + "offset": "0x38", + "convention": "field", + "prototype": "int -- the difficulty tier, wire tag \"CDiff\". Set to -1 by the ctor 0x0051a820 and written ONLY by SVSOSlaversRefuel_UpdateDifficultyTier, which stores (scanIndex - 1) and only when it differs. Reachable values are 0 (frame 1..49) and 1 (frame 50..99) ONLY: frame <= 0 exits at index 0 and frame >= 100 walks off the end of the 3-entry threshold table, so BOTH edges store nothing and the tier can never reach 2", + "status": "verified", + "source": "findings/control-flow/hive-creation-rng.md (lane L1 2026-09-08); threshold table read off the stores at 0x00515868..0x0051588c" + } + ] +} diff --git a/ghidra/generated/sots_addresses.h b/ghidra/generated/sots_addresses.h index b38e10d..99c07a1 100644 --- a/ghidra/generated/sots_addresses.h +++ b/ghidra/generated/sots_addresses.h @@ -1,5 +1,5 @@ // GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1). -// Source: sots-re ghidra/addresses.json @ 2e935b8, generated 2026-09-08 by tools/gen_addresses.py +// Source: sots-re ghidra/addresses.json @ ea7881a, generated 2026-09-08 by tools/gen_addresses.py // Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated). #pragma once #include @@ -1913,6 +1913,50 @@ constexpr uint32_t StrategyServer_FinalizeTurnRecords = 0x0038a0e0; constexpr uint32_t ServerSystem_ComputeMaxIncome = 0x003521c0; // note Game::SNMAllCombatDone RTTI vtable, four slots (0x0079e590, 0x0082a100, 0x0082a170, 0x0079e500 -- the middle pair are the network Read/Write). Layout by enumeration from the two stack constructors and from every offset OnAllCombatDone_Tail reads: `struct SNMAllCombatDone { void* vptr; std::vector results; }`, 0x10 bytes -- which is why the handler passes msg+4 and not msg. Three construction sites: RunCombatRound 0x007cc847 (stack), the combat server FUN_007cfd00+0x541 = 0x007d0241 (stack; sends it to every player whose +0x44 is 4 or 5, then sets combatServer->+0x60 = 9; NOTE Ghidra sizes FUN_007cfd00 at 384 B but its real body runs to the ret at 0x007d02b9), and the deserialization factory 0x008663b0 (operator new(0x14) -- 4 bytes larger than the enumerated size, UNEXPLAINED) [verified] constexpr uint32_t SNMAllCombatDone_layout = 0x00624758; +// site call RNG_NextInt (0x004271c0) inside SVSOSwarmQueen_RegisterHives, ECX = GetGame()->RNG(+0x16c) + 4, bound = HI - LO passed BY POINTER at [ebp-0x14]; return address 0x00527714, then `add eax,edi` where edi = frame + LO. ONE STRATEGIC-GENERATOR WORD PER NEW HIVE, taken inside StrategyServer::BeginProcessTurn -- outside BOTH turn drivers and before either. Corrects svsctob-writers.md, which put the call at 0x0052770c (that is the `mov [ebp-0x14],esi` storing the bound) [verified] +constexpr uint32_t SVSOSwarmQueen_RegisterHives_DrawSite = 0x0012770f; +// site `inc DWORD PTR [esi+0x8]` -- the NextQ slip. FOUR gate-failure edges converge here: 0x005277e3 (frame <= *SwarmQueen_GateFrameFloor_ptr), 0x005277fe (queens count >= the per-turn allowance), 0x00527848 (age < *SwarmQueen_GateQueenAge_ptr), 0x0052784f (hive's system handle is 0). Consumes NO generator word on any of them; TickHives calls no RNG primitive anywhere in its body (0x00527770..0x005279a1 read whole) [verified] +constexpr uint32_t SVSOSwarmQueen_TickHives_SlipSite = 0x0012785a; +// data int* -- pointer to the LOW end of the new-hive queen-countdown offset. RegisterHives computes nextQ = frame + *this + RNG_NextInt(*SwarmQueen_HiveNextQOffsetHi_ptr - *this). Behind a pointer, so no .text or .data reference initialises it in a form a static reader can follow; the value is read live by the shim at RegisterHives entry [mapped] +constexpr uint32_t SwarmQueen_HiveNextQOffsetLo_ptr = 0x006e0204; +// data int* -- pointer to the HIGH end of the new-hive queen-countdown offset. The RNG_NextInt bound is (*this - *SwarmQueen_HiveNextQOffsetLo_ptr) and NextInt is INCLUSIVE of its bound, so the countdown lands in [frame+LO, frame+HI] [mapped] +constexpr uint32_t SwarmQueen_HiveNextQOffsetHi_ptr = 0x006e0208; +// data int* -- TickHives gate 1: `if (GetGame()->Frame(+0xc) <= *this) slip`. No queen can spawn at or below this frame [mapped] +constexpr uint32_t SwarmQueen_GateFrameFloor_ptr = 0x006e0210; +// data int* -- read in TickHives' SPAWN arm (0x005278d8), after a queen has been created: when *this <= 0 the vector at queen+0x30 is trimmed to 0 entries, otherwise to *this. Never exercised on any corpus save (no hive has ever had a queen) [mapped] +constexpr uint32_t SwarmQueen_SpawnListCap_ptr = 0x006e0220; +// data int* -- TickHives gate 3: an age in turns, compared against `GetGame()->Frame - resolvedQueen->+0xc`; below it the hive slips instead of spawning [mapped] +constexpr uint32_t SwarmQueen_GateQueenAge_ptr = 0x006e0228; +// field std::vector -- {_Myfirst 0x10, _Mylast 0x14, _Myend 0x18, _Alval 0x1c}, element stride 0x10: {void* vptr = 0x009f1a68, StarSystem* sys(+4), int nextQ(+8), Queen* queen(+0xc)}. The WIRE order is HiveID / QueenID / NextQ, which is NOT the member order [verified] +constexpr uint32_t SVSOSwarmQueen_off_Hives = 0x00000010; +// field std::vector at +0x20..+0x24, element stride 0xc -- TickHives' second gate divides the byte span by 12. Empty on every corpus save [verified] +constexpr uint32_t SVSOSwarmQueen_off_Queens = 0x00000020; +// field int -- the difficulty tier, wire tag "CDiff". Set to -1 by the ctor 0x0051a820 and written ONLY by SVSOSlaversRefuel_UpdateDifficultyTier, which stores (scanIndex - 1) and only when it differs. Reachable values are 0 (frame 1..49) and 1 (frame 50..99) ONLY: frame <= 0 exits at index 0 and frame >= 100 walks off the end of the 3-entry threshold table, so BOTH edges store nothing and the tier can never reach 2 [verified] +constexpr uint32_t SVSOSlaversRefuel_off_CDiff = 0x00000038; +// thiscall void __thiscall Game::AITColonize::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITColonizeGoal::Execute is the same address. 48 bytes; a forwarder to the parameterised worker 0x0068b280 called as worker(ecx = agent, this, pass, this->+0x8, &this->+0x20, &this->+0x10) with `edi = this->+0xc` passed as an IMPLICIT REGISTER ARGUMENT (whole-program-optimised custom convention; a reimplementation that ports only the stack arguments passes garbage). Reaches the colonize order (list 7) at depth 4 via 0x0068b280 -> 0x006930f0 -> 0x00578ff0 -> 0x00769640 [unverified] +constexpr uint32_t AITColonize_Execute = 0x0028b400; +// thiscall void __thiscall Game::AITEscortGateInvade::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITEscortGateInvadeGoal::Execute is the same address. 80 bytes; forwards to 0x0068c5d0 as __fastcall(ecx = this->+0xc, edx = agent) plus 8 stack arguments. Reaches list 7 at depth 4 [unverified] +constexpr uint32_t AITEscortGateInvade_Execute = 0x0028c7c0; +// thiscall void __thiscall Game::AITInvade::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. SHARED BODY: Game::AITInvadeGoal::Execute is the same address. 160 bytes; forwards to 0x0068d460 with 11 arguments, then maintains this->+0x38 and this->+0x39. +0x39 is computed at 0x0068d80f-0x0068d82a as (0x006a6380(agent, this->+0xc) < 2 * 0x006a6260(agent, this->+0xc)) and is what IAITask slot 11 returns for this class. Reaches list 14 at depth 5 [unverified] +constexpr uint32_t AITInvade_Execute = 0x0028d7a0; +// thiscall void __thiscall Game::AITNodeBore::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Does its setup (0x00685810) and its finaliser (0x0068e090) only when pass == 0, and forwards `pass` to 0x0068a520 in both passes. Reaches list 14 at depth 5 via 0x0068a520 -> 0x006ceef0 -> 0x006c16c0 -> 0x006987e0 -> 0x007634d0. Zuul-flavoured task (the node-bore is a Zuul mechanic), so on a non-Zuul board it may be absent from the task list entirely [unverified] +constexpr uint32_t AITNodeBore_Execute = 0x0028e590; +// thiscall void __thiscall Game::AITRaid::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. THE ONE TASK WHOSE PASS-0 BEHAVIOUR IS NOT SETTLED: at 0x0068e89e it calls 0x006b76a0 and then at 0x0068e8b8 loops the list-16 order method 0x007635f0(client, fleetId, 1) over the returned fleets, and NEITHER the call nor the loop carries a `pass` guard of its own. AI3 §2.3's argument says the fleet vector at [ebp-0x28] is empty on pass 0 because it is downstream of the same hub, but [ebp-0x28] was never traced to closure. This address exists as an address so that a zero at 0x007635f0 can be read: entered-and-silent and never-entered are opposite answers (method rule 20) [unverified] +constexpr uint32_t AITRaid_Execute = 0x0028e670; +// thiscall void __thiscall Game::AITAdvanceIdleShips::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Whole body is pass-1 only: `if (pass != 1) return` at 0x0068f25a. Table priority 0, so it is always the LAST task in the descending-priority sweep -- the AI sweeps up whatever is still idle only after every other task has taken both its minimum and its desired force, which is what the two-tier quota model predicts [unverified] +constexpr uint32_t AITAdvanceIdleShips_Execute = 0x0028f230; +// thiscall void __thiscall Game::AITBuildDeepScanShips::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Reaches the BUILD order (list 3) at depth 6 via 0x006ce460 -> 0x006ce360 -> 0x006ce190 -> 0x006bd790 -> 0x006b3bc0 -> 0x00762fd0, the same chain as AITBuildPoliceShips [unverified] +constexpr uint32_t AITBuildDeepScanShips_Execute = 0x002901a0; +// thiscall void __thiscall Game::AITBuildPoliceShips::Execute(StrategyAIAgent* agent, int pass) -- RET 8, IAITask vtable slot 5. Reaches the BUILD order (list 3) at depth 6, identical chain to AITBuildDeepScanShips. One of the two candidate producers of the single list-3 element the reference AI emits on turn 1 [unverified] +constexpr uint32_t AITBuildPoliceShips_Execute = 0x00290380; +// thiscall bool __thiscall Game::StrategyClient::OrderList16(int objectId, bool flag) -- RET 8. THE LIST-16 PRODUCER. Opens with `cmp byte [this+0x15c], 0` -- the End-Turn latch StrategyClient::EndTurn 0x00783be0 sets at 0x00783c59 before BuildTurnCommands runs -- so like every other order method it refuses once the turn has been submitted. Element record for list 16 is {i32, bool} (lane Q), matching the (objectId, flag) pair; list 16 is in the PAYING half of the ModCount table (bump at 0x00821e23 in its applier 0x00821e20). Reached from AITRaid::Execute at depth 1. The method is named for the list it fills, NOT for what the flag means: no save has ever carried a list-16 element and nothing has been observed [unverified] +constexpr uint32_t StrategyClient_OrderList16 = 0x003635f0; +// offset int aidf -- the difficulty level (0..2) stored by ServerPlayer::Read 0x008804d0 at 0x00880fa3, the selector LoadDifficultyRow 0x005a3990 uses to pick the row it copies for this player. VERIFIED LIVE on VM146: read at ComputeBudget entry on every one of the 8 players of turn1-state across 3,895 calls, it is 1 on all eight, which is the difficulty level the whole corpus carries and the level whose AI column holds the 1.1x income modifier [verified] +constexpr uint32_t ServerPlayer_off_Aidf = 0x00000368; +// offset DifficultyMods* -- a POINTER to the per-player difficulty record, NOT the record inline. This entry exists to correct a lane L5 mistake and to save the next lane the run. addresses.json already records DifficultyMods_Select 0x0059b490 being called as Select(p->diffMods /*+0x36c*/, p), and lane L5 read the 0x1c bytes AT +0x36c expecting {int id; float ai[3]; float other[3]}. They are not that: the first dword is 0x0da0ec20..0x0da13360 on the eight players of turn1-state -- eight distinct heap addresses within 18 KB of each other and of the ServerPlayer allocations themselves (the player object was at 0x0da08600) -- and the floats behind it decode as denormal garbage (1.7e-38, 1.4e-43). So the field is a pointer and the record is one dereference further on. HYPOTHESIS, not verified: this run did not follow the pointer, so it is consistent with +0x36c being a pointer to the DifficultyMods record and equally consistent with its being some other per-player sub-object pointer. What IS established is the thing the reading was for: the difficulty row is reachable from a ServerPlayer, so compute_budget.h's old coverage note ('not reachable from a ServerPlayer, so the two relevant entries are fitted constants') is wrong as written. The fitted pair {3.0, 1.5} / {1.0, 1.0} remains UNVERIFIED against the running game; the next run should dereference this field behind a readability guard and compare the selected triple against those constants [hypothesis] +constexpr uint32_t ServerPlayer_off_DiffModsPtr = 0x0000036c; +// data double 0x3FEB333340000000 = 0.85000002384185791 = (double)0.85f -- the research-yield factor, multiplied in by ComputeBudget at 0x00863601 as a QWORD operand. THE WIDTH IS THE FINDING, and what makes it a rule-23 case rather than a curiosity is its two NEIGHBOURS in the same product: 1.15 at 0x00a1a4b8 and 0.5 at 0x009e20a0 are stored as EXACT doubles, while this one is a widened float. Three literals, one expression, two widths -- so a C++ port that writes all three as decimals is wrong in exactly one place. The exact-decimal product (money/50 x 1.15 x 0.5 x 0.85) is integral when researchMoney is a multiple of 40,000, and the image's larger constant lands one ABOVE it there: at 40,000 the game gives 391 research points and the decimal gives 390. NOT VISIBLE ON THE CORPUS, and that is now measured rather than argued: the live compare run presented 9 distinct researchMoney values across 3,895 calls and NONE of them is a multiple of 40,000. Read live out of the running process as well, so nothing patched or relocated it [verified] +constexpr uint32_t g_ResearchYieldFactor = 0x0062d818; // thiscall double (ServerSystem* sys, double overHarvestRate) // `ret 8`, real end 0x007505a5. Returns 0 when the system has no owner (+0x100) or rbfl (+0x1dc) is non-zero. Otherwise the SUM of three terms -- the over-harvest demand x SpeciesDef+0x50, (TRes + available resources) x StripMineFraction x 0.9, and the population output of groups 0, 1 and 2 -- multiplied in one uninterrupted 80-bit chain by player OutMod, sys.OutMod, player +0x224, RebOutMod, ScOutMod, and finally by ADDICTION_OUTPUT_MOD when the addiction phase is >= 3. SIDE-EFFECT FREE: it and all seven callees were checked for stores to the game state (0x0074a6d0's only writes are through its int64 out-parameters), which is why this and not ComputeOutputFromRates is the compare target -- that one repairs ships in orbit [verified] constexpr uint32_t ServerSystem_ComputeTotalOutput = 0x00350480; // thiscall double (ServerSystem* sys, int groupType, int species, double count) // `ret 0x10`, real end 0x0074b871. THE population -> output term: returns 0 for count <= 0, else max(0, POPTYPE[groupType].outputMod x (stationFactor x 1.8) x moraleMod x (count / 500000)). stationFactor is 1 + stations x STATION_BONUS_IMPERIAL_OUTPUT and applies to groupType 0 of an owned system only (and only while that constant is > 0); moraleMod applies to groupType 1 only. So output points per head are typeOutputMod x 1.8 / 500000 -- exactly 3.6e-6 for an imperial population with no stations [verified] diff --git a/verify/results/shim/l1/README.md b/verify/results/shim/l1/README.md new file mode 100644 index 0000000..e4e5d61 --- /dev/null +++ b/verify/results/shim/l1/README.md @@ -0,0 +1,16 @@ +# Lane L1 — the turn-begin RNG run, 2026-09-08 + +Instrument: `sots-engine` `wip/l1`, build `0117495-dirty-20260908T2113Z`. Findings: +`findings/control-flow/hive-creation-rng.md` and `findings/subsystems/snlv-measured.md`. + +| file | what | +|---|---| +| `trace-et1.jsonl` | `l1-turn1.sav` (= `verify/results/saves/turn1-state.sav`) + **one** End Turn — the hive-creation turn. `SVSOSwarmQueen::RegisterHives` words 170 → 172. | +| `trace-et2.jsonl` | the same session after a **second** End Turn; the second bracket is 192 → 211 with a residual of 0. Supersedes `trace-et1.jsonl`, which is kept because it is the record as it stood before the control turn existed. | +| `trace-oracle.jsonl` | `ref-turn2.sav` + one End Turn with the same build and config. This run reproduced the determinism oracle byte for byte (`bb4fd9ac…` / `978041ac…`), so it is the rule-19 control for everything in the other two. | +| `watch-arm.txt` | the `watch.mode=snlv` arming sweep: all 28 systems with `AFlags`, `SnLv` and the decoded per-player 2-bit contact levels, plus the canary self-test. | +| `watch-hits.txt` | 24 hardware watchpoint hits — 20 writes to Spica's `SnLv` (all from 0x0086a789) and 4 to its `NVO` record's `{touched, TShn}` pair. | +| `saves/l1-t1t2-post.sav` | post-turn autosave of the hive-creation turn, **full L1 hook set**. | +| `saves/l1-t1t2-post-hooksoff.sav` | the same turn with **`hooks=off`, nothing installed**. It differs from the line above AND from the historical `turn2-state.sav`, in one field: player 3's `ResTNm`. See the finding's §6.1 — this pair is why `turn1-state.sav` must not be used as a determinism oracle. | +| `saves/l1-t2t3-pre.sav`, `saves/l1-t2t3-post.sav` | the second bracket's two autosaves. | +| `rep.py` | the reader used for every table in the finding: prints each record's hook, depth, `rng` word delta and non-pointer arguments. `uv run python3 rep.py trace-et2.jsonl`. | diff --git a/verify/results/shim/l1/rep.py b/verify/results/shim/l1/rep.py new file mode 100644 index 0000000..d14719b --- /dev/null +++ b/verify/results/shim/l1/rep.py @@ -0,0 +1,36 @@ +import json, sys + +SKIP = {'rng','rng_words_in','rng_left_in','server','host','out_name','results', + 'queen','slavers','server_cached','host_plus_0x54','sites','thresholds'} + +def val(x): + if isinstance(x, dict) and 't' in x: + v = x.get('v') + if x['t'] == 'struct' and isinstance(v, dict): + return {k: val(w) for k, w in v.items()} + if x['t'] == 'list' and isinstance(v, list): + return [val(w) for w in v] + return v + return x + +for path in sys.argv[1:]: + print("="*72); print(path) + for ln in open(path): + r = json.loads(ln) + if 'hook' not in r: + print("meta:", r.get('build_id') or list(r)[:6]); continue + hook = r['hook']; depth = r.get('depth') + side = r.get('side') or {} + rng = side.get('rng') or {} + b = val(rng.get('before')) or {}; a = val(rng.get('after')) or {} + bw, aw = b.get('words'), a.get('words') + d = (aw - bw) if isinstance(aw, int) and isinstance(bw, int) else None + print(f"[d{depth}] {hook:52s} words {bw} -> {aw} DELTA {d}") + for arg in (r.get('args') or []): + n = arg.get('n') + if n in SKIP: continue + print(f" {n} = {json.dumps(val(arg))}") + for nm, s in side.items(): + if nm == 'rng': continue + print(f" side[{nm}] {json.dumps(val(s.get('before')))} -> {json.dumps(val(s.get('after')))}") + if r.get('err'): print(" ERR", r['err']) diff --git a/verify/results/shim/l1/saves/l1-t1t2-post-hooksoff.sav b/verify/results/shim/l1/saves/l1-t1t2-post-hooksoff.sav new file mode 100644 index 0000000..91461b5 Binary files /dev/null and b/verify/results/shim/l1/saves/l1-t1t2-post-hooksoff.sav differ diff --git a/verify/results/shim/l1/saves/l1-t1t2-post.sav b/verify/results/shim/l1/saves/l1-t1t2-post.sav new file mode 100644 index 0000000..6de7680 Binary files /dev/null and b/verify/results/shim/l1/saves/l1-t1t2-post.sav differ diff --git a/verify/results/shim/l1/saves/l1-t2t3-post.sav b/verify/results/shim/l1/saves/l1-t2t3-post.sav new file mode 100644 index 0000000..9a8522a Binary files /dev/null and b/verify/results/shim/l1/saves/l1-t2t3-post.sav differ diff --git a/verify/results/shim/l1/saves/l1-t2t3-pre.sav b/verify/results/shim/l1/saves/l1-t2t3-pre.sav new file mode 100644 index 0000000..de31ae0 Binary files /dev/null and b/verify/results/shim/l1/saves/l1-t2t3-pre.sav differ diff --git a/verify/results/shim/l1/trace-et1.jsonl b/verify/results/shim/l1/trace-et1.jsonl new file mode 100644 index 0000000..43bf775 --- /dev/null +++ b/verify/results/shim/l1/trace-et1.jsonl @@ -0,0 +1,14 @@ +{"meta":{"format":1,"build":"0117495-dirty-20260908T2113Z","exe_sha256":"970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841","started":"2026-09-08T21:16:37Z","inline_max":64,"hooks":{"Shim::SelfTest::Fill":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"complete","why":"Fill writes buf[0..n) and nothing else; the whole range is a declared region","unmodelled":[]}},"Mars::GlobalConsts::LoadFile":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"erases each consumed key from the caller's std::map","risk":"medium","why":"the map is a LoadAll temporary; declaring a red-black tree as a region is not possible before the call. First-occurrence-wins is reproduced in game::config::apply instead, so the *effect* is modelled, the container is not","mitigation":"LoadAll's post-state would have to be hooked to see it"},{"what":"writes three kinds of line to the game log (unrecognised key, applied key, expected-but-not-found)","risk":"low","why":"log text is not part of the simulation state","mitigation":""},{"what":"opens the file through the VFS and allocates/releases a refcounted buffer","risk":"low","why":"ours performs the same two calls, so allocation behaviour matches by construction rather than by comparison","mitigation":""},{"what":"String slots assign through the engine's own std::string, leaking one heap block per long string in compare mode","risk":"low","why":"start-up only; documented in docs/M1.md","mitigation":""}]}},"Game::ServerPlayer::ComputeBudget":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend)","risk":"high","why":"they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing","mitigation":"declared input boundary; see budget_inputs.h"},{"what":"ServerSystem::ComputeOutput repairs damaged ships in orbit","risk":"high","why":"replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it","mitigation":"guard:budget_object does not reach the ships; unverified"},{"what":"the difficulty-mods row from StrategyServer::GetDifficultyMods","risk":"medium","why":"not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs","mitigation":""},{"what":"the research-allocation vector's heap block","risk":"low","why":"only the element count is compared; the three words are heap pointers the default policy ignores","mitigation":""}]}},"Game::WeaponDictionary::Init":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"LoadWeapon -> WeaponDef::ParseScript registers each weapon's name with the string table and resolves `requires` against the live TechTree","risk":"high","why":"per-file parsing is M3 scope; ours delegates to the game's own LoadWeapon, so a compare run performs the registration a SECOND time and neither the string table nor the tech tree is a declared region","mitigation":"suspected cause of the sibling section hook's compare crash (docs/M2.md)"},{"what":"allocates 123 WeaponDef objects (0x278 bytes each) on the game heap","risk":"low","why":"the definitions do not exist when the hook is entered, so they cannot be a before-snapshot; the dictionary region compares them by id/name/path","mitigation":""},{"what":"the word at dictionary+0x14","risk":"low","why":"not modelled; emitted as an opaque pointer, which the default policy ignores -- a change is visible in a trace but never a divergence","mitigation":"guard:dict"},{"what":"writes lines to the game log for a missing manifest","risk":"low","why":"log text is not simulation state","mitigation":""},{"what":"std::sort tie order for equal weapon names","risk":"low","why":"msvc_sort.h replays MSVC 2010's introsort, but the shipped data has no tied names, so the tie rule is unexercised rather than verified","mitigation":""}]}},"Game::SectionDictionary::SectionDictionary":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"LoadSection registers each section with the string table and the live TechTree, and may append to the dictionary's own vector","risk":"high","why":"M3 scope; ours delegates to the game's LoadSection after the original has already built all 885 definitions, so the second pass registers duplicates -- the leading hypothesis for this hook's compare-mode crash","mitigation":"see docs/M2.md; compare mode for this hook is not safe to run"},{"what":"post-load validation pass over every definition's @-token against the string table","risk":"medium","why":"runs after the loop and touches no declared region","mitigation":""},{"what":"allocates 885 SectionDef objects (0x3d8 bytes each) on the game heap","risk":"low","why":"they do not exist at hook entry; compared by index/species/id/token","mitigation":""},{"what":"the word at dictionary+0x14","risk":"low","why":"not modelled; emitted as an ignored pointer","mitigation":"guard:dict"},{"what":"the before-snapshot of the object is uninitialised heap","risk":"medium","why":"the hook is on the constructor, so `before` is meaningless and only `after` carries information","mitigation":""}]}},"Game::TechTree::ProcessResearch":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage: ours reproduces the decision and the id sequence, so region:events compares next_id, but the composed EvDsc/EvMsg text is not reproduced and no region can see it","risk":"medium","why":"text comes from the game's string table, which the engine must not carry; ours posts into its own EventStorage and writes only the counts into the scratch copy, so no live byte moves and replace mode posts nothing at all","mitigation":"region:events"},{"what":"composes EVENT_TECHS_UNLOCKED's message from the unlocked techs' names","risk":"low","why":"the trigger and the list are modelled (SetResearched's availability sweep plus the tail collector, both read off the instruction stream), so region:events compares next_id; the names come from the game's string table, so the message is composed from node indices instead and is not the game's text","mitigation":"region:events"},{"what":"TechTree::SetResearched in REPLACE mode: nothing of it runs","risk":"high","why":"the cascade is compare-mode only. In replace mode every pointer is live game memory, and applying half of OnTechResearched -- the observed-tech append and the research-event roll, but not the tech-effect field writes -- would leave the player in a state no code path produces. A replace run therefore still leaves the completed node unstamped and no tech unlocked","mitigation":"guard:player, guard:tree_header"},{"what":"ServerPlayer::OnTechResearched's tech effects: the ~90 hard-coded ServerPlayer field writes, the plague-cure masks, the design-option bitmasks and the species tech flags","risk":"high","why":"B2's milestone. `ours` models only the two parts of the callback this hook's regions can see -- the observed-tech append and the RNG word RollResearchEvent draws before its branch (one word on a missed roll, two on a fired plague roll) -- and the rest is what the player guard reports","mitigation":"guard:player"},{"what":"the research-event branch RollResearchEvent takes when its roll beats the odds (ServerPlayer::OnResearchRollSucceeded: the plague and AI-rebellion event paths)","risk":"high","why":"RollResearchEvent draws one NextFloat unconditionally and that draw IS modelled -- but that is only the cost of REACHING the branch. A FIRED roll costs one or two words: the plague path draws a SECOND word (NextInt) to pick an owned system and posts EVENT_PLAGUE_OUTBREAK, while the rebellion path allocates an AIRebellion at ServerPlayer+0x3b8 and CANCELS the current research (no further draw). The branch is entered only for the plague and AI-rebellion tech families, whose odds are 0 everywhere else, and it has never been observed firing in three sessions -- which is why every earlier note in this repo said 'exactly one NextFloat' and nothing caught it. If it is ever entered, region:rng is the check","mitigation":"region:rng"},{"what":"constructs the ObservedTech element it appends to ServerPlayer+0x274","risk":"medium","why":"`ours` models the append DECISION -- RecordObservedTech de-duplicates by tech name, so it decides whether the vector grows -- and moves the scratch header's byte span by one 0x2c element per append. The element's own fields (turn_first, turn_last, detected, the name string, `with`) are not built, and no region can see them","mitigation":"region:observed_techs"},{"what":"the tree's completion-order counter (TechTree+0x20) is read pre-call, not modelled as a region","risk":"low","why":"the per-node `order` word IS compared, and it is stamped from a counter `ours` seeds from the pre-call read and advances itself; the counter's own final value is only seen by the tree_header guard","mitigation":"guard:tree_header"},{"what":"writes a completion line to the game log","risk":"low","why":"log text is not simulation state","mitigation":""}]}},"Game::ServerPlayer::OnTechResearched":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent","risk":"high","why":"the same class of write as B3's defect, and this hook has no replace-mode oracle that could catch it: gotcha 4 in docs/B2.md says a changed save hash on a completion turn is expected and therefore not a finding","mitigation":"guard:player (EventStorage is inline at ServerPlayer+0x29c)"},{"what":"writes every owned system's AI flag (CCC_AIVrus / CCC_AISlv), re-evaluates the arcology civilian cap, cures addiction and clears plague across systems AND ships","risk":"high","why":"writes through pointers to other objects; compare mode must not touch live state, and no region reaches them","mitigation":""},{"what":"the pending plague-cure roll (ServerPlayer::RollResearchEvent)","risk":"high","why":"it draws one word from the strategic generator unconditionally, and a SECOND word (NextInt) when the roll beats the odds and takes the plague path -- which also posts EVENT_PLAGUE_OUTBREAK, while the rebellion path cancels the research. So a fired roll costs one or two words, not one; running it in compare mode would consume real randomness. The two words it guards are still cleared and the record says whether it would have fired","mitigation":"this is the extra draw B3 observed on a completion"},{"what":"TechTree::SetResearched for the Zuul boarding-pod grant","risk":"medium","why":"it would mutate the live tree, and it recurses","mitigation":""},{"what":"allocates or frees the node-bore block at ServerPlayer+0x308","risk":"medium","why":"ours has no allocator the game's runtime could free, so replace mode calls the game's own updater -- which means replace mode never exercises our node-bore selection at all","mitigation":"region:node_bore, declared only when the block already exists"}]}},"Game::ServerSystem::ProcessTurn":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history","risk":"high","why":"the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged","mitigation":"guard:system"},{"what":"every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue","risk":"high","why":"declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters","mitigation":"guard:system covers the system object only, not the other objects"},{"what":"ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer","risk":"medium","why":"writes through a pointer to another object; no region reaches the player","mitigation":""},{"what":"ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent","risk":"low","why":"the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled","mitigation":""},{"what":"replace mode is refused for this hook","risk":"medium","why":"our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook","mitigation":""}]}},"Game::ServerSystem::GroupOutput":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"the imperial and civilian output modifiers, the 1.8 factor and the 500000 divisor are literals inside the executable, so nothing about them is assumed from the data files","unmodelled":[{"what":"the station count that scales the imperial term","risk":"high","why":"the original gets it from a helper that walks the system's fleets and their ships through virtual calls and takes the system in EBX, which a hook cannot call portably. `ours` therefore assumes ZERO stations. A divergence on an imperial row is expected to be exactly the station factor, and the record carries `count` and the return, so the factor is MEASURED from the trace rather than fitted","mitigation":"declared input boundary; the trace makes it recoverable"},{"what":"the slave row's output modifier, and every value the data files supply","risk":"medium","why":"SLAVES_OUTPUT_MOD, the two morale thresholds and their two modifiers, and STATION_BONUS_IMPERIAL_OUTPUT are read out of the live process's globals and logged with every record, so the record says which value drove it","mitigation":"logged as `tuning` on every record"}]}},"Game::ServerSystem::ComputeTotalOutput":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"the station count, as for GroupOutput","risk":"high","why":"`ours` assumes zero stations, so a system with an imperial population and a station diverges by the station factor","mitigation":"declared input boundary"},{"what":"the slave population and its xenotech adjustment","risk":"high","why":"the original's slave count is not a plain field: it runs the count through a per-species xenotech factor. `ours` takes the slave term as ZERO, so any system holding slaves diverges. The record logs the raw group-2 population sums so a divergence can be attributed","mitigation":"declared input boundary"},{"what":"the capacity surplus the civilian term adds for the owner's own species","risk":"medium","why":"the original calls the carrying-capacity helper twice with different out-parameter slots and adds max(0, B - A) to the civilian count; `ours` uses the raw civilian population. The surplus is zero except when the colony is at its cap","mitigation":"declared input boundary"},{"what":"the addiction phase","risk":"low","why":"`ours` assumes it is below 3, so ADDICTION_OUTPUT_MOD never applies; the record logs the system's addiction table length so the case is visible","mitigation":""}]}},"Game::ServerPlayer::ProcessTurn":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"phases 2, 3 and 6 -- the savings apply Sav = SatAdd(budget[0], net), the three aid/trade records copied out of the budget, and the research refund -- are NOT declared and NOT compared","risk":"high","why":"all three are pure functions of ComputeBudget's 22-slot output and of ProcessResearch's overBudget out-parameter, and both live in the original's own stack frame ([ebp-0x90] and [ebp-0x14]). The three ways to reach them are each worse than not having them: call ComputeBudget ourselves (it repairs damaged ships in orbit -- harness-audit #6), read them out of the nested B1/B3 hooks (harness-audit #5, the self-fulfilling compare), or infer them from the observed Sav delta (the same trap). The formulas ARE written and unit-tested in player_turn_inputs.cpp; they are simply not wired into the verdict","mitigation":"guard:player reports Sav, +0xc8 and +0xcc moving"},{"what":"ComputeBudget (0x00863030) itself, including ServerSystem::ComputeOutput, which REPAIRS DAMAGED SHIPS IN ORBIT","risk":"high","why":"B1 is the hook that checks the budget; its ship-repair side effect happens inside this call too, once per system per player per turn","mitigation":"run hook.Game::ServerPlayer::ComputeBudget=compare alongside"},{"what":"TechTree::ProcessResearch and the whole SetResearched / OnTechResearched cascade it can trigger: progress, the completion roll, the 5% decay sweep, the child-unlock cascade, ~90 tech-effect field writes, EVENT_RESEARCH_* and EVENT_TECHS_UNLOCKED","risk":"high","why":"declared input boundary. B3/U own it; this hook reports the pre-call research state and lets the player guard say how much moved","mitigation":"guard:player, and hook.Game::TechTree::ProcessResearch=compare"},{"what":"RollResearchAccident (0x00889dc0) draws Mars::RNG::NextInt(100) whenever the research boost is non-zero, can apply progress loss across EVERY system, and posts EVENT_LABACCIDENT_{SMALL,MEDIUM,LARGE}","risk":"high","why":"it runs before ProcessResearch and gates it entirely. Its draws move the declared `rng` region, so they are visible -- but as an unexplained RNG divergence, not as their cause","mitigation":"region:rng shows the draws; nothing shows the systems it wrote"},{"what":"the ResearchRollPending roll: RollResearchEvent draws one NextFloat, and when it BEATS the odds the plague branch draws a SECOND word (NextInt) and posts EVENT_PLAGUE_OUTBREAK, while the rebellion branch allocates an AIRebellion at ServerPlayer+0x3b8 and cancels the current research","risk":"high","why":"every previous coverage note in this repo says 'exactly one NextFloat'. That is the cost of REACHING the branch; a fired roll costs one or two words. The branch has never been observed firing. `predict_roll` says whether we expected the roll, `region:rng` says what it actually cost, and `roll_flags` says whether ResErrRoll was consumed","mitigation":"region:rng + region:roll_flags + guard:player; ours never rolls"},{"what":"ServerPlayer::ProcessSpecialProjects (0x00840fe0) and, for an AI player, ConstructionSpend (0x00817f90)","risk":"medium","why":"1015 bytes of special-project state, plus the log line \"SpecialProject: %s completed investigation phase\"","mitigation":"guard:player"},{"what":"the EVENT_NO_RESEARCH post at 0x0089168c, which appends to the player's EventStorage (+0x29c) and bumps EvNxID (+0x2b0)","risk":"medium","why":"B3's exact failure mode: a list append outside every Result region. Its condition is fully known (ResT == 0, nothing completed this turn, and at least one state-2 tech exists) but ours posts nothing","mitigation":"guard:player reports the EvNxID bump"},{"what":"PruneRaidTargets (0x00863cf0) erases from the heap vector at +0x338","risk":"low","why":"the vector header is inside the player guard, but its elements are heap memory no region reaches, and the erase runs each removed element's scalar destructor","mitigation":"guard:player sees the header move only"},{"what":"the timed-bonus element regions are captured from the PRE-CALL element addresses, and the sweep may shrink the vector under them","risk":"low","why":"erase moves data down inside the same buffer and only lowers _Mylast, so the addresses stay valid and readable. Elements past the new _Mylast are compared as stale bytes; `bonus_header` is the region that carries the count, and ours writes the same stale tail the original leaves","mitigation":"region:bonus_header"},{"what":"replace mode is refused for this hook","risk":"medium","why":"ours models the driver's own writes and none of ComputeBudget, ProcessResearch, ProcessSpecialProjects or the raid-target prune. A replace run would leave a player in a state no code path produces, and there is therefore no oracle layer behind this compare","mitigation":""}]}},"Game::StrategyServer::MoveFleet":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)","risk":"high","why":"declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals","mitigation":"guard:fleet sees the fleet's own words; the event and the system do not"},{"what":"on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits","risk":"high","why":"writes through pointers to ships and to the system","mitigation":""},{"what":"the tanker top-up refuels other ships in the fleet","risk":"medium","why":"the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason","mitigation":""},{"what":"a node-line waypoint's step comes from the stutter profile","risk":"medium","why":"NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded","mitigation":"declared gap: docs/B4.md"},{"what":"a missed probabilistic jump scatters the fleet in a random direction","risk":"medium","why":"the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss","mitigation":""},{"what":"the route revalidation and the waypoint list itself","risk":"medium","why":"declared input boundary; the waypoint vector is not a region","mitigation":""}]}},"Game::StrategyServer::ProcessFleetMovement":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"`ours` re-reads the LIVE fleet list after the original has run","risk":"high","why":"the gate-traffic total is computed by the original at the very end of the pass, so a pre-call snapshot would diverge for the wrong reason. It breaks the compare invariant that ours never touches live memory, and it makes this hook's verdict partly self-fulfilling: the input to our arithmetic is the original's own post-move state","mitigation":""},{"what":"drives MoveFleet up to five times per fleet","risk":"high","why":"every undeclared effect of MoveFleet happens inside this call too; the pass schedule is recorded in the arguments but never compared","mitigation":""},{"what":"writes FPdpos into every fleet and clears flags 0x2 and 0x100 on every fleet","risk":"high","why":"no region covers the fleets, only the players' gate-traffic words","mitigation":""},{"what":"OnFleetArrived posts EVENT_FLEET_ARRIVED","risk":"high","why":"the same class of write as B3's defect, and there is no replace mode for this hook, so nothing behind the compare could catch it either","mitigation":""},{"what":"the original accumulates by player->index but writes back by the player's position in the server vector, into a fixed 32-int array with no bounds check","risk":"medium","why":"a real latent bug in the original that our side reproduces only while index == position; the reference save never separates them","mitigation":""},{"what":"PassSchedule() is never called by the hook, and FleetSummary::targetFleetId / relation are never filled","risk":"medium","why":"the header claims ours predicts the call order for a trace to check; that prediction is not actually emitted","mitigation":""}]}},"Game::StrategyHost::Autosave":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the whole save write: four path buffers, the ENDTURN pair removal, the backup rotation, the per-player connection detach/reattach, and SaveGame_WriteFile 0x00877070 itself","risk":"low","why":"this hook exists to timestamp the generator at the two moments the two save files are written. It is a marker and models nothing","mitigation":"region:rng only"},{"what":"the generator is reached through a CACHED StrategyServer pointer, not from this call's own arguments","risk":"medium","why":"`this` is the global at 0x00b29f98, hardcoded by both call sites, and no argument here names the server. On the first pre-turn autosave after a load no turn driver has run yet, so the cache is empty and that record carries no ledger position -- the FIRST BRACKET OF A SESSION IS INCOMPLETE BY CONSTRUCTION and must not be read as a zero-cost turn","mitigation":"arg:server_cached / host_plus_0x54 / server_agrees say which pointer was used and whether the +0x54 candidate is the same object"}]}},"Game::StrategyServer::ProcessTurn":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"no model of the turn's RNG cost: 32 phases, each of which may draw","risk":"high","why":"ProcessResearch's completion roll, RollResearchAccident's NextInt(100), the ResearchRollPending roll (one word, or two on the plague path), and whatever ProcessStations / ProcessSurrenders / ProcessMissions / ProcessSpecialProjects spend -- none of which has ever been measured. `ours` predicts nothing and the record reports the measurement","mitigation":"region:rng measures the total; the per-phase split is not resolved here"},{"what":"this hook takes the address the fpu module also wants to sample","risk":"low","why":"MinHook allows one hook per target. `fpu.sample_turn=off` releases StrategyServer::ProcessTurn so this hook can install; with it on, this hook fails to install and the trace is missing half the ledger","mitigation":"shim.log records the MH_CreateHook status for both"}]}},"Game::StrategyServer::OnAllCombatDone_Tail":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"36 phases, of which two can draw and neither is modelled here","risk":"high","why":"phase 6 reaches the unread 7499-byte combat resolver 0x007d5af0 (NextInt on the node-cannon path, Twist plus NextInt on the salvage path) and phase 11 draws one word per expired node line. `predict_nodeline_words` covers only the second, and the nested ApplyEncounterResult / NodeLineDecay hooks are what attribute the split","mitigation":"region:rng plus the two nested hooks"},{"what":"whether this handler runs on a turn with NO combat is what this hook is here to settle, and until it has run it is a hypothesis","risk":"medium","why":"combat-done-tail.md \u00c2\u00a76 infers it from the determinism note -- the post-turn autosave appears on every End Turn and this handler is its only reachable caller -- not from the instruction stream","mitigation":"arg:encounters says how many encounters this call saw; a call with 0 settles it"}]}},"Game::StrategyServer::ApplyEncounterResult":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the combat resolver 0x007d5af0 (7499 B) is completely unread","risk":"high","why":"this hook measures what its subtree spends and models none of it. Nothing about combat determinism can be settled until that function is read; this only puts a number on the hole","mitigation":"region:rng measures the subtotal"},{"what":"the ~0xea0-byte combat report, the CombatReport list append at S+0x1fc, the ClientEncounterResults push into S+0x2f4, the per-ship turn stamps and the pairwise engagement bits","risk":"medium","why":"all of it is game state this hook does not declare and does not check","mitigation":""}]}},"Game::StrategyServer::NodeLineDecay":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the collapse itself: 0x007a92e0 (690 B) and 0x007a4700 (2244 B) destroy or halt fleets and post EVENT_NODEDECAY_FLEET_DESTROYED_VIANODE / _HALTED / _HALTED_VIANODE, and loop 3 posts two more decay-stage events","risk":"high","why":"ours advances the generator and writes nothing else. In compare mode that is the intent -- the check is the word count -- but it means a clean verdict here says nothing about which lines actually collapsed","mitigation":"region:rng only"},{"what":"the draw-count model is verified by a DIRECT-call sweep of the downstream pair; their subtrees contain unresolved indirect call sites","risk":"medium","why":"if one of those vtable slots reaches a generator, the measured delta will exceed `predict_words` and this hook will diverge -- which is the correct outcome, and the reason the prediction is recorded as an argument","mitigation":"arg:predict_words vs region:rng is exactly that check"},{"what":"the expiry formula reproduces a signed idiv on nptf/npdtf without knowing whether nptf can be negative","risk":"low","why":"the original never sign-checks the traffic accumulator. The model truncates toward zero the same way; if the field is always non-negative the question never arises, and no save has been observed with a negative one","mitigation":""}]}},"Game::StrategyServer::ProcessNodeSpaceTravel":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"2945 bytes of node-space movement, entirely unmodelled and never swept for RNG by any lane","risk":"medium","why":"it is hooked here only because it runs TWICE a turn -- ProcessTurn phase 7 and tail phase 10 -- so a draw inside it would be double-counted by anyone modelling it once. The record says whether it draws at all","mitigation":"region:rng"}]}},"Game::EncounterDetect::AssignContacts":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the contact-to-detector assignment itself, and the two-pass outer loop","risk":"medium","why":"this hook exists because the draw here is INLINED and therefore invisible to every call-graph sweep and to the entry-point detours -- it is the one site in ProcessTurn's closure that neither instrument can see. It measures the word cost of the whole call and models nothing","mitigation":"region:rng; arg:detectors/contacts/max_trials bound the expected count"},{"what":"the per-trial threshold is 0.25f or 0.0f depending on two tech lookups, and the accept test short-circuits the inner loop","risk":"low","why":"so the measured cost is between |contacts| and |contacts| x |detectors| and the exact number depends on tech state this hook does not read","mitigation":"arg:max_trials is the upper bound only"}]}},"Game::EncounterDetect::ProcessTeamRecord":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the whole body of ProcessTeamRecord: the gate call, the two vector builds and the bucket construction","risk":"medium","why":"this hook measures the word cost of the call and recomputes three integers the original derives from the same record. It models none of the work and asserts nothing about the contact assignment","mitigation":"region:rng; arg:gate/pred_contacts/pred_detectors are a prediction, not a check"},{"what":"`+0xfc` and `+0xfb` are read as BYTES, following lane I's reading of the classifier functions","risk":"medium","why":"if either is really a wider field, every count here is wrong in the same direction and no cross-check inside this hook would notice. So the dword at +0xfc is read as well and any row where the two disagree is COUNTED, not silently resolved -- a non-zero disagreement count means the byte reading is unsafe on this workload","mitigation":"arg:fc_byte_vs_dword_disagreements; arg:entry_flags carries the raw values"},{"what":"an entry whose object pointer is unreadable is skipped","risk":"low","why":"it is not counted into any of the three classes, so entries != contacts + detectors + neither is the signal that this happened","mitigation":"arg:entries against the three class counts"}]}}}}} +{"ts":347714711,"hook":"Game::StrategyHost::Autosave","mode":"trace","call_id":0,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x015a9f98","n":"host"},{"t":"ptr","v":"0x00cff754","n":"out_name"},{"t":"bool","v":true,"n":"end_turn"},{"t":"ptr","v":"0x00000000","n":"server_cached"},{"t":"ptr","v":"0x04627d50","n":"host_plus_0x54"},{"t":"bool","v":false,"n":"server_agrees"},{"t":"ptr","v":"0x00000000","n":"rng"},{"t":"null","v":null,"n":"rng_words_in"},{"t":"null","v":null,"n":"rng_left_in"}],"ret":{"t":"ptr","v":"0x00cff754"},"side":{}} +{"ts":347759966,"hook":"Game::SVSOSwarmQueen::RegisterHives","mode":"trace","call_id":3,"thread":8356,"depth":2,"args":[{"t":"ptr","v":"0x046f6b98","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":-1,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"null","v":null,"n":"hives"},{"t":"i32","v":28,"n":"systems"},{"t":"i32","v":2,"n":"systems_tagged"},{"t":"i32","v":2,"n":"predict_new_hives"},{"t":"i32","v":2,"n":"predict_min_words"},{"t":"struct","v":{"rva":{"t":"u32","v":7209476},"ptr":{"t":"u32","v":22595552},"value":{"t":"i32","v":20}},"n":"hive_nextq_lo"},{"t":"struct","v":{"rva":{"t":"u32","v":7209480},"ptr":{"t":"u32","v":22595556},"value":{"t":"i32","v":30}},"n":"hive_nextq_hi"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":170,"n":"rng_words_in"},{"t":"i32","v":454,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":454},"index":{"t":"i32","v":170},"block":{"t":"i32","v":0},"words":{"t":"i64","v":170},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347760297,"hook":"Game::SVSOSwarmQueen::TickHives","mode":"trace","call_id":4,"thread":8356,"depth":2,"args":[{"t":"ptr","v":"0x046f6b98","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":2,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"list","v":[{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e46e788"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":30},"queen":{"t":"ptr","v":"0x00000000"}}},{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e46dbe8"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":28},"queen":{"t":"ptr","v":"0x00000000"}}}],"n":"hives"},{"t":"struct","v":{"rva":{"t":"u32","v":7209488},"ptr":{"t":"u32","v":22595564},"value":{"t":"i32","v":10}},"n":"gate_frame_floor"},{"t":"struct","v":{"rva":{"t":"u32","v":7209512},"ptr":{"t":"u32","v":22413860},"value":{"t":"i32","v":5}},"n":"gate_queen_age"},{"t":"struct","v":{"rva":{"t":"u32","v":7209504},"ptr":{"t":"u32","v":22413852},"value":{"t":"i32","v":3}},"n":"spawn_list_cap"},{"t":"bool","v":true,"n":"predict_gate1_fails"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":172,"n":"rng_words_in"},{"t":"i32","v":452,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347760384,"hook":"Game::SVSOSwarmQueen::OnTurnBegin","mode":"trace","call_id":2,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x046f6b98","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":-1,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"null","v":null,"n":"hives"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":170,"n":"rng_words_in"},{"t":"i32","v":454,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":454},"index":{"t":"i32","v":170},"block":{"t":"i32","v":0},"words":{"t":"i64","v":170},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347760893,"hook":"Game::StrategyServer::BeginProcessTurn","mode":"trace","call_id":1,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":1,"n":"turn"},{"t":"i32","v":0,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":-1,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":170,"n":"rng_words_in"},{"t":"i32","v":454,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":454},"index":{"t":"i32","v":170},"block":{"t":"i32","v":0},"words":{"t":"i64","v":170},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}}},"frame":{"before":{"t":"struct","v":{"value":{"t":"i32","v":1}}},"after":{"t":"struct","v":{"value":{"t":"i32","v":2}}}}}} +{"ts":347761193,"hook":"Game::StrategyServer::ProcessNodeSpaceTravel","mode":"trace","call_id":6,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":11,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":-1,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":172,"n":"rng_words_in"},{"t":"i32","v":452,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347763107,"hook":"Game::StrategyServer::ProcessTurn","mode":"trace","call_id":5,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"f32","v":1,"n":"dt"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":10,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":-1,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":172,"n":"rng_words_in"},{"t":"i32","v":452,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347798130,"hook":"Game::StrategyServer::ApplyEncounterResult","mode":"trace","call_id":8,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"ptr","v":"0x351c8c70","n":"encounter"},{"t":"ptr","v":"0x3522b5a0","n":"result"},{"t":"u8","v":1,"n":"res_no_battle"},{"t":"u8","v":0,"n":"res_peaceful"},{"t":"u8","v":0,"n":"res_surrendered"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":12,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":1,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347798320,"hook":"Game::StrategyServer::ProcessNodeSpaceTravel","mode":"trace","call_id":9,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":12,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347798478,"hook":"Game::StrategyServer::NodeLineDecay","mode":"trace","call_id":10,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":12,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"i32","v":43,"n":"node_paths"},{"t":"i32","v":43,"n":"np_permanent"},{"t":"i32","v":0,"n":"np_immortal"},{"t":"i32","v":0,"n":"np_mortal"},{"t":"i32","v":-1,"n":"np_min_life"},{"t":"i32","v":0,"n":"np_within5"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347798822,"hook":"Game::SVSOSlaversRefuel::UpdateDifficultyTier","mode":"trace","call_id":11,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x046f6a78","n":"slavers"},{"t":"i32","v":2,"n":"frame"},{"t":"i32","v":-1,"n":"cdiff_in"},{"t":"list","v":[{"t":"i32","v":1},{"t":"i32","v":50},{"t":"i32","v":100}],"n":"thresholds"},{"t":"str","v":"store","n":"predict_path"},{"t":"i32","v":0,"n":"predict_cdiff_out"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"cdiff":{"before":{"t":"struct","v":{"value":{"t":"i32","v":-1}}},"after":{"t":"struct","v":{"value":{"t":"i32","v":0}}}},"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347799377,"hook":"Game::StrategyServer::OnAllCombatDone_Tail","mode":"trace","call_id":7,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"ptr","v":"0x35375f0c","n":"results"},{"t":"i32","v":1,"n":"result_count"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":11,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":1,"n":"encounters"},{"t":"i32","v":43,"n":"node_paths"},{"t":"i32","v":43,"n":"np_permanent"},{"t":"i32","v":0,"n":"np_immortal"},{"t":"i32","v":0,"n":"np_mortal"},{"t":"i32","v":-1,"n":"np_min_life"},{"t":"i32","v":0,"n":"np_within5"},{"t":"i32","v":0,"n":"predict_nodeline_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347839195,"hook":"Game::StrategyHost::Autosave","mode":"trace","call_id":12,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x015a9f98","n":"host"},{"t":"ptr","v":"0x00cff544","n":"out_name"},{"t":"bool","v":false,"n":"end_turn"},{"t":"ptr","v":"0x0e483f40","n":"server_cached"},{"t":"ptr","v":"0x04627d50","n":"host_plus_0x54"},{"t":"bool","v":false,"n":"server_agrees"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"},{"t":"list","v":[{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":1210132},"calls":{"t":"u32","v":2},"words":{"t":"u32","v":2},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":1603725},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":4775764},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":5139977},"calls":{"t":"u32","v":16},"words":{"t":"u32","v":16},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":4797483},"calls":{"t":"u32","v":8},"words":{"t":"u32","v":8},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":4797720},"calls":{"t":"u32","v":8},"words":{"t":"u32","v":8},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":1061538},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":3746217},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":1546517},"calls":{"t":"u32","v":3},"words":{"t":"u32","v":3},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":5139977},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":1543445},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":2743342},"calls":{"t":"u32","v":3},"words":{"t":"u32","v":3},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}}],"n":"draw_sites"},{"t":"u32","v":38,"n":"draw_site_words"},{"t":"u32","v":38,"n":"draw_site_calls"},{"t":"u32","v":8,"n":"draw_site_words_other_rng"},{"t":"u32","v":8,"n":"draw_site_calls_other_rng"},{"t":"u32","v":0,"n":"draw_site_overflow"},{"t":"list","v":[{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot13"},"rva":{"t":"u32","v":4749232},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot14"},"rva":{"t":"u32","v":4774784},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot13"},"rva":{"t":"u32","v":4779904},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot15"},"rva":{"t":"u32","v":4377760},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::SpyManager::Slot13RngCallee"},"rva":{"t":"u32","v":4458720},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::TradeManager::Slot13RngCalleeA"},"rva":{"t":"u32","v":4328608},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::TradeManager::Slot13RngCalleeB"},"rva":{"t":"u32","v":4764736},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManager::CreateRaidEncounter"},"rva":{"t":"u32","v":4798624},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManager::GenerateTradeRaidEncounters"},"rva":{"t":"u32","v":4797072},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot15 [control]"},"rva":{"t":"u32","v":4751152},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot14 [control]"},"rva":{"t":"u32","v":4559056},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::EncounterDetect::Run [control]"},"rva":{"t":"u32","v":3977344},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}}],"n":"probe_entries"}],"ret":{"t":"ptr","v":"0x00cff544"},"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} diff --git a/verify/results/shim/l1/trace-et2.jsonl b/verify/results/shim/l1/trace-et2.jsonl new file mode 100644 index 0000000..e5d0d58 --- /dev/null +++ b/verify/results/shim/l1/trace-et2.jsonl @@ -0,0 +1,27 @@ +{"meta":{"format":1,"build":"0117495-dirty-20260908T2113Z","exe_sha256":"970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841","started":"2026-09-08T21:16:37Z","inline_max":64,"hooks":{"Shim::SelfTest::Fill":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"complete","why":"Fill writes buf[0..n) and nothing else; the whole range is a declared region","unmodelled":[]}},"Mars::GlobalConsts::LoadFile":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"erases each consumed key from the caller's std::map","risk":"medium","why":"the map is a LoadAll temporary; declaring a red-black tree as a region is not possible before the call. First-occurrence-wins is reproduced in game::config::apply instead, so the *effect* is modelled, the container is not","mitigation":"LoadAll's post-state would have to be hooked to see it"},{"what":"writes three kinds of line to the game log (unrecognised key, applied key, expected-but-not-found)","risk":"low","why":"log text is not part of the simulation state","mitigation":""},{"what":"opens the file through the VFS and allocates/releases a refcounted buffer","risk":"low","why":"ours performs the same two calls, so allocation behaviour matches by construction rather than by comparison","mitigation":""},{"what":"String slots assign through the engine's own std::string, leaking one heap block per long string in compare mode","risk":"low","why":"start-up only; documented in docs/M1.md","mitigation":""}]}},"Game::ServerPlayer::ComputeBudget":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend)","risk":"high","why":"they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing","mitigation":"declared input boundary; see budget_inputs.h"},{"what":"ServerSystem::ComputeOutput repairs damaged ships in orbit","risk":"high","why":"replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it","mitigation":"guard:budget_object does not reach the ships; unverified"},{"what":"the difficulty-mods row from StrategyServer::GetDifficultyMods","risk":"medium","why":"not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs","mitigation":""},{"what":"the research-allocation vector's heap block","risk":"low","why":"only the element count is compared; the three words are heap pointers the default policy ignores","mitigation":""}]}},"Game::WeaponDictionary::Init":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"LoadWeapon -> WeaponDef::ParseScript registers each weapon's name with the string table and resolves `requires` against the live TechTree","risk":"high","why":"per-file parsing is M3 scope; ours delegates to the game's own LoadWeapon, so a compare run performs the registration a SECOND time and neither the string table nor the tech tree is a declared region","mitigation":"suspected cause of the sibling section hook's compare crash (docs/M2.md)"},{"what":"allocates 123 WeaponDef objects (0x278 bytes each) on the game heap","risk":"low","why":"the definitions do not exist when the hook is entered, so they cannot be a before-snapshot; the dictionary region compares them by id/name/path","mitigation":""},{"what":"the word at dictionary+0x14","risk":"low","why":"not modelled; emitted as an opaque pointer, which the default policy ignores -- a change is visible in a trace but never a divergence","mitigation":"guard:dict"},{"what":"writes lines to the game log for a missing manifest","risk":"low","why":"log text is not simulation state","mitigation":""},{"what":"std::sort tie order for equal weapon names","risk":"low","why":"msvc_sort.h replays MSVC 2010's introsort, but the shipped data has no tied names, so the tie rule is unexercised rather than verified","mitigation":""}]}},"Game::SectionDictionary::SectionDictionary":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"LoadSection registers each section with the string table and the live TechTree, and may append to the dictionary's own vector","risk":"high","why":"M3 scope; ours delegates to the game's LoadSection after the original has already built all 885 definitions, so the second pass registers duplicates -- the leading hypothesis for this hook's compare-mode crash","mitigation":"see docs/M2.md; compare mode for this hook is not safe to run"},{"what":"post-load validation pass over every definition's @-token against the string table","risk":"medium","why":"runs after the loop and touches no declared region","mitigation":""},{"what":"allocates 885 SectionDef objects (0x3d8 bytes each) on the game heap","risk":"low","why":"they do not exist at hook entry; compared by index/species/id/token","mitigation":""},{"what":"the word at dictionary+0x14","risk":"low","why":"not modelled; emitted as an ignored pointer","mitigation":"guard:dict"},{"what":"the before-snapshot of the object is uninitialised heap","risk":"medium","why":"the hook is on the constructor, so `before` is meaningless and only `after` carries information","mitigation":""}]}},"Game::TechTree::ProcessResearch":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage: ours reproduces the decision and the id sequence, so region:events compares next_id, but the composed EvDsc/EvMsg text is not reproduced and no region can see it","risk":"medium","why":"text comes from the game's string table, which the engine must not carry; ours posts into its own EventStorage and writes only the counts into the scratch copy, so no live byte moves and replace mode posts nothing at all","mitigation":"region:events"},{"what":"composes EVENT_TECHS_UNLOCKED's message from the unlocked techs' names","risk":"low","why":"the trigger and the list are modelled (SetResearched's availability sweep plus the tail collector, both read off the instruction stream), so region:events compares next_id; the names come from the game's string table, so the message is composed from node indices instead and is not the game's text","mitigation":"region:events"},{"what":"TechTree::SetResearched in REPLACE mode: nothing of it runs","risk":"high","why":"the cascade is compare-mode only. In replace mode every pointer is live game memory, and applying half of OnTechResearched -- the observed-tech append and the research-event roll, but not the tech-effect field writes -- would leave the player in a state no code path produces. A replace run therefore still leaves the completed node unstamped and no tech unlocked","mitigation":"guard:player, guard:tree_header"},{"what":"ServerPlayer::OnTechResearched's tech effects: the ~90 hard-coded ServerPlayer field writes, the plague-cure masks, the design-option bitmasks and the species tech flags","risk":"high","why":"B2's milestone. `ours` models only the two parts of the callback this hook's regions can see -- the observed-tech append and the RNG word RollResearchEvent draws before its branch (one word on a missed roll, two on a fired plague roll) -- and the rest is what the player guard reports","mitigation":"guard:player"},{"what":"the research-event branch RollResearchEvent takes when its roll beats the odds (ServerPlayer::OnResearchRollSucceeded: the plague and AI-rebellion event paths)","risk":"high","why":"RollResearchEvent draws one NextFloat unconditionally and that draw IS modelled -- but that is only the cost of REACHING the branch. A FIRED roll costs one or two words: the plague path draws a SECOND word (NextInt) to pick an owned system and posts EVENT_PLAGUE_OUTBREAK, while the rebellion path allocates an AIRebellion at ServerPlayer+0x3b8 and CANCELS the current research (no further draw). The branch is entered only for the plague and AI-rebellion tech families, whose odds are 0 everywhere else, and it has never been observed firing in three sessions -- which is why every earlier note in this repo said 'exactly one NextFloat' and nothing caught it. If it is ever entered, region:rng is the check","mitigation":"region:rng"},{"what":"constructs the ObservedTech element it appends to ServerPlayer+0x274","risk":"medium","why":"`ours` models the append DECISION -- RecordObservedTech de-duplicates by tech name, so it decides whether the vector grows -- and moves the scratch header's byte span by one 0x2c element per append. The element's own fields (turn_first, turn_last, detected, the name string, `with`) are not built, and no region can see them","mitigation":"region:observed_techs"},{"what":"the tree's completion-order counter (TechTree+0x20) is read pre-call, not modelled as a region","risk":"low","why":"the per-node `order` word IS compared, and it is stamped from a counter `ours` seeds from the pre-call read and advances itself; the counter's own final value is only seen by the tree_header guard","mitigation":"guard:tree_header"},{"what":"writes a completion line to the game log","risk":"low","why":"log text is not simulation state","mitigation":""}]}},"Game::ServerPlayer::OnTechResearched":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent","risk":"high","why":"the same class of write as B3's defect, and this hook has no replace-mode oracle that could catch it: gotcha 4 in docs/B2.md says a changed save hash on a completion turn is expected and therefore not a finding","mitigation":"guard:player (EventStorage is inline at ServerPlayer+0x29c)"},{"what":"writes every owned system's AI flag (CCC_AIVrus / CCC_AISlv), re-evaluates the arcology civilian cap, cures addiction and clears plague across systems AND ships","risk":"high","why":"writes through pointers to other objects; compare mode must not touch live state, and no region reaches them","mitigation":""},{"what":"the pending plague-cure roll (ServerPlayer::RollResearchEvent)","risk":"high","why":"it draws one word from the strategic generator unconditionally, and a SECOND word (NextInt) when the roll beats the odds and takes the plague path -- which also posts EVENT_PLAGUE_OUTBREAK, while the rebellion path cancels the research. So a fired roll costs one or two words, not one; running it in compare mode would consume real randomness. The two words it guards are still cleared and the record says whether it would have fired","mitigation":"this is the extra draw B3 observed on a completion"},{"what":"TechTree::SetResearched for the Zuul boarding-pod grant","risk":"medium","why":"it would mutate the live tree, and it recurses","mitigation":""},{"what":"allocates or frees the node-bore block at ServerPlayer+0x308","risk":"medium","why":"ours has no allocator the game's runtime could free, so replace mode calls the game's own updater -- which means replace mode never exercises our node-bore selection at all","mitigation":"region:node_bore, declared only when the block already exists"}]}},"Game::ServerSystem::ProcessTurn":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history","risk":"high","why":"the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged","mitigation":"guard:system"},{"what":"every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue","risk":"high","why":"declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters","mitigation":"guard:system covers the system object only, not the other objects"},{"what":"ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer","risk":"medium","why":"writes through a pointer to another object; no region reaches the player","mitigation":""},{"what":"ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent","risk":"low","why":"the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled","mitigation":""},{"what":"replace mode is refused for this hook","risk":"medium","why":"our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook","mitigation":""}]}},"Game::ServerSystem::GroupOutput":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"the imperial and civilian output modifiers, the 1.8 factor and the 500000 divisor are literals inside the executable, so nothing about them is assumed from the data files","unmodelled":[{"what":"the station count that scales the imperial term","risk":"high","why":"the original gets it from a helper that walks the system's fleets and their ships through virtual calls and takes the system in EBX, which a hook cannot call portably. `ours` therefore assumes ZERO stations. A divergence on an imperial row is expected to be exactly the station factor, and the record carries `count` and the return, so the factor is MEASURED from the trace rather than fitted","mitigation":"declared input boundary; the trace makes it recoverable"},{"what":"the slave row's output modifier, and every value the data files supply","risk":"medium","why":"SLAVES_OUTPUT_MOD, the two morale thresholds and their two modifiers, and STATION_BONUS_IMPERIAL_OUTPUT are read out of the live process's globals and logged with every record, so the record says which value drove it","mitigation":"logged as `tuning` on every record"}]}},"Game::ServerSystem::ComputeTotalOutput":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"the station count, as for GroupOutput","risk":"high","why":"`ours` assumes zero stations, so a system with an imperial population and a station diverges by the station factor","mitigation":"declared input boundary"},{"what":"the slave population and its xenotech adjustment","risk":"high","why":"the original's slave count is not a plain field: it runs the count through a per-species xenotech factor. `ours` takes the slave term as ZERO, so any system holding slaves diverges. The record logs the raw group-2 population sums so a divergence can be attributed","mitigation":"declared input boundary"},{"what":"the capacity surplus the civilian term adds for the owner's own species","risk":"medium","why":"the original calls the carrying-capacity helper twice with different out-parameter slots and adds max(0, B - A) to the civilian count; `ours` uses the raw civilian population. The surplus is zero except when the colony is at its cap","mitigation":"declared input boundary"},{"what":"the addiction phase","risk":"low","why":"`ours` assumes it is below 3, so ADDICTION_OUTPUT_MOD never applies; the record logs the system's addiction table length so the case is visible","mitigation":""}]}},"Game::ServerPlayer::ProcessTurn":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"phases 2, 3 and 6 -- the savings apply Sav = SatAdd(budget[0], net), the three aid/trade records copied out of the budget, and the research refund -- are NOT declared and NOT compared","risk":"high","why":"all three are pure functions of ComputeBudget's 22-slot output and of ProcessResearch's overBudget out-parameter, and both live in the original's own stack frame ([ebp-0x90] and [ebp-0x14]). The three ways to reach them are each worse than not having them: call ComputeBudget ourselves (it repairs damaged ships in orbit -- harness-audit #6), read them out of the nested B1/B3 hooks (harness-audit #5, the self-fulfilling compare), or infer them from the observed Sav delta (the same trap). The formulas ARE written and unit-tested in player_turn_inputs.cpp; they are simply not wired into the verdict","mitigation":"guard:player reports Sav, +0xc8 and +0xcc moving"},{"what":"ComputeBudget (0x00863030) itself, including ServerSystem::ComputeOutput, which REPAIRS DAMAGED SHIPS IN ORBIT","risk":"high","why":"B1 is the hook that checks the budget; its ship-repair side effect happens inside this call too, once per system per player per turn","mitigation":"run hook.Game::ServerPlayer::ComputeBudget=compare alongside"},{"what":"TechTree::ProcessResearch and the whole SetResearched / OnTechResearched cascade it can trigger: progress, the completion roll, the 5% decay sweep, the child-unlock cascade, ~90 tech-effect field writes, EVENT_RESEARCH_* and EVENT_TECHS_UNLOCKED","risk":"high","why":"declared input boundary. B3/U own it; this hook reports the pre-call research state and lets the player guard say how much moved","mitigation":"guard:player, and hook.Game::TechTree::ProcessResearch=compare"},{"what":"RollResearchAccident (0x00889dc0) draws Mars::RNG::NextInt(100) whenever the research boost is non-zero, can apply progress loss across EVERY system, and posts EVENT_LABACCIDENT_{SMALL,MEDIUM,LARGE}","risk":"high","why":"it runs before ProcessResearch and gates it entirely. Its draws move the declared `rng` region, so they are visible -- but as an unexplained RNG divergence, not as their cause","mitigation":"region:rng shows the draws; nothing shows the systems it wrote"},{"what":"the ResearchRollPending roll: RollResearchEvent draws one NextFloat, and when it BEATS the odds the plague branch draws a SECOND word (NextInt) and posts EVENT_PLAGUE_OUTBREAK, while the rebellion branch allocates an AIRebellion at ServerPlayer+0x3b8 and cancels the current research","risk":"high","why":"every previous coverage note in this repo says 'exactly one NextFloat'. That is the cost of REACHING the branch; a fired roll costs one or two words. The branch has never been observed firing. `predict_roll` says whether we expected the roll, `region:rng` says what it actually cost, and `roll_flags` says whether ResErrRoll was consumed","mitigation":"region:rng + region:roll_flags + guard:player; ours never rolls"},{"what":"ServerPlayer::ProcessSpecialProjects (0x00840fe0) and, for an AI player, ConstructionSpend (0x00817f90)","risk":"medium","why":"1015 bytes of special-project state, plus the log line \"SpecialProject: %s completed investigation phase\"","mitigation":"guard:player"},{"what":"the EVENT_NO_RESEARCH post at 0x0089168c, which appends to the player's EventStorage (+0x29c) and bumps EvNxID (+0x2b0)","risk":"medium","why":"B3's exact failure mode: a list append outside every Result region. Its condition is fully known (ResT == 0, nothing completed this turn, and at least one state-2 tech exists) but ours posts nothing","mitigation":"guard:player reports the EvNxID bump"},{"what":"PruneRaidTargets (0x00863cf0) erases from the heap vector at +0x338","risk":"low","why":"the vector header is inside the player guard, but its elements are heap memory no region reaches, and the erase runs each removed element's scalar destructor","mitigation":"guard:player sees the header move only"},{"what":"the timed-bonus element regions are captured from the PRE-CALL element addresses, and the sweep may shrink the vector under them","risk":"low","why":"erase moves data down inside the same buffer and only lowers _Mylast, so the addresses stay valid and readable. Elements past the new _Mylast are compared as stale bytes; `bonus_header` is the region that carries the count, and ours writes the same stale tail the original leaves","mitigation":"region:bonus_header"},{"what":"replace mode is refused for this hook","risk":"medium","why":"ours models the driver's own writes and none of ComputeBudget, ProcessResearch, ProcessSpecialProjects or the raid-target prune. A replace run would leave a player in a state no code path produces, and there is therefore no oracle layer behind this compare","mitigation":""}]}},"Game::StrategyServer::MoveFleet":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)","risk":"high","why":"declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals","mitigation":"guard:fleet sees the fleet's own words; the event and the system do not"},{"what":"on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits","risk":"high","why":"writes through pointers to ships and to the system","mitigation":""},{"what":"the tanker top-up refuels other ships in the fleet","risk":"medium","why":"the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason","mitigation":""},{"what":"a node-line waypoint's step comes from the stutter profile","risk":"medium","why":"NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded","mitigation":"declared gap: docs/B4.md"},{"what":"a missed probabilistic jump scatters the fleet in a random direction","risk":"medium","why":"the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss","mitigation":""},{"what":"the route revalidation and the waypoint list itself","risk":"medium","why":"declared input boundary; the waypoint vector is not a region","mitigation":""}]}},"Game::StrategyServer::ProcessFleetMovement":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"`ours` re-reads the LIVE fleet list after the original has run","risk":"high","why":"the gate-traffic total is computed by the original at the very end of the pass, so a pre-call snapshot would diverge for the wrong reason. It breaks the compare invariant that ours never touches live memory, and it makes this hook's verdict partly self-fulfilling: the input to our arithmetic is the original's own post-move state","mitigation":""},{"what":"drives MoveFleet up to five times per fleet","risk":"high","why":"every undeclared effect of MoveFleet happens inside this call too; the pass schedule is recorded in the arguments but never compared","mitigation":""},{"what":"writes FPdpos into every fleet and clears flags 0x2 and 0x100 on every fleet","risk":"high","why":"no region covers the fleets, only the players' gate-traffic words","mitigation":""},{"what":"OnFleetArrived posts EVENT_FLEET_ARRIVED","risk":"high","why":"the same class of write as B3's defect, and there is no replace mode for this hook, so nothing behind the compare could catch it either","mitigation":""},{"what":"the original accumulates by player->index but writes back by the player's position in the server vector, into a fixed 32-int array with no bounds check","risk":"medium","why":"a real latent bug in the original that our side reproduces only while index == position; the reference save never separates them","mitigation":""},{"what":"PassSchedule() is never called by the hook, and FleetSummary::targetFleetId / relation are never filled","risk":"medium","why":"the header claims ours predicts the call order for a trace to check; that prediction is not actually emitted","mitigation":""}]}},"Game::StrategyHost::Autosave":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the whole save write: four path buffers, the ENDTURN pair removal, the backup rotation, the per-player connection detach/reattach, and SaveGame_WriteFile 0x00877070 itself","risk":"low","why":"this hook exists to timestamp the generator at the two moments the two save files are written. It is a marker and models nothing","mitigation":"region:rng only"},{"what":"the generator is reached through a CACHED StrategyServer pointer, not from this call's own arguments","risk":"medium","why":"`this` is the global at 0x00b29f98, hardcoded by both call sites, and no argument here names the server. On the first pre-turn autosave after a load no turn driver has run yet, so the cache is empty and that record carries no ledger position -- the FIRST BRACKET OF A SESSION IS INCOMPLETE BY CONSTRUCTION and must not be read as a zero-cost turn","mitigation":"arg:server_cached / host_plus_0x54 / server_agrees say which pointer was used and whether the +0x54 candidate is the same object"}]}},"Game::StrategyServer::ProcessTurn":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"no model of the turn's RNG cost: 32 phases, each of which may draw","risk":"high","why":"ProcessResearch's completion roll, RollResearchAccident's NextInt(100), the ResearchRollPending roll (one word, or two on the plague path), and whatever ProcessStations / ProcessSurrenders / ProcessMissions / ProcessSpecialProjects spend -- none of which has ever been measured. `ours` predicts nothing and the record reports the measurement","mitigation":"region:rng measures the total; the per-phase split is not resolved here"},{"what":"this hook takes the address the fpu module also wants to sample","risk":"low","why":"MinHook allows one hook per target. `fpu.sample_turn=off` releases StrategyServer::ProcessTurn so this hook can install; with it on, this hook fails to install and the trace is missing half the ledger","mitigation":"shim.log records the MH_CreateHook status for both"}]}},"Game::StrategyServer::OnAllCombatDone_Tail":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"36 phases, of which two can draw and neither is modelled here","risk":"high","why":"phase 6 reaches the unread 7499-byte combat resolver 0x007d5af0 (NextInt on the node-cannon path, Twist plus NextInt on the salvage path) and phase 11 draws one word per expired node line. `predict_nodeline_words` covers only the second, and the nested ApplyEncounterResult / NodeLineDecay hooks are what attribute the split","mitigation":"region:rng plus the two nested hooks"},{"what":"whether this handler runs on a turn with NO combat is what this hook is here to settle, and until it has run it is a hypothesis","risk":"medium","why":"combat-done-tail.md \u00c2\u00a76 infers it from the determinism note -- the post-turn autosave appears on every End Turn and this handler is its only reachable caller -- not from the instruction stream","mitigation":"arg:encounters says how many encounters this call saw; a call with 0 settles it"}]}},"Game::StrategyServer::ApplyEncounterResult":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the combat resolver 0x007d5af0 (7499 B) is completely unread","risk":"high","why":"this hook measures what its subtree spends and models none of it. Nothing about combat determinism can be settled until that function is read; this only puts a number on the hole","mitigation":"region:rng measures the subtotal"},{"what":"the ~0xea0-byte combat report, the CombatReport list append at S+0x1fc, the ClientEncounterResults push into S+0x2f4, the per-ship turn stamps and the pairwise engagement bits","risk":"medium","why":"all of it is game state this hook does not declare and does not check","mitigation":""}]}},"Game::StrategyServer::NodeLineDecay":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the collapse itself: 0x007a92e0 (690 B) and 0x007a4700 (2244 B) destroy or halt fleets and post EVENT_NODEDECAY_FLEET_DESTROYED_VIANODE / _HALTED / _HALTED_VIANODE, and loop 3 posts two more decay-stage events","risk":"high","why":"ours advances the generator and writes nothing else. In compare mode that is the intent -- the check is the word count -- but it means a clean verdict here says nothing about which lines actually collapsed","mitigation":"region:rng only"},{"what":"the draw-count model is verified by a DIRECT-call sweep of the downstream pair; their subtrees contain unresolved indirect call sites","risk":"medium","why":"if one of those vtable slots reaches a generator, the measured delta will exceed `predict_words` and this hook will diverge -- which is the correct outcome, and the reason the prediction is recorded as an argument","mitigation":"arg:predict_words vs region:rng is exactly that check"},{"what":"the expiry formula reproduces a signed idiv on nptf/npdtf without knowing whether nptf can be negative","risk":"low","why":"the original never sign-checks the traffic accumulator. The model truncates toward zero the same way; if the field is always non-negative the question never arises, and no save has been observed with a negative one","mitigation":""}]}},"Game::StrategyServer::ProcessNodeSpaceTravel":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"2945 bytes of node-space movement, entirely unmodelled and never swept for RNG by any lane","risk":"medium","why":"it is hooked here only because it runs TWICE a turn -- ProcessTurn phase 7 and tail phase 10 -- so a draw inside it would be double-counted by anyone modelling it once. The record says whether it draws at all","mitigation":"region:rng"}]}},"Game::EncounterDetect::AssignContacts":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the contact-to-detector assignment itself, and the two-pass outer loop","risk":"medium","why":"this hook exists because the draw here is INLINED and therefore invisible to every call-graph sweep and to the entry-point detours -- it is the one site in ProcessTurn's closure that neither instrument can see. It measures the word cost of the whole call and models nothing","mitigation":"region:rng; arg:detectors/contacts/max_trials bound the expected count"},{"what":"the per-trial threshold is 0.25f or 0.0f depending on two tech lookups, and the accept test short-circuits the inner loop","risk":"low","why":"so the measured cost is between |contacts| and |contacts| x |detectors| and the exact number depends on tech state this hook does not read","mitigation":"arg:max_trials is the upper bound only"}]}},"Game::EncounterDetect::ProcessTeamRecord":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the whole body of ProcessTeamRecord: the gate call, the two vector builds and the bucket construction","risk":"medium","why":"this hook measures the word cost of the call and recomputes three integers the original derives from the same record. It models none of the work and asserts nothing about the contact assignment","mitigation":"region:rng; arg:gate/pred_contacts/pred_detectors are a prediction, not a check"},{"what":"`+0xfc` and `+0xfb` are read as BYTES, following lane I's reading of the classifier functions","risk":"medium","why":"if either is really a wider field, every count here is wrong in the same direction and no cross-check inside this hook would notice. So the dword at +0xfc is read as well and any row where the two disagree is COUNTED, not silently resolved -- a non-zero disagreement count means the byte reading is unsafe on this workload","mitigation":"arg:fc_byte_vs_dword_disagreements; arg:entry_flags carries the raw values"},{"what":"an entry whose object pointer is unreadable is skipped","risk":"low","why":"it is not counted into any of the three classes, so entries != contacts + detectors + neither is the signal that this happened","mitigation":"arg:entries against the three class counts"}]}}}}} +{"ts":347714711,"hook":"Game::StrategyHost::Autosave","mode":"trace","call_id":0,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x015a9f98","n":"host"},{"t":"ptr","v":"0x00cff754","n":"out_name"},{"t":"bool","v":true,"n":"end_turn"},{"t":"ptr","v":"0x00000000","n":"server_cached"},{"t":"ptr","v":"0x04627d50","n":"host_plus_0x54"},{"t":"bool","v":false,"n":"server_agrees"},{"t":"ptr","v":"0x00000000","n":"rng"},{"t":"null","v":null,"n":"rng_words_in"},{"t":"null","v":null,"n":"rng_left_in"}],"ret":{"t":"ptr","v":"0x00cff754"},"side":{}} +{"ts":347759966,"hook":"Game::SVSOSwarmQueen::RegisterHives","mode":"trace","call_id":3,"thread":8356,"depth":2,"args":[{"t":"ptr","v":"0x046f6b98","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":-1,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"null","v":null,"n":"hives"},{"t":"i32","v":28,"n":"systems"},{"t":"i32","v":2,"n":"systems_tagged"},{"t":"i32","v":2,"n":"predict_new_hives"},{"t":"i32","v":2,"n":"predict_min_words"},{"t":"struct","v":{"rva":{"t":"u32","v":7209476},"ptr":{"t":"u32","v":22595552},"value":{"t":"i32","v":20}},"n":"hive_nextq_lo"},{"t":"struct","v":{"rva":{"t":"u32","v":7209480},"ptr":{"t":"u32","v":22595556},"value":{"t":"i32","v":30}},"n":"hive_nextq_hi"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":170,"n":"rng_words_in"},{"t":"i32","v":454,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":454},"index":{"t":"i32","v":170},"block":{"t":"i32","v":0},"words":{"t":"i64","v":170},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347760297,"hook":"Game::SVSOSwarmQueen::TickHives","mode":"trace","call_id":4,"thread":8356,"depth":2,"args":[{"t":"ptr","v":"0x046f6b98","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":2,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"list","v":[{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e46e788"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":30},"queen":{"t":"ptr","v":"0x00000000"}}},{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e46dbe8"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":28},"queen":{"t":"ptr","v":"0x00000000"}}}],"n":"hives"},{"t":"struct","v":{"rva":{"t":"u32","v":7209488},"ptr":{"t":"u32","v":22595564},"value":{"t":"i32","v":10}},"n":"gate_frame_floor"},{"t":"struct","v":{"rva":{"t":"u32","v":7209512},"ptr":{"t":"u32","v":22413860},"value":{"t":"i32","v":5}},"n":"gate_queen_age"},{"t":"struct","v":{"rva":{"t":"u32","v":7209504},"ptr":{"t":"u32","v":22413852},"value":{"t":"i32","v":3}},"n":"spawn_list_cap"},{"t":"bool","v":true,"n":"predict_gate1_fails"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":172,"n":"rng_words_in"},{"t":"i32","v":452,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347760384,"hook":"Game::SVSOSwarmQueen::OnTurnBegin","mode":"trace","call_id":2,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x046f6b98","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":-1,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"null","v":null,"n":"hives"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":170,"n":"rng_words_in"},{"t":"i32","v":454,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":454},"index":{"t":"i32","v":170},"block":{"t":"i32","v":0},"words":{"t":"i64","v":170},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347760893,"hook":"Game::StrategyServer::BeginProcessTurn","mode":"trace","call_id":1,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":1,"n":"turn"},{"t":"i32","v":0,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":-1,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":170,"n":"rng_words_in"},{"t":"i32","v":454,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":454},"index":{"t":"i32","v":170},"block":{"t":"i32","v":0},"words":{"t":"i64","v":170},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}}},"frame":{"before":{"t":"struct","v":{"value":{"t":"i32","v":1}}},"after":{"t":"struct","v":{"value":{"t":"i32","v":2}}}}}} +{"ts":347761193,"hook":"Game::StrategyServer::ProcessNodeSpaceTravel","mode":"trace","call_id":6,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":11,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":-1,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":172,"n":"rng_words_in"},{"t":"i32","v":452,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347763107,"hook":"Game::StrategyServer::ProcessTurn","mode":"trace","call_id":5,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"f32","v":1,"n":"dt"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":10,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":-1,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":172,"n":"rng_words_in"},{"t":"i32","v":452,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":452},"index":{"t":"i32","v":172},"block":{"t":"i32","v":0},"words":{"t":"i64","v":172},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347798130,"hook":"Game::StrategyServer::ApplyEncounterResult","mode":"trace","call_id":8,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"ptr","v":"0x351c8c70","n":"encounter"},{"t":"ptr","v":"0x3522b5a0","n":"result"},{"t":"u8","v":1,"n":"res_no_battle"},{"t":"u8","v":0,"n":"res_peaceful"},{"t":"u8","v":0,"n":"res_surrendered"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":12,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":1,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347798320,"hook":"Game::StrategyServer::ProcessNodeSpaceTravel","mode":"trace","call_id":9,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":12,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347798478,"hook":"Game::StrategyServer::NodeLineDecay","mode":"trace","call_id":10,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":12,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"i32","v":43,"n":"node_paths"},{"t":"i32","v":43,"n":"np_permanent"},{"t":"i32","v":0,"n":"np_immortal"},{"t":"i32","v":0,"n":"np_mortal"},{"t":"i32","v":-1,"n":"np_min_life"},{"t":"i32","v":0,"n":"np_within5"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347798822,"hook":"Game::SVSOSlaversRefuel::UpdateDifficultyTier","mode":"trace","call_id":11,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x046f6a78","n":"slavers"},{"t":"i32","v":2,"n":"frame"},{"t":"i32","v":-1,"n":"cdiff_in"},{"t":"list","v":[{"t":"i32","v":1},{"t":"i32","v":50},{"t":"i32","v":100}],"n":"thresholds"},{"t":"str","v":"store","n":"predict_path"},{"t":"i32","v":0,"n":"predict_cdiff_out"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"cdiff":{"before":{"t":"struct","v":{"value":{"t":"i32","v":-1}}},"after":{"t":"struct","v":{"value":{"t":"i32","v":0}}}},"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347799377,"hook":"Game::StrategyServer::OnAllCombatDone_Tail","mode":"trace","call_id":7,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"ptr","v":"0x35375f0c","n":"results"},{"t":"i32","v":1,"n":"result_count"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":11,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":1,"n":"encounters"},{"t":"i32","v":43,"n":"node_paths"},{"t":"i32","v":43,"n":"np_permanent"},{"t":"i32","v":0,"n":"np_immortal"},{"t":"i32","v":0,"n":"np_mortal"},{"t":"i32","v":-1,"n":"np_min_life"},{"t":"i32","v":0,"n":"np_within5"},{"t":"i32","v":0,"n":"predict_nodeline_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":347839195,"hook":"Game::StrategyHost::Autosave","mode":"trace","call_id":12,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x015a9f98","n":"host"},{"t":"ptr","v":"0x00cff544","n":"out_name"},{"t":"bool","v":false,"n":"end_turn"},{"t":"ptr","v":"0x0e483f40","n":"server_cached"},{"t":"ptr","v":"0x04627d50","n":"host_plus_0x54"},{"t":"bool","v":false,"n":"server_agrees"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"},{"t":"list","v":[{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":1210132},"calls":{"t":"u32","v":2},"words":{"t":"u32","v":2},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":1603725},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":4775764},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":5139977},"calls":{"t":"u32","v":16},"words":{"t":"u32","v":16},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":4797483},"calls":{"t":"u32","v":8},"words":{"t":"u32","v":8},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":4797720},"calls":{"t":"u32","v":8},"words":{"t":"u32","v":8},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":1061538},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":3746217},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":1546517},"calls":{"t":"u32","v":3},"words":{"t":"u32","v":3},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":5139977},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":1543445},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":2743342},"calls":{"t":"u32","v":3},"words":{"t":"u32","v":3},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}}],"n":"draw_sites"},{"t":"u32","v":38,"n":"draw_site_words"},{"t":"u32","v":38,"n":"draw_site_calls"},{"t":"u32","v":8,"n":"draw_site_words_other_rng"},{"t":"u32","v":8,"n":"draw_site_calls_other_rng"},{"t":"u32","v":0,"n":"draw_site_overflow"},{"t":"list","v":[{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot13"},"rva":{"t":"u32","v":4749232},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot14"},"rva":{"t":"u32","v":4774784},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot13"},"rva":{"t":"u32","v":4779904},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot15"},"rva":{"t":"u32","v":4377760},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::SpyManager::Slot13RngCallee"},"rva":{"t":"u32","v":4458720},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::TradeManager::Slot13RngCalleeA"},"rva":{"t":"u32","v":4328608},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::TradeManager::Slot13RngCalleeB"},"rva":{"t":"u32","v":4764736},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManager::CreateRaidEncounter"},"rva":{"t":"u32","v":4798624},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManager::GenerateTradeRaidEncounters"},"rva":{"t":"u32","v":4797072},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot15 [control]"},"rva":{"t":"u32","v":4751152},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot14 [control]"},"rva":{"t":"u32","v":4559056},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::EncounterDetect::Run [control]"},"rva":{"t":"u32","v":3977344},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}}],"n":"probe_entries"}],"ret":{"t":"ptr","v":"0x00cff544"},"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460039657,"hook":"Game::StrategyHost::Autosave","mode":"trace","call_id":13,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x015a9f98","n":"host"},{"t":"ptr","v":"0x00cff754","n":"out_name"},{"t":"bool","v":true,"n":"end_turn"},{"t":"ptr","v":"0x0e483f40","n":"server_cached"},{"t":"ptr","v":"0x04627d50","n":"host_plus_0x54"},{"t":"bool","v":false,"n":"server_agrees"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":{"t":"ptr","v":"0x00cff754"},"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460081860,"hook":"Game::SVSOSwarmQueen::RegisterHives","mode":"trace","call_id":16,"thread":8356,"depth":2,"args":[{"t":"ptr","v":"0x046f6b98","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":2,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"list","v":[{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e46e788"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":31},"queen":{"t":"ptr","v":"0x00000000"}}},{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e46dbe8"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":29},"queen":{"t":"ptr","v":"0x00000000"}}}],"n":"hives"},{"t":"i32","v":28,"n":"systems"},{"t":"i32","v":2,"n":"systems_tagged"},{"t":"i32","v":0,"n":"predict_new_hives"},{"t":"i32","v":0,"n":"predict_min_words"},{"t":"struct","v":{"rva":{"t":"u32","v":7209476},"ptr":{"t":"u32","v":22595552},"value":{"t":"i32","v":20}},"n":"hive_nextq_lo"},{"t":"struct","v":{"rva":{"t":"u32","v":7209480},"ptr":{"t":"u32","v":22595556},"value":{"t":"i32","v":30}},"n":"hive_nextq_hi"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460082102,"hook":"Game::SVSOSwarmQueen::TickHives","mode":"trace","call_id":17,"thread":8356,"depth":2,"args":[{"t":"ptr","v":"0x046f6b98","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":2,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"list","v":[{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e46e788"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":31},"queen":{"t":"ptr","v":"0x00000000"}}},{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e46dbe8"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":29},"queen":{"t":"ptr","v":"0x00000000"}}}],"n":"hives"},{"t":"struct","v":{"rva":{"t":"u32","v":7209488},"ptr":{"t":"u32","v":22595564},"value":{"t":"i32","v":10}},"n":"gate_frame_floor"},{"t":"struct","v":{"rva":{"t":"u32","v":7209512},"ptr":{"t":"u32","v":22413860},"value":{"t":"i32","v":5}},"n":"gate_queen_age"},{"t":"struct","v":{"rva":{"t":"u32","v":7209504},"ptr":{"t":"u32","v":22413852},"value":{"t":"i32","v":3}},"n":"spawn_list_cap"},{"t":"bool","v":true,"n":"predict_gate1_fails"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460082205,"hook":"Game::SVSOSwarmQueen::OnTurnBegin","mode":"trace","call_id":15,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x046f6b98","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":2,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"list","v":[{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e46e788"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":31},"queen":{"t":"ptr","v":"0x00000000"}}},{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e46dbe8"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":29},"queen":{"t":"ptr","v":"0x00000000"}}}],"n":"hives"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460082291,"hook":"Game::StrategyServer::BeginProcessTurn","mode":"trace","call_id":14,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":12,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}},"frame":{"before":{"t":"struct","v":{"value":{"t":"i32","v":2}}},"after":{"t":"struct","v":{"value":{"t":"i32","v":3}}}}}} +{"ts":460082771,"hook":"Game::StrategyServer::ProcessNodeSpaceTravel","mode":"trace","call_id":19,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":23,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460085781,"hook":"Game::StrategyServer::ProcessTurn","mode":"trace","call_id":18,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"f32","v":1,"n":"dt"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":22,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460120495,"hook":"Game::StrategyServer::ApplyEncounterResult","mode":"trace","call_id":21,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"ptr","v":"0x351c8c70","n":"encounter"},{"t":"ptr","v":"0x3522b108","n":"result"},{"t":"u8","v":1,"n":"res_no_battle"},{"t":"u8","v":0,"n":"res_peaceful"},{"t":"u8","v":0,"n":"res_surrendered"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":24,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":1,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460120716,"hook":"Game::StrategyServer::ProcessNodeSpaceTravel","mode":"trace","call_id":22,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":24,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460120886,"hook":"Game::StrategyServer::NodeLineDecay","mode":"trace","call_id":23,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":24,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"i32","v":43,"n":"node_paths"},{"t":"i32","v":43,"n":"np_permanent"},{"t":"i32","v":0,"n":"np_immortal"},{"t":"i32","v":0,"n":"np_mortal"},{"t":"i32","v":-1,"n":"np_min_life"},{"t":"i32","v":0,"n":"np_within5"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460121104,"hook":"Game::SVSOSlaversRefuel::UpdateDifficultyTier","mode":"trace","call_id":24,"thread":8356,"depth":1,"args":[{"t":"ptr","v":"0x046f6a78","n":"slavers"},{"t":"i32","v":3,"n":"frame"},{"t":"i32","v":0,"n":"cdiff_in"},{"t":"list","v":[{"t":"i32","v":1},{"t":"i32","v":50},{"t":"i32","v":100}],"n":"thresholds"},{"t":"str","v":"no_store_unchanged","n":"predict_path"},{"t":"null","v":null,"n":"predict_cdiff_out"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"}],"ret":null,"side":{"cdiff":{"before":{"t":"struct","v":{"value":{"t":"i32","v":0}}},"after":{"t":"struct","v":{"value":{"t":"i32","v":0}}}},"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460121808,"hook":"Game::StrategyServer::OnAllCombatDone_Tail","mode":"trace","call_id":20,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x0e483f40","n":"server"},{"t":"ptr","v":"0x353026c4","n":"results"},{"t":"i32","v":1,"n":"result_count"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":23,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":1,"n":"encounters"},{"t":"i32","v":43,"n":"node_paths"},{"t":"i32","v":43,"n":"np_permanent"},{"t":"i32","v":0,"n":"np_immortal"},{"t":"i32","v":0,"n":"np_mortal"},{"t":"i32","v":-1,"n":"np_min_life"},{"t":"i32","v":0,"n":"np_within5"},{"t":"i32","v":0,"n":"predict_nodeline_words"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":460160719,"hook":"Game::StrategyHost::Autosave","mode":"trace","call_id":25,"thread":8356,"depth":0,"args":[{"t":"ptr","v":"0x015a9f98","n":"host"},{"t":"ptr","v":"0x00cff544","n":"out_name"},{"t":"bool","v":false,"n":"end_turn"},{"t":"ptr","v":"0x0e483f40","n":"server_cached"},{"t":"ptr","v":"0x04627d50","n":"host_plus_0x54"},{"t":"bool","v":false,"n":"server_agrees"},{"t":"ptr","v":"0x0e477d08","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"},{"t":"list","v":[{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":1603725},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":5139977},"calls":{"t":"u32","v":16},"words":{"t":"u32","v":16},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":4797483},"calls":{"t":"u32","v":8},"words":{"t":"u32","v":8},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":4797720},"calls":{"t":"u32","v":8},"words":{"t":"u32","v":8},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":1061538},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":3746217},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":1546517},"calls":{"t":"u32","v":3},"words":{"t":"u32","v":3},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":5139977},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":1543445},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":2743342},"calls":{"t":"u32","v":3},"words":{"t":"u32","v":3},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}}],"n":"draw_sites"},{"t":"u32","v":35,"n":"draw_site_words"},{"t":"u32","v":35,"n":"draw_site_calls"},{"t":"u32","v":8,"n":"draw_site_words_other_rng"},{"t":"u32","v":8,"n":"draw_site_calls_other_rng"},{"t":"u32","v":0,"n":"draw_site_overflow"},{"t":"list","v":[{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot13"},"rva":{"t":"u32","v":4749232},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":2},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot14"},"rva":{"t":"u32","v":4774784},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":2},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot13"},"rva":{"t":"u32","v":4779904},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":2},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot15"},"rva":{"t":"u32","v":4377760},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":2},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::SpyManager::Slot13RngCallee"},"rva":{"t":"u32","v":4458720},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::TradeManager::Slot13RngCalleeA"},"rva":{"t":"u32","v":4328608},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::TradeManager::Slot13RngCalleeB"},"rva":{"t":"u32","v":4764736},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManager::CreateRaidEncounter"},"rva":{"t":"u32","v":4798624},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManager::GenerateTradeRaidEncounters"},"rva":{"t":"u32","v":4797072},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":2},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot15 [control]"},"rva":{"t":"u32","v":4751152},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":2},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot14 [control]"},"rva":{"t":"u32","v":4559056},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":2},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::EncounterDetect::Run [control]"},"rva":{"t":"u32","v":3977344},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":2},"installed":{"t":"bool","v":true}}}],"n":"probe_entries"}],"ret":{"t":"ptr","v":"0x00cff544"},"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} diff --git a/verify/results/shim/l1/trace-oracle.jsonl b/verify/results/shim/l1/trace-oracle.jsonl new file mode 100644 index 0000000..bfb8f8a --- /dev/null +++ b/verify/results/shim/l1/trace-oracle.jsonl @@ -0,0 +1,14 @@ +{"meta":{"format":1,"build":"0117495-dirty-20260908T2113Z","exe_sha256":"970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841","started":"2026-09-08T21:26:13Z","inline_max":64,"hooks":{"Shim::SelfTest::Fill":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"complete","why":"Fill writes buf[0..n) and nothing else; the whole range is a declared region","unmodelled":[]}},"Mars::GlobalConsts::LoadFile":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"erases each consumed key from the caller's std::map","risk":"medium","why":"the map is a LoadAll temporary; declaring a red-black tree as a region is not possible before the call. First-occurrence-wins is reproduced in game::config::apply instead, so the *effect* is modelled, the container is not","mitigation":"LoadAll's post-state would have to be hooked to see it"},{"what":"writes three kinds of line to the game log (unrecognised key, applied key, expected-but-not-found)","risk":"low","why":"log text is not part of the simulation state","mitigation":""},{"what":"opens the file through the VFS and allocates/releases a refcounted buffer","risk":"low","why":"ours performs the same two calls, so allocation behaviour matches by construction rather than by comparison","mitigation":""},{"what":"String slots assign through the engine's own std::string, leaking one heap block per long string in compare mode","risk":"low","why":"start-up only; documented in docs/M1.md","mitigation":""}]}},"Game::ServerPlayer::ComputeBudget":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"slots 1, 2, 3, 4, 7 and 11 are produced by callees this milestone does not model (per-system output, trade, ship-carried population, a second manager, the build-queue spend)","risk":"high","why":"they are copied out of the original's own output and back into the same slots, so they match BY CONSTRUCTION and prove nothing","mitigation":"declared input boundary; see budget_inputs.h"},{"what":"ServerSystem::ComputeOutput repairs damaged ships in orbit","risk":"high","why":"replace mode runs the original a second time on a scratch Budget to harvest the six unmodelled slots, so that repair happens TWICE per turn in replace mode and nothing in the trace would show it","mitigation":"guard:budget_object does not reach the ships; unverified"},{"what":"the difficulty-mods row from StrategyServer::GetDifficultyMods","risk":"medium","why":"not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs","mitigation":""},{"what":"the research-allocation vector's heap block","risk":"low","why":"only the element count is compared; the three words are heap pointers the default policy ignores","mitigation":""}]}},"Game::WeaponDictionary::Init":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"LoadWeapon -> WeaponDef::ParseScript registers each weapon's name with the string table and resolves `requires` against the live TechTree","risk":"high","why":"per-file parsing is M3 scope; ours delegates to the game's own LoadWeapon, so a compare run performs the registration a SECOND time and neither the string table nor the tech tree is a declared region","mitigation":"suspected cause of the sibling section hook's compare crash (docs/M2.md)"},{"what":"allocates 123 WeaponDef objects (0x278 bytes each) on the game heap","risk":"low","why":"the definitions do not exist when the hook is entered, so they cannot be a before-snapshot; the dictionary region compares them by id/name/path","mitigation":""},{"what":"the word at dictionary+0x14","risk":"low","why":"not modelled; emitted as an opaque pointer, which the default policy ignores -- a change is visible in a trace but never a divergence","mitigation":"guard:dict"},{"what":"writes lines to the game log for a missing manifest","risk":"low","why":"log text is not simulation state","mitigation":""},{"what":"std::sort tie order for equal weapon names","risk":"low","why":"msvc_sort.h replays MSVC 2010's introsort, but the shipped data has no tied names, so the tie rule is unexercised rather than verified","mitigation":""}]}},"Game::SectionDictionary::SectionDictionary":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"LoadSection registers each section with the string table and the live TechTree, and may append to the dictionary's own vector","risk":"high","why":"M3 scope; ours delegates to the game's LoadSection after the original has already built all 885 definitions, so the second pass registers duplicates -- the leading hypothesis for this hook's compare-mode crash","mitigation":"see docs/M2.md; compare mode for this hook is not safe to run"},{"what":"post-load validation pass over every definition's @-token against the string table","risk":"medium","why":"runs after the loop and touches no declared region","mitigation":""},{"what":"allocates 885 SectionDef objects (0x3d8 bytes each) on the game heap","risk":"low","why":"they do not exist at hook entry; compared by index/species/id/token","mitigation":""},{"what":"the word at dictionary+0x14","risk":"low","why":"not modelled; emitted as an ignored pointer","mitigation":"guard:dict"},{"what":"the before-snapshot of the object is uninitialised heap","risk":"medium","why":"the hook is on the constructor, so `before` is meaningless and only `after` carries information","mitigation":""}]}},"Game::TechTree::ProcessResearch":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"posts EVENT_RESEARCH_OVERBUDGET on the owner's EventStorage: ours reproduces the decision and the id sequence, so region:events compares next_id, but the composed EvDsc/EvMsg text is not reproduced and no region can see it","risk":"medium","why":"text comes from the game's string table, which the engine must not carry; ours posts into its own EventStorage and writes only the counts into the scratch copy, so no live byte moves and replace mode posts nothing at all","mitigation":"region:events"},{"what":"composes EVENT_TECHS_UNLOCKED's message from the unlocked techs' names","risk":"low","why":"the trigger and the list are modelled (SetResearched's availability sweep plus the tail collector, both read off the instruction stream), so region:events compares next_id; the names come from the game's string table, so the message is composed from node indices instead and is not the game's text","mitigation":"region:events"},{"what":"TechTree::SetResearched in REPLACE mode: nothing of it runs","risk":"high","why":"the cascade is compare-mode only. In replace mode every pointer is live game memory, and applying half of OnTechResearched -- the observed-tech append and the research-event roll, but not the tech-effect field writes -- would leave the player in a state no code path produces. A replace run therefore still leaves the completed node unstamped and no tech unlocked","mitigation":"guard:player, guard:tree_header"},{"what":"ServerPlayer::OnTechResearched's tech effects: the ~90 hard-coded ServerPlayer field writes, the plague-cure masks, the design-option bitmasks and the species tech flags","risk":"high","why":"B2's milestone. `ours` models only the two parts of the callback this hook's regions can see -- the observed-tech append and the RNG word RollResearchEvent draws before its branch (one word on a missed roll, two on a fired plague roll) -- and the rest is what the player guard reports","mitigation":"guard:player"},{"what":"the research-event branch RollResearchEvent takes when its roll beats the odds (ServerPlayer::OnResearchRollSucceeded: the plague and AI-rebellion event paths)","risk":"high","why":"RollResearchEvent draws one NextFloat unconditionally and that draw IS modelled -- but that is only the cost of REACHING the branch. A FIRED roll costs one or two words: the plague path draws a SECOND word (NextInt) to pick an owned system and posts EVENT_PLAGUE_OUTBREAK, while the rebellion path allocates an AIRebellion at ServerPlayer+0x3b8 and CANCELS the current research (no further draw). The branch is entered only for the plague and AI-rebellion tech families, whose odds are 0 everywhere else, and it has never been observed firing in three sessions -- which is why every earlier note in this repo said 'exactly one NextFloat' and nothing caught it. If it is ever entered, region:rng is the check","mitigation":"region:rng"},{"what":"constructs the ObservedTech element it appends to ServerPlayer+0x274","risk":"medium","why":"`ours` models the append DECISION -- RecordObservedTech de-duplicates by tech name, so it decides whether the vector grows -- and moves the scratch header's byte span by one 0x2c element per append. The element's own fields (turn_first, turn_last, detected, the name string, `with`) are not built, and no region can see them","mitigation":"region:observed_techs"},{"what":"the tree's completion-order counter (TechTree+0x20) is read pre-call, not modelled as a region","risk":"low","why":"the per-node `order` word IS compared, and it is stamped from a counter `ours` seeds from the pre-call read and advances itself; the counter's own final value is only seen by the tree_header guard","mitigation":"guard:tree_header"},{"what":"writes a completion line to the game log","risk":"low","why":"log text is not simulation state","mitigation":""}]}},"Game::ServerPlayer::OnTechResearched":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent","risk":"high","why":"the same class of write as B3's defect, and this hook has no replace-mode oracle that could catch it: gotcha 4 in docs/B2.md says a changed save hash on a completion turn is expected and therefore not a finding","mitigation":"guard:player (EventStorage is inline at ServerPlayer+0x29c)"},{"what":"writes every owned system's AI flag (CCC_AIVrus / CCC_AISlv), re-evaluates the arcology civilian cap, cures addiction and clears plague across systems AND ships","risk":"high","why":"writes through pointers to other objects; compare mode must not touch live state, and no region reaches them","mitigation":""},{"what":"the pending plague-cure roll (ServerPlayer::RollResearchEvent)","risk":"high","why":"it draws one word from the strategic generator unconditionally, and a SECOND word (NextInt) when the roll beats the odds and takes the plague path -- which also posts EVENT_PLAGUE_OUTBREAK, while the rebellion path cancels the research. So a fired roll costs one or two words, not one; running it in compare mode would consume real randomness. The two words it guards are still cleared and the record says whether it would have fired","mitigation":"this is the extra draw B3 observed on a completion"},{"what":"TechTree::SetResearched for the Zuul boarding-pod grant","risk":"medium","why":"it would mutate the live tree, and it recurses","mitigation":""},{"what":"allocates or frees the node-bore block at ServerPlayer+0x308","risk":"medium","why":"ours has no allocator the game's runtime could free, so replace mode calls the game's own updater -- which means replace mode never exercises our node-bore selection at all","mitigation":"region:node_bore, declared only when the block already exists"}]}},"Game::ServerSystem::ProcessTurn":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history","risk":"high","why":"the same class of write as B3's defect. sim::ProcessColonyTurn does compute the morale events (ColonyTurnResult), but the hook never emits them: DescribeMoraleEvents is dead code, so they are neither compared nor logged","mitigation":"guard:system"},{"what":"every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue","risk":"high","why":"declared input boundary -- ProcessTurn is a dispatcher and only the words it writes itself are modelled. The callees raise EVENT_SLAVES_DEAD, EVENT_SYSTEM_REBELLION_CONTINUES, the plague events and SEBuildCompleted, create ships and bump per-player ShipRecords counters","mitigation":"guard:system covers the system object only, not the other objects"},{"what":"ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer","risk":"medium","why":"writes through a pointer to another object; no region reaches the player","mitigation":""},{"what":"ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent","risk":"low","why":"the generator IS a declared region, so a moved post-state is visible and names the system whose rebellion fired -- it is reported, not modelled","mitigation":""},{"what":"replace mode is refused for this hook","risk":"medium","why":"our side models the dispatcher's own writes and none of the callees, so a replace run would silently skip a colony's whole turn. There is therefore no oracle layer behind the compare for this hook","mitigation":""}]}},"Game::ServerSystem::GroupOutput":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"the imperial and civilian output modifiers, the 1.8 factor and the 500000 divisor are literals inside the executable, so nothing about them is assumed from the data files","unmodelled":[{"what":"the station count that scales the imperial term","risk":"high","why":"the original gets it from a helper that walks the system's fleets and their ships through virtual calls and takes the system in EBX, which a hook cannot call portably. `ours` therefore assumes ZERO stations. A divergence on an imperial row is expected to be exactly the station factor, and the record carries `count` and the return, so the factor is MEASURED from the trace rather than fitted","mitigation":"declared input boundary; the trace makes it recoverable"},{"what":"the slave row's output modifier, and every value the data files supply","risk":"medium","why":"SLAVES_OUTPUT_MOD, the two morale thresholds and their two modifiers, and STATION_BONUS_IMPERIAL_OUTPUT are read out of the live process's globals and logged with every record, so the record says which value drove it","mitigation":"logged as `tuning` on every record"}]}},"Game::ServerSystem::ComputeTotalOutput":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"the station count, as for GroupOutput","risk":"high","why":"`ours` assumes zero stations, so a system with an imperial population and a station diverges by the station factor","mitigation":"declared input boundary"},{"what":"the slave population and its xenotech adjustment","risk":"high","why":"the original's slave count is not a plain field: it runs the count through a per-species xenotech factor. `ours` takes the slave term as ZERO, so any system holding slaves diverges. The record logs the raw group-2 population sums so a divergence can be attributed","mitigation":"declared input boundary"},{"what":"the capacity surplus the civilian term adds for the owner's own species","risk":"medium","why":"the original calls the carrying-capacity helper twice with different out-parameter slots and adds max(0, B - A) to the civilian count; `ours` uses the raw civilian population. The surplus is zero except when the colony is at its cap","mitigation":"declared input boundary"},{"what":"the addiction phase","risk":"low","why":"`ours` assumes it is below 3, so ADDICTION_OUTPUT_MOD never applies; the record logs the system's addiction table length so the case is visible","mitigation":""}]}},"Game::ServerPlayer::ProcessTurn":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"phases 2, 3 and 6 -- the savings apply Sav = SatAdd(budget[0], net), the three aid/trade records copied out of the budget, and the research refund -- are NOT declared and NOT compared","risk":"high","why":"all three are pure functions of ComputeBudget's 22-slot output and of ProcessResearch's overBudget out-parameter, and both live in the original's own stack frame ([ebp-0x90] and [ebp-0x14]). The three ways to reach them are each worse than not having them: call ComputeBudget ourselves (it repairs damaged ships in orbit -- harness-audit #6), read them out of the nested B1/B3 hooks (harness-audit #5, the self-fulfilling compare), or infer them from the observed Sav delta (the same trap). The formulas ARE written and unit-tested in player_turn_inputs.cpp; they are simply not wired into the verdict","mitigation":"guard:player reports Sav, +0xc8 and +0xcc moving"},{"what":"ComputeBudget (0x00863030) itself, including ServerSystem::ComputeOutput, which REPAIRS DAMAGED SHIPS IN ORBIT","risk":"high","why":"B1 is the hook that checks the budget; its ship-repair side effect happens inside this call too, once per system per player per turn","mitigation":"run hook.Game::ServerPlayer::ComputeBudget=compare alongside"},{"what":"TechTree::ProcessResearch and the whole SetResearched / OnTechResearched cascade it can trigger: progress, the completion roll, the 5% decay sweep, the child-unlock cascade, ~90 tech-effect field writes, EVENT_RESEARCH_* and EVENT_TECHS_UNLOCKED","risk":"high","why":"declared input boundary. B3/U own it; this hook reports the pre-call research state and lets the player guard say how much moved","mitigation":"guard:player, and hook.Game::TechTree::ProcessResearch=compare"},{"what":"RollResearchAccident (0x00889dc0) draws Mars::RNG::NextInt(100) whenever the research boost is non-zero, can apply progress loss across EVERY system, and posts EVENT_LABACCIDENT_{SMALL,MEDIUM,LARGE}","risk":"high","why":"it runs before ProcessResearch and gates it entirely. Its draws move the declared `rng` region, so they are visible -- but as an unexplained RNG divergence, not as their cause","mitigation":"region:rng shows the draws; nothing shows the systems it wrote"},{"what":"the ResearchRollPending roll: RollResearchEvent draws one NextFloat, and when it BEATS the odds the plague branch draws a SECOND word (NextInt) and posts EVENT_PLAGUE_OUTBREAK, while the rebellion branch allocates an AIRebellion at ServerPlayer+0x3b8 and cancels the current research","risk":"high","why":"every previous coverage note in this repo says 'exactly one NextFloat'. That is the cost of REACHING the branch; a fired roll costs one or two words. The branch has never been observed firing. `predict_roll` says whether we expected the roll, `region:rng` says what it actually cost, and `roll_flags` says whether ResErrRoll was consumed","mitigation":"region:rng + region:roll_flags + guard:player; ours never rolls"},{"what":"ServerPlayer::ProcessSpecialProjects (0x00840fe0) and, for an AI player, ConstructionSpend (0x00817f90)","risk":"medium","why":"1015 bytes of special-project state, plus the log line \"SpecialProject: %s completed investigation phase\"","mitigation":"guard:player"},{"what":"the EVENT_NO_RESEARCH post at 0x0089168c, which appends to the player's EventStorage (+0x29c) and bumps EvNxID (+0x2b0)","risk":"medium","why":"B3's exact failure mode: a list append outside every Result region. Its condition is fully known (ResT == 0, nothing completed this turn, and at least one state-2 tech exists) but ours posts nothing","mitigation":"guard:player reports the EvNxID bump"},{"what":"PruneRaidTargets (0x00863cf0) erases from the heap vector at +0x338","risk":"low","why":"the vector header is inside the player guard, but its elements are heap memory no region reaches, and the erase runs each removed element's scalar destructor","mitigation":"guard:player sees the header move only"},{"what":"the timed-bonus element regions are captured from the PRE-CALL element addresses, and the sweep may shrink the vector under them","risk":"low","why":"erase moves data down inside the same buffer and only lowers _Mylast, so the addresses stay valid and readable. Elements past the new _Mylast are compared as stale bytes; `bonus_header` is the region that carries the count, and ours writes the same stale tail the original leaves","mitigation":"region:bonus_header"},{"what":"replace mode is refused for this hook","risk":"medium","why":"ours models the driver's own writes and none of ComputeBudget, ProcessResearch, ProcessSpecialProjects or the raid-target prune. A replace run would leave a player in a state no code path produces, and there is therefore no oracle layer behind this compare","mitigation":""}]}},"Game::StrategyServer::MoveFleet":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)","risk":"high","why":"declared input boundary -- an arriving call is expected to differ in all of it, and none of it is declared, so the compare says nothing about arrivals","mitigation":"guard:fleet sees the fleet's own words; the event and the system do not"},{"what":"on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits","risk":"high","why":"writes through pointers to ships and to the system","mitigation":""},{"what":"the tanker top-up refuels other ships in the fleet","risk":"medium","why":"the per-ship range regions would show it, but ours does not model it, so a fleet with a tanker diverges for a known reason","mitigation":""},{"what":"a node-line waypoint's step comes from the stutter profile","risk":"medium","why":"NodeLineStep / BuildStutterSegments are written and unit-tested but not wired in; the hook steps every waypoint type as speed x dt, so a node-line leg is knowingly mis-stepped and only its type is recorded","mitigation":"declared gap: docs/B4.md"},{"what":"a missed probabilistic jump scatters the fleet in a random direction","risk":"medium","why":"the direction is a second draw whose mapping is not modelled; ours leaves the position alone and reports the scatter distance, so the generator region diverges by one word on a miss","mitigation":""},{"what":"the route revalidation and the waypoint list itself","risk":"medium","why":"declared input boundary; the waypoint vector is not a region","mitigation":""}]}},"Game::StrategyServer::ProcessFleetMovement":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"`ours` re-reads the LIVE fleet list after the original has run","risk":"high","why":"the gate-traffic total is computed by the original at the very end of the pass, so a pre-call snapshot would diverge for the wrong reason. It breaks the compare invariant that ours never touches live memory, and it makes this hook's verdict partly self-fulfilling: the input to our arithmetic is the original's own post-move state","mitigation":""},{"what":"drives MoveFleet up to five times per fleet","risk":"high","why":"every undeclared effect of MoveFleet happens inside this call too; the pass schedule is recorded in the arguments but never compared","mitigation":""},{"what":"writes FPdpos into every fleet and clears flags 0x2 and 0x100 on every fleet","risk":"high","why":"no region covers the fleets, only the players' gate-traffic words","mitigation":""},{"what":"OnFleetArrived posts EVENT_FLEET_ARRIVED","risk":"high","why":"the same class of write as B3's defect, and there is no replace mode for this hook, so nothing behind the compare could catch it either","mitigation":""},{"what":"the original accumulates by player->index but writes back by the player's position in the server vector, into a fixed 32-int array with no bounds check","risk":"medium","why":"a real latent bug in the original that our side reproduces only while index == position; the reference save never separates them","mitigation":""},{"what":"PassSchedule() is never called by the hook, and FleetSummary::targetFleetId / relation are never filled","risk":"medium","why":"the header claims ours predicts the call order for a trace to check; that prediction is not actually emitted","mitigation":""}]}},"Game::StrategyHost::Autosave":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the whole save write: four path buffers, the ENDTURN pair removal, the backup rotation, the per-player connection detach/reattach, and SaveGame_WriteFile 0x00877070 itself","risk":"low","why":"this hook exists to timestamp the generator at the two moments the two save files are written. It is a marker and models nothing","mitigation":"region:rng only"},{"what":"the generator is reached through a CACHED StrategyServer pointer, not from this call's own arguments","risk":"medium","why":"`this` is the global at 0x00b29f98, hardcoded by both call sites, and no argument here names the server. On the first pre-turn autosave after a load no turn driver has run yet, so the cache is empty and that record carries no ledger position -- the FIRST BRACKET OF A SESSION IS INCOMPLETE BY CONSTRUCTION and must not be read as a zero-cost turn","mitigation":"arg:server_cached / host_plus_0x54 / server_agrees say which pointer was used and whether the +0x54 candidate is the same object"}]}},"Game::StrategyServer::ProcessTurn":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"no model of the turn's RNG cost: 32 phases, each of which may draw","risk":"high","why":"ProcessResearch's completion roll, RollResearchAccident's NextInt(100), the ResearchRollPending roll (one word, or two on the plague path), and whatever ProcessStations / ProcessSurrenders / ProcessMissions / ProcessSpecialProjects spend -- none of which has ever been measured. `ours` predicts nothing and the record reports the measurement","mitigation":"region:rng measures the total; the per-phase split is not resolved here"},{"what":"this hook takes the address the fpu module also wants to sample","risk":"low","why":"MinHook allows one hook per target. `fpu.sample_turn=off` releases StrategyServer::ProcessTurn so this hook can install; with it on, this hook fails to install and the trace is missing half the ledger","mitigation":"shim.log records the MH_CreateHook status for both"}]}},"Game::StrategyServer::OnAllCombatDone_Tail":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"36 phases, of which two can draw and neither is modelled here","risk":"high","why":"phase 6 reaches the unread 7499-byte combat resolver 0x007d5af0 (NextInt on the node-cannon path, Twist plus NextInt on the salvage path) and phase 11 draws one word per expired node line. `predict_nodeline_words` covers only the second, and the nested ApplyEncounterResult / NodeLineDecay hooks are what attribute the split","mitigation":"region:rng plus the two nested hooks"},{"what":"whether this handler runs on a turn with NO combat is what this hook is here to settle, and until it has run it is a hypothesis","risk":"medium","why":"combat-done-tail.md \u00c2\u00a76 infers it from the determinism note -- the post-turn autosave appears on every End Turn and this handler is its only reachable caller -- not from the instruction stream","mitigation":"arg:encounters says how many encounters this call saw; a call with 0 settles it"}]}},"Game::StrategyServer::ApplyEncounterResult":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the combat resolver 0x007d5af0 (7499 B) is completely unread","risk":"high","why":"this hook measures what its subtree spends and models none of it. Nothing about combat determinism can be settled until that function is read; this only puts a number on the hole","mitigation":"region:rng measures the subtotal"},{"what":"the ~0xea0-byte combat report, the CombatReport list append at S+0x1fc, the ClientEncounterResults push into S+0x2f4, the per-ship turn stamps and the pairwise engagement bits","risk":"medium","why":"all of it is game state this hook does not declare and does not check","mitigation":""}]}},"Game::StrategyServer::NodeLineDecay":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the collapse itself: 0x007a92e0 (690 B) and 0x007a4700 (2244 B) destroy or halt fleets and post EVENT_NODEDECAY_FLEET_DESTROYED_VIANODE / _HALTED / _HALTED_VIANODE, and loop 3 posts two more decay-stage events","risk":"high","why":"ours advances the generator and writes nothing else. In compare mode that is the intent -- the check is the word count -- but it means a clean verdict here says nothing about which lines actually collapsed","mitigation":"region:rng only"},{"what":"the draw-count model is verified by a DIRECT-call sweep of the downstream pair; their subtrees contain unresolved indirect call sites","risk":"medium","why":"if one of those vtable slots reaches a generator, the measured delta will exceed `predict_words` and this hook will diverge -- which is the correct outcome, and the reason the prediction is recorded as an argument","mitigation":"arg:predict_words vs region:rng is exactly that check"},{"what":"the expiry formula reproduces a signed idiv on nptf/npdtf without knowing whether nptf can be negative","risk":"low","why":"the original never sign-checks the traffic accumulator. The model truncates toward zero the same way; if the field is always non-negative the question never arises, and no save has been observed with a negative one","mitigation":""}]}},"Game::StrategyServer::ProcessNodeSpaceTravel":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"2945 bytes of node-space movement, entirely unmodelled and never swept for RNG by any lane","risk":"medium","why":"it is hooked here only because it runs TWICE a turn -- ProcessTurn phase 7 and tail phase 10 -- so a draw inside it would be double-counted by anyone modelling it once. The record says whether it draws at all","mitigation":"region:rng"}]}},"Game::EncounterDetect::AssignContacts":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the contact-to-detector assignment itself, and the two-pass outer loop","risk":"medium","why":"this hook exists because the draw here is INLINED and therefore invisible to every call-graph sweep and to the entry-point detours -- it is the one site in ProcessTurn's closure that neither instrument can see. It measures the word cost of the whole call and models nothing","mitigation":"region:rng; arg:detectors/contacts/max_trials bound the expected count"},{"what":"the per-trial threshold is 0.25f or 0.0f depending on two tech lookups, and the accept test short-circuits the inner loop","risk":"low","why":"so the measured cost is between |contacts| and |contacts| x |detectors| and the exact number depends on tech state this hook does not read","mitigation":"arg:max_trials is the upper bound only"}]}},"Game::EncounterDetect::ProcessTeamRecord":{"ftol":0,"ftol_kind":"abs","ptr":"ignore","coverage":{"state":"partial","why":"","unmodelled":[{"what":"everything the original writes except the strategic generator","risk":"high","why":"this hook family measures ONE thing -- how many words the generator advances and where. It declares no region over game state and makes no claim about it. A clean run here says the RNG accounting is right and says nothing whatever about whether the turn was computed correctly","mitigation":"region:rng is the only check; the turn's own correctness is B1/B3/B4's job"},{"what":"the ledger reports WORDS, not draws","risk":"low","why":"a NextInt that rejects three times is four words and one call. Words are the unit that decides whether a save reproduces; they are the wrong unit for counting decisions, and nothing here should be read as a draw count","mitigation":"region:rng carries left/block/words, never a call count"},{"what":"the ledger is deliberately blind to WHICH primitive spent a word","risk":"low","why":"that is the design, and it is why this instrument was preferred to hooking the primitives: the image has FOUR draw entry points (NextFloat 0x0047d830, NextInt 0x004271c0, Chance 0x008e6dd0 and NextUInt 0x004f7670, the last of which appears in no previous lane's primitive set) plus inlined draws in at least twelve functions, two of them reachable from the turn roots. A primitive-counting hook would have silently undercounted every one of those","mitigation":"region:rng reads the state, so an inlined draw is as visible as a called one"},{"what":"a generator position the ledger cannot place reads `words: null`","risk":"medium","why":"a block more than 4096 twists ahead of the frontier, or any state behind the anchor, is reported unknown rather than guessed. A null in a ledger field is a measurement failure and must not be read as zero","mitigation":"region:rng emits null explicitly; tracecmp shows it as a value, not a gap"},{"what":"the whole body of ProcessTeamRecord: the gate call, the two vector builds and the bucket construction","risk":"medium","why":"this hook measures the word cost of the call and recomputes three integers the original derives from the same record. It models none of the work and asserts nothing about the contact assignment","mitigation":"region:rng; arg:gate/pred_contacts/pred_detectors are a prediction, not a check"},{"what":"`+0xfc` and `+0xfb` are read as BYTES, following lane I's reading of the classifier functions","risk":"medium","why":"if either is really a wider field, every count here is wrong in the same direction and no cross-check inside this hook would notice. So the dword at +0xfc is read as well and any row where the two disagree is COUNTED, not silently resolved -- a non-zero disagreement count means the byte reading is unsafe on this workload","mitigation":"arg:fc_byte_vs_dword_disagreements; arg:entry_flags carries the raw values"},{"what":"an entry whose object pointer is unreadable is skipped","risk":"low","why":"it is not counted into any of the three classes, so entries != contacts + detectors + neither is the signal that this happened","mitigation":"arg:entries against the three class counts"}]}}}}} +{"ts":245045059,"hook":"Game::StrategyHost::Autosave","mode":"trace","call_id":0,"thread":6420,"depth":0,"args":[{"t":"ptr","v":"0x015a9f98","n":"host"},{"t":"ptr","v":"0x006ff928","n":"out_name"},{"t":"bool","v":true,"n":"end_turn"},{"t":"ptr","v":"0x00000000","n":"server_cached"},{"t":"ptr","v":"0x03cb8458","n":"host_plus_0x54"},{"t":"bool","v":false,"n":"server_agrees"},{"t":"ptr","v":"0x00000000","n":"rng"},{"t":"null","v":null,"n":"rng_words_in"},{"t":"null","v":null,"n":"rng_left_in"}],"ret":{"t":"ptr","v":"0x006ff928"},"side":{}} +{"ts":245089074,"hook":"Game::SVSOSwarmQueen::RegisterHives","mode":"trace","call_id":3,"thread":6420,"depth":2,"args":[{"t":"ptr","v":"0x03d86328","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":2,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"list","v":[{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e06e5f8"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":31},"queen":{"t":"ptr","v":"0x00000000"}}},{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e06f480"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":29},"queen":{"t":"ptr","v":"0x00000000"}}}],"n":"hives"},{"t":"i32","v":28,"n":"systems"},{"t":"i32","v":2,"n":"systems_tagged"},{"t":"i32","v":0,"n":"predict_new_hives"},{"t":"i32","v":0,"n":"predict_min_words"},{"t":"struct","v":{"rva":{"t":"u32","v":7209476},"ptr":{"t":"u32","v":22595552},"value":{"t":"i32","v":20}},"n":"hive_nextq_lo"},{"t":"struct","v":{"rva":{"t":"u32","v":7209480},"ptr":{"t":"u32","v":22595556},"value":{"t":"i32","v":30}},"n":"hive_nextq_hi"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":245089369,"hook":"Game::SVSOSwarmQueen::TickHives","mode":"trace","call_id":4,"thread":6420,"depth":2,"args":[{"t":"ptr","v":"0x03d86328","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":2,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"list","v":[{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e06e5f8"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":31},"queen":{"t":"ptr","v":"0x00000000"}}},{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e06f480"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":29},"queen":{"t":"ptr","v":"0x00000000"}}}],"n":"hives"},{"t":"struct","v":{"rva":{"t":"u32","v":7209488},"ptr":{"t":"u32","v":22595564},"value":{"t":"i32","v":10}},"n":"gate_frame_floor"},{"t":"struct","v":{"rva":{"t":"u32","v":7209512},"ptr":{"t":"u32","v":22413860},"value":{"t":"i32","v":5}},"n":"gate_queen_age"},{"t":"struct","v":{"rva":{"t":"u32","v":7209504},"ptr":{"t":"u32","v":22413852},"value":{"t":"i32","v":3}},"n":"spawn_list_cap"},{"t":"bool","v":true,"n":"predict_gate1_fails"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":245089500,"hook":"Game::SVSOSwarmQueen::OnTurnBegin","mode":"trace","call_id":2,"thread":6420,"depth":1,"args":[{"t":"ptr","v":"0x03d86328","n":"queen"},{"t":"i32","v":3,"n":"scenario_tag"},{"t":"i32","v":10,"n":"enc_id"},{"t":"i32","v":2,"n":"hives_in"},{"t":"i32","v":-1,"n":"queens_in"},{"t":"list","v":[{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e06e5f8"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":31},"queen":{"t":"ptr","v":"0x00000000"}}},{"t":"struct","v":{"sys":{"t":"ptr","v":"0x0e06f480"},"egg_scio":{"t":"i32","v":3},"next_q":{"t":"i32","v":29},"queen":{"t":"ptr","v":"0x00000000"}}}],"n":"hives"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":245089628,"hook":"Game::StrategyServer::BeginProcessTurn","mode":"trace","call_id":1,"thread":6420,"depth":0,"args":[{"t":"ptr","v":"0x0e059150","n":"server"},{"t":"i32","v":2,"n":"turn"},{"t":"i32","v":12,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":-1,"n":"encounters"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}},"frame":{"before":{"t":"struct","v":{"value":{"t":"i32","v":2}}},"after":{"t":"struct","v":{"value":{"t":"i32","v":3}}}}}} +{"ts":245090079,"hook":"Game::StrategyServer::ProcessNodeSpaceTravel","mode":"trace","call_id":6,"thread":6420,"depth":1,"args":[{"t":"ptr","v":"0x0e059150","n":"server"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":23,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":-1,"n":"encounters"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":245093672,"hook":"Game::StrategyServer::ProcessTurn","mode":"trace","call_id":5,"thread":6420,"depth":0,"args":[{"t":"ptr","v":"0x0e059150","n":"server"},{"t":"f32","v":1,"n":"dt"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":22,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":-1,"n":"encounters"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":192,"n":"rng_words_in"},{"t":"i32","v":432,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":432},"index":{"t":"i32","v":192},"block":{"t":"i32","v":0},"words":{"t":"i64","v":192},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":245141242,"hook":"Game::StrategyServer::ApplyEncounterResult","mode":"trace","call_id":8,"thread":6420,"depth":1,"args":[{"t":"ptr","v":"0x0e059150","n":"server"},{"t":"ptr","v":"0x34db9890","n":"encounter"},{"t":"ptr","v":"0x34dfd440","n":"result"},{"t":"u8","v":1,"n":"res_no_battle"},{"t":"u8","v":0,"n":"res_peaceful"},{"t":"u8","v":0,"n":"res_surrendered"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":24,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":1,"n":"encounters"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":245141544,"hook":"Game::StrategyServer::ProcessNodeSpaceTravel","mode":"trace","call_id":9,"thread":6420,"depth":1,"args":[{"t":"ptr","v":"0x0e059150","n":"server"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":24,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":245141771,"hook":"Game::StrategyServer::NodeLineDecay","mode":"trace","call_id":10,"thread":6420,"depth":1,"args":[{"t":"ptr","v":"0x0e059150","n":"server"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":24,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":0,"n":"encounters"},{"t":"i32","v":43,"n":"node_paths"},{"t":"i32","v":43,"n":"np_permanent"},{"t":"i32","v":0,"n":"np_immortal"},{"t":"i32","v":0,"n":"np_mortal"},{"t":"i32","v":-1,"n":"np_min_life"},{"t":"i32","v":0,"n":"np_within5"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":245142132,"hook":"Game::SVSOSlaversRefuel::UpdateDifficultyTier","mode":"trace","call_id":11,"thread":6420,"depth":1,"args":[{"t":"ptr","v":"0x03d86250","n":"slavers"},{"t":"i32","v":3,"n":"frame"},{"t":"i32","v":0,"n":"cdiff_in"},{"t":"list","v":[{"t":"i32","v":1},{"t":"i32","v":50},{"t":"i32","v":100}],"n":"thresholds"},{"t":"str","v":"no_store_unchanged","n":"predict_path"},{"t":"null","v":null,"n":"predict_cdiff_out"},{"t":"i32","v":0,"n":"predict_words"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"}],"ret":null,"side":{"cdiff":{"before":{"t":"struct","v":{"value":{"t":"i32","v":0}}},"after":{"t":"struct","v":{"value":{"t":"i32","v":0}}}},"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":245142763,"hook":"Game::StrategyServer::OnAllCombatDone_Tail","mode":"trace","call_id":7,"thread":6420,"depth":0,"args":[{"t":"ptr","v":"0x0e059150","n":"server"},{"t":"ptr","v":"0x34fb67cc","n":"results"},{"t":"i32","v":1,"n":"result_count"},{"t":"i32","v":3,"n":"turn"},{"t":"i32","v":23,"n":"phase_counter"},{"t":"i32","v":8,"n":"players"},{"t":"i32","v":1,"n":"encounters"},{"t":"i32","v":43,"n":"node_paths"},{"t":"i32","v":43,"n":"np_permanent"},{"t":"i32","v":0,"n":"np_immortal"},{"t":"i32","v":0,"n":"np_mortal"},{"t":"i32","v":-1,"n":"np_min_life"},{"t":"i32","v":0,"n":"np_within5"},{"t":"i32","v":0,"n":"predict_nodeline_words"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"}],"ret":null,"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} +{"ts":245182239,"hook":"Game::StrategyHost::Autosave","mode":"trace","call_id":12,"thread":6420,"depth":0,"args":[{"t":"ptr","v":"0x015a9f98","n":"host"},{"t":"ptr","v":"0x006ff718","n":"out_name"},{"t":"bool","v":false,"n":"end_turn"},{"t":"ptr","v":"0x0e059150","n":"server_cached"},{"t":"ptr","v":"0x03cb8458","n":"host_plus_0x54"},{"t":"bool","v":false,"n":"server_agrees"},{"t":"ptr","v":"0x0e088c98","n":"rng"},{"t":"i64","v":211,"n":"rng_words_in"},{"t":"i32","v":413,"n":"rng_left_in"},{"t":"list","v":[{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":1603725},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":5139977},"calls":{"t":"u32","v":16},"words":{"t":"u32","v":16},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":4797483},"calls":{"t":"u32","v":8},"words":{"t":"u32","v":8},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":4797720},"calls":{"t":"u32","v":8},"words":{"t":"u32","v":8},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":1061538},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":3746217},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":true}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":1546517},"calls":{"t":"u32","v":3},"words":{"t":"u32","v":4},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextFloat"},"ret_rva":{"t":"u32","v":5139977},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"Chance"},"ret_rva":{"t":"u32","v":1543445},"calls":{"t":"u32","v":1},"words":{"t":"u32","v":1},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}},{"t":"struct","v":{"entry":{"t":"str","v":"NextInt"},"ret_rva":{"t":"u32","v":2743342},"calls":{"t":"u32","v":3},"words":{"t":"u32","v":3},"no_draw_calls":{"t":"u32","v":0},"strategic":{"t":"bool","v":false}}}],"n":"draw_sites"},{"t":"u32","v":35,"n":"draw_site_words"},{"t":"u32","v":35,"n":"draw_site_calls"},{"t":"u32","v":9,"n":"draw_site_words_other_rng"},{"t":"u32","v":8,"n":"draw_site_calls_other_rng"},{"t":"u32","v":0,"n":"draw_site_overflow"},{"t":"list","v":[{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot13"},"rva":{"t":"u32","v":4749232},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot14"},"rva":{"t":"u32","v":4774784},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot13"},"rva":{"t":"u32","v":4779904},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot15"},"rva":{"t":"u32","v":4377760},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::SpyManager::Slot13RngCallee"},"rva":{"t":"u32","v":4458720},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::TradeManager::Slot13RngCalleeA"},"rva":{"t":"u32","v":4328608},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::TradeManager::Slot13RngCalleeB"},"rva":{"t":"u32","v":4764736},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManager::CreateRaidEncounter"},"rva":{"t":"u32","v":4798624},"calls":{"t":"u32","v":0},"calls_since_launch":{"t":"u32","v":0},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManager::GenerateTradeRaidEncounters"},"rva":{"t":"u32","v":4797072},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerSpyManager::vslot15 [control]"},"rva":{"t":"u32","v":4751152},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::ServerTradeManagerImpl::vslot14 [control]"},"rva":{"t":"u32","v":4559056},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}},{"t":"struct","v":{"name":{"t":"str","v":"Game::EncounterDetect::Run [control]"},"rva":{"t":"u32","v":3977344},"calls":{"t":"u32","v":1},"calls_since_launch":{"t":"u32","v":1},"installed":{"t":"bool","v":true}}}],"n":"probe_entries"}],"ret":{"t":"ptr","v":"0x006ff718"},"side":{"rng":{"before":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}},"after":{"t":"struct","v":{"left":{"t":"i32","v":413},"index":{"t":"i32","v":211},"block":{"t":"i32","v":0},"words":{"t":"i64","v":211},"block_hash":{"t":"u64","v":"7256082235576268916"}}}}}} diff --git a/verify/results/shim/l1/watch-arm.txt b/verify/results/shim/l1/watch-arm.txt new file mode 100644 index 0000000..1e517f9 --- /dev/null +++ b/verify/results/shim/l1/watch-arm.txt @@ -0,0 +1,77 @@ + +17:31:12.313 [tid 3828] watch: VEH=01848448 text=[0x00e81000,0x0145ca00) out=C:\SOTS\shim.watch.txt +17:31:12.329 [tid 3828] watch: arm hook StrategyServer::ApplyAllTurnCommands rva=0x0038f6a0 va=0120f6a0 create=MH_OK +enable=MH_OK +17:34:46.960 [tid 3828] watch: systems vector @0x1f2b20ac first=0x0e5e46c8 last=0x0e5e4738 end=0x0e5e4738 count=28 +(expect 28 on the reference game) +17:34:46.960 [tid 3828] watch: NVO header probe -- ServerSystem+0x274 validated on 9 systems, +0x26c on 0; using ++0x274 (pointer delta 0) +17:34:46.960 [tid 3828] watch: sys[0] @0x0e5bd0b0 'Caladan' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[1] @0x0e587d80 'Hyperion' AFlags=0x10 SnLv=0x00000300 lv[0..14]=0 0 0 0 3 0 0 0 0 +0 0 0 0 0 0 NVO=1 NVE=1 root=0x0e5ca6d0 key=4 ok=1 +17:34:46.960 [tid 3828] watch: sys[2] @0x0e588060 'Tully' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[3] @0x0e588340 'Foo' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[4] @0x0e588620 'Gamma Cephei' AFlags=0x1 SnLv=0x00000003 lv[0..14]=3 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 NVO=1 NVE=1 root=0x1f309038 key=0 ok=1 +17:34:46.960 [tid 3828] watch: sys[5] @0x0e588900 'Biter' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[6] @0x0e588be0 'Flinx' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[7] @0x0e588ec0 'Procyon' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[8] @0x0e5891a0 'Kor?Voth' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[9] @0x0e589480 'Ke'Rassak' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[10] @0x0e589760 'Koa?Tuun' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[11] @0x0e5dc6c8 'Spica' AFlags=0x0 SnLv=0x00000200 lv[0..14]=0 0 0 0 2 0 0 0 0 0 0 +0 0 0 0 NVO=1 NVE=0 root=0x1f309888 key=4 ok=1 +17:34:46.960 [tid 3828] watch: sys[12] @0x0e5dc9a8 'Junction' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[13] @0x0e5dcc88 'Nemesis' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[14] @0x0e5dcf68 'Keu?Loko' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[15] @0x0e5dd248 'Ke'Dolarra' AFlags=0x2 SnLv=0x0000000c lv[0..14]=0 3 0 0 0 0 0 0 +0 0 0 0 0 0 0 NVO=1 NVE=1 root=0x1f3090d0 key=1 ok=1 +17:34:46.960 [tid 3828] watch: sys[16] @0x0e5dd528 'Koa?Vo' AFlags=0x80 SnLv=0x0000c000 lv[0..14]=0 0 0 0 0 0 0 3 0 0 +0 0 0 0 0 NVO=1 NVE=1 root=0x1f3096c0 key=7 ok=1 +17:34:46.960 [tid 3828] watch: sys[17] @0x1f2e7890 'Delphi' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[18] @0x1f2e8718 'Kaa?Vaalu' AFlags=0x10 SnLv=0x00000300 lv[0..14]=0 0 0 0 3 0 0 0 +0 0 0 0 0 0 0 NVO=1 NVE=1 root=0x1f309330 key=4 ok=1 +17:34:46.960 [tid 3828] watch: sys[19] @0x1f2e6fd8 'Mu Bootis' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[20] @0x1f2e7e60 'Kor?Hakaan' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[21] @0x1f2e72c0 'Serpens' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[22] @0x1f2e8148 'Markab' AFlags=0x10 SnLv=0x00000300 lv[0..14]=0 0 0 0 3 0 0 0 0 0 +0 0 0 0 0 NVO=1 NVE=1 root=0x1f309920 key=4 ok=1 +17:34:46.960 [tid 3828] watch: sys[23] @0x1f2e75a8 'Pascal' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[24] @0x1f2e7b78 'Kao?Lolto' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[25] @0x1f2e8430 'Kea?Pono' AFlags=0x10 SnLv=0x00008300 lv[0..14]=0 0 0 0 3 0 0 2 0 +0 0 0 0 0 0 NVO=2 NVE=1 root=0x1f309168 key=4 ok=1 +17:34:46.960 [tid 3828] watch: sys[26] @0x1f2e8a00 'Gienah' AFlags=0x0 SnLv=0x00000000 lv[0..14]=0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 NVO=0 NVE=0 root=0x00000000 key=0 ok=0 +17:34:46.960 [tid 3828] watch: sys[27] @0x1f2eea08 'Ko'Rorkor' AFlags=0x10 SnLv=0x00000300 lv[0..14]=0 0 0 0 3 0 0 0 +0 0 0 0 0 0 0 NVO=1 NVE=1 root=0x1f309ae8 key=4 ok=1 +17:34:46.960 [tid 3828] watch: trade routes -- manager=0x0e5c0818 (S+4+0x154) vector@mgr+0x3c first=0x00000000 +last=0x00000000 end=0x00000000 count=0 +17:34:46.960 [tid 3828] watch: spy programs -- manager=0x1f2c8218 (S+4+0x158) vector@mgr+0x10 first=0x00000000 +last=0x00000000 end=0x00000000 count=0 +17:34:46.960 [tid 3828] watch: SELFTEST canary writes=1 traps=1 dr7=0xdddd0055 PASS +17:34:46.960 [tid 3828] watch: slot 0 -> 'Spica' NVO root+0x10 {touched:i16,TShn:i16} key=player 4 = 0x1f309898 +17:34:46.960 [tid 3828] watch: slot 1 -> 'Spica' SnLv (sys+0x24) = 0x00000200 at arm; key player 4 level 2 = +0x0e5dc6ec +17:34:46.960 [tid 3828] watch: slot 2 -> trade routes vector _Mylast (mgr+0x40, count 0 at arm) = 0x0e5c0858 +17:34:46.960 [tid 3828] watch: slot 3 -> spy programs vector _Mylast (mgr+0x14, count 0 at arm) = 0x1f2c822c +17:34:46.960 [tid 3828] watch: ARMED on tid 3828 dr7=0xdddd0055, S=1f2b2068 (ApplyAllTurnCommands this) + + diff --git a/verify/results/shim/l1/watch-hits.txt b/verify/results/shim/l1/watch-hits.txt new file mode 100644 index 0000000..06980b8 --- /dev/null +++ b/verify/results/shim/l1/watch-hits.txt @@ -0,0 +1,24 @@ +watchhit seq=0 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x00fc5a84,0x014187a6,0x012ea916,0x014052f8 +watchhit seq=1 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a +watchhit seq=2 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a +watchhit seq=3 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a +watchhit seq=4 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a +watchhit seq=5 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a +watchhit seq=6 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a +watchhit seq=7 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x0125cb9f scan=0x014187a6,0x012ea916,0x014052f8,0x00fa677a +watchhit seq=8 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db95a value=131075(0x00020003) tid=3828 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806 +watchhit seq=9 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db965 value=196611(0x00030003) tid=3828 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806 +watchhit seq=10 mark=1 slot=1 dr6=0xffff0ff2 eip=0x0129e78b value=512(0x00000200) tid=3828 ebpret=0x0129e99e ebpret2=0x012ea927 scan=0x0129e99e,0x012ea927,0x014052f8,0x00fa677a +watchhit seq=11 mark=1 slot=1 dr6=0xffff0ff2 eip=0x0129e78b value=512(0x00000200) tid=3828 ebpret=0x0129e99e ebpret2=0x012ea927 scan=0x0129e99e,0x012ea927,0x014052f8,0x00fa677a +watchhit seq=12 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x00fc5a84,0x014187a6,0x012ea916,0x012e2696 +watchhit seq=13 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73 +watchhit seq=14 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73 +watchhit seq=15 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73 +watchhit seq=16 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73 +watchhit seq=17 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73 +watchhit seq=18 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73 +watchhit seq=19 mark=1 slot=1 dr6=0xffff0ff2 eip=0x012ea78c value=512(0x00000200) tid=3828 ebpret=0x012ea916 ebpret2=0x01259820 scan=0x014187a6,0x012ea916,0x012e2696,0x0120ae73 +watchhit seq=20 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db95a value=196611(0x00030003) tid=3828 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806 +watchhit seq=21 mark=1 slot=0 dr6=0xffff0ff1 eip=0x011db965 value=196611(0x00030003) tid=3828 ebpret=0x011dbd4a ebpret2=0x011dbdc3 scan=0x011d29f6,0x014056db,0x011dbd4a,0x012ea806 +watchhit seq=22 mark=1 slot=1 dr6=0xffff0ff2 eip=0x0129e78b value=512(0x00000200) tid=3828 ebpret=0x0129e99e ebpret2=0x012ea927 scan=0x0129e99e,0x012ea927,0x012e2696,0x0120ae73 +watchhit seq=23 mark=1 slot=1 dr6=0xffff0ff2 eip=0x0129e78b value=512(0x00000200) tid=3828 ebpret=0x0129e99e ebpret2=0x012ea927 scan=0x0129e99e,0x012ea927,0x012e2696,0x0120ae73