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).
Question
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?
Answer (short)
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:
asm
00133e0a 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 ...
i.e. if the global is 0, every CVAR_CHEAT-flagged cvar is force-reset to
default right here, logging "Resetting cheat cvar: %s\n" per cvar
(FUN_7ff74ad6daa0/its twin FUN_7ff74ad6dabc, both just
for (cvar = g_cvarListHead; cvar; cvar = cvar->next) if (cvar->flags & mask) reset(cvar)).
This runs after command-line +cvar value pairs have already taken effect
(they are applied before this point — see §5) and before default.cfg is
executed at all.
2. The cvar-assignment fallback inside the command dispatcher
(FUN_7ff74ad7e102, RVA 0x9ee102 — this is idCmdSystemLocal'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 is FUN_7ff74ad6d810 (confirmed by Ghidra's own
COMPUTED_CALL resolution at the call site). This is the actual, single
choke point every bare name value line goes through, whether typed at a
console, sitting in default.cfg, or exec'd from any .cfg — not a
dedicated set/seta command, the generic "token isn't a command, try it as
a cvar" fallback itself):
c
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_NOCHEAT per docs/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") and CVAR_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.
1. Finding the two gate strings and following them to code
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).
1a. The per-cvar gate, decompiled in full
// 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;
}
1b. Disassembly of the idCommonLocal::Init gate site (ground truth, not
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.
2. The global is never written — exhaustive, not a sample
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.
3. The gate sits at the dispatcher's cvar-fallback, not a dedicated command
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.
4. The same global also gates ~70 sites inside gameplay/physics code
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.
5. Command-line cvars: confirmed sequence, exact mechanism still open
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.
PROVEN (this track, decrypted-runtime static evidence — no game was launched)
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.
INFERRED (plausible, not measured this pass)
- 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).
Failed hypotheses / dead ends (this track)
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).
Recipe for the loader
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.
Open questions
- 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.
Smallest in-game test
(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.
Reproducibility
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
Pass 2 -- no-patch routes to the console and to cheat cvars
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/.
P2.0 Answer
- 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.
P2.1 The force path (decompiled)
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.
P2.2 Which cvars are gated: cheat is the default
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.
P2.3 The 24 call [reg+0x38] sites after a load of the cvar-system singleton (0x15ba110)
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.
P2.4 Console: key chain (decompiled)
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.
P2.5 DevGUI: gating, row execution, debugmenu.txt
- 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.
P2.6 DoomScript
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.
P2.7 Ranked routes
| # | 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) |
P2.8 Smallest in-game test (for the maintainer)
(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.
P2.9 What stays open
- 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.