Wolfenstein Modloader Buy me a coffeeKaffee spendieren

In plain wordsIn einfachen Worten

Console variables (cvars) are the game's settings switches. This report rebuilt the complete list for Wolfenstein II (TNC) by reading a memory image of the running game, without starting or changing anything. It found 3,256 registered cvars, of which only 304 can be changed from the console or command line. The older list of 2,231 entries was wrong both ways: 409 names were not cvars and 1,434 real ones were missing.

  • 3,256 cvars are registered; exactly the 304 marked as exposed are settable, matching the game's own output.
  • Two independent methods agree on all 3,256 names, defaults and descriptions.
  • 189 descriptions in the old list belonged to a different cvar.
  • The intro-skip setting does not exist in TNC, so that start option has no effect.
  • Open: g_damageScale looks locked by its flags but was once recorded as accepted.

Konsolenvariablen (Cvars) sind die Einstellungsschalter des Spiels. Dieser Bericht hat die vollständige Liste für Wolfenstein II (TNC) neu aufgebaut, indem er ein Speicherabbild des laufenden Spiels gelesen hat, ohne etwas zu starten oder zu ändern. Er fand 3.256 registrierte Cvars, von denen nur 304 über Konsole oder Kommandozeile änderbar sind. Die alte Liste mit 2.231 Einträgen war in beide Richtungen falsch: 409 Namen waren keine Cvars, und 1.434 echte fehlten.

  • 3.256 Cvars sind registriert; genau die 304 als „exposed“ markierten sind setzbar, passend zur Ausgabe des Spiels.
  • Zwei unabhängige Methoden stimmen bei allen 3.256 Namen, Standardwerten und Beschreibungen überein.
  • 189 Beschreibungen der alten Liste gehörten zu einer anderen Cvar.
  • Die Einstellung zum Überspringen des Intros gibt es in TNC nicht, die Startoption wirkt also nicht.
  • Offen: g_damageScale wirkt laut Flags gesperrt, wurde aber einmal als angenommen protokolliert.

Technical detailsTechnische Details

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

Worker w-cvartable-a1, Task cvartable2. Source: re_probes/runtime_87400/module.bin (92,188,672 Bytes, Layout "virtual RVA", Base 0x7FF62C210000, pid 87400). Tool: tools/probe_cvartable.py. Results: re_probes/cvartable/cvars_tnc_v2.json (Catalog) and re_probes/cvartable/diff_vs_docs.json (Checks + Diff). Purely static, read-only; no game launch, no process attach, no write access outside re_probes/cvartable/ and tools/probe_cvartable.py.

PYTHONPATH=re_probes/python_deps python tools/probe_cvartable.py

Summary

3256 Cvars are registered. Exactly 304 of them are accessible from outside.

Both figures are measurable and documented, not estimated:

  • The length of the static Registrar list (3256) matches exactly with the counter of the live list in the Cvar system.
  • The 304 with set CVAR_EXPOSE are count-equivalent to the 304 Cvars that the Engine itself outputs via listCvars - help (docs/cvars_tnc_echt.json). None are missing, none are extra.

The 2231 entries in docs/cvars_tnc.json are therefore incorrect in two ways, both directions: 409 names are not Cvars, and 1434 real Cvars are missing. The file is thus not only "overestimated" but simultaneously "underestimated".

The reason why the Engine outputs so little on listCvars - help g_ is not that there are few g_ Cvars (there are 287), but rather the same mechanism found by w-cmdtable-a1 with commands: the Restriction Level. The Cvar search is gated just like the command search.

1. Registry Structure

idCVarSystemLocal resides as a Singleton in .data at RVA 0x41244E0 (vtable 0x2978510, verified). It contains two idList heads and two hash indices:

Field RVA Meaning Status in Snapshot
+0x08 / +0x10 / +0x14 0x41244E8 List all 3256 / Capacity 4096
+0x28 / +0x30 / +0x34 0x4124508 List exposed 304 / Capacity 512
+0x48 0x4124528 Hash Index "all" –
+0x70 0x4124550 Hash Index "exposed" –
0x41244D8 – Restriction Level (shared with the command system) 0 (Idle state)

The arrays themselves are Heap allocations (0x2f4f8c0, 0x2f578f0) and lie outside the Module Capture — therefore, like with commands, enumeration had to run via registration.

idCVar object layout, 0x90 Bytes, read from constructor at 0x9ECA0 and Registrar loop at 0x123CD90, not assumed:

+0x00 idStr value (vptr, data*, len, SSO buffer from +0x18)
+0x30 intValue     +0x34 floatValue
+0x48 name*        +0x50 resetString*   +0x58 description*
+0x60 flags        +0x64 valueMin       +0x68 valueMax
+0x70 valueStrings** +0x78 callback*   +0x88 next (static list)

valueMin = 1.0, valueMax = -1.0 is the sentinel for "unbounded"; 131 Cvars have real limits.

The flag bits originate from the Reflection table in .rdata, read twice (24-Byte set at 0x2480670 as decimal text, 16-Byte set at 0x2517EF0 as number) — both say the same thing:

0x1 BOOL  0x2 INTEGER  0x4 FLOAT  (0x0 STRING, TYPE_MASK = 0|1|2|4)
0x8 CHEAT  0x10 NOCHEAT  0x20 TIME_MS  0x2000 NETWORKSYNC  0x4000 INIT
0x8000 ROM  0x10000 ARCHIVE  0x20000 LOCAL_SETTING  0x40000 MODIFIED
0x80000 MODIFIED2  0x100000 SHADERSYNC  0x200000 EXPOSE
0x400000 SHIPPINGDISABLED  0x800000/0x1000000 RESOURCE_DEPENDENCY_*

2. Two Independent Attacks

Data Attack – idCVarSystemLocal::RegisterStaticVars (vtable slot 28, RVA 0x123CD40, identified via the string idCVarSystemLocal::RegisterStaticVars: '%s' was statically duplicated) reads the list head at 0x123CD4D from RVA 0x30B58F8 — directly next to the command registrar 0x30B5900 — and links each node. Bit 0x200000 decides at 0x123CF18 regarding inclusion in the second list:

0x123cf18 test dword [rbp+0x60], 0x200000 ; CVAR_EXPOSE 0x123cf1f je 0x123cfea ; otherwise only list 1 0x123cfea mov rbp, [rbp+0x88] ; next node

The walk yields 3256 nodes, 3256 unique names, no duplicates.

Code Attack – the constructor is inlined at every declaration site and always ends with a call to the common value initializer 0x1238730 (identifiable by idCvar: Init value '%s' is not valid for BOOL cvar %s). There are 3258 such call sites, each in its own compiler-generated initialization function. A register/store tracker reads out name, resetString, description, flags, min, max. Result: 3256 objects; the two remaining call sites (0x1239034, 0x123932C) lie within the Cvar system itself and have no object in rcx — these are set/reset paths, not registrations.

Cross-check: 3256 vs. 3256, identical object sets, 0 deviations in Name, Default, and Description. For Flags, 59 entries differ, and they differ exclusively in the runtime bits CVAR_MODIFIED/MODIFIED2 — these are the 59 Cvars that were modified during the recorded session. The catalog masks these bits out and reports them separately as session_modified. 8 default values could not be resolved by the code attack (Build-Info Cvars, whose reset strings are interned at runtime); this is a gap in the code attack, not a contradiction, and is reported accordingly.

3. Why the Engine Only Shows Eight to Eleven g_ Cvars

idCVarSystemLocal::Find (vtable slot 4, RVA 0x1239700) selects the hash index using the same restriction level as the command dispatcher:

0x1239737  mov   r10, [0x41244d8]     ; Restriction-Level
0x1239744  mov   ecx, 0x48            ; Hash "all"
0x1239749  mov   r14d, 0x70           ; Hash "exposed"
0x1239754  cmove r14d, ecx            ; Level 0 -> all, otherwise only exposed

The same pattern appears in listCvars (0x123CA84, selection between +0x8 and +0x28). And the command dispatcher passes an unknown command at 0x126533E to idCVarSystemLocal::Command (slot 17, 0x1239F20), which in turn calls Find — both +g_fov 100 on the command line and g_fov 100 in the console run through this exact lookup.

The console and +-prefixed parameters always run at Level 1 according to the command report. Consequently, exactly the 304 exposed Cvars are readable and settable from outside, while the remaining 2952 are not. "Registered" and "settable" are two different things in this engine, and the difference is a factor of 10.

Testable counter-examples that would refute the model, and their outcomes:

Observation Model Prediction Finding
listCvars - help bot_ → "0 cvars listed" bot_fakePing, bot_minClients exist but are not exposed confirmed
g_infiniteAmmo, g_freeCamSpeed → "Unknown command" (measured in docs/stand.md) both registered (0xa, 0xc), both not exposed confirmed
listCvars - help g_ yields few hits 11 Expose-Cvars contain g_, all with descriptions confirmed, see below

Correction in docs/stand.md: it states "eight Cvars" for listCvars - help g_. The dump log in docs/cvars_tnc_echt.json itself records for the search text g_ that "found: 11, reported: 11", and my table provides exactly those same 11 (10 with prefix g_ plus popupDialog_debug). The eight is an older, outdated number. This does not change the conclusion — 11 out of 287 registered g_ Cvars is the same statement.

4. Controls

Control Result
Positive control: all 304 from docs/cvars_tnc_echt.json 0 missing
Descriptions against engine output 295 identical, 9 differ only through console dump normalization, 0 real discrepancies
Expose set == Engine set yes, 304 == 304, in both directions
Cross-check Code attack ↔ Data walk 3256/3256, only_data=0, only_code=0, Name/Default/Description 0 discrepancies
Negative control --sec-delta 0x8 / 0x10 / 0x28 / -0x1000 / 0x200000 Each terminated with Exit 2, no table found
Negative control --base-delta 0x10000 Termination: vtable 0x2968510 != expected 0x2978510
Negative control --sec-delta 0x8 --ignore-anchor (anchor intentionally skipped) Walk yields 0 nodes
Negative control --validator-sweep see below

Regarding the nine "Dump normalizations": in seven cases, the console dump swallowed a / or compressed spaces (screenres / 8 → screenres 8, 1x2/2x1 → 1x2 2x1). In two cases, my table is better than the engine output: r_SSR has no description there, while the table lists 1: Enable screen space reflections pass. Thus, the discrepancies lie on the dump side, not the table side; the comparison script excludes them individually rather than removing them via normalization.

The --validator-sweep is the most honest of the controls and partially fails negatively: running the Record Validator without a chain over all 4,547,850 8-byte offsets in .data, it accepts 9,512 of them — 3,256 true positives and 6,256 false positives. However, the longest chain reachable from any of these false positives contains only 2 nodes. Thus, the Record Validator alone is not a detector; what provides the proof is the chained list plus its length matching the live counter. A false head yields no plausible table but rather one or two nodes.

5. Retail Block: the flags say something different than wolfsdk/cvars.py

CVAR_SHIPPINGDISABLED (0x400000) is set to null Cvars in this EXE and is never queried anywhere in the Cvar system. A complete scan of the Cvar code (0x1214600–0x123D300) for a test against 0x400000 yields no hit. The string CANNOT be set in retail does not exist in NewColossus_x64vk.exe — it originates from The New Order.

Consequently, RETAIL_BLOCKED from wolfsdk/cvars.py is not measured against this EXE. The module docstring states this itself ("mined out of the retail WolfNewOrder_x64.exe"), but the loader uses the list for Part 2. Compared against the real table:

Count
measured RETAIL_BLOCKED names 26
of which not registered at all in TNC 11
of which registered but not exposed (model says: not settable) 15
of which exposed (would be a contradiction) 0

The eleven non-existent ones are: ai_invulnerable, com_allowConsole, com_skipIntroVideo, g_AimAssist_Disable_All, g_gameDifficulty, g_goreBloodyMess, g_showThinks, g_useSavegameDifficulty, pm_autoSprint, pm_crouchspeed, pm_noBob.

One true contradiction remains: g_damageScale. It is registered (0xc = FLOAT|CHEAT), not exposed — the flags thus say "unreachable" — but the measurement recorded it as "silently accepted". The resolution is likely that "silently accepted" constitutes no effect proof at all: g_permaGodMode, g_permaInfiniteAmmo, and com_showFPS were also evaluated as "accepted" and simply do not exist in TNC. A command line hitting an unknown name does not protest — it does nothing. I do not smooth this over: as long as g_damageScale is not clarified via runtime testing, here stands an unresolved contradiction, and a runtime test was forbidden to me.

Also to be corrected: ALWAYS = {"com_skipIntroVideo": "1"} in wolfsdk/cvars.py hangs for Part 2 on an argument whose CVar does not exist in TNC. This is precisely the open point from docs/stand.md ("README still claims that the intro skip works") — it is now statically decided: in Wolfenstein II, this argument has no effect.

Which of the 3256 are therefore "settable in retail"? The engine's answer: the 304 with CVAR_EXPOSE, and only those. settable_in_retail in the new dataset is exactly this bit and nothing else. Of these 304, all 68 are CVAR_ARCHIVE Cvars — that is, everything that writeConfig writes is also accessible.

6. Diff against docs/cvars_tnc.json

Count
Entries in the old file 2231
of which actually registered Cvars 1822
of which Phantoms (no registered Cvar) 409
Phantoms that nevertheless carry an invented description 27
registered Cvars missing from the old file 1434
Entries with description (real names) 1347
of which description correct 1119
of which description incorrect 228
… and of which literally the description of another Cvar 189

The 189 are the hard core of the assignment error: the old method distributed descriptions by neighborhood and word overlap in .rdata, and in 189 cases it assigned the text of another, real existing CVar. Examples:

Cvar In the file Actually
ai_interactionView_Distance "if where we are and where we think we are supposed to be ex…" (belongs to ai_interactionView_CorrectionDistance) "how far away from the ai"
ai_searchThresholdDebug Text of ai_searchTimeoutDebug "…how long the search timeout threshold is"
ai_droppedProjectileZ "a grenade exists without a projectile decl…" (an error message) "random interval applied to dropped projectiles z component"

The cross-check mentioned in docs/stand.md confirms itself: com_assertNoDialog appears in the new dataset with "bypass the popup assert dialog", exactly as output by the engine.

7. The Replacement Dataset

re_probes/cvartable/cvars_tnc_v2.json, 3256 entries, 1.85 MB. Per CVar:

name, description, default, type, flags, flags_decoded, exposed,
settable_in_retail, cheat, archived, read_only, init_only, min, max,
value_strings, callback_rva, record_rva, session_value, session_modified

Distribution: 1138 bool, 950 int, 902 float, 235 string, 31 time_ms; 3044 with description, 212 without (there null stands, not guessed); 3142 with Default; 131 with real Min/Max bounds; 4 with value_strings (sys_lang, mt_genVmtrCompression, mt_genPageCompression — and sys_lang provides the twelve language names as a selection list); 2961 with CVAR_CHEAT, 68 CVAR_ARCHIVE, 119 CVAR_INIT, 20 CVAR_ROM.

Two fields require a warning because they would otherwise be confused again:

  • default is the reset string declared in the binary image — this is the default value, which the old file unsuccessfully searched for.
  • session_value is the value in the Snapshot, i.e., of an ongoing session, and explicitly not a default value. For 25 Cvars it is null, because their value lies in the heap and not in the module capture.

The dataset is a proposal and is deliberately not placed in docs/. For the Loader GUI, only exposed == true should be interesting: 304 lines guaranteed to work, instead of 2231, of which the majority do nothing.

8. Anomaly

A Cvar is registered with an empty name: Record 0x2f0f0f0, Default -10.1, Description "offset modifier for the top cutoff plane", Flags 0xc. Both attacks agree on this point — the instruction at 0x9ECA0 loads the empty string 0x209CB3C as the name in both constructor branches. The obviously intended name snapEdit_topCutOffPlaneHeight is located in .rdata at 0x2621CA0 directly before Default and Description, but is not referenced by any instruction. This is an Engine bug, and at the same time the clearest example of the separation we are discussing here: snapEdit_topCutOffPlaneHeight is a string in the binary and not a registered Cvar, while a registered Cvar without a name exists that can never be addressed.

9. Open Points and Risks

  • g_damageScale — see Section 5. Unclear contradiction between flag model and measured list. Verifiable with a start and qconsole.log, which was denied to me.
  • The Snapshot is one session. Cvars that are only registered dynamically under certain conditions would not have been seen by me. The fact that the Live Counter exactly matches the length of the static list (3256 == 3256) means: in this session there was not a single dynamically registered Cvar. Whether this is always true, the Snapshot does not say.
  • The Restriction-Level stood at 0 in the Snapshot. That Console and + parameters raise it to 1 is taken from the Command Report and not measured anew by me; my evidence for this are the three counter-examples in Section 3.
  • 8 Default values (Build-Info-Cvars) come only from the data walk, not from the code attack. They appear in the catalog but are not double-booked.
  • "String present in .rdata" was consistently not evaluated as evidence. Each of the 3256 entries has both a decoded constructor instruction address and a validated node in the Registrar list.