Measured using tools/probe_savegame.py (read-only) against existing save games plus string analysis of both game executables. Everything claimed here is measured; where something remained open, it is explicitly noted.
Summary:
| The New Order | The New Colossus | |
|---|---|---|
| Storage Location | base/savegame/ |
base/savegame.user/<SteamID64>/SLOT0..2/ |
| Save Game Data | Plaintext binary, unencrypted | Plaintext binary, unencrypted |
| Profile/Progression | profile.bin, unencrypted |
profile.bin, progression.bin, AES-encrypted |
| Integrity Protection | 4-byte header + checksum= in game.details |
Additionally one 40-byte .verify per file |
| Manipulation Detected? | Checksum must match | .verify is tamper-proof, but blocks loading only if present |
1. Directories
The New Order
C:\Users\<user>\Saved Games\MachineGames\Wolfenstein The New Order\base\
wolfConfig.cfg Plaintext, Cvars
savegame\profile.bin 3950 B, Binary structure (H=4,08; zlib 30 %)
savegame\GAME-AUTOSAVE\
checkpoint0.dat 26572 B
mapstart0.dat 3002 B
game.details 143 B, Plaintext
No .verify files, no progression.bin. TNO does not protect save games cryptographically — no cryptographic symbols appear in the Exe at all (Blowfish, AES, SHA, HMAC, BCrypt: zero hits).
The New Colossus
base\savegame.user\<SteamID>\
profile.bin 3976 B encrypted + no .verify
profile2.bin 72 B encrypted + profile2.bin.verify
migrated.bin 72 B encrypted
SLOT0..SLOT2\
PROFILE\progression.bin 136472 B encrypted + .verify
PROFILE\progression2.bin 9800 B encrypted + .verify
GAME-AUTOSAVE0..2, GAME-HELM, GAME-TRANSITION, FC0-AUTOSAVE0..1\
checkpoint.dat up to 564 kB Plaintext binary + .verify
checkpoint_mapstart.dat ~700 B + .verify
checkpoint_alt.dat 4 B (empty) + .verify
game.details ~400 B Plaintext + .verify
game_duration.dat ~9,5 kB + .verify
level_duration.dat 167 B + .verify
level_duration_alt.dat 4 B (empty) + .verify
WOLFORG-SAVEDATA\HIGHSCOR.WL6 508 B (Wolf3D-Highscores) + .verify
Total of 99 .verify files, all exactly 40 bytes.
2. game.details — Plaintext
Both games write a pure key=value text file:
checksum=213035449
completionRation=50%
date=1764620045
difficulty=2
gamePlayTime=2265
gameVersion=1145896965
mapName=game/wolf/submarine/submarine
...
gameVersion is a tag constant, not a date:
- TNO
1145896962=0x44490002="DI"+ Version 2 - TNC
1145896965=0x44490005="DI"+ Version 5
The same pattern is embedded in the header of .dat files (see below) as 0x444D0005 = "DM" + Version 5.
checksum is a signed 32-bit value and not CRC-32 or Adler-32 over the rest of the file, nor over mapName (verified across all 13 files, no hits). It changes per save game even with the same map, depending on the save game content.
3. .dat Files — 4-Byte Header plus Plaintext Structure
All .dat files from both games begin with 4 bytes, followed by the actual structure:
checkpoint.dat fd a0 5e f3 | 05 00 4d 44 | 26 00 00 00 | 01 00 | 22 00 00 00 "2018…"
game_duration.dat 7e d1 7e ca | 44 4d 00 05 | 00 00 00 26 | 01 | 1f 00 00 00 "idGameL…"
checkpoint_mapstart.dat 2d c2 d7 7d | 00 60 ff cd | 00 00 00 03 | …
checkpoint_alt.dat 3b 75 65 5e (header only, body empty)
Notable: checkpoint.dat writes the inner header little-endian (05 00 4d 44 = 0x444D0005), while *_duration.dat writes the same value big-endian (44 4d 00 05). The subsequent strings are length-prefixed with LE lengths in both cases. Thus, two different serializers are used.
The 4-Byte Header — Solved: idLibs MD5_BlockChecksum, big-endian
def savehash(rumpf):
w = struct.unpack("<4I", hashlib.md5(rumpf).digest())
return struct.pack(">I", w[0] ^ w[1] ^ w[2] ^ w[3])
In other words: MD5 over the payload (everything from byte 4), reading the four 32-bit words of the digest little-endian, XORing them, and writing the result big-endian to the file. This is literally idLibs MD5_BlockChecksum — the same folding function id Software has used since Quake 3 for block checksums. Only the storage format is big-endian, while everything else in these files is little-endian; it was precisely this that caused the previous search to fail.
The known empty value explains itself naturally: MD5("") is d41d8cd9 8f00b204 e9800998 ecf8427e; folding the four LE words yields 0x3B75655E, which when written big-endian becomes the bytes 3b 75 65 5e — interpreted as a u32 little-endian, this gives the frequently cited 0x5E65753B.
Verified: python tools/savehash.py pruefen recalculates the header for every savegame file on this machine.
82 of 82 files match header, 0 deviations.
This covers both titles combined: 80 .dat files (The New Order and The New Colossus), profile.bin from The New Order, and HIGHSCOR.WL6. Payload lengths range from 0 to 1,165,441 bytes; 45 of them are pairwise distinct (44 non-empty). Neither game.details (pure text, starting with checksum=) nor the AES files from The New Colossus have such a header (the first 8 bytes there are the Nonce) — these are not included in the count and correctly fail.
Regarding robustness: the formula was guessed from one data point (the empty value) and subsequently correctly predicts 44 further, mutually independent 32-bit values. Even generously calculated — say, a total of 2^40 formula variants were tried — the probability that one of them matches all 44 by chance is 2^40 / 2^(32·44) = 2^-1368. The counter-check stands as an assert in tools/savehash.py --selbsttest: flipping a single bit results in a different header, one that none of the existing savegames possess.
What the Header is Provably Not
The path to this solution cleared an entire search space, which remains valuable even if the answer is already known. From the savegames themselves, this can be measured without starting the game: there are ten pairs of equal-length payloads with different content (lengths 402, 450, 735, 5829, 9465, 9717). For equal lengths, the Init and XorOut values of a CRC emerge from the difference, leaving
kopf₁ ⊕ kopf₂ ≡ (rumpf₁ ⊕ rumpf₂)(x) · x³² (mod P)
This means: the sought polynomial P divides the known number D(x) = (rumpf₁⊕rumpf₂)(x)·x³² + (kopf₁⊕kopf₂)(x) over GF(2). The greatest common divisor of all ten Ds must therefore contain P.
GCD = 1. Degree 0, no common factor of any degree.
Thus, the header is not a CRC-32 — for no polynomial, no start value, no XorOut, and neither of the two bit reflections. The same conclusion holds separately by file type (checkpoint_mapstart.dat, game_duration.dat — which use provably different serializers) and with 16 variants of the data stream (Byte-, 16-, 32-, 64-bit word order × Reflection × Header LE/BE) plus appended secrets of every length 0…512: everywhere degree < 32. The solver is self-tested; it finds 0x04C11DB7 on synthetic data in both normal and reflected forms.
Additionally rejected (Seed calculated from zero, checked against all 42 non-empty hulls, 6 frames each — 0 hits): xxHash32, MurmurHash3 x86_32, MurmurHash2, Jenkins lookup3/hashlittle, Jenkins one-at-a-time, FNV-1, FNV-1a, djb2, djb2-xor, sdbm, SuperFastHash, 32-bit word product, and word rotation XOR.
The CRC tables in the Exes were cleanly extracted from .rdata/.data and recalculated against their polynomials (.text is SteamStub-encrypted in both Exes and remains unreadable):
| Exe | Address | Polynomial |
|---|---|---|
| TNO | 0x141079F50 (.rdata), 0x1415BC670 (.data) |
0xEDB88320 CRC-32 |
| TNO | 0x14107A750 (.rdata) |
0xE1351B80 |
| TNC | 0x14209B400 (.rdata) |
0x82F63B78 CRC-32C |
| TNC | 0x1425C9140, 0x1425D0040 (.rdata), 0x142EE0FD0 (.data) |
0xEDB88320 |
| TNC | 0x1425D0840 (.rdata) |
0xE1351B80 |
So the tables do exist — they are simply not used for the game state header. superscriptx64.dll (TNO, unencrypted .text) contains no CRC table at all.
The Header Depends Only on Content
80 .dat files, 43 different hulls. No hull carries two different headers, and no header belongs to two different hulls. The eleven content-identical level_duration.dat files are located in eleven different folders and carry the same header — the path does not matter, nor does the filename (checkpoint_alt.dat and level_duration_alt.dat are both empty and both carry 0x5E65753B).
A lucky find in the inventory is the pair
SLOT0/GAME-AUTOSAVE1/game_duration.dat against
SLOT0/GAME-TRANSITION/game_duration.dat: 9465-byte hulls that differ by exactly one byte (Offset 3900, 0x99 vs 0x00), with a header difference of 0x6ACD75CB. This is precisely the "change one byte and observe" experiment without needing to touch a save file.
Content of checkpoint.dat (TNC)
Plain text, easy to read. At the start of the file is a string table with LE-u32 length prefixes, among which:
ammo/player/sp/handgun weapon/player/sp/handgun
ammo/player/sp/shotgun weapon/player/sp/shotgun
ammo/player/sp/lasergewehr ammo/player/sp/ubergewehr_laser
player/perkitem unlocks/upgradekit abilities/healthy
Important for expectations: This is the name table, not the values. Ammo counts and upgrade flags are located in the binary hull and are addressed via an index into this table. An ammo value cannot therefore be directly changed by searching for ammo/player/sp/handgun next to it; one must first disassemble the component blocks
(idGameLocal_CheckpointInfo:10, idGameMapMemoryManager:20/30, …). This is feasible, but constitutes real format work.
4. The .verify Files — Verified, Not Guessed
The hypothesis "40 bytes = SHA-1 (20) plus remainder" was tested byte-for-byte: for all 99 .verify files, MD5, SHA-1, SHA-256, CRC-32 (LE and BE) were computed over
- the entire source file
- the source file without its 4-byte header
filename‖contentandcontent‖filenamelength(u32)‖content
and each of these values was compared against the first n bytes, the last n bytes, both byte-swapped, and as a substring anywhere within the .verify.
Total hits: 0.
More significant is a second finding:
28 byte-identical source files result in 28 different
.verifyfiles. (Similarly, 11 identicallevel_duration.dat→ 11 different.verify.)
Same content, different checksum — this rules out every pure hash function and every deterministic encryption. A random value per write operation is embedded within it. This fits: across all 99 files, not a single 16-byte block repeats, and the entropy reaches 100% of the achievable 40 bytes.
What the Exe Says About It
NewColossus_x64vk.exe contains a complete crypto layer over Windows CNG (BCrypt):
idSafeBCryptAlgorithmHandle: BCryptOpenAlgorithmProvider failed: %x idSecureHash::SetFunction: Invalid hashFunction value : %d idSecureHMAC::Initialize: Invalid hmacFunction value : %d idSecureBlockCipher::EncryptWithOSPadding: Not initialized idSecureAEADCipher::Encrypt: nonce must be exactly 12 bytes (bcrypt restriction) idSecureAEADCipher: BCryptSetProperty( BCRYPT_CHAINING_MODE ) failed: %x
UTF-16 algorithm names listed alongside are SHA1, SHA256, SHA384, SHA512, MD5, AES, ChainingModeGCM, and AuthTagLength. File naming patterns include %s.verify, %s.temp.verify, and *.verify.
The sizes confirm the pattern. Every encrypted file is ≡ 8 (mod 16):
| File | Size | −8 | in 16-Byte Blocks |
|---|---|---|---|
*.verify |
40 | 32 | 2 |
profile2.bin, migrated.bin |
72 | 64 | 4 |
profile.bin |
3976 | 3968 | 248 |
progression2.bin |
9800 | 9792 | 612 |
progression.bin |
136472 | 136464 | 8529 |
Thus: 8-byte header (random salt/nonce) + AES block cipher. For .verify files, this is exactly 2 AES blocks, fitting for a SHA-1 value (20 bytes) or MD5 (16 bytes) with padding. The key resides in the executable; without it, a .verify cannot be recalculated.
And: TNO does not have this
The profile.bin from The New Order is not encrypted — entropy 4.08 bits/byte, zlib compresses it to 30%, with 129 repeated 8-byte blocks and per-row zeros. A normal binary structure.
5. What This Means for Cheats
The most important finding lies not in the files, but in the executable:
02686858 Skip entering developer mode when loading corrupted saves
02686898 devMode_skipCorruptedSaves
02686ed0 Loading Dev Mode or corrupted save game.
026e0b98 SAVEGAME_E_CORRUPTED
02981748 devMode_enable
02981810 devMode_fatalErrorOnEnter
026c12b8 +devMode_
02794d58 DEVMODE_DEVELOPER / 02794df0 DEVMODE_PRODUCTION
A save game whose .verify does not match is not rejected by The New Colossus, but instead loaded as "corrupted" — and the game enters Developer Mode in the process. The .verify is therefore not a loading barrier, but an Achievement/Dev-Mode flag.
This flips the assessment:
- The
.verifyis not a lock. A modifiedcheckpoint.datwill be loaded. The question is only whether the 4-byte header of the file itself matches — and this is independent of the.verifyand calculable since this measurement (tools/savehash.py set). - The actual lever is
+devMode_. That the executable recognizes a command-line prefix+devMode_and exposesdevMode_enableas a CVar is more interesting for the CVar lock than any save game hack. This should be pursued as its own thread (seedocs/limits.md). - Profile/Progression remain locked.
progression.binandprofile.bin(TNC) are AES-encrypted; permanent unlocks cannot be set there without the key. - For The New Order, nothing is encrypted, and the 4-byte header is calculable. Thus, the save game there is open: modify the body, run
tools/savehash.py set, and you are done. The work shifts from cracking the checksum to deconstructing the component format within the body.
6. Open Points
- Calculation method of the
checksum=field ingame.details. It is not an MD5 fold — tested against all 14game.detailsentries against the text without thechecksum=line, againstmapName, against every sibling file (whole, body only, header only), and against the concatenation of all sibling headers, each folded, byte-rotated, and truncated to 4 bytes: 0 hits. - Whether TNO also only warns or rejects the save game on checksum error — untested, as this would require starting the game.
- AES key and exact payload of the
.verify(SHA-1 or MD5).
7. Tools
python tools/probe_savegame.py # both titles
python tools/probe_savegame.py tnc # The New Colossus only
python tools/savehash.py pruefen # recalculate header of each file
python tools/savehash.py kopf <datei> # expected/actual header of a file
python tools/savehash.py setzen <ein> <aus> # recalculate header, write to NEW file
python tools/savehash.py --selbsttest
probe_savegame.py opens exclusively for reading and writes nothing. savehash.py also reads only; it writes solely to setzen, always to a new path — the target path must not be in the save game folder nor in the Steam library, otherwise it will abort. A corrupted save game costs real game progress.