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_Nis the global Patch Layer (partner:gameresources,chunkid 0), which is mounted with every map — in that case, the threechunk_16cases are regular, and the search runs globally over all mounted containers. - Or
patch_Nis 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 namesgameresources.resources(0x1426c21a8),mp_gameresources.resources,snap_gameresources.resourcesand 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
.datafinds zero hits: the pointers are loaded in encrypted.textvialea. MEASURED. Therefore, the descriptions above remain unassigned. - No format string for archive names, see Section 4.
- The decision point itself is not findable because
.textis 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_3is 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.resourcesand possiblygameresources.resourcesbeforehand — 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.resourcesis 35.5 MiB,chunk_16.resourcesis 92.9 MiB,gameresources.resourcesis 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:
-
Rebuild
patch_4_chunkbase_6.resources. This results in the smallest archive of the game at 401,255 bytes and 25 entries (allgeistidentity). 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 associatedpatch_4_chunkbase_6.texdbis 32 bytes in size — Magic4fa5c2292ef3c761, two hashes, entry count 0. An empty texture container is therefore valid and delivered. -
Rebuild
patch_4.resources(58 MiB, 2,649 entries) for Layer-0 cards andgameresources. Still one-twelfth of a freshgameresourcesrebuild. -
Set
patchlevelto 5 and place a freshpatch_5.resources+patch_5_chunkbase_6.resourcesalongside only the own assets — that's the promised few hundred KiB. Cost: an intervention inpackagemapspec.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.resourcescompletely: ≈ 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 intocsizeplus the null-filled gap behind it requires no rebuild — according to 4.1 this applies to 21/25weapon, 12/12damage, 23/25entityDef. 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.readreads the copy in the newestpatch_Narchive, else the first copy found. The sort key_precedenceputs the newestpatch_Nfirst 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 beforepatch_*, 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 documentwolfsdk/tncpatch.py—Installation.readand_precedence: which copy the loader reads (sections 8, 9)wolfsdk/idcl.py— archive format,timestamp_us,data_hashtools/extract_tnc.py— extractiondocs/moddingflaeche-tnc.md4.4/4.5/4.6 — cost accounting, duplicate namesdocs/ladepfade.md—packagemapspec.json, DLC folders, log filesdocs/audio.md—prodspec.jsonwith the samepatchlevel 4