# End-Turn determinism oracle — verified on the real game (2026-09-07) Question: "load the same save, press End Turn, and the resulting autosave is byte-identical run to run" (the Phase-2 verification oracle). Answer: **YES — byte-identical**, across five runs, in two separate game processes, with and without an intervening manual save, and also identical to the autosave the *original* (non-loaded, freshly created) game wrote in the earlier session. ## Verdict **byte-identical.** No masking is needed for the End Turn oracle when both sides start from the same save file. sha256 of `(Autosave).sav` after End Turn from `Autosave - turn2.sav` is `978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921` in every run (A–E) and equals the `Autosave - turn3.sav` captured in the first session (different process, game created live, not loaded). The only place bytes ever varied is **load → save round-trip of a *post-turn* autosave**: the game resets a per-player `Status` int (4 → 0) on load, which also moves the derived top-level `Checksum`. See "Round-trip stability" below for the canonicalization rule if a harness ever compares a loaded `(Autosave).sav` against a re-save of it. ## Revalidated 2026-09-08 (lane H) Lane N's End Turns overwrote the three autosave *contents* on VM140 (the file set was untouched). Lane H regenerated the pair by the documented procedure — load `ref-turn2.sav` (`ab4ac2d7…`, verified first), one End Turn, build `recap-7584bad-20260908T0615Z`, `hooks=trace` — and **both hashes below reproduced exactly**: `(Autosave EndTurn).sav` 66,732 B `bb4fd9ac89f41e3b…`, `(Autosave).sav` 67,219 B `978041acd168b56e…`. That is a stronger result than a repeat, because the process is no longer the one this document measured: nineteen template hooks and seven RNG entry-point detours are now installed instead of a single asm stub, an x87 control-word module is present, and `SavedGames\` holds eight files rather than four. The oracle survives all of it. Copies kept at `verify/results/shim/probes/oracle-restore/h-oracle-{endturn,autosave}.sav`, and the same bytes were put back on the VM as the three autosaves. **Related, and it belongs next to the oracle:** lane H found that a MinHook detour on **0x00893290** changes the game's output — same input save, `(Autosave).sav` 4 bytes different and one generator word short (`findings/control-flow/tail-probes.md` §2). The un-instrumented game and the lane-Z instrument agree; that one hook does not. **A behavioural harness must be checked against the oracle, not only against itself.** The cheap standing check is: load `verify/results/shim/tailrng/z2-endturn.sav`, one End Turn, expect `(Autosave).sav` = `b2124798470d85ea86b0e9e4abfd9e23f3be73caa1115dd7614021be76305939`. ## Environment * VM140 `sots-re-win10`, `C:\SOTS\Sword of the Stars.exe` GOG v1.8.1 (`ver.1.8.1 Wed Dec 13 03:38:31 2017`), DXVK 3.1 on Mesa lavapipe, windowed 1024x768 (see `Z:\saves\README.md` for the launch recipe). * Phase-2 shim (`binkw32.dll` proxy, build `4a15301-20260907T2118Z`) deployed with `shim.cfg` `hooks=trace` — only hooks `Mars_Application_Initialize` and logs; it was loaded in both processes used here (`shim.log`: pid 3808 then pid 4384). * Input save: `Z:\saves\Autosave - turn2.sav` (sha256 `ab4ac2d7…`, 66,739 B) copied to `C:\SOTS\SavedGames\ref-turn2.sav`. It is the *post-turn* autosave of turn 2 (state after End Turn #1 completed): 2 players (`re` = Morrigi @ Gamma Cephei, `Fane Lao` = AI), 28 stars, game name `MyGameverify1`. * The three autosaves the game had in `SavedGames\` before this work (turn 5-ish, 76 kB, from a later session) were moved to `pre-existing\` so the Load dialog only showed known files. * UI driven with `recipe/click_helper.ps1` via the `SOTSUI` scheduled task; screenshots via `qm screendump`. Click path per run (1024x768 window at 0,0): main menu `Load Game` (512,536) → `Single Player` (512,290) → `OK` (551,523) → pick row → `OK` (682,624) → lobby `Launch` (511,663) → ~30 s load → strategy map "Turn 2", Imperial Savings 289,688 → `End Turn` (100,714) → ~5 s → "Turn 3", Savings 532,369 → in-game menu (1000,714) → `Quit to Main Menu` (938,699) → `OK` (537,377). Manual save: menu → `Save` (938,679) → type suffix into the pre-filled name → `Save` (773,234) → `OK` → `Done` (773,542). ## Runs | Run | Process | Procedure | `(Autosave EndTurn).sav` | `(Autosave).sav` | |---|---|---|---|---| | A | pid 3808 (already running) | load `ref-turn2` → End Turn | `bb4fd9ac89f41e3b` 66,732 B | `978041acd168b56e` 67,219 B | | B | pid 3808 (same process) | quit to menu → load `ref-turn2` → End Turn | `bb4fd9ac89f41e3b` | `978041acd168b56e` | | C | **pid 4384 (game exited with code 0, relaunched via task `SOTS`)** | load `ref-turn2` → End Turn | `bb4fd9ac89f41e3b` | `978041acd168b56e` | | D | pid 4384 | load `ref-turn2` → **manual save `MyGameverify1rtD`** → End Turn | `bb4fd9ac89f41e3b` | `978041acd168b56e` | | E | pid 4384 | load `MyGameverify1verify1` (the Status=0 form) → **manual save `MyGameverify1rtE`** → End Turn | `bb4fd9ac89f41e3b` | `978041acd168b56e` | | ref (first session, 16:03) | pid 6396-era, game created live, never loaded | End Turn #2 of the live game | `Autosave EndTurn - turn3.sav` = `bb4fd9ac89f41e3b` | `Autosave - turn3.sav` = `978041acd168b56e` | Full hashes (`runX/sha256.txt`): ``` 978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921 (Autosave).sav [turn-3 post-turn state] runs A B C D E + first session bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 (Autosave EndTurn).sav [turn-2 pre-turn state] runs A B C D E + first session bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 MyGameverify1rtD.sav manual save after loading ref-turn2 (run D) bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 MyGameverify1rtE.sav manual save after loading MyGameverify1verify1 (run E) bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 MyGameverify1verify1.sav manual save from the first session (turn 2) ab4ac2d7e2977260a4de916f65c7ecc6dd5b0094dac4bb51241317232ea17ab4 ref-turn2.sav input (= Z:\saves\Autosave - turn2.sav) ``` Inflated (gunzipped) streams are equally identical: `(Autosave).sav` → 609,080 B `17db7ed455357c4d…`, `(Autosave EndTurn).sav` → 603,360 B `29f194dfcd932b93…`, `ref-turn2.sav` → 603,360 B `2fcf34ce4296b160…`. The gzip container is deterministic too (header `1f8b 0800 00000000 00 0b`: MTIME=0, XFL=0, OS=11/NTFS, no name/comment) — the compressed bytes match, not just the payload. ## What End Turn writes On the transition to the new turn the game writes, in `C:\SOTS\SavedGames\` (all at the same second): * `(Autosave EndTurn).sav` — the **pre-turn** state, i.e. the state at the moment End Turn was pressed. It is byte-identical to a manual save made on that turn. * `(Autosave).sav` — the **post-turn** state (new turn number, RNG advanced, economy applied). * `(Autosave Backup).sav` — the previous `(Autosave).sav`, rotated (only appears from the second End Turn in a process's SavedGames history; it is a rename, hash unchanged). Nothing is written on Load or on Launch from the lobby. The Load dialog lists files by filename with the in-file `GameName`, player count and `Turn`. Files in the first-session `Z:\saves\` were renamed copies of these (`Autosave - turnN.sav` = `(Autosave).sav`, etc.). No timestamp, save name, machine id or session salt is stored anywhere in the file: `MyGameverify1verify1`, `MyGameverify1rtD`, `MyGameverify1rtE` and `(Autosave EndTurn)` — four different file names, three of them written minutes apart in two processes — are one and the same byte sequence. ## Round-trip stability (load → save with no End Turn) * Loading the **pre-turn / manual form** and saving immediately reproduces the loaded bytes exactly: `MyGameverify1verify1.sav` (`bb4fd9ac…`) → load → Save → `MyGameverify1rtE.sav` = `bb4fd9ac…`. **Stable.** * Loading the **post-turn `(Autosave).sav` form** and saving immediately does *not* reproduce it: `ref-turn2.sav` (`ab4ac2d7…`, 66,739 B) → load → Save → `MyGameverify1rtD.sav` = `bb4fd9ac…` (66,732 B). The two inflated streams (603,360 B each) differ in exactly **5 bytes**: ``` inflated offset (1-based) ref-turn2 re-save field (save_reader --dump path) 89 0x64 0x54 Summary.Checksum int -1205790620 -> -1205790636 (delta 16 = 4 x 4) 66291 (0x102e8+4) 4 0 Player[PlryName="re"].Status 116067 (0x1c558+4) 4 0 Player[PlryName="Fane Lao"].Status 177739 (0x2b640+4) 4 0 Player[PlryName="Singularity"].Status (1st of two Singularity records) 235567 (0x39824+4) 4 0 Player[PlryName="Singularity"].Status (2nd) ``` The other four `Player.Status` fields (Alien Menace, Peacekeeper Enforcer, Von Neumann, Independent Colony) are 0 in both. Interpretation: `Status=4` is the "turn ended / ready" flag the server leaves on the four turn-participating player records when it writes the post-turn autosave; loading a game clears it. `Summary.Checksum` moves by exactly the sum of the changed bytes (16), consistent with an additive checksum over the payload — treat it as **derived**, never as state. (`dumps/diff_ref-turn2_vs_EndTurn.txt`, `dumps/cmp_ref-turn2_vs_EndTurn.txt`.) ## Recommended canonicalization rule for the harness 1. **Oracle compare (End Turn):** compare `(Autosave).sav` and `(Autosave EndTurn).sav` **byte-for-byte** (sha256 of the file is enough; no inflate, no masking). Expected for the reference run from `Autosave - turn2.sav`: `(Autosave).sav` = `978041acd168b56e…`, `(Autosave EndTurn).sav` = `bb4fd9ac89f41e3b…`. 2. **If a re-implementation compares its output against a *loaded* post-turn autosave, or compares a pre-turn save with a post-turn one of the same turn**, canonicalize by masking exactly: * every `Player.Status` int (set to 0), and * `Summary.Checksum` (recompute, or ignore). Nothing else varies. Use `save_reader.py --dump` on the inflated stream; both fields are named as above at fixed paths. 3. Do not mask anything in the gzip container — it is deterministic (MTIME 0), so a container-level sha256 is a valid first-line check; inflate only to localize a mismatch. ## Files in this directory * `runA/ … runE/` — the raw `SavedGames\` contents after each run, plus `sha256.txt`. * `pre-existing/` — the three later-turn autosaves that were in `SavedGames\` before this work (moved aside). * `dumps/` — `save_reader.py --dump` text of `ref-turn2` (input), `Autosave-EndTurn-turn2` and `Autosave-turn3` (outputs), plus the field diff and `cmp -l` of the round-trip pair. * `inflated/` — gunzipped streams of the same three files. * `screenshots/` — console captures of each run's loaded state / result / save dialogs. * `hashes.txt` — sha256 of every `.sav` across all runs. State left behind: the game is still running on VM140 (pid 4384, task `SOTS`, shim `hooks=trace`), sitting on the turn-3 strategy map after run E; `C:\SOTS\SavedGames\` holds the run-E set (`ref-turn2.sav`, `MyGameverify1*.sav`, the three `(Autosave*)` files).