Skip to content

Research Report R2 Newassets

LopeKinz edited this page Oct 1, 2026 · 1 revision

Adding Brand-New Decl Entries (Not Just Replacing) in TNC

Every existing writer in this project replaces the payload of an entry that already exists in an archive's entry table; none of them change the entry count, string table, or dependency table — a previously-flagged open gap. This note asks whether adding a genuinely new (type, name) entry is a hard engine limitation or just a missing tool, and shows structurally that it is the latter. A new entry — reusing an existing type string, or introducing a brand-new type string to a container — was hand-built and round-tripped end to end, re-read correctly by the project's own reader, and passed every independent hash check with real negative controls. Critically, adding a new name sidesteps the still-unresolved question of which of two containers wins when they both claim the same name — a brand-new name has nothing to collide with. Everything here is a structural proof against the file format and the project's own readers; whether the live game engine accepts an appended container at mount time is the one remaining untested gap.

What changes, by layer

Layer Must change for an add?
Entry table Yes — one new 144-byte row, entry count +1
String table Yes — +1 row for a new name, or +2 if the type string is also new to the container
String-index pool Yes — +2 index values for the new entry's (type, name) pair
Entry hashes Yes, computed from the already-solved formulas (not copied)
Header hashes Yes, computed in order — metadata hash first, then the header hash that covers it
Dependency table No, if nothing yet references the new entry
Map preload list Optional — an unlisted resource is created and loaded on first reference; a preload line is retail convention (100% coverage) and a performance pre-cache, not a requirement
Container identity No restriction found — a new entry can live in any mounted container, including a small, purpose-built add-on, exactly like over a thousand real retail (type, name) pairs that exist only in a patch archive

Structural proof

Two cases were built and verified from the same unmodified source archive: (1) an existing type string, new name; (2) a brand-new type string the archive had never carried at all. Both:

  • left every original entry byte-identical (same hash, same offset, same payload);
  • used uncompressed payloads for the new entry (no compression dependency);
  • fit inside the source archive's existing metadata slack, so no existing payload offset moved. (The code path for when metadata growth would not fit that slack — shifting the data-start address and every later payload offset — is the same relocation math the project's existing archive-rebuild function already performs for a replace; it was not separately implemented here, the tool raises rather than guess.)

Checks: the project's reader finds the new entry by (type, name) with exactly 1 hit; an exhaustive per-entry data-hash check passes on all entries including the new one; both header hashes pass; the dependency-staleness check correctly reports nothing stale (nothing references the new entry yet).

Negative controls, each caught:

  • Flipping one payload byte of the new entry → the data-hash check flags exactly that entry.
  • Corrupting the header's entry count → does not crash the reader. The string/dependency table addresses are independent header fields, so the file parses fine and the new entry is simply hidden (a useful correction: a wrong count silently omits, it does not corrupt).
  • Pointing the new entry's string-index at an existing entry's name instead of its own → no crash either; it silently creates a duplicate-name alias, the same hazard the project's own reader already documents for several hundred genuine duplicate-name pairs that exist in retail data.
  • Corrupting only the metadata hash → both the metadata hash and the header hash are correctly flagged as bad (the header hash covers the range containing the metadata hash, a documented build-order dependency).

Registration / visibility (from code, not assumed)

Name resolution for a decl/resource is not a fixed, compiled-in list. The resource-load function searches by name and, if not already known, creates and loads it fresh — across every currently-mounted container. This is literally how an unlisted resource still loads on first reference, and the same mechanism that lets patch- and DLC-only assets exist at all in retail data.

Fresh disassembly this pass: a registry address previously named by other work belongs to a function with 9 call sites across 6 functions, one of them inside the already-known inherit-parser for weapon/ammo-type decls, called immediately after loading the parent decl. This reads as runtime dependency/hot-reload bookkeeping (recording what the parent decl pointer is), not the name→resource binding itself — inferred from call-site shape, not a full decompile.

Important distinction: inventing a brand-new decl type (not just a new name within an existing type) is a much bigger undertaking — each type is a compiled-in parser class reached by RTTI/vtable dispatch, and nothing in the traced data-driven path creates a new one. Not attempted; no evidence here supports it being possible without binary-patching the executable.

Open questions

  • Metadata growth beyond a given archive's existing slack — a known, bounded extension of the existing rebuild math, just not separately implemented in this proof; check large archives individually before relying on in-place growth.
  • Whether the live engine's mount-time check accepts an appended container beyond what the already-measured hash checks require.

In-game test (not yet run)

In a custom-map container, append a new ammo decl under a name that does not exist in retail, with a distinctive clip size. Retarget the already-proven pistol-decl-edit technique to point the pistol at that new ammo name instead of the real one. Install, load the custom map, pick up the pistol, and read the HUD magazine count: the distinctive new value appearing confirms the newly-added, retail-absent decl name was found and loaded by the live engine — the one gap that cannot be proven at a desk.

Clone this wiki locally