Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Latest research roundsNeueste Forschungsrunden

TNO Cheat Gate: The Actual Code, Now That .text Is Decrypted

Partly solvedTeilweise gelöst Original page in the GitHub wikiOriginalseite im GitHub-Wiki

In plain wordsIn einfachen Worten

This report read the decrypted program code of Wolfenstein: The New Order (TNO) to find what blocks cheat settings (cvars). It is one hidden number that is always 0 and is never changed anywhere in the program: it resets all cheat cvars at startup and rejects a plain 'name value' line. A second pass found that this check only covers that plain form: commands like toggle, the script command setcvar and DevGUI sliders set cheat cvars past it, and the console opens with Ctrl plus the ^/~ key, not that key alone. All of this comes from reading code; an older measurement shows the toggle route accepted on the command line, but nobody has tried Ctrl+^ in the game yet.

  • The block is one number that no instruction in the 12,436,956-byte code section ever writes (80 reads, 0 writes), and it read 0 in the capture of the running game.
  • At startup every cheat cvar is reset to its default, and a plain 'name value' line, typed or from a config file, prints 'CANNOT be set in retail!'.
  • Correction in pass 2: toggle, cvarAdd and four similar commands, the script command sys.setcvar and DevGUI sliders set cvars with a force flag that skips the block.
  • Every cvar registered without one of four exemption flags (saved options are exempt) counts as a cheat, including devgui, which is why the stock F10 bind 'devgui 1' never opened the DevGUI.
  • god, noclip and give are not commands, but toggle g_permaGodMode 0 1 should work because its code ignores the block; untested, since nobody has pressed Ctrl+^ in TNO yet.

Dieser Bericht hat den entschlüsselten Programmcode von Wolfenstein: The New Order (TNO) gelesen, um herauszufinden, was Cheat-Einstellungen (Cvars) blockiert. Es ist eine einzige versteckte Zahl, die immer 0 ist und nirgends im Programm geändert wird: Sie setzt beim Start alle Cheat-Cvars zurück und lehnt eine einfache Zeile „Name Wert“ ab. Ein zweiter Durchgang fand, dass diese Prüfung nur genau diese einfache Form abdeckt: Befehle wie toggle, der Skriptbefehl setcvar und DevGUI-Schieberegler setzen Cheat-Cvars an ihr vorbei, und die Konsole öffnet sich mit Strg plus der ^/~-Taste, nicht mit dieser Taste allein. Das alles stammt aus Codelesen; eine ältere Messung zeigt, dass der toggle-Weg auf der Kommandozeile angenommen wird, aber Strg+^ hat in TNO noch niemand im Spiel ausprobiert.

  • Die Sperre ist eine Zahl, die kein Befehl im 12.436.956 Byte großen Codeabschnitt je schreibt (80 Lesezugriffe, 0 Schreibzugriffe), und im Abbild des laufenden Spiels stand sie auf 0.
  • Beim Start wird jede Cheat-Cvar auf ihren Standardwert zurückgesetzt, und eine einfache Zeile „Name Wert“, getippt oder aus einer Konfigurationsdatei, meldet „CANNOT be set in retail!“.
  • Korrektur im zweiten Durchgang: toggle, cvarAdd und vier ähnliche Befehle, der Skriptbefehl sys.setcvar und DevGUI-Schieberegler setzen Cvars mit einem Erzwingen-Schalter, der die Sperre überspringt.
  • Jede Cvar, die ohne eines von vier Ausnahme-Flags registriert ist (gespeicherte Optionen sind ausgenommen), gilt als Cheat, auch devgui; deshalb öffnete die Standardbelegung F10 = „devgui 1“ das DevGUI nie.
  • god, noclip und give sind keine Befehle, aber toggle g_permaGodMode 0 1 sollte wirken, weil sein Code die Sperre nicht prüft; ungetestet, denn Strg+^ hat in TNO noch niemand gedrückt.

Technical detailsTechnische Details

Der technische Teil ist auf Englisch – so, wie er im Wiki steht. Unterstrichene Begriffe haben eine Erklärung auf Deutsch.

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:

  1. idCommonLocal::Init (FUN_7ff74a4c3380, RVA 0x133380), right after it logs "Command line is: %s\n" and before it execs default.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 (RVA 0x1df21c8, a 32-bit .data global) is read with a plain cmp dword ptr [addr], 0 at RVA 0x133e0a inside idCommonLocal::Init (FUN_7ff74a4c3380), gating a direct call to idCVarSystem::ResetFlaggedVariables(CVAR_CHEAT = 8) through vtable slot +0xa0 of the cvar-system singleton at RVA 0x15ba110 — disassembled byte- for-byte in §1b.
  • The exact same global is read with the exact same idiom inside FUN_7ff74ad6d810 (RVA 0x9dd810), gating whether a cvar carrying neither CVAR_INIT (0x4000) nor CVAR_ROM (0x8000) nor CVAR_NOCHEAT (0x10) can be assigned at all — else prints "%s CANNOT be set in retail!" (RVA 0x10519f8).
  • FUN_7ff74ad6d810 is not a standalone "set" command: it is the fallback branch of the command dispatcher (FUN_7ff74ad7e102, RVA 0x9ee102), reached for every token that fails to match a registered command name — confirmed both by Ghidra's COMPUTED_CALL resolution at the call site and by reading the dispatcher's own decompiled control flow (§3).
  • Exhaustive scan of the full 12 436 956-byte .text section (every RIP-relative operand, 4 possible trailing-immediate lengths, both absolute mov r64,imm64-then-indirect-store and direct-displacement forms) finds zero instructions anywhere that write DAT_7ff74c1821c8 (80 reads, 0 writes) — 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 as 0x00000000 at capture time.
  • ResetFlaggedVariables's loop body (FUN_7ff74ad6daa0/FUN_7ff74ad6dabc, byte-identical twins at adjacent .pdata entries) walks the cvar list head DAT_7ff74c286cf0, matches cvar->flags & mask, calls the cvar's own reset-to-default accessor, clears/sets CVAR_MODIFIED2 (0x40000), and logs "Resetting cheat cvar: %s\n" — matching r6's real qconsole.log excerpt line for line.
  • No CMD_* command-flag enum exists in the dispatcher itself (re-confirmed by reading FUN_7ff74ad7e102 directly this pass, not just by string absence as in r6) — 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], 0 sites in the 0x790000–0x7b4000 function cluster are individual cheat-adjacent gameplay/physics routines (plausibly noclip movement, god-mode damage bypass, etc. — the address range overlaps where pm_* cvar-driven movement code would live) each with their own inlined early-out on the same global — plausible from the function signatures (this-pointer + float params, dense contiguous run) and from r6's independent finding that several pm_*/g_* cvars in this same family are in RETAIL_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 value pairs are applied through a code path that does not call through FUN_7ff74ad6d810 at all (only through this, consistent with r6's measured "command line passes silently, config files don't") — inferred from the measured asymmetry plus confirmed call ordering in idCommonLocal::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 (opcode 0x83 /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 for imm32 forms) found 59 more read sites immediately, including the exact idCommonLocal::Init gate site itself. Fixed in find_gate_writes.py, which now tries all four tails and is the version --self-check verifies.
  • CVAR_CHEAT/CVAR_NOCHEAT as literal strings would lead to the flag check via xrefs. Both strings exist in .rdata (confirmed, r6 already 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 immediate 8/0x10 directly (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):

  1. Stop trying to deliver RETAIL_BLOCKED cvars through any file/exec/ command-line channel — docs/limits.md already measured this, and this pass explains why it will never work: differently-sourced config lines, +set, and exec'd cfgs all funnel through the one fallback in §3, and command-line-applied values get wiped by the §1b sweep regardless of source, before default.cfg even runs. This is not a gap in the loader's routing — it is architecturally closed.
  2. Non-cheat-flagged tweaks are unaffected and are exactly the ones wolfsdk/cvars.py already ships outside RETAIL_BLOCKED — no change needed there.
  3. The one remaining, unproven lever is still r6's script-VM route (setcvar/executeCommandText callable from a map's .script, or a raw idTarget_Cvar entity) — this report adds a concrete prediction for it: if executeCommandText("pm_autoSprint 1") is queued from inside a loaded map's script, it will be tokenized and executed through the exact same FUN_7ff74ad7e102 dispatcher described in §3, landing in the exact same FUN_7ff74ad6d810 gate 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 than r6'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.
  4. 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 value parser actually live? Not found this pass (§5) — FUN_7ff74a4c23a0 was a dead end (driver-version check, §"Failed hypotheses"). Likely earlier than idCommonLocal::Init (bootstrap/WinMain). Finding it would let someone confirm directly (by reading its code, still no game launch needed) whether it really bypasses FUN_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_7ff74c1821c8 itself 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:

  1. 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 any default.cfg-related line? That is exactly the order this report predicts from the idCommonLocal::Init code (section 5) — if yes, a free confirmation without any new game start.
  2. If a TNO map with its own .script is built anyway (e.g. for another purpose): add a line executeCommandText( "pm_autoSprint 1" ); and then check qconsole.log for "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

  1. 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 and docs/limits.md assumed) does nothing, which looks exactly like "console disabled".
  2. Cheat cvars are settable with no patch, because every typed cvar-setter path except one bypasses the gate. The gate (0x9dd810) is only the bare name value fallback of the command dispatcher. toggle, cvarAdd, cvarRandom, addWrap, addClamp, cvarMultiply (registered commands, ungated), the script native sys.setcvar, the entity class idTarget_Cvar, the SWF natives setCVarString/Integer and the DevGUI cvar rows all go through idCVarSystem slot +0x38 = FUN 0x9dd710, which calls Set with force=1.
  3. Why F10 never opened the DevGUI: the stock bind is bind "F10" "devgui 1" (the maintainer's own wolfConfig.cfg). devgui 1 is a bare cvar assignment, gated, so "devgui CANNOT be set in retail!". devgui is cheat by default (P2.2). A bind of toggle devgui 0 1 takes 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". devgui is a cvar, not one of the 192 commands, so it hits 0x9dd810 and 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 (0x86d767 new, ctor 0x98cb30, Init 0x98da50 sets obj[1]=1) and drawn once per frame from the game render function (0x868e02 -> FUN_7ff74ad198c0, rva 0x9898c0).
  • Draw/event gate FUN_7ff74ad18fe0 (rva 0x988fe0): open = devgui>0 && obj[1] and the game object exists and game[0x180ffd]==0 and a state counter (vtbl+0x140) <1. When not satisfied it force-resets devgui to 0 (FUN_ad6d480(&devgui,0,1)). So devgui=1 set at the main menu (the old +toggle devgui measurement pressed F8/F10 only there; qconsole.log ends at the menu) is zeroed by the game itself; set it while a level runs. What game[0x180ffd] / vtbl+0x140 mean is not resolved.
  • Row execution: command rows call cmdSystem slot +0x40 (FUN_98e91c, prints "DevGUI Command: %s"), i.e. the dispatcher (commands ungated; their toggle x 0 1 strings force-set); cvar slider rows call SetCVarFloat (0x98e80d, slot +0x50, force). DevGUI buttons do not hit the gate.
  • debugmenu.txt: no consumer found. The exe has no string debugmenu (ASCII or UTF-16), and the file's key names cmd (136 uses) and res (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. idDebugMenu has RTTI/typeinfo but no vtable. The loader's devmenu.py page is therefore very probably never read in retail, and the loader cannot change the compiled DevGUI rows.
  • The debugmenu.txt currently in the archive (22 419 B, 9 pages) contains no WOLFSDK page.

P2.6 DoomScript

  • script_main() runs at every map start: FUN_7ff74abf5440 (rva 0x865440) creates the frameCommands thread, looks up the function script_main (0x975e40), spawns and executes it (0x865e74..0x865ecd); the file is /script/script_main.script (661 B plain text, string at 0xffde00), included before any level script.
  • script_events.script declares scriptEvent void setcvar(string name, string value) (line 3283), string getcvar(string) (3286), void executeCommandText(string command) (3518), void say(string) (3274). Retail script_util.script:405 already calls sys.setcvar("g_stopTime","1"); five map script sites call sys.getcvar(...).
  • sys.setcvar -> 0x97cbe0 -> cvar slot 0x38 (force). sys.executeCommandText -> 0x97e360 -> cmdSystem slot +0x40 (typed-text executor 0x9ee190, same dispatcher as the console); sys.say (0x97cb40) formats say "%s" into the same slot. A bare cvar name inside executeCommandText is gated; toggle x 0 1 inside it is not.
  • The script console command (0x847db0) compiles void ConsoleFunctionN(){<text>;} through the same compiler, so script 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.)

  1. 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.
  2. Type: toggle g_permaGodMode 0 1 + Enter. Expectation: a line with g_permaGodMode, no "CANNOT be set in retail". For comparison g_permaGodMode 1: that should report "CANNOT be set in retail!".
  3. In a loaded level: toggle devgui 0 1 -> the DevGUI overlay should appear (arrow keys, Enter, Esc). Afterwards bind F10 with bind F10 "toggle devgui 0 1". If step 1 shows nothing: report which keyboard/key; then the script_main.script hook (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 (0x13dff0 read only around the call site), or a keyboard layout whose key does not produce DIK 0x29.
  • DevGUI visibility conditions (game[0x180ffd], vtbl+0x140) unidentified; whether devgui sticks at 1 inside a level is unmeasured. The debugmenu.txt consumer absence is a string argument (no cmd/res literal), not proof (a hashed-token parser would leave no strings).
  • idTarget_Cvar keyVals syntax and spawn; the 3-arg script native at 0xac65/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.script edit 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.