Wolfenstein Modloader Buy me a coffeeKaffee spendieren

In plain wordsIn einfachen Worten

This page is a technical reference for the file formats of Wolfenstein: The New Order, checked against all 14 archives and 203,843 assets. It explains how the game lists its archives, how each index points to data, and how the text definitions, BIM images, audio and virtual textures are stored. It also explains how the mod tool patches files safely: new data goes into the existing slot, because the game crashes if entries are simply appended. Some parts, like the tail of each index entry and the virtual texture pixels, are still not understood.

  • The index files have no checksum, so in-place patching is not detected by the game.
  • Appending data to the end crashed the game ('FileTable is out of order'); data must stay in its slot or the archive is rebuilt.
  • Editing and writing back 60,233 text definitions reproduces the original bytes exactly.
  • BIM images can be read in all 4 format codes, but only uncompressed RGBA8 images can be written back.
  • Virtual texture pixels cannot be replaced yet; their image data is not decoded.

Diese Seite ist eine technische Referenz der Dateiformate von Wolfenstein: The New Order, geprüft an allen 14 Archiven und 203.843 Assets. Sie erklärt, wie das Spiel seine Archive auflistet, wie jeder Index auf die Daten zeigt und wie Text-Definitionen, BIM-Bilder, Audio und Virtual Textures gespeichert sind. Außerdem beschreibt sie, wie das Mod-Tool Dateien sicher patcht: Neue Daten kommen in den vorhandenen Platz, weil das Spiel abstürzt, wenn man sie einfach anhängt. Manches, etwa das Ende jedes Indexeintrags und die Pixel der Virtual Textures, ist noch nicht verstanden.

  • Die Indexdateien haben keine Prüfsumme; direktes Patchen bemerkt das Spiel daher nicht.
  • Daten ans Ende anzuhängen ließ das Spiel abstürzen ('FileTable is out of order'); sie müssen im alten Platz bleiben oder das Archiv wird neu gebaut.
  • 60.233 Textdefinitionen lassen sich bearbeiten und ergeben unverändert zurückgeschrieben exakt die Originalbytes.
  • BIM-Bilder sind in allen 4 Formatcodes lesbar, zurückschreiben lassen sich nur unkomprimierte RGBA8-Bilder.
  • Pixel der Virtual Textures lassen sich noch nicht ersetzen; ihre Bilddaten sind nicht entschlüsselt.

Technical detailsTechnische Details

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

All of this information comes from the reverse engineering of the retail files for Wolfenstein: The New Order (Steam, WolfNewOrder_x64.exe, 26,345,040 bytes). Every statement has been verified against all 14 archives and 203,843 assets unless explicitly marked as unverified.


1. base/master.index

The root: a list of archive pairs.

u8[4]    Magic  03 53 45 52   ("\x03SER")
BE u32   Number of pairs      (Retail: 14)
repeated 2 x Number:
  LE u32  Length
  char[]  Name, ASCII, null-terminated

The names alternate: chunk0.index, chunk0.resources, chunk1.index, … The file is 520 bytes in the retail version, which fits exactly: 8 bytes header + 10 x 36 + 4 x 38.

Mixed Endianness. The count is in Big Endian, while string lengths are in Little Endian. This is not a reading error; it applies consistently across all .index files.


2. base/chunkN.index

Table of contents for chunkN.resources.

u8[4]    Magic  03 53 45 52
BE u32   Size of payload data (file size minus 32)
u8[24]   Null bytes
BE u32   Counter A          (chunk0: 12220)
BE u32   Counter B          (chunk0: 15069, approximately equal to entry count)
BE u32   0
--- entries begin at offset 0x2C ---

An entry:

LE u32 + char[]   Type      e.g. "weapon", "image", "material"
LE u32 + char[]   Name      e.g. "weapon/shotgun_base"
LE u32 + char[]   Path      e.g. "generated/decls/weapon/weapon/shotgun_base.decl"
BE u32            Offset    into the associated .resources file
BE u32            usize     Uncompressed size
BE u32            csize     Size on disk
u8[?]             Rest      variable, mostly null bytes

The trailing data at the end of an entry is not fully understood. For most types, it is 13 bytes (9 null bytes plus a sequential BE-u32 ID); for audio types like sample and sound, it is larger. Precisely because of this, this SDK serializes the index never: it keeps the original buffer and overwrites only the 12 bytes of offset/usize/csize during patching. All unknown data survives unchanged, verified by the fact that an unmodified index is written byte-for-byte back.

To determine entry boundaries, the parser uses a plausibility check rather than fixed step size: three printable ASCII strings, followed by 0 < csize <= usize and offset + csize <= file_size. This maintains synchronization across the variable trailing data.

There is no checksum field. This is why patching in-place is safe and undetectable by the game.


3. base/chunkN.resources

Raw data blob, addressed via the index.

u8[4]    Magic  03 53 45 52
u8[12]   Null bytes
--- payload follows at offset 16, tightly packed ---

Per entry:

  • csize == usize → the bytes are stored uncompressed.
  • otherwise → raw DEFLATE (zlib.decompressobj(-15), no zlib header).

The game itself stores verbatim whenever compression yields nothing; this SDK does the same.

Patching

New data is written into the slot already occupied by the entry. Offset and csize remain unchanged; only usize moves. If the new deflate stream is shorter than csize, the input is padded with a Decl comment until the stream is exactly csize bytes long (resources.deflate_exact()); raw entries (csize == usize) accept only an exact replacement of equal length.

Appending is incorrect. An earlier version of the patcher appended new data to the end of the file and moved the index there. This passed every byte test but still caused the game to crash on startup: FATAL ERROR: idBackgroundLoader::BeginBackgroundLoads: FileTable is out of order. The engine requires ascending offsets. Similarly, csize must not shrink — otherwise subsequent entries shift (No address, error: 487). Both are measured; see limits.md §4.

If a replacement does not fit into its slot, the entire archive is rewritten in original order (Archive.rebuild()). Only then do offsets change — and they all change together, preserving the order.

Reversion therefore occurs in three stages, in this order (patch.revert()):

  1. Replay fully copied .resources files from base/_wolfsdk_backup/ — and delete the copy afterward.
  2. Rewrite individually overwritten slots from the Journal (the original bytes are base64-encoded in journal.json).
  3. Restore the backed-up .index files.

A .resources file is never truncated: its length does not change due to patching at all.


4. Decls

Text format, available in two dialects that coexist within the same file tree:

// weapon, entityDef, damage, inventoryItem
{
    edit = {
        maxDamage = 25;
        validAmmoClips = { num = 1; item[0] = { clipSize = 20; } }
    }
}

// material, md6Def, skins
{
    diffusemap  "models/weapons/Handgun/Handgun"
    virtualmapping { 0.015625, 0.015625, 0.078125, 0.031250 }
    offset ( 0 0 0 )
    prop "_info" { tag "muzzle" { trans ( 0 0 0 ) } }
}

Features covered by the parser:

  • = and ; are optional
  • Values: String, Word, Block {…}, Tuple (…)
  • Comments // and /* */
  • Qualified blocks: prop "_info" { … } → path prop:_info
  • Indexed fields: item[0] remains as item[0] part of the path
  • inherit = "…" refers to a base Decl (inheritance)

Addressing

Paths are dot-separated:

edit.damageParms.maxDamage
edit.validAmmoClips.item[0].clipSize
props.prop:_info.tag:muzzle.trans

Duplicate keys on the same level receive a suffix #1, #2, … starting from the second occurrence.

python -m wolfsdk paths <type:name> lists all paths of a Decl.

Editing

The editor remembers the byte span for each value and replaces only this excerpt when writing. Nothing is reformatted or re-serialized. Verified against 60,233 Decls: parsing and unchanged reversion yields exactly the input.

Counting Basis. 60,233 are index entries in text types, counting multiple occurrences of the same asset in several chunks. When reduced to unique (Type, Name) pairs, there are 25,887, of which 25,559 are valid UTF-8 and parse correctly; the 328 outliers are 327 embedded binary files under file: and one renderProg with non-UTF-8 bytes (TNO-Modding-Surface.md header). Both numbers match; they count different things — "0 parse errors" and "328 outliers" are not contradictory.


5. BIM Images (image)

LE u32   Hash / ID            (Purpose unconfirmed)
u8[4]    Magic  09 4D 49 42   ("BIM" + Version 9, byte-swapped)
BE u32 x 13   Header fields
--- then per Mip level: ---
BE u32   Width
BE u32   Height
BE u32   Byte count
u8[]     Data

Known header fields (index starting at 0 after Magic):

# Meaning
0 Texture type (0 = 2D, 2 = Cubemap face)
1 Width
2 Height
3 Depth
4 Mip count
6 Format code
11, 12 Base width, base height

Format codes, derived from bytes per pixel across all 2,566 images — each one matches exactly:

Code Format Bytes
3 RGBA8 4 / Pixel
5 A8 / Distance Field 1 / Pixel
10 BC1 / DXT1 8 / 4x4 block
11 BC3 / DXT5 16 / 4x4 block

python -m wolfsdk image-info <name> provides all this information for an image.

wolfsdk/bim.py models the header, wolfsdk/image.py the pixels — in both directions:

Direction Function Status
BIM → RGBA decode(), decode_preview() all four format codes, measured against 2,566 images
RGBA → PNG to_png() Standard library, zlib
PNG → RGBA from_png() including filter removal
RGBA → BIM to_bim(), png_to_bim() only format code 3 (RGBA8), header taken from the replaced image

About the command line: wolfsdk image-export and wolfsdk image-import.

Two limitations remain: There is no BC1/BC3 compressor, so a substitute is typically larger than the original and forces an archive rebuild; and whether a substituted image looks correct in-game is not verified — it is only documented that the engine successfully loaded four self-built BIMs without error messages (limits.md §4). For weapon and character surfaces, this is not the lever: they reside in the Megatexture, see limits.md §5.


6. Audio

streamed.resources and english.streamed are concatenated Ogg Vorbis streams (OggS-Magic immediately at the file start). Both containers are not in master.index.

The .index entries for types sample, sound, and voicetrack contain no audio data themselves — they are descriptors pointing to it: 20 bytes after the offset/usize/csize triple stands as a BE u32 representing the byte offset into the container. Verified across all 61,318 sample entries by traversing the Ogg page chain up to the EOS page and comparing against the length in the descriptor: 0 deviations. The slots lie on a 16384-byte grid, non-overlapping.

Anyone holding sample for the audio bytes is searching in the wrong container — and overlooking that streamed.resources and english.streamed are not in the backup. Details in audio.md §3 and §6.


7. Virtual Textures

virtualtextures/*.pages, 39 files, totaling 35.6 GB (33.2 GiB; measured on 21.09.2026). Materials with usevirtualmapping 1 derive their pixels from there, not from image assets. The field virtualmapping { u, v, du, dv } in the material represents the coordinates within the pages.

The container is decrypted and verified — file header (32 bytes, all seven fields), offset scale, set header (40 bytes), page payload, quadtree derivation, self-test over seven fully traversed files with no unassigned bytes. The complete description is in virtualtextures.md.

The only unresolved issue is the pixel data (a cropped JPEG-XR stream, TNO-Virtual-Textures.md §8). Therefore, it remains: Virtual texture pixels are not replaceable.


8. Cvars and Start Arguments

The engine accepts Cvar assignments directly on the command line:

WolfNewOrder_x64.exe +g_fov 105 +bind F1 "toggle g_permaGodMode 0 1"

This confirms that the game-specific base/build.cfg uses exactly this syntax:

+feature_hq 1 +feature_lowgore 0 +sys_region "ww" +g_EnableGore 1 ...

The Retail EXE contains around 2,000 Cvar names, including preserved developer cvars (g_permaGodMode, g_permaInfiniteAmmo, com_allowConsole, devgui) and the commands teleport and devmap. noclip, god, notarget, give and spawn do not exist in The New Order (the earlier string-based list was wrong); the registry has 3,194 cvars, see TNO-CVar-Catalog.

The strings lie in the .rdata pool roughly as Description \0 Default \0 Name \0, but the linker frequently deduplicates common values like "0" and "1", making it impossible to reliably assign Defaults. The names, however, are fine. There is no static registration table — MSVC passes the pointers RIP-relative in the code.