Wolfenstein Modloader Buy me a coffeeKaffee spendieren

In plain wordsIn einfachen Worten

A handover note from 21 September 2026 covering both games. The main find: the retail version of Wolfenstein II (TNC) accepts a developer mode switch and opens a real developer console, which was impossible in Wolfenstein: The New Order (TNO). The note also records several bugs in the mod patcher; two severe ones were later repaired but not yet independently verified, with a warning not to apply mods until then.

  • Starting TNC with com_enableDeveloperMode 1 opens a working developer console.
  • The old TNC cvar list of 2,231 entries was too large and partly mislabeled.
  • TNO save headers are an MD5-based checksum; 82 of 82 files match.
  • Patcher bugs F1 (broken rollback) and F2 (mixing up unrelated textures) were fixed, but the audit was not finished.
  • The program code is encrypted on disk, so static analysis only yields class names and function boundaries.

Eine Übergabenotiz vom 21. September 2026 zu beiden Spielen. Der wichtigste Fund: Die Verkaufsversion von Wolfenstein II (TNC) akzeptiert einen Entwicklermodus-Schalter und öffnet eine echte Entwickler-Konsole, was in Wolfenstein: The New Order (TNO) unmöglich war. Außerdem hält die Notiz mehrere Fehler im Mod-Patcher fest; zwei schwere wurden später repariert, aber noch nicht unabhängig geprüft, verbunden mit der Warnung, bis dahin keine Mods anzuwenden.

  • Mit com_enableDeveloperMode 1 gestartet, öffnet TNC eine funktionierende Entwicklerkonsole.
  • Die alte TNC-Cvar-Liste mit 2.231 Einträgen war zu groß und teils falsch beschriftet.
  • Die Kopfzeilen der TNO-Spielstände sind eine MD5-basierte Prüfsumme; 82 von 82 Dateien passen.
  • Patcher-Fehler F1 (kaputtes Zurückrollen) und F2 (Vermischen fremder Texturen) wurden behoben, die Prüfung aber nicht abgeschlossen.
  • Der Programmcode ist auf der Platte verschlüsselt; statische Analyse liefert nur Klassennamen und Funktionsgrenzen.

Technical detailsTechnische Details

Der technische Teil ist auf Englisch – so, wie er im Wiki steht. Unterstrichene Begriffe haben eine Erklärung auf Deutsch.

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.py are 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, .png lie uncompressed in the archive, write_in_place/rebuild would switch them to Deflate. Must be done before every menu text change.
  • com_skipIntroVideo stands in cvars.RETAIL_BLOCKED (measured), yet the README claims the intro skip works. One of these two is false, to be resolved with a start and qconsole.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

  1. patch.py:125 — broken Journal reads as "no Journal". read_journal() returns None on 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, and revert() restores only the .index files. Result: clean indices over rebuilt .resources — exactly the offset discrepancy this module is built to handle. Fix: only FileNotFoundError → None, rest → PatchError; write Journal via .tmp + os.replace.

  2. 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-calculated sha256 of the indices sits unused in the Manifest.

  3. patch.py:99 find() — only one name variant is patched. Confirmed on real data: 40 groups with multiple spellings, e.g., image textures/common/flat_local versus hqcompressnormal textures/common/flat_local. Whoever patches one leaves the other in the original — affects the Gore texture mod immediately.

  4. resources.py:280 — rebuild() checks too late, namely after tmp.replace(). Caught within apply(), but dangerous for direct callers from tools/.

  5. patch.py:274 — deflate_exact runs 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() returns True when the journal is unreadable — "Play" then starts without inquiry with an unknown mod state.
  • cli.py:446 catches only GameError; PatchError from cmd_info/cmd_play lands 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 .resources if 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.