Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Latest research roundsNeueste Forschungsrunden

Kiscule Command Nodes Run Through the Same Unrestricted Path as resourceExec

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

In plain wordsIn einfachen Worten

Wolfenstein II hides most of its 234 console commands; only 34 can be typed normally. This report read the game's machine code and found that the 'Command' node in mission scripts (Kiscule) puts its text into a queue that the engine runs against the full, unlocked list twice every frame. So a trigger in a custom map could in principle switch on God mode or infinite ammo with no console at all. This is proven only by reading the code; it has not yet been tried with a real script node in the running game.

  • 234 commands exist, only 34 are open to the normal console.
  • The Command node only queues text and never calls the restricted dispatcher itself.
  • The engine's per-frame function runs the unrestricted queue against the full catalog twice per frame.
  • None of the 7 Command nodes in the original game needs this unlocked path, so there is no shipped example.
  • The related resourceExec route is confirmed in game (God mode, infinite ammo); the Command-node route is untested.

Wolfenstein II versteckt die meisten seiner 234 Konsolenbefehle; nur 34 lassen sich normal eintippen. Dieser Bericht hat den Maschinencode des Spiels gelesen: Der „Command“-Knoten in Missionsskripten (Kiscule) legt seinen Text in eine Warteschlange, die die Engine zweimal pro Bild gegen die volle, freigeschaltete Befehlsliste abarbeitet. Ein Auslöser in einer eigenen Karte könnte also Gottmodus oder unendliche Munition ganz ohne Konsole einschalten. Das ist nur durch Codelesen bewiesen; mit einem echten Skriptknoten im laufenden Spiel wurde es noch nicht ausprobiert.

  • Es gibt 234 Befehle, nur 34 sind in der normalen Konsole erreichbar.
  • Der Command-Knoten stellt nur Text in die Warteschlange und ruft den eingeschränkten Befehlsverteiler nie selbst auf.
  • Die Bild-für-Bild-Funktion der Engine arbeitet die freie Warteschlange zweimal pro Bild gegen den vollen Katalog ab.
  • Keiner der 7 Command-Knoten im Originalspiel braucht diesen freien Weg, es gibt also kein mitgeliefertes Beispiel.
  • Der verwandte resourceExec-Weg ist im Spiel bestätigt (Gottmodus, unendliche Munition); der Weg über den Command-Knoten ist ungetestet.

Technical detailsTechnische Details

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

Follow-up question to the project's console-restriction research: a mission script (kiscule) can contain an idKisculeNodeCommand node that runs a raw command string (e.g. "r_dof 0", "triggernextmap") when triggered. Does that node call the restricted console dispatcher the same way typing a command does, or does it reach the same unrestricted path resourceExec is already known to use? Found by direct disassembly: the unrestricted path. The node's handler never calls the dispatcher itself — it queues the command text into one of the engine's two command buffers, selected by whatever the restriction level happens to be at that instant, and that level sits at the unrestricted value for essentially the entire session. This closes the single open question the earlier console-restriction research flagged as its top priority. The code path is proven by disassembly end to end; using it from an actual kiscule node in a running game has not been tested. It should behave the same as the already-proven resourceExec route, which the project has confirmed live: Godmode and g_infiniteAmmo both took effect in game, while Notarget printed as set but enemies already engaged in combat kept attacking — the unlock reaches the command/cvar, not necessarily every AI behavior that reads it.

Background: the 234-command catalog and the restriction level

The engine registers 234 commands total, of which only 34 are exposed to the normal console — commands like map, devmap, God, killMonsters, teleport are registered but not on the exposed list, so typing them normally returns "Unknown command". A single global integer (not a CVar) decides which list the dispatcher searches: level 0 searches all 234, level 1 searches only the 34 exposed ones. resourceExec is one of the 34 exposed commands, and it deliberately sets the level to 0 for the duration of the script it runs — already proven and already confirmed live for Godmode/g_infiniteAmmo. This report does not re-derive that mechanism; it asks whether a kiscule Command node reaches the same unrestricted window.

Note for anyone cross-checking older notes: SpawnAI, give, spawn, and noclip are not among the 234 registered commands in the retail build at all — they fail identically at any restriction level, exposed or not. They are not part of this report's chain.

The Exec handler: queues text, never dispatches directly

Found via the class's RTTI vtable (two slots unique to idKisculeNodeCommand among five sibling node types checked): the registered Exec callback reads the command parameter, appends a trailing newline, and calls idCmdSystemLocal::AppendCommandText — not the command dispatcher. Condensed from the disassembly:

read "command" parameter, append '\n'
rax = singleton's current vtable pointer
cmp rax, <the real, un-hooked vtable>       ; defensive check, always true in retail
je  <call directly into the selected buffer>
  cmp [restriction_level], 0
  cmovne rcx, <restricted buffer>            ; level != 0 -> restricted buffer
                                              ; level == 0 (default) -> unrestricted buffer
  append(buffer, text)
jne <call through the vtable instead — same destination, defensive fallback only>

No call to the dispatcher and no write to the restriction-level global appears anywhere in this function. A Command node's Exec pulse only ever queues text; it neither executes immediately nor changes the ambient level itself.

The drain: idCommonLocal::Frame(), twice a frame

The generic buffer-drain helper (already known to run once at startup for default.cfg/package.cfg) turns out to have a second call site: the engine's own main per-frame function (identified by its own embedded assert string and independently cross-checked against its position in the class's vtable). It drains both command buffers, twice every single frame:

drain(level=0)   ; unrestricted buffer against the full 234-command/3256-cvar catalog
drain(level=1)   ; restricted buffer against the 34/304 exposed ones

— twice, in two separate places inside the same function. So: whatever a Command node queues while the ambient restriction level sits at 0 (true for essentially the whole session, per the prior research's exhaustive save/restore trace) is executed against the full, unrestricted catalog on the very next frame, with no patch, no resourceExec, and no DevGUI needed. A second confirmation that this is a general engine idiom, not a one-off for kiscule: a key-binding deserialization routine builds command strings ("unbindall\n", "bindset %d\n") and appends them into the exact same unrestricted buffer using the identical pattern.

A caveat found along the way

The task behind this research suggested that a retail Command node using a non-exposed command name would itself be evidence for the level-0 path. That does not hold for the actual retail data, reported honestly rather than cherry-picked: of the 7 retail idKisculeNodeCommand instances that exist, r_dof and print (6 of the 7 uses) are both already exposed commands that would work at either restriction level, and the 7th (triggernextmap) is not a registered command under any name or case at all — it would fail with "Unknown command" regardless of level. The yes/no verdict in this report rests entirely on the code-path proof above, not on an empirical retail example.

PROVEN

  • The Command node's Exec handler (found via RTTI vtable) calls the devirtualized inline of AppendCommandText, never the dispatcher, confirmed via raw disassembly of both the common and the defensive-fallback branch.
  • The buffer-select logic matches the already-known resourceExec addresses exactly.
  • The per-frame drain function is idCommonLocal::Frame() (confirmed by its own embedded assert string and independently by its vtable slot position), and it drains the unrestricted buffer at level 0 then the restricted buffer at level 1, twice per frame.
  • None of the 7 retail Command-node instances actually needs the level-0 path to work (6 are already-exposed commands, 1 isn't a valid command at all).

INFERRED

  • That the per-frame drain function is reached every simulated frame during actual gameplay — not independently traced from the top-level message loop, but the standard, unambiguous meaning of an engine's own Frame()-named function that runs this exact "drain both command buffers" pair, matching the universal idTech-family pattern.
  • That a trigger firing a Command node during gameplay is never synchronous with the brief window where a console keystroke/exec/resourceExec holds the level at 1 — not formally proven impossible, just never documented or plausible as a reachable interaction. The one theoretical edge case would instead land in the restricted buffer and execute at level 1 on the same frame's second drain.

In-game test (not yet run)

In a test map, wire a trigger volume directly to a Command node with command = "God 1" (capitalized — the lowercase god is not registered at all). Enter the trigger volume, then take damage from an enemy without touching the console. If no damage is taken, God 1 reached the dispatcher through the full, level-0 catalog — it is not among the 34 exposed commands and cannot be reached through the normal console (level 1) at all. As a falsifiable control, run a second instance with command = "triggernextmap" in parallel: since that name is not a registered command under any case, nothing should happen regardless of level; if it does have an effect, the chain documented here is wrong or incomplete. A sharper, damage-test-free variant: use the non-exposed cvar g_infiniteAmmo 1 instead and fire without reloading — matches the project's already-confirmed-live resourceExec result for the same cvar.

Related: Research Report: Cmdtable (the 234/34 command catalog this builds on), Research Report: Execcarrier (the resourceExec carrier route, confirmed live), Developer Mode, Kiscule Scripting, Mission Kiscule Scripting.