Repository navigation
Research Report R7 Tno Limits
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).
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.
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).
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.
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).
// 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)(); // FATALA 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 <= 32Unlike 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).
| 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.
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.
- TNO's
idGameLocal::RegisterEntityhas a managed-object array of 8192 (0x2000) entries, of which indices0–0x1ffd(8189) are the usable free-slot search space and0x1ffe/0x1fffare reserved sentinels (one of three decompiled copies assigns0x1ffeto the world entity directly, RVA0x865440); a separate lifetime spawn-id counter caps at 524,287 — both confirmed across 3 independent inlined call sites (RVAs0x8609db,0x860870,0x865440) plus a 4th independent confirmation of the0x2000-entry size from an unrelated function's sentinel constant (0x1fff, RVA0x6d9ef0) — both overflows are FATAL (swi(3)trap). -
idTarget_ShowAI's "Too many AIs active" check (RVA0x7b78a0) reads a per-entity field atparam_1+0x37c,-1= unlimited, compares to a live count, and is a soft warning, not fatal — independently confirmsr6_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) (RVA0xc8ae0) — the one cap family in this report that never crashes. -
idDeclEnv'srenderParmscap is 80 (0x50), FATAL (RVA0x113720). - 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) (RVA0x313810);MAX_DYNAMIC_ENVIRONMENT= 128 (soft, reject) (RVA0x3179b9). - 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 — reconfirmsr6_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.
- 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'sMAX_GENTITIESnumeric 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.jsonlive-registry walk exists to check this properly (see Open questions).
- Tried resolving
vtbl_idCVarSystemLocal/vtbl_idCmdSystemLocalby name throughdecomp.pythe same way TNC's curated function names work — "no function matches" for both.decomp.py's name resolver only walksFunctionManager.getFunctions(); RTTI vtable labels are data symbols in this snapshot (perD:/wolf-decomp/tno_import_log.txt: "labeled 2369 RTTI vtables", distinct from the function list), not reachable this way. Reusing TNC'sprobe_cvartable.py/cmdtable.mdapproach for TNO (walking the live cvar/command registry the way those tools read TNC'smodule.bindirectly, 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--stringspasses ("too many"/"projectil"and"visible light|Maximum allowed per frame|shadow caster"). Not a decompiler limitation — the messages are not present in TNO's.rdataat all. -
"Stack depth: %d bytes, %d max\n"/"Call depth: %d, %d max\n"(flagged by name only inr6_tnoscript.md§3.3) — Ghidra's xref scan: no references found for either, exactly the class of missr6_enginelimits.mdhit for most of theMAX_*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 endr6_enginelimits.mdalready catalogued for TNC.
- 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.
-
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. -
Script mods should stay well under 64 nested calls and 32 KiB of
global script memory. A
thread-heavy.scriptfile 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. -
A cloth-sim prop (flag/cape/curtain) must stay at
width*height ≤ 100particles 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. - 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.
-
An
envdecl (environment/skybox-style decl) is capped at 80renderParms— fatal if exceeded (§5); relevant only to anyone editingenvdecls directly, which is a narrow but real part of TNO's decl surface. -
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.
-
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
idCVarSystemLocalsingleton RVA and itsRegisterStaticVarsthe way TNC'scvartable.md/cmdtable.mdtracks 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--gzfalready 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.
# 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
(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):
-
AI population limit, harmless (§2): In an own test map/an own
.scriptspawn setup, set anidTarget_ShowAI(or the decl/entity field behind it, presumably analogous to TNC'sm_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 inqconsole.log, and the game keeps running normally — no crash, nothing breaks. -
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/Groupfield, 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 (int3trap, 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.
English Wiki generated from the measured research notes in docs/. Evidence grades and uncertainty are preserved intentionally.
- WolfSDK Loader · DE
- WolfSDK Studio · DE
- FAQ · DE
- Mod format · DE
- Modding Guide
- Modding Surface
- CVar Catalog
- CVar Validation (old scrape)
- Texture Database
- Developer Mode
- Dev Menu
- Custom-Map Status