Repository navigation
Research Report R7 Tno Cheat Gate
Round-7 research pass reading The New Order's cheat gate as decompiled code from the decrypted runtime image. One 32-bit global, never written anywhere in the shipped binary, gates everything: in idCommonLocal::Init it triggers the reset of all CVAR_CHEAT cvars, and at the dispatcher's bare name value fallback it rejects them. A second pass (appended below) finds no-patch routes: the console opens on Ctrl plus the ^/~ key rather than through com_allowConsole, and toggle and the other typed cvar setters take a force path past the gate. Static evidence only; nobody has pressed Ctrl+^ in TNO yet. The loader-side follow-up is TNO cvar registry (R8).
Read-only. Track tnocheatgate. Builds directly on r6_tnoconsole.md (static-only,
encrypted .text: no CMD_* flag enum, CVAR_CHEAT 0x8/CVAR_NOCHEAT 0x10 flag
bits recovered from .rdata strings, wolfsdk/cvars.py::RETAIL_BLOCKED — 25 cvars
measured in a real run to print "%s CANNOT be set in retail!") and on
docs/devmodus.md (the flag enum). The difference this pass: a decrypted runtime
snapshot now exists (re_probes/runtime_tno_186352/module.bin, captured read-only
from the live, running retail game, pid 186352, base 0x140000000/rebased to
0x7ff749000000 in the snapshot) with a matching Ghidra GZF
(D:/wolf-decomp/ghidra/tno_runtime.gzf), so for the first time the TNO cheat gate
can be read as actual disassembled/decompiled code instead of inferred from string
adjacency. Nothing was launched, nothing was attached to, no game or save file was
touched. New files: this report and re_probes/tnocheatgate/find_gate_writes.py
(the reproducibility script for §2's exhaustive write-search, --self-check re-runs
and re-verifies the central claim every time).
Where exactly, in code, does TNO reject or reset cheat cvars — the Set() path,
the source/flags check, any "cheats enabled" global, developer mode, net/session
cheat flags — what conditions allow them, and is there a no-patch route (config
file, exec path, DevGUI action, startup order) the loader could use to set them?
One single 32-bit global, never written anywhere in the shipped binary, gates
everything. Ghidra-labelled DAT_7ff74c1821c8 (RVA 0x1df21c8, in .data,
read live as 0 from the running process). It is read with a plain
cmp dword ptr [rip+disp], 0 at two load-bearing sites plus, separately,
~70 more sites scattered through gameplay/physics code (same global, same
== 0 idiom, most likely individual cheat-adjacent command implementations each
guarding themselves at entry — not run down to identity, see §4) — and zero
write sites anywhere in the 12 436 956-byte .text section, checked
exhaustively across every RIP-relative x86-64 addressing form (find_gate_writes.py,
self-check passes). The two load-bearing sites:
-
idCommonLocal::Init(FUN_7ff74a4c3380, RVA0x133380), right after it logs"Command line is: %s\n"and before it execsdefault.cfg:i.e. if the global is 0, every00133e0a cmp dword ptr [rip+0x1cbe3b7], 0 ; RVA 0x1df21c8 (DAT_7ff74c1821c8) 00133e11 jne 0x133e28 00133e13 mov rcx, [rip+0x1486314] ; RVA 0x15ba110 (idCVarSystem singleton) 00133e1a mov rax, [rcx] 00133e1d mov edx, 8 ; CVAR_CHEAT = 0x8 00133e22 call [rax+0xa0] ; idCVarSystem::ResetFlaggedVariables(CVAR_CHEAT) 00133e28 ...
CVAR_CHEAT-flagged cvar is force-reset to default right here, logging"Resetting cheat cvar: %s\n"per cvar (FUN_7ff74ad6daa0/its twinFUN_7ff74ad6dabc, both justfor (cvar = g_cvarListHead; cvar; cvar = cvar->next) if (cvar->flags & mask) reset(cvar)). This runs after command-line+cvar valuepairs have already taken effect (they are applied before this point — see §5) and beforedefault.cfgis executed at all. -
The cvar-assignment fallback inside the command dispatcher
(
FUN_7ff74ad7e102, RVA0x9ee102— this isidCmdSystemLocal's token-execute loop: it walks the hash-bucketed registered-command table, and only when a typed/exec'd token matches no registered command at all does it fall through to(*vtbl[0x78])()on the cvar-system singleton — this vtable slot isFUN_7ff74ad6d810(confirmed by Ghidra's ownCOMPUTED_CALLresolution at the call site). This is the actual, single choke point every barename valueline goes through, whether typed at a console, sitting indefault.cfg, orexec'd from any.cfg— not a dedicatedset/setacommand, the generic "token isn't a command, try it as a cvar" fallback itself):(if ((DAT_7ff74c1821c8 == 0) && ((cvar->flags & 0x10) == 0)) { pcVar7 = "%s CANNOT be set in retail!"; } else { ... actually assign the value, then printf("\"%s\" is:\"%s\"^7 default:\"%s\"\n", ...) ... }
0x10=CVAR_NOCHEATperdocs/devmodus.md's enum — a cvar carrying that bit is exempt and always settable; a cvar without it needs the global nonzero. Two other, unrelated gates sit in the same function and fire first:CVAR_INIT(0x4000, "can only be set on the command line") andCVAR_ROM(0x8000, "is read only").)
Both checks read the exact same global, with the exact same == 0 test, and
nothing in the shipped .text ever sets it to anything else. That is the
entire "cheat gate": not a build-type enum, not a signed/encrypted check, not a
sv_cheats-equivalent cvar of its own (it has no name string anywhere — it is
addressed only ever by raw RIP-relative displacement, never printed, never
listed) — a single always-zero flag. No config file, no exec path, no
command-line cvar/bind, no DevGUI action can set it, because none of those
channels write raw process memory; they all route through cvars/commands, and
the flag itself is not a cvar. The only way MachineGames' own dev/QA builds
would ever have had it nonzero is a build-time code path (#ifdef out in
retail) that simply isn't present in this binary at all — consistent with
docs/devmodus.md's and r6's independent finding that no devMode_t/
com_enableDeveloperMode/CVAR_SHIPPINGDISABLED exists in TNO either. This is
a stronger, code-level restatement of what docs/limits.md already
measured empirically (every RETAIL_BLOCKED cvar in wolfsdk/cvars.py really
is blocked, through every channel tested) — now with the exact instruction and
the proof that it is not a channel/source distinction at all, but one
dead global.
python tools/decomp.py --strings "CANNOT be set in retail" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
=== strings matching 'CANNOT be set in retail' ===
0x7ff74b3e19f8 (rva 0x10519f8) "%s CANNOT be set in retail!"
<- FUN_7ff74ad6d810 @ 7ff74ad6d810
<- FUN_7ff74ad6d2d0 @ 7ff74ad6d2d0
python tools/decomp.py --strings "Resetting cheat cvar" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
=== strings matching 'Resetting cheat cvar' ===
0x7ff74b3e1a60 (rva 0x1051a60) "Resetting cheat cvar: %s\n"
<- FUN_7ff74ad6dabc @ 7ff74ad6dabc
Unlike r6 (encrypted .text, 0 xrefs ever resolvable), these xrefs are real —
the Ghidra GZF was built from the decrypted snapshot. FUN_7ff74ad6d810
decompiles to the Set-fallback handler quoted in the Answer; its only
caller via Ghidra's COMPUTED_CALL resolution is FUN_7ff74ad7e102 at RVA
0x9ee13f — the command-dispatcher's "token didn't match any command, try the
cvar system" fallback (full decompile in §3). FUN_7ff74ad6dabc (the
ResetFlaggedVariables body) itself has no static callers (small, tail-
duplicated loop body — a second, byte-identical copy of the same loop,
FUN_7ff74ad6daa0, is the one actually reachable, see §2); its one real caller
is idCommonLocal::Init (FUN_7ff74a4c3380, RVA 0x133380), found by
disassembling the COMPUTED_CALL Ghidra attributed to it from RVA 0x133e22
(full byte trace in §1b).
// FUN_7ff74ad6d810, RVA 0x9dd810 -- idCmdSystem's cvar-set fallback
undefined8 FUN_7ff74ad6d810(longlong *param_1, int *param_2)
{
...
lVar3 = (**(code **)(*param_1 + 0x20))(param_1, puVar8); // look up the cvar by name
if (lVar3 == 0) return 0; // not a cvar either -> caller prints "Unknown command"
if (*param_2 == 1) { ... print current value and default ...; return 1; } // bare "name" with no value
lVar5 = FUN_7ff74ada3cb0(param_2,1,0xffffffff,0,0); // join remaining tokens into the value string
uVar1 = *(uint *)(lVar3 + 0x48); // cvar->flags
if ((uVar1 >> 0xe & 1) != 0) { // bit 0x4000 = CVAR_INIT
print "%s can only be set on the command line"; return 1;
}
if ((uVar1 >> 0xf & 1) != 0) { // bit 0x8000 = CVAR_ROM
print "%s is read only"; return 1;
}
if ((DAT_7ff74c1821c8 == 0) && ((uVar1 & 0x10) == 0)) { // bit 0x10 = CVAR_NOCHEAT
pcVar7 = "%s CANNOT be set in retail!";
} else {
... actually sets lVar3's value, clears modified flags, marks CVAR_MODIFIED2 (0x40000) ...
if ((flags & CVAR_ARCHIVE) && buildType==3) pcVar7 = "Archived CVar '%s' in production...";
}
FUN_7ff74ad5a7b0(pcVar7, name); // only printed in the two warning branches above
return 1;
}decompiler reconstruction)
python3 -c "
import sys; sys.path.insert(0,'tools')
from tnc_runtime_code import Image
im = Image('re_probes/runtime_tno_186352')
im.disasm(0x133dd8, 0x60)"
00133dd8 call 0x1323a0 ; (unrelated: GPU-driver-version check, see §5)
00133ddd test al, al
00133ddf jne 0x133de8
00133de1 xor ecx, ecx
00133de3 call 0x4e84e0 ; (driver-too-old warning dialog)
00133de8 mov rcx, [rip+0x146d8b9] ; RVA 0x15a16a8
00133def mov rax, [rcx]
00133df2 call [rax+8]
00133df5 mov rcx, [rip+0x1486314] ; RVA 0x15ba110 (idCVarSystem singleton)
00133dfc mov rax, [rcx]
00133dff mov edx, 0x10000 ; CVAR_ARCHIVE
00133e04 call [rax+0x98] ; (unrelated housekeeping -- "move archived cvars to a dict"-style call)
00133e0a cmp dword ptr [rip+0x1cbe3b7], 0 ; RVA 0x1df21c8 <-- THE GATE
00133e11 jne 0x133e28
00133e13 mov rcx, [rip+0x14862f6] ; RVA 0x15ba110
00133e1a mov rax, [rcx]
00133e1d mov edx, 8 ; CVAR_CHEAT
00133e22 call [rax+0xa0] ; ResetFlaggedVariables(CVAR_CHEAT)
00133e28 lea rcx, [rip+0x1dc3a81] ; RVA 0x1ef78b0
00133e2f call 0x83dac0
(im.disasm/im.function come from tools/tnc_runtime_code.py's Image class,
pointed at re_probes/runtime_tno_186352 instead of the TNC snapshot — the
class is title-agnostic, as the task brief says.) This matches the decompiled
C in the Answer exactly and is the only place in the whole binary that calls
ResetFlaggedVariables with CVAR_CHEAT (8) as the mask.
re_probes/tnocheatgate/find_gate_writes.py scans every byte offset in
.text (12 436 956 bytes) as a candidate RIP-relative displacement, for
four possible "tail lengths" (0 = LEA/MOV-reg/CMP-reg forms, 1 =
CMP/MOV r/m,imm8, 2 = unused here, 4 = CMP/MOV r/m,imm32), computes the
referenced RVA for each, and classifies every hit whose computed RVA equals
the target as a read or a write by its opcode+modrm byte. This matters:
a first attempt only tried tail=0 and found 21 hits, all reads, and wrongly
read that as "maybe this is written through an indirect pointer my scan can't
see" — fixed by realizing cmp dword ptr [addr], 0 (opcode 0x83 /7, the
actual Init-site instruction) has a trailing immediate byte that shifts the
RIP-relative base by one, which tail=0 cannot find at all. With all four
tails:
python re_probes/tnocheatgate/find_gate_writes.py 0x1df21c8
...
80 references, 0 of them writes
80 total read/compare sites (mostly the same cmp dword ptr [RVA 0x1df21c8], 0
idiom, one lone cmp ..., 1 at RVA 0x149e0f), spread across roughly 20
distinct functions — zero mov [RVA],imm, zero mov [RVA],reg, zero
read-modify-write (add/or/and/xor/sub [RVA], ...) anywhere. Also checked and
negative: a mov r64, imm64 loading the absolute virtual address
0x7ff74c1821c8 anywhere in .text (would indicate an indirect/computed
store this RVA-relative scan can't see) — 0 occurrences; the RVA 0x1df21c8
itself as a raw 4-byte value anywhere in the captured image (would indicate a
lookup/descriptor table referencing it) — 0 occurrences. Also checked: the
.data bytes backing this address in the live, running-process capture
read back as 0x00000000 — i.e. this was measured not just absent-of-writes
in the code, but actually zero while the real game was running.
This is the central, load-bearing claim of this report and the thing most
worth independently re-checking: python re_probes/tnocheatgate/ find_gate_writes.py --self-check re-runs the scan and asserts both that the
idCommonLocal::Init gate site is still found and that the write count is
still zero.
FUN_7ff74ad7e102 (RVA 0x9ee102), decompiled in full:
void FUN_7ff74ad7e102(longlong param_1)
{
puVar1 = *(undefined8 **)(param_1 + 8); // head of a hash bucket of registered commands
do {
if (puVar1 == 0) { // walked off the end -- no command matched
cVar2 = (**(code **)(*DAT_7ff74b94a110 + 0x78))(); // == FUN_7ff74ad6d810, per Ghidra's own
// COMPUTED_CALL resolution at this call site
if (cVar2 == '\0') print "Unknown command '%s'";
return;
}
if (name_matches(puVar1)) { call puVar1's handler; return; }
puVar1 = puVar1->next;
} while (true);
}So every exec'd/typed line is first tried against the registered command
table (§4 of r6_tnoconsole.md already mined ~1150 of these: kill,
devmap, killMonsters, ...), and only if that fails does it fall
through to "try this as cvarname value" — which is exactly
FUN_7ff74ad6d810, the gated handler from §1a. This answers one of r6's own
open framing questions more precisely than "commands have no flag at all":
dedicated commands (the idCmdSystem::AddCommand table) are indeed
structurally ungated (confirmed again, no CMD_* flag field read anywhere in
this dispatch loop), but cvar assignment — which is what every
RETAIL_BLOCKED entry in wolfsdk/cvars.py actually is — is not reached
through a separate "set" command at all; it's the dispatcher's universal
fallback, so there is exactly one gate for it, not one per cfg-reading
command.
find_gate_writes.py 0x1df21c8 (or the raw numpy scan in §2) lists every read
site's containing function. Besides the two load-bearing ones above, roughly
70 more cmp dword ptr [RVA 0x1df21c8], 0 sites sit inside a dense run of
otherwise-unnamed functions, most densely clustered in two address ranges:
0x48dfc0/0x7905b0/0x792931-0x7b3da4 (a long, mostly-contiguous chain:
0x791830, 0x791c60, 0x7922a0, 0x792340, 0x7923b1, 0x792437, 0x7931ef, 0x7936e0, 0x794520, 0x794740, 0x794fc0, 0x795510, 0x796530, 0x7a9808, 0x7b3c30, all taking a longlong param_1 "this"-pointer and float
parameters — consistent with idPlayer/idPhysics_Player movement code, the
same subsystem pm_noclipspeed/pm_autoSprint/pm_crouchspeed etc. (all
RETAIL_BLOCKED, all pm_*) live in). Not individually identified by name
(no string/xref ties any one of these specifically to noclip or god — that
would need either a build with debug symbols or dynamic tracing, neither
available here) — flagged as INFERRED, not proven, below. The pattern strongly
suggests each of these ~15 functions is one cheat-adjacent gameplay routine
(noclip movement, god-mode damage skip, infinite ammo, etc.) with its own
inlined if (!DAT_7ff74c1821c8) return;/early-out, in addition to the
cvar-level gate in §1a — i.e. even if a CVAR_CHEAT cvar's value could
somehow be forced to 1 by some other means, several of the gameplay effects
it's supposed to drive would still independently no-op on the same always-zero
global.
r6_tnoconsole.md §2 already measured (from a real qconsole.log) that
+pm_autoSprint 1 etc. on the command line take effect silently (no
"CANNOT be set in retail!") but are then wiped by "Resetting cheat cvar: %s"
— i.e. command-line cvars do not go through the §1a gate at assignment
time, only through the §1b reset sweep afterward. This pass's decompile of
idCommonLocal::Init confirms the ordering exactly: the reset-sweep call
(§1b) sits right after the "Command line is: %s\n" log line and right before
default.cfg is execed — consistent with command-line tokens having already
been applied (via some earlier, separate mechanism, almost certainly before
idCommonLocal::Init even runs, since the log line only displays the
already-parsed command line rather than applying it) and then being reset here,
before any cfg file — including default.cfg itself — gets a chance to
touch them. One dead end this pass hit chasing this: the function called
immediately after the log line (FUN_7ff74a4c23a0, called via
cVar5 = ...(); if (!cVar5) warn-dialog();) was a promising-looking candidate
for "the command-line parser", but decompiling it in full shows it is
unrelated — a GPU driver version/compatibility check ("found %s driver %d.%d.%d.%d (limited to %d.%d.%d.%d)", AMD/NVIDIA driver-version string
parsing, a "Graphics Driver Warning" dialog). The actual routine that turns
+cvar value command-line pairs into cvar assignments was not located this
pass — it most plausibly runs even earlier (WinMain/engine bootstrap,
before idCommonLocal::Init), which is consistent with every other reload
mechanism here being read-only strings or statically-exported symbols rather
than labelled functions. Flagged as an open question (§8) rather than
guessed at further.
-
DAT_7ff74c1821c8(RVA0x1df21c8, a 32-bit.dataglobal) is read with a plaincmp dword ptr [addr], 0at RVA0x133e0ainsideidCommonLocal::Init(FUN_7ff74a4c3380), gating a direct call toidCVarSystem::ResetFlaggedVariables(CVAR_CHEAT = 8)through vtable slot+0xa0of the cvar-system singleton at RVA0x15ba110— disassembled byte- for-byte in §1b. - The exact same global is read with the exact same idiom inside
FUN_7ff74ad6d810(RVA0x9dd810), gating whether a cvar carrying neitherCVAR_INIT(0x4000) norCVAR_ROM(0x8000) norCVAR_NOCHEAT(0x10) can be assigned at all — else prints"%s CANNOT be set in retail!"(RVA0x10519f8). -
FUN_7ff74ad6d810is not a standalone "set" command: it is the fallback branch of the command dispatcher (FUN_7ff74ad7e102, RVA0x9ee102), reached for every token that fails to match a registered command name — confirmed both by Ghidra'sCOMPUTED_CALLresolution at the call site and by reading the dispatcher's own decompiled control flow (§3). - Exhaustive scan of the full 12 436 956-byte
.textsection (every RIP-relative operand, 4 possible trailing-immediate lengths, both absolutemov r64,imm64-then-indirect-store and direct-displacement forms) finds zero instructions anywhere that writeDAT_7ff74c1821c8(80reads,0writes) —re_probes/tnocheatgate/find_gate_writes.py --self-check. - The live, running-process capture (
re_probes/runtime_tno_186352/module.bin, pid 186352) reads this address back as0x00000000at capture time. -
ResetFlaggedVariables's loop body (FUN_7ff74ad6daa0/FUN_7ff74ad6dabc, byte-identical twins at adjacent.pdataentries) walks the cvar list headDAT_7ff74c286cf0, matchescvar->flags & mask, calls the cvar's own reset-to-default accessor, clears/setsCVAR_MODIFIED2(0x40000), and logs"Resetting cheat cvar: %s\n"— matchingr6's realqconsole.logexcerpt line for line. - No
CMD_*command-flag enum exists in the dispatcher itself (re-confirmed by readingFUN_7ff74ad7e102directly this pass, not just by string absence as inr6) — the dispatcher only ever checks "does this token's name match a registered command", never a per-command cheat/expose bit.
- The ~70 other
cmp [RVA 0x1df21c8], 0sites in the0x790000–0x7b4000function cluster are individual cheat-adjacent gameplay/physics routines (plausiblynoclipmovement, god-mode damage bypass, etc. — the address range overlaps wherepm_*cvar-driven movement code would live) each with their own inlined early-out on the same global — plausible from the function signatures (this-pointer +floatparams, dense contiguous run) and fromr6's independent finding that severalpm_*/g_*cvars in this same family are inRETAIL_BLOCKED, but no individual function in this cluster was tied to a specific named command or cvar — would need either symbol recovery or dynamic tracing to confirm. - Command-line
+cvar valuepairs are applied through a code path that does not call throughFUN_7ff74ad6d810at all (only through this, consistent withr6's measured "command line passes silently, config files don't") — inferred from the measured asymmetry plus confirmed call ordering inidCommonLocal::Init, not from having located and read that parsing routine itself (§5, §8).
-
FUN_7ff74a4c23a0(the function called right after the"Command line is: %s\n"log line) is the command-line argument parser. Plausible from its position; decompiling it in full shows it is an unrelated GPU driver version/compatibility check (AMD/NVIDIA driver string parsing, a "Graphics Driver Warning" message-box flow) — a real, named dead end, not silently dropped (§5). -
A tail=0-only RIP-relative scan is sufficient to find every reference to a
given global. First attempt at §2 found 21 reads and 0 writes and
(wrongly, for one draft of this report) treated that as inconclusive /
possible-indirect-write evidence. Re-derived:
cmp dword ptr [addr], imm8(opcode0x83 /7, the actual gate instruction) has a trailing immediate byte the RIP-relative effective address is computed past, which tail=0 cannot see at all; adding tail=1 (and tail=4 forimm32forms) found 59 more read sites immediately, including the exactidCommonLocal::Initgate site itself. Fixed infind_gate_writes.py, which now tries all four tails and is the version--self-checkverifies. -
CVAR_CHEAT/CVAR_NOCHEATas literal strings would lead to the flag check via xrefs. Both strings exist in.rdata(confirmed,r6already found them) but Ghidra reports zero code references to either — the actual code never loads these name strings to test the flag, it tests the literal immediate8/0x10directly (r6's own strings-first approach could not have found the gate this way even with decrypted.text; following the message strings, as this pass did, was the only route that worked).
There is no no-patch route for a cvar that is actually CVAR_CHEAT-flagged
in retail (the 25 in wolfsdk/cvars.py::RETAIL_BLOCKED, measured, and now
explained at the instruction level): the single global every code path checks
is permanently zero and is written by nothing in the shipped binary, in any
config file, any exec'd script, any command-line argument, or any DevGUI
action — all of those are cvar/command values, and the gate is not a cvar.
Concretely, for the loader's own feature set (wolfsdk/mod.py /
wolfsdk/cvars.py):
-
Stop trying to deliver
RETAIL_BLOCKEDcvars through any file/exec/ command-line channel —docs/limits.mdalready measured this, and this pass explains why it will never work: differently-sourced config lines,+set, andexec'd cfgs all funnel through the one fallback in §3, and command-line-applied values get wiped by the §1b sweep regardless of source, beforedefault.cfgeven runs. This is not a gap in the loader's routing — it is architecturally closed. -
Non-cheat-flagged tweaks are unaffected and are exactly the ones
wolfsdk/cvars.pyalready ships outsideRETAIL_BLOCKED— no change needed there. -
The one remaining, unproven lever is still
r6's script-VM route (setcvar/executeCommandTextcallable from a map's.script, or a rawidTarget_Cvarentity) — this report adds a concrete prediction for it: ifexecuteCommandText("pm_autoSprint 1")is queued from inside a loaded map's script, it will be tokenized and executed through the exact sameFUN_7ff74ad7e102dispatcher described in §3, landing in the exact sameFUN_7ff74ad6d810gate described in §1a — i.e. it will almost certainly be blocked identically ("CANNOT be set in retail!"), unless script-VM command execution is proven to carry some other, unrelated privilege this report found no trace of. This is a sharper, more pessimistic prediction thanr6's "unproven, maybe privileged" framing — worth stating plainly so effort isn't spent rebuilding a map chunk to test something this pass's code reading suggests will fail the same way everything else does. -
A real unlock needs the global itself to become nonzero, which (per §2)
requires something no config/exec/command-line/DevGUI channel can do:
writing 4 bytes directly into the running process's memory at
base + 0x1df21c8. That is binary/memory patching, explicitly out of scope for this project (docs/ghidra.md's own stated boundary: no unpacker, no code patches), not a "no-patch route" by definition — stated here only to close the question, not as a suggestion.
-
Where does the command-line
+cvar valueparser actually live? Not found this pass (§5) —FUN_7ff74a4c23a0was a dead end (driver-version check, §"Failed hypotheses"). Likely earlier thanidCommonLocal::Init(bootstrap/WinMain). Finding it would let someone confirm directly (by reading its code, still no game launch needed) whether it really bypassesFUN_7ff74ad6d810's gate outright, or whether it goes through a third code path not seen in this report. -
What are the ~70 other
DAT_7ff74c1821c8-gated functions, by name? Listed by RVA in §4/§2 output, not identified — would need either partial symbol recovery (none shipped in this binary) or dynamic step-tracing (out of scope: no process attach permitted here). -
Is
DAT_7ff74c1821c8itself cross-referenced by name anywhere outside code (a debug-symbol PDB, a leaked internal build, MachineGames' own tooling)? Not searched for in this pass — out of scope (would require sources this project does not have access to), noted only so a future pass doesn't assume it was checked. -
Does the script-VM route (
executeCommandText) actually reach the dispatcher the way §3 predicts? Still genuinely unproven either way without building and loading a map chunk — this report only sharpens the prediction (see Recipe §3), it does not resolve it.
(Translated from German.)
This finding is almost entirely secured by reading code (no game-start dependency any more for the core claim) — still, two tests that would cheaply settle the only remaining open practical point (section "Recipe" point 3), without having to build an extra map for it:
-
Check
docs/limits.md's own log once more, closely, for the order (if the raw file is still available,%USERPROFILE%\Saved Games\ MachineGames\Wolfenstein The New Order\base\qconsole.log): do the"Resetting cheat cvar: ..."lines appear before anydefault.cfg-related line? That is exactly the order this report predicts from theidCommonLocal::Initcode (section 5) — if yes, a free confirmation without any new game start. -
If a TNO map with its own
.scriptis built anyway (e.g. for another purpose): add a lineexecuteCommandText( "pm_autoSprint 1" );and then checkqconsole.logfor"pm_autoSprint CANNOT be set in retail!". This report predicts that exactly this line appears (not "works silently") — a single, clearly falsifiable test for the central new prediction from section 3.
No saves affected, no memory patch, no new mod needed for test 1.
python re_probes/tnocheatgate/find_gate_writes.py --self-check
# reads/cmps found: 80 writes found: 0
# idCommonLocal::Init gate site (rva 0x133e0c) present: True
# self-check: OK
python tools/decomp.py --strings "CANNOT be set in retail" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
python tools/decomp.py --strings "Resetting cheat cvar" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
python tools/decomp.py FUN_7ff74ad6d810 --xrefs --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
python tools/decomp.py FUN_7ff74ad7e102 --xrefs --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
python tools/decomp.py FUN_7ff74a4c3380 --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf # idCommonLocal::Init, long
Static only, same decrypted image (re_probes/runtime_tno_186352), nothing launched.
Reproducibility: python re_probes/tnocheatgate/pass2_scan.py (self-checks P2.1-P2.4).
Scratch: D:/wolf-decomp/tmp/tnocheatgate2/.
-
The console is not blocked by
com_allowConsole. Nothing in the console code reads that cvar.idConsoleLocal::ProcessEvent(0x14f140) opens the console on Ctrl + key 0x29 (grave /^/~), with no cheat/retail test. A plain~(what pass 1 anddocs/limits.mdassumed) does nothing, which looks exactly like "console disabled". -
Cheat cvars are settable with no patch, because every typed cvar-setter path except one
bypasses the gate. The gate (
0x9dd810) is only the barename valuefallback of the command dispatcher.toggle,cvarAdd,cvarRandom,addWrap,addClamp,cvarMultiply(registered commands, ungated), the script nativesys.setcvar, the entity classidTarget_Cvar, the SWF nativessetCVarString/Integerand the DevGUI cvar rows all go throughidCVarSystemslot+0x38=FUN 0x9dd710, which calls Set withforce=1. -
Why F10 never opened the DevGUI: the stock bind is
bind "F10" "devgui 1"(the maintainer's ownwolfConfig.cfg).devgui 1is a bare cvar assignment, gated, so "devgui CANNOT be set in retail!".devguiis cheat by default (P2.2). A bind oftoggle devgui 0 1takes the force path. Separately,debugmenu.txt(the loader's WOLFSDK page) has no consumer in the exe (P2.5), so even an open DevGUI would not show the loader page.
FUN_7ff74ad6d2d0 (rva 0x9dd2d0) Set(cvar, value, force):
if (force == 0) { INIT(0x4000) -> "can only be set on the command line"; ROM(0x8000) -> "is read only";
if (DAT_1df21c8 == 0 && !(flags & 0x10)) -> "%s CANNOT be set in retail!" }
(strcmpi old/new: unchanged value -> return 0) ... assign, flags |= MODIFIED2
FUN_7ff74ad6d710 (rva 0x9dd710) = vtable slot +0x38: cvar = find(name); if (cvar) Set(cvar, value, 1);
FUN_7ff74ad6d810 (rva 0x9dd810) = vtable slot +0x78: the dispatcher fallback, gate inline (pass 1 §1a)
slots +0x40/+0x48/+0x50 (0x9dcd30/0x9dcdb0/0x9dce30) = SetCVarBool/Int/Float(name,v):
format v to string, then (*vtbl+0x38)(name, str, 0) -> same force path
FUN_7ff74ad6d480 (rva 0x9dd480) = idCVar::SetInteger(v, force) -> Set(cvar, str, force)
Vtable of idCVarSystem = RVA 0x104f968 (slot 0x38 -> 0x9dd710, 0x78 -> 0x9dd810).
Negative control: the only code that prints "CANNOT be set in retail" is 0x9dd810 and the
force==0 branch of 0x9dd2d0. Measured control from docs/limits.md: +exec file.cfg (bare
assignment) is blocked while +toggle devgui is accepted with no warning ("devgui = 1.000000"),
which is what the decompile predicts.
FUN_7ff74ad6ea10 (cvar constructor, rva 0x9dea10; test edi,0x1c010 / or edi,0x10 / or edi,8):
if ((flags & 0x1c010) == 0) flags |= 8 /*CHEAT*/; else flags = (flags & ~8) | 0x10 /*NOCHEAT*/;Any cvar registered without NOCHEAT (0x10), INIT (0x4000), ROM (0x8000) or ARCHIVE (0x10000) is cheat.
com_allowConsole is registered with flags=1 (BOOL) so it is cheat; devgui flags=2 so it is cheat.
Archived cvars (graphics/audio options) are exempt, which is why wolfConfig.cfg lines such as
vt_pageImageSizeUnique "8192" load fine.
pass2_scan.py finds 24 hits (pass 1's verifier said ~23). 3 are false positives (other object).
| site(s) | what it is | loader-reachable data? |
|---|---|---|
0x9ddd70,0x9dde08 (fn 0x9ddc00) |
console command toggle (commands.json callback 0x9ddc00) |
yes: any command text |
0x9ddfc1 (0x9ddf00) |
cvarAdd |
yes |
0x9de13b (0x9ddff0) |
cvarRandom |
yes |
0x9de2a0 (0x9de170) |
addWrap |
yes |
0x9de410 (0x9de2e0) |
addClamp |
yes |
0x9de527 (0x9de450) |
cvarMultiply |
yes |
0x97cc02 (helper 0x97cbe0) |
script native sys.setcvar(name,value): called from the script-VM native switch at 0x63f3 with args [rsi+8],[rsi+0x20] (two strings); neighbours are the idSys natives print 0x97cae0, println 0x97cb10, say 0x97cb40
|
yes: .script is a plain-text file asset |
0x7a2deb (fn 0x7a2d70, slot 0x4c8 of the idTarget_Cvar vtable 0xc18c28) |
idTarget_Cvar entity: loops keyVals (stride 0x50, count at +0x380) and force-sets each name/value |
yes: .entities is a plain-text file asset; unused by all 25 retail maps (grep), keyVals text syntax not recovered |
0x26509d (0x265010) |
SWF setCVarString(name,value) (vtable of idSWFScriptFunction_setCVarString, slot +0x48) |
only if a .bswf calls it; stock string pools name no cvar (r3_swfui.md); .bswf not authorable |
0x264fc7 (0x264f50, slot 0x48) |
SWF setCVarInteger |
same |
0x98e80d (0x98e740, slot 0x50) |
DevGUI slider row (SetCVarFloat) | only inside an open DevGUI |
0x14c519,0x14c83b; slot-0x40/0x48/0x50 sites 0x14c4a0..0x14c577, 0x6c4f00..0x6c52f4
|
video-capture code: fixed names com_capturePath, com_fixedTic, r_postProcessHQ, vt_useMinLodTexture, ... |
no |
0x223499..0x2235f5 (fn 0x223345) |
cm_drawColor (collision debug draw) |
no |
0x7e7785,0x7ef73b,0x7efaa8 (+ slot-0x50 0x7ef792..0x7efb5f) |
vehicle_follow / vehicle_cam_tag, fixed names |
no |
0x6a2331 (0x6a2310, called from the native switch at 0xac7d) |
a 3-string-arg script native (args 2,3 become name,value); event name not identified (only setcvar/getcvar carry "cvar" in script_events.script) |
would be script-reachable, identity open |
0x15f359, 0x15fa4a, 0x9505bc
|
false positives: cmdSystem slot 0x38 (bind arg completion) / another object |
-- |
The wider set of typed setters (slots 0x40/0x48/0x50, ~90 more sites) was sampled: all fixed-name
engine code such as the demo-capture block; none reads a name from a decl.
Negative control for the scan: 0x15f359 looked like a hit; the loads just before it show
[0x15ba108] (cmdSystem) not [0x15ba110], so it is idCmdSystem slot 0x38.
FUN_7ff74a4df140 (rva 0x14f140, idConsoleLocal vtable 0xe6aeb8 + 0x28) is the console's
ProcessEvent:
if (event.type == 1 /*key*/ && event.key == 0x29 /*DIK_GRAVE*/) bVar1 = IsCtrlDown(); // func 0x15d650
...
if (bVar1) { key-down only (flags&1, no repeat); if (!IsActive()) SetDisplayFraction(shift ? A : B); else Close(); return 1; }0x15d650 = keystate[0x1d] || keystate[0x9d] (left/right Ctrl; table 0x1cc4dc0; verified by
pass2_scan.py). 0x15d630 is the Shift twin (0x2a/0x36) that picks the console height. The event
pump (idCommonLocal main loop 0x13dff0, call at 0x13fa2d) hands every event to the console's slot
+0x28 first, with no cvar test around the call. Enter in the console (0x14ed00, keys 0x1c/0x9c)
echoes the line and calls cmdSystem slot +0x48, so the line goes to the normal dispatcher
(commands ungated, bare cvar assignments gated). idConsole (vtable 0xe6aa38) is an abstract
interface (all slots _purecall 0xac16dc); idConsoleLocal is statically constructed
(0xb78590 -> ctor 0x153980).
Negative control, com_allowConsole (object 0x16b0ce0): the only code reference to the object is
cmp dword [obj+0x28],0 at 0x86d6ee in game-init FUN_7ff74abfb8b0, which stores (value!=0) in
byte [game+0x22bddc] (getter vtable slot 0x5f8 = 0x850b10); the getter's consumers are
FUN_7ff74a870070 ("Achievement unlock") and siblings. So com_allowConsole means "console
used -> no achievements"; it does not gate the console. A plain ~ without Ctrl falls through to
IsActive()==0 -> return 0.
Console commands that exist (192, re_probes/tnocommands/commands.json): map, devmap, cmap,
devcmap, script, trigger, teleport, setviewpos, kill, killEntity, killAI, killMonsters,
damage, freeze, exec, vstr, bind, unbind, toggle, cvarAdd, reset, restartmap, ...
god, noclip, notarget, give do not exist in the table, so they print "Unknown command".
The equivalents are cheat cvars (g_permaGodMode, g_permaInfiniteAmmo, pm_*) via toggle.
Do those cvars then act? The value readers of g_permaGodMode (0x9267fa, 0x92f1a0) and
g_permaInfiniteAmmo (0x8151f0, 0x914968, 0x9a5678, 0x9a6050, 0x9ac110) contain no read
of the cheat global 0x1df21c8 (same scan as pass 1 §2), so a force-set value takes effect.
- Stock bind (the maintainer's
wolfConfig.cfg, bindset 0):bind "F10" "devgui 1".devguiis a cvar, not one of the 192 commands, so it hits0x9dd810and is blocked (P2.2: flags=2, cheat by default). - The DevGUI is compiled C++ (
idDevGuiMainPage_{Main,AI,Player,Weapon,Collision,Sound,QA,Render, Performance},idDevGuiChildPage_*), constructed in game-init (0x86d767new, ctor0x98cb30, Init0x98da50setsobj[1]=1) and drawn once per frame from the game render function (0x868e02->FUN_7ff74ad198c0, rva0x9898c0). - Draw/event gate
FUN_7ff74ad18fe0(rva0x988fe0): open =devgui>0 && obj[1]and the game object exists andgame[0x180ffd]==0and a state counter (vtbl+0x140)<1. When not satisfied it force-resetsdevguito 0 (FUN_ad6d480(&devgui,0,1)). Sodevgui=1set at the main menu (the old+toggle devguimeasurement pressed F8/F10 only there;qconsole.logends at the menu) is zeroed by the game itself; set it while a level runs. Whatgame[0x180ffd]/vtbl+0x140mean is not resolved. - Row execution: command rows call
cmdSystemslot+0x40(FUN_98e91c, prints "DevGUI Command: %s"), i.e. the dispatcher (commands ungated; theirtoggle x 0 1strings force-set); cvar slider rows callSetCVarFloat(0x98e80d, slot+0x50, force). DevGUI buttons do not hit the gate. -
debugmenu.txt: no consumer found. The exe has no stringdebugmenu(ASCII or UTF-16), and the file's key namescmd(136 uses) andres(61 uses) do not exist as NUL-terminated strings. What is present is the generator template (0xfff59d):{ 'tag' : '%s', 'title' : '%s', 'cmd' : 'si_spawnspot %s; si_devspawninventory 1; ... map %s', 'res' : '%s' },and{ 'pageName' : '..', 'entries' : [, i.e. the tool that wrote the Maps-Fe/Maps-Wy pages.idDebugMenuhas RTTI/typeinfo but no vtable. The loader'sdevmenu.pypage is therefore very probably never read in retail, and the loader cannot change the compiled DevGUI rows. - The
debugmenu.txtcurrently in the archive (22 419 B, 9 pages) contains noWOLFSDKpage.
-
script_main()runs at every map start:FUN_7ff74abf5440(rva0x865440) creates theframeCommandsthread, looks up the functionscript_main(0x975e40), spawns and executes it (0x865e74..0x865ecd); the file is/script/script_main.script(661 B plain text, string at0xffde00), included before any level script. -
script_events.scriptdeclaresscriptEvent void setcvar(string name, string value)(line 3283),string getcvar(string)(3286),void executeCommandText(string command)(3518),void say(string)(3274). Retailscript_util.script:405already callssys.setcvar("g_stopTime","1"); five map script sites callsys.getcvar(...). -
sys.setcvar->0x97cbe0-> cvar slot0x38(force).sys.executeCommandText->0x97e360->cmdSystemslot+0x40(typed-text executor0x9ee190, same dispatcher as the console);sys.say(0x97cb40) formatssay "%s"into the same slot. A bare cvar name insideexecuteCommandTextis gated;toggle x 0 1inside it is not. - The
scriptconsole command (0x847db0) compilesvoid ConsoleFunctionN(){<text>;}through the same compiler, soscript sys.setcvar("devgui","1");is a one-line console equivalent.
| # | route | needs | evidence |
|---|---|---|---|
| 1 | Console: Ctrl+^, then toggle <cheatcvar> 0 1, cvarAdd, or script sys.setcvar("x","v"); |
game running, nothing else | full code chain (P2.4); never run in game |
| 2 |
Rebind F10: edit own wolfConfig.cfg bind "F10" "devgui 1" to bind "F10" "toggle devgui 0 1" (or type bind F10 "toggle devgui 0 1" in the console; bind marks the config dirty) and press it inside a level
|
route 1 or a text editor | gate + force path decompiled; DevGUI visibility conditions partly unresolved |
| 3 |
script_main.script hook (loader replaces the 661-B file): sys.setcvar("g_permaGodMode","1"), sys.executeCommandText("toggle devgui 0 1") run at every map start, no console, no keys |
loader file replace (rebuild path, per r7_tnoscriptvm.md §6) |
native handler + call site proven; added lines untested in game |
| 4 |
idTarget_Cvar in a map .entities + trigger <name>
|
keyVals syntax unknown, spawn untested |
handler proven (force), spawn unproven |
| 5 | loader debugmenu.txt page |
-- | dead: no consumer (P2.5) |
| 6 | SWF setCVar*
|
.bswf authoring |
dead: not authorable, no cvar literals in stock pools |
| - | command line +toggle x 0 1
|
known to work | measured (limits.md) |
(Translated from German.)
- In the game (or main menu) press Ctrl + ^ (the key left of the 1, with Ctrl held;
^alone does nothing). An input line should appear.Ctrl + Shift + ^as a control. - Type:
toggle g_permaGodMode 0 1+ Enter. Expectation: a line withg_permaGodMode, no "CANNOT be set in retail". For comparisong_permaGodMode 1: that should report "CANNOT be set in retail!". - In a loaded level:
toggle devgui 0 1-> the DevGUI overlay should appear (arrow keys, Enter, Esc). Afterwards bind F10 withbind F10 "toggle devgui 0 1". If step 1 shows nothing: report which keyboard/key; then thescript_main.scripthook (route 3) is the next stage.
- Nobody has pressed Ctrl+
^in TNO: the chain is code-proven, not run. Not excluded: a window/focus filter before the event pump (0x13dff0read only around the call site), or a keyboard layout whose key does not produce DIK0x29. - DevGUI visibility conditions (
game[0x180ffd],vtbl+0x140) unidentified; whetherdevguisticks at 1 inside a level is unmeasured. Thedebugmenu.txtconsumer absence is a string argument (nocmd/resliteral), not proof (a hashed-token parser would leave no strings). -
idTarget_CvarkeyValssyntax and spawn; the 3-arg script native at0xac65/0x6a2310. - Other cheat cvars were not individually checked for gate-free readers (only
g_permaGodMode,g_permaInfiniteAmmo); pass 1's ~15 gated gameplay functions are unidentified. - The
script_main.scriptedit grows the file past its compressed slot, so the loader's rebuild path is needed (not run here).
Related: Research Report: R6 Tno Console, Research Report: R3 Swfui, Research Report: R7 Tno Script Vm, Research Report: R8 Tno Cvars, Developer Mode, Modding Limits, Ghidra Analysis, TNO CVar Catalog.
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