Wolfenstein Modloader Buy me a coffeeKaffee spendieren

In plain wordsIn einfachen Worten

This report covers a new asset browser tab in the project's mod tool for Wolfenstein II (TNC). It lists all 218,311 entries from 29 archives, shows textures as images, and decrypts and unpacks protected text files, such as the English game text, for viewing and editing. Edits are saved as mods in a separate folder. Whether the game accepts such saved changes has not been tested in the game.

  • 379 of 400 sample images (94.75%) show a preview; the rest are special lighting formats.
  • All 239 encrypted or packed text files survive unpack, display and repack byte-for-byte.
  • Seven reviewer findings were fixed, e.g. unsafe folder names and 0-byte saves.
  • Assets that exist only in a DLC can be viewed but not saved as mods.
  • Behavior for Wolfenstein: The New Order (TNO) stays the same, except for safer mod saving.

Der Bericht behandelt einen neuen Asset-Browser-Reiter im Mod-Werkzeug des Projekts für Wolfenstein II (TNC). Er listet alle 218.311 Einträge aus 29 Archiven, zeigt Texturen als Bilder und entschlüsselt und entpackt geschützte Textdateien wie den englischen Spieltext zum Ansehen und Bearbeiten. Änderungen werden als Mod in einem eigenen Ordner gespeichert. Ob das Spiel solche Änderungen annimmt, wurde im Spiel nicht getestet.

  • 379 von 400 Beispielbildern (94,75 %) zeigen eine Vorschau; der Rest sind spezielle Licht-Formate.
  • Alle 239 verschlüsselten oder gepackten Textdateien überstehen Entpacken, Anzeige und Neupacken byte-genau.
  • Sieben Befunde der Prüfer wurden behoben, etwa unsichere Ordnernamen und 0-Byte-Dateien.
  • Assets, die nur in einem DLC liegen, kannst du ansehen, aber nicht als Mod speichern.
  • Für Wolfenstein: The New Order (TNO) bleibt alles gleich, bis auf sichereres Speichern von Mods.

Technical detailsTechnische Details

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

Status 2026-09-24, Worker w-assetviewer-a1. Continues an abandoned attempt: wolfsdk/tncview.py, with the changes to wolfsdk/browser.py and tools/verify_browser_tnc.py already in place; this report was missing. Verified, hardened three locations (see "Changed in this Pass"), and ran everything again against the actual installations.

Second Pass (same day): The seven findings from the auditors are fixed, see "Auditor Findings". Each finding first received a check in tools/verify_browser_tnc.py, which failed on the old code (20 ERRORS), and now runs successfully.

wolfsdk/gui.py is not touched: The GUI already passes a tncpatch.Installation to the browser for Part 2; that suffices.

What the Tab Shows for Wolfenstein II Now

After "Load", there are 29 Archives with 218,311 Entries in the list:

Location Archives Entries
base/ 24 178,077
dlc/dlc_0/base/ (Test DLC, not retail) 1 7,128
dlc/dlc_1/base/ 1 13,366
dlc/dlc_2/base/ 1 6,985
dlc/dlc_3/base/ 1 12,754
dlc/dlc_5/base/ (Exec-Carrier, not retail) 1 1

The columns remain unchanged. The archive appears in the info line; for DLCs with additions, e.g., Archive dlc_1/chunk_20.resources (DLC, read-only).

Type (Count) Text Tab Image Tab Export... Save as Mod / Import
image (37,910) Hex of the Payload Preview ≤ 512 px; below that a line with size, format, Mip number, displayed Mip, source (TexDB/embedded), and hints .png = highest decodable Mip in full size (Status names the Mip); otherwise raw data Like Part 1, only via Import (raw data)
cfile (68) decrypted; also unpacked if declen > 0; info line decrypted or decrypted and unpacked – Plaintext, or .raw = Payload as in Archive Text edit → re-encrypted under the same Resource Name before being read back
compfile (171) unpacked; info line unpacked – Unpacked content or .raw Text edit → Kraken repacked (like retail), read back
everything else (180,162) Text if tncview.as_text preserves it losslessly, otherwise Hex; Path Tab for Decls like Part 1 – Raw data Text edit → byte-exact return (Decl query like Part 1); binary → "Text Only"; Import takes raw data

New for "everything else": Kraken-compressed entries (Archive Mode 2 and 4) are now unpacked. Previously, selection reported only a read error for them.

Saving writes only to mods/<folder>/, and <folder> must be a simple name directly under mods/. A new mod gets "spiel": "tnc", an existing folder for Part 1 is rejected, and an existing, broken mod.json remains untouched. Saving and importing from a DLC archive are rejected with a reason because tncpatch only writes to base/.

Lazy loading: "Load" reads only the tables (0.6 s). Oodle is not loaded and no .texdb table is read until an entry is selected; this has been verified.

Numbers

  • Images, Sample: 400 random image entries from all 29 archives (Seed 2026), via the same path as the tab (tncview.payload → tncview.present).
    • 379 with preview (94.75 %). 21 instead show a reason, and all 21 are light samples: 18 BC6H, 2 BIM v13, 1 BIM v12.
    • Notes for the 379: 142 × "Tile chain skipped" (the preview falls to a smaller Mip), 21 × "TexDB without block" (fallback to the next Mip), 15 × "scaled down".
    • Time per selection for reading and decoding, warm cache: Median 0.03 s, p90 0.08 s, Maximum 0.48 s.
  • In the real widget:
    • BC7 2048² (woodboard_02, Preview Mip 2 512²): 0.64 s
    • DLC texture gunslinger_shirt (dlc_1): 0.17 s
    • strings/english.lang (2,049,240 bytes plaintext): 0.09 s
    • c2v1.entities (DLC, 8,329,537 characters): 0.17 s
  • All 239 cfile/compfile (68 + 171) in all 29 archives run the path unpack → Text → Tk-Text → back and come out byte-for-byte identical. 8 of them are not valid UTF-8 and are shown as latin-1, 67 end with NUL. Tk rejects NUL, so the end is retained and appended again when saving. 0 are binary, 0 errors.
  • Other TNC entries that look like text (looks_textual, all 29 archives without image/cfile/compfile): 49,086. Of these, as_text rejects 138 as binary; they no longer pass through the Text widget:
    • 93 destructionmodeldata, 28 json, 12 hknavmesh, 3 staticShadowGeom: Binary formats containing text with NUL. Tk cuts off at the first NUL. An unchanged "Save as Mod" wrote a 0-byte file for the json entries (Finding 2).
    • 2 particle with each one single control byte (\x01 or \x06): zerstorer_backpack_thrusters_explosion, ubergewehr/projectile_sphere. These now only go through Import.

Try it out

python -m wolfsdk gui

Select "Wolfenstein II" at the top, then open the Assets tab and press Load.

  • Type image, search woodboard_02 → Tab Image
  • Type cfile, search english.lang → Tab Text; the info line says "decrypted and unpacked"
  • Type compfile, search c2v1.entities → DLC entry, text starts with Version 5
  • Change text → Save as Mod → enter folder name → activate and apply in the Mods tab

Tests

python tools/verify_browser_tnc.py takes about 10 s with warm disk cache, up to 45 s cold; with --alle the run over all 239 cfile/compfile is added (+36 s). Exit 0 with "All good." (second run: 62 tests, with --alle 63, 0 ERRORS). The tool builds the real Browser with a hidden Tk window and clicks via tree selection.

  • TNC positive:

    • The loader lists 24 + 5 archives and decodes nothing.
    • BC7 and BC1 are converted into a single PhotoImage with neighbor correlation of 0.92 and 0.97, respectively, indicating image data rather than noise.
    • DLC image (dlc_1): Correlation 0.96.
    • english.lang: Text begins with BOM + // string table and is byte-identical to the decryption output.
    • Unprocessed storage → "Nothing changed."
    • Processed → The mod file is encrypted (ff ff ff ff…), opens under the same name as editor text, and mod.build_payloads contains it.
    • .cfg with trailing NUL: Round-Trip successful.
    • c2v1.entities: Version 5.
    • compfile processed → repacked, unpacked to editor text.
    • Export: Plaintext, .raw, and full-size PNG.
  • Negative controls:

    • Stored payload under a different name → HMAC error.
    • Intentional encryption under a false name → the browser rejects saving; nothing is written.
    • BC6H and BIM v13 display a reason instead of crashing.
    • 3-byte payload → "Image data corrupted (IndexError …)".
    • Random pixels are recognized as noise.
    • Tk truly rejects NUL.
    • Saving from DLC rejected; nothing written.
  • Part 1 (TNO), old vs new: Identical clicks on the current browser.py and the one from git HEAD (loaded via git show) leave identical widget states and identical files. Compared were info lines, text, paths, image pixels, status, "Save as Mod" (bytes from mod.json and Asset), and PNG bytes. Verified across 6 entries (3 images, weapon, damage, anim).

  • Mutation samples (in a scratch copy, not in the repo):

    1. Removal of read-back check in tncview.wrap → The exact check "Negative: packaged under false name → saving rejected" fails.
    2. Adding a space to the Part 1 info line → All 6 checks "Part 1 … identical with HEAD" fail.

tools/verify_gui.py before/after:

  • Before: Scratch mirror of the working state with browser.py from HEAD.
  • After: The repo.

Both end with Exit 0 and "All good.", and the outputs are character-identical (diff empty). Among other things, they contain: "All five work areas built", "Mods read in 19", "Title change loads another catalog 31 -> 2231 Cvars", "Mod list follows title 7 Mod(s) for tnc". Running the second pass again before and after corrections yields Exit 0 and character-identical output. config.json was backed up before and restored afterwards.

Examiner findings (second pass)

Each check is located in check_findings or at the end of check_tno. The "before" column shows the output on the code prior to correction.

# Finding Issue Previous Behavior Correction
1 Importing to cfile/compfile blindly repacks everything that wasn't already unpacked .raw from english.lang to default_sp.cfg; custom packed .raw from english.lang without Oodle (simulated) Both FAILED: Mod written (Garbage or double-encrypted) tncview.for_import distinguishes: saved form exactly this entry → kept; readable text → gets repacked; everything else (foreign resource name, missing Oodle/AES, other binary data) → "Import failed" with reason
2 Unpacked TNC entries ran through looks_textual and the Text widget all 30 json in base/ saved unprocessed FAILED, 16 of them wrote a Mod (0 Bytes) All TNC entries saved via _save_tnc: only what as_text accepts and Tk holds exactly unchanged → "Nothing changed.", binary → "Only text … Importing"
3 Folder name unchecked when attached to mods_dir absolute path, ..\x, ../x, a/b, CON, nul.txt, vb. all 7 FAILED, written outside of mods/ or mods\CON, mods\vb only a simple name (no separators, no drive letter, no .., no dot/space at end, no device name); additionally Path.resolve(): target must lie directly under mods/
4 Filename without type, two assets shared a file material and skins models/animals/bessie/bessie into one Mod FAILED, both keys → assets/models_animals_bessie_bessie The first file keeps its old name. If another asset of the same mod uses the file already, it gets ~2, ~3 … (Comparison like Windows: case insensitive). The same key takes its file back on re-save. Existing mods remain valid.
5 Traceback in dialog broken JSON, JSON list, assets as list, filename > 255 4 × FAILED with "Traceback (most recent call last)" mod.json is read before every write. Broken → "mod.json not readable", it remains unchanged, nothing is written. Write errors → "Save failed" with reason and file, for > 255 characters with exactly this hint.
6 OSError/MemoryError from Oodle compressor landed in Tk Oodle.compress raises OSError or MemoryError, during save and during import 3 × FAILED "… escapes" both paths catch TncCryptError, OSError, MemoryError → dialog "… Nothing saved."; the packing happens before the first write attempt
7 "Load" also reset selection for Part 1 TNO: Selection, Load, then Export and Import against HEAD FAILED "Export False, Import 0/2 files" Selection is only discarded on game switch, then with preview (own check "Game switch on load"). Within a game it remains as in HEAD.

Additionally new, as protection against regression: Import of own .raw remains byte-identical; imported plaintext is encrypted under default_sp.cfg; a TNC-Decl (projectile_ent/bottle_throw) is recognized and processed unchanged byte-for-byte saved; no message of the whole run contains "Traceback".

Behavioral changes in Part 1 (TNO) – only in _write_mod, which both parts use (Findings 3–5), everything else like HEAD:

  1. A folder name that is not a simple name is rejected. HEAD wrote there, also outside of mods/.
  2. If another asset of the same mod already uses the flat filename, the new file gets …~2. HEAD silently overwrote the file. Without such collisions, files and mod.json are byte-identical to HEAD, which is verified.
  3. An existing mod.json that is not a JSON object with an assets object is reported before anything is written. HEAD wrote the asset first and then showed a Traceback.
  4. Write errors show a German message with reason and file instead of a Traceback.

Not changed, although related (Part 1 should remain like HEAD):

  • Part 1 continues with looks_textual. In TNO, 60,411 non-image entries are considered textual; 15 of them (e.g., model, such as maps/game/wolf/c01/c01p1/_combo/_world.bmodel) contain a NUL byte in the middle of their content. Tk truncates there. Saving "As Mod" on top of that would, like finding 2, only write the beginning.

  • Placing a TNO asset into a TNC mod folder is assumed as in HEAD. Only Part 2 checks spiel.

Changes in this Pass

  • tncview.wrap reads every newly wrapped cfile/compfile back with unwrap and outputs it only if exactly the editor text comes out. The reason: The module assetcrypt is considered untrustworthy in finding 2 (7/8). Therefore, an error there cannot create a mod that the game does not open. Import runs via the same path.
  • Non-TncImageErrors from tncimage now read as "Image data corrupted (Type: …)" instead of just with the bare Python type.
  • PNG Export: The status line names the exported Mip. A tile chain can push the export below the original size, and that should be visible.

Limits

  • Light probes without image: BC6H as well as BIM v10/v12/v13 show a reason. In the sample set, these were 5.25% of all images.
  • Tile chains (Codec 2, those ≥ 512-px Mips of most BC4/BC5 Maps) are not decoded. Preview and PNG Export take the next Mip; a 2048 Normal Map comes out as 256² in this way.
  • "TexDB without Block": There are textures whose large Mips have no .texdb. They fall back to a smaller Mip, in the extreme case 32², and that is displayed.
  • No PNG Import: Importing takes raw data (.bimage), as in Part 1.
  • "Export all hits" writes the payload behind the archive compression, i.e., cfile/compfile in their stored form, not decrypted. This fits what importing expects.
  • Not tested in game (the game was never started):
    • A saved cfile is always unpacked (declen = 0xFFFFFFFF). The jle in the reader after that is allowed; loading a file like this the game has not done yet.
    • If a payload does not fit into its slot, tncpatch packs the entry at archive level with Kraken (compression 0 → 2).
  • DLC only read: Assets that lie only in a DLC cannot be modded via the tab.
  • Windows only: tnccrypt uses bcrypt.dll and finds Oodle via Game.find(title='tnc'). If that fails, it shows the info line "not readable: …" and the text tab Hex; it does not crash.
  • Corrupted compfiles do not fall back: Kraken frames have no CRC. Retail does not affect this, but a broken user DLC can show nonsense as text.
  • Foreign exceptions from tncimage:
    • IndexError at b"BIM"
    • ValueError at a .texdb offset behind the file end
    • KeyError in decode at an unknown format

tncview.present catches all of them. The actual correction belongs in tncimage.py. The first two I could fix in the second pass; it is not needed because present already catches them. Therefore I did not touch tncimage.py.

  • Text that Tk does not losslessly hold blocks saving. In the 239 cfile/compfiles, this never happened.
  • Binary entries and text with control characters (see "Numbers", 138 entries) can only be replaced via importing.
  • Import on cfile/compfile takes only the stored form of exactly this entry or text without control characters. The unpacked content of a binary cfile/compfile would not work; retail has none.
  • Asset names whose flat filename is longer than 255 characters (338 animEvent in TNC) cannot be saved into a mod. A message comes that says so.
  • No atomic writing: If writing fails halfway through a file (disk full), the asset file can be half-written. mod.json is written last. The behavior is the same as in HEAD.

Notes for the Master

  • tools/verify_gui.py writes config.json. The title change test calls _resolve_game → Game.remember() → save_config; only the check_play stub is executed. After execution, game_path pointed to TNO instead of TNC. I restored config.json from HEAD; that was the state at session start. The correction belongs to the owner of verify_gui.py. Until then, secure config.json before execution.
  • The installation now has dlc_0 and dlc_5. Both are test DLCs from other agents; dlc_4 no longer exists. Both are also listed as read-only. The check requires only that dlc_1..3 be present.
  • tools/probe_tno_tex.py contains NUL bytes. The file originates from another, aborted workflow and has not been touched. The compileall in verify_all fails on it, independently of browser.py. python -m compileall -q wolfsdk tools/verify_browser_tnc.py runs cleanly.

Files

  • wolfsdk/tncview.py (new):
    • dlc_archives, read_only, where – Archives
    • payload – Archive compression including mode 4, ported from tools/extract_tnc.py
    • unwrap / wrap / for_import – the second wrapper
    • as_text / from_text – text without loss
    • present – everything for a selection
  • wolfsdk/browser.py: Part-2 branch in Load, Select, Export, All-export, Import, and Save-as-mod. The Part-1 code runs unchanged, except the shared _write_mod (Findings 3–5, see "Behavioral Changes in Part 1").
  • tools/verify_browser_tnc.py (new; in the second pass, check_findings and checks after reload were added)