- determinism oracle regenerated and byte-identical (bb4fd9ac / 978041ac) - the four phase-23/33 draw-bearing tail callees run EVERY turn; the three inner functions holding the draws run zero times -- the gate is inside each outer body - CreateRaidEncounter is called (2 on one turn) and draws nothing: candidate list empty - Zuul: 7 calls / 7 words per trade-raid Chance site, 14 not 16, as predicted - EncounterDetect_Run receives an EMPTY record vector, so ProcessTeamRecord and AssignContacts never run; the 2-word detection residual is in 0x007d5150's subtree - a MinHook detour on 0x00893290 changes the game's output; bisected over six runs. The un-instrumented game and lane Z's instrument agree, so lane Z's numbers stand - lane AI1 insert: P2 held across two fresh processes, Rung B stays as written
11 KiB
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.exeGOG v1.8.1 (ver.1.8.1 Wed Dec 13 03:38:31 2017), DXVK 3.1 on Mesa lavapipe, windowed 1024x768 (seeZ:\saves\README.mdfor the launch recipe). - Phase-2 shim (
binkw32.dllproxy, build4a15301-20260907T2118Z) deployed withshim.cfghooks=trace— only hooksMars_Application_Initializeand logs; it was loaded in both processes used here (shim.log: pid 3808 then pid 4384). - Input save:
Z:\saves\Autosave - turn2.sav(sha256ab4ac2d7…, 66,739 B) copied toC:\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 nameMyGameverify1. - The three autosaves the game had in
SavedGames\before this work (turn 5-ish, 76 kB, from a later session) were moved topre-existing\so the Load dialog only showed known files. - UI driven with
recipe/click_helper.ps1via theSOTSUIscheduled task; screenshots viaqm 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).savform 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
- Oracle compare (End Turn): compare
(Autosave).savand(Autosave EndTurn).savbyte-for-byte (sha256 of the file is enough; no inflate, no masking). Expected for the reference run fromAutosave - turn2.sav:(Autosave).sav=978041acd168b56e…,(Autosave EndTurn).sav=bb4fd9ac89f41e3b…. - 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.Statusint (set to 0), and Summary.Checksum(recompute, or ignore). Nothing else varies. Usesave_reader.py --dumpon the inflated stream; both fields are named as above at fixed paths.
- every
- 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 rawSavedGames\contents after each run, plussha256.txt.pre-existing/— the three later-turn autosaves that were inSavedGames\before this work (moved aside).dumps/—save_reader.py --dumptext ofref-turn2(input),Autosave-EndTurn-turn2andAutosave-turn3(outputs), plus the field diff andcmp -lof 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.savacross 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).