Round-7 research pass, one of the first with a decrypted TNO runtime image: can a brand-new entry be added to a TNO archive, and would the engine find it? A Python writer appends a new material entry to a copy of chunk1; every original entry reads back byte-identically and all checks pass, including negative controls tied to the engine's own decompiled loader checks (ascending offsets, the per-chunk hash table, the entry-count field). Structural proof on copies only; whether the running game finds the new entry is untested.
Track tnoaddentry, round 7, 2026-10-01. Scope: TNO only
(<Steam library>\steamapps\common\Wolfenstein.The.New.Order, id Tech 5,
WolfNewOrder_x64.exe). Read-only on both game installs and on every project
file except this track's own folder re_probes/tnoaddentry/ and this report.
The game was not launched, no debugger attached, no process touched.
New this round, not available to any prior TNO track: a decrypted TNO
runtime image (re_probes/runtime_tno_186352/module.bin, captured read-only
from the running game, PID 186352) and a Ghidra project built from it
(D:/wolf-decomp/ghidra/tno_runtime.gzf), queried via
python tools/decomp.py <rva|name> [--xrefs] --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf.
r6_tnodecls.md (round 6) explicitly flagged "no TNO runtime image exists"
as the reason several of its findings stayed INFERRED and its single named
next step was "build and self-test an append-entry writer... still not
built." Both limits are gone this round: the writer is built and
self-tested below (§3), and three of round 6's INFERRED items move to
PROVEN by direct decompiled-code read (§2).
Answer in one paragraph
The writer works, structurally, end to end. A Python script
(re_probes/tnoaddentry/probe_append.py) appends one brand-new (material,
"material/tnoaddentry_test") entry to a disposable copy of chunk1.index +
chunk1.resources — cloning a real donor entry's unexplained tail bytes
rather than guessing them, placing the new payload 16-byte-aligned strictly
after the current end of .resources (so the offset-ascending invariant
holds by construction), and bumping both header counters plus the
file-size header field. Re-read with this project's own
wolfsdk.resources.Archive/ResourceIndex reader: all 4241 original
entries come back byte-identical (type, name, path, offset, usize, csize,
and their byte position in the file, §3), the new entry is found by
(type, name), and its payload decompresses back to the exact bytes
written. 14 of 14 checks pass, including three negative controls tied
directly to mechanisms this round newly decompiled from the real engine
(§2/§4): an entry placed at offset 0 instead of true EOF fails the exact
ascending-offset check idBackgroundLoader::BeginBackgroundLoads performs
(decompiled, not just inferred from a crash message, like round 6 had to);
a reused (chunk,type) id is not rejected by the file format itself,
confirming round 6's id-uniqueness rule is a convention the writer must
enforce, not something the format checks; and, most importantly, this
project's own reader is strictly more lenient than the real engine: it
resyncs to EOF and finds the new entry even if the header's entry-count
field is left un-bumped, while the real engine's own parse loop — newly
decompiled this round, idResourceManagerLocal::Init,
FUN_7ff74a4f9680 @ rva 0x169680 — reads that exact header field as an
explicit loop bound and would stop before ever reaching an entry appended
past it. Whether the real game actually resolves the new name at runtime
was not tested (the game was never launched, per the rules) — but the
mechanism that would have to do it is no longer a black box: Init builds,
separately per chunk (not merged globally, not a sorted list), a
classic open-chained hash table over every entry's name (the
idHashIndex::Add pattern — hash, mask, indexChain[new] = hashTable[h];
hashTable[h] = new; — decompiled directly, §2.2), from entries it parses
fresh out of the chunk's own .index file every time the game starts.
A correctly-appended entry — ascending offset, bumped header count, cloned
tail — is exactly the shape of data this function already walks and hashes
4241 times over for chunk1 alone; there is no cached, precomputed, or
baked lookup table anywhere else in the pipeline that would need to be
separately invalidated. That is the strongest statement this round can
make without turning the game on.
§1 Archive format — not re-derived, cited
wolfsdk/resources.py, docs/formats.md, docs/limits.md and
r6_tnodecls.md already nail this down; nothing here is re-measured except
where §2 moves an INFERRED claim to PROVEN. Quick recap for context:
master.index: magic03 53 45 52, BE u32 pair-count, then LE-length-prefixed index/resources name pairs. 14 pairs in TNO, flat, no DLC tier (r4_tnocustom.md). Not touched this round — appending one entry to an existing chunk never needs to.chunkN.indexheader (0x2C= 44 bytes): magic(4) + BE u32 "file size − 32"(4) + 24 zero bytes + BE u32 counter A(4) + BE u32 counter B(4) + BE u32 zero(4). Entries follow: 3 LE-length-prefixed ASCII strings (type, name, path) + BE u32 × 3 (offset, usize, csize) + a variable tail.r6_tnodecls.md(round 6, re-verified across all 14 chunks): the tail is 13 bytes for every decl-ish type checked (entityDef,weapon,damage,ammo,projectile,aiBehavior,material,trackingParms,faction,gamemodesp,soundmixing) — 9 zero bytes + a BE u32 id that is unique per (chunk, type), not globally.sound/particle/renderProg/cuttableResourcehave real duplicate ids and longer, heterogeneous tails (133/205 B) — explicitly avoided as a donor type below.- Counter A ≈ distinct
(type,name)pair count in that chunk (exact in 12/14 chunks); counter B ≈ raw entry count including duplicates (r6_tnodecls.md§3). Confirmed again this round onchunk1:counterA=3508, counterB=4241, andlen(entries)==4241exactly. - No checksum anywhere in an entry or in the file (
docs/formats.md). - Payloads are raw DEFLATE (
wbits=-15,Z_SYNC_FLUSH-terminated, notZ_FINISH) or stored verbatim (csize==usize); 16-byte-aligned, packed with strictly ascending offsets (docs/limits.md§3/§4).
§2 Runtime resource lookup — newly decompiled this round
All of this is read directly out of WolfNewOrder_x64.exe's own code via
the new TNO Ghidra snapshot. RVA = address − base (0x7ff74a390000 per
re_probes/runtime_tno_186352/manifest.json). Every function below was
reached by following a string the task flagged as a likely dead end for
Ghidra's own xref index ("no references found") — scanning is unnecessary
here because tools/decomp.py --strings did return real callers for
every string actually used below; no manual RIP-relative scan was needed
this round.
2.1 The ascending-offset invariant — now PROVEN in code, not just inferred from a crash
docs/limits.md documented FATAL ERROR:
idBackgroundLoader::BeginBackgroundLoads: FileTable is out of order as a
measured crash message from an earlier, now-abandoned append attempt —
real, but only ever observed as an effect. This round traces the actual
check:
python tools/decomp.py --strings "BeginBackgroundLoads" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
=> 0x7ff74b2008a0 "idBackgroundLoader::BeginBackgroundLoads: FileTable is out of order"
<- FUN_7ff74a4fefe0 @ 7ff74a4fefe0 (rva 0x16efe0)
Decompiled (python tools/decomp.py FUN_7ff74a4fefe0 --xrefs --gzf ...),
the load-bearing fragment:
if ((0 < (longlong)uVar10) &&
(*(uint *)(*(longlong *)param_1[1] + 0x2c8 + uVar8) <
*(uint *)(*(longlong *)param_1[1] + -0x30 + uVar8))) {
FUN_7ff74ad5abb0(s_idBackgroundLoader__BeginBackgro_7ff74b2008a0);
pcVar2 = (code *)swi(3); // int3 / fatal trap
(*pcVar2)();
return;
}
Two in-memory entry records, stride 0x2f8 (760) bytes apart
(-0x30 + 0x2f8 == 0x2c8 — same field, one record back), each comparing
their field at relative offset +0x2c8. §2.3 independently identifies
+0x2c8 as the parsed offset field (it is read first, immediately
after the three length-prefixed strings, in the per-entry parser). This is
exactly docs/limits.md's invariant, now read directly out of the
comparison instruction rather than inferred from its failure message: if
entries[i].offset < entries[i-1].offset (file order), the loader traps.
Our writer's negative control NC1 (§4) re-implements this exact comparison
in Python and confirms both halves: appended-at-EOF passes, a hypothetical
offset-0 placement fails it the same way the real code would.
2.2 Per-chunk hash table — answers "hash tables? sorted lists? per-chunk?"
python tools/decomp.py --strings "idResource|ResourceCache|FindResource" --gzf ...
=> "idResourceManagerLocal" (RTTI)
=> "idResourceList::HASH_SIZE" (RTTI)
=> "idResourceManagerLocal::Init2" <- FUN_7ff74a4fccf0 @ rva 0x16ccf0
=> "idResourceManagerLocal::Init - index header bad magic %d"
<- FUN_7ff74a4f9680 @ rva 0x169680 (this is Init, not Init2)
FUN_7ff74a4f9680 (idResourceManagerLocal::Init) loops once per archive
(14 chunks), and for each one:
- Validates the chunk's own magic — the exact literal compared is
0x3534552, i.e.03 53 45 52read as one 32-bit word — a second, independent confirmation ofMAGICstraight from the engine's own check, not just from file-format reverse engineering. - Reads a header count field and uses it as the only loop bound for
parsing that chunk's entries (
while (iVar16 < uStack_518) { ... FUN_7ff74a4f9330(...) ... }) — there is no EOF-sentinel or resync-based termination in the real engine, unlike this project's ownResourceIndex.load()(which resyncs until end-of-buffer regardless of any header count, by design —docs/formats.mdcalls this out as deliberate robustness against the unknown tail). This is the single most practically important finding of this round: the header's entry count is load-bearing for the real engine even though it is invisible to this project's own reader — confirmed as a real gap by NC3 (§4). - For each of those entries, calls
FUN_7ff74a4f9330(the per-entry stream parser, §2.3) and then — the actual lookup-structure build — computes a hash of the entry's name and inserts it into a per-archive open-chained hash table, the textbookidHashIndex::Addshape:
uVar11 = func_0x7ff74ad58000(*puVar1); // hash(name)
uVar19 = *(uint *)((longlong)plVar12 + 0x1c); // hashMask
if (/* table empty/needs growth */) {
FUN_7ff74ad62c30(plVar12, (int)plVar12[2], iVar10); // grow buckets
} else if (/* chain array too small */) {
FUN_7ff74ad629d0(plVar12, iVar16 + 1); // grow chain
}
lVar13 = (longlong)(int)(hashMask & uVar11 & uVar19);
*(undefined4 *)(indexChain + lVar21*4) = *(undefined4 *)(hashTable + lVar13*4);
*(int *)(hashTable + lVar13*4) = iVar16; // hashTable[h] = newIndex
This is idHashIndex::Add(key, index) from id Tech's shared container
library (indexChain[index] = hashTable[h]; hashTable[h] = index;),
instantiated once per archive object, built fresh from that one
chunk's own entries every time Init() runs. This directly answers the
question: hash table, not a sorted list, and scoped per chunk, not
merged across all 14 — matching r6_tnodecls.md's independently
measured per-chunk id-uniqueness scope (§3 there) with an actual
mechanism, not just a correlation.
func_0x7ff74ad58000 (the hash function itself) was not independently
disassembled this round — it falls in a gap this quick Ghidra import did
not analyze (!! no function contains this address) — so it is cited by
its call shape (hash-in, masked bucket index out, immediately fed into an
idHashIndex-style insert) rather than by its own body. Plausibly
idStr::Hash or an equivalent FNV/djb2-style string hash used throughout
id Tech; not proven, flagged as open (§6).
What exactly gets hashed — name alone, or type+name — is not fully
traced. FUN_7ff74a4f9330 (§2.3) parses type, name and path as three
separate fields; the string handed to the hash call in Init comes out of
a local buffer sized like a single idStr, strongly suggesting name
alone, with type presumably available as a secondary check against
whatever the chain lookup returns (every sampled name in the corpus is
already type-namespaced by convention, e.g. weapon/knife_base, which
would make type-less name collisions rare in practice regardless). Not
conclusively pinned — flagged INFERRED, not PROVEN (§6).
2.3 The per-entry parser — confirms the field layout our Python reader assumes
FUN_7ff74a4f9330 (called once per entry from Init's loop) reads, in
this exact stream order: a 4-byte length + bytes (type string, immediately
hashed into a cached field — a detail our own reader does not need and
does not replicate), then two more length-prefixed strings via a shared
helper (name, path), then three consecutive 4-byte big-endian-ish fields
read back to back — exactly SIZE_FIELDS = 12 bytes, exactly where
wolfsdk/resources.py's struct.unpack_from(">3I", buf, pos) already
reads them, confirmed by the comparison in §2.1 using the first of these
three as "offset." Then a genuinely structured tail, not just
unexplained padding: a 4-byte count, followed by that many 24-byte
sub-records (explaining why audio types — sample/sound, which carry
extra per-variant data — measured longer, heterogeneous tails in
r6_tnodecls.md §3), then two more small fixed fields. For count==0
(the common case for decl-ish types) this accounts for 9 of the 13 bytes
r6_tnodecls.md measured; the remaining ~4 bytes were not reconciled
this round (flagged open, §6) — not load-bearing for the writer, because
the writer never synthesizes a tail from scratch: it clones a real donor
entry's tail verbatim and only overwrites the trailing id (§3), which is
provably a valid instance of whatever this structure actually is, by
construction, regardless of whether every byte in it is explained.
2.4 Two more functions read, for completeness
FUN_7ff74a4faf20parsesmaster.index(0-hit on "DLC" anywhere in this path, consistent withr4_tnocustom.md's "flat, no DLC tier" finding): same magic check, then a BE-u32 pair count, then a loop reading two length-prefixed strings per iteration into a 160-byte-stride archive array — structurally the same thingwolfsdk/resources.py:load_master()already does, now independently confirmed from the engine's own code.FUN_7ff74a4fb430("could not open resource file %s" / "chunk missing in LoadNamedResourceFile" / "The map %s isn't present in the chunk list") is the per-chunk-open entry point that callsBeginBackgroundLoads(§2.1) and also separately opensgenerated/buildGame/<map>.mapResources— a different, per-map manifest layer, out of scope for this track (append-entry, not map-authoring) and already the subject of the TNC-sidemapmanifesttrack's brief for the other game.
§3 The writer — built and self-tested
re_probes/tnoaddentry/probe_append.py. Operates only on a disposable
copy (D:/wolf-decomp/tmp/tnoaddentry/copy/chunk1.{index,resources},
shutil.copyd once from base/_wolfsdk_backup, the project's own
confirmed-pristine backup — never the live install, never written to in
place). chunk1 chosen as the smallest pair (751 KB index / 73 MB
resources) for a fast, cheap round-trip.
Recipe:
- Pick a donor entry of the target type (
material, chosen for its clean, uniform 13-byte tail perr6_tnodecls.md—sound/particleavoided on purpose, see §1). Clone its tail bytes verbatim; only the trailing 4-byte id is replaced. - New id =
max(existing ids of this type in this chunk) + 1(measured 170materialentries inchunk1, max id 2491 → new id 2492) —r6_tnodecls.md's already-measured per-(chunk,type) rule, re-applied here for the first time in a real writer. - New payload (129 B decl text) is DEFLATE-packed the way the game's own
packer does (
wolfsdk/resources._deflate,Z_SYNC_FLUSH) and appended 16-byte-aligned strictly after the current end of.resources— so its offset is, by construction, larger than every existing offset (§2.1). - New entry bytes (3 length-prefixed strings + offset/usize/csize + the
cloned-and-patched tail) are appended to the end of the
.indexbuffer. - Both header counters are bumped by 1 (one new raw entry, one new
distinct name) and the header's "file size − 32" field is recomputed —
all three confirmed present and correctly valued on the pristine file
before any edit (
asserted in the script).
Result, re-read with wolfsdk.resources.Archive/ResourceIndex
(this project's own reader, unmodified):
Original: 4241 entries, index 751275 B, resources 72937791 B
Header: magic=b'\x03SER' size_field=751243 (filesize-32=751243) counterA=3508 counterB=4241
Donor: <Entry material:water/water_c01p1_close 1851B @42908448> tail=13 bytes 000000000000000000000009bb
material ids in chunk1: 170 entries, max=2491 -> new_id=2492
New payload: usize=129 csize=103 pad=1 new_offset=72937792 (prev max offset=72928064)
After append: 4242 entries (expected 4242)
ok count increased by exactly 1
ok all 4241 original entries byte-identical (type/name/path/offset/usize/csize/pos)
ok index bytes outside header-counters/size-field/appended-tail are untouched
ok .resources bytes [0:72937791] byte-identical to the pristine original
ok new entry found via Archive.find((type,name))
ok new entry fields match what was written
ok Archive.read(new_entry) == original payload bytes
ok header size_field == new filesize-32
ok header counterA/+1, counterB/+1
14/14 checks passed
This is the exact property the task asked for: append one entry, re-read
with the project reader, every other entry byte-identical — now proven,
not just designed on paper the way r6_tnodecls.md §3 left it.
§4 Negative controls
Every positive claim above has a paired negative control, re-runnable from the same script:
| id | claim | result |
|---|---|---|
| NC1a | appended-at-EOF passes the real ascending-offset check (§2.1, reimplemented exactly) | pass |
| NC1b | the same entry, hypothetically placed at offset 0 instead of EOF, fails that same check | fails, as expected — proves the check is discriminating, not vacuously true |
| NC2 | a reused (chunk,type) id (duplicate of an existing material id) is not caught by this project's own reader |
not caught — confirms r6_tnodecls.md's id-uniqueness rule is a convention the writer must enforce; the file format itself has no such constraint |
| NC3a | this project's reader finds the new entry even when the header's entry-count field is left un-bumped | found anyway (our reader resyncs to EOF, ignoring header counts) |
| NC3b | …but the real engine's own count-bounded parse loop (§2.2, Init) would not reach an un-bumped append |
correctly predicted to miss it — the single sharpest "our tool is more lenient than the real engine" gap this round found |
NC3 is the most important one: it is a direct, constructed demonstration that "round-trips through our own tooling" (what round 6 called the achievable ceiling without a runtime image) and "the real engine would parse it" are not the same claim, and that the gap between them is now a known, specific, fixable thing (bump the header count), not an unknown unknown.
§5 Would an appended entry actually be found? Synthesis
Three independent conditions, each now traced to a specific decompiled check, all satisfiable by the writer in §3:
- Offset must ascend (§2.1,
BeginBackgroundLoads) — satisfied by appending strictly after the current end of.resources. - The header's entry count gates whether
Initever parses the new entry at all (§2.2) — satisfied by bumping counter B (and A, for the distinct-name count the file format also tracks). - The parsed entry is then hashed into a per-chunk table by name (§2.2) — nothing about this step is name-specific or type-specific in a way that would reject a well-formed new entry; it is the exact same code path every one of the chunk's other 4241 entries already goes through.
What this round cannot settle without launching the game: whether
FUN_7ff74a4f9330's per-entry parse has some additional validation this
round's reading missed (the ~4 reconciled-vs-13 tail byte gap, §2.3, §6),
and whether anything downstream of the hash-table lookup (the actual
decl/material/sound consumer, e.g. the decl-text parser or the material
compiler) imposes a constraint the archive layer does not — out of scope
for an archive-format track either way. Structurally, though, this is the
first TNO round to end with "here is the exact mechanism that would have
to find it, traced to its own insert loop" rather than "the mechanism is
unknown, encrypted, out of reach."
PROVEN
- The chunkN.index/
.resourcesappend-entry writer works end to end on a real archive copy: 14/14 checks, all 4241 pre-existing entries byte-identical after appending one new(material, name)entry, new entry found and its payload round-trips exactly (§3). idBackgroundLoader::BeginBackgroundLoads(FUN_7ff74a4fefe0, rva0x16efe0) enforces the offset-ascending invariant by direct comparison of two in-memory entries' offset fields — previously known only from its crash message, now read in the comparison instruction itself (§2.1).idResourceManagerLocal::Init(FUN_7ff74a4f9680, rva0x169680) builds, separately for each of the 14 chunks, an open-chained hash table (idHashIndex::Addpattern) over every entry it parses from that chunk's own.indexfile — answers "hash table, per-chunk" directly from decompiled code (§2.2).Init's per-chunk entry-parse loop bound is an explicit header count field read fresh from the file, not an EOF sentinel — there is no termination condition that would let the real engine "just keep reading" past a stale count the way this project's own resync-based reader does (§2.2, confirmed discriminating by NC3).- The per-entry parser (
FUN_7ff74a4f9330, rva0x169330) reads type, name, path, then three consecutive 4-byte fields at exactly the stream positionwolfsdk/resources.pyalready treats as offset/usize/csize — independent confirmation of the field layout from the engine's own parser, not just from successful round-trips against retail data (§2.3). master.indexparsing (FUN_7ff74a4faf20, rva0x16af20) matcheswolfsdk/resources.py:load_master()structurally: magic check, BE u32 pair count, length-prefixed name pairs (§2.4).- The magic
03 53 45 52is checked by the engine as the literal 32-bit value0x3534552in at least two independent call sites (Initandmaster.index's parser) — a second, code-level confirmation ofMAGIC(§2.2, §2.4). - The chunkN.index header's three maintained fields — "file size − 32" at
offset 4, counter A at
0x20, counter B at0x24— hold their expected values on the real, unmodifiedchunk1.index(asserted in the script before any edit):size_field == filesize-32andcounterB == len(entries)exactly (§3).
INFERRED
- That the per-chunk hash key is the entry's name alone, not a
composed type+name string — supported by the hashed value coming from a
single-
idStr-sized local buffer right after the per-entry parse, but the exact contents of that buffer were not traced byte-for-byte (§2.2). - That
func_0x7ff74ad58000(the hash function itself) is a generic string hash (plausiblyidStr::Hash) — inferred from its call shape (hash in, masked bucket out, fed straight into anidHashIndex-style insert), not from reading its own body (it sits in a gap this quick Ghidra import did not disassemble) (§2.2). - That a correctly-appended entry (ascending offset, bumped header count,
cloned tail) would be accepted by the real engine's
Initand become findable by name at runtime — every condition this round could check statically is satisfied by the writer in §3, but this is still a prediction from code reading, not an observed in-game result (§5). - That the ~4-byte gap between this round's traced tail structure (9 bytes
for
count==0) andr6_tnodecls.md's measured 13-byte tail reflects an unresolved field this round's reading missed, rather than a measurement error in either report — plausible, not reconciled (§2.3, §6).
FAILED HYPOTHESES
- None outright refuted this round — this track built on round 6's open item rather than re-testing a specific round-6 claim. The one thing this round's own NC3 control disproves is an implicit assumption a writer might otherwise make: "if our own reader accepts an appended entry, the real engine would too." False — shown directly (§4): the real engine's parse loop is bounded by a header field our reader does not even consult.
§6 Open questions
- The actual in-game test (below) — not run, per the rules (never launch the game). This is the single biggest remaining gap between "proven structurally" and "proven to work."
- Whether the per-chunk hash key is name-only or type+name (§2.2) —
would need one more decompiler pass specifically on what fills the
local buffer handed to the hash call inside
Init's loop body. func_0x7ff74ad58000's own body (the hash function) — in a gap this quick Ghidra import did not analyze; a deeper/slower analysis pass (not attempted this round, out of the ~15s-per-call budget this track had) could resolve it.- The exact ~4-byte gap in the tail's structure (§2.3) — not load-bearing
for the writer (which clones real tails verbatim) but would fully close
r6_tnodecls.md's own open "tail not fully understood" item if resolved. - Whether anything downstream of the archive/hash layer (the decl-text parser, the material compiler, a "known type names" allowlist) rejects a well-formed new entry for a reason this track's scope — the archive container, not the decl/material content pipeline — did not reach.
Recipe (for a future round that makes the writer productive)
(Translated from German.)
- Take
re_probes/tnoaddentry/probe_append.pyas the template, swap the game-data path (SRC_DIR) and the target type/name/payload. - Always append at the real end of the file (never write into the middle of the file) — that is the one invariant that leads to a crash immediately when violated (§2.1).
- Carry both header counters and the "file size−32" field along — invisible to this project, but live-verified as load-deciding for the real engine (§2.2, §4 NC3).
- Never build tail bytes by hand — clone a real entry of the same type
and set only the last 4 bytes (the running ID) to
max(existing IDs of this type in this chunk) + 1(§3, already measured inr6_tnodecls.md).
In-game test (for the maintainer)
(Translated from German.)
Nothing of this has been tried in the game yet — everything below is the smallest next step, not a result. Two stages, from the lowest-risk to the most meaningful:
- Stage 1 — does the engine load the bigger file at all without complaint?
Experimentally put the
chunk1test folder built in this report (D:\wolf-decomp\tmp\tnoaddentry\copy\chunk1.index+chunk1.resources) over a backup copy ofbase/chunk1.index/chunk1.resources(never overwrite the original, back up beforehand as usual), start the game, look atqconsole.log. Expected according to this report: no linezlib inflate error,index header bad magic,FileTable is out of orderand no crash at start — that directly confirms whetherInit(§2.2) accepts the file that is one entry larger, before anything at all references the new name. - Stage 2 — is the new name really found? First use
python -m wolfsdk paths <type:name>to find a text decl field that already references an existingmaterialname (e.g. adiffusemapor skin reference), redirect this field experimentally to the new name (material/tnoaddentry_testor a clone of a real, already used material) (setpatch, the same mechanism asexample_white_handgun/widerstand, already checked live). Load a map on which this field is actually used. Expected: the affected object looks unchanged (the cloned material is bit-identical to the original) andqconsole.logshows no "resource not found" line for the new name — that would be the first real proof that an appended entry is found by name by the running engine, not only by this project's own reading tool.
No damage expected in either stage: stage 1 touches no existing bytes (only appending), stage 2 clones real, already working content under a new name — in the worst case the map does not load or the object is missing, none of which damages a save.
Files
| File | Content |
|---|---|
re_probes/tnoaddentry/probe_append.py |
The writer + self-test + 3 negative controls (§3/§4), re-runnable, writes only under D:/wolf-decomp/tmp/tnoaddentry/. |
re_probes/agent_reports/r7_tnoaddentry.md |
This report. |
Related: Research Report: R6 Tno Decls, Research Report: R4 Tno Custom Maps, File Formats, Modding Limits, TNO Modding Surface.