Handover before a session restart. What is stated here has not been recorded in any other document.
The Most Important Discovery: Developer Console in Wolfenstein II
The Retail version of The New Colossus accepts com_enableDeveloperMode 1 and opens a real developer console. In Part 1, this was exactly impossible — there, the DevGUI is not connected in the Retail build.
Proven by a screenshot of the screen: Console overlay with input line ], engine outputs (QUICKSAVE, WARNING: [SOUND] Missing sound event ...), and the build string ...-vulkan retail.
Start as follows:
python -c "import sys; sys.path.insert(0,'.'); from wolfsdk.game import Game; \
Game.find(title='tnc').launch(['+com_enableDeveloperMode','1'])"
Steam asks for arguments via a steam:// URL ("Start the game with custom start options", Continue/Cancel). Without clicking, nothing starts. This is recorded in C:\Program Files (x86)\Steam\logs\console_log.txt as LaunchApp waiting for user response to ShowGameArgs.
Remote Control of the Console
scratchpad\type.ps1 types lines into the game window (Scancodes, because the engine does not read virtual keys). It checks before each line who has focus and aborts otherwise — without this check, three console commands landed in the user's chat window.
Verified working commands:
| Command | Effect |
|---|---|
writeConfig <file> |
writes all Cvars with real values |
conDump <file> |
writes the console buffer |
listCvars <search> |
Values, substring search |
listCvars - help <search> |
real descriptions from the engine |
Both files land in
%USERPROFILE%\Saved Games\MachineGames\Wolfenstein II The New Colossus\base\.
Additional commands found in the EXE: listCmds, listDecls, reloadDecls, god, noclip, notarget, spawn, teleport, devmap, killMonsters, kill, r_showTris, exec, bind, toggle, screenshot.
Two Corrections to My Own Work
1. docs/cvars_tnc.json with 2231 entries is overestimated. The engine itself responds to listCvars - help g_ with eight Cvars, and to bot_ with "0 cvars listed". Most harvested names are therefore not registered Cvars. The file is a candidate list, not a catalog.
2. The descriptions therein are partially misassigned. Cross-check: com_assertNoDialog is named by the engine as "bypass the popup assert dialog", while in my file it held the description of another Cvar. Correct were instead g_showHud, g_bloodEffects, g_fov.
Next step: Iterate listCvars - help through a–z, mix results, rebuild cvars_tnc.json from them. An initial attempt failed because the dump script counted old files instead of checking if new ones were written — check timestamps, otherwise you measure yourself.
Open Issues
- One agent is still running: Game state format for both games + code review of
resources.py/patch.py. wolfsdk/idcl.py+wolfsdk/oodle.pyare present but not connected — an unpacker for Wolfenstein-II assets is missing as the final step.- Raw write path in Patcher is missing:
.bswf,.bimage,.lang,.dct,.pnglie uncompressed in the archive,write_in_place/rebuildwould switch them to Deflate. Must be done before every menu text change. com_skipIntroVideostands incvars.RETAIL_BLOCKED(measured), yet the README claims the intro skip works. One of these two is false, to be resolved with a start andqconsole.log.
State of Installations
Part 1 is patched: 7 Mods, 373 Assets, Backup ok, Journal and config.json identical. Part 2 is untouched — there only reading and starting occurred. Wolfenstein II ran last with the console open.
python tools/verify_all.py runs all harmless checks.
Addendum: Findings of the Last Agent (just before the restart)
Patcher — five real bugs, none fixed
-
patch.py:125— broken Journal reads as "no Journal".read_journal()returnsNoneon every error, including invalid JSON. A crash during_write_journal(line 335 writes non-atomically) leaves a partial JSON; the loader then reports "Original state" despite the patch being applied, andrevert()restores only the .index files. Result: clean indices over rebuilt .resources — exactly the offset discrepancy this module is built to handle. Fix: onlyFileNotFoundError→None, rest →PatchError; write Journal via.tmp+os.replace. -
patch.py:179— Game update not detected. The Manifest records only the size per.resources, and complains only if it becomes smaller. A same-size or larger update gets through;revert()then copies old indices over new data without a rollback path. The already-calculatedsha256of the indices sits unused in the Manifest. -
patch.py:99find()— only one name variant is patched. Confirmed on real data: 40 groups with multiple spellings, e.g.,image textures/common/flat_localversushqcompressnormal textures/common/flat_local. Whoever patches one leaves the other in the original — affects the Gore texture mod immediately. -
resources.py:280—rebuild()checks too late, namely aftertmp.replace(). Caught withinapply(), but dangerous for direct callers fromtools/. -
patch.py:274—deflate_exactruns twice, the most expensive function in the module.
Seven plausible suspicions were refuted instead: no duplicate or overlapping offsets, no gaps, rebuild() reproduces file size byte-exactly. The module's assumptions hold.
Save Games
.verify files are not reproducible: 28 byte-identical source files yield 28 different .verify files, meaning a random value is embedded per write operation. Consistent with this, every encrypted file ≡ 8 (mod 16) — 8 bytes of salt plus AES blocks, and the EXE brings a Windows-CNG layer along.
But the lock does not work at all: Part 2 refuses to load a save game with an incorrect .verify only in Dev Mode (Loading Dev Mode or corrupted save game., Cvar devMode_skipCorruptedSaves).
New lead, more valuable than any save-game hack: the command-line prefix +devMode_ with devMode_enable, devMode_fatalErrorOnEnter, DEVMODE_DEVELOPER/PRODUCTION.
~~Open in Part 1: the 4-byte header of each .dat is a checksum, whose algorithm was not found.~~ Done, 22.09. It is idLibs MD5_BlockChecksum: MD5 over the body, the four digest words read little-endian and XOR-folded — stored but big-endian, as the sole field in these files. This has failed at brute-forcing it. 82/82 files with headers match; details and the negative proof against every CRC-32 in docs/savegames.md, tool tools/savehash.py.
WARNING — Status 21:20, before the second restart
Do not run wolfsdk apply until F1 is fixed
The Patcher repair from tonight has introduced two severe bugs. Both reproduced in a sandbox, both still unfixed. A ready-made fix script lies under
<scratchpad>\fix_f1_f2.py — it has not been applied because applying it would be unverified.
F1 — the rollback in apply() is dead (patch.py:413 / :301)
ensure_backup() stamps size and mtime of every .resources before writing, while _record_resources() refreshes the stamp only after the try block. If an exception occurs in between, verify_backup() sees exactly the deviation that apply() itself created, treats it as a game update, and the new rule "every problem blocks" denies its own rollback.
Issue: The installation remains half-patched; the original error is replaced by a misleading message, and every subsequent revert also fails — permanently, because the stamp never matches again. The only way out is manual work or 40 GB of Steam file verification.
The rebuild path hits deterministically (file grows), while the in-place path relies on mtime once more than one second has passed — with 373 assets over 14 archives, this always occurs.
Fix: If a journal exists, the files are our own; the stamp comparison should then be skipped. Without a journal, any deviation is truly an external change, so the check remains active.
F2 — find() collects foreign assets (patch.py:118 / :139)
Grouping is done via the last whitespace token. For bracketed option forms, this is a number, not a path:
find('image', 'shrink( .../keller/keller_head_ao, 512)') -> 3 hits
... keller_head_ao (correct, 2x)
... lasercutter_v2_glass (FOREIGN)
16 of the 40 variant groups are merged this way; the largest, ('image','1)'), contains 74 unrelated names. apply() writes the payload to every hit — thus a texture mod would overwrite up to 73 uninvolved textures and rebuild foreign archives.
Today not critical: of the 373 active assets, exactly one has variants, and that is a legitimate spelling. It is a mine, not current damage.
Fix: Group by path — the token containing a slash. No or multiple slashes: no group, only exact match.
F3 — The verification suite does not cover these modules
tools/verify_all.py skips verify_patch.py and verify_gui.py because the WRITE_CALLS filter sees install.apply(). However, verify_patch.py is the only end-to-end test for patch.py and resources.py, and it operates solely within a temp directory. Therefore, the five repairs are not covered by the suite at all.
Other open points from the same inspection
gui.py:586_mods_in_sync()returnsTruewhen the journal is unreadable — "Play" then starts without inquiry with an unknown mod state.cli.py:446catches onlyGameError;PatchErrorfromcmd_info/cmd_playlands as a Traceback for the user.- Only one archive pair was tested. The multi-archive case of
apply()is not covered.
What else is missing
The attempt at a retail lock in Part 2 failed: g_infiniteAmmo and g_freeCamSpeed do not exist there (Unknown command); they came from the harvested name list. For a valid attempt, take names from docs/cvars_tnc_echt.json. The game crashed with an Access Violation (21:11:04); save files are intact.
And a find from our own log, not yet pursued: Part 2 searches %USERPROFILE%\Saved Games\...\base\ before the Steam folder. Experiments on this are ready in tools/experiment_loadpaths.py.
Status 21:45 — third restart, all aborted
F1 and F2 are repaired, inspection not yet complete
wolfsdk/patch.py now carries both fixes:
- F1:
verify_backup()skips the stamp comparison of.resourcesif a journal exists (ours = self.journal_path.is_file()). Without a journal, game update detection remains active. - F2: new method
Installation._path_key(name)groups via the single token with a slash instead of the last token.
Self-proven: the contaminated case now yields 4 hits, all the same path (keller_head_ao under shrink( and resize(); the foreign lasercutter_v2_glass is gone. textures/common/flat_local still finds both legitimate spellings. Largest group 2 instead of 74, 35 groups instead of 40.
Not yet verified — the independent auditor was aborted mid-run:
- Does the rollback in
apply()now truly proceed after an exception, and does it retain the original error? - Does
revert()continue to refuse when actually changed from outside (Manifest present, Journal deleted)? If F1-Fix disables this, it is a regression.
Last status of the auditor before the abort: 275 names lose their grouping due to the F2-Fix, because they contain multiple tokens with a forward slash — yet there is 0 overlap, meaning these 275 have no variants at all. Re-calculate.
Until this is verified: do not run wolfsdk apply against the real installation.
Ghidra — Installation started, aborted
Script: <scratchpad>\setup_ghidra.ps1, callable again, skips already loaded files. Target %USERPROFILE%\tools, partial download resides in %USERPROFILE%\tools\_download.
- Ghidra 12.1.4 (543 MB), JDK 21 Temurin (196 MB), both as ZIP without installer and without elevation.
- URLs are stored in
%TEMP%\ghidra_url.txt,ghidra_name.txt,jdk_url.txt.
Measured and critical for expectations: .text has entropy 8.000 with 0 % null bytes in both EXEs — fully encrypted, indistinguishable from random. Static code analysis yields nothing there.
What Ghidra still provides: RTTI class hierarchy and vtables from .rdata, plus 172,586 function boundaries from .pdata (TNO 50,901, TNC 121,685).
For actual code, only the runtime path works: attach a debugger to the running process; there, .text is decrypted. It does not generate a DRM-free file. Creating an unpacked EXE remains excluded.
Open tasks in Bus
patchverify (independently verify F1/F2) and suite (verify_all should run verify_patch.py instead of skipping) — both unfinished, tasks in history. Bus resides under .agent-cache.