Round-6 research pass on how The New Order drives map events. The answer is two layers of one system: a plain-text entity graph of idTarget_*/idTrigger_* classes wired by targets lists (plus scriptObject template bindings), and a real, compiled idTech4/Doom3-lineage script VM. New relays and script functions can be authored with the existing tools; new trigger volumes cannot. String- and data-level evidence only (no TNO runtime image existed yet), and nothing was tested in game. The code-level follow-up is The DoomScript VM (R7).
Read-only against <Steam library>/steamapps/common/Wolfenstein.The.New.Order (archives
read from base/_wolfsdk_backup/ via wolfsdk.patch.Installation/pristine(), exactly
the pattern tools/entities.py and tools/probe_tno_maps.py already use) and the EXE's
.rdata/.data as a flat ASCII string scan (.text is SteamStub-encrypted and
unreadable statically — confirmed again here, consistent with docs/ghidra.md; TNO has
no runtime image, unlike TNC's re_probes/runtime_87400/module.bin, so every EXE-side
claim below is a string/offset in the file, not a disassembly or a decompile). No game was
launched, no process attached. All writes are under this track's own
re_probes/tnoscript/ (extraction scripts + the 25 .entities and 37 .script files
pulled from the archives for reading, ~44 MB total, nothing outside that folder and this
report).
Builds on and does not redo: docs/moddingflaeche-tno.md §4.1–4.2 (first-pass
identification of .entities/.script as the two map-event layers, the in-place-patch
budget of 0–428 bytes forcing a full chunk rebuild for both, marked "vermutlich,
ungeprüft", i.e. "probable, unchecked"), docs/ui-assets.md §7.3 ("the most interesting unfollowed thread" — named
script_events.script's 913 signatures as the most powerful unused lever and explicitly
stopped there because following it needs a launched game), docs/kiscule.md (TNC's
node-graph equivalent — cited throughout for contrast, not reused), docs/ghidra.md /
docs/ghidra/klassen_tno.csv (TNO RTTI: 2 368 classes with inheritance, used here for the
unused-class cross-check).
Pass 1 errors (fixed in this pass)
The verifier refuted 3 claims in pass 1, all now fixed below with re-measured data (same
extraction re-run from the archives, out/ deleted again afterward):
- "
scriptobjectnever appears as an entity field" — wrong.scriptObject = { type = "X"; <params>; }is a common, structured entity block: 173 instances across 20 of the 25.entitiesfiles, naming 24 distinctobjecttemplate types. This isn't a minor correction — it answers §9's own "genuinely open" question ("how is ascript_util.scriptobject bound to an entity"):scriptObject.typeis the binding. See new §4.1a. - "
moveSelf/rotateSelf/bobSelf/randomActivatorbound to zero entities" — wrong, same root cause as #1: the grep looked for the bare template name as a spawnarg key, not inside ascriptObject.typevalue. Re-measured:rotateSelf=36,moveSelf=22,bobSelf=6,randomActivator=3 (67 instances). See §4.1a. - "211-opcode table at
0xdb6db8–0xdb7ee8" was presented as the table — it's one of 3 byte-identical copies (0xdb6db8,0xdf49e8,0x1030e80, each exactly 211 entries), and 92 moreOP_-prefixed strings exist elsewhere for two unrelated systems (an NFSv4 protocol op set, a shader/face-blend op set) that a naive re-scan would conflate with the VM's opcode set. Re-measured exactly, not just acknowledged — see new §3.1.
Also folded in from the verifier's "missed" list: the script_scriptEventsPath /
script_debug / script_suppressPrints cvars (§3.2a), the c16p0 script-less-map converse
case (§5, confirmed consistent), and the full 24-type scriptObject census (§4.1a). The
116-vs-112 class-count regex wording was imprecise, not the numbers — fixed in §4.3's
footnote. idTarget_Cvar reachability remains genuinely open (§6) — no runtime image, no
way to resolve it statically; a TNO-specific cheat-cvar gate was found this pass (§6a) and
is relevant context, not a resolution.
Question
TNO level scripting: how does id Tech 5 Wolfenstein drive events in maps (script decls / entity targets / triggers / a script VM?), its format and command set (from data files and EXE strings), how do TNO map entities reference scripts, and can custom events (trigger → spawn/door/sound/objective) be authored for TNO maps with the existing tools?
Answer in one paragraph
All three guesses in the question are correct at once — they are the same system in two
layers, not alternatives. Layer 1 is the entity graph already partly covered by
moddingflaeche-tno.md: 25 .entities maps carry 116 distinct idTarget_*/idTrigger_*
classes as plain decl-text entities (the New Colossus "Kiscule" node-graph system does
not exist in this engine generation at all — there is no binary graph format; the whole
graph is the decl text), wired together by a targets = { item[0] = "entityname"; } list
exactly like classic idTech4 target/targetname, with volumes (idTrigger) distinguished
from pure logic relays (idTarget_Count, 8 029 instances — the most common class in the
game) by whether they carry a clipModelInfo.clipModelName pointing at a separately baked,
binary cm collision model. A third, named-but-templated authoring lever sits inside the
entity graph too: scriptObject = { type = "X"; } blocks (173 instances, 24 distinct
template names) bind a named object behavior — a mover, rotator, or one of 20 other
per-map templates — directly to an entity (§4.1a, corrected this pass from pass 1's wrong
"never found" claim). Layer 2 is a real, compiled, C-like scripting VM, the
unmodified idTech4/Doom3 "DoomScript" lineage confirmed by 211 distinct OP_* bytecode
mnemonics found verbatim in .rdata, in 3 byte-identical copies (OP_PUSH_ENT at file
offset 0xdb7de0, OP_GOTO at 0xdb7ec8, full table 0xdb6db8–0xdb7ee8 and two more
copies at 0xdf49e8/0x1030e80, see §3.1 — corrected this pass from pass 1's single-copy
framing) plus the idCompiler's own error
strings ("Compile failed in file %s.", "scriptEvents can only defined in the global
namespace", w:\tech5\tungsten\game\script\Program.cpp(1270)), a stack-based thread
scheduler (idInterpreter, idThread — present in the RTTI, §2), and a 913-signature
native command table in script/script_events.script (giveInventoryItem, spawn,
teleport, respawn, kill, radiusDamage, openDoor, startSoundShader, nextMap,
devMap, doCheckpoint, setcvar, drawText, wait, …). The bridge between the two
layers is proven, not inferred: idTarget_Count relay entities carry an optional
call = "functionName" field, and across all 25 maps, 513 of 516 such fields (99.4 %)
resolve case-insensitively to an actual top-level function with the fixed signature
void name(entity activatedEntity, entity activator) defined in that map's own
<mapid>.script or one of its same-chapter sidecar scripts (measured below, §5 — the
same kind of cross-check kiscule.md ran for TNC, with a comparable ~99 % hit rate and a
handful of named retail defects, not parser failures). Map scripts are not referenced
by name anywhere in the map data — the engine loads maps/<id>.script by a fixed naming
convention identical to <id>.entities (32 of 32 map scripts match a map 1:1; confirmed
indirectly, §5). Authoring custom events with the existing tools: yes for logic (new or
edited relay chains — spawn/door/sound/objective targets, new script functions, new call
wiring — all plain text, all inside already-indexed file assets that already tolerate
arbitrary-length rewrites via a full chunk rebuild), no for new physical trigger volumes
(their shape lives in a separately baked, binary, unwriteable cm asset that cannot be
newly indexed — see §7). None of this was run in the game; every claim below is
format/string-level, and the recipe in §8 is the next, concrete, unperformed step.
1. What this track measured, reproducibly
python re_probes/tnoscript/extract.py # all 37 .script files -> out/scripts/
python re_probes/tnoscript/extract.py entities # all 25 .entities maps -> out/entities/
python re_probes/tnoscript/maplink.py # the call="..." -> function cross-check (§5)
python re_probes/tnoscript/strings_scan.py <text> # ASCII scan over WolfNewOrder_x64.exe (.rdata/.data only)
All four reuse existing, already-proven infrastructure only: wolfsdk.patch.Installation + tools/build_gore_deep.pristine() for archive access (the same call
tools/entities.py/tools/probe_tno_maps.py use), and a flat regex string scan for the
EXE (no disassembler, no Ghidra project — not needed for .rdata literals).
2. The two layers, by RTTI class name
docs/ghidra/klassen_tno.csv confirms both layers are real engine classes, not an
artifact of the decl text:
| Layer | Classes (examples) | RTTI base chain |
|---|---|---|
| Entity graph | idTarget_Count, idTrigger, idTrigger_Hurt, … |
idEntity;idEventReceiver;idClass |
| Script VM | idScriptObject, idThread |
idClass / idEventReceiver;idClass |
| AI↔script glue | AC_DoomscriptDone, AC_ExecutingScript, AC_NoActionScript, idAIScriptProxy, idAIScriptedAnimWeb |
idAIStateTransition… / idEventReceiver;idClass |
(idProgram, idCompiler, idInterpreter, idVarDef, idEventDef — the classic idTech4
compiler/VM classes — are not in the RTTI dump because they are not polymorphic
idClass descendants, so MSVC never emits a type_info for them; their existence is
instead confirmed directly by the string evidence in §3, which is the stronger proof for a
compiler anyway — error messages, not just a name.)
3. The script VM, read straight from .rdata/.data
3.1 Bytecode: 211 opcodes — in 3 identical copies, plus 92 decoys from unrelated systems
0x00db6db8 OP_RETURN ... 0x00db7ee8 OP_CONTINUE (211 entries)
0x00df49e8 OP_RETURN ... 0x00df56d8 OP_CONTINUE (211 entries, byte-identical)
0x01030e80 OP_RETURN ... 0x01032e08 OP_CONTINUE (211 entries, byte-identical,
immediately adjacent to the
idCompiler string block at
0x01032e00, §3.2)
Corrected from pass 1, which cited only the first copy as if it were the table.
Re-scanned this pass with a case-sensitive, prefix-anchored match (" OP_[A-Z]", 725 hits
total over the whole EXE, up from pass 1's looser case-insensitive substring query that also
pulled in unrelated idProp_* class names): the 211-mnemonic table exists 3 times,
byte-identical, at the three offsets above (3 × 211 = 633 of the 725 hits). The remaining
92 are genuine OP_-prefixed strings belonging to two unrelated systems that happen to
share the OP_ prefix convention — confirmed by reading them, not assumed:
- 38 + 27 + 22 = 87 at
0xd84360/0xdd5b68/0x1066db8are either an NFSv4 protocol operation set (OP_ACCESS,OP_CLOSE,OP_COMMIT,OP_CREATE,OP_DELEGPURGE,OP_DELEGRETURN, …) or a shader/material expression opcode set (OP_TYPE_MOVE/OP_TYPE_SWIZZLE/OP_TYPE_ADD/OP_TYPE_UNMASKED_MOVE_CONSTANT, present in 2 near-identical copies of its own). - 5 at
0xf3d334are a facial-animation blend opcode set (OP_DECODE_FACECOMPOSITE_AND_BLEND,OP_DECODE_AND_PUSH,OP_POP_AND_BLEND,OP_END,OP_NO).
None of these 92 overlap the DoomScript mnemonic vocabulary (no OP_PUSH_*, OP_CALL,
OP_THREAD, OP_GOTO, etc. among them) — the 211-mnemonic vocabulary count from pass 1
is unaffected and independently re-confirmed exact, but a plain grep OP_ without the exact
offset window (which is what pass 1's own §1 commands implicitly invited a re-run to do)
would silently mix in 92 unrelated strings from two other engine subsystems. Fixed tooling
note: re_probes/tnoscript/strings_scan.py <query> does case-insensitive substring matching
by design (so idProp_* matches a query of "OP_") — anyone re-running §1's commands for
the opcode table specifically should grep the output for " OP_[A-Z]" (case-sensitive,
prefixed by two spaces = start of the string field), not just the raw query match.
Interleaved with the opcode entries (in all 3 copies) are per-opcode type-tag strings
(int, <PUSH>, <CALL>, <THREAD>, <GOTO> …) that are debug-print metadata, not
opcodes themselves (excluded from the 211). This is the exact Doom3/Quake4 idProgram
bytecode set unmodified (arithmetic per-type: OP_MUL_F/_V/_I/_FV/_VF/_IV/_VI/_FI/_IF;
"unary"/compound-assign variants OP_UADD_*/OP_USUB_*/OP_UMUL_*; array indexing
OP_INDEXARRAY_FF…DI; the dispatch triad OP_CALL/OP_EVENTCALL/OP_OBJECTCALL/
OP_SYSCALL/OP_LISTEVENTCALL). Only the mnemonic strings are proven here — the
numeric opcode values and the dispatch table that maps them to handlers live in the
encrypted .text, so the byte-level bytecode format (operand widths, instruction size)
is not recovered, only the complete instruction vocabulary. Why the table is
triplicated is not determined (3 separate static const arrays compiled into 3 translation
units is the ordinary explanation for a linked C++ binary; not pursued further, cosmetic to
the research question).
3.2 The compiler: idCompiler, confirmed by its own diagnostics
Contiguous block 0x01032e00–0x01036000 is the compiler's string table. Selected,
exact, at their file offsets:
0x01033108 Divide by zero
0x01033118 Script referenced decl '%s' but decl does not exist
0x010331a8 Unknown token '%s'
0x01033240 Function '%s' not found
0x01033280 Unexpected end of file inside function '%s'. Missing closing braces.
0x010332e0 Unexpected end of file inside object '%s'. Missing closing braces.
0x01033330 Unexpected end of file inside namespace '%s'. Missing closing braces.
0x01033568 scriptEvents can only defined in the global namespace
0x01033b30 Objects cannot be defined within functions or other objects
0x01033ba0 Objects may only inherit from objects.
0x01033c18 Functions may not be defined within other functions
0x01034100 /script/script_events.script (auto-#include, confirmed)
0x01034130 /script/script_defs.script (auto-#include, confirmed)
0x01034188 Compiled '%s': %.1f ms
0x01035048 w:\tech5\tungsten\game\script\Program.cpp(1270) : TAG_SCRIPT
0x010353a0 Exceeded maximum allowed number of functions (%d)
0x010353d8 Exceeded maximum allowed number of statements (%d)
0x01035460 script/disasm.txt (a disassembly dump path — undocumented
cvar/command gates it, name not found
in this scan)
0x010354b0 ---------- Compile stats ----------
0x010355f8 Compile failed.
0x01035638 Compile failed in file %s.
0x01035678 Initializing scripts
This nails down what moddingflaeche-tno.md §4.2 only inferred from the two auto-include
comments in script_main.script: compilation happens once, at map load
("Initializing scripts" / "Compiled '%s': %.1f ms"), is fatal on error ("Compile
failed in file %s." — no fallback), and the leaked build-tree path confirms the TNO engine
fork's internal name "tungsten" (also the asset-path prefix seen throughout
moddingflaeche-tno.md's AI examples, e.g. ai/tungsten/c06/... — same codename, now
tied to the exact source file).
A second, separate block right before it (0x01032e14–0x01032fff) is a generic
HTML-table doc writer shared by at least two features: a "Wolfenstein The New Order CVar
List" page and, immediately above it, ../docs/Editing/Script/script_operators.html
with "<p>This file has been automatically generated with the '%s' console command.</p>"
— the engine can dump its own operator-precedence table to HTML via a console command,
whose exact name this scan did not resolve (format strings only name it as %s, filled at
runtime; .text is encrypted so the AddCommand call site is unreachable statically).
Flagged as an open lead, not used further here.
3.2a Three script-subsystem cvars found in .rdata (new this pass, from the verifier's "missed" list, re-confirmed independently)
Immediately next to the script_operators.html doc-writer block (§3.2):
0x01032e20 If set, tells engine to load script_events.script from specified location
0x01032e70 script_scriptEventsPath
A real cvar the engine checks at script-compile time to redirect where it loads
script_events.script from — this is the most directly relevant lever this pass found
for "can custom events be authored": if settable, it could point the compiler at a
mod-extended event table without touching the encrypted .text's native dispatch. Whether
it is settable from a loader-replaced cfile (the TNC-style resourceExec <file> -s route,
docs/moddingflaeche-tnc.md) is unverified for TNO — flagged, not tested (no TNO
console-restriction work exists yet, same gap pass 1 already flagged).
Two more, found and read in context (not just named):
0x00ffdd50 / 0x010351a8 / 0x010351f8 / 0x01035228 / 0x01035dc8 script_debug
(5 separate GetBool()-style check sites, gating different VM debug prints: "Script: Init
List %s to %d", "Script: Create List %d", "Script: REMOVE LIST %d | TOTAL : %d" — a single
verbose-logging toggle checked at several call sites, not 5 different cvars)
0x010361b0 suppress script tty prints
0x010361d0 script_suppressPrints
No default-value or CVAR_* flag string sits adjacent to either (unlike script_suppressPrints,
whose description string "suppress script tty prints" sits directly before its name — the
ordinary idCVar-registration string layout), so their flags/defaults are not recovered
here, only their existence and purpose strings.
3.3 Thread/stack model — confirms the thread keyword seen in script_util.script
0x010344a8 %s(%d): Thread '%s': %s
0x010345c8 call stack overflow
0x01035f98 idStateThread::GetScriptFunction : Null entity
Scripts run as cooperative threads with their own call stack ("Stack depth: %d bytes, %d
max", "Call depth: %d, %d max"), and idStateThread ties the identical thread machinery
into the AI state system (AC_ExecutingScript/AC_DoomscriptDone, §2) — the same VM
drives both map-trigger callbacks and AI state-machine script hooks, one interpreter, two
call sites.
4. The data format, parsed directly (37/37 .script, 25/25 .entities)
4.1 .script — C-like source, unchanged from the leaked idTech4/Doom3 dialect
37 files (32 map-specific + 5 shared: script_main.script, script_util.script,
script_defs.script, script_events.script, debug_drawtext.script), all plain ASCII
text, sizes 415 B (c02p0.script) to 246 921 B (script_events.script). Confirmed
constructs, read directly out of the files (not reconstructed from memory of the format):
// #define / #ifndef preprocessor (script_defs.script)
#define BIT( num ) ( 1 << ( num ) )
#define waitUntil( x ) while( !( x ) ) { sys.waitFrame(); }
// top-level function, the fixed callback signature every "call" target needs:
void call_freeLook( entity activatedEntity, entity activator ) { ... }
// an "object" — a named, strongly-typed bundle of persistent members + methods,
// one of which (`onTrigger`) the engine calls back directly:
object moveSelf {
void init();
void onTrigger( entity activator );
float moveTime; float moveAccelTime; float moveDecelTime;
float distance; int direction; boolean toggle;
};
void moveSelf::onTrigger( entity activator ) {
self.move( direction, distance );
if( toggle ) distance = -distance;
}
// threads, entity-method calls, sys.* built-ins:
thread delayRemoveThread( ent, mytime );
$player1.getUserButtons();
sys.wait( 0.01 );
script_util.script additionally declares four more object templates: moveSelf,
rotateSelf, bobSelf, randomActivator — bound to retail entities, and the binding
mechanism itself, both corrected in §4.1a below (pass 1 grepped for the bare template name
as a spawnarg key and found nothing, because the real field is one level deeper).
4.1a scriptObject = { type = "X"; } — the entity↔script-object binding (corrected this pass, was wrongly called absent/unresolved)
Pass 1 claimed the string "scriptobject" never appears as an entity field and left "how is
a script_util.script object bound to an entity" as a genuinely open question (§9). Both
were wrong, from the same miss: the real field is a structured block, not a bare key:
entity { entityDef gp_func_movers_mover_corridor_blocker {
...
clipModelInfo = { clipModelName = "maps/.../gp_func_movers_mover_corridor_blocker"; }
scriptObject = {
type = "moveSelf";
moveTime = "3"; moveAccelTime = "0"; moveDecelTime = "0";
}
} } -- c01p4.entities:23839
entity { entityDef gp_mover_... {
scriptObject = {
type = "rotateSelf";
moveTime = "2"; moveAccelTime = "1"; moveDecelTime = "1";
rotationVector = "0 150 0"; toggle = "0";
}
} } -- c02p1.entities:32900
This is the binding: scriptObject.type names an object template by exact name, and
the fields alongside type are that object's own public members (moveTime,
rotationVector, toggle, … — the identical field names script_util.script's object
moveSelf { ...; float moveTime; ...; } declares, §4.1). Measured directly:
grep -c 'scriptObject = {' out/entities/*.entities -> 173 blocks total, 20 of 25 files
grep -A1 'scriptObject = {' ... | grep 'type =' -> 24 distinct type values:
flameburstprefab=45 rotateSelf=36 moveSelf=22 brakeblock=20 missilepoddamage=6
chainEvent=6 bobSelf=6 elevator=5 breakable_poster_trigger=5 randomActivator=3
holding_door=3 grenaderespawner=3 playerPusher=2 (+11 singletons: waterlevel,
wallCrushMover, supersoldat, ripOffMaskObject, prototyperobot, mechadeathshead_stage2/3,
londonmonitorturret, londonMonitorParams, londonMonitor, heavyrobot)
The 4 script_util.script-shared templates account for 67 of the 173 blocks
(rotateSelf=36, moveSelf=22, bobSelf=6, randomActivator=3 — directly refuting pass
1's "zero" claim). The other 20 distinct template names are not in script_util.script
at all — they are object definitions living in individual per-map .script files,
a second tier of the same mechanism pass 1 didn't enumerate:
grep -rhoE '^object\s+[A-Za-z_][A-Za-z0-9_]*' out/scripts/ | sort -u
-> Drone, RandomObjectRotate, bobSelf, brakeblock, breakable_poster_trigger, bus_move,
chainEvent, debug_drawtext, dynamicPlane, dynamicSun, elevator, explodeObject,
flameburstprefab, floatingBodies, grenaderespawner, hatch, heavyrobot, holding_door,
londonMonitor, londonMonitorParams, londonmonitorturret, mechadeathshead_stage2/3,
missilepoddamage, moveSelf, objectFollowSpline, panzerhund_move, pendulum, playerPusher,
prototyperobot, randomActivator, ripOffMaskObject, rotateSelf, searchLight, splineCar,
spotMover, spotTarget, supersoldat, swingingObject, wallCrushMover, waterlevel
Every scriptObject.type value measured above matches one of these object template names
exactly. This widens §8's authorable-object surface: a custom scriptObject block on an
entity, naming either a shared template (script_util.script) or a new per-map object
definition added to that map's own .script, is a third authoring lever alongside the
idTarget_* relay graph and the call="X" bridge — not run in-game, same caveat as the
rest of §8, but no longer an open question about whether the mechanism exists at all.
4.2 script_events.script — 913 native scriptEvent signatures, the command set
$ grep -c "^scriptEvent" out/scripts/script/script_events.script
913
This is the full native API surface a map script can call — sys.* globals and
entity.*/object.* methods, auto-generated (comment: "This defines the function
prototypes for all events..." per moddingflaeche-tno.md §4.2) and auto-#included in
every script (confirmed string 0x01034100 /script/script_events.script). A sample
directly answering "spawn/door/sound/objective":
scriptEvent entity spawn( string classname );
scriptEvent void respawn();
scriptEvent void respawnWithFade( string spawnSpot, int fadeTimeMS );
scriptEvent void nextMap( string mapName, string spawnSpot, string layersActive, string layersDeactive );
scriptEvent void devMap( string mapName, string spawnSpot, string layersActive, string layersDeactive );
scriptEvent void openDoor();
scriptEvent void linkSoundDoor();
scriptEvent vector getCenterOfDoor();
scriptEvent float startSoundShader( decl shader, int channel );
scriptEvent void stopSound( int channel, int netSync );
scriptEvent void doCheckpoint( entity activator );
scriptEvent void setcvar( string name, string value );
scriptEvent void executeCommandText( string command );
scriptEvent void drawText( string text, vector origin, float scale, vector color, int align, float lifetime );
(Full list: out/scripts/script/script_events.script, 913 lines, already extracted by
this track.) devMap as a scriptEvent (not just a console command) is new relative to
prior tracks' console-command work — it is reachable from inside a compiled map script
without touching the restriction-level machinery r4_consolecmds.md/r5_kisculecmd.md
measured for TNC at all (different engine, different gate — not re-checked here, flagged
in §9).
4.3 .entities — the target/trigger graph, 116 classes measured, spawnargs read directly
idTrigger (volume, needs a baked collision model):
entity { entityDef sound_trigger_condor9_enter {
inherit = "triggers/trigger"; class = "idTrigger"; expandInheritance = false;
edit = {
spawnPosition = { x=-1072; y=-408; z=-528; }
targets = { num=5; item[0]="sound_tr_enter_cockpit"; ... }
clipModelInfo = { clipModelName = "maps/game/wolf/c01/c01p1/sound_trigger_condor9_enter"; }
}
} }
idTarget_Count used as target/relay (pure logic, no clipModelInfo — proof that
relays are not volume-bound, §7), with the call bridge into the script VM:
entity { entityDef call_boattest {
inherit = "target/relay"; class = "idTarget_Count"; expandInheritance = false;
edit = {
"int" count = 1;
targets = { num=1; item[0]="gp_target_show_36"; }
delay = 0.0199999996;
call = "call_boattest";
repeat = true;
}
} }
Fields measured directly, across many instances: count (activations needed before
firing — idTarget_Count_Random additionally randomizes which target in the list
fires, 454 retail instances, e.g. a bank of flak-impact speakers picked at random),
delay (seconds before the chained targets/call fire), repeat (re-arms instead of
one-shot), triggerOnce (seen on idTrigger volumes), call (the script bridge, §5).
116 distinct classes whose name contains Target or Trigger anywhere appear across
the 25 maps (corrected this pass: pass 1's text described this as the anchored regex
id(Target|Trigger)[A-Za-z_]*, which does not actually match 3 of the 4 gap classes below —
that regex requires Target/Trigger immediately after id, which idSoundTrigger,
idPlayerHealthSoundTrigger and idInfo_Trigger_Facing_Target don't satisfy; the true
matcher is the broader "contains" grep, grep -h -o 'class = "id[A-Za-z_]*"' ... | grep -E
'Target|Trigger', independently re-run this pass and confirmed to give exactly 116). §6
below counts 112 of these because it deliberately narrows to the idTarget_/idTrigger_
(with underscore) naming convention to line up one-to-one against the RTTI dump — the
4-class gap is idTrigger itself (the bare volume base class), idSoundTrigger,
idPlayerHealthSoundTrigger and idInfo_Trigger_Facing_Target, which don't follow that
convention. Same number (116), two different matching rules producing it and the narrower
one (112) — not a contradiction, just two footnoted counts (the same kind of footnote
docs/kiscule.md needed for its own two counting rules), but pass 1's prose named the wrong
rule for the 116 figure. Directly relevant to the "spawn/door/sound/objective" part of the question, all
already used in retail, all plain decl text, all editable:
| Need | Retail class | Example edit field |
|---|---|---|
| spawn | idTarget_Spawn |
entityDefs.item[0].text = "generated/maps/.../name.decl", spawnEditable.spawnAt |
| door | idTarget_Door, idTarget_LockDoor/idTarget_UnlockDoor, idTrigger_OpenDoor |
— |
| sound | idTarget_StartSoundShader, idTarget_SoundStart/_SoundStop, idSoundTrigger |
soundShader name |
| objective (automap icon) | idTarget_Show/idTarget_Hide targeting a gp_misc_automap_objective_* entity |
targets |
| objective (mission end) | idTarget_CompleteGame, idTarget_NextMap |
— |
| inventory/secrets | idTarget_GiveItems, idTarget_GiveSecrets, idTarget_RemoveItems |
itemList |
| checkpoint | idTarget_PlayerCheckpoint (+ scriptEvent doCheckpoint) |
— |
| AI direct event | idTarget_ActionScript (eventName + typed parms, e.g. action_ClearPerceptionFlag) |
separate from the .script VM — a data-driven AI-event dispatch, not a call |
idTarget_Spawn's referenced decl (generated/maps/.../<name>.decl) is itself a shipped,
already-indexed asset — re-pointing an existing idTarget_Spawn at a different,
already-existing generated decl is plausible with the already-proven set-style decl edit;
authoring a brand-new spawn prefab is not (same "no new (type,name) index entry"
constraint moddingflaeche-tno.md §6.4 already established for decls in general, inherited
here unchanged — not re-measured, cited).
5. How entities reference scripts: a measured, not assumed, answer
Two separate mechanisms, both measured directly over the extracted files, not inferred from a single example:
(a) The whole map script loads by naming convention, never by an explicit reference.
No field anywhere in any .entities file, mapInfo decl, or the .mapresources preload
list (python tools/probe_tno_maps.py mapres — rows are opaque index-entry IDs, no
filenames, checked directly) names a .script file. Instead, every one of the 32
map-specific .script files shares its exact basename with exactly one .entities
map (c01p1.script↔c01p1.entities, c04p2_3.script↔c04p2_3.entities, …, 1:1,
confirmed by listing both sets). Combined with script_main.script's own comment ("the
main script that is loaded before any level scripts load") this is naming-convention
loading, the same pattern Doom3/Quake4 use (<mapname>.script auto-loads with
<mapname>.map) — inferred from the 1:1 naming match and the compiler's per-map
"Compiled '%s'" log line, not observed at runtime (no game launch).
The converse was flagged as unchecked by the verifier and is checked now: of the 25
.entities maps, exactly one, c16p0, has no matching .script file anywhere in
the archive at all (confirmed: comm -23 of the 25 .entities basenames against the 32
map-script basenames). This is consistent with, not a counter-example to, the model above:
c16p0.entities also has zero call="X" fields (confirmed), so the per-map compile
step would have nothing to call even if a c16p0.script existed — a map's script file is
presumably only emitted/shipped when that map actually uses the call bridge, matching the
"auto-#includes the two shared scripts regardless, loads the per-map one if present"
model exactly.
(b) Individual relay entities reference individual script functions via call, and
this is the part actually cross-checked, not just read off one example:
python re_probes/tnoscript/maplink.py
For every .entities file, every call = "X" field is matched (case-insensitively — the
DoomScript identifier rule, confirmed by call_freelook/call_freeLook differing only in
case in c01p1.entities/c01p1.script) against every top-level function defined in that
map's own script and every other .script in the same chapter folder (map scripts are
not self-contained — c06p1.entities also calls into c06p1_parking.script,
c06p1_vo.script, shaft.script, shaft_drones.script; c14p1.entities also calls into
c14p1_boss.script/c14p1_elevator.script — these sidecar scripts are a second, smaller
mechanism this track found and had not been named before):
TOTAL: 513/516 call="..." fields resolve to a same-map top-level function (case-insensitive)
The 3 unresolved are genuine retail defects, not a model error — checked individually:
c06p1: call="call_chase_to_ruble"— the function exists inc06p1_parking.scriptline 247, but is wrapped in/* ... */— commented out in shipped retail data. The relay entity still exists and still names it; at runtime thiscallcan only be a silent no-op (or, if the compiler still errors on an unresolvedcallstring at trigger time rather than compile time, a dead branch — not determined, no launch).c06p1: call="spawn_shaft_gp_drone_1"/"spawn_shaft_gp_drone_3"— no function of this name exists anywhere in the extracted 37 scripts. Genuinely dangling, same defect class as TNC Kiscule's 43 dead edges in 29 files (docs/kiscule.md§3.3) — a known, harmless shape of retail data, not unique to this track's method.
This 99.4% figure, with 3 named and individually explained exceptions, is the strongest available substitute for the game-only "does it actually run" test the rules otherwise require and explicitly permit here (the game cannot be launched by this track).
6. Unused classes: the RTTI cross-check (same method as kiscule.md §4, reused on purpose)
grep -oE '^id(Target_|Trigger_)[A-Za-z0-9_]*' docs/ghidra/klassen_tno.csv | sort -u # 197
grep -h -o 'class = "id[A-Za-z_]*"' out/entities/*.entities | sort -u # 112 of those 197 appear
197 distinct idTarget_*/idTrigger_* RTTI classes exist in the shipped EXE; 112 are
used across the 25 retail maps; 85 are not used anywhere in retail. Unlike TNC's 113
unused Kiscule node classes (which were mostly generic logic ops — LogicGateAnd,
CompareBool, …), a meaningful chunk of TNO's 85 look purpose-built and specific:
idTarget_Cvar, idTarget_Print, idTarget_UnlockMap, idTarget_UnlockSecret,
idTarget_SetHelp, idTarget_GameStateIntSet/_GameStateIntInc, idTarget_PlayAnim,
idTarget_HideShowMesh, idTarget_LevelTransition, idTarget_SoundDuck/_SoundSync,
plus a visible block of QA-only entities (idTarget_Test*, idTarget_TestScreenCompare,
idTarget_TestShotsFired) and coop/multiplayer leftovers (idTarget_CoopGiveScore,
idTarget_CoopObjectiveTracker, idTrigger_Multiplayer, idTrigger_GameChallenge — TNO
retail has no multiplayer, so these are dead weight from a shared engine build, not a cut
TNO feature). idTarget_Cvar is the standout for this project's stated goal ("engine
ausreizen"): a trigger-driven cvar setter would be a second, independent route around the
command-restriction gate r4_consolecmds.md found for TNC's different engine — whether
the class still spawns correctly when referenced from a hand-edited .entities file is
completely untested, zero evidence either way beyond "the RTTI entry and vtable exist."
Flagged as the single most promising next experiment, not claimed as working.
6a. idTarget_Cvar reachability — pushed as far as static evidence allows, still open
Two new pieces of evidence this pass, neither resolves it, both relevant:
idTarget_Cvar's RTTI entry has the same shape as every used class:idTarget_Cvar, idEntity;idEventReceiver;idClass,0x140C18C28,0x1410A6E28(vtable + type_info addresses both present,docs/ghidra/klassen_tno.csv). idTech4-lineage engines spawn entities by name lookup into a reflection table (idClass::CreateInstance/idTypeInfo) populated by static initializers at process start — not a hand-written per-class switch statement — so architecturally there is no obvious reason a fully-RTTI'd-but-unused class likeidTarget_Cvarwould be rejected by the spawn path differently from a used one. This is an architectural inference from the known idTech4 pattern, not a TNO-specific proof — the actual spawn dispatch is in the encrypted.textand was not read.- TNO has its own cheat-cvar restriction system, confirmed by strings near the
CVAR_CHEAT/CVAR_NOCHEAT/CVAR_ARCHIVEflag-name table (0xd7d0a8block) and a separate function's diagnostics:"...T be set in retail!"(truncated "CANNOT be set in retail!"),"Archived CVar '%s' in production mode","Resetting cheat cvar: %s"(0x01050400–0x01050480). This is a different, TNO-native gate, not TNC'sresourceExecmechanism (r4_consolecmds.md/r5_kisculecmd.mdare TNC-only, never re-checked for TNO, still true). Whether thesetcvar/executeCommandTextscriptEvents (§4.2) go through this same gate, a different one, or none, is not determined — the call site is encrypted.text. Named here because it's concrete, reproducible string evidence where pass 1 had none, not because it answers the question.
7. What is provably NOT authorable with the existing tools
New physical trigger volumes — no. Every idTrigger-derived entity with a shape
(not a pure relay) carries clipModelInfo.clipModelName, a reference to a separately
baked, binary cm decl (8 661 such entries across the game per docs/engine-surface.md,
listed there as flatly binary, unextracted/unwriteable). Adding a new named cm asset
hits the same wall moddingflaeche-tno.md §6.4 already proved for decls in general —
Installation.apply replaces existing (type,name) index entries, it cannot insert a new
one. This means a brand-new trigger volume with new geometry is not reachable. The only
moves available are: reusing an existing volume's clipModelName for different logic (same
shape, new behavior), or repositioning an existing volume via its own spawnPosition (not
measured here whether the baked cm shape follows spawnPosition or is already
world-baked — open, §9).
New top-level (type,name) assets of any kind — no, unchanged from the general finding
already on record; not re-derived here.
8. What IS authorable, and the recipe — all steps individually proven as possible, the combination untested end-to-end
- Edit an existing relay's logic without adding anything — change
delay,count,repeat, thetargetslist, or redirect an existingcallto a different, already defined function in the same chapter. Pureset-style value edits, same mechanism the project has already proven in-game for TNC decl edits (per memory:tnc-decl-edits.md, magazine 30) — the TNO analogue is unproven in-game but structurally identical (same decl-text grammar, same patch pipeline). - Add a brand-new relay + a brand-new script function, both inside the existing
.entities/.scripttext blobs for one map (growing an already-indexedfileasset's payload, not creating a new index entry — the distinction §7 turns on). Concretely: addentity { entityDef my_new_relay { inherit="target/relay"; class="idTarget_Count"; edit = { targets={num=1;item[0]="<existing door/sound/spawn target>";} call="my_new_func"; } } }to the map's.entities, andvoid my_new_func( entity activatedEntity, entity activator ) { <any scriptEvent from script_events.script> }to the matching.script. Both files already tolerate 0–428 bytes in place (moddingflaeche-tno.md§4.1) and a full chunk rebuild for anything larger — the rebuild path already exists (Installation.apply's rebuild branch, cited there, not re-derived here). - Wire it to an existing trigger volume by adding the new relay's name to that
volume's own
targetslist — no new collision geometry needed, §7. - For an objective marker, target an existing
gp_misc_automap_objective_*-style entity'sidTarget_Show/idTarget_Hidethe same way §4.3's table shows retail doing. - (new this pass) Attach a
scriptObjectblock to an entity for templated behavior (mover/rotator/trigger-prefab logic) instead of a bespoke relay+function pair — either reuse a shared template (scriptObject = { type = "moveSelf"; moveTime = "2"; };, §4.1a) or add a newobjectdefinition to the map's own.scriptand reference it by name the same way the 20 per-map templates (flameburstprefab,brakeblock,elevator, …) do.
None of steps 1–5 has been run against the game. This is the recipe this track's research converges on, not a verified result.
PROVEN (format/string level, this track, reproducible with the commands in §1)
- The entity-trigger graph and the script VM are two measured, distinct layers, both real engine systems (RTTI classes + compiler/VM strings), not an assumption from the question.
- 211 distinct
OP_*DoomScript bytecode mnemonics exist, in 3 byte-identical copies in.rdata(0xdb6db8,0xdf49e8,0x1030e80), includingOP_PUSH_ENT/OP_GOTOthatmoddingflaeche-tno.mdonly knew as unlocated examples; a further 92OP_-prefixed strings elsewhere belong to two unrelated systems (NFSv4 protocol ops, a shader/face-blend opcode set) and do not overlap the DoomScript vocabulary (§3.1, corrected this pass). scriptObject = { type = "X"; <params>; }is the entity↔script-objectbinding: 173 instances across 20/25 maps, 24 distinct template types, 4 of them (moveSelf=22,rotateSelf=36,bobSelf=6,randomActivator=3) the sharedscript_util.scripttemplates, the other 20 defined per-map in that map's own.script(§4.1a, new/corrected this pass — resolves what pass 1 called a genuinely open question).c16p0.entitiesis the one retail map with no matching.scriptfile at all, consistent with it also having zerocall="X"fields (§5, confirmed this pass).- The compiler is
idCompiler(not named in RTTI, but in its own diagnostic strings), compiles once per map at load, is fatal on error, auto-includesscript_events.script+script_defs.script, and its engine fork is internally named "tungsten" (leaked build path). - 913 native
scriptEventsignatures inscript/script_events.script, includingspawn,openDoor,startSoundShader,nextMap/devMap,doCheckpoint— directly answering the "spawn/door/sound/objective" part of the command-set question. - 116 distinct
idTarget_*/idTrigger_*classes used across the 25 retail maps; 197 exist in the RTTI; 85 unused, named individually (§6). - The
call="X"bridge from relay entity to script function: 513/516 (99.4 %) resolve to a real, same-chapter function, case-insensitively, with the 3 exceptions individually explained (1 commented-out retail function, 2 genuinely dangling — both harmless defect classes, not a method error). .scriptfiles load per map by a 1:1 filename convention with.entities, with a secondary same-folder sidecar-script mechanism (newly named by this track, not previously documented) needed to resolve 41 of the 516callfields.- New physical trigger volumes cannot be authored (binary
cmdependency + the existing "no new index entry" wall); new logic relays and new script functions can be added inside already-indexed files without that restriction.
INFERRED (plausible, not run)
- The full recipe in §8 (steps 1–5) works end-to-end in a launched game.
idTarget_Cvar(unused in retail, RTTI-present with the same vtable/type_info shape as used classes, §6a) could set cvars from a trigger, possibly bypassing TNO's own cheat-cvar/"production mode" gate (strings found this pass, §6a) the wayresourceExecbypasses TNC's — architecturally plausible given idTech4's reflection-based entity spawn, but the actual spawn dispatch and thesetcvar/executeCommandTextscriptEvent call sites are both in encrypted.textand were not read; still completely unverified.- A re-pointed
idTarget_Spawn(existing relay, different existinggenerated/....decltarget) would spawn a different already-shipped prefab. - A new
scriptObject-bound entity (newobjectadded to a map's own.script, referenced by a new entity'sscriptObject.type, §4.1a/§8.5) would run itsinit/onTriggercallbacks the same way the 173 retail instances do — not run in-game.
FAILED HYPOTHESES / dead ends (named, not hidden)
- Assumed TNC's Kiscule node-graph binary format might have a TNO-side analogue reachable through the same archive layer — no, confirmed structurally: there is no binary graph container in TNO at all; the trigger graph is plain decl text from the start. Not a bug, a different engine generation.
- Removed this pass (was wrong, see "Pass 1 errors" above and §4.1a): pass 1 claimed the
spawnarg
scriptobjectnever appears as an entity field. It does, as the structured blockscriptObject = { type = "X"; }, 173 times — the grep that produced "zero" was looking for the wrong shape (a bare key, not a nested block).superScriptObjects(a separate, unrelated music-modifier field) is still real and still distinct from this mechanism, that part of the pass-1 text was not wrong, only the "never appears" claim around it. - Tried to resolve the exact console-command name behind the auto-generated
script_operators.html/CVar-list HTML dump (§3.2) — the format strings only hold a%splaceholder for the command name; theAddCommandcall site is in the encrypted.text. Capped at this one string-level attempt per the project's "~3 identical failures" rule (this was the only attempt, not three — stopped because further attempts would need disassembly, which TNO's read-only string-level scope here does not have).
Open questions (§9)
- Does a repositioned
idTrigger's bakedcmshape follow a changedspawnPosition, or is the collision mesh already world-baked at build time? Decides whether "move an existing trigger volume" is a real option. (Still open, not pushed further this pass — needs the binarycmformat read, out of this track's string-level scope.) - ~~How is a
script_util.scriptobjectbound to a specific entity~~ — resolved this pass, see §4.1a (scriptObject.type). No longer open. - Whether
idTarget_Cvar/idTarget_Print(RTTI-present, zero retail instances) still spawn correctly if written into a hand-edited.entitiesfile — still the single most promising next experiment; pushed as far as static evidence allows this pass (§6a: same RTTI shape as used classes, a TNO-native cheat-cvar gate exists) without resolving it — needs a runtime image or an actual in-game test, neither available to this track. - The exact console command that dumps
script_operators.html(cosmetic, not blocking). - Whether
devMap/setcvar/executeCommandTextas scriptEvents reach the same restriction-level gate TNC's console does, TNO's own cheat-cvar/"production mode" gate found this pass (§6a), or neither — still not determined, the call sites are in encrypted.text; would need the same kind of workr4_consolecmds.mddid for TNC, from scratch, for TNO, or a runtime image TNO does not have.
Smallest test for the maintainer
(Translated from German.)
The safest first, with no new file length and no new trigger volume: in a
copy of c01p1.entities, change the entry call_boattest (delay = 0.0199999996;) to
delay = 5; (pure numeric value, no length change, rebuild probably not
needed) and load c01p1 — if the boat test fires noticeably later than in the unpatched
version, the idTarget_Count→call mechanism is confirmed live in the game, not only in the
text. The next, more meaningful step: in the same copy, append a new, own
idTarget_Count entry that points at a newly written function
void call_wolfsdk_test( entity activatedEntity, entity activator ) { sys.print("wolfsdk
r6 alive\n"); } in the corresponding c01p1.script, and attach it to an already existing,
early-reachable trigger (e.g. a door or sound trigger from chapter 1) as an
additional targets target — that needs a full chunk rebuild (the budget according to
moddingflaeche-tno.md is not enough), but it is the first real test of whether new
script functions and new relays are accepted by the game at all.
New this round (§4.1a): as an alternative to a new call function, a
scriptObject = { type =
"moveSelf"; moveTime = "2"; }; block can experimentally be appended to an already existing, unused entity
(a pure value addition, known field) — if
the entity then actually moves in the game, the second, previously unrecognised
authoring route (template objects instead of relay + function) is also confirmed live.
Related: Research Report: R4 Console Commands, Research Report: R5 Kiscule Command, Research Report: R7 Tno Script Vm, Ghidra Analysis, TNO Modding Surface, UI Assets, Kiscule Scripting, TNC Modding Surface, Engine Modding Surface, TNO Maps.