Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Game systemsSpielsysteme

Do patch_*.resources take precedence? — 670 MB against 300 KB

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

In plain wordsIn einfachen Worten

In Wolfenstein II (TNC), about 1,700 game files exist twice: in a base archive and in a later patch archive. This page asks which copy the game uses, because that decides whether a mod needs only a small extra archive or a rebuild of a 670 MB file. Everything measurable without starting the game points to the patch copy winning, but this is still not measured in the running game. Since WolfSDK 0.2.0 the loader reads the newest patch copy and writes its result into every copy, so mods no longer depend on the answer.

  • 1,013 files exist only in patch archives, so patch archives are definitely loaded.
  • In 1,680 duplicate pairs the patch copy is newer 1,635 times and never older.
  • The game program knows a container type named PATCH and a field named patchPriority.
  • Before 0.2.0 the loader read the older base copy and wrote it into every copy, so a mod could undo official patch changes; now it reads the newest patch copy.
  • Decisive test, still not run: change the throwing-knife limit in both copies and read the number in the HUD.

In Wolfenstein II (TNC) liegen rund 1.700 Spieldateien doppelt vor: in einem Basis-Archiv und in einem späteren Patch-Archiv. Die Seite fragt, welche Kopie das Spiel nutzt, denn davon hängt ab, ob ein Mod nur ein kleines Zusatzarchiv braucht oder eine 670 MB große Datei neu gebaut werden muss. Alles, was sich ohne Spielstart messen lässt, spricht für die Patch-Kopie, im laufenden Spiel gemessen ist es aber weiterhin nicht. Seit WolfSDK 0.2.0 liest der Loader die neueste Patch-Kopie und schreibt sein Ergebnis in jede Kopie, Mods hängen also nicht mehr von der Antwort ab.

  • 1.013 Dateien gibt es nur in Patch-Archiven, also werden Patch-Archive sicher geladen.
  • Bei 1.680 doppelten Paaren ist die Patch-Kopie 1.635 Mal neuer und nie älter.
  • Das Spielprogramm kennt einen Container-Typ namens PATCH und ein Feld namens patchPriority.
  • Vor 0.2.0 las der Loader die ältere Basis-Kopie und schrieb sie in jede Kopie, ein Mod konnte so offizielle Patch-Änderungen rückgängig machen; jetzt liest er die neueste Patch-Kopie.
  • Entscheidender Test, noch nicht ausgeführt: das Wurfmesser-Limit in beiden Kopien ändern und die Zahl im HUD ablesen.

Technical detailsTechnische Details

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

The question determines the cost of a mod for Wolfenstein II. If the Patch copy wins, only a small custom archive is needed. If it does not win, every change requires rebuilding gameresources.resources (670 MiB) and a backup of 3.4 GiB.

All findings here are extracted read-only from the unmodified Retail installation at <Steam library>\steamapps\common\Wolfenstein.II.The.New.Colossus. Reproducible with:

python tools/probe_patchprec.py --spec         # packagemapspec.json, base + dlc
python tools/probe_patchprec.py --dupes        # the 1 701 duplicated pairs
python tools/probe_patchprec.py --diff 10      # byte-wise comparison examples
python tools/probe_patchprec.py --candidates   # pairs where precedence can be measured
python tools/probe_patchprec.py --strings --window 1426cf000-1426d1000

Each statement carries a grade: MEASURED (read from files), DEDUCED (inferred from structure, not directly observed), or SPECULATED (plausible but unsecured).


0. Answer with Evidence Grade

Short: Anything measurable without starting the game supports Patch archive precedence, and no single finding argues against it. It is not proven — precedence is runtime behavior. The evidence grade ranges from "strong" to "overwhelming," but not to "proven." The final piece costs one game start and two changed bytes, see Section 6.

# Finding Grade
1 1 013 (Type,Name) pairs exist exclusively in Patch archives — including 287 image, 265 material, 6 perks, 5 upgrade, 4 table. They are nowhere else. MEASURED
2 In 1 680 Base/Patch collisions, the Patch copy is 1 635 times younger, 45 times same age, 0 times older. MEASURED
3 The Exe knows an enumeration type idResourceStorage::containerType_t with exactly three values: MAIN, MAP, PATCH. MEASURED
4 The Exe knows a struct field named patchPriority, immediately next to containerFileName, resourceName, containerIndex, dataOffset. MEASURED
5 A rollback case: geistidentity:geist/identity/elite.identity lies in gameresources with hash 7f78958e…, is modified in patch_2 and patch_3, and set exactly to the Base hash in patch_4. A higher layer takes back from a lower one. MEASURED
6 The contents of Patch copies are consistently corrections: fields added, broken references fixed, numbers rounded — never regressions. MEASURED
7 patchlevel is a search upper bound, not a guarantee of existing files: the three DLCs report patchlevel 2 or 3 and deliver null patch_*.resources. MEASURED
8 Therefore: the Engine searches Patch layers from bottom to top, skips missing ones, and the highest present one wins. DEDUCED
9 A custom patch_5 is only reachable if patchlevel in packagemapspec.json is increased to 5. DEDUCED

Finding 1 alone already proves that Patch archives are mounted and searched — otherwise the game would be missing 287 images and 265 materials. Only the question of direction on collision remains open. Findings 2–6 support this.

Update 2026-10-04. Still not measured at runtime. Since WolfSDK 0.2.0 the loader reads the newest patch copy and writes every copy, so mods no longer depend on the answer: sections 8 and 9.


1. packagemapspec.json — fully evaluated

Clear-text JSON next to the archives. The Engine reads it; the key names appear as literals in the Exe, close together — that is the parser block:

0x1426d0228  refchunks
0x1426d0238  chunkid
0x1426d0240  basechunkid
0x1426d0250  packagemapspec.json
0x1426d0268  patchinfo

MEASURED, four files:

Spec Maps patchlevel layer chunkid basechunkid refchunks patch_*.resources alongside
base/ 31 4 0 (9×), 1 (22×) 0,1,3,4,5,7–16 0, 6 — 8
dlc/dlc_1/base/ 4 2 −1 20 6 9,12,13 0
dlc/dlc_2/base/ 3 3 −1 22 6 9,13 0
dlc/dlc_3/base/ 3 3 −1 24 6 — 0

Meaning of the Fields

Field Meaning Grade
name Map name, as loaded by the game MEASURED (matches base/maps/…)
chunkid In which chunk_<id>.resources/.texdb the map resides MEASURED (1,3,4,5,7–16 ↔ exactly the existing files)
basechunkid Shared base archive of the map: 0 = none (only gameresources), 6 = chunkbase_6 MEASURED
isbasechunk Only common and init, both chunkid 0 MEASURED
layer 0 or 1 in the main game, −1 in DLC. Matches 1:1 with basechunkid: layer 0 ⇔ basechunkid 0, layer 1 ⇔ basechunkid 6, without exception across all 31 maps MEASURED
refchunks Only in DLC Specs: additional chunks to load MEASURED
patchinfo.patchlevel How many patch layers are searched UNREVEALED, see below

layer is a runtime concept, not a pure ordering marker. The Exe carries the Cvar resource_ignoreLayers (0x1426cf068) and, alongside it in the same string block, the description "ignore layer bits and load all layers during map loads" (0x1426cee38). Thus there are Layer-Bits that decide which packages to mount when loading a map — and a switch to ignore them. This fits com_forceAllMapLayersToLoad (0x1426c41f0). (The Name↔Description mapping is unproven due to MSVC string pooling; the two strings lie 0x30 apart in the same block. UNREVEALED.)

patchlevel — Search Limit, Not File List

This is the sharpest single finding of this section. base/ reports patchlevel 4 and has exactly eight matching files: patch_1..patch_4 and patch_1_chunkbase_6..patch_4_chunkbase_6. Alone, this would be ambiguous.

The DLCs resolve the ambiguity: dlc_1 reports patchlevel 2, dlc_2 and dlc_3 report patchlevel 3 — and in none of the three base folders does a single patch_*.resources exist. There is only chunk_20/chunk_22/chunk_24. The three DLCs function correctly in the Retail game.

UNREVEALED: The engine loops n = 1 … patchlevel, attempts to open each layer, and skips missing ones silently. patchlevel is an upper limit for the search. A missing layer is not an error — precisely the property that makes a self-built patch archive usable.

UNREVEALED, Flip Side: A patch_5.resources will never be searched at patchlevel 4. To add a new layer, one must increase patchlevel to 5 — a 130-byte plaintext file managed by Steam and overwritten with every update.

Name Formation

Four literals lie directly adjacent in the Exe — this is the kit for archive names:

0x1426cedf4  .texdb
0x1426cedfc  patch
0x1426cee04  chunk
0x1426cee10  chunkbase

and the format string %s_%d_%s_%d (0x1426cf720) builds patch_<n>_chunkbase_<m> from them. The same schema is used verbatim by the sound system: sound/soundbanks/%s/patch_%d_sound.pack (0x14268c120), and sound/soundbanks/%s/prodspec.json also carries patchlevel — according to docs/audio.md on 4. MEASURED. The patch layer model is thus not restricted to .resources, but engine-wide.

Which Patch Family Covers Which Base?

MEASURED across all 1,696 archive-spanning collisions, resolved by basechunkid of the partner base archive:

Patch Family Partner with basechunkid 0 Partner with basechunkid 6
patch_N (no suffix) 1 321 3
patch_N_chunkbase_6 0 830

Families therefore follow layer/basechunkid — to 99.86 %.

patch_N_chunkbase_6 contains exclusively Layer-1 archives.

patch_N contains almost exclusively Layer-0 archives, with exactly three exceptions, all from chunk_16 (Layer 1):

ammo       ammo/player/sp/knife              chunk_16 → patch_3
weapon     weapon/player/sp/joe_melee        chunk_16 → patch_2
projectile projectile/player/joe_throw_can   chunk_16 → patch_2

Two interpretations, both open (SPECULATED):

  • The flat family patch_N is the global Patch Layer (partner: gameresources, chunkid 0), which is mounted with every map — in that case, the three chunk_16 cases are regular, and the search runs globally over all mounted containers.
  • Or patch_N is strictly bound to Layer 0 — in that case, the three entries are dead loads that are never read.

Which interpretation is correct decides the same experiment (Section 6, Variant A against B).

The base archives found, MEASURED:

patch_N              gameresources 848, chunk_5 118, chunk_3 31, chunk_4 28, chunk_1 20, chunk_16 3
patch_N_chunkbase_6  chunk_10 170, chunk_16 166, chunk_13 90, chunk_9 70, chunk_12 33,
                     chunkbase_6 30, chunk_14 29, chunk_11 21, chunk_15 14, chunk_7 5, chunk_8 4

2. The 1,701 Duplicates

python tools/probe_patchprec.py --dupes. MEASURED, 24 archives, 178,077 entries, 167,694 unique (Type,Name) pairs.

stored multiple times                              5,983
of which with differing data_hash                  1,701
   of which cross-archive                          1,696
   of which only within a single archive            5   (all renderProgResource)
only present in Patch archives                     1,013

This breaks down the number 1,701 from docs/moddingflaeche-tnc.md precisely.

Form of Collisions

1,372  Base + Patch
  156  Base + Patch + Patch
  138  Base + Patch + Patch + Patch
   14  Base + Patch + Patch + Patch + Patch
   16  Patch + Patch            (without base copy)

The Timestamps — the strongest numerical finding

Every IDCL entry carries timestamp_us (Unix microseconds, see wolfsdk/idcl.py). Over all 1,680 collisions with Base and Patch copies:

Patch copy younger than base copy 1,635
Patch copy same age 45
Patch copy older 0

Not a single counterexample in 1,680 pairs. MEASURED.

And the time window per archive cleanly orders the layers:

Archive group youngest entry
all 14 chunk_* + chunkbase_6 + gameresources 2017‑10‑30 (chunk_16: 2017‑11‑03)
patch_1, patch_1_chunkbase_6 2017‑11‑03
patch_2, patch_2_chunkbase_6 2017‑12‑07
patch_3, patch_3_chunkbase_6 2018‑02‑24
patch_4 2018‑11‑21
patch_4_chunkbase_6 2018‑09‑18

The game released on 27.10.2017. Base archives end three days later, and the Patch layers follow strictly monotonically behind. The layer number is the time axis. MEASURED.

The Rollback Test — the sharpest single finding

If a higher layer wins, it must be possible to revert a change by re-storing the original content in a higher layer. If instead the Base wins, such an entry would be meaningless from the outset. Sought: pairs where the highest Patch layer has the same data_hash as the Base, but the lowest does not.

MEASURED — one hit:

geistidentity : geist/identity/elite.identity
   gameresources   37297 B  2017-10-08  hash 7f78958e0f28e61c
   patch_2         37297 B  2017-10-11  hash bbd330ed0007235b
   patch_3         37297 B  2017-10-10  hash ef3df1f595b341f6
   patch_4         37297 B  2018-09-18  hash 7f78958e0f28e61c   ← back to base

patch_4 delivers the exact content of the base copy. Assuming "Base wins," both this entry and the two changes in patch_2 and patch_3 would have been ineffective from the start — three pointless entries in a row. Assuming "Highest layer wins," it is a clean retraction of an error from Patch 2/3.

Honest limitation: one single case. In the reverse direction (lowest patch layer equals base, highest deviating), there are 45 cases — these are neutral, they are compatible with both assumptions.


3. Do the Duplicates differ in content?

python tools/probe_patchprec.py --diff 10. Ten text assets from both copies are unpacked and compared. MEASURED: byte-identical 0, different 10. There is no case where only the hash differs while the content remains identical.

The differences are readable and unidirectional — Patch 3 (February 2018) incorporates the same Balancing change line by line:

entityDef player_c01_coma           chunk_1 242 B  →  patch_3 291 B
   +    playerHealth = { damageScale = 0.75; }

entityDef player_firefighter        chunk_9 411 B  →  patch_3_chunkbase_6 460 B
   +    playerHealth = { damageScale = 0.75; }

entityDef player_powerarmor         gameresources 400 B  →  patch_3 424 B
   +    damageScale = 0.75;

entityDef player_manhattan          gameresources 398 B  →  patch_3 414 B
   +    damageScale = 0.75;
   -    armoredDamageScale = 0.150000006;
   +    armoredDamageScale = 0.1;

Patch 1 (November 2017) appears as a bug fix:

entityDef ai/zerstorer/zerstorer_base   chunkbase_6 4324 B → patch_1_chunkbase_6 4409 B
   -    declBehaviorVo = "mercury/vo/zerstorer";
   +    decl = "behaviors/zitadelle";
   +    declBehaviorVo = "mercury/vo/default";
   +    declBehaviorEvents = "behaviorevents/default";

A missing decl and a missing declBehaviorEvents are supplemented, and a sound reference to an existing one is bent around. This is the signature of a patch that repairs a crash or silent enemy behavior.

ELUCIDATED: These changes are unilaterally sensible. A player for whom the base copy wins would have received none of the advertised Balancing updates in the February patch and would still possess the broken Zerstorer reference.

A counterfinding that must be mentioned: the patch copy is not always larger. 596 times larger, 817 times equal size, 267 times smaller. Patches therefore also remove fields (ai/zitadelle/zitadelle_base: patch_3 removes a declBehaviorVo line relative to the base). This fits "replace," not "supplement" — a patch delivers the complete asset content, not a delta record. MEASURED.


4. The Loading Logic in the Exe

NewColossus_x64vk.exe, 58 062 032 bytes. The .text section is SteamStub-encrypted (entropy 8,000, see docs/ghidra.md), so the code itself is not readable. What is readable: .rdata and .data.

The Discovery: containerType_t has its own value PATCH

0x14279a7b8  idResourceStorage::containerType_t::PATCH
0x14279a7e8  idResourceStorage::containerType_t::MAIN
0x14279a818  idResourceStorage::containerType_t::MAP
0x142818820  idResourceStorage::containerType_t

MEASURED. The engine distinguishes three container types, and one of them is named PATCH. This matches exactly with the file layout:

Container Type Files
MAIN gameresources.resources
MAP chunk_1..16, chunkbase_6
PATCH patch_1..4, patch_1..4_chunkbase_6

ELUCIDATED: A dedicated container type for patches is only necessary if patch containers are handled differently when being looked up compared to the others. If they were equivalent, MAIN/MAP would suffice.

The second discovery: a field named patchPriority

In the reflection block of the resource structure, 0x14271c200–0x14271c500:

resourceTypeIndex · compressionMode · compressedSize · descStringIndex nameStringIndex · containerIndex · dataOffset · containerFileName resourceName · patchPriority · typeIndex · metaDataList · …

MEASURED. A field literally named patchPriority exists between resourceName and typeIndex, immediately adjacent to containerFileName and dataOffset. This is the field list of a resource entry.

ELUCIDATED: There is an explicit, per-entry patch ordering. The name leaves no room for alternative interpretation.

What could not be shown (Negative finding, MEASURED): Which byte in the IDCL entry corresponds to patchPriority remains open. The field list originates from the reflection block of the build structure idResourceEntryOptions — there are also pure tool fields like pendingCompression, finalize, and includeEmbeddedDependencies. patchPriority could therefore be a pure build tool field that does not land in the file, but instead determines layer assignment during building. The IDCL entries have several unnamed fields (unknown_0x28, unknown_0x30, unknown_0x71, unknown_0x74, unknown_0x78, unknown_0x88) — all are 0 in every Retail entry, so none of them could serve as a filled priority. SPECULATED: The priority is derived at runtime from the mount order, not from a field in the file.

The third finding: a container comparator

0x14279a6f8  idResourceStorage::compareEntryFilter_t::IN_ANY
0x14279a728  idResourceStorage::compareEntryFilter_t::IN_BOTH
0x14279a760  idResourceStorage::compareEntryFilter_t::IN_CONTAINER_0
0x1426d1b60  idResourceStorage::CompareContainers: Could not open container %s

MEASURED. The engine can diff two containers against each other and filter by "in both" / "only in the first". ELUCIDATED: This is exactly the tool used to build a patch container — one compares old against new and writes the differences. The fact that this tool exists in the Retail build does not mean it is accessible.

What is NOT present at that location

Negative finding, MEASURED. There is in the entire Exe:

  • no format string %s.resources — only the three fixed names gameresources.resources (0x1426c21a8), mp_gameresources.resources, snap_gameresources.resources and the bare suffix .resources (0x1426c21e0);
  • no message like "duplicate resource", "overriding", "shadowed by" or "already provided by" in the resource area;
  • no Cvar that switches priority.

The location where the engine decides between two copies is therefore in the encrypted code and not accessible via strings. One cannot proceed further without a SteamStub unpacker. This is the hard limit of this investigation.

Tools for the attempt, which were noticed

MEASURED (Names exact; descriptions lie in the same string block, the mapping Name↔Description is not proven due to string pooling):

Cvar Description in the same block
resource_spewPakSpam "Set to 1 for huge console spew"
resource_updatePakCacheTable "print verbose information when updating the cache table from a pak file or remvoing preload entries"
resource_debugLevel, resource_debugName, resource_showLoads, resource_spamLoads, resource_warnOnLoad, resource_printMatchingId —
resource_ignoreLayers "ignore layer bits and load all layers during map loads"
resource_loadLooseAssets "check for loose assets when satisfying file io from a resource file"
resourceStorage_checkDataCheckSum "check resource data checksums on resource load"
resourceStorage_validateDeclHashes "makes sure the hash stored for decls matches the hash of the actual decl"
resource_verifyResources "verify resources"

The last three are the danger zone for every write attempt. This fits the runtime message

0x1426ce4b0 idResourceList::Resource_GetEmbeddedStreamingFile: default hash mismatch (resource updated on disk?) loaded: %llx, container …

And, for debugging purposes in the attempt, these three MEASURED:

0x1426cddf8  container entry defaulted: %s
0x1426cdf98  %s:%s:%s container entry defaulted
0x1426ce2e0  resource '%s' of type '%s' is orphaned (no container contains it anymore)

If a self-built patch archive does not carry the file, the diagnosis is in the log: container entry defaulted means "found but unusable", orphaned means "no container knows the name anymore".

REVEALED: There is a hash check that can detect a resource changed on disk. Whether it is active in the Retail build is open — docs/stand.md notes that Part 1 loaded flawlessly with 373 patched assets, but that is a different engine. Unverified for Part 2.

Important for expectation management: docs/cvars_tnc_echt.json (304 Cvars actually observed in-game) contains exactly one of all these names: resource_usePersistHeap. The remaining resource_* Cvars are present as strings in the Exe but do not appear in any listCvars dump. SPECULATED: They are either registered dev-only or filtered by listCvars. Therefore, the first step of every attempt is listCvars resource in the console.


5. What Could Not Be Shown

Full negative findings so that no one searches for them again.

  • No dependency from Base on Patch. Across all 16 Base archives and their 52,417 entries with dependency lists, there are zero references to any of the 1,013 patch-only assets. MEASURED. Logically: the Base archives were built before the Patch assets existed. The idea of deriving a precedence proof via dependency graphs is thus dead.
  • No Cvar pointers readable. The attempt to link Cvar name and description via pointer pairs in .data finds zero hits: the pointers are loaded in encrypted .text via lea. MEASURED. Therefore, the descriptions above remain unassigned.
  • No format string for archive names, see Section 4.
  • The decision point itself is not findable because .text is encrypted.

6. The Decisive Experiment

Not performed — here is how to do it.

Stage 0: Observe without touching a file

The Retail files contradict each other at one place that is visible in the HUD. ammo:ammo/player/sp/knife (Throwing Knife), both copies stored uncompressed (Mode 0):

Copy File maxCount
Base chunk_16.resources, Offset 28,553,856 6
Patch patch_3.resources, Offset 248,832 3

MEASURED. Start a Freedom Chronicles mission from game/dlc/c00/… (the three c00 maps lie in chunk_16 and belong to the base game), collect knives, read the upper limit in the HUD:

  • 3 → the Patch copy wins.
  • 6 → the Base copy wins, or patch_3 is not mounted at all for a Layer-1 map (see Section 1, the three exceptions). Ambiguous — therefore Stage 1.

The read-only diagnostic path has since been cleared: The complete stored Cvar dump of a real TNC session contains from this family only resource_usePersistHeap. resource_spewPakSpam, resource_debugLevel, resource_showLoads, resourceStorage_checkDataCheckSum and resourceStorage_validateDeclHashes are not registered there. The strings exist in the EXE but are not reachable as settable Cvars in this Retail build. This path cannot decide precedence.

Stage 1: Two Bytes, One Pass, Three Possible Outcomes

Both copies are Mode 0 (uncompressed), and both changes are length-neutral — csize remains equal, the entry table is not touched, no Oodle, no archive rebuild. A hex editor suffices.

Variant A — Flat Patch Family over a Layer-1 Card (tests Priority and the global Mount question simultaneously):

File Absolute Byte is set to
base\chunk_16.resources 28 554 020 6 1
base\patch_3.resources 249 110 3 9

Context for verification before writing (both locations): …4/knife";\r\n\t\tmaxCount = <Number>;\r\n\t\tprojectile…

Then start a c00 mission and read the knife limit in the HUD:

Observation Conclusion
9 Patch wins. The question is answered; a Mod costs a small archive.
1 Base wins. Every Mod costs a full rebuild.
6 The Base copy is used, but the change to it was discarded → Integrity check active (resourceStorage_checkDataCheckSum).
3 The Patch copy is used, but the change was discarded → same thing, just reversed.
something else It's not this ammo Decl setting the limit. Switch targets.

Outcomes 6 and 3 are the reason why both sides are changed simultaneously: they cleanly separate "the copy loses" from "the change is rejected".

Variant B — Always Mounted Pairing (gameresources ↔ patch_3, no Layer reservation). entityDef:player_manhattan, also both modes 0:

File Absolute Byte Field is set to
base\gameresources.resources 564 755 916 armoredDamageScale 0.150000006 (11 chars) 0.000000000
base\patch_3.resources 21 721 956 armoredDamageScale 0.1 (3 chars) 9.9

Start the Manhattan chapter over the Chapter selection, taking a hit with armor:

Observation Conclusion
Armor swallows everything, health bar does not move Base wins
A hit clears both armor and life Patch wins
Normal damage neither of the two changes applies → Integrity check

Variant B is the cleaner test (both containers are mounted on every card), Variant A is the cheaper accessible one. Together they also separate the second open question: if A applies but not B, something is wrong with gameresources; if B applies but not A, the flat Patch Family is not global but bound to Layer 0.

Security

  • Backup chunk_16.resources, patch_3.resources and possibly gameresources.resources beforehand — or, cheaper, note the four changed bytes with their original values; the changes are length-neutral and can be reversed byte by byte.
  • patch_3.resources is 35.5 MiB, chunk_16.resources is 92.9 MiB, gameresources.resources is 669.6 MiB. For Variant A, a 129 MiB backup is sufficient.
  • Steam's "Verify Files" restores everything but costs the download.
  • A savegame can store values between runs — start new for each run, do not load.

After: the cheap way, if the answer is Yes

UNLOCKED, three paths, ascending by risk:

  1. Rebuild patch_4_chunkbase_6.resources. This results in the smallest archive of the game at 401,255 bytes and 25 entries (all geistidentity). Take the 25 assets along with their own assets; that's complete. It covers Layer-1 cards (chunkbase_6, chunk_7..16).
    Side note, MEASURED: The associated patch_4_chunkbase_6.texdb is 32 bytes in size — Magic 4fa5c2292ef3c761, two hashes, entry count 0. An empty texture container is therefore valid and delivered.

  2. Rebuild patch_4.resources (58 MiB, 2,649 entries) for Layer-0 cards and gameresources. Still one-twelfth of a fresh gameresources rebuild.

  3. Set patchlevel to 5 and place a fresh patch_5.resources + patch_5_chunkbase_6.resources alongside only the own assets — that's the promised few hundred KiB. Cost: an intervention in packagemapspec.json, which Steam resets on every update, and the unverified assumption from Finding 9.

Paths 1 and 2 do not rely on packagemapspec.json and are therefore more robust. All three remain OPEN until Stage 1 has run.


7. If the Answer is No

Then this isn't a null result, but a planning basis — and the numbers for it are already in docs/moddingflaeche-tnc.md 4.4:

  • Rebuild gameresources.resources completely: ≈ 25 seconds at 26.9 MiB/s measured Oodle throughput, all 24 archives ≈ 2 minutes. Computation time is not the problem.
  • Backup copy of all .resources: 3.4 GiB. That is the problem.
  • The solution that then works: the In-Place path from wolfsdk/idcl.py (plan_in_place/write_in_place). A change that fits into csize plus the null-filled gap behind it requires no rebuild — according to 4.1 this applies to 21/25 weapon, 12/12 damage, 23/25 entityDef. The 1,701 duplicates would then be simply irrelevant, and one always edits the base copy.

In this case, it must also be clarified what the 1,013 patch-only assets are for — 287 images and 265 materials that lie nowhere else. If the Base wins, there must be a second mechanism that searches the Patch containers even if the Base doesn't know the name ("Base wins on collision, Patch fills gaps"). This is the only hypothesis compatible with all findings of this document except number 5 — and Stage 1 refutes or confirms it equally.


8. Which Copy of a Duplicated Asset the WolfSDK Loader Reads (since 0.2.0)

A mod edits an asset's current text, so the loader must read one copy when the same (type, name) exists in several archives (1,701 retail pairs).

  • Since WolfSDK 0.2.0 tncpatch.Installation.read reads the copy in the newest patch_N archive, else the first copy found. The sort key _precedence puts the newest patch_N first and keeps the rest in search order. This is the copy the Studio already showed.
  • Before 0.2.0 it read the first hit. chunk_* sorts before patch_*, so that was the older base copy: of the 1,680 base/patch collisions the patch copy is newer 1,635 times. A mod built on the base copy wrote the old base text, plus its edits, into every copy, so it could undo the official patch changes in that asset.

Which copy the engine loads is still not measured at runtime; section 9 explains why the loader no longer depends on it.

Keywords: duplicate assets, patch copy, base copy, which copy, Installation.read, mod undoes patch, Patch-Kopie, doppelte Assets

9. Why the WolfSDK Loader Writes Every Copy of a Duplicated Asset

The loader reads one copy, layers the mods' edits over it and writes the result into every copy of the asset. Afterwards all copies carry the same bytes, so for what the game ends up loading the precedence question of this page does not arise.

The cost: where two copies differ, the other copy's content is replaced by the read copy plus the edits, not merged with it. That is why the choice of the read copy in section 8 matters: reading the older base copy replaced newer patch text in all copies.

The same rule covers Kiscule scripts that exist more than once (Kiscule Scripting) and the Studio: a replacement saved there becomes a mod, which the loader applies this way.

Keywords: every copy, all archives, loader writes, patch precedence workaround, apply, merge, alle Kopien, Loader


Sources

  • tools/probe_patchprec.py — all numbers in this document
  • wolfsdk/tncpatch.py — Installation.read and _precedence: which copy the loader reads (sections 8, 9)
  • wolfsdk/idcl.py — archive format, timestamp_us, data_hash
  • tools/extract_tnc.py — extraction
  • docs/moddingflaeche-tnc.md 4.4/4.5/4.6 — cost accounting, duplicate names
  • docs/ladepfade.md — packagemapspec.json, DLC folders, log files
  • docs/audio.md — prodspec.json with the same patchlevel 4