Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Research reportsForschungsberichte

dlc_4 Map plus cfile:wolfsdk_test.cfg

Partly solvedTeilweise gelöst Original page in the GitHub wikiOriginalseite im GitHub-Wiki

In plain wordsIn einfachen Worten

The plan for Wolfenstein II: The New Colossus was to put a custom encrypted command file into a replacement map archive in the dlc_4 folder. The archive was built and passes 21 of 21 checks. However, the game's code shows that the DLC folder is chosen by the map name, and the first match wins. So for the normal map the game would mount the original dlc_2 archive, not the custom one. This comes from static code analysis, not from a test in the game.

  • The built archive has 7,129 entries; all 7,128 old entries stay byte-identical apart from their position.
  • The new file decrypts correctly from inside the built archive; 7 of 7 negative controls fail as intended.
  • The engine searches the base game first, then the DLC list in order (dlc_1 to dlc_4), and the first match wins.
  • The custom archive would only be mounted when loading the custom map name c2v1_custom.
  • Untested idea: renaming the folder dlc_4 to dlc_0 might put it first; possible sound and ownership check risks are noted.

Der Plan für Wolfenstein II: The New Colossus war, eine eigene verschlüsselte Befehlsdatei in ein Ersatz-Kartenarchiv im Ordner dlc_4 zu legen. Das Archiv ist gebaut und besteht 21 von 21 Prüfungen. Der Spielcode zeigt aber, dass der DLC-Ordner über den Kartennamen gewählt wird und der erste Treffer gewinnt. Für die normale Karte würde das Spiel also das Original-Archiv aus dlc_2 einbinden, nicht das eigene. Das stammt aus statischer Codeanalyse, nicht aus einem Test im Spiel.

  • Das gebaute Archiv hat 7.129 Einträge; alle 7.128 alten Einträge bleiben bis auf ihre Position byte-identisch.
  • Die neue Datei entschlüsselt korrekt aus dem gebauten Archiv heraus; 7 von 7 Gegenproben schlagen wie gewollt fehl.
  • Die Engine sucht zuerst im Hauptspiel, dann der Reihe nach in der DLC-Liste (dlc_1 bis dlc_4); der erste Treffer gewinnt.
  • Das eigene Archiv würde nur beim Laden des eigenen Kartennamens c2v1_custom eingebunden.
  • Ungetestete Idee: Den Ordner dlc_4 in dlc_0 umzubenennen, könnte ihn nach vorne bringen; mögliche Risiken bei Sound und Besitzprüfung sind notiert.

Technical detailsTechnische Details

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

Route cfileindlc4, 2026-09-23. New: tools/build_dlc4_with_cfile.py, re_probes/dlc4_cfile/chunk_22.resources. Nothing installed, the game was not started, nothing written under <Steam library> or Saved Games (only read there).

Result in two sentences

The container is built and passes 21/21 checks. However, the idea behind it does not hold: The Exe selects the DLC directory via the map name, not via the chunk ID. When loading the retail game state game/dlc/c02/c2v1, it mounts ../dlc//dlc_2/base/chunk_22.resources. This also applies with today's dlc_4 spec (18:35), which lists c2v1 as well, because dlc_2 appears before dlc_4 in the DLC list. Our chunk_22 is only mounted when loading c2v1_custom. This finding stems from static analysis of the running Exe and was not measured within the game itself.

1. The Container

Source re_probes/entitypipe/base/chunk_22.resources, 7128 entries, sha256 a8da9e4e… (= installed dlc_4)
Target re_probes/dlc4_cfile/chunk_22.resources, 7129 entries, 151 703 648 bytes, sha256 cb3d8bac6fa574173212cc681f75c3043d1a8bc7d925497c4db93f493c807f03
New Entry Last table position, cfile:wolfsdk_test.cfg, comp 0, csize = usize = 96, 0 dependencies, payload byte-identical to re_probes/cfilekey/wolfsdk_test.cfg.cfile

Nothing new was written for the build. It is appended using tools/build_tnc_map_alias_v3.write_archive, the function that previously attached 143 alias entries to this container. The line template is the entry from the exe carrier. Afterwards, wolfsdk/idclwrite.finalize runs. Nothing needed recalculation (data_hash 0, hash_0x60 0 changed), so the hashes of write_archive were already correct. The string table grows at the back by cfile and wolfsdk_test.cfg, and the string index pool by 2 entries. All old indices remain valid; therefore, nothing changes in any old entry except the offset.

Running python tools/build_dlc4_with_cfile.py --pruefen yields 21/21:

  • 7129 entries on re-read with wolfsdk/idcl.py.
  • 7128 old table lines are byte-identical except for +0x38 (offset). The check compared all 144 bytes, including +0x50/+0x60, versions, flags, and run indices.
  • 7128 payloads are byte-identical. Dependency records (1943) and Dep-index pool are byte-identical. The old string index pool, the old 8821 strings, and their raw bytes are each prefixes of the new ones.
  • No payload overlaps another, and the last one ends at the file end. The 5 empty cm entries share an offset like in Retail. My first, stricter check for unique offsets therefore failed and was corrected.
  • The new cfile decrypts from the built container to b'map game/dlc/c02/c2v1_custom\n'. With base/ names or 16 appended filler bytes, the tag fails.
  • probe_datahash: +0x50 7128/7128 (1 entry with compression mode 4 skipped, same as in source), rc 0. probe_idclhash: +0x08 1/1 +0x20 1/1, rc 0.

The control runs with --negativ on a disposable copy in the Temp directory. There, a ciphertext byte and the header byte 0x18 are flipped. Afterwards, probe_datahash, probe_idclhash, and pruefe() must fail. Additionally, zeilen_diff must see changes at +0x50, string_index, and compression, and report nothing on the offset. See §4 for the result.

2. Which chunk_22 is mounted

The check runs with python tools/build_dlc4_with_cfile.py --vorrang (35/35). It checks 29 instructions and 4 strings at fixed RVAs in re_probes/runtime_87400/module.bin. The control run with addresses shifted by about 1 byte hits none of them.

  • LoadNamedResourceBlock (0xac1730): r13 holds the map name, populated by the format argument of chunk missing in LoadNamedResourceBlock for map %s. The execution flow is as follows:
    • 0xabf440(MapName) retrieves the Spec entry; the chunk ID resides at [r15+0x30].
    • 0xabf500(MapName) retrieves the directory prefix.
    • 0xabd940(chunkID) constructs chunk_22.resources without a directory path.
    • 0x1211e70 combines the prefix and name, then mounts at 0xacc6b0.

The same logic applies to .texdb. The RefChunks (chunk_9, chunk_13) and chunkbase_6 are mounted without a prefix from base/. * 0xabf500 and 0xabf440: Both first search in base/packagemapspec.json (this+8), then in the DLC list (this+0x30, 160 bytes per entry, MapSpec at +0x138). The first match wins (jne exits the loop). The prefix is "%s/%s" % (DLC-Path, "base/"). The chunk ID is never compared across DLCs. Therefore, the question "dlc_2 or dlc_4 with the same chunk ID 22" is not posed to the engine at all. * Order of the DLC List: SearchDLC (0xabdcf0) skips an already existing dlcid (the first wins), otherwise it hangs (0xabe2c6) and logs immediately after found dlc id %d at %s (0xabe503). Thus, the log order corresponds to the list order. Measured in qconsole.loadsave.log: dlc_1, dlc_2, dlc_3, dlc_4. * Simulated against installed Specs (read-only): c2v1 -> ../dlc//dlc_2/base/chunk_22.resources, c2v1_custom -> ../dlc//dlc_4/base/chunk_22.resources. * Consistent with today's measurement at 18:27: The startup routine 0xabdaa0 mounts only gameresources and Patches. DLC Chunks come exclusively via LoadNamedResourceBlock. Therefore, resourceExec finds nothing in the main menu. * packagechunklist.json is read at 0xb76330 (chunks/chunkid/packagechunks). This lies in the neighborhood of idStreamingInstallManager, so the file is UNLOCKED as a Streaming-Install Manifest and not a Mount List. The warning is irrelevant for this question.

Summary: dlc_primary.md and map_selection.md held precedence for equal names as "statically unproven". For the directory path, it is now statically proven: first match in order base, then DLC list. Only what a runtime measurement would show remains open. A contradiction in the game would imply this interpretation is false.

3. Implications

  • The planned path (Loading Retail-Save c2v1, then resourceExec wolfsdk_test.cfg) again predicts can't generate in production because dlc_2/chunk_22 is mounted. This is a cheap falsification test of this analysis.
  • Our container is mounted when c2v1_custom loads. Then the cfile is superfluous because one is already in the map. It only proves that a custom cfile in the game is resolvable.
  • Leverage without Retail write access (not tested): Rename the folder dlc/dlc_4 to dlc/dlc_0. The dlcid 4 remains, and the Spec already lists c2v1. If the directory enumeration is alphabetical (the measured order 1–4 fits this, but it has not been proven), then dlc_0 precedes dlc_2. Then for the Retail-Save, our chunk_22 will be mounted. It contains all 6985 c2v1 retail payloads byte-identically. Risks:
    • The same prefix goes to a virtual call at 0xac1a47..0xac1a8d. It is SUSPECTED that this is the DLC Sound Mount, and dlc_4 has no sound/ folder.
    • An entitlement check for DLC 2 for the Save cannot be ruled out.

Log evidence that the lever works: found dlc id 4 at ../dlc//dlc_0 appears before found dlc id 1.

4. Counter-controls (--negativ)

7/7, rc 0:

Manipulation Result
zeilen_diff: one bit in +0x50, string_index, and compression all three detected
zeilen_diff: a bit in the offset ignored, as intended
Ciphertext byte and header byte 0x18 flipped (copy) probe_datahash 7127/7128 rc 1, probe_idclhash +0x08 0/1 rc 1
pruefe() on the same copy 17/21: Payload, Decryption (HMAC-Tag incorrect), and both checks fail

Limitation: The equality checks of the old entries were only tested against manipulation (zeilen_diff) in memory, not with a second damaged 150-MB copy.

Reproduction

python tools/build_dlc4_with_cfile.py            # build + 21 checks (~2.5 min)
python tools/build_dlc4_with_cfile.py --pruefen  # check only
python tools/build_dlc4_with_cfile.py --negativ  # negative controls, copy in temp directory
python tools/build_dlc4_with_cfile.py --vorrang # Mount rule on module.bin, 35 checks