Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Latest research roundsNeueste Forschungsrunden

TNO Engine Limits from Decompiled Code: Entities, AI, Script VM, Physics, Decls, Audio, Render

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 the fixed limits at which the game warns or crashes. It found that at most 8,189 entities can exist at the same time, that an AI group with more than 64 members crashes the game exactly as in Wolfenstein II, and that scripts crash beyond 64 nested calls or 32 KiB of global memory. Some limits only print a warning, like the per-spawner AI population cap, and none of the about 20 decompiled functions behind them reads a console setting. Everything is static code reading; the game was never launched.

  • Entities: 8,192 slots, of which 8,189 are usable at the same time, plus a lifetime spawn counter capped at 524,287; both overflows crash the game.
  • An AI 'Group' is capped at 64 members and crashes when exceeded, the same value and behaviour as in Wolfenstein II; the 'Too many AIs' cap is a per-spawner field (-1 = unlimited) and only warns.
  • Scripts crash at 65 or more nested calls or more than 32,768 bytes of global memory; a cloth sheet with more than 100 particles crashes; an env decl allows at most 80 renderParms.
  • Soft limits that warn instead of crashing: physics trace models (32 vertices, 32 edges, 16 polygons), 256 GUI surfaces, 128 dynamic environment probes, 2 sound listeners and 180 nodes in an AI path search.
  • Open: the script limits for functions and statements, the AI blackboard and weapon-stats capacities, and whether TNO has any setting-controlled limit at all; nothing was tested in game.

Dieser Bericht hat den entschlüsselten Programmcode von Wolfenstein: The New Order (TNO) gelesen, um die festen Grenzen zu finden, bei denen das Spiel warnt oder abstürzt. Ergebnis: Höchstens 8.189 Entities können gleichzeitig existieren, eine KI-Gruppe mit mehr als 64 Mitgliedern lässt das Spiel genau wie in Wolfenstein II abstürzen, und Skripte stürzen ab, wenn sie mehr als 64 Aufrufe tief verschachtelt sind oder mehr als 32 KiB globalen Speicher belegen. Manche Grenzen geben nur eine Warnung aus, etwa die KI-Obergrenze pro Spawner, und keine der rund 20 dekompilierten Funktionen dahinter liest eine Konsolen-Einstellung. Alles beruht auf statischem Codelesen; das Spiel wurde nie gestartet.

  • Entities: 8.192 Plätze, davon 8.189 gleichzeitig nutzbar, dazu ein Zähler für alle jemals erzeugten Entities mit Obergrenze 524.287; beide Überläufe lassen das Spiel abstürzen.
  • Eine KI-„Group“ ist auf 64 Mitglieder begrenzt und stürzt beim Überschreiten ab, gleicher Wert und gleiches Verhalten wie in Wolfenstein II; die Grenze „Too many AIs“ ist ein Feld pro Spawner (-1 = unbegrenzt) und warnt nur.
  • Skripte stürzen ab bei 65 oder mehr verschachtelten Aufrufen oder mehr als 32.768 Bytes globalem Speicher; ein Stoffobjekt mit mehr als 100 Partikeln stürzt ab; eine env-Decl erlaubt höchstens 80 renderParms.
  • Weiche Grenzen, die warnen statt abzustürzen: Physik-Kollisionsmodelle (32 Eckpunkte, 32 Kanten, 16 Polygone), 256 GUI-Flächen, 128 dynamische Umgebungssonden, 2 Sound-Zuhörer und 180 Knoten bei einer KI-Wegsuche.
  • Offen: die Skript-Grenzen für Funktionen und Anweisungen, die Kapazitäten von KI-Blackboard und Waffenstatistik und ob TNO überhaupt eine per Einstellung steuerbare Grenze hat; im Spiel wurde nichts getestet.

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 on The New Order's hard engine limits, read from decompiled code — the TNO counterpart of Engine limits (R6). It finds a proven entity-slot ceiling with a separate spawn counter, a fatal AI "Group" member cap identical to TNC's, a fatal script call-stack depth, and hard and soft physics caps; the AI population limit is a per-spawner field, not a global constant, as in TNC. Static evidence only; the game was never launched.

Track tnolimits. Read-only on both game installs and on every project file outside re_probes/tnolimits/ (unused — no scratch file was needed) and this report. The game was never launched, no process was attached. This track's sole new capability is tools/decomp.py --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf, querying re_probes/runtime_tno_186352/module.bin (a read-only memory snapshot of TNO, PID 186352, base 0x7FF74A390000, captured earlier and already analyzed into D:/wolf-decomp/ghidra/tno_runtime.gzf — see D:/wolf-decomp/tno_import_log.txt: 50,884 functions from .pdata, 2,369 RTTI vtables labeled, no curated/command names yet, unlike TNC's tnc_runtime.gzf). Every RVA below is a file offset into that module.bin (rva = address - 0x7FF74A390000), computed and cross-checked with Python, never by hand. Builds directly on r6_enginelimits.md (same question, TNC side; TNO side there was string-only, no runtime image existed yet) and does not redo r6_tnoconsole.md/docs/limits.md (retail cvar lock, cheat-reset mechanism — a different kind of limit).

Question

TNO's hard engine limits from code: entity/AI/light/physics/decl/string/ memory caps (MAX_* constants, asserts, "overflow"/"exceeded" errors); which are cvars and which are compiled; what happens at the limit (warn vs fatal); compare with TNC's r6_enginelimits.md.

Answer (short)

Headline: TNO's decompiled runtime resolves one of TNC's own standing open questions, in TNO's own code. r6_enginelimits.md §2.1 could not find any caller of TNC's entity-registration overflow branch (Ghidra's static xref scan found zero callers for the fatal path itself) and explicitly recorded the real MAX_GENTITIES-equivalent numeric value as unrecoverable. TNO's decompiled idGameLocal::RegisterEntity (3 independent inlined copies, RVAs 0x8609db/0x860870/0x865440, §1) does have found callers and a directly-readable literal: a managed-object pointer array of 8192 (0x2000) slots, of which indices above 0x1ffd (8189) are refused ("no free entities", fatal), with 0x1ffe/0x1fff used as reserved sentinel ids (the world entity is explicitly assigned 0x1ffe in one of the three copies) — and a separate monotonic "ever spawned" counter capped at 0x7ffff (524,287), whose overflow is the actual "idGameLocal::RegisterEntity: spawn count overflow" message both games share verbatim. This is TNO's own number, not proof TNC's is identical (r6_enginelimits.md itself cautions against that assumption) — but it is the first PROVEN, not just named-but-unreachable, entity-slot ceiling found in this project for either game.

Second confirmation: the "Group has too many members" cap is identical in both games, and now proven fatal in both. r6_enginelimits.md §3.2 found the format string byte-identical in TNO's static .rdata but could not read .text to confirm the number. Decompiled TNO (RVA 0x57cecf): literal 0x40 = 64, followed immediately by swi(3) (an int3 trap — an unconditional crash, the same "print then trap" idiom as every other fatal cap found this round) — exactly TNC's value and TNC's fatal behaviour, now independently proven on both sides rather than inferred from a shared string.

Everything else found this round is new ground r6 did not cover (TNC side not reproduced here either): the TNO script VM has a hard 64-deep call stack (RVA 0x96e7d0, fatal) and a hard 32 KiB global-variable memory pool (RVA 0x9791e0, fatal); physics has a hard 100-particle cloth cap (RVA 0x29d480, fatal) and a soft 32-vertex/32-edge/16-polygon cap on trace-model-from-submodel generation (RVA 0xc8ae0, warn-and-reject, not fatal); an idDeclEnv (environment decl) is capped at 80 renderParms (RVA 0x113720, fatal); the render layer soft-caps 256 GUI surfaces (RVA 0x313810) and 128 dynamic-environment probes (RVA 0x3179b9), both reject-and-continue, not fatal; audio soft-caps 2 sound listeners (RVA 0x4a3260); AI pathfinding's Dijkstra search soft-caps at 180 visited nodes (RVA 0x7902e0) with a verbose diagnostic dump instead of a crash. Three subsystems (AI blackboard variable pools, script max-functions/max-statements, the "too many weapon stats"/"too many touched triggers" registries) have a capacity held in a data field, not a literal in the instruction stream — same class of dead end r6_enginelimits.md hit for ~440 MAX_* names; confirmed, not resolved, this round.

Which are cvars and which are compiled, for TNO specifically: every single cap found this round is compiled or data-driven-but-not-cvar-backed — not one of the ~20 decompiled functions below reads a cvar before comparing against its limit. This is a materially different picture from TNC's §4 (131 cvars with engine-clamped min/max, several of them exactly the kind of streaming/world-geometry/physics pool a modder would want to raise) — but it is not proof TNO has no such cvars anywhere; this round inspected the ~20 functions behind the error strings found, not TNO's full cvar registry (TNO has no cvars_tnc_v2.json-equivalent catalogue — see open questions). The one direct attempt to reuse TNC's approach (finding idCVarSystemLocal's singleton the way tools/probe_cvartable.py does for TNC) failed outright this round (see Failed hypotheses) — the TNC-side cvar/command registry array-size ceiling (§1 of r6_enginelimits.md: 256/64 commands, 4096/512 cvars) and the cvar min/max table (§4) have no TNO counterpart in this project yet.

Negative evidence against two of TNC's own cross-game guesses: TNC's hard-coded 16-projectile multi-fire clamp message ("Tried to spawn too many projectiles") does not exist anywhere in TNO's string table (full "too many"/"projectil" scans, zero hits) — not an unconfirmed number this time, a confirmed absent feature/message. TNC's unnamed light-count caps (GatherShadowCasters's "Maximum allowed per frame", "Too many visible lights") also do not exist in TNO's string table — r6_enginelimits.md's own open question about those two functions' literal values cannot be settled from the TNO side; they appear to be TNC-only code (later engine revision only, or renamed beyond a substring match).

1. Entities: the slot array, the spawn-id counter, and what each overflow does

Three independent, differently-inlined copies of idGameLocal::RegisterEntity all reference "idGameLocal::RegisterEntity: spawn count overflow" (RVA 0xffeb60) and all agree on the same two literals:

// FUN_7ff74abf09db, rva 0x8609db (one of three inlined copies)
if (0x7ffff < *(int *)(unaff_RBX + 0x170598)) {       // monotonic spawn-id counter
    FUN_7ff74ad5ab70(s_idGameLocal__RegisterEntity__spa_7ff74b38eb60);
    pcVar2 = (code *)swi(3); (*pcVar2)();             // int3 trap -- FATAL
    return;
}
if (unaff_ESI < 0) {                                   // caller passed "pick a free slot"
    lVar1 = *(longlong *)(unaff_RBX + 0x161d0 + (longlong)*(int*)(unaff_RBX+0x2e1d0)*8);
    while (lVar1 != 0 && (*(int*)(unaff_RBX + 0x2e1d0) < 0x1ffe)) {  // linear free-slot search, capped
        ... *(int*)(unaff_RBX + 0x2e1d0) = iVar3; ...
    }
    unaff_ESI = *(int *)(unaff_RBX + 0x2e1d0);
    if (0x1ffd < unaff_ESI) {                          // 8189 -- no slot left
        FUN_7ff74ad5ab70(s_no_free_entities_7ff74b38eb98);
        pcVar2 = (code *)swi(3); (*pcVar2)();          // FATAL
        return;
    }
    ...
}

A third copy (RVA 0x865440, the largest/least-inlined of the three — grep for the same two constants inside it) additionally contains *(undefined4 *)(lVar15 + 0x218) = 0x1ffe; — assigning the world entity its id directly as the top-of-range sentinel 0x1ffe (8190), consistent with the classic id Tech ENTITYNUM_WORLD/ENTITYNUM_NONE convention of reserving the last few ids below the array size. The int array parallel to the pointer array runs 0x261d0–0x2e1d0 (exactly 0x8000 bytes = 8192 * 4), independently confirming the pointer array's declared capacity is 8192 (0x2000) entries, of which indices 0–0x1ffd (8189) are the actual free-list search space and 0x1ffe/0x1fff are reserved. Separately, FUN_7ff74aa69ef0 ("touched triggers", §5) uses literal 0x1fff (8191) as its own "no entity" sentinel default — a fourth, independent confirmation of the same 0x2000-entry array size.

The "spawn count" field (+0x170598) is a different counter from the slot-array index — it only increments, it is not decremented on entity removal (unlike the free-slot index, which is explicitly reused), and its own cap (0x7ffff = 524,287) is roughly 64× the slot array size. This explains the message's own wording ("spawn count", not "entity count"): it is almost certainly a lifetime spawn-id generator (every entity ever created in a level, including ones since freed, gets a unique id up to ~524k), separate from "how many entities can be alive at once" (~8189). Modding relevance: a level that spawns and frees an unusually large number of entities over its lifetime (not simultaneously alive) is the one that could theoretically hit the 0x7ffff ceiling; a level with too many simultaneously alive entities hits the 0x1ffd ceiling instead — two different failure modes, two different literal numbers, same source string family.

2. AI population cap: per-spawner field, not a global constant — confirmed identical in TNO

FUN_7ff74ab478a0 (name resolved directly by decomp.py, RVA 0x7b78a0) is the function behind all three "idTarget_ShowAI ... Too many AIs active!" strings (TNO's name for TNC's idTarget_AISpawn, same message family, r6_enginelimits.md §2 already flagged the naming parallel but had no TNO code to check it against):

iVar2 = func_0x7ff74abe4880(DAT_7ff74c1360e0);      // live active-AI count
iVar15 = *(int *)(param_1 + 0x37c);                 // per-entity cap field (-1 = unlimited)
bVar17 = iVar15 < 0;
if ((bVar17) || (iVar2 < iVar15)) {
    ... /* cap not exceeded, or unlimited: normal spawn logic follows */
}
else {
    uVar6 = func_0x7ff74abcdac0(param_1 + 0x18);
    FUN_7ff74ad5a7b0(s_idTarget_ShowAI___entity___s__To_7ff74b343e30, uVar6, iVar2,
                     *(undefined4 *)(param_1 + 0x37c));   // warn, NOT fatal
    ...
}

FUN_7ff74ad5a7b0 is a thin wrapper around FUN_7ff74ad5a270(3, fmt, args) — a leveled print/log call, not a no-return trap (unlike every fatal case in this report, which goes through a different function, FUN_7ff74ad5ab70, immediately followed by swi(3)). Execution continues normally after every "Too many AIs" call site in this function (three call sites inside one function body, all reached via break/fallthrough, never return into a crash). This independently confirms, in TNO's own disassembly, exactly what r6_enginelimits.md §2 proved for TNC: the cap is a per-entity, map-author-set field (TNO reads it at a fixed struct offset +0x37c directly, rather than through TNC's generic decl-field-by-name-hash accessor — a build/inlining difference, not a semantic one), compared against a live counter, with -1 meaning unlimited, and exceeding it is a logged warning, not a crash. No global "max AI in world" constant exists in this function, same as TNC.

3. The two literals extracted with full confidence: Group cap (64, fatal) and script call-stack depth (64, fatal)

// FUN_7ff74a90cecf, rva 0x57cecf -- "Group '%s' has too many members"
void FUN_7ff74a90cecf(void) {
  undefined8 uVar2 = func_0x7ff74abcdac0();
  FUN_7ff74ad5ab70(s_Group___s__has_too_many_members__7ff74b2c27d0, uVar2, 0x40);  // 0x40 = 64
  pcVar1 = (code *)swi(3); (*pcVar1)();                                            // FATAL
}

r6_enginelimits.md §3.2 found TNC's own value for this same cap (0x40, also fatal, also via the "print then no-return call" idiom) and the byte-identical TNO string, but explicitly could not confirm the TNO number. It is confirmed now: 64, fatal, in both games.

// FUN_7ff74acfe7d0, rva 0x96e7d0 -- idThread/idProgram-style VM call-stack push
if (0x3f < *(int *)(param_1 + 0x318)) {                 // 0x3f = 63 -> cap is 64 deep
    FUN_7ff74acfe320(param_1, s_call_stack_overflow_7ff74b3c5bc8);
    pcVar3 = (code *)swi(3); (*pcVar3)();               // FATAL
    return;
}

This is the literal behind "call stack overflow" (RVA 0x1035bc8, one of the "locals stack overflow"/"EnterFuncton: locals stack overflow" family r6_tnoscript.md §3.3 already found by name but had not decompiled — that report also surfaced "Stack depth: %d bytes, %d max\n"/"Call depth: %d, %d max\n", which Ghidra's xref scan shows "no references found" for (pure debug-dump strings, not the check itself) — the actual bound was recovered by decompiling the function behind the sibling, found-reference error string instead, the same successful pattern r6_enginelimits.md §3 used for TNC. A script that nests 65 or more function calls (thread calls, event handlers calling into other event handlers, etc.) crashes TNO outright.

// FUN_7ff74ad091e0, rva 0x9791e0 -- script global/AST memory accounting
*(int *)(lVar15 + 0xfc) = *(int *)(lVar15 + 0xfc) + iVar14;
if (0x8000 < *(int *)(lVar15 + 0xfc)) {                 // 0x8000 = 32768 bytes
    uVar10 = FUN_7ff74a395990(auStack_4028, s_Exceeded_global_memory_size___d_b_7ff74b3c6938, 0x8000);
    ...
    pcVar2 = (code *)swi(3); (*pcVar2)();               // FATAL
    return;
}

TNO's script compiler hard-caps a level's (or shared .script's) global variable/constant memory at exactly 32 KiB, fatal on overflow. The two sibling caps in the same compiler — "Exceeded maximum allowed number of functions (%d)" (RVA 0x97ab10) and "...statements (%d)" (RVA 0x979a60) — are real, fatal (swi(3), same idiom), and found (unlike most of r6's MAX_* family), but their actual capacity is read from a struct field (param_1+0x10120/+0x10124 and +0x2c138/+0x2c13c respectively) rather than a bare immediate — the literal that initializes that field was not traced this round (see Open questions).

4. Physics: one hard fatal cap, one soft warn-and-reject cap

// FUN_7ff74a62d480, rva 0x29d480 -- cloth-sheet constructor
iVar4 = param_2 * param_3;             // width * height
if (iVar4 < 0x65) {                    // must stay under 101 particles
    ... /* build the cloth system */
    return param_1;
}
FUN_7ff74ad5ab70(s_Cloth_system___s__has_too_many_p_7ff74b226020, ..., iVar4, 100);
pcVar6 = (code *)swi(3); (*pcVar1)();  // FATAL

A cloth sheet (width × height particle grid) over 100 particles crashes TNO (message literally states "Decrease ... to be less than %d" with 100 as the argument). Any mod authoring a new cloth-sim prop (flag, curtain, cape) must keep width*height ≤ 100.

// FUN_7ff74a458ae0, rva 0xc8ae0 -- TrmFromSubModel (build a physics trace model from a render submodel)
if (0x20 < *(int *)(param_2 + 0x54))      // > 32 verts: WARN, continues (non-fatal)
    FUN_7ff74ad5a710(...vertices(%d) (max %d)..., 0x20);
if (0x20 < *(int *)(param_2 + 0x4c))      // > 32 edges: WARN, continues (non-fatal)
    FUN_7ff74ad5a710(...edges(%d) (max %d)..., 0x20);
if (0x10 < *(int *)(param_2 + 0x3c)) {    // > 16 polygons: WARN *and* reject
    FUN_7ff74ad5a710(...polygons(%d) (max %d)..., 0x10);
    FUN_7ff74a458530(param_1); return 0;  // abort, NOT a crash
}
if (iVar4 < 0x21 && iVar3 < 0x21) { ... }  // only proceeds if verts AND edges both <= 32

Unlike every other cap in this report, overflowing this one never crashes: the vertex/edge checks only warn (and the function still bails out a few lines later via the same <= 0x20 gate), and the polygon check explicitly warns-then-returns-zero. A destructible/physics-trace submodel is limited to 32 vertices, 32 edges and 16 polygons; exceeding any of them silently produces no trace model (the object becomes non-collidable via this path) rather than crashing the game — a materially different failure mode from TNC's one disassembled physics-adjacent bound (r6_enginelimits.md §3.3's [-16,16] cuttable-model cube, also non-fatal but a spatial bound, not a count).

5. Decl, audio, AI-nav and render soft/hard caps found incidentally

subsystem cap value enforcement RVA source function
idDeclEnv (environment decl) renderParms hard 80 (0x50) FATAL (swi(3)) 0x113720 idDeclEnv::Parse-equivalent
sound-world listeners hard 2 soft (warns, resets to 0) 0x4a3260 sound-world AddListener-equiv
AAS/nav Search_Dijkstra visited-node limit hard 180 (0xb4) soft (verbose diagnostic dump, falls back, no crash) 0x7902e0 Search_Dijkstra-equiv
GUI surface stack (MAX_GUI_SURFACES) hard 256 (0x100) soft (warn, silently drops the new surface) 0x313810 idRenderModelGui::AdvanceSurf-equiv
dynamic environment probes (MAX_DYNAMIC_ENVIRONMENT) hard 128 (0x80) soft (warn, caller gets -1) 0x3179b9 dynamic-environment-probe allocator
approximated light-model buffer data field, not literal — soft, rate-limited (only every 500th overflow is logged) 0x475bfc light-model gather
AI blackboard int/float/string variable pools (3 parallel) data field, not literal — soft (returns null, caller must handle) 0x54b700/0x54b7c0/0x54b880 AI blackboard Set*-equivalents
weapon-stats registry dynamically resized (idList-shaped) — (capacity is a runtime-configured field, grows) soft warn only if that external capacity is hit 0x8e79d0 weapon-stats add
touched-triggers-per-frame registry dynamically resized — soft warn 0x6d9ef0 trigger-touch add

The last two generalize r6_enginelimits.md §5's TNC finding (decl/resource counts are idList-shaped, not a small fixed idArray) to two more TNO subsystems: neither has a small compiled ceiling, both only warn if an externally supplied capacity (set elsewhere, not found this round) is reached.

6. The shared MAX_* RTTI-name family: same dead end in TNO as in TNC

python tools/decomp.py --strings "MAX_GENTITIES|MAX_RENDER_LIGHTS|ENTITYNUM_MAX_NORMAL" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
  MAX_RENDER_LIGHTS       (no references found)
  MAX_GENTITIES           (no references found)
  ENTITYNUM_MAX_NORMAL    (no references found)

Exactly as r6_enginelimits.md §6 found for TNC: these are RTTI/reflection debug-name literals, not strings fed to a comparison+warn/fatal call site, so Ghidra's static xref scan finds no referencing instruction — the pattern that did work everywhere else in this report (find the error-message sibling string instead, which — unlike the bare MAX_* name — sits next to the actual literal at its one call site) does not apply to this family in either game. Confirms, does not newly resolve, r6_enginelimits.md's own conclusion that the ~440 MAX_* names are structural metadata, not runtime-checked bounds with a logged path.

PROVEN

  • TNO's idGameLocal::RegisterEntity has a managed-object array of 8192 (0x2000) entries, of which indices 0–0x1ffd (8189) are the usable free-slot search space and 0x1ffe/0x1fff are reserved sentinels (one of three decompiled copies assigns 0x1ffe to the world entity directly, RVA 0x865440); a separate lifetime spawn-id counter caps at 524,287 — both confirmed across 3 independent inlined call sites (RVAs 0x8609db, 0x860870, 0x865440) plus a 4th independent confirmation of the 0x2000-entry size from an unrelated function's sentinel constant (0x1fff, RVA 0x6d9ef0) — both overflows are FATAL (swi(3) trap).
  • idTarget_ShowAI's "Too many AIs active" check (RVA 0x7b78a0) reads a per-entity field at param_1+0x37c, -1 = unlimited, compares to a live count, and is a soft warning, not fatal — independently confirms r6_enginelimits.md §2's TNC finding, in TNO's own disassembly this time.
  • The "Group has too many members" cap is 64 (0x40), FATAL in TNO (RVA 0x57cecf) — matches TNC's value exactly (r6_enginelimits.md §3.2), resolving that report's own "could not confirm for TNO" caveat.
  • TNO's script VM call-stack depth cap is 64 (0x3f+1), FATAL (RVA 0x96e7d0).
  • TNO's script global/constant memory pool cap is 32,768 bytes (0x8000), FATAL (RVA 0x9791e0).
  • TNO's cloth-sheet particle-count cap is 100 (0x64), FATAL (RVA 0x29d480).
  • TrmFromSubModel's vertex/edge cap is 32 each (soft warn, continues) and its polygon cap is 16 (soft warn + reject, not fatal) (RVA 0xc8ae0) — the one cap family in this report that never crashes.
  • idDeclEnv's renderParms cap is 80 (0x50), FATAL (RVA 0x113720).
  • Sound-world listener cap is 2 (soft, resets to 0) (RVA 0x4a3260).
  • AAS/nav Dijkstra search node-visit cap is 180 (0xb4, soft, diagnostic dump + fallback) (RVA 0x7902e0).
  • MAX_GUI_SURFACES = 256 (soft, drop) (RVA 0x313810); MAX_DYNAMIC_ENVIRONMENT = 128 (soft, reject) (RVA 0x3179b9).
  • The MAX_*/RTTI-name family (MAX_GENTITIES, MAX_RENDER_LIGHTS, ENTITYNUM_MAX_NORMAL) shows zero referencing instructions in TNO's decompiled runtime, same as TNC — reconfirms r6_enginelimits.md §6's conclusion with TNO's own decompiler this time, not string-matching alone.
  • TNC's 16-projectile multi-fire clamp message and its light-count-cap messages (GatherShadowCasters, "Too many visible lights") are absent from TNO's entire string table (zero matches on full-text scans) — a confirmed feature/message absence, not an unconfirmed number.

INFERRED (plausible, not nailed down this round)

  • That the script compiler's max-functions/max-statements caps (RVAs 0x97ab10/0x979a60) have some specific literal value — the check and its fatal enforcement are proven, but the capacity is read from a struct field whose own initializer was not traced back to a constant this round (same class of gap as TNC's MAX_GENTITIES numeric value before this track, just not resolved here either).
  • That the approximated-light-model buffer and the three AI-blackboard variable pools have a genuinely fixed capacity at all, as opposed to one computed at level-load from decl/config data — the enforcement (soft reject) is proven, the capacity's origin is not traced.
  • That TNO's apparent total absence of cvar-gated caps in the ~20 functions inspected generalizes to "TNO has no cvar-controlled engine limits at all" — more likely this round simply didn't sample any cvar-backed subsystem; no TNO equivalent of TNC's cvars_tnc_v2.json live-registry walk exists to check this properly (see Open questions).

Failed hypotheses / dead ends

  • Tried resolving vtbl_idCVarSystemLocal/vtbl_idCmdSystemLocal by name through decomp.py the same way TNC's curated function names work — "no function matches" for both. decomp.py's name resolver only walks FunctionManager.getFunctions(); RTTI vtable labels are data symbols in this snapshot (per D:/wolf-decomp/tno_import_log.txt: "labeled 2369 RTTI vtables", distinct from the function list), not reachable this way. Reusing TNC's probe_cvartable.py/cmdtable.md approach for TNO (walking the live cvar/command registry the way those tools read TNC's module.bin directly, entirely read-only, no process attach) would need the TNO singleton's RVA found by hand first — not attempted further this round given the time budget; a real next step, not a dead end in principle.
  • Searched TNO's full string table for TNC's "Tried to spawn too many projectiles" family and for "Too many visible lights"/"Maximum allowed per frame" — zero matches for either, confirmed by two independent --strings passes ("too many"/"projectil" and "visible light|Maximum allowed per frame|shadow caster"). Not a decompiler limitation — the messages are not present in TNO's .rdata at all.
  • "Stack depth: %d bytes, %d max\n"/"Call depth: %d, %d max\n" (flagged by name only in r6_tnoscript.md §3.3) — Ghidra's xref scan: no references found for either, exactly the class of miss r6_enginelimits.md hit for most of the MAX_* family. Worked around, not solved: found the actual bound via the sibling "call stack overflow" string instead (§3), which did have a found reference.
  • idMemLocal::Alloc: exceeded MAX_TRACKED_POINTERS, idWinding::Split: points exceeded estimate, idFile_Memory::TruncateData: ... exceeded allocated size — all no references found; not pursued further, same indirect/optimized-call-site class of dead end r6_enginelimits.md already catalogued for TNC.

Recipe: what this means for modding TNO, today

  1. Don't build an encounter "Group" (shared AI roster/TargetList) bigger than 64 members — confirmed fatal in TNO now, not just inferred from a shared string. Split a larger set-piece into two or more groups instead.
  2. AI population caps (maxActiveAIs-equivalent, idTarget_ShowAI) are per-spawner data, exactly like TNC — a custom map wanting unlimited AI just sets the field to -1 (or omits it); this is a data change, not a cvar, not a patch, and failing to do so only prints a warning, never crashes.
  3. Script mods should stay well under 64 nested calls and 32 KiB of global script memory. A thread-heavy .script file that calls deep chains of helper threads/functions, or declares a very large number of global constants/arrays, risks the two fatal VM caps in §3 — both are hard crashes with no warning beforehand.
  4. A cloth-sim prop (flag/cape/curtain) must stay at width*height ≤ 100 particles or the game crashes outright on spawn (§4) — this is a genuinely new, previously-undocumented hard limit for anyone adding cloth to a custom prop.
  5. A destructible/physics-trace submodel should stay at ≤32 vertices, ≤32 edges, ≤16 polygons — the only cap family in this report that fails soft (object just becomes non-collidable) instead of crashing, so this one is forgiving to get wrong during iteration, unlike every other cap here.
  6. An env decl (environment/skybox-style decl) is capped at 80 renderParms — fatal if exceeded (§5); relevant only to anyone editing env decls directly, which is a narrow but real part of TNO's decl surface.
  7. Entity population: a single TNO level is extremely unlikely to hit either entity ceiling found in §1 (~8189 simultaneously alive, ~524k lifetime spawns) through normal level design — recorded for completeness and because it resolves r6_enginelimits.md's own open question, not because it is a practical modding constraint today.

Open questions

  • TNO's cvar/command registry array sizes (TNC's own §1: 256/64 commands, 4096/512 cvars) — no TNO counterpart was attempted this round beyond the one failed name-lookup (see Failed hypotheses). Finding TNO's idCVarSystemLocal singleton RVA and its RegisterStaticVars the way TNC's cvartable.md/cmdtable.md tracks did (a dedicated disassembly pass, not a quick side quest) is the natural next step and would also make a live cvar min/max catalogue possible for TNO for the first time.
  • The literal values behind the script compiler's max-functions/ max-statements caps (§3) — the fatal check is proven, the capacity field's own origin was not traced to a constant.
  • Whether TNO has any cvar-gated engine limit at all — this round sampled ~20 cap-enforcing functions and found none, but that is not an exhaustive search of TNO's cvar surface (no live-registry walk exists for TNO yet, see above).
  • The AI blackboard pools, the light-model-approximation buffer, and the weapon-stats/touched-triggers registries' actual capacities — all confirmed data-driven, none traced to their initializing constant/decl field this round.
  • Whether TNC has equivalents of the new TNO-only caps found here (script call-stack depth, script global memory, cloth particles, env decl renderParms, GUI surfaces, dynamic environment probes, Dijkstra node limit, sound listeners) — not cross-checked against TNC's own runtime this round; tools/decomp.py's default --gzf already points at TNC's snapshot, so this is a same-method, same-tool follow-up for whoever picks it up next, not a new capability gap.

Reproduction

# All queries below ran against the TNO runtime snapshot, ~15-90s each:
python tools/decomp.py --strings "overflow" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
python tools/decomp.py --strings "exceeded" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
python tools/decomp.py --strings "too many" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
python tools/decomp.py --strings "projectil" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
python tools/decomp.py --strings "visible light|Maximum allowed per frame|shadow caster" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
python tools/decomp.py --strings "MAX_GENTITIES|MAX_RENDER_LIGHTS|ENTITYNUM_MAX_NORMAL" --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf

# Entity registration (3 inlined copies):
python tools/decomp.py 0x8609db 0x860870 0x865440 --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
# AI population per-spawner cap:
python tools/decomp.py 0x7b78a0 --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
# "Group has too many members" (64, fatal):
python tools/decomp.py 0x57cecf --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
# Script VM call-stack depth (64, fatal) + global memory (32KiB, fatal):
python tools/decomp.py 0x96e7d0 0x9791e0 --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
# Physics: cloth cap (100, fatal) + TrmFromSubModel (32/32/16, soft):
python tools/decomp.py 0x29d480 0xc8ae0 --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf
# Decl / audio / AI-nav / render soft+hard caps:
python tools/decomp.py 0x113720 0x4a3260 0x7902e0 0x313810 0x3179b9 --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf

# Negative control (vtable name lookup fails -- data symbol, not a function):
python tools/decomp.py vtbl_idCVarSystemLocal vtbl_idCmdSystemLocal --gzf D:/wolf-decomp/ghidra/tno_runtime.gzf

Smallest in-game test

(Translated from German.)

Everything in this report is a compiled limit, not a cvar — so there is no console command to try out, only a scenario that actually reaches the limit. By far the safest test (no crash to expect, only a log line):

  1. AI population limit, harmless (§2): In an own test map/an own .script spawn setup, set an idTarget_ShowAI (or the decl/entity field behind it, presumably analogous to TNC's m_maxActiveAIs — exact field name in TNO's own .entities/decls still to be verified) to a small value (e.g. 2) and let more AIs spawn at the same time. Expectation according to this report: the surplus AIs are not spawned, a line "Too many AIs active! Number of AIs active: ..., limit set to: ..." appears in qconsole.log, and the game keeps running normally — no crash, nothing breaks.

  2. Riskier, but unambiguous proof of a hard crash (§3): If a deliberate crash test is wanted (e.g. to confirm that the 64 "Group" cap really is fatal): extend an own AI encounter group (shared TargetList/Group field, as the retail maps use for mass encounters) to 65 or more members and load the scene. Expectation according to this report: the game crashes immediately on reaching the 65th member (int3 trap, no graceful exit) — save beforehand, this test is deliberately destructive and serves only as a control, not for normal modding.

Related: Research Report: R6 Engine Limits, Research Report: R6 Tno Console, Research Report: R6 Tno Script, Research Report: Cmdtable, Research Report: Cvartable, Modding Limits.