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:
parent
3e918d7372
commit
7eabfefff2
19 changed files with 2982 additions and 9 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
189
findings/subsystems/eventlive-verification.md
Normal file
189
findings/subsystems/eventlive-verification.md
Normal 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).
|
||||
1054
verify/results/compare/eventlive-b3-t1-5.json
Normal file
1054
verify/results/compare/eventlive-b3-t1-5.json
Normal file
File diff suppressed because it is too large
Load diff
46
verify/results/compare/eventlive-b3-t1-5.md
Normal file
46
verify/results/compare/eventlive-b3-t1-5.md
Normal 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
|
||||
763
verify/results/compare/eventlive-b3-t1.json
Normal file
763
verify/results/compare/eventlive-b3-t1.json
Normal 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": []
|
||||
}
|
||||
23
verify/results/compare/eventlive-b3-t1.md
Normal file
23
verify/results/compare/eventlive-b3-t1.md
Normal 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
|
||||
723
verify/results/compare/eventlive-b3-zuul.json
Normal file
723
verify/results/compare/eventlive-b3-zuul.json
Normal 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": []
|
||||
}
|
||||
23
verify/results/compare/eventlive-b3-zuul.md
Normal file
23
verify/results/compare/eventlive-b3-zuul.md
Normal 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
|
||||
BIN
verify/results/saves/zuul-turn5-species5.sav
Normal file
BIN
verify/results/saves/zuul-turn5-species5.sav
Normal file
Binary file not shown.
BIN
verify/results/shim/eventlive/c6b-loaded.png
Normal file
BIN
verify/results/shim/eventlive/c6b-loaded.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
BIN
verify/results/shim/eventlive/c7b-turn3.png
Normal file
BIN
verify/results/shim/eventlive/c7b-turn3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
BIN
verify/results/shim/eventlive/load-dialog-8files.png
Normal file
BIN
verify/results/shim/eventlive/load-dialog-8files.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
69
verify/results/shim/eventlive/shim-zuul.log
Normal file
69
verify/results/shim/eventlive/shim-zuul.log
Normal 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
|
||||
79
verify/results/shim/eventlive/shim.log
Normal file
79
verify/results/shim/eventlive/shim.log
Normal 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
|
||||
BIN
verify/results/shim/eventlive/t7-after.png
Normal file
BIN
verify/results/shim/eventlive/t7-after.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
BIN
verify/results/shim/eventlive/zuul-turn1.png
Normal file
BIN
verify/results/shim/eventlive/zuul-turn1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
BIN
verify/traces/eventlive-b3-t1-5.jsonl.gz
Normal file
BIN
verify/traces/eventlive-b3-t1-5.jsonl.gz
Normal file
Binary file not shown.
4
verify/traces/eventlive-b3-t1.jsonl
Normal file
4
verify/traces/eventlive-b3-t1.jsonl
Normal file
File diff suppressed because one or more lines are too long
BIN
verify/traces/eventlive-b3-zuul.jsonl.gz
Normal file
BIN
verify/traces/eventlive-b3-zuul.jsonl.gz
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue