Repository navigation
Research Report R7 Tno Map Loading
TNO Map Loading in Code: Command Handlers, Map_f, Chunk Mounting, and a Working DLC-Extension Pipeline
Round-7 research pass on how The New Order chooses and loads a map, read from decompiled code: map, devmap and three siblings share idCommonLocal::Map_f, whose retail-path check for a combo map is only a warning, and the master.index mount is confirmed from code. The main new result is that TNO scans <install-root>\dlc\ for folders with an info.txt, checks an app ID against Steam ownership, and mounts that folder's own master.index — a DLC-extension route for custom content. Static evidence only; the folder-scan route is untested in game.
Track tnomaploading, round 7, 2026-10-01. First round with a decrypted TNO
runtime image (re_probes/runtime_tno_186352/module.bin, pid 186352,
base 0x7ff74a390000, captured read-only from the running retail game) and a
TNO-scoped Ghidra snapshot (D:/wolf-decomp/ghidra/tno_runtime.gzf, 50,884
functions from .pdata, 2,369 RTTI vtables labelled, no curated function
names). Every prior TNO round (r4_tnocustom.md, r6_tnoconsole.md,
r6_tnodecls.md, r6_tnomegatex.md) explicitly stated TNO's .text was
unreachable ("no TNO runtime image exists"); that is no longer true, and this
round spends its whole budget on the one question that needed it most: how
does TNO's own code choose and load a map, and what does that mean for a
custom/modified map.
Read-only throughout. Never launched the game, never attached a debugger.
Queried tools/decomp.py (max 2 concurrent calls per the tool's own
copy-on-open design) and, where Ghidra's string xrefs were empty/incomplete
(stated explicitly below every time this happened, not glossed over),
scanned module.bin directly for 8-byte pointer references, reusing
tools/tnc_runtime_code.py's own method (its Image class, pointed at the
TNO snapshot directory instead of TNC's). New files live under
re_probes/tnomaploading/ (one script, probe_maploading.py, read-only,
reproduces every string/offset cited below from scratch, with one negative
control — run it yourself: python re_probes/tnomaploading/probe_maploading.py
→ 20/20 known strings match, mapInfo field table OK, ALL OK).
How TNO chooses and loads a map in code: map/devmap command handlers (if
they exist), chapter decls, mapInfo.vmtrChunkNumber, master.index chunk
mounting, megatexture binding; what a custom or modified map needs; and the
best route to load a modified or new map in TNO, building on r4_tnocustom.md.
devmap and four siblings (map, cmap, devcmap, checkmap) are real,
registered console commands, all routed through one shared function this
round identifies by its own embedded debug string as idCommonLocal::Map_f
(RVA 0x148ec0). It parses argv, recognises flags combo/dev/reset/
check/nextMapDiscSwap, and — the one finding this round ranks
everything else under — in a normal production/retail launch its only
map-existence check is whether maps/<name>/_combo/<name>.proc exists, and
that check is advisory, not a hard gate: missing it only logs "You need to build the combo map for: %s" and the function still proceeds to start the
map. The hard "Can't find map '%s'" abort (checking maps/<name>.entities
directly) only fires on a non-production build path (see §2 — gated by a
flag this round could identify but not fully pin down the write-site of).
master.index's own format is confirmed from code, not just static bytes:
magic "RE5" + a version byte, a count, then per-chunk two filename reads,
each opened through the filesystem singleton and appended to one growable
array (§3).
The single biggest new result: that growable chunk array is not only
filled from the base 14 chunkN.index/chunkN.resources pairs.
idSessionLocal's own EnumerateDownloadableContent (RVA 0x4ee2b0, own
debug string) genuinely scans <install-root>\dlc\ on local disk for
subfolders containing a valid info.txt, reads an appID and a dlcID
(must be 1..16) out of it, checks appID against a Steamworks ownership
call, and on success queues the folder; OpenDLCFileHandles (RVA 0x16d090,
own debug string) then opens that folder's own master.index the exact
same way as the base one and appends its chunk pairs into the same array
— with a by-id "already loaded, skipping" guard. This is a real, working,
code-confirmed extension point that r4_tnocustom's two rounds concluded did
not exist for TNO ("no dlc_0-style route", ranked last, based on reading the
wrong game's decompiler, since no TNO image existed yet). It is not the
same shape as TNC's dlc_0 precedence (this is pure addition of new
chunk pairs, not an override of existing ones) — but it is a genuine,
intended, always-live mounting path for a custom map's own archive, gated
only by a Steam-ownership check this report shows is very likely satisfiable
with the base game's own AppID (confirmed from the local Steam install:
201810, <Steam library>\steamapps\appmanifest_201810.acf). This is now
Route 1, replacing both of r4_tnocustom's untested/unsolved Routes 2 and 3.
Ghidra's static string xrefs were mostly empty for this binary (stated in
every prior TNO round as a structural limitation of the quick import — "no
command table for TNO yet"); --strings still lists the strings and, where a
reference happened to resolve, names the containing function. Searching
"loads a combo map" (the exact cvar/command description text
r6_tnoconsole.md's own mined table already found for devmap/cmap/
devcmap/checkmap) hit exactly one function for every one of its five
description variants:
python tools/decomp.py --strings "loads a combo map" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
"loads a combo map" <- FUN_7ff74a4c7950 (x2)
"loads a combo map in developer mode" <- FUN_7ff74a4c7950 (x2)
"loads a combo map in developer mode from checkpoint load" <- FUN_7ff74a4c7950
Decompiling FUN_7ff74a4c7950 (RVA 0x137950) shows it is the console
command-registration block itself: 40+ consecutive calls through
idCmdSystemLocal's vtable (+0x18, confirmed by its own RTTI label
vtbl_idCmdSystemLocal on a related call site, matching the already-known
engine family) — each call is literally AddCommand(name, handler, description, completionFn). Reading the raw command-name string operands
(Ghidra doesn't label them, since nothing in the quick import's analyzer set
resolves a bare DAT_… as a C string unless something else references it —
read directly from module.bin instead, same method as everywhere else in
this report):
| name string (VA) | handler | description |
|---|---|---|
map (0x7ff74b1f111c) |
FUN_7ff74a4d9ff0 (RVA 0x149ff0) |
"loads a combo map" |
cmap (0x7ff74b1f19a4) |
FUN_7ff74a4d9ff0 (same) |
"loads a combo map" |
devmap (0x7ff74b1f19d4) |
FUN_7ff74a4da050 (RVA 0x14a050) |
"loads a combo map in developer mode" |
devcmap (0x7ff74b1f1a08) |
FUN_7ff74a4da050 (same) |
"loads a combo map in developer mode" |
checkmap (0x7ff74b1f1a50) |
FUN_7ff74a4da0c0 (RVA 0x14a0c0) |
"loads a combo map in developer mode from checkpoint load" |
slowmap |
FUN_7ff74a4d9f70 |
"loads a map using slow non-shipp[ing path]" |
restartmap |
FUN_7ff74a4dc960 |
"restarts the current map" |
restartmaphere |
FUN_7ff74a4dcb30 |
"restarts the current map and tel[eports]" |
nextmap |
FUN_7ff74a4da120 |
"transitions to the next map" |
All five map-loading handlers (map/cmap/devmap/devcmap/checkmap)
are thin wrappers: each prepends its own argv, appends one or two literal
flag words, and calls the same core function, FUN_7ff74a4d8ec0 (RVA
0x148ec0):
FUN_7ff74a4d9ff0 (map, cmap) : appends "combo"
FUN_7ff74a4da050 (devmap, devcmap): appends "dev", then "combo"
FUN_7ff74a4da0c0 (checkmap) : appends "check"
All three handlers verified directly against this report's own re-read
strings (re_probes/tnomaploading/probe_maploading.py line-level checks).
There is also a separate, standalone helper, FUN_7ff74acb4350 (RVA
0x924350), that formats a literal "devmap %s\n" string and feeds it
through idCmdSystemLocal's vtable +0x48 (a BufferCommandText/
AppendCommandText-shaped call, same vtable object as AddCommand above) —
this is some menu/UI flow (unidentified — no xrefs to this helper either,
Ghidra's import limitation again) directly injecting a devmap <name>
command the same way a player typing it in the console would, i.e. a second,
code-confirmed, non-console way devmap gets invoked.
This function's own embedded string proves its name — one of its error
paths is literally FUN_7ff74ad5a710(s_idCommonLocal__Map_f_map_doesnt_h...)
= "idCommonLocal::Map_f map doesnt have valid data". This is the strongest
possible identification this method can give (not a guess from shape or
analogy — the function tells you its own class::method name).
Argument parsing (param_1 is an idCmdArgs-shaped struct: an int32
argc at offset 0, argv[0] not used here, argv[1] = the map name at
offset +4, further argv[N] from offset +6*4 onward stepping by two
int32 slots): a loop from argv[2] onward checks each token case-sensitively
against five literals — "combo", "dev", "reset", "check",
"nextMapDiscSwap" — setting one local flag byte per hit. "check" sets
two flags at once (its own flag, and the same bit "combo" sets) — so
checkmap's single appended "check" token reaches the identical downstream
branch as map/cmap's appended "combo", and devmap/devcmap append
both "dev" and "combo" explicitly. Net effect, re-derived from all
five wrapper handlers together: all five player-facing map commands set
the same "combo" flag one way or another — there is no map-loading
command that skips the combo-flag branch in §2.2 below.
Before any existence check, the function builds a default extension string
"map", then asks (via a second helper) whether the configured/queried
extension resolves to "entities" — if so, swaps the extension string to
"entities". This is the parser-level confirmation (not just static-text
inference, as in r4_tnocustom.md) that a map name is ultimately resolved
to a maps/<name>.<ext> path with ext defaulting to entities — matching
file:maps/<id>.entities exactly as r4_tnocustom.md found by reading decl
text. The two extensions are not interchangeable at the call site: if the
extension is "entities" and the existence check (next section) fails,
an extra hint fires: "Use the writeEntitiesFile command to convert the map to an entities file.\n" (own string, 0x7ff74b1f7f20) — i.e. the engine
still recognises a raw, uncompiled .map (editor format) as a legitimate
predecessor to the shipped .entities, consistent with the standard id Tech
5 build pipeline (.map → writeEntitiesFile → .entities).
A global flag (DAT_7ff74c181dd8) selects between two completely different
bodies of logic. Strong circumstantial evidence for its meaning: the
only other place in this image that writes to it is FUN_7ff74a4faf20
(§3, the base-archive mount function), and it writes 0 there in the
same few instructions as a logged call to idLib::SetProduction( PROD_DEVELOPMENT ) — i.e. this flag reads as "is this a shipping/production
run" (non-zero) vs. "development, no base archive could even be opened"
(zero). INFERRED, not proven — this report did not find or trace the
call that sets it to non-zero on a normal run, only the one call that zeros
it in a degraded scenario.
When the flag is non-zero (the expected state for any real, retail
launch where master.index opened successfully — see §3):
maps/%s/_combo/%s.proc <- own string, 0x7ff74b1f7f70
You need to build the combo map for: %s <- own string, 0x7ff74b1f7f88 (LOG ONLY)
The file-existence check (filesystem singleton's vtable +0x88, same call
shape used everywhere else in this function) runs, and if it returns false,
the function logs the warning and keeps going — there is no early return,
no goto, nothing skipped; the very next lines unconditionally set up
session-start fields ("direct"/"topic"/"player_start" substrings,
own-string-confirmed at 0x7ff74b1f77f4/0x7ff74b1f77fc/0x7ff74b1f7ff8)
and call through to what is almost certainly idSessionLocal::StartNewGame
(two further vtable calls on a second singleton, offsets +0x98 then
+0x48, immediately followed by two paired FUN_7ff74a4d6060(5, …) /
FUN_7ff74a4d6060(6, …) calls that read exactly like a loading-screen
push/pop pair). This is the single most actionable finding in this
report: a map that ships maps/<name>.entities but has no baked
_combo/<name>.proc at all is not rejected by Map_f — it only gets one
log line, and the engine still attempts to start it.
When the flag is zero (development/no-archive path), the function
instead builds maps/%s (own string 0x7ff74b1f7ef0), runs the same
vtable +0x88 existence check against maps/<name>.<ext> directly, and
this one does gate: on failure it logs "Can't find map '%s'\n" (own
string 0x7ff74b1f7ef8), optionally appends the writeEntitiesFile hint
(§2.1), and jumps straight to the function's cleanup/exit label — no session
start happens at all. On success it instead checks for an optional
<name>_region.map sibling file and, if present, logs "Loading region map: %s\n" and substitutes <name>_region for the rest of the call — a cosmetic
per-region overlay feature, not required for a basic load.
Combining §2.1–§2.2, re-stated as a checklist, ranked by how hard each requirement actually is to satisfy, per this code (not per guesswork):
-
Not a hard requirement in production:
maps/<name>/_combo/<name>.procexisting. Missing it only logs a warning inMap_fitself. (What happens later — whether the renderer/entity-spawn path that actually reads this.procfile for geometry crashes or silently no-ops when it's absent — is not traced this round; §2.2's own "proceeds anyway" behaviour is proven only up to theStartNewGame-equivalent call, not past it. Flagged as an open question, not glossed over.) -
Needed only on the non-production code path (§2.2's
elsebranch):maps/<name>.entities(or.map) must exist, checked directly by path. For a normal retail launch this check does not run at all. - Chapter-select and the actual
mapInfo/vmtrChunkNumberdecl fields (below, §4) were not found referenced anywhere inMap_fitself — this function only cares about the map name string and the two files named above;mapInfo/vmtrChunkNumberare read elsewhere (most likely at the point the resource manager actually opens the content, downstream ofMap_f— not traced this round, time-boxed in favour of the DLC-mount finding below, which this report ranks as far higher-value for "how do I add a map").
--strings "master.index" resolves two containing functions. The first,
FUN_7ff74a4faf20 (RVA 0x16af20), is the base-archive loader called once
at startup:
- Opens
master.index(two candidate names tried,DAT_7ff74b9254f8then a fallbackDAT_7ff74b925500, both resolved through the filesystem singleton's vtable+0xf8, an "open file" call reused everywhere in this report). - On total failure (neither name opens): logs
idLib::SetProduction( PROD_DEVELOPMENT ), zeroesDAT_7ff74c181dd8(§2.2), and returns early — no base chunks are mounted at all in this branch. - On success: reads a 4-byte magic, compared against the constant
0x03534552— byte order'R' 'E' '5' 0x03, i.e."RE5"+ a version byte, confirmingdocs/formats.md's already-documented archive magic directly from the mounting code, not just from static archive bytes. Reads a 4-byte count (N), ensures array capacity forNentries, then loopsNtimes: reads two length-prefixed strings per entry (the.indexand.resourcesfilenames for that chunk pair), opens each through the same filesystem singleton (+0xf8), and stores both handles plus a small int tag (read from a separate fixed-size array,DAT_7ff74b933820, indexed by chunk number — plausiblydiscNumberor a DLC-id slot, not traced further) into one growable array atparam_1[2]/param_1[3](capacity/count fields at struct offsets+0x1c/+0x20).
This is the exact mechanism TNO's own 14 chunkN.index/chunkN.resources
pairs get mounted by — now PROVEN from the mounting code itself, where
every prior round only had the static master.index bytes and the project's
own reader (wolfsdk/resources.py) to go on.
The second function referencing "master.index", FUN_7ff74a4fd090 (RVA
0x16d090), carries its own debug string:
"OpenDLCFileHandles: %d packages mounted.\n" (0x7ff74b1ff5f0). It:
- Asks a session-like singleton (
DAT_7ff74b936d10, vtable+600=+0x258) for a package count, then for each indexiasks the same singleton for a package id (vtable+0x260) and a package path string (vtable+0x268). - For each package: checks whether that id is already present in the
installation's chunk array (by the same per-entry int tag
OpenDLCFileHandleschecks, at struct offset+0x9cof each chunk-array entry — the exact fieldFUN_7ff74a4faf20populates fromDAT_7ff74b933820for base chunks too) — if so, logs"...already loaded, skipping..."and moves on. - Otherwise builds
<package path>\master.index, opens it through the same filesystem singleton call used for the base mount, checks the same"RE5"magic, reads a count and loops reading index/resources filename pairs exactly like §3 — and appends every resulting chunk pair into the very same growable array the base 14 chunks live in (param_1[0x10]/param_1[0x18]/param_1[0x1c], the same structMap_fand the base mount both operate on via the installation object).
Where does that package count/id/path come from? --strings "OpenDLCFileHandles" led to it; searching nearby led to the actual
discovery function, idSessionLocal::EnumerateDownloadableContent (own
string "EnumerateDownloadableContent: %s\n", 0x7ff74b2ad1e8,
FUN_7ff74a87e2b0, RVA 0x4ee2b0):
- Gets the filesystem's base path (vtable
+0x130, arg0) and appends the literal"\dlc"(own string0x7ff74b2ad1e0) — i.e.<install root>\dlc, a sibling ofbase\, not inside it. -
Lists the directory (
FUN_7ff74ada39c0, given a"/"-shaped pattern argument) — a genuine filesystem directory listing, confirmed by the surrounding pure-filesystem call shapes, not a Steamworks call at this step. - For every entry found (name length
> 2): logs"Found DLC '%s'...", builds<entry>/<name>/info.txtvia the template string"%s/%s/info.txt"(0x7ff74b2ad228), checks existence (filesystem+0x88). Missing it logs"Error, found %s directory without info.txt!\n"and the folder is skipped — confirmsr4_tnocustom.md§4's staticdlc\<name>\info.txtfinding directly from the scanning code, not just from a string block. - If present: reads the whole file (filesystem
+0xC8/200, a "read file into buffer" call), parses it with a dedicated parser (FUN_7ff74a8627c0, own string context"info.txt") into a dict-shapeddlcInfoVarsstructure (confirmed field-lookup pattern below) — on parse failure:"...FAILED to parse dlcInfoVars in info.txt (%s)."(0x7ff74b2ad320, exact string match forr4_tnocustom.md's own static citation of this dialect name). - Looks up
appID(own string0x7ff74b2ad2a0) as an integer. Calls a global function-pointer getter (DAT_7ff74af6ec38) for what is, by every contextual shape here (a lazily-fetched global interface, an integer "app id" argument, and the very next log line), almost certainly a SteamworksISteamApps-equivalent accessor; if it returns null (no Steamworks interface at all) or its vtable+0x38call (passed theappID) returns false, the package is rejected outright:"\t\tUser does not own this DLC.\n"(0x7ff74b2ad300) — this is the code-level confirmation ofr4_tnocustom.md§4's "Steam-ownership-gated" read of the staticinfo.txtblock, now proven as an actual branch, not inferred from the string's mere presence. - On success: looks up
dlcID(own string0x7ff74b2ad2a8— resolvingr4_tnocustom.md's own counting caveat about this exact string appearing once as a field name and once inside prose: that prose string,"Found invalid dlcID in info (%d)"at0x7ff74b2ad2d8, is the range check fired when the parseddlcIDinteger is not in1..16inclusive (iVar4 - 1U < 0x10as an unsigned-wraparound range check) — confirmed from the actual comparison, not guessed from the message text alone. On a valid 1–16dlcID: logs"\t\t...successfully mounted '%s. ID: %d\n"(0x7ff74b2ad2b0) and appends the parsed package (path + id) to the session object's own growable DLC-package list (struct offset+0xec78, the exact fieldOpenDLCFileHandlesiterates later via the vtable+0x258/+0x260/+0x268calls in §4 above — confirmed the two functions operate on the same struct by this shared offset, not just by being "nearby" in the binary).
Chain, stated as one sentence: a folder at <TNO install root>\dlc\ <anything>\ containing a valid info.txt with an appID the running
Steam account owns and a dlcID in 1..16 gets its own master.index +
chunk pairs mounted into the live installation on every normal launch,
merged into the exact same chunk array the base 14 chunks live in — proven
end to end through four functions (EnumerateDownloadableContent →
OpenDLCFileHandles → the shared base-mount reader logic reused for the DLC
master.index → Map_f's own existence checks, which (§2.2) do not
distinguish a base-chunk map from a DLC-chunk one at all).
TNO's own Steam AppID, read directly from this machine's own Steam library metadata (read-only, outside both forbidden game-install directories):
<Steam library>\steamapps\appmanifest_201810.acf
"appid" "201810"
"name" "Wolfenstein: The New Order"
Since the ownership check (§4 step 5) is almost certainly a
"does this Steam account own app <id>"-style call, using TNO's own
AppID as the info.txt's appID value means the check is checking
"does the player own the game they are currently running" — trivially true
for anyone who can launch the game at all. This is INFERRED, not tested
(no game was launched) — flagged honestly as the one load-bearing guess this
whole route rests on, and it is exactly what the smallest in-game test below
is designed to confirm or refute cheaply, before anything else is built on
top of it.
--strings "vmtrChunkNumber" found the string but Ghidra's static xrefs were
empty ("no references found" — the known import limitation, stated up
front). Per the task's own fallback method, scanned module.bin directly
for an 8-byte little-endian pointer to that string's address — exactly
one hit anywhere in the whole 36 MB image, at file offset 0x127f758.
Reading the surrounding bytes as a repeating 0x48-byte (9-qword) record
(type-name pointer, an unidentified pointer, the field-name pointer — this
is the slot the search found — a packed (flags:u32 << 32 | structOffset:u32)
value, and two more unidentified slots) gives three consecutive, clearly
decl-field-registration-shaped records:
dlcId struct offset 0x68 (type "int")
vmtrChunkNumber struct offset 0x6c (type "int")
installChunkAllocationNumber struct offset 0x70 (type "int")
Re-derived from scratch, not hard-coded, by
re_probes/tnomaploading/probe_maploading.py (mapInfo field table OK).
vmtrChunkNumber's identity and int typing is now PROVEN from the
engine's own field-reflection table, not just from parsing retail decl
text as r4_tnocustom.md did. New, not in any prior report: the
immediately-preceding field is dlcId — a mapInfo-decl member this
project has never documented, sitting 4 bytes before vmtrChunkNumber in
the parsed struct. No shipped retail mapInfo: decl was ever found setting
a non-default dlcId (consistent with TNO shipping zero DLC content), but
the field being real, typed, and reflected means mapInfo was built with
DLC-sourced maps in mind from the start — and its name lines up exactly with
§4's dlcID (the info.txt field, range 1..16) and OpenDLCFileHandles's
own per-chunk id tag (struct offset +0x9c). INFERRED, not traced: that
a mapInfo decl's dlcId is meant to be set to the same id a DLC package
registers itself under in its own info.txt, as the mechanism that tells
the engine which mounted chunk a given map's content lives in when more than
one DLC is mounted. Not required to use Route 1 below (a single DLC package
with its own self-contained chunk already works via Installation-style
"search every mounted chunk" lookups, per r6_tnodecls.md §4's already-
proven "patch every copy" code read) — flagged as a mechanism worth setting
correctly by analogy, not as a blocking requirement.
Megatexture binding itself (landPageFile/vmtr.pages reading) was already
fully decoded by r6_tnomegatex.md from decl text and file format alone;
this round did not re-derive it and found no code path in Map_f itself
that touches it directly (consistent with megatexture data being resolved
lazily by the renderer from the decl, not by the map-load command).
PROVEN (direct decompilation + own-string identification, this round):
-
map,cmap,devmap,devcmap,checkmapare real, registeredidCmdSystemLocalcommands; all five route through one shared function this round identifies by its own embedded string asidCommonLocal::Map_f(§1–§2). - In the production/retail code path,
Map_f's only map-existence check (maps/<name>/_combo/<name>.proc) is advisory — missing it only logs"You need to build the combo map for: %s"and the function proceeds to the session-start calls regardless (§2.2). -
master.index's magic ("RE5"+ version byte), its count field, and its per-entry two-filename-then-open structure, read directly from the mount function, not from static archive bytes alone (§3). - A genuine, always-live DLC content-package discovery and mount pipeline
exists and is wired end to end:
EnumerateDownloadableContent(disk scan of<install root>\dlc\,info.txt/dlcInfoVarsparse,appIDSteam-ownership check,dlcID1..16range check) feedsOpenDLCFileHandles(opens<package>\master.index, same magic, appends chunk pairs into the same array the base 14 chunks live in, per-id dedup) (§4). This directly refutesr4_tnocustom.md's "nodlc_0-style route exists for TNO" as a blanket statement — true for precedence/override (still true, not challenged here), false for addition, which is a real, shipped, always-running mechanism. - TNO's own Steam AppID is
201810(read from the local Steam library metadata,appmanifest_201810.acf). -
mapInfo's field-reflection table:dlcId(new, struct offset0x68),vmtrChunkNumber(struct offset0x6c),installChunkAllocationNumber(struct offset0x70), all typedint, independently re-derivable by scanning for the one pointer tovmtrChunkNumber's string (§5,probe_maploading.py). - The default map-file extension is
entities, selectable tomap, exactly matchingfile:maps/<id>.entitiesalready found by reading decl text inr4_tnocustom.md— now confirmed from the resolving code itself (§2.1).
INFERRED (plausible from context, not traced to a definite call target):
-
DAT_7ff74c181dd8's exact meaning ("is this a production/shipping run") — based on it being zeroed in the same breath as a loggedidLib::SetProduction( PROD_DEVELOPMENT )call, in the one branch where no base archive could be opened at all (§2.2, §3). The call that sets it non-zero on a normal run was not found. -
DAT_7ff74af6ec38's getter and its vtable+0x38call being SteamworksISteamApps-shaped ownership checks (BIsSubscribedApp/BIsDlcInstalled-equivalent) — based on the argument shape (one integer "app id"), the surrounding log text ("User does not own this DLC"), and the lazy-global-getter pattern, not from a resolved vtable/RTTI name. - That using TNO's own AppID (
201810) as a customdlc\*\info.txt'sappIDwould pass the ownership check (§4.1) — plausible, untested. - That
mapInfo.dlcIdis meant to cross-reference a mounted DLC package's own id (§5) — plausible from the matching field name and the shared1..16/+0x9cid-tag shape, not traced into the code that actually readsmapInfoat map-load time (not found, time-boxed). - What happens downstream of
Map_f'sStartNewGame-equivalent call if the_combo/<name>.procfile genuinely does not exist (§2.3 point 1) — not traced past that call.
FAILED HYPOTHESIS (refined from a prior round, not invented fresh):
-
r4_tnocustom.md(both passes): "Adlc_0-style folder precedence route does NOT exist for TNO" — concluded frommapspecbeing a 0-hit string and no(type,name)ever being duplicated across TNO's 14 base chunks, using the only decompiler available at the time (which indexed TNC's runtime, not TNO's — explicitly flagged as a dead end in that report's own §6). Still true as a precedence/override claim (nothing in this round found a shadowing mechanism), but this round's §4 shows TNO has a different, additive DLC-mount mechanism that is real, compiled in, and runs on every launch — the earlier conclusion was correct about the specific TNC-style mechanism it tested for, and silent about the one that actually exists, because the tooling to find it did not exist yet.
Route 1 — ship a dlc\<name>\ content package with its own master.index + chunk(s) — NEW, strongest evidence, untested in-game
Mechanism: fully proven end to end in code (§3–§4). Build a minimal archive
pair (reusing wolfsdk/resources.py's own writer/reader, already proven
against TNO's real chunk format by r6_tnodecls.md/r4_tnocustom.md) with
whatever mapInfo/file:*.entities/chapter decls are wanted — either
copies of existing maps, edited copies, or (if a new, from-scratch
(type,name) entry is wanted) new entries, which this format accepts far
more readily than TNC's IDCL (r6_tnodecls.md §3: no checksum, no header
hash chain, a resync-tolerant parser, and the append-id rule is now fully
known: max(existing ids of this type in this chunk) + 1, though an actual
append-writer was never built in any TNO round, this one included — still
the single largest remaining engineering gap, not a research gap). Ship a
master.index for just this package (magic "RE5" + version byte + count +
filename pairs, §3) alongside an info.txt with appID=201810 and a
dlcID in 1..16 not already used. Never tested in-game — the
ownership-check guess (§4.1) and the writer itself are both unbuilt/
unverified this round; ranked first purely on code-evidence strength, not
on having been run.
Route 2 — edit an existing campaign map's own chunk in place — unchanged, strongest already-verified evidence
Everything r4_tnocustom.md/r6_tnodecls.md/r6_tnomegatex.md already
proved end-to-end against the real write path is untouched by this round:
mods/supersoldaten_c15 (10/25 maps are genuinely in-place-rewritable,
c05_07_09 chosen as the smallest real example, 12/12 offline checks pass
including negative controls). This round's §2.2 finding is actually good
news for this route specifically: since the _combo/<name>.proc check is
advisory, an in-place .entities edit that doesn't touch geometry (like
supersoldaten_c15's enemy-type swap) was never at any risk from that check
in the first place — it only matters for a map whose _combo folder
doesn't exist at all, i.e. Route 1's brand-new maps, not Route 2's edits of
existing ones.
Route 3 — self-built extra chunk pair appended directly to the base master.index — superseded by Route 1, not recommended
r4_tnocustom.md listed this as untested/unmeasurable without a TNO runtime
image. That image now exists, and this round found a better-evidenced way to
get the identical technical effect (an extra chunk pair merged into the live
installation) through a mechanism the retail binary already implements and
runs unconditionally (Route 1) — hand-editing the base master.index's own
count/entry list would touch the one file every other mod and the base game
itself depends on, for no benefit Route 1 doesn't already offer more safely.
Not re-tested this round; superseded, not refuted.
- Is
DAT_7ff74af6ec38's target genuinely Steamworks, and doesappID=201810actually pass its ownership check? The single most important unresolved question this report produced — see the in-game test below, which is designed to answer exactly this, cheaply, before anything else is built. - What happens after
Map_f'sStartNewGame-equivalent call if_combo/<name>.procgenuinely does not exist (§2.3) — does the renderer silently skip world geometry, or crash later? Not traced. - Where is
mapInfo/vmtrChunkNumberactually read at map-load time (which function resolves a map name to "which mounted chunk" via this field)? Not found this round —Map_fitself never references it. - Does a mounted DLC package's
mapInfo.dlcIdneed to match its owninfo.txt'sdlcIDfor anything to work, or is chunk search purely content-based regardless of this field (asr6_tnodecls.md§4's "patch every copy" code read would suggest)? INFERRED only (§5). - An append-entry writer for a brand-new
(type,name)decl inside a from-scratch chunk —r6_tnodecls.md's own open item, still unbuilt here too; Route 1 does not strictly need it (copying/editing existing decls' text into a new chunk works without ever appending a new entry type-name pair, since the new chunk's own index is built fresh, not patched) — but building it would let Route 1 also start from a genuinely blank slate instead of copied retail content.
(Translated from German. The example folder name has been changed to a neutral
mod_test; any folder name works.)
Goal: check the one assumption this whole new route rests on as cheaply
as possible — whether TNO's dlc\ folder scan runs in retail at all,
and whether the Steam ownership check passes with the game's own app ID —
before anything bigger is built.
Step 1 (without any file, pure scan evidence):
- Create an empty folder
dlc\mod_test\directly next tobase\(<Steam library>\steamapps\common\Wolfenstein.The.New.Order\dlc\mod_test\) — no file in it, only the empty folder. Not a single original file is touched. - Start the game normally, then search the log file
(
%USERPROFILE%\Saved Games\MachineGames\Wolfenstein The New Order\base\qconsole.log) after the start. - Expected, if the code path found in this report actually runs
in retail: a line
Found DLC 'mod_test'...followed byError, found mod_test directory without info.txt!— that confirms risk-free that thedlc\scan is active in the real retail build (not only compiled in, but dead).
Step 2 (only once step 1 shows the lines):
- Put an
info.txtinto the same folder with (raw dialect form, asr4_tnocustom.md§4 already found statically — field names now confirmed from the code, §4 above):appID 201810 dlcID 1 - Restart the game, check
qconsole.logagain:- Line
...successfully mounted 'mod_test. ID: 1→ the ownership check with the own app ID passes — that is the confirmation this whole route is waiting for, and makes route 1 the next sensible expansion stage (ownmaster.index+ map data in this folder). - Line
User does not own this DLC.→ the assumption from §4.1 is refuted, the own app ID is not enough; then the next step would be to try another, guaranteed-owned app ID (e.g. another already installed Steam app ID from the same account). - Neither of the two lines, or a crash → the parser
(
dlcInfoVars) expects a different format than assumed here; then"...FAILED to parse dlcInfoVars..."would be the next line to search for.
- Line
Both steps are a pure folder/file-creation test outside any
original file, nothing is overwritten, nothing has to be reset
except deleting the folder dlc\mod_test\ again.
python tools/decomp.py --strings "loads a combo map" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
python tools/decomp.py 0x148ec0 --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf # idCommonLocal::Map_f
python tools/decomp.py 0x16af20 --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf # base master.index mount
python tools/decomp.py 0x16d090 --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf # OpenDLCFileHandles
python tools/decomp.py 0x4ee2b0 --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf # EnumerateDownloadableContent
python re_probes/tnomaploading/probe_maploading.py
# 20/20 known strings match
# negative control ('doesNotExistTNO_xyz_marker'): not found (expected)
# mapInfo field table OK
# ALL OK
Related: Research Report: R4 Tno Custom Maps, Research Report: R6 Tno Console, Research Report: R6 Tno Decls, Research Report: R6 Tno Megatexture, File Formats, TNO Maps, Asset Loading Paths.
English Wiki generated from the measured research notes in docs/. Evidence grades and uncertainty are preserved intentionally.
- WolfSDK Loader · DE
- WolfSDK Studio · DE
- FAQ · DE
- Mod format · DE
- Modding Guide
- Modding Surface
- CVar Catalog
- CVar Validation (old scrape)
- Texture Database
- Developer Mode
- Dev Menu
- Custom-Map Status