lane V: live verification of the event-posting model against an advance prediction

Lane P wrote the expected numbers into sots-engine docs/P-events-wiring.md §4 before
this run existed. Every one held.

First End Turn (ref-turn2 -> Launch -> End Turn, shim.cfg.recapb3 unchanged, build
eventlive-dd38117-20260908T0916Z, main dd38117 with no source change): 3 calls, 3
compared, 0 divergent, tracecmp exit 0 — where lane R's run exited 1 on
side.events.after.v.next_id orig=4 ours=3. Call 0 read turn=3,
events_turn_bucket_exists=true, events_next_id_in=3, events_in_turn_bucket=1,
events_dedup_risk=0, no events_scan_truncated, next_id 3->4 on both sides,
turns/turns_bytes 2/48 unchanged, node[144].progress 2879->5768, flag 1->2, rng
identical — the whole predicted list.

Five End Turns: exit 1 with 2 divergent calls instead of 3, each short by exactly 1
(next_id orig 7 ours 6, orig 12 ours 11) = the deliberately unmodelled
EVENT_TECHS_UNLOCKED. A 0-divergence result there would have been suspicious.

sizeof(Game::ObservedTech) = 44 measured live: observed_techs.bytes grew by exactly
44 on both completion calls (440->484, 484->528), confirming lane X's static pin
behaviourally. Non-researching players measured 880 = 20 x 44 and never moved.

One deviation from lane R, and it is a workload effect, not a defect: rng diverged on
call 9 because the completed tech had research_roll_pending set and the
OnTechResearched callback drew one word ours does not (left 374 vs 375). Lane R's
"RNG matched 15 of 15" was workload luck; the honest statement is that the rng region
matches on every call that does not complete a roll-triggering tech.

Zuul double roll CLOSED. No species-5 save existed, so this lane made one: a custom
game with only Zuul in the Available Species pool, 4 End Turns under the same compare
config. 8 calls, 8 compared, 0 divergent, exit 0; all four researching calls have
species=5 and advance the generator by two, not one (left 540->538, 522->520,
504->502, 485->483), and ours reproduced every post-state bit-for-bit. Save added as
verify/results/saves/zuul-turn5-species5.sav.

End-Turn oracle byte-identical to lane R's on the first turn, so none of this
perturbs the game. clean_room_check OK and host ctest 33/33, run as separate commands.
VM140 restored to the recap build at the main menu and released.
This commit is contained in:
alex 2026-09-08 05:59:22 -04:00
parent 3e918d7372
commit 7eabfefff2
19 changed files with 2982 additions and 9 deletions

View file

@ -55,20 +55,20 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
| engine: game/design | engine | verified | high | 100% | 2026-09-07 | merged; see ShipDesignDef row |
| P2-B1 ComputeBudget (behavioral) | phase2 | verified | med | 75% | 2026-09-08 | LIVE + GREEN on declared regions (4,437 compares, 0 divergences; replace-mode oracle byte-identical). Corrections: out-param int[22]; researchMoneyKept gated on a research target. QUALIFIED BY THE HARNESS AUDIT: replace mode runs the ORIGINAL A SECOND TIME to harvest slots -> a real per-turn double effect (ComputeOutput repairs ships in orbit) that no region reaches - UNRESOLVED; the over-budget int at Budget+0x64 was captured only as an argument and args are never compared. Golden trace needs recapture with guards. Coverage gaps: 8 slots always 0 (expenses/aid/debt) . RECAPTURED WITH GUARDS 2026-09-08 (lane R): 4284 compared, 0 diverged, exit 0; `budget_object` guard 0 undeclared writes, so Budget+0x64 never took a different value (weaker than 'never written'). COVERAGE IS NARROWER THAN THE NUMBER: only **20 distinct (player, output) states** in 4284 calls (4278 are the UI polling one player; the turn pass is ~8 calls), and **13 of the 22 slots are 0 on EVERY call** - including 5 of the 6 declared input-boundary slots, which therefore prove nothing twice over. 8 modelled slots ever carry a value |
| P2-B2 OnTechResearched (behavioral) | phase2 | verified | high | 80% | 2026-09-08 | LIVE: 3 completions compared, 0 divergences (incl. one tech absent from the effects table = tail-only, the case an early return would have broken). float32 CONFIRMED ON THE GAME bit-for-bit (con_mod 1.0->0.899999976 = (float)(x+(double)0.1f)). Found B3's rolling effect: ServerPlayer::RollResearchEvent draws one NextFloat at the top of the callback - modelled + RNG region declared, but the branch never fired here so NOT behaviourally proven. KNOWN GAP: ours posts no events and the player event list is still undeclared, so the clean compare bounds the economy fields only and the replace/oracle pass was deliberately run on a no-completion turn (weak check) . RECAPTURED WITH GUARDS 2026-09-08 (lane R): 2 calls, 0 divergences; the `player` guard reports **player+0x2b0 (EvNxID) on BOTH calls**, so 'ours posts no events' is now measured, not assumed. **RollResearchEvent branch FIRED and matched** (see its row). Also caught an undeclared `vector<ObservedTech>` append at player+0x274. Reachable only after 5 End Turns from ref-turn2 |
| 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 |
| 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: **V-eventlive** (F-fpucw released 2026-09-08 05:05 local; M-movefleet before it). 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-<lane>, stage dist-<lane>, deploy C:\SOTS\shimdist-<lane>. 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. |
| Zuul double-roll (behavioural) | verify | backlog | — | 0% | 2026-09-08 | CONFIRMED NEEDED: ref-turn2 has only species 0 and 2, so the double roll is verified by disassembly + host tests only. Needs one compare from a species-5 save; the check is just that `left` drops by 2 not 1 |
| VM140 exclusivity (lab rule) | meta | verified | high | 100% | 2026-09-08 | one agent at a time. Holder: **FREE** (V-eventlive released 2026-09-08 05:52 local; 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-<lane>, stage dist-<lane>, deploy C:\SOTS\shimdist-<lane>. 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 <text>` 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. |
| 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 |
| 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 |
| 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 |
| section-loader compare crash | verify | backlog | — | 0% | 2026-09-08 | SectionDictionary compare crashes the engine while the identical weapon path succeeds -> fault is in re-running LoadSection, not the manifest reader. Next boundary: hook LoadSection itself. docs/M2.md has 3 ranked leads |
| P2-B4 colony + movement (behavioral) | phase2 | mapped | med | 70% | 2026-09-08 | LIVE: 36 calls compared, 0 divergences, tracecmp exit 0. Scout's headline: RNG left-delta 0 and mt hash identical on ALL 28 systems (only ProcessRebellion draws; none fired) - fpu_cw 0x127f confirmed. One real fleet move reproduced bit-for-bit; PlanFleetMovement's schedule matched. 3 prototypes + 6 helpers VERIFIED and written back to Ghidra; 22 formula corrections. THREE hook bugs found by reading the trace, not the verdict (stale args from describe_args-before-regions; StrategyServer has TWO bases 4 bytes apart; off_Fleets was a Ghidra-base number used as raw) - each would have given a clean compare that checked nothing. COVERAGE IS THIN: only 3 owned systems, 1 moving fleet, gate traffic all-zero; bats2, plague, rebellion, slaves, terraform, jumps, arrivals untested. No replace mode (input boundary). VM released: main menu, hooks=trace, build b4-fix2-20260908T0615Z . **QUALIFIED 2026-09-08 by lane R:** with 45 MoveFleet calls over 5 turns instead of 7 over 1, **8 of the 15 moving calls diverge by 1 ULP of position**. B4's clean verdict was a ONE-SAMPLE verdict - fleet 34 (its only mover) still matches bit-for-bit; the bug appears once other fleets move. ProcessTurn recaptured at 140 calls / 0 divergences, but only `ntdev` (15x) and `rcex` (7x) ever moved: everything else was byte-identical on all 140, so the verdict bounds two counters. Guard found the AI home system's fleet vector growing every turn (the build queue emitting a ship) |
| harness gap: undeclared side-effect lists | verify | verified | high | 100% | 2026-09-08 | FIXED STRUCTURALLY (engine 3f0721f+): compile-time-required Coverage on every descriptor (a hook without one does not compile); Guard regions that catch AND localise undeclared writes (names player+0x2b0, not 'the hash moved'); replace mode now emits records; tracecmp prints coverage on every report + --strict-coverage. A hook claiming 'complete' while a guard caught an undeclared write now counts as a DIVERGENCE. Audit found 23 undeclared side effects: docs/harness-audit.md |
| RollResearchEvent draw (behavioural) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane R.** On turn 6 (`IND_TRKSTL`, tech 10094) `research_roll_pending` was true going in; the original drew exactly one word (rng `left` 375->374, `next_index` 249->250) and cleared the flag, and `ours` reproduced both bit-for-bit on the scratch generator. 0 divergences on that call. Needs 5 End Turns from ref-turn2 to reach - the reference turn has no completion at all |
| RollResearchEvent draw (behavioural) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane R.** On turn 6 (`IND_TRKSTL`, tech 10094) `research_roll_pending` was true going in; the original drew exactly one word (rng `left` 375->374, `next_index` 249->250) and cleared the flag, and `ours` reproduced both bit-for-bit on the scratch generator. 0 divergences on that call. Needs 5 End Turns from ref-turn2 to reach - the reference turn has no completion at all | . RE-OBSERVED by lane V from the CALLER side 2026-09-08: on the B3 hook's own call 9 the same draw shows as an `rng` divergence (`left` 374 vs 375) plus guard spans `player+0x3b4` (`ResErrRoll`) and `player+0x196` (byte 2 of design mask B at `+0x194`). Still NO save that starts with `research_roll_pending` true - the flag has only ever been seen set and cleared inside one turn
| golden-trace recapture (post-guards) | verify | verified | high | 100% | 2026-09-08 | DONE on the live game, build `recap-7584bad-20260908T0615Z` (NO source change needed - the audit's machinery did all of it). **B3 ProcessResearch: the defect is VISIBLE** - `side.events.after.v.next_id orig=4 ours=3`, one divergent call of 3 on the reference turn and its ONLY divergent field; bit-for-bit the `EvNxID 4->3` that previously needed a 609 KB save diff. Over 5 turns 15 calls / 3 diverged, **RNG matched 15/15**, and the two completion calls miss TWO event ids each. **B1 ComputeBudget: verdict held** - 4284 compared, 0 diverged, exit 0, `budget_object` guard caught 0 undeclared writes (Budget+0x64 never changed value). **MoveFleet: 8 of 45 diverge by 1 ULP of position** (new; see its own row). First guarded captures for OnTechResearched (2 calls), ServerSystem::ProcessTurn (140 calls) and MoveFleet (45). Guards mapped SetResearched live (ConMod[0..2], OutMod, PopMod, ResTNm, TechTree+0x20 order counter) and found an UNDECLARED `vector<ObservedTech> otch` append at player+0x274. Oracle held on every run's first End Turn. Report `findings/subsystems/golden-trace-recapture.md`; engine `docs/R-recapture.md`; traces `verify/traces/recap-*`, reports `verify/results/compare/recap-*`. sots-engine branch `wip/recapture` e50d5e5 (merged with main 82ef52f; ctest 32/32, clean-room OK) |
| MoveFleet position rounding (1 ULP) | verify | verified | high | 100% | 2026-09-08 | **CLOSED by lane M.** Mechanism read off the instruction stream, not fitted: the engine's `Mars_Vec3_Normalize` (0x00422520, 123 callers) narrows to float32 FOUR times - `sumsq = f32(x*x+y*y+z*z)` (products/adds stay in 53-bit regs, only the SUM is stored), `len = f32(sqrt(sumsq))`, `inv = f32(1.0/len)` a RECIPROCAL that is MULTIPLIED through rather than three divides, and `dir.c = f32(delta.c*inv)`; and MoveFleet stores each `dest.c - pos.c` BACK TO A FLOAT32 SLOT before calling it, and takes the leg distance from that same call's return value. `ours` did all of it in double. The position tail (`f32(pos + f32(dir*move))`) was already right, which is exactly why the error was a constant ABSOLUTE ~1.2e-7. Confirmed OFFLINE first (an arrival copies the destination verbatim, so calls 115/155 hand you fleet 34's and fleet 50's exact float32 destinations = 8 fully determined legs; the 5-narrowing model reproduces the ORIGINAL bit-for-bit on all 8, the old double model reproduces `ours` on the 3 divergent ones), then LIVE: control run 8/45 diverged exit 1, fixed run **0/45 diverged exit 0**, with identical args, identical pos.before and identical ORIGINAL pos.after on all 45 calls. Report `findings/subsystems/movefleet-position-rounding.md`, engine `docs/M-movefleet.md`, branch `wip/movefleet` 2aa8cba |
| undeclared ObservedTech append | verify | backlog | — | 0% | 2026-09-08 | NEW (lane R). A tech completion grows `vector<ObservedTech> otch` at ServerPlayer+0x274 (all three vector words move = a realloc). Seen as an undeclared write by BOTH the ProcessResearch `player` guard and the OnTechResearched `player` guard. It is serialized ServerPlayer state and it is in NO coverage note anywhere - a third list append in the same neighbourhood as the event list. B3's replace oracle never saw it because turn 1 of ref-turn2 has no completion | **APPEND SITE NOW NAMED (lane X): `RecordObservedTech` 0x007ba1a0, called from OnTechResearched; stride 0x2c.** Still needs a declared region + a model in ours.
| undeclared ObservedTech append | verify | mapped | high | 60% | 2026-09-08 | NEW (lane R). A tech completion grows `vector<ObservedTech> otch` at ServerPlayer+0x274 (all three vector words move = a realloc). Seen as an undeclared write by BOTH the ProcessResearch `player` guard and the OnTechResearched `player` guard. It is serialized ServerPlayer state and it is in NO coverage note anywhere - a third list append in the same neighbourhood as the event list. B3's replace oracle never saw it because turn 1 of ref-turn2 has no completion | **APPEND SITE NOW NAMED (lane X): `RecordObservedTech` 0x007ba1a0, called from OnTechResearched; stride 0x2c.** Still needs a declared region + a model in ours. . PARTLY ADDRESSED (lane P + lane V 2026-09-08): it IS now a declared **Result** region (`observed_techs`, ServerPlayer+0x274), so it has left the guards' undeclared list and appears in the diff instead - lane V measured the span growing by exactly 44 on both completion calls (440->484, 484->528) and `ours` short by that 44 each time. STILL OPEN: `ours` does not append, so the region diverges on every completion. The element and the append site are fully pinned (lane X/S: sizeof 0x2c, RecordObservedTech 0x007ba1a0, DE-DUPLICATES BY TECH NAME), so what is left is modelling it - and that belongs with whoever takes SetResearched
| unnamed offsets from guard hits | verify | backlog | — | 0% | 2026-09-08 | NEW (lane R). Three spans the guards report every run and no addresses.json entry names: **ServerSystem+0xd8 (1 B)** and **ServerSystem+0x238 (4 B)** - written by the AI home system on every colony turn, alongside the fleet-vector growth; **StarFleet+0xdc (1 B)** - written on every moving MoveFleet call, just past Speed (FPsp2 @0xd8). Cheap wins for the contract |
| waypoint types 2-5 have no coverage | verify | backlog | — | 0% | 2026-09-08 | NEW (lane M, promoted from a coverage line to its own row because it is now the biggest gap in `MoveFleet`). Types 2 (node line), 3 (node route), 4 (gate teleport) and 5 (probabilistic jump) have NEVER fired in any capture, and the node-line step is WRONG BY CONSTRUCTION - `sim::NodeLineStep` and `sim::BuildStutterSegments` are written and host-tested but are NOT wired into the hook, which steps every waypoint type as `speed x dt`. **`ref-turn2` structurally cannot exercise them**: lane M held the VM and tried. The only mover in that save is the AI, which travels straight runs; the player that would travel a node line has `DE 00 CR 00 DN 00` at its home system (screenshot `verify/results/shim/mf-human-home-no-ships.png`), so Move/Manage Fleets are greyed out on every turn. Needs a ship built over several turns, or - much cheaper - a PURPOSE-BUILT SAVE with a fleet already in orbit next to a node line. Same save would unblock the gate-traffic and probabilistic-jump rows. Also owed on that path: `sim::Distance` is still plain double, and `Mars_Vec3_Length` (0x004224b0) says every vector length in the engine is float32-narrowed twice, so the stutter geometry is probably 1 ULP out the same way the position update was - deliberately left alone by lane M because there is no behavioural evidence to correct it against |
| ref-turn2 has no tech completion | meta | verified | high | 100% | 2026-09-08 | TRAP for anyone writing a workload (lane R). The documented one-End-Turn recipe produces **zero** `OnTechResearched` calls - an empty log that still passes. It takes 5 End Turns (to turn 7) to reach a completion. Also: only the FIRST End Turn is reproducible - it hashed to the oracle on all four runs and its research calls reproduce docs/B3.md exactly, but from turn 4 the AI picks a different research target than B3 recorded while the point totals stay nearly identical. Treat anything past turn 1 as *a* run, not *the* run |
@ -77,19 +77,19 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
| event posting API | subsystem | mapped | high | 90% | 2026-09-08 | RECOVERED (lane E, `findings/subsystems/events.md`). Container: `EventStorage` embedded at `ServerPlayer+0x29c` (0x1c), `EvNxID` at +0x14 = player+0x2b0 — exactly the guard's byte run. Nested `vector<TurnEvents{int EvTurn; vector<PlayerEvent>}>`, record 0x74 B, tags `EvEID EvDsc EvMsg EvImg EvLoc EvPos EvAct EvCID`; layout confirmed field-by-field against turn3-state.sav, which CONTAINS the overbudget record. Entry point `int __thiscall EventStorage::PostEvent(this, string BYVAL, string BYVAL, obj*, Vector3*, turn, const char* img, int act)` 0x008862b0 RET 0x4c — **161 call sites in 113 functions, the whole sim's event API**. B3 defect fully explained: 0x00587b97, in the completion-roll-FAILED branch under `!wasDone && nowDone && owner`. 3 note corrections (EvPos is FLT_MAX not inf; the save array is turn-bucketed not flat; TECHS_UNLOCKED has no parent clause). 56 entries in addresses.json; 11 prototypes + 13 labels + 12 comments + 2 structs written back to Ghidra. Engine: `sots-engine` branch `wip/events` a7348be, `src/game/events` + 112 checks, ctest 32/32. NOT YET WIRED INTO A HOOK — see `docs/E-events.md` for the proposed region/Coverage change |
| state-checksum replay harness | verify | verified | high | 90% | 2026-09-08 | Lane C: `verify/state-checksum/` (tool, 38 tests, `STATE_CHECKSUM.md`, evidence in `verify/results/state-checksum/`). Whole-state digest tree; **coverage is PROVED by byte-for-byte re-serialisation**, not declared - the answer to empty-region-set green verdicts. Localises: the known load->re-save delta reports as exactly 5 named leaves (`/Sim/players/Player[496 "Singularity"]/Status: 4 -> 0`, `/Summary/Checksum`), and one real End Turn as 108 attributed diffs. All 10 saves STABLE + COVERED. Float policy = exact bits by default, `canonical` for -0.0/NaN only, **tolerance deliberately not a hashing mode** (it lives in `--ulps` on the differ); corpus has 0 NaN/-0.0/subnormals so canonical is a no-op today. Chain record/verify validated on the real turn1-3 saves. REMAINING 10%: the VM-driven replay loop is designed (§5) but UNRUN - needs the VM holder. Open question named in §3.5 with the experiment that settles it (force `fpu_cw` 0x027f/0x127f/0x137f across End Turn, checksum the three autosaves) **§3.5 CLOSED 2026-09-08 by lane F** (see the fpu_cw row): measured, not assumed — 53-bit == 64-bit, so `floats=bits` costs the SSE port nothing; 24-bit and round-up each name one witness. The tool localised both to single leaves out of 35,394, with coverage PROVED on all 8 new saves. |
| MoveFleet position ULP divergence | phase2 | verified | high | 100% | 2026-09-08 | **DONE (lane M).** First arithmetic divergence caught by BEHAVIOURAL compare rather than static reading, and it is fixed by matching the original's precision sequence rather than by fitting numbers - see the `MoveFleet position rounding (1 ULP)` row for the mechanism. Live 8 -> 0 on the same 45 calls, control run included so the before/after is this lane's own measurement. **COVERAGE IS UNCHANGED AND STILL THIN**: 15 of 45 calls move and all 15 are the same straight-run waypoint type. Waypoint types 2-5 were ATTEMPTED and could NOT be reached - the only player that would travel a node line has `DE 00 CR 00 DN 00` at its home system on this save, so its Move/Manage Fleets buttons are greyed out every turn and there is literally nothing to send along the node lines the map draws. Reaching them needs a ship BUILT over several turns, or (cheaper) a purpose-built save that starts with a fleet in orbit beside a node line. The node-line step is still wrong by construction: NodeLineStep/BuildStutterSegments exist and are unit-tested but are not wired into the hook |
| ObservedTech append (undeclared) | verify | backlog | — | 0% | 2026-09-08 | Lane R's guards caught a vector<ObservedTech> append at `player+0x274` during SetResearched. It is SERIALIZED state and appears in NO coverage note anywhere - found only because guards localise rather than just flag a moved hash. Needs a declared region + a model in ours |
| ObservedTech append (undeclared) | verify | mapped | high | 60% | 2026-09-08 | Lane R's guards caught a vector<ObservedTech> append at `player+0x274` during SetResearched. It is SERIALIZED state and appears in NO coverage note anywhere - found only because guards localise rather than just flag a moved hash. Needs a declared region + a model in ours | . PARTLY ADDRESSED (lane P + lane V 2026-09-08): it IS now a declared **Result** region (`observed_techs`, ServerPlayer+0x274), so it has left the guards' undeclared list and appears in the diff instead - lane V measured the span growing by exactly 44 on both completion calls (440->484, 484->528) and `ours` short by that 44 each time. STILL OPEN: `ours` does not append, so the region diverges on every completion. The element and the append site are fully pinned (lane X/S: sizeof 0x2c, RecordObservedTech 0x007ba1a0, DE-DUPLICATES BY TECH NAME), so what is left is modelling it - and that belongs with whoever takes SetResearched
| fpu_cw sensitivity experiment | verify | verified | high | 100% | 2026-09-08 | **DONE (lane F): 53-bit vs 64-bit x87 makes NO difference — the SSE port has no double-rounding budget to preserve, `STATE_CHECKSUM.md` §3.5 CLOSED.** 7 End Turns from ref-turn2, 6 control words, whole-state checksum on each. stock / 0x027f / 0x127f / **0x137f (64-bit)** all give `978041ac…` identical across all 35,394 leaves. Two settings DO move state, each reproduced on a repeat run: **0x007f (true 24-bit)** -> `/Sim/systems/Sys[112 "Gamma Cephei"]/Pop2/PopG/PopC 540000000->540000002` (+derived Summary/Checksum); **0x1a7f (53-bit, round-UP)** -> `/Sim/fleets/Flt[34 "Beta Fleet"]/Pos/.[0]` and `/Pos/.[2]`, 1 ULP each. So the port must hold intermediates at 53 bits (never compute a chain in `float`) and use round-to-nearest — both SSE defaults, now measured. **THE BRIEFED TRIPLE WAS UNDER-POWERED: 0x027f is 53-bit (differs from 0x127f only in bit 12, infinity control, ignored since the 387) and 0x137f is 64-bit, not a rounding change** — run as written, all three come back identical and would have 'proved' something false. PC=bits 8-9, RC=bits 10-11. Setting PROVEN to hold: read-back at each force + 38 in-pipeline hook samples per run spanning turn phases 4/6/8, all at the forced value (`verify/results/fpu-cw/cw-census.txt`). `Mars::Application::Run` calls `_controlfp(0x50000,0x3070300)` at 0x0089f606 EVERY FRAME = 0x127f, so forcing at EndTurn is wiped before BeginProcessTurn; BeginProcessTurn is the point that works. TRAP: under 24-bit the CRT's own %g rendering degrades, so trace TEXT is not a comparison surface — use `verify/fpu-cw/trace_bitdiff.py`. findings/subsystems/fpu-precision-sensitivity.md |
| Summary.Checksum algorithm | objects | blocked | — | 0% | 2026-09-08 | Lane C RULED OUT two candidates so nobody repeats them: NOT a byte sum over the inflated stream, NOT a sum over the int leaves. Each is consistent with the -16 re-save delta but leaves no constant residual across turns |
| event posting in ours | phase2 | in-progress | — | 0% | 2026-09-08 | Lane P: make ours actually post events so ProcessResearch's `side.events.after.v.next_id` 4->3 divergence closes. Converts harness-audit row 1 from known-defect to checked, and unbounds B2/B3 whose clean compares currently cover economy fields only |
| LAB RULE: no `git add -A` in sots-re while lanes run | meta | verified | high | 100% | 2026-09-08 | MY error, caught by lane M: an integrator `git add -A` in the SHARED `sots-re` clone swept a running lane's in-progress files into commit 9d385a7 mid-run (remainder landed in f5b37c2). Nothing was lost, but authorship and atomicity were. RULE: while any lane is live, the integrator stages sots-re by explicit PATH only (`git add campaign/board.md campaign/DASHBOARD.md`), never `-A`. Lanes own their own subtrees. sots-engine is unaffected - lanes work in per-lane worktrees there, which is exactly why that repo has not had this problem |
| MoveFleet waypoint types 2-5 | verify | backlog | — | 0% | 2026-09-08 | Still ZERO behavioural coverage after lane M. Not for lack of trying: the only mover in ref-turn2 is the AI (straight runs only), and the player that would travel a node line has DE/CR/DN all 00 at its home system, so Move/Manage Fleets are greyed out every turn - there is nothing to send along the node lines the map draws. Needs a ship built over several turns or a purpose-built save. The type-2 node-line step is still WRONG BY CONSTRUCTION (B4). Also: sim::Distance deliberately left in double (only stutter geometry uses it); Mars_Vec3_Length says it is probably 1 ULP out the same way, but there is zero behavioural evidence to correct it against - do not "fix" it blind |
| P2-P event posting in ours | phase2 | mapped | high | 80% | 2026-09-08 | HOST-VERIFIED, VM RUN QUEUED (lane F holds VM140). next_id reaches 4 in a host reproduction of recap-b3 call 0, fixture rebuilt from raw bytes at the real 0x1c/0x18/0x74 strides and cross-checked against turn3-state.sav with lane C's state_checksum --tree. Count-only (lane E option a): ours never calls the game's PostEvent and REPLACE MODE WRITES NOTHING - a bumped EvNxID with no record behind it would corrupt the very save the oracle hashes. Three design points: the event scan is taken in describe_args BEFORE the original (taken after, ours would dedup against the original's own posts and agree for the wrong reason); dedup risk is MEASURED and reported as events_dedup_risk, not assumed; KeylessEventText resolves keys to "%s" so the shim carries no prose. VERIFIED from the instruction stream: SetResearched 0x00581e10 calls owner vft+0x10 with (flags>>2)&1 and ProcessResearch passes flags=2, so silent=false and the completion event IS posted - previously only inferable from "EvNxID moved by two". ctest 33/33, shim cross-builds on CT111 (lane P could only syntax-check) |
| P2-P event posting in ours | phase2 | verified | high | 95% | 2026-09-08 | HOST-VERIFIED, VM RUN QUEUED (lane F holds VM140). next_id reaches 4 in a host reproduction of recap-b3 call 0, fixture rebuilt from raw bytes at the real 0x1c/0x18/0x74 strides and cross-checked against turn3-state.sav with lane C's state_checksum --tree. Count-only (lane E option a): ours never calls the game's PostEvent and REPLACE MODE WRITES NOTHING - a bumped EvNxID with no record behind it would corrupt the very save the oracle hashes. Three design points: the event scan is taken in describe_args BEFORE the original (taken after, ours would dedup against the original's own posts and agree for the wrong reason); dedup risk is MEASURED and reported as events_dedup_risk, not assumed; KeylessEventText resolves keys to "%s" so the shim carries no prose. VERIFIED from the instruction stream: SetResearched 0x00581e10 calls owner vft+0x10 with (flags>>2)&1 and ProcessResearch passes flags=2, so silent=false and the completion event IS posted - previously only inferable from "EvNxID moved by two". ctest 33/33, shim cross-builds on CT111 (lane P could only syntax-check) | . **LIVE-VERIFIED AGAINST AN ADVANCE PREDICTION (lane V 2026-09-08, build `eventlive-dd38117-20260908T0916Z`, main dd38117 unchanged).** Lane P wrote the expected numbers into `docs/P-events-wiring.md` §4 BEFORE the run; every one held. First End Turn: **3 calls, 3 compared, 0 diverged, exit 0** (was 1 diverged) with `turn=3`, `events_turn_bucket_exists=true`, `events_next_id_in=3`, `events_in_turn_bucket=1`, `events_dedup_risk=0`, no `events_scan_truncated`, `next_id` 3->4 both sides, `turns`/`turns_bytes` 2/48 unchanged, node[144] 2879->5768 + flag 1->2, rng identical. Five End Turns: exit 1 with **2 divergent calls instead of 3**, each short by EXACTLY 1 (`next_id` orig 7 ours 6, orig 12 ours 11) = the unmodelled `EVENT_TECHS_UNLOCKED`, exactly as forecast - 0 divergences there would have been suspicious, not good. §4.3 conversion held too: `player+0x274/0x278/0x27c` left the guard's undeclared list (13 spans in 2 calls -> 10) and appear in the diff as `observed_techs.bytes`. End-Turn oracle byte-identical to lane R's. clean_room_check OK, ctest 33/33 (separate commands). Report `findings/subsystems/eventlive-verification.md`; engine `docs/V-eventlive.md`; traces `verify/traces/eventlive-b3-*`, reports `verify/results/compare/eventlive-b3-*`
| EVENT_TECHS_UNLOCKED not posted (predicted residual) | verify | backlog | — | 0% | 2026-09-08 | Lane P FLAGGED RATHER THAN GUESSED. Trigger IS pinned (SetResearched's sweep sets state=2 + stamps turnAvailable sticky at -1; tail loop collects state==2 && turnAvailable==currentTurn) but evaluating it needs the unlock cascade ours deliberately does not run. The driver takes the unlock list as an INPUT and is handed nullptr ("no list") - deliberately distinct from an empty list ("computed, empty"). PREDICTED RESIDUAL: next_id short by exactly 1 on every completion call. Posting it "whenever something completed" would score on this save and be WRONG the first time a completion unlocks nothing - the exact false-pass shape this project keeps catching |
| sizeof(ObservedTech) unpinned | objects | verified | high | 100% | 2026-09-08 | **PINNED (lane X).** `sizeof(Game::ObservedTech) = 0x2c (44)` -- three independent proofs: the magic divide `0x2e8ba2e9 sar 3` (= /44, exact) at 0x0087239f, `imul reg,reg,0x2c` at 0x0087243a / 0x007b735b, and the search stride `add edi,0x2c` at 0x007ba257. **Append site = `RecordObservedTech+0xdf` (0x007ba27f): `lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320`** -- a de-duplicating append, direct callee of OnTechResearched 0x00891790; the realloc through 0x007b5820 is why all three vector words move. Element FULLY MAPPED (lane S, from ObservedTech::Write 0x00817cf0 / Read 0x00817c40): +0x00 vptr 0x00a2439c (RTTI `.?AVObservedTech@Game@@`), +0x04 uint16 `otnF`, +0x06 uint16 `otnL`, +0x08 **bool** `odet` (1 byte), +0x0c std::string `otch` (**0x1c**, so +0x24 is its _Alval, NOT a field), +0x28 int `owith` -- 0x2c exactly, nothing unaccounted. Same shape as Game::ObservedWeapon (Write 0x00817bc0, tag `owep`). Built the general tool the row asked for: `tools/x86disp.py`, an x86 displacement xref scanner (100% code coverage, 0.17% desync). `findings/subsystems/observedtech-append.md` |
| sizeof(ObservedTech) unpinned | objects | verified | high | 100% | 2026-09-08 | **PINNED (lane X).** `sizeof(Game::ObservedTech) = 0x2c (44)` -- three independent proofs: the magic divide `0x2e8ba2e9 sar 3` (= /44, exact) at 0x0087239f, `imul reg,reg,0x2c` at 0x0087243a / 0x007b735b, and the search stride `add edi,0x2c` at 0x007ba257. **Append site = `RecordObservedTech+0xdf` (0x007ba27f): `lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320`** -- a de-duplicating append, direct callee of OnTechResearched 0x00891790; the realloc through 0x007b5820 is why all three vector words move. Element FULLY MAPPED (lane S, from ObservedTech::Write 0x00817cf0 / Read 0x00817c40): +0x00 vptr 0x00a2439c (RTTI `.?AVObservedTech@Game@@`), +0x04 uint16 `otnF`, +0x06 uint16 `otnL`, +0x08 **bool** `odet` (1 byte), +0x0c std::string `otch` (**0x1c**, so +0x24 is its _Alval, NOT a field), +0x28 int `owith` -- 0x2c exactly, nothing unaccounted. Same shape as Game::ObservedWeapon (Write 0x00817bc0, tag `owep`). Built the general tool the row asked for: `tools/x86disp.py`, an x86 displacement xref scanner (100% code coverage, 0.17% desync). `findings/subsystems/observedtech-append.md` | . **CONFIRMED LIVE (lane V 2026-09-08)**: on the running game the `observed_techs` Result region grew by **exactly 44 bytes** on each of the two tech-completion calls of the 5-End-Turn run (440->484, 484->528), and both non-researching players' vectors measured 880 = 20 x 44 and never moved. Behavioural confirmation of a purely static pin
| lea-displacement xref scanner | meta | verified | high | 100% | 2026-09-08 | `tools/x86disp.py` -- fixes the systemic blind spot that Ghidra does not index ModRM displacements. Full x86-32 length decoder swept from Ghidra's 41,089 function starts: 2,174,504 instructions, 612,166 displacement sites, **100.0% code coverage, 70 desyncs (0.17%), zero unknown opcodes**. Validated against ground truth before use (re-finds `lea eax,[ecx+0x29c]` in GetEventStorage, both OnTechResearched +0x29c sites, and one NEW ProcessTurn site). HONEST LIMITS: it is a **recall** tool, not an oracle -- class-level precision at 0x274 is ~13% by function (99 sites / 45 functions, ~6 real), i.e. a 900x search-space cut that still needs one call-graph check. The naive byte scan it replaces is not wrong so much as **blind**: it misses 80/99 real sites at 0x274 and 13,784/14,611 at disp8 0x14. `cohort` ranking must never be used as a hard filter -- it would have discarded the correct ObservedTech answer. Works off a gitignored local cache in `dumps/`, so it does not hammer CT111 |
| lea-displacement xref scanner | meta | verified | high | 100% | 2026-09-08 | `tools/x86disp.py` - full x86-32 length decoder swept from 41,089 Ghidra function starts: 2,174,504 instructions, 612,166 disp sites, 0 unknown opcodes, 100.0% code coverage, 0.17% desyncs. VALIDATED against ground truth before any new claim (rediscovers GetEventStorage's lea ecx+0x29c, EvNxID +0x2b0; positive control: given 50 known ServerPlayer offsets, FUN_0087fac0 scores 50/50 = the serializer, nothing close). BUILD GOTCHA: clipping sweeps at fva+Ghidra sizeInBytes lost 11% of functions to mid-instruction truncation; sweeping to the NEXT function start took coverage 89% -> 100%. HONEST LIMIT: the win is RECALL not precision - naive lea-only scan MISSES 80 of 99 real 0x274 sites; class-level precision ~13% by function. Value = search space 41,411 -> 45 (~900x), then disambiguate by call graph |
| RANKER TRAP: cohort filter discards correct answers | meta | verified | high | 100% | 2026-09-08 | Lane X's cohort ranker WOULD HAVE DISCARDED THE CORRECT ANSWER. RecordObservedTech touches only 0x274/0x278 and nothing else on ServerPlayer, so every --min>=1 cohort filter drops it. What actually closed the case was plain `query` + ONE call-graph lookup on OnTechResearched's callees. RULE now in the tool docstring: it is a RANKER, NEVER a filter. Displacement scan for recall, call graph for disambiguation; neither alone sufficed |
| ObservedTech struct | objects | verified | high | 100% | 2026-09-08 | sizeof = 0x2c (44) by THREE independent proofs: exact magic divide 0x2e8ba2e9 sar 3 (= ceil(2^35/44), emulated against n=0..1000) at 0x0087239f; imul reg,reg,0x2c at 0x0087243a/0x007b735b; search stride add edi,0x2c at 0x007ba257. APPEND SITE: RecordObservedTech+0xdf (0x007ba27f) `lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320`. RecordObservedTech (0x007ba1a0) is a DIRECT CALLEE of OnTechResearched and DE-DUPLICATES BY TECH NAME before appending - a naive push_back in the reimpl WOULD DIVERGE on re-observation. Realloc through 0x007b5820 explains why lane R saw all three vector words move. Element FULLY MAPPED by lane S via the serializer lane X pointed at: +0x00 vptr 0x00a2439c (RTTI .?AVObservedTech@Game@@), +0x04 uint16 otnF, +0x06 uint16 otnL, +0x08 bool odet (ONE BYTE), +0x0c std::string otch (0x1c -> +0x24 is the string's _Alval, not a field), +0x28 int owith. Matches save_reader.py's on-disk order exactly. Lane P's live byte delta should still read exactly 44 |
| ObservedTech struct | objects | verified | high | 100% | 2026-09-08 | sizeof = 0x2c (44) by THREE independent proofs: exact magic divide 0x2e8ba2e9 sar 3 (= ceil(2^35/44), emulated against n=0..1000) at 0x0087239f; imul reg,reg,0x2c at 0x0087243a/0x007b735b; search stride add edi,0x2c at 0x007ba257. APPEND SITE: RecordObservedTech+0xdf (0x007ba27f) `lea ecx,[player+0x274]; call vector_ObservedTech_push_back 0x007b7320`. RecordObservedTech (0x007ba1a0) is a DIRECT CALLEE of OnTechResearched and DE-DUPLICATES BY TECH NAME before appending - a naive push_back in the reimpl WOULD DIVERGE on re-observation. Realloc through 0x007b5820 explains why lane R saw all three vector words move. Element FULLY MAPPED by lane S via the serializer lane X pointed at: +0x00 vptr 0x00a2439c (RTTI .?AVObservedTech@Game@@), +0x04 uint16 otnF, +0x06 uint16 otnL, +0x08 bool odet (ONE BYTE), +0x0c std::string otch (0x1c -> +0x24 is the string's _Alval, not a field), +0x28 int owith. Matches save_reader.py's on-disk order exactly. Lane P's live byte delta should still read exactly 44 | . **CONFIRMED LIVE (lane V 2026-09-08)**: on the running game the `observed_techs` Result region grew by **exactly 44 bytes** on each of the two tech-completion calls of the 5-End-Turn run (440->484, 484->528), and both non-researching players' vectors measured 880 = 20 x 44 and never moved. Behavioural confirmation of a purely static pin
| std::string size 0x18 vs 0x1c CONTRADICTION | objects | verified | high | 100% | 2026-09-08 | **RESOLVED (lane S): 0x1c was right all along; ONE layout binary-wide.** `_Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18`, sizeof 0x1c. `ObservedTech+0x24` is the string's own trailing allocator word, not the unaccounted data field lane X read it as. Settled by three COMPLETE ENUMERATIONS of the element, each of which skips +0x24: `ObservedTech::Write` 0x00817cf0 (serialises +0x04/+0x06/+0x08/+0x0c/+0x28 and nothing else), `ObservedTech_ctor` 0x008562a0, and the inlined copy ctor at 0x0079a184. Then generalised: new `tools/strfootprint.py` recovers every `(base,disp,tag)` handed to the Mars::Stream string helpers across the whole exe -- **65 std::string members off a non-stack base, ZERO with a sibling field inside the 0x1c span, 51 of the 52 measurable inter-member gaps exactly +0x1c** (the one +0x20 is StrategyServer KeyPath, +0x1c on its own Read side -- the writer skips a member). Corroborated by the vector<string> walk stride `add esi,0x1c` @0x00699c29, PostEvent's by-value strings at [ebp+8]/[ebp+0x24] with RET 0x4c, and MoraleEvent 0x50 = name@0x34 + 0x1c. NO empty-base variant, no custom allocator, no game-local string class. BLAST RADIUS: **zero recovered struct tables were wrong** -- every string-bearing layout in struct-recovery / save-editor-structs / events / schema-gaps-resolved already used 0x1c, `ServerPlayer::pswd` @0x2dc..0x2f7 included (Write 0x008563e0 puts the next member exactly 0x1c above). Only 4 prose statements carried the 0x18 number, all corrected. save_reader 36/36 and state_checksum unaffected and still green. **LESSON: never size a struct member from the offsets the code TOUCHES** -- `_Alval` is an empty allocator, never loaded or stored, so a touch-scan undercounts every string AND every vector by exactly 4. Size from an enumeration: serializer, ctor, copy ctor, or container stride. `observedtech-append.md` §9 |
| harness-audit row 11 CORRECTED (Budget+0x64) | verify | verified | high | 100% | 2026-09-08 | Lane X: row 11 is NOT SUPPORTED. ComputeBudget writes its Budget* only through esi into +0x00..+0x54; its only two +0x64 accesses are LOADS OFF A DIFFERENT BASE. And ProcessResearch's `int* overbudget` is a ProcessTurn STACK LOCAL (lea edx,[ebp-0x14] at 0x008914a5), not Budget+0x64. Agrees with lane R's 0-of-4284 guard result. Reclassified to "nothing shown to write it"; only a watchpoint settles it definitively |
| STANDING RULE: size structs by enumeration, never by touch-scan | meta | verified | high | 100% | 2026-09-08 | Produced by lane S after lane X's 0x18/0x1c scare. `_Alval` is std::allocator<char>, an EMPTY class: it occupies a word but is NEVER loaded or stored, so it is INVISIBLE to any analysis based on what the code touches. Sizing a member that way undercounts by exactly 4. RULE: size a member from an ENUMERATION - serializer, ctor, copy ctor, or container stride - because an enumeration can show ABSENCE where a touch-scan cannot. Same trap is live for std::vector here: {_Myfirst,_Mylast,_Myend,_Alval} = 0x10, ALLOCATOR-LAST, the opposite of the MSVC _String_val allocator-first shape the textbooks describe. Now in re-windows-2000s-howto.md 1c and struct-recovery.md 0 |

View file

@ -0,0 +1,189 @@
# Live verification of the event-posting model, and two workloads (lane V, 2026-09-08)
Lane P wrote a numeric prediction of this run into `sots-engine docs/P-events-wiring.md` §4
**before** the run existed. This is the check. **The prediction held field for field on both
runs** — the strongest evidence this project produces, because it was written down first.
* Build `eventlive-dd38117-20260908T0916Z` (sots-engine main `dd38117`, no source change),
cross-built on CT111 `/srv/re-lab/build/sots-engine-v`, staged `/srv/re-lab/shim/dist-v` →
`C:\SOTS\shimdist-v` on VM140. Exports byte-identical to the real `binkw32.dll` (66 names).
* Recipe: `shim.cfg.recapb3` **unchanged**, `ref-turn2.sav` → Launch → End Turn, then four more
End Turns in the same session (turn 2 → turn 7). One `rui.ps1` call per click, screenshot
between every one.
* Oracle held: `(Autosave EndTurn).sav` = `bb4fd9ac89f41e3b…`, `(Autosave).sav` =
`978041acd168b56e…` — identical to lane R's, so the events wiring does not perturb the game.
* Artefacts: `verify/traces/eventlive-b3-*.jsonl[.gz]`,
`verify/results/compare/eventlive-b3-*.{json,md}`, shim logs and screenshots in
`verify/results/shim/eventlive/`.
## 1. First End Turn — 3 calls, 3 compared, **0 divergent**, exit 0
Lane R's run exited 1 here with one divergent field
(`side.events.after.v.next_id orig=4 ours=3`). That field is now clean, and the fields lane P
named as "the model is right for the right reason" all read as forecast:
| field, call 0 | predicted | observed |
|---|---|---|
| `turn` | 3 | 3 |
| `events_turn_bucket_exists` | true | true |
| `events_next_id_in` | 3 | 3 |
| `events_in_turn_bucket` | 1 | 1 |
| `events_dedup_risk` | 0 | 0 |
| `events_scan_truncated` | absent | absent |
| `events.next_id` | 3 → 4 both sides | 3 → 4, no divergence |
| `events.turns` / `turns_bytes` | 2 / 0x30 unchanged | 2 / 48 unchanged |
| `node[144].progress` | 2879 → 5768 | 2879 → 5768 |
| `node[144].flag` | 1 → 2 | 1 → 2 |
| `rng` | identical | `left` 432→431, `next_index` 192→193, `mt` hash identical |
| `observed_techs.bytes` | unchanged, all 3 calls | 440 / 880 / 880, unchanged |
Alloc `{tech 144, 2889}`, species 2, `fpu_cw = 0x127f`. Calls 1 and 2 are the other players with
empty event lists (`EvNxID = 0`) and their scratch headers came back byte-identical.
## 2. Five End Turns — exit 1, **2 divergent calls**, each short by exactly 1
| call | alloc | draws | `next_id` orig → ours | predicted |
|---|---|---|---|---|
| 0 | {144, 2889} | 1 | 4 → 4 | 4 / 4 ✓ |
| 3 | {144, 2898} | 0 | **7 → 6** | 7 / 6 ✓ |
| 6 | {142, 3064} | 1 | 8 → 8 | 8 / 8 ✓ |
| 9 | {142, 3074} | 0 (+1 in the callback, §4) | **12 → 11** | 12 / 11 ✓ |
| 12 | {9, 3086} | 1 | 14 → 14 | 14 / 14 ✓ |
| 1,2,4,5,7,8,10,11,13,14 | zero-spend | 0 | 0 → 0 | 0 / 0 ✓ |
Lane R's run had 3 divergent calls and the two completions were short by **two** ids each; both
are now short by **one**, and that one is `EVENT_TECHS_UNLOCKED`, which lane P deliberately did
not model because evaluating its trigger needs the child-unlock cascade `ours` does not run.
A 0-divergence result here would have been *suspicious*, not good.
Note the run is not lane R's run: from turn 4 the AI picks a different research target (lane R's
trap #2), and calls 6, 9 and 12 have different techs from lane R's capture. **The event id
totals landed on the predicted values anyway**, which is a stronger result than a replay would
have been.
### Guard conversion (`P-events-wiring.md` §4.3) — held
`observed_techs` is a Result region now, so `player+0x274/0x278/0x27c` no longer appear as
undeclared guard spans; the vector shows up in the diff as `observed_techs.bytes` instead.
Undeclared spans: **13 in 2 calls (lane R) → 10 in 2 calls**.
Guard spans this run, resolved against the recovered `ServerPlayer` table:
| offset | call 3 | call 9 | field |
|---|---|---|---|
| `player+0x10c/0x110/0x114` | 3 each | | `ConMod[0..2]` |
| `player+0x124` | 3 | | `OutMod` |
| `player+0x196` | | 1 | byte 2 of design-option **mask B** at `+0x194` (`TechMaskA + 4`) — lane R saw it from inside `OnTechResearched` as `b` 64 → 2097216 = `0x40 → 0x200040` |
| `player+0x294` | 4 | 4 | `ResTNm`, the research-target pointer cleared |
| `player+0x3b4` | | 1 | `ResErrRoll` = `research_roll_pending`, cleared by the callback |
| `tree_header+0x20` | 1 | 1 | the tree's completion-order counter |
So the two spans that are "new" relative to lane R are both already-named fields, seen from the
`ProcessResearch` caller instead of from inside `OnTechResearched`. Nothing here is unnamed.
## 3. `sizeof(Game::ObservedTech) = 44` — measured on the running game
| completion call | `observed_techs.bytes` (orig side) | delta |
|---|---|---|
| 3 | 440 → 484 | **+44** |
| 9 | 484 → 528 | **+44** |
An independent **behavioural** confirmation of a purely static pin: lane X pinned 0x2c three
ways off the instruction stream (magic divide by 44, `imul reg,reg,0x2c`, `add edi,0x2c`) and
lane S mapped every field from the serializer. The running game agrees to the byte. Both
non-researching players' vectors measured 880 = 20 × 44 on all 15 calls and never moved.
Recorded on `ObservedTech_sizeof` and `ServerPlayer_off_ObservedTechs` in `ghidra/addresses.json`.
Caveat for the lane that wires the append: `RecordObservedTech` (0x007ba1a0) de-duplicates by
tech name, so a re-observation appends nothing. "No delta" is not a contradiction until you have
checked whether the tech was already observed.
## 4. One deviation from lane R, and it is a workload effect
`rng` diverged on call 9: original `left` 374 / `next_index` 250, `ours` 375 / 249 — **the
original drew one word `ours` did not.**
That is the `RollResearchEvent` draw inside `OnTechResearched`, at exactly the two generator
positions lane R measured from inside that hook (`left` 375 → 374, `next_index` 249 → 250) when
`IND_TRKSTL` completed with `research_roll_pending` set. B3 declared `SetResearched` and its
owner callback out of scope, and `P-events-wiring.md` §3 says the callback "consumes one extra
RNG word", so this is a declared boundary showing itself.
**What it corrects:** lane R's "**RNG matched 15 of 15**" was workload luck, not a property.
Lane R's five-turn session happened not to complete a roll-triggering tech on the B3 hook; mine
did. The honest statement is: `ProcessResearch`'s `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 — until whichever lane takes `SetResearched` models the callback.
## 5. Workload unblocked: **Zuul double roll, closed**
The board row asked for one live compare from a species-5 save where `left` drops by 2, not 1.
No save in `verify/` had a Zuul player, so the double roll was disassembly + host tests only.
Made on the VM: a custom game with **only Zuul left enabled in the Available Species pool**
(so both players are Zuul), same 28-star / 2-player / 7 ly settings, then four End Turns under
the same `shim.cfg.recapb3` compare config.
**8 calls, 8 compared, 0 divergent, `tracecmp` exit 0.** The four researching calls all have
`species = 5` and all advance the generator by **two**:
| call | turn | alloc | `left` | `next_index` | `mt` hash |
|---|---|---|---|---|---|
| 0 | 2 | {144, 982} | 540 → **538** | 84 → **86** | unchanged |
| 2 | 3 | {144, 960} | 522 → **520** | 102 → **104** | unchanged |
| 4 | 4 | {144, 1417} | 504 → **502** | 120 → **122** | unchanged |
| 6 | 5 | {144, 1372} | 485 → **483** | 139 → **141** | unchanged |
`ours` reproduced every one bit-for-bit (the `rng` region is compared, and no call diverged).
Calls 1, 3, 5, 7 are the second Zuul player with a zero allocation — no draw, and clean.
Save: `verify/results/saves/zuul-turn5-species5.sav`
(sha256 `48559ab5b719b332e47b4bf43af85a616bff1508125d62edb211b80637234af6`, 59,131 B, turn 5,
2 Zuul players, homeworld **Gallandro**, Rip Bore DE in Alpha Fleet, `Slaves: 0` on the planet
panel). Left on the VM as `C:\SOTS\SavedGames\zuul-turn5.sav`.
No tech completed in five turns (node 144 reached ~4,700 of 5,000), so this save does **not**
yet exercise a Zuul completion, `SetResearched`, or the Zuul boarding-pod grant. Load it and run
one more End Turn to get there.
## 6. Workloads NOT achieved
* **Expense sliders / a debtor / a research-aid treaty.** Not attempted. The eight always-zero
`ComputeBudget` slots stay unexercised. Getting there needs deliberate overspending across
several turns and a second empire willing to sign an aid treaty — a multi-turn play session,
not a click path.
* **A save that sets `research_roll_pending` at load time.** Not made. The flag was *observed*
set and cleared inside a turn (§4), which is what the RollResearchEvent row already covers,
but there is still no save that starts with it true.
* **A tech completion in the Zuul game.** One more End Turn from `zuul-turn5.sav` should do it.
## 7. VM140 click-path notes (additions to `running-the-game.md`)
* **The >60 s startup is real, a third time.** After `schtasks /Run /TN SOTS` the Kerberos intro
is still on screen at t+30 s and the main menu appeared at ≈ t+70 s. Screenshot and verify;
never sleep-and-click.
* **One click per `rui.ps1` call, confirmed again.** A five-click `cmd.txt` on the Available
Species checkboxes lost sync and left three of five toggles in the wrong state. Single clicks
with a screenshot between worked every time.
* **`type` into the game's text fields did not register** in this session — neither the Create
Game name field nor the Save Game file-name field took `SendKeys` text (the field simply kept
its default). Workaround used: save under the default name and **rename the `.sav` on disk**
afterwards; the Load dialog lists files by filename, so a renamed save appears correctly.
* **Load-dialog geometry with the 8-file set is unchanged for existing lanes.** `zuul-turn5`
sorts after `ref-turn2`, so `ref-turn2` is still row 7 at **(400, 436)** and `zuul-turn5` is
the new row 8 at **(400, 465)**. Rows are 29 px apart from y = 262. Verified by screenshot
(`verify/results/shim/eventlive/load-dialog-8files.png`).
* Custom Game Setup coordinates (1024×768 windowed): species checkboxes at y = 449, x = 178
(Human) / 322 (Hiver) / 466 (Tarkas) / 611 (Liir) / 901 (Morrigi) — all five clicked and
verified; Zuul's sits at x ≈ 756 by the same 144-px pitch but was never clicked. `Create Game`
(836, 707); game-name `OK` (600, 393); lobby `Launch` (511, 663). The setup screen **remembers
the previous game's settings**, so 28 stars / 2 players carried over from the reference game.
* Quit path used at the end: menu (1000, 714) → `Quit to Main Menu` (938, 699) → `OK` (537, 377).
## 8. VM state left behind
Restored: build `recap-7584bad-20260908T0615Z` from `C:\SOTS\shimdist-recap`, `hooks=trace`,
**main menu verified by screenshot**. `C:\SOTS\shimdist-v` and `C:\SOTS\ui\v{deploy,release,click}.ps1`
left in place as a template. `SavedGames` now holds **8** files: the pre-existing 7 plus
`zuul-turn5.sav`; the three autosaves belong to the Zuul game (they were the reference game's
before, and are transient either way).

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,46 @@
## tracecmp report: v-b3-t1-5.jsonl
- build: eventlive-dd38117-20260908T0916Z started: 2026-09-08T09:20:00Z inline_max: 256
- calls: 15 compared: 15 diverged: 2 invalid records: 0 warnings: 0
- coverage: 15 guarded call(s), 10 undeclared write(s) in 2 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | 15 | compare:15 | 15 | 2 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | partial | events, node[0], node[105], node[106], node[107], node[108], +271 | player, tree_header | 10 in 2 call(s) | 6 |
#### Game::TechTree::ProcessResearch — not checked by this run
- (medium) 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 — 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 [region:events]
- (medium) posts EVENT_TECHS_UNLOCKED once after the per-node loop, for the nodes SetResearched made available this turn — the set comes from the child-unlock cascade, which ours does not run; the pass driver takes the unlock list as an input and is given `no list` rather than an empty one, so a missing input cannot look like a modelled negative. Expect region:events to under-count next_id by exactly 1 on every call that completes a tech [region:events]
- (high) appends to the owner's vector<ObservedTech> (ServerPlayer+0x274) on every tech completion — serialized ServerPlayer state that no coverage note in B2 or B3 mentioned until lane R's guard caught it. The element is now fully pinned (sizeof 44, {u16 turn_first, u16 turn_last, bool detected, string tech_name, int with}) and the append de-duplicates by tech name, but ours still does not append; the region reports the byte span, which must grow by exactly 44 per completion [region:observed_techs]
- (high) TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback — its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word [guard:player, guard:tree_header]
- (medium) bumps the tree's completion-order counter (TechTree+0x20) — part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region [guard:tree_header]
- (low) writes a completion line to the game log — log text is not simulation state
- guard hits in compare mode: player+0x10c:3, player+0x110:3, player+0x114:3, player+0x124:3, player+0x294:4, tree_header+0x20:1, player+0x196:1, player+0x3b4:1
### Game::TechTree::ProcessResearch: first 2 of 2 divergent call(s)
- call_id 3 (v-b3-t1-5.jsonl:5)
side.events.after.v.next_id [exact] orig={"t":"i32","v":7} ours={"t":"i32","v":6}
side.node[132].after.v.cost_rp [exact] orig={"t":"i32","v":10000} ours={"t":"i32","v":2147483647}
side.node[132].after.v.state [exact] orig={"t":"i32","v":2} ours={"t":"i32","v":0}
side.node[132].after.v.turn_available [exact] orig={"t":"i32","v":4} ours={"t":"i32","v":-1}
side.node[136].after.v.cost_rp [exact] orig={"t":"i32","v":16000} ours={"t":"i32","v":2147483647}
side.node[136].after.v.state [exact] orig={"t":"i32","v":2} ours={"t":"i32","v":0}
side.node[136].after.v.turn_available [exact] orig={"t":"i32","v":4} ours={"t":"i32","v":-1}
side.node[142].after.v.cost_rp [exact] orig={"t":"i32","v":8000} ours={"t":"i32","v":2147483647}
… 5 more
- call_id 9 (v-b3-t1-5.jsonl:11)
side.events.after.v.next_id [exact] orig={"t":"i32","v":12} ours={"t":"i32","v":11}
side.node[133].after.v.cost_rp [exact] orig={"t":"i32","v":8000} ours={"t":"i32","v":2147483647}
side.node[133].after.v.state [exact] orig={"t":"i32","v":2} ours={"t":"i32","v":0}
side.node[133].after.v.turn_available [exact] orig={"t":"i32","v":6} ours={"t":"i32","v":-1}
side.node[142].after.v.order [exact] orig={"t":"i32","v":23} ours={"t":"i32","v":-1}
side.node[142].after.v.turn_researched [exact] orig={"t":"i32","v":6} ours={"t":"i32","v":-1}
side.observed_techs.after.v.bytes [exact] orig={"t":"i32","v":528} ours={"t":"i32","v":484}
side.rng.after.v.left [exact] orig={"t":"i32","v":374} ours={"t":"i32","v":375}
… 1 more

View file

@ -0,0 +1,763 @@
{
"coverage_contradicted": [],
"coverage_unstated": [],
"format": 1,
"hooks": {
"Game::TechTree::ProcessResearch": {
"calls": 3,
"compared": 3,
"coverage": {
"checked_regions": [
"events",
"node[0]",
"node[105]",
"node[106]",
"node[107]",
"node[108]",
"node[109]",
"node[10]",
"node[110]",
"node[111]",
"node[112]",
"node[113]",
"node[114]",
"node[115]",
"node[116]",
"node[117]",
"node[118]",
"node[119]",
"node[11]",
"node[120]",
"node[121]",
"node[122]",
"node[123]",
"node[124]",
"node[125]",
"node[126]",
"node[127]",
"node[128]",
"node[129]",
"node[12]",
"node[130]",
"node[131]",
"node[132]",
"node[133]",
"node[134]",
"node[135]",
"node[136]",
"node[137]",
"node[138]",
"node[139]",
"node[13]",
"node[140]",
"node[141]",
"node[142]",
"node[143]",
"node[144]",
"node[145]",
"node[146]",
"node[147]",
"node[148]",
"node[149]",
"node[14]",
"node[151]",
"node[152]",
"node[153]",
"node[154]",
"node[155]",
"node[156]",
"node[157]",
"node[158]",
"node[159]",
"node[15]",
"node[160]",
"node[161]",
"node[162]",
"node[163]",
"node[164]",
"node[165]",
"node[166]",
"node[167]",
"node[168]",
"node[169]",
"node[16]",
"node[170]",
"node[171]",
"node[172]",
"node[173]",
"node[174]",
"node[175]",
"node[177]",
"node[178]",
"node[179]",
"node[17]",
"node[180]",
"node[181]",
"node[182]",
"node[183]",
"node[184]",
"node[185]",
"node[186]",
"node[187]",
"node[188]",
"node[189]",
"node[18]",
"node[190]",
"node[191]",
"node[192]",
"node[193]",
"node[194]",
"node[195]",
"node[196]",
"node[197]",
"node[198]",
"node[199]",
"node[19]",
"node[1]",
"node[200]",
"node[201]",
"node[202]",
"node[203]",
"node[204]",
"node[205]",
"node[206]",
"node[207]",
"node[208]",
"node[209]",
"node[20]",
"node[210]",
"node[211]",
"node[212]",
"node[213]",
"node[214]",
"node[215]",
"node[216]",
"node[217]",
"node[218]",
"node[219]",
"node[21]",
"node[220]",
"node[221]",
"node[222]",
"node[223]",
"node[224]",
"node[225]",
"node[226]",
"node[227]",
"node[228]",
"node[229]",
"node[22]",
"node[230]",
"node[231]",
"node[232]",
"node[233]",
"node[234]",
"node[235]",
"node[236]",
"node[237]",
"node[238]",
"node[239]",
"node[23]",
"node[240]",
"node[241]",
"node[242]",
"node[243]",
"node[244]",
"node[245]",
"node[246]",
"node[247]",
"node[248]",
"node[249]",
"node[24]",
"node[250]",
"node[251]",
"node[252]",
"node[253]",
"node[254]",
"node[255]",
"node[256]",
"node[257]",
"node[258]",
"node[259]",
"node[25]",
"node[260]",
"node[261]",
"node[262]",
"node[263]",
"node[264]",
"node[265]",
"node[266]",
"node[267]",
"node[268]",
"node[269]",
"node[26]",
"node[270]",
"node[271]",
"node[272]",
"node[273]",
"node[274]",
"node[275]",
"node[276]",
"node[277]",
"node[278]",
"node[279]",
"node[27]",
"node[280]",
"node[281]",
"node[282]",
"node[283]",
"node[284]",
"node[285]",
"node[286]",
"node[287]",
"node[288]",
"node[289]",
"node[28]",
"node[290]",
"node[291]",
"node[292]",
"node[29]",
"node[2]",
"node[30]",
"node[31]",
"node[32]",
"node[33]",
"node[34]",
"node[35]",
"node[36]",
"node[37]",
"node[38]",
"node[39]",
"node[3]",
"node[40]",
"node[41]",
"node[42]",
"node[43]",
"node[44]",
"node[45]",
"node[46]",
"node[47]",
"node[48]",
"node[49]",
"node[4]",
"node[50]",
"node[51]",
"node[52]",
"node[53]",
"node[54]",
"node[55]",
"node[56]",
"node[57]",
"node[58]",
"node[5]",
"node[60]",
"node[61]",
"node[62]",
"node[63]",
"node[64]",
"node[65]",
"node[66]",
"node[67]",
"node[68]",
"node[69]",
"node[6]",
"node[70]",
"node[72]",
"node[74]",
"node[77]",
"node[78]",
"node[7]",
"node[80]",
"node[81]",
"node[85]",
"node[86]",
"node[87]",
"node[88]",
"node[89]",
"node[8]",
"node[90]",
"node[91]",
"node[92]",
"node[94]",
"node[98]",
"node[99]",
"node[9]",
"observed_techs",
"overbudget",
"rng"
],
"guarded_calls": 3,
"guards": [
"player",
"tree_header"
],
"spans": {
"compare": []
},
"state": "partial",
"undeclared_calls": 0,
"undeclared_writes": 0,
"unmodelled": [
{
"mitigation": "region:events",
"risk": "medium",
"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",
"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",
"risk": "medium",
"what": "posts EVENT_TECHS_UNLOCKED once after the per-node loop, for the nodes SetResearched made available this turn",
"why": "the set comes from the child-unlock cascade, which ours does not run; the pass driver takes the unlock list as an input and is given `no list` rather than an empty one, so a missing input cannot look like a modelled negative. Expect region:events to under-count next_id by exactly 1 on every call that completes a tech"
},
{
"mitigation": "region:observed_techs",
"risk": "high",
"what": "appends to the owner's vector<ObservedTech> (ServerPlayer+0x274) on every tech completion",
"why": "serialized ServerPlayer state that no coverage note in B2 or B3 mentioned until lane R's guard caught it. The element is now fully pinned (sizeof 44, {u16 turn_first, u16 turn_last, bool detected, string tech_name, int with}) and the append de-duplicates by tech name, but ours still does not append; the region reports the byte span, which must grow by exactly 44 per completion"
},
{
"mitigation": "guard:player, guard:tree_header",
"risk": "high",
"what": "TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback",
"why": "its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word"
},
{
"mitigation": "guard:tree_header",
"risk": "medium",
"what": "bumps the tree's completion-order counter (TechTree+0x20)",
"why": "part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region"
},
{
"mitigation": "",
"risk": "low",
"what": "writes a completion line to the game log",
"why": "log text is not simulation state"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [],
"diverged": 0,
"diverged_call_ids": [],
"errors": 0,
"modes": {
"compare": 3
}
}
},
"inputs": [
"/tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/out/v-b3-t1.jsonl"
],
"invalid": [],
"kind": "report",
"meta": [
{
"build": "eventlive-dd38117-20260908T0916Z",
"exe_sha256": "970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841",
"format": 1,
"hooks": {
"Game::SectionDictionary::SectionDictionary": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "see docs/M2.md; compare mode for this hook is not safe to run",
"risk": "high",
"what": "LoadSection registers each section with the string table and the live TechTree, and may append to the dictionary's own vector",
"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": "",
"risk": "medium",
"what": "post-load validation pass over every definition's @-token against the string table",
"why": "runs after the loop and touches no declared region"
},
{
"mitigation": "",
"risk": "low",
"what": "allocates 885 SectionDef objects (0x3d8 bytes each) on the game heap",
"why": "they do not exist at hook entry; compared by index/species/id/token"
},
{
"mitigation": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"why": "not modelled; emitted as an ignored pointer"
},
{
"mitigation": "",
"risk": "medium",
"what": "the before-snapshot of the object is uninitialised heap",
"why": "the hook is on the constructor, so `before` is meaningless and only `after` carries information"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::ComputeBudget": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "declared input boundary; see budget_inputs.h",
"risk": "high",
"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)",
"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": "guard:budget_object does not reach the ships; unverified",
"risk": "high",
"what": "ServerSystem::ComputeOutput repairs damaged ships in orbit",
"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": "",
"risk": "medium",
"what": "the difficulty-mods row from StrategyServer::GetDifficultyMods",
"why": "not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs"
},
{
"mitigation": "",
"risk": "low",
"what": "the research-allocation vector's heap block",
"why": "only the element count is compared; the three words are heap pointers the default policy ignores"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::OnTechResearched": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:player (EventStorage is inline at ServerPlayer+0x29c)",
"risk": "high",
"what": "posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent",
"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": "",
"risk": "high",
"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",
"why": "writes through pointers to other objects; compare mode must not touch live state, and no region reaches them"
},
{
"mitigation": "this is the extra draw B3 observed on a completion",
"risk": "high",
"what": "the pending plague-cure roll (ServerPlayer::RollResearchEvent)",
"why": "it draws exactly one word from the strategic generator unconditionally; 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": "",
"risk": "medium",
"what": "TechTree::SetResearched for the Zuul boarding-pod grant",
"why": "it would mutate the live tree, and it recurses"
},
{
"mitigation": "region:node_bore, declared only when the block already exists",
"risk": "medium",
"what": "allocates or frees the node-bore block at ServerPlayer+0x308",
"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"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerSystem::ProcessTurn": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:system",
"risk": "high",
"what": "the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history",
"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 covers the system object only, not the other objects",
"risk": "high",
"what": "every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue",
"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": "",
"risk": "medium",
"what": "ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer",
"why": "writes through a pointer to another object; no region reaches the player"
},
{
"mitigation": "",
"risk": "low",
"what": "ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent",
"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": "",
"risk": "medium",
"what": "replace mode is refused for this hook",
"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"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::MoveFleet": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:fleet sees the fleet's own words; the event and the system do not",
"risk": "high",
"what": "on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)",
"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": "",
"risk": "high",
"what": "on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits",
"why": "writes through pointers to ships and to the system"
},
{
"mitigation": "",
"risk": "medium",
"what": "the tanker top-up refuels other ships in the fleet",
"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": "declared gap: docs/B4.md",
"risk": "medium",
"what": "a node-line waypoint's step comes from the stutter profile",
"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": "",
"risk": "medium",
"what": "a missed probabilistic jump scatters the fleet in a random direction",
"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": "",
"risk": "medium",
"what": "the route revalidation and the waypoint list itself",
"why": "declared input boundary; the waypoint vector is not a region"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::ProcessFleetMovement": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "",
"risk": "high",
"what": "`ours` re-reads the LIVE fleet list after the original has run",
"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": "",
"risk": "high",
"what": "drives MoveFleet up to five times per fleet",
"why": "every undeclared effect of MoveFleet happens inside this call too; the pass schedule is recorded in the arguments but never compared"
},
{
"mitigation": "",
"risk": "high",
"what": "writes FPdpos into every fleet and clears flags 0x2 and 0x100 on every fleet",
"why": "no region covers the fleets, only the players' gate-traffic words"
},
{
"mitigation": "",
"risk": "high",
"what": "OnFleetArrived posts EVENT_FLEET_ARRIVED",
"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": "",
"risk": "medium",
"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",
"why": "a real latent bug in the original that our side reproduces only while index == position; the reference save never separates them"
},
{
"mitigation": "",
"risk": "medium",
"what": "PassSchedule() is never called by the hook, and FleetSummary::targetFleetId / relation are never filled",
"why": "the header claims ours predicts the call order for a trace to check; that prediction is not actually emitted"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::TechTree::ProcessResearch": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "region:events",
"risk": "medium",
"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",
"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",
"risk": "medium",
"what": "posts EVENT_TECHS_UNLOCKED once after the per-node loop, for the nodes SetResearched made available this turn",
"why": "the set comes from the child-unlock cascade, which ours does not run; the pass driver takes the unlock list as an input and is given `no list` rather than an empty one, so a missing input cannot look like a modelled negative. Expect region:events to under-count next_id by exactly 1 on every call that completes a tech"
},
{
"mitigation": "region:observed_techs",
"risk": "high",
"what": "appends to the owner's vector<ObservedTech> (ServerPlayer+0x274) on every tech completion",
"why": "serialized ServerPlayer state that no coverage note in B2 or B3 mentioned until lane R's guard caught it. The element is now fully pinned (sizeof 44, {u16 turn_first, u16 turn_last, bool detected, string tech_name, int with}) and the append de-duplicates by tech name, but ours still does not append; the region reports the byte span, which must grow by exactly 44 per completion"
},
{
"mitigation": "guard:player, guard:tree_header",
"risk": "high",
"what": "TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback",
"why": "its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word"
},
{
"mitigation": "guard:tree_header",
"risk": "medium",
"what": "bumps the tree's completion-order counter (TechTree+0x20)",
"why": "part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region"
},
{
"mitigation": "",
"risk": "low",
"what": "writes a completion line to the game log",
"why": "log text is not simulation state"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::WeaponDictionary::Init": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "suspected cause of the sibling section hook's compare crash (docs/M2.md)",
"risk": "high",
"what": "LoadWeapon -> WeaponDef::ParseScript registers each weapon's name with the string table and resolves `requires` against the live TechTree",
"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": "",
"risk": "low",
"what": "allocates 123 WeaponDef objects (0x278 bytes each) on the game heap",
"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": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"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": "",
"risk": "low",
"what": "writes lines to the game log for a missing manifest",
"why": "log text is not simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "std::sort tie order for equal weapon names",
"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"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Mars::GlobalConsts::LoadFile": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "LoadAll's post-state would have to be hooked to see it",
"risk": "medium",
"what": "erases each consumed key from the caller's std::map",
"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": "",
"risk": "low",
"what": "writes three kinds of line to the game log (unrecognised key, applied key, expected-but-not-found)",
"why": "log text is not part of the simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "opens the file through the VFS and allocates/releases a refcounted buffer",
"why": "ours performs the same two calls, so allocation behaviour matches by construction rather than by comparison"
},
{
"mitigation": "",
"risk": "low",
"what": "String slots assign through the engine's own std::string, leaking one heap block per long string in compare mode",
"why": "start-up only; documented in docs/M1.md"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Shim::SelfTest::Fill": {
"coverage": {
"state": "complete",
"unmodelled": [],
"why": "Fill writes buf[0..n) and nothing else; the whole range is a declared region"
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
}
},
"inline_max": 256,
"started": "2026-09-08T09:20:00Z"
}
],
"totals": {
"calls": 3,
"compared": 3,
"coverage_contradicted": 0,
"coverage_unstated": 0,
"diverged": 0,
"guarded_calls": 3,
"invalid_records": 0,
"undeclared_calls": 0,
"undeclared_writes": 0
},
"warnings": []
}

View file

@ -0,0 +1,23 @@
## tracecmp report: v-b3-t1.jsonl
- build: eventlive-dd38117-20260908T0916Z started: 2026-09-08T09:20:00Z inline_max: 256
- calls: 3 compared: 3 diverged: 0 invalid records: 0 warnings: 0
- coverage: 3 guarded call(s), 0 undeclared write(s) in 0 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | 3 | compare:3 | 3 | 0 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | partial | events, node[0], node[105], node[106], node[107], node[108], +271 | player, tree_header | 0 | 6 |
#### Game::TechTree::ProcessResearch — not checked by this run
- (medium) 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 — 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 [region:events]
- (medium) posts EVENT_TECHS_UNLOCKED once after the per-node loop, for the nodes SetResearched made available this turn — the set comes from the child-unlock cascade, which ours does not run; the pass driver takes the unlock list as an input and is given `no list` rather than an empty one, so a missing input cannot look like a modelled negative. Expect region:events to under-count next_id by exactly 1 on every call that completes a tech [region:events]
- (high) appends to the owner's vector<ObservedTech> (ServerPlayer+0x274) on every tech completion — serialized ServerPlayer state that no coverage note in B2 or B3 mentioned until lane R's guard caught it. The element is now fully pinned (sizeof 44, {u16 turn_first, u16 turn_last, bool detected, string tech_name, int with}) and the append de-duplicates by tech name, but ours still does not append; the region reports the byte span, which must grow by exactly 44 per completion [region:observed_techs]
- (high) TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback — its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word [guard:player, guard:tree_header]
- (medium) bumps the tree's completion-order counter (TechTree+0x20) — part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region [guard:tree_header]
- (low) writes a completion line to the game log — log text is not simulation state

View file

@ -0,0 +1,723 @@
{
"coverage_contradicted": [],
"coverage_unstated": [],
"format": 1,
"hooks": {
"Game::TechTree::ProcessResearch": {
"calls": 8,
"compared": 8,
"coverage": {
"checked_regions": [
"events",
"node[0]",
"node[106]",
"node[107]",
"node[108]",
"node[109]",
"node[10]",
"node[110]",
"node[111]",
"node[112]",
"node[113]",
"node[114]",
"node[115]",
"node[116]",
"node[117]",
"node[118]",
"node[119]",
"node[120]",
"node[121]",
"node[122]",
"node[123]",
"node[124]",
"node[125]",
"node[126]",
"node[129]",
"node[130]",
"node[131]",
"node[132]",
"node[133]",
"node[134]",
"node[135]",
"node[136]",
"node[137]",
"node[138]",
"node[139]",
"node[140]",
"node[141]",
"node[143]",
"node[144]",
"node[145]",
"node[146]",
"node[147]",
"node[148]",
"node[149]",
"node[14]",
"node[151]",
"node[152]",
"node[153]",
"node[154]",
"node[155]",
"node[156]",
"node[157]",
"node[158]",
"node[159]",
"node[160]",
"node[161]",
"node[162]",
"node[163]",
"node[164]",
"node[165]",
"node[166]",
"node[167]",
"node[168]",
"node[169]",
"node[170]",
"node[171]",
"node[172]",
"node[173]",
"node[174]",
"node[175]",
"node[176]",
"node[177]",
"node[178]",
"node[179]",
"node[180]",
"node[181]",
"node[182]",
"node[183]",
"node[184]",
"node[185]",
"node[186]",
"node[187]",
"node[188]",
"node[189]",
"node[18]",
"node[190]",
"node[191]",
"node[192]",
"node[193]",
"node[194]",
"node[195]",
"node[196]",
"node[197]",
"node[198]",
"node[199]",
"node[19]",
"node[200]",
"node[201]",
"node[202]",
"node[203]",
"node[204]",
"node[205]",
"node[206]",
"node[207]",
"node[208]",
"node[209]",
"node[210]",
"node[211]",
"node[212]",
"node[213]",
"node[214]",
"node[215]",
"node[216]",
"node[218]",
"node[219]",
"node[21]",
"node[220]",
"node[221]",
"node[222]",
"node[223]",
"node[224]",
"node[225]",
"node[226]",
"node[227]",
"node[228]",
"node[229]",
"node[22]",
"node[230]",
"node[231]",
"node[232]",
"node[233]",
"node[234]",
"node[235]",
"node[236]",
"node[237]",
"node[238]",
"node[239]",
"node[23]",
"node[240]",
"node[241]",
"node[242]",
"node[243]",
"node[244]",
"node[245]",
"node[246]",
"node[247]",
"node[248]",
"node[24]",
"node[259]",
"node[25]",
"node[260]",
"node[261]",
"node[262]",
"node[263]",
"node[264]",
"node[26]",
"node[270]",
"node[271]",
"node[272]",
"node[273]",
"node[274]",
"node[275]",
"node[276]",
"node[27]",
"node[282]",
"node[283]",
"node[284]",
"node[285]",
"node[286]",
"node[287]",
"node[288]",
"node[289]",
"node[28]",
"node[290]",
"node[291]",
"node[292]",
"node[29]",
"node[30]",
"node[31]",
"node[32]",
"node[33]",
"node[34]",
"node[35]",
"node[36]",
"node[37]",
"node[38]",
"node[39]",
"node[3]",
"node[41]",
"node[42]",
"node[43]",
"node[44]",
"node[45]",
"node[46]",
"node[47]",
"node[48]",
"node[49]",
"node[50]",
"node[51]",
"node[52]",
"node[53]",
"node[54]",
"node[55]",
"node[56]",
"node[57]",
"node[58]",
"node[61]",
"node[62]",
"node[63]",
"node[64]",
"node[65]",
"node[66]",
"node[67]",
"node[68]",
"node[69]",
"node[6]",
"node[70]",
"node[72]",
"node[74]",
"node[80]",
"node[81]",
"node[85]",
"node[89]",
"node[8]",
"node[90]",
"node[91]",
"node[92]",
"node[93]",
"node[94]",
"node[95]",
"node[96]",
"node[98]",
"node[99]",
"node[9]",
"observed_techs",
"overbudget",
"rng"
],
"guarded_calls": 8,
"guards": [
"player",
"tree_header"
],
"spans": {
"compare": []
},
"state": "partial",
"undeclared_calls": 0,
"undeclared_writes": 0,
"unmodelled": [
{
"mitigation": "region:events",
"risk": "medium",
"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",
"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",
"risk": "medium",
"what": "posts EVENT_TECHS_UNLOCKED once after the per-node loop, for the nodes SetResearched made available this turn",
"why": "the set comes from the child-unlock cascade, which ours does not run; the pass driver takes the unlock list as an input and is given `no list` rather than an empty one, so a missing input cannot look like a modelled negative. Expect region:events to under-count next_id by exactly 1 on every call that completes a tech"
},
{
"mitigation": "region:observed_techs",
"risk": "high",
"what": "appends to the owner's vector<ObservedTech> (ServerPlayer+0x274) on every tech completion",
"why": "serialized ServerPlayer state that no coverage note in B2 or B3 mentioned until lane R's guard caught it. The element is now fully pinned (sizeof 44, {u16 turn_first, u16 turn_last, bool detected, string tech_name, int with}) and the append de-duplicates by tech name, but ours still does not append; the region reports the byte span, which must grow by exactly 44 per completion"
},
{
"mitigation": "guard:player, guard:tree_header",
"risk": "high",
"what": "TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback",
"why": "its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word"
},
{
"mitigation": "guard:tree_header",
"risk": "medium",
"what": "bumps the tree's completion-order counter (TechTree+0x20)",
"why": "part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region"
},
{
"mitigation": "",
"risk": "low",
"what": "writes a completion line to the game log",
"why": "log text is not simulation state"
}
],
"verdict": "partial",
"why": ""
},
"diffs": [],
"diverged": 0,
"diverged_call_ids": [],
"errors": 0,
"modes": {
"compare": 8
}
}
},
"inputs": [
"/tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/out/zuul.jsonl"
],
"invalid": [],
"kind": "report",
"meta": [
{
"build": "eventlive-dd38117-20260908T0916Z",
"exe_sha256": "970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841",
"format": 1,
"hooks": {
"Game::SectionDictionary::SectionDictionary": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "see docs/M2.md; compare mode for this hook is not safe to run",
"risk": "high",
"what": "LoadSection registers each section with the string table and the live TechTree, and may append to the dictionary's own vector",
"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": "",
"risk": "medium",
"what": "post-load validation pass over every definition's @-token against the string table",
"why": "runs after the loop and touches no declared region"
},
{
"mitigation": "",
"risk": "low",
"what": "allocates 885 SectionDef objects (0x3d8 bytes each) on the game heap",
"why": "they do not exist at hook entry; compared by index/species/id/token"
},
{
"mitigation": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"why": "not modelled; emitted as an ignored pointer"
},
{
"mitigation": "",
"risk": "medium",
"what": "the before-snapshot of the object is uninitialised heap",
"why": "the hook is on the constructor, so `before` is meaningless and only `after` carries information"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::ComputeBudget": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "declared input boundary; see budget_inputs.h",
"risk": "high",
"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)",
"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": "guard:budget_object does not reach the ships; unverified",
"risk": "high",
"what": "ServerSystem::ComputeOutput repairs damaged ships in orbit",
"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": "",
"risk": "medium",
"what": "the difficulty-mods row from StrategyServer::GetDifficultyMods",
"why": "not reachable from a ServerPlayer, so the two relevant entries are fitted constants measured from the B1 trace rather than snapshotted inputs"
},
{
"mitigation": "",
"risk": "low",
"what": "the research-allocation vector's heap block",
"why": "only the element count is compared; the three words are heap pointers the default policy ignores"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerPlayer::OnTechResearched": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:player (EventStorage is inline at ServerPlayer+0x29c)",
"risk": "high",
"what": "posts EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE on the owner's EventStorage when !silent",
"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": "",
"risk": "high",
"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",
"why": "writes through pointers to other objects; compare mode must not touch live state, and no region reaches them"
},
{
"mitigation": "this is the extra draw B3 observed on a completion",
"risk": "high",
"what": "the pending plague-cure roll (ServerPlayer::RollResearchEvent)",
"why": "it draws exactly one word from the strategic generator unconditionally; 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": "",
"risk": "medium",
"what": "TechTree::SetResearched for the Zuul boarding-pod grant",
"why": "it would mutate the live tree, and it recurses"
},
{
"mitigation": "region:node_bore, declared only when the block already exists",
"risk": "medium",
"what": "allocates or frees the node-bore block at ServerPlayer+0x308",
"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"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::ServerSystem::ProcessTurn": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:system",
"risk": "high",
"what": "the addiction sweep raises MoraleEvents, which are constructed and appended to the system's capped morale history",
"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 covers the system object only, not the other objects",
"risk": "high",
"what": "every callee: the plague pass, imperial and civilian growth, the resource debit, in-orbit refuel, slaves, rebellion and the build queue",
"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": "",
"risk": "medium",
"what": "ApplyInfraBonus / ApplyPopBonus read the owner's home-system id, and the build queue writes the owning ServerPlayer",
"why": "writes through a pointer to another object; no region reaches the player"
},
{
"mitigation": "",
"risk": "low",
"what": "ProcessRebellion is the pass's only RNG consumer and its draw count is data-dependent",
"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": "",
"risk": "medium",
"what": "replace mode is refused for this hook",
"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"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::MoveFleet": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "guard:fleet sees the fleet's own words; the event and the system do not",
"risk": "high",
"what": "on arrival: dispatches SEFleetArrived and runs one of three arrival handlers by destination kind (enter system / join fleet / stop at point)",
"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": "",
"risk": "high",
"what": "on departure: cancels every still-acting ship (with a log line each) and calls ServerSystem::FleetDeparts, which rewrites the system's ownership bits",
"why": "writes through pointers to ships and to the system"
},
{
"mitigation": "",
"risk": "medium",
"what": "the tanker top-up refuels other ships in the fleet",
"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": "declared gap: docs/B4.md",
"risk": "medium",
"what": "a node-line waypoint's step comes from the stutter profile",
"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": "",
"risk": "medium",
"what": "a missed probabilistic jump scatters the fleet in a random direction",
"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": "",
"risk": "medium",
"what": "the route revalidation and the waypoint list itself",
"why": "declared input boundary; the waypoint vector is not a region"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::StrategyServer::ProcessFleetMovement": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "",
"risk": "high",
"what": "`ours` re-reads the LIVE fleet list after the original has run",
"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": "",
"risk": "high",
"what": "drives MoveFleet up to five times per fleet",
"why": "every undeclared effect of MoveFleet happens inside this call too; the pass schedule is recorded in the arguments but never compared"
},
{
"mitigation": "",
"risk": "high",
"what": "writes FPdpos into every fleet and clears flags 0x2 and 0x100 on every fleet",
"why": "no region covers the fleets, only the players' gate-traffic words"
},
{
"mitigation": "",
"risk": "high",
"what": "OnFleetArrived posts EVENT_FLEET_ARRIVED",
"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": "",
"risk": "medium",
"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",
"why": "a real latent bug in the original that our side reproduces only while index == position; the reference save never separates them"
},
{
"mitigation": "",
"risk": "medium",
"what": "PassSchedule() is never called by the hook, and FleetSummary::targetFleetId / relation are never filled",
"why": "the header claims ours predicts the call order for a trace to check; that prediction is not actually emitted"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::TechTree::ProcessResearch": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "region:events",
"risk": "medium",
"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",
"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",
"risk": "medium",
"what": "posts EVENT_TECHS_UNLOCKED once after the per-node loop, for the nodes SetResearched made available this turn",
"why": "the set comes from the child-unlock cascade, which ours does not run; the pass driver takes the unlock list as an input and is given `no list` rather than an empty one, so a missing input cannot look like a modelled negative. Expect region:events to under-count next_id by exactly 1 on every call that completes a tech"
},
{
"mitigation": "region:observed_techs",
"risk": "high",
"what": "appends to the owner's vector<ObservedTech> (ServerPlayer+0x274) on every tech completion",
"why": "serialized ServerPlayer state that no coverage note in B2 or B3 mentioned until lane R's guard caught it. The element is now fully pinned (sizeof 44, {u16 turn_first, u16 turn_last, bool detected, string tech_name, int with}) and the append de-duplicates by tech name, but ours still does not append; the region reports the byte span, which must grow by exactly 44 per completion"
},
{
"mitigation": "guard:player, guard:tree_header",
"risk": "high",
"what": "TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback",
"why": "its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word"
},
{
"mitigation": "guard:tree_header",
"risk": "medium",
"what": "bumps the tree's completion-order counter (TechTree+0x20)",
"why": "part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region"
},
{
"mitigation": "",
"risk": "low",
"what": "writes a completion line to the game log",
"why": "log text is not simulation state"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Game::WeaponDictionary::Init": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "suspected cause of the sibling section hook's compare crash (docs/M2.md)",
"risk": "high",
"what": "LoadWeapon -> WeaponDef::ParseScript registers each weapon's name with the string table and resolves `requires` against the live TechTree",
"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": "",
"risk": "low",
"what": "allocates 123 WeaponDef objects (0x278 bytes each) on the game heap",
"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": "guard:dict",
"risk": "low",
"what": "the word at dictionary+0x14",
"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": "",
"risk": "low",
"what": "writes lines to the game log for a missing manifest",
"why": "log text is not simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "std::sort tie order for equal weapon names",
"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"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Mars::GlobalConsts::LoadFile": {
"coverage": {
"state": "partial",
"unmodelled": [
{
"mitigation": "LoadAll's post-state would have to be hooked to see it",
"risk": "medium",
"what": "erases each consumed key from the caller's std::map",
"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": "",
"risk": "low",
"what": "writes three kinds of line to the game log (unrecognised key, applied key, expected-but-not-found)",
"why": "log text is not part of the simulation state"
},
{
"mitigation": "",
"risk": "low",
"what": "opens the file through the VFS and allocates/releases a refcounted buffer",
"why": "ours performs the same two calls, so allocation behaviour matches by construction rather than by comparison"
},
{
"mitigation": "",
"risk": "low",
"what": "String slots assign through the engine's own std::string, leaking one heap block per long string in compare mode",
"why": "start-up only; documented in docs/M1.md"
}
],
"why": ""
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
},
"Shim::SelfTest::Fill": {
"coverage": {
"state": "complete",
"unmodelled": [],
"why": "Fill writes buf[0..n) and nothing else; the whole range is a declared region"
},
"ftol": 0,
"ftol_kind": "abs",
"ptr": "ignore"
}
},
"inline_max": 256,
"started": "2026-09-08T09:33:49Z"
}
],
"totals": {
"calls": 8,
"compared": 8,
"coverage_contradicted": 0,
"coverage_unstated": 0,
"diverged": 0,
"guarded_calls": 8,
"invalid_records": 0,
"undeclared_calls": 0,
"undeclared_writes": 0
},
"warnings": []
}

View file

@ -0,0 +1,23 @@
## tracecmp report: zuul.jsonl
- build: eventlive-dd38117-20260908T0916Z started: 2026-09-08T09:33:49Z inline_max: 256
- calls: 8 compared: 8 diverged: 0 invalid records: 0 warnings: 0
- coverage: 8 guarded call(s), 0 undeclared write(s) in 0 call(s); 0 hook(s) unstated, 0 contradicted
| hook | calls | modes | compared | diverged | errors |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | 8 | compare:8 | 8 | 0 | 0 |
### coverage
| hook | verdict | compared regions | guards | undeclared writes | unmodelled |
|---|---|---|---|---|---|
| Game::TechTree::ProcessResearch | partial | events, node[0], node[106], node[107], node[108], node[109], +231 | player, tree_header | 0 | 6 |
#### Game::TechTree::ProcessResearch — not checked by this run
- (medium) 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 — 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 [region:events]
- (medium) posts EVENT_TECHS_UNLOCKED once after the per-node loop, for the nodes SetResearched made available this turn — the set comes from the child-unlock cascade, which ours does not run; the pass driver takes the unlock list as an input and is given `no list` rather than an empty one, so a missing input cannot look like a modelled negative. Expect region:events to under-count next_id by exactly 1 on every call that completes a tech [region:events]
- (high) appends to the owner's vector<ObservedTech> (ServerPlayer+0x274) on every tech completion — serialized ServerPlayer state that no coverage note in B2 or B3 mentioned until lane R's guard caught it. The element is now fully pinned (sizeof 44, {u16 turn_first, u16 turn_last, bool detected, string tech_name, int with}) and the append de-duplicates by tech name, but ours still does not append; the region reports the byte span, which must grow by exactly 44 per completion [region:observed_techs]
- (high) TechTree::SetResearched on completion: the turn/order stamps, the child unlock cascade, the recursive research of zero-cost children, and the owner's OnTechResearched callback — its own milestone (B2); the callback writes live player state that compare mode must not touch, and it consumes one extra RNG word [guard:player, guard:tree_header]
- (medium) bumps the tree's completion-order counter (TechTree+0x20) — part of SetResearched; the per-node `order` word is compared but the counter it comes from was not a region [guard:tree_header]
- (low) writes a completion line to the game log — log text is not simulation state

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

View file

@ -0,0 +1,69 @@
05:33:49.374 [tid 4212] ==== sots-engine shim (binkw32 proxy) build eventlive-dd38117-20260908T0916Z ====
05:33:49.374 [tid 4212] exe: C:\SOTS\Sword of the Stars.exe
05:33:49.374 [tid 4212] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=4964 shim=695c0000
05:33:49.374 [tid 4212] addresses: Source: sots-re ghidra/addresses.json @ 3768f24, generated 2026-09-08 by tools/gen_addresses.py
05:33:49.374 [tid 4212] config: hooks=trace
05:33:49.374 [tid 4212] config: hook.Shim::SelfTest::Fill=off
05:33:49.374 [tid 4212] config: hook.Mars::GlobalConsts::LoadFile=off
05:33:49.374 [tid 4212] config: hook.Game::WeaponDictionary::Init=off
05:33:49.374 [tid 4212] config: hook.Game::SectionDictionary::SectionDictionary=off
05:33:49.374 [tid 4212] config: hook.Game::ServerPlayer::ComputeBudget=off
05:33:49.374 [tid 4212] config: hook.Game::ServerPlayer::OnTechResearched=off
05:33:49.374 [tid 4212] config: hook.Game::ServerSystem::ProcessTurn=off
05:33:49.374 [tid 4212] config: hook.Game::StrategyServer::MoveFleet=off
05:33:49.374 [tid 4212] config: hook.Game::StrategyServer::ProcessFleetMovement=off
05:33:49.374 [tid 4212] config: hook.Game::TechTree::ProcessResearch=compare
05:33:49.374 [tid 4212] config: trace.path=C:\SOTS\shim.trace.jsonl
05:33:49.374 [tid 4212] config: trace.inline_max=256
05:33:49.374 [tid 4212] config: trace.flush=always
05:33:49.437 [tid 4212] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always)
05:33:49.437 [tid 4212] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50
05:33:49.437 [tid 4212] hook: MH_Initialize -> MH_OK
05:33:49.437 [tid 4212] hook: MH_CreateHook -> MH_OK (trampoline=00580fe0)
05:33:49.452 [tid 4212] hook: MH_EnableHook -> MH_OK
05:33:49.452 [tid 4212] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295)
05:33:49.452 [tid 4212] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed)
05:33:49.452 [tid 4212] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174)
05:33:49.452 [tid 4212] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed)
05:33:49.452 [tid 4212] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed)
05:33:49.452 [tid 4212] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f)
05:33:49.452 [tid 4212] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=00580fc0)
05:33:49.468 [tid 4212] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=compare
05:33:49.468 [tid 4212] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f)
05:33:49.468 [tid 4212] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 mode=off (not installed)
05:33:49.468 [tid 4212] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 mode=off (not installed)
05:33:49.468 [tid 4212] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 mode=off (not installed)
05:33:49.468 [tid 4212] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 mode=off (not installed)
05:33:49.468 [tid 4212] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed)
05:33:49.468 [tid 4212] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=off value=0x0000 sample_ticks=on
05:33:49.468 [tid 4212] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=01203be0 MH_CreateHook -> MH_OK (trampoline=00580fa0)
05:33:49.484 [tid 4212] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK
05:33:49.484 [tid 4212] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=012598e0 MH_CreateHook -> MH_OK (trampoline=00580f80)
05:33:49.484 [tid 4212] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK
05:33:49.484 [tid 4212] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0125c6c0 MH_CreateHook -> MH_OK (trampoline=00580f60)
05:33:49.499 [tid 4212] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK
05:33:49.499 [tid 4212] fpu: DemoApp::OnTick rva=0x0049a640 -> va=0131a640 MH_CreateHook -> MH_OK (trampoline=00580f40)
05:33:49.515 [tid 4212] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK
05:33:49.515 [tid 4212] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0
05:33:49.530 [tid 4212] Application::Initialize called (this=02fa8128)
05:33:50.296 [tid 4212] fpu: TICK BASELINE at OnTick (this=02fa8128): cw=0x127f 53bit-double/nearest
05:42:19.570 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=0de9f568): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:42:22.695 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=3468ad80): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:42:22.726 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=34687a88): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:42:23.679 [tid 4212] fpu: sample at StrategyServer::BeginProcessTurn (this=0de383c0): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:42:23.679 [tid 4212] fpu: sample at StrategyServer::ProcessTurn (this=0de383c0): cw=0x127f 53bit-double/nearest
05:43:27.882 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=0de9f568): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:43:30.961 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=3468ad80): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:43:30.992 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=34687a88): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:43:31.961 [tid 4212] fpu: sample at StrategyServer::BeginProcessTurn (this=0de383c0): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:43:31.961 [tid 4212] fpu: sample at StrategyServer::ProcessTurn (this=0de383c0): cw=0x127f 53bit-double/nearest
05:43:58.836 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=0de9f568): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:44:01.929 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=3468ad80): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:44:01.961 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=34687a88): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:44:02.929 [tid 4212] fpu: sample at StrategyServer::BeginProcessTurn (this=0de383c0): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:44:02.929 [tid 4212] fpu: sample at StrategyServer::ProcessTurn (this=0de383c0): cw=0x127f 53bit-double/nearest
05:44:30.226 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=0de9f568): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:44:33.304 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=3468ad80): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:44:33.336 [tid 4212] fpu: sample at StrategyClient::EndTurn (this=34687a88): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:44:34.320 [tid 4212] fpu: sample at StrategyServer::BeginProcessTurn (this=0de383c0): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:44:34.320 [tid 4212] fpu: sample at StrategyServer::ProcessTurn (this=0de383c0): cw=0x127f 53bit-double/nearest

View file

@ -0,0 +1,79 @@
05:20:00.663 [tid 500] ==== sots-engine shim (binkw32 proxy) build eventlive-dd38117-20260908T0916Z ====
05:20:00.663 [tid 500] exe: C:\SOTS\Sword of the Stars.exe
05:20:00.663 [tid 500] exe base=0x00e80000 (link-time image base 0x00400000, ASLR delta +11010048) pid=936 shim=695c0000
05:20:00.663 [tid 500] addresses: Source: sots-re ghidra/addresses.json @ 3768f24, generated 2026-09-08 by tools/gen_addresses.py
05:20:00.663 [tid 500] config: hooks=trace
05:20:00.663 [tid 500] config: hook.Shim::SelfTest::Fill=off
05:20:00.663 [tid 500] config: hook.Mars::GlobalConsts::LoadFile=off
05:20:00.663 [tid 500] config: hook.Game::WeaponDictionary::Init=off
05:20:00.663 [tid 500] config: hook.Game::SectionDictionary::SectionDictionary=off
05:20:00.663 [tid 500] config: hook.Game::ServerPlayer::ComputeBudget=off
05:20:00.663 [tid 500] config: hook.Game::ServerPlayer::OnTechResearched=off
05:20:00.663 [tid 500] config: hook.Game::ServerSystem::ProcessTurn=off
05:20:00.663 [tid 500] config: hook.Game::StrategyServer::MoveFleet=off
05:20:00.663 [tid 500] config: hook.Game::StrategyServer::ProcessFleetMovement=off
05:20:00.663 [tid 500] config: hook.Game::TechTree::ProcessResearch=compare
05:20:00.663 [tid 500] config: trace.path=C:\SOTS\shim.trace.jsonl
05:20:00.663 [tid 500] config: trace.inline_max=256
05:20:00.663 [tid 500] config: trace.flush=always
05:20:00.726 [tid 500] trace: C:\SOTS\shim.trace.jsonl (default mode trace, inline_max 256, flush always)
05:20:00.726 [tid 500] hook: Mars_Application_Initialize rva=0x004a0e50 -> va=01320e50
05:20:00.726 [tid 500] hook: MH_Initialize -> MH_OK
05:20:00.726 [tid 500] hook: MH_CreateHook -> MH_OK (trampoline=01bb0fe0)
05:20:00.741 [tid 500] hook: MH_EnableHook -> MH_OK
05:20:00.741 [tid 500] cfg: GlobalConsts hook ready (scale constant 0.017453292519943295)
05:20:00.741 [tid 500] hook: Mars::GlobalConsts::LoadFile rva=0x004b73c0 mode=off (not installed)
05:20:00.741 [tid 500] dict: dictionaries hook ready (crt new=6adc232b delete=6adc0174)
05:20:00.741 [tid 500] hook: Game::WeaponDictionary::Init rva=0x0019a4c0 mode=off (not installed)
05:20:00.741 [tid 500] hook: Game::SectionDictionary::SectionDictionary rva=0x00176f40 mode=off (not installed)
05:20:00.741 [tid 500] research: ProcessResearch hook ready (Cost=00ffda00, node=0x34, rng=0x9cc, fpu_cw=0x027f)
05:20:00.741 [tid 500] hook: Game::TechTree::ProcessResearch rva=0x001876c0 -> va=010076c0 MH_CreateHook -> MH_OK (trampoline=01bb0fc0)
05:20:00.757 [tid 500] hook: Game::TechTree::ProcessResearch MH_EnableHook -> MH_OK mode=compare
05:20:00.757 [tid 500] techfx: OnTechResearched hook ready (regions=15, gate=0/0, fpu_cw=0x027f)
05:20:00.757 [tid 500] hook: Game::ServerPlayer::OnTechResearched rva=0x00491790 mode=off (not installed)
05:20:00.757 [tid 500] hook: Game::ServerPlayer::ComputeBudget rva=0x00463030 mode=off (not installed)
05:20:00.757 [tid 500] hook: Game::ServerSystem::ProcessTurn rva=0x003598e0 mode=off (not installed)
05:20:00.757 [tid 500] hook: Game::StrategyServer::MoveFleet rva=0x003d9ee0 mode=off (not installed)
05:20:00.757 [tid 500] hook: Game::StrategyServer::ProcessFleetMovement rva=0x003da9a0 mode=off (not installed)
05:20:00.757 [tid 500] fpu: module init, entry cw=0x027f 53bit-double/nearest; force=off value=0x0000 sample_ticks=on
05:20:00.757 [tid 500] fpu: StrategyClient::EndTurn rva=0x00383be0 -> va=01203be0 MH_CreateHook -> MH_OK (trampoline=01bb0fa0)
05:20:00.773 [tid 500] fpu: StrategyClient::EndTurn MH_EnableHook -> MH_OK
05:20:00.773 [tid 500] fpu: StrategyServer::BeginProcessTurn rva=0x003d98e0 -> va=012598e0 MH_CreateHook -> MH_OK (trampoline=01bb0f80)
05:20:00.773 [tid 500] fpu: StrategyServer::BeginProcessTurn MH_EnableHook -> MH_OK
05:20:00.773 [tid 500] fpu: StrategyServer::ProcessTurn rva=0x003dc6c0 -> va=0125c6c0 MH_CreateHook -> MH_OK (trampoline=01bb0f60)
05:20:00.788 [tid 500] fpu: StrategyServer::ProcessTurn MH_EnableHook -> MH_OK
05:20:00.788 [tid 500] fpu: DemoApp::OnTick rva=0x0049a640 -> va=0131a640 MH_CreateHook -> MH_OK (trampoline=01bb0f40)
05:20:00.804 [tid 500] fpu: DemoApp::OnTick MH_EnableHook -> MH_OK
05:20:00.804 [tid 500] selftest: Shim::SelfTest::Fill mode=off checksum=075ef0c3 records=0
05:20:00.820 [tid 500] Application::Initialize called (this=03b18128)
05:20:01.554 [tid 500] fpu: TICK BASELINE at OnTick (this=03b18128): cw=0x127f 53bit-double/nearest
05:24:40.874 [tid 500] fpu: sample at StrategyClient::EndTurn (this=0e963f00): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:24:43.937 [tid 500] fpu: sample at StrategyClient::EndTurn (this=35279a78): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:24:43.952 [tid 500] fpu: sample at StrategyClient::EndTurn (this=3527b050): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:24:43.983 [tid 500] fpu: sample at StrategyClient::EndTurn (this=3527b798): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:24:44.937 [tid 500] fpu: sample at StrategyServer::BeginProcessTurn (this=0e8eba90): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:24:44.937 [tid 500] fpu: sample at StrategyServer::ProcessTurn (this=0e8eba90): cw=0x127f 53bit-double/nearest
05:26:33.843 [tid 500] fpu: sample at StrategyClient::EndTurn (this=0e963f00): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:26:36.937 [tid 500] fpu: sample at StrategyClient::EndTurn (this=35279a78): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:26:36.968 [tid 500] fpu: sample at StrategyClient::EndTurn (this=3527b050): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:26:36.983 [tid 500] fpu: sample at StrategyClient::EndTurn (this=3527b798): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:26:37.968 [tid 500] fpu: sample at StrategyServer::BeginProcessTurn (this=0e8eba90): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:26:37.968 [tid 500] fpu: sample at StrategyServer::ProcessTurn (this=0e8eba90): cw=0x127f 53bit-double/nearest
05:27:16.640 [tid 500] fpu: sample at StrategyClient::EndTurn (this=0e963f00): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:27:19.687 [tid 500] fpu: sample at StrategyClient::EndTurn (this=35279a78): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:27:19.718 [tid 500] fpu: sample at StrategyClient::EndTurn (this=3527b050): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:27:19.733 [tid 500] fpu: sample at StrategyClient::EndTurn (this=3527b798): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:27:20.702 [tid 500] fpu: sample at StrategyServer::BeginProcessTurn (this=0e8eba90): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:27:20.702 [tid 500] fpu: sample at StrategyServer::ProcessTurn (this=0e8eba90): cw=0x127f 53bit-double/nearest
05:28:01.202 [tid 500] fpu: sample at StrategyClient::EndTurn (this=0e963f00): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:28:04.265 [tid 500] fpu: sample at StrategyClient::EndTurn (this=35279a78): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:28:04.296 [tid 500] fpu: sample at StrategyClient::EndTurn (this=3527b050): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:28:04.312 [tid 500] fpu: sample at StrategyClient::EndTurn (this=3527b798): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:28:05.265 [tid 500] fpu: sample at StrategyServer::BeginProcessTurn (this=0e8eba90): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:28:05.265 [tid 500] fpu: sample at StrategyServer::ProcessTurn (this=0e8eba90): cw=0x127f 53bit-double/nearest
05:28:46.796 [tid 500] fpu: sample at StrategyClient::EndTurn (this=0e963f00): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:28:49.874 [tid 500] fpu: sample at StrategyClient::EndTurn (this=35279a78): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:28:49.890 [tid 500] fpu: sample at StrategyClient::EndTurn (this=3527b050): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:28:49.921 [tid 500] fpu: sample at StrategyClient::EndTurn (this=3527b798): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:28:50.874 [tid 500] fpu: sample at StrategyServer::BeginProcessTurn (this=0e8eba90): cw=0x127f 53bit-double/nearest [no fpu.force configured]
05:28:50.874 [tid 500] fpu: sample at StrategyServer::ProcessTurn (this=0e8eba90): cw=0x127f 53bit-double/nearest

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.