Round-7 research pass on The New Order's console commands, read from decompiled code: the registration call has no flags parameter at all, and every registered command was enumerated (from direct call sites plus a self-registering list Ghidra's xrefs missed). map, devmap, kill, teleport, setviewpos, screenshot and many more are real commands; god, noclip, notarget, give and spawn are not — they exist only as DevGUI label text, which corrects round 6. timescale is a cvar. Static only; the start-argument test at the end has not been run.
Track tnocommands. Builds on re_probes/agent_reports/r6_tnoconsole.md (TNO's
restriction model, static-only: no runtime image existed yet, DevGUI table mined
from raw .rdata string adjacency). This track has something r6 didn't: TNO now
has a decrypted, live runtime capture (re_probes/runtime_tno_186352/module.bin,
base 0x7ff74a390000, pid 186352, .text/.rdata/.data/.pdata all captured)
and a Ghidra decompiler over it (tools/decomp.py ... --gzf
D:/wolf-decomp/ghidra/tno_runtime.gzf). Everything below is either decompiled C
read straight out of that capture, or bytes read directly from the capture's
.data (which holds real, live pointer values from the process that was
running when it was captured, not static-image placeholders) — not string
adjacency. Read-only throughout: nothing was launched, nothing attached, no file
was written outside re_probes/tnocommands/ and this report. New files:
re_probes/tnocommands/mine_commands.py (the miner, reproducible,
--self-check-free but every address it prints is independently re-derivable
from the comments inline) and re_probes/tnocommands/commands.json (192
commands: name, flags, description, callback RVA, arg-completion RVA, source).
Question
Find TNO's command registration call (AddCommand-equivalent: name/callback/
flags/description), dump every registered command with its flags, and
determine which of map/devmap/god/noclip/notarget/give/spawn/
kill/teleport/setviewpos/timescale/screenshot are usable from the
retail console right now.
Answer (short)
idCmdSystem::AddCommand genuinely has no flags parameter in TNO — proven
from decompiled code, not inferred from an absent string. The singleton is
idCmdSystemLocal (RVA 0x15ba120, vtable RVA 0x104fa68, identity confirmed
against docs/ghidra/rtti_tno.json, not just by convention). Its AddCommand
(vtable slot +0x18, RVA 0x9edf30) takes exactly four arguments —
(name, callback, description, argCompletion) — read directly out of its own
decompiled body (§1) and confirmed again by independently reading every one of
192 distinct real call sites this track found (§2/§3): not one passes a
fifth, integer "flags" argument; the slot is either 0 or an argCompletion
function pointer. There is no CMD_EXPOSE/CMD_CHEAT/CMD_SHIPPINGDISABLED
bit to find because the call simply has nowhere to put one — this closes
out r6's inference ("no CMD_* enum found by string search") with the
stronger claim the task asked for: the registration call itself has no such
parameter, full stop.
map/devmap/cmap/checkmap/kill/teleport/setviewpos/getviewpos/
centerview/where/screenshot/rawscreenshot/envshot/toggle/bind/
unbind/reset/exec/vstr/listCmds/killMonsters and 172 more are all
real, registered commands (full table in commands.json), and since
there is no flags concept at all, there is no "exposed vs. non-exposed" split
the way TNC has — once the console/dispatcher is reached at all (the still-open
com_allowConsole question from r6 §2), any of these 192 should be callable by
name with no further per-command gate.
god, noclip, notarget, give, spawn, killPlayer (bare), changeTeam,
NoPlayerDeath are not in this table — this corrects r6, which called some of
these "clean, wired Label→Action pairs." §4 shows exactly where those strings
actually live: inside the DevGUI row-builder function, passed to a widget
constructor that itself calls idCmdSystemLocal's lookup function
(CommandExists, vtable +0x28, decompiled in §4) to validate the string before
treating it as runnable — and that lookup walks the exact same linked list
AddCommand populates (both read/write the same [this+8] head pointer,
confirmed in each one's own decompiled body). Since these names are absent from
that list under both of this track's independent enumeration methods (§2
direct-call-site scan, §3 self-registrar-list walk) and have no second code
reference anywhere outside the DevGUI table (§4b), they would fail that very
validation — they are DevGUI label text for commands that were apparently
planned but never wired to a real AddCommand call in this retail build, not
functioning commands with a cheat flag. timescale is real but is a cvar,
not a command (§5) — timescale .1 works as <cvar> <value>, the same syntax
DevGUI's own "Slow Time" row uses.
1. The registration call, read out of its own decompiled body
String search (direct bytes.find, not adjacency) locates
"idCmdSystemLocal::AddCommand: %s already defined\n" at RVA 0x1053d20.
tools/tnc_runtime_code.py's Image.xrefs() (a RIP-relative-lea byte scanner
over the whole .text, independent of Ghidra) finds exactly one reference
to it anywhere in the binary: RVA 0x9ee014, inside the function
(0x9edf30, 0x9ee025). Decompiled (tools/decomp.py 0x9edf30):
void FUN_7ff74ad7df30(longlong param_1 /*this*/, undefined8 param_2 /*name*/,
longlong param_3 /*callback*/, undefined8 param_4 /*description*/,
undefined8 param_5 /*argCompletion*/)
{
puVar1 = *(undefined8 **)(param_1 + 8); // walk the command list head
while (true) {
if (puVar1 == 0) { // not found -> allocate + link new node
... puVar1 = Heap::Alloc(0x28); // 5 x 8-byte fields = 40 bytes
puVar1[1] = param_2; puVar1[3] = param_5; puVar1[2] = param_3; puVar1[4] = param_4;
*puVar1 = *(undefined8*)(param_1+8); // node->next = old head
*(undefined8**)(param_1+8) = puVar1; // head = node
return;
}
if (0 == func_0x7ff74ad559d0(param_2, puVar1[1])) break; // name match (idStr::Cmp)
puVar1 = (undefined8*)*puVar1;
}
if (param_3 == puVar1[2]) return; // same callback already registered -> no-op
FUN_7ff74ad5a710(s_idCmdSystemLocal__AddCommand___s_already_defined, param_2); // warn
}
Node layout, read straight from the storage order above:
{next, name, callback, argCompletion, description} — four data fields,
not five. There is no slot for flags anywhere in this struct. This one
function is called through vtable slot +0x18 on the idCmdSystemLocal
singleton (global pointer at RVA 0x15ba108, which this capture shows
pointing at 0x7ff74b94a120, RVA 0x15ba120 — a static object in .data,
vtable RVA 0x104fa68). Cross-checked against docs/ghidra/rtti_tno.json
(adjusting for its static image base 0x140000000 vs. this capture's runtime
base): 0x14104fa68 → "idCmdSystemLocal" — an independent, RTTI-sourced
confirmation this is the right object, not just the right-shaped one.
idCmdSystemLocal's lookup method (vtable +0x28, RVA 0x9edc20) walks
the same [this+8] head:
undefined4 FUN_7ff74ad7dc20(longlong this, undefined8 text, char tokenize)
{
puVar1 = *(undefined8**)(this + 8);
while (puVar1) {
if (tokenize == 0) { if (0 == idStr_Cmp(text, puVar1[1])) return 1; } // exact match
else { if (-1 != idStr_Icmpn_or_similar(text, puVar1[1],0,0,0xffffffff)) return 1; } // prefix match
puVar1 = (undefined8*)*puVar1;
}
return 0;
}
(idCVarSystemLocal's own equivalent, RVA 0x9dd630, is the same two-mode
shape over its own hash table — confirmed by decompiling it too, not assumed
by symmetry.)
2. 147 commands from 147 direct call sites across 12 functions
Image.xrefs()-independent, tools/decomp.py 0x9edf30 --xrefs lists every
xref TO the registration function: 152 COMPUTED_CALLs across 12 distinct
caller functions (Ghidra resolved these because the vtable pointer chain is
concrete data in this live capture, not a relocation it has to guess).
11 of the 12 are large, flat Init()-style functions that just call
AddCommand once per command in a row (e.g. FUN_7ff74abddb00, RVA
0x84db00, 67 calls — engine/AI/debug commands: kill, teleport
(argCompletion 0x852d90, entity-name completion), setviewpos, getviewpos,
centerview, where, killMonsters, killAI, healAI, testWeapon,
DebugUnlockPerkByAbility, ...; FUN_7ff74a4c7950, RVA 0x137950, 45 calls —
map/cmap/devmap/checkmap/slowmap (shared argCompletion 0x9ef070,
a map-name completer), quit/exit, writeConfig, saveGame/loadGame,
vid_restart, demorecord*, ...). The 12th caller is the one call inside a
for loop over a second mechanism (§3) — Ghidra's computed-call analysis
found it as one call site (correct: it's one instruction, executed N times at
runtime), but resolving what it registers needs the linked-list walk below,
not decompiled text.
re_probes/tnocommands/mine_commands.py parses every one of these direct call
sites' decompiled C: resolves name/description by re-reading the full,
untruncated string straight from module.bin at the VA Ghidra's own label
encodes (Ghidra's auto-labels truncate — e.g. its label says
exportDuplicateCollisionGeometry, the real, in-game name is
exportDuplicateCollisionGeometryModel, recovered correctly by this script
precisely because it never trusts the label text, only the VA suffix it
contains), and takes callback/argCompletion RVAs directly from Ghidra's
FUN_<va>/SUB_<va> naming (which already is the VA). 147 unique names
resolved (151 individual call instructions minus 4 incidental name collisions);
one call (the loop) correctly flagged UNRESOLVED by the script and handled
separately.
3. +45 more from a self-registering linked list Ghidra's xrefs missed entirely
idCmdSystemLocal::Init itself (RVA 0x9efa70, found the same way as §1 — by
locating the six always-present built-ins listCmds/exec/vstr/echo/
parse/wait via Image.xrefs() and decompiling their containing function)
registers those six, then:
for (puVar3 = func_0x7ff74ad95fd0(); puVar3 != 0; puVar3 = *puVar3)
AddCommand(this, puVar3[1], puVar3[2], puVar3[3], puVar3[4]);
func_0x7ff74ad95fd0 (RVA 0xa05fd0) is a one-line mov rax,[rip+disp]; ret
reading a global at RVA 0x1f01640 — the head of a static, self-registering
command list (same shape as TNC's own, see re_probes/agent_reports/
cmdtable.md §2: C++ global objects that link themselves into a list at load
time; the matching node-constructor is at RVA 0xa05fe0). This is exactly the
registration path the task brief warned about ("Ghidra's static xrefs are
incomplete... find references yourself"): idCmdSystemLocal::AddCommand's own
xrefs TO list (§2) does not include this loop's callees individually —
only the one loop-call instruction — because the five arguments it passes come
from runtime-walked heap data, not compile-time constants Ghidra's constant
propagation can fold.
Walked directly from the capture (re_probes/tnocommands/mine_commands.py
walk_registrar_list(), live pointer values, not guessed): head
0x1f01640 → 0x7ff74c294f20, 45 nodes, 0 cycles, terminates cleanly at
null. This is where toggle ("toggles a cvar"), bind/unbind/unbindAll/
listBinds, reset ("resets a cvar to its default value"), cvar_restart,
listCvars, connect/startServer, messagemode, print/clear/history/
con_watch/con_unwatch/activateConsole, idStudio ("design studio for the
game"), makeDeclTree, three ps3_test* leftovers, and more come from — none
of these were in §2's direct-call-site set, and all of them are commands TNC
has too (per cmdtable.md), which is why their total absence from §2 alone
would have been a red flag, not a clean result.
147 + 45 = 192 distinct commands, zero overlap in name between the two
sources (re_probes/tnocommands/commands.json, full table with callback_rva/
arg_completion_rva/source per entry). flags is kept as a JSON field for
parity with TNC's table shape and is null on every single entry — not a
placeholder, a fact (§1).
4. Where god/noclip/notarget/give/spawn actually live
Direct string search in module.bin for god\0, noclip\0, notarget\0,
give throwable/player/grenade, killPlayer %d: each exists exactly once
in the whole 36 MB capture, all clustered at RVA 0x103a0a4–0x103b600 — the
DevGUI label/action table region r6 already found by string adjacency. This
track's addition: Image.xrefs() shows each of those five strings has
exactly one code reference (a RIP-relative lea) anywhere in .text, and
all five land inside the same function, FUN_7ff74ad1a100 (RVA 0x98a100)
— the DevGUI page builder. Decompiled excerpt (full page-builder function is
~230 lines; this is the "God Mode" row, byte-identical pattern for every
checkbox row on the page):
uVar10 = FUN_7ff74ad1fee0(&list);
uVar10 = FUN_7ff74ad200c0(uVar10, s_God_Mode); // set row label "God Mode"
uVar10 = FUN_7ff74ad20380(uVar10, &DAT_..."god", bVar15); // set row's command text = "god"
FUN_7ff74ad20690(uVar10, uVar6); // set row's current-checked bool
FUN_7ff74ad20380 (RVA 0x990380) is the actual widget constructor, and this
is where the lookup from §1 gets used:
longlong FUN_7ff74ad20380(longlong this, undefined8 commandText, char doCheck) {
... allocate a &vtbl_cellCmd_t node, store commandText in it ...
if (doCheck != '\0') {
bool ok = idCVarSystemLocal->Exists(commandText, 1); // vtable+0x28 on DAT_..a110
if (!ok) ok = idCmdSystemLocal->Exists(commandText, 1); // vtable+0x28 on DAT_..a108 (§1)
if (!ok) FUN_7ff74ad5a7b0(s_idGuiListBox__Command___Command___not_found, commandText);
}
...
}
DAT_7ff74b94a110 and DAT_7ff74b94a108 are two adjacent globals; reading
their live pointee vtables from the capture and matching against
docs/ghidra/rtti_tno.json identifies them unambiguously:
0x14104fa68 → idCmdSystemLocal, 0x14104f968 → idCVarSystemLocal. So this
widget validates its command text against both real systems, using the
tokenize/prefix mode (doCheck's 3rd arg 1) — meaning a multi-word action
like "toggle g_showHud 0 1" is checked by its first token (toggle, a real
command, §3) and a bare word like "god" is checked as a whole — and "god"
is in neither system's list (§2/§3 exhaustively enumerate both). Per-string
verdict, all re-derived the same way:
| DevGUI label | action text | first-token check target | found in §2/§3? |
|---|---|---|---|
| God Mode | god |
idCmdSystemLocal (cmd) |
no |
| No Clip | noclip |
idCmdSystemLocal (cmd) |
no |
| No Target | notarget |
idCmdSystemLocal (cmd) |
no |
| Give Grenades | give throwable/player/grenade |
give (cmd) |
no |
| Spawn Imp | spawn zion/ai/imp/demon_imp |
spawn (cmd) |
no |
| Suicide | killPlayer %d |
killPlayer (cmd) |
no |
| No Player Death | NoPlayerDeath |
NoPlayerDeath (cmd) |
no |
| Set Team Red/Blue | changeTeam 1/2 |
changeTeam (cmd) |
no |
| Remove AI | killMonsters |
killMonsters (cmd) |
yes (§2) |
| Show Hud | toggle g_showHud 0 1 |
toggle (cmd) |
yes (§3) |
| Show Hands | toggle hands_show |
toggle (cmd) |
yes (§3) |
| Slow Time | timescale .1 |
timescale (cvar, §5) |
yes (cvar list) |
This both confirms and corrects r6. r6 correctly found notarget/give.../
spawn.../killMonsters/NoPlayerDeath/changeTeam as DevGUI-row strings and
called them (hedged, but still) "clean, wired Label→Action pairs." This track
shows killMonsters is the only one of that list that is actually a real,
reachable command; the rest are row text for a validation check (shown above,
decompiled) that they would fail. Whether that check actually runs at
runtime in retail (doCheck/bVar15, computed once per DevGUI page from
*(char*)(pageObject+0x58), meaning untested — purpose of that one byte is
not identified) doesn't change the practical answer: even if the check is
skipped, clicking the row still has to eventually dispatch "god" through
idCmdSystemLocal's executor, which — per §2/§3's exhaustive enumeration of
every node that executor can find — doesn't have an entry for it either.
5. timescale is a cvar, not a command
Raw string search: timescale\0\0\01\0\0\0puts the game in slowmo\0 — name,
then a bare numeric-literal token (1, the cvar's default), then the
description, in that exact order. This is the standard cvar-registration
string-adjacency shape (docs/cvars_tnc.json's own method, and r6 §4's for
TNO) — a command's registration (§1/§2) never has a literal token between name
and description, only a cvar's does. Consistent with swf_timescale existing
right next to it (a second, UI-animation-speed cvar with the same naming
pattern). Not in commands.json by design — it is a real, working cvar, just
not a command, so timescale .1 at the console works as "set this cvar to
.1", not as a command invocation with an argument.
PROVEN (this track — decompiled code and/or live .data values, not string adjacency)
idCmdSystemLocalsingleton RVA0x15ba120(pointer at0x15ba108), vtable RVA0x104fa68, RTTI-confirmed (docs/ghidra/rtti_tno.json,0x14104fa68 → "idCmdSystemLocal").AddCommand(vtable+0x18, RVA0x9edf30) takes exactly(name, callback, description, argCompletion)— no flags field exists in the node or the call, read from the function's own decompiled body, not inferred from a missing string.CommandExists-equivalent (vtable+0x28, RVA0x9edc20) walks the exact same[this+8]linked listAddCommandwrites to, in two modes (exact / tokenize-prefix).idCVarSystemLocalsingleton identified the same RTTI-confirmed way (0x14104f968), its own+0x28lookup (RVA0x9dd630) decompiled and confirmed to be the same two-mode shape over its own hash table.- 192 distinct, real, registered commands recovered by two independent
methods (147 via 12 direct
AddCommandcall-site functions, §2; 45 via walking the self-registering linked list at RVA0x1f01640, §3) —re_probes/tnocommands/commands.json. Zero name overlap between the two sources. god,noclip,notarget,give,spawn,killPlayer(bare),NoPlayerDeath,changeTeamare absent from both sources, and have zero code references anywhere outside the single DevGUI-table-builder function that stores them as display/validation text (Image.xrefs(), independent of Ghidra, one hit each).timescaleis a cvar (default1, description "puts the game in slowmo"), not a command.map's real description is "loads a combo map" (§2/commands.json) — corrects r6 §4's adjacency-mined guess ("shows collision model info", which r6 itself flagged as a likely-wrong neighbour grab).
INFERRED (plausible, not fully nailed down)
- Whether the DevGUI's
doCheck/bVar15validation (§4) actually executes in the shipped retail build, or whether*(char*)(pageObject+0x58)is always the value that skips it — not identified; doesn't change the practical conclusion (§4, last paragraph) either way. - r6's standing conclusion that TNO's command dispatch is architecturally
ungated (no per-command restriction level the way TNC has) is now proven at
one more remove: not just "no flag enum found by string search" but "the
registration call has no parameter for one." Whether the console itself
(
com_allowConsole) is reachable in retail remains exactly as open as r6 left it — this track is about the command table, not the console-open gate, and found nothing new on that front.
Failed hypotheses / dead ends (this track)
- A second, flagged
AddCommandoverload for "cheat" commands (the TNC pattern,0x200000-flaggedGod/InfiniteHealth/NoPlayerDeath/Notarget, percmdtable.md§6): searched for by checking every one of §2/§3's 192 call sites for an extra integer argument distinct from0or a function pointer — none found; also checked whetheridCmdSystemLocalhas a second vtable slot with an AddCommand-shaped signature (5 args) — vtable slots+0x18(AddCommand) and+0x28(Exists) are the only two this track needed and found; no evidence of a third. Concluded: this engine version genuinely predates that TNC-only mechanism, not just "didn't look hard enough" (same conclusion r6 reached from the negative string search, now from the call-site shape itself). - A naive full-
.textlinear capstone disassembly scanning for directcall 0xa05fe0(the registrar-node constructor), to cross-check the linked-list walk's node count independently: 0 hits, almost certainly because linear disassembly of a 12 MB section desyncs after the first embedded-data byte run (jump tables, padding) — this is exactly why every other tool in this repo disassembles per-.pdata-function-range instead of linearly; dead end, abandoned in favor of the (already-working, cross-validated) linked-list walk. - Treating the DevGUI label/action table (r6 §4b) as itself a command
registry: it isn't one — it is the consumer of
idCmdSystemLocal's table (§4), one level downstream, which is exactly why 8 of its entries reference commands that were never actually registered.
Recipe
# find a command/cvar's registration site from scratch, same method as this track:
python3 - <<'EOF'
data = open("re_probes/runtime_tno_186352/module.bin","rb").read()
i = data.find(b"YOUR_NAME_HERE\x00") # NUL-bounded, exact
print(hex(i))
EOF
PYTHONPATH=re_probes/python_deps python3 -c "
import sys; sys.path.insert(0,'tools')
from tnc_runtime_code import Image
im = Image('re_probes/runtime_tno_186352')
print(im.xrefs('YOUR_NAME_HERE'))" # -> containing function's RVA range
python tools/decomp.py <that RVA> --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
# regenerate the full table (12 direct-call functions + the self-registrar walk):
python re_probes/tnocommands/mine_commands.py --text <decomp.py dump of all 12> \
--out re_probes/tnocommands/commands.json
Open questions
- Is
com_allowConsolereachable in retail (r6's own open question — this track adds nothing new here, since it studied the command table, not the console-open gate). - What is
*(char*)(devguiPage+0x58)("doCheck"/bVar15in §4) actually wired to — a build config, an edit-mode flag, something else? Static-only answer wasn't findable without walking that object's constructor(s), which this track judged not worth the budget given the practical conclusion doesn't depend on it. - Were
god/noclip/notarget/give/spawn/etc. commands in an earlier build (pre-release, debug) that got stripped, leaving only their DevGUI label text behind? Plausible given the naming (DebugUnlockPerkByAbilityetc. did survive) but unverifiable from one retail binary alone.
Smallest in-game test
(Translated from German.)
Without a mod, only start arguments:
WolfNewOrder_x64.exe +bind F5 god +bind F6 noclip +bind F7 notarget +bind F8 kill +bind F9 teleport +bind F10 "map test"
Start the game (console not needed — according to docs/limits.md/r6, +bind is processed
even without console access), load a save, then press F5, F6,
F7 in order and search qconsole.log
(%USERPROFILE%\Saved Games\MachineGames\Wolfenstein The New Order\base\qconsole.log)
afterwards:
- If
Unknown command 'god'or'noclip'/'notarget'appears after F5/F6/F7 → confirms this report directly: these three are not real, callable commands in retail, regardless of whether the console is open or not (section 4 — they exist only as text in the DevGUI table, never as anAddCommandcall). - F8 (
kill) and F9 (teleport), by contrast, should go through withoutUnknown command(killkills the player immediately,teleportwithout a target probably does nothing visible except producing no error message) — both are documented inre_probes/tnocommands/commands.jsonwith a real callback RVA. - F10 (
map test) should not showUnknown command, but either load or abort with a different, map-specific error message (map/devmapare registered, §2) — that is the difference from r6's so far unanswered question of whether the dispatcher is reached at all: anUnknown command 'map'line, on the other hand, would mean that even registered commands do not arrive via+bind, which would put this whole report into perspective (negative control for the central claim).
Back up saves first (as already recommended in r6) — unclear whether TNO marks saves when entering a cheat state.
Related: Research Report: R6 Tno Console, Research Report: Cmdtable, Research Report: R7 Tno Cheat Gate, Modding Limits, TNO CVar Catalog.