Round-6 research pass on the hard limits of both engines that matter to modders — entities, AI, lights, map size, streaming pools, decl counts, physics bodies and string lengths — on which of them are raisable cvars and which are compiled constants, and on what happens at the limit (warning, cull or fatal error). For TNC (decompiled), the command and cvar registries themselves are fixed-size arrays, a few literal caps were pulled out of disassembly, and most named MAX_* constants exist only as names whose values could not be extracted. TNO had no runtime image at the time, so its side is a string scan only; the decompiled TNO follow-up is TNO engine limits (R7). Static evidence only; neither game was launched. The verifier's corrections (pass-1 errors) and the PROVEN/INFERRED split are kept as written.
Track enginelimits. Read-only on both game installs and on every project file
outside re_probes/enginelimits/ and this report. Neither game was launched,
no process attached. Builds on and does not redo: r4_consolecmds.md/
cmdtable.md/cvartable.md (the cvar/command restriction-level gate and its
two hard-capacity arrays, reused directly in §1), r4_aispawnwaves.md
(maxActiveAIs is per-spawner data, not a global AI cap — reused in §2),
r5_kisculecmd.md (the resourceExec -s / Kiscule-Command route non-exposed
cvars need), docs/limits.md (TNO's retail console lock and archive-offset
invariants — a different limit, not duplicated here), docs/engine-surface.md
(TNC asset-type counts, reused in §5). Tools: python tools/decomp.py
<rva|name> [--xrefs] [--callers] [--strings PATTERN] (TNC only, Ghidra over
re_probes/runtime_87400/module.bin) and a new, small read-only scanner,
re_probes/enginelimits/tno_strings.py <regex>, written this track because
TNO has no runtime image and no decompiled code (.text is SteamStub-
encrypted, confirmed again in docs/ghidra.md) — it is a plain ASCII-string
scan over WolfNewOrder_x64.exe's .rdata/.data, the only static-analysis
option available for TNO. All TNC addresses below are RVAs into
re_probes/runtime_87400/module.bin (base 0x7FF62C210000); all TNO
addresses are RVAs into WolfNewOrder_x64.exe (base 0x140000000).
Pass 1 errors (verifier round)
A verifier independently re-checked every claim in the first pass and found 5 concrete errors, all confirmed by re-running the exact same read-only tools this round. Fixed in place below; marked here for the record:
- False PROVEN claim:
idAAS2File::MAX_AREASwas claimed to exist "verbatim in both TNC and TNO". Re-randecomp.py --strings idAAS2File,--strings AAS2,--strings MAX_AREAS— zero hits in TNC, all three. The symbol exists only in TNO (RVA0xd95778). §6 table and the PROVEN list corrected below. - False "TNO only" claim: §6 claimed
MAX_RENDER_LIGHTS/MAX_RENDER_MODELSwere "not found in TNC's--strings MAX_sweep". Re-randecomp.py --strings MAX_RENDER— both exist in TNC (RVA0x279f100/0x279f0e8). The "may use different naming post-refactor" speculation built on this false premise is dropped. - Statistic mislabeled: §2 called the
maxActiveAIscorpus's12a "median". Recomputed directly fromr4_aispawnwaves.md's own cited n=504 distribution: the median is 8 (ranks 252/253 both fall in the8bucket);12is the mode (177/504, the single most common value) —r4_aispawnwaves.mditself already says "mode12" correctly; this report's prose conflated the two. Fixed below. - Citation error: the cuttable/fracture-model
[-16, 16]string was cited at RVA0x2652620; re-runningdecomp.py --strings "16, 16"finds it at RVA0x26526c0(0xA0 off). String content and existence were correct, only the address was wrong. Fixed below. - Unflagged tooling artifact: Ghidra's decompiler tags the call site for
the (confirmed non-fatal) 16-projectile clamp with
/* WARNING: Subroutine does not return */, the same tag used on the genuinely fatal calls — the report never flagged that this tag is unreliable in this snapshot. Investigated further this round (§2.2 below): the tag turns out to be wrong on every non-fatal call site found so far, and there is a reliable alternative discriminator.
The verifier also flagged 4 missed items — physics body limits, a real
raisable poi_ArtificialPOILimit cvar, a 4th hard numeric MAX_* constant
(MAX_DYNAMIC_ENVIRONMENT = 128), and partial resolution of the two
light/shadow caps §4/§8 left open. All four are addressed below (§2.2, §3.4,
§3.5, §4 shadows/lights subsection, §7).
Question
Hard limits of both engines for modders: max entities, max AI, max active lights, map size/coordinate limits, streaming/memory pools, decl/resource count limits, physics body limits, string/name length limits. Which limits are cvars (raisable) and which are compiled constants; what happens at the limit (warning, cull, fatal).
Answer (short)
TNC (decompiled, strong evidence): two completely different kinds of limit coexist, and conflating them is the easiest way to get this wrong.
- Hard, fixed-size C arrays with no cvar at all, found by direct
disassembly, not by name-matching: the command and cvar registries
themselves are capped — 234/256 commands, 34/64 exposed commands,
3256/4096 cvars, 304/512 exposed cvars (§1, from
r4_consolecmds.mdcvars_tnc_v2.json's ownruntime_listsmetadata, independently re-verified here). This is the one limit that directly bounds "how many new commands/cvars can a mod register" — headroom: 22 commands, 30 exposed commands, 840 cvars, 208 exposed cvars before the engine's own array caps out. No cvar raises this; it is a compiled array size.
- ~400 named
MAX_*/*_MAX_IDC++ static-const symbols compiled into both EXEs as RTTI/debug-string literals (§6) —MAX_GENTITIES,ENTITYNUM_MAX_NORMAL,MAX_RENDER_LIGHTS,MAX_WORLD_AREAS, etc. Proven to exist (the strings are real, referenced by RTTI/type tables) in both TNC and TNO verbatim for most of the family — itself evidence the two engines share code lineage — but for the overwhelming majority the literal numeric value could not be extracted this round: they are compiled directly into comparison instructions at each call site, not stored as a single addressable constant, and digging out "the" value would mean finding and disassembling every use site individually. (Not every name is shared —idAAS2File::MAX_AREASis TNO-only, corrected from pass 1's wrong "both" claim, §6.) Five exceptions where the value was pulled out of disassembly (§3): 16 (max projectiles spawned per weapon-fire call, TNC), 64 (max members of an AI "Group", TNC, string-identical warning exists in TNO too), 128 (MAX_DYNAMIC_ENVIRONMENT, TNC, found this round, §3.4), 8196 (max visible lights per frame, TNC, found this round, §4), and the[-16,16]world-unit cube a cuttable/fracture model's geometry must fit inside (TNC). - 131 cvars with an engine-enforced
min/maxclamp range and 309 more whose name suggests a cap (not independently verified as clamped) — full table extracted fromre_probes/cvartable/cvars_tnc_v2.json(§4), covering AI/physics/streaming/render/world-geometry caps with their defaults, ranges,CVAR_flags and whether they are console-reachable (exposed) or need theresourceExec -s/ Kiscule-Command route proven inr4_consolecmds.md/r5_kisculecmd.md. - AI population caps are level-design data, not an engine constant.
idTarget_AISpawn's "Too many AIs active!" warning (disassembled, §2) reads a per-entity fieldm_maxActiveAIsset by the map author (-1= unlimited, corpus mode12/177 of 504, median8, perr4_aispawnwaves.md) and compares it to a live AI counter — there is no global "max AI in world" engine constant gating this; a custom map is free to setmaxActiveAIs = -1on every spawner. - What happens at the limit differs by subsystem, now proven across 6
disassembled call sites and traced to a single reliable discriminator
(§2.2): every fatal-vs-warn call in this report routes through one of
two shared logger trampolines, and the trampoline's own first argument to
the underlying log function (5 = warn, 7 = fatal) decides the outcome —
not Ghidra's own "does not return" tag, which this round found to be
wrong on every one of the 4 confirmed-non-fatal call sites. FATAL
(severity 7, terminates first): the entity registry overflow
(
idGameLocal::RegisterManagedObject, §2.1) and the AI "Group"-size cap (§3.2). Warn, logged, execution continues (severity 5): the per- spawner AI cap (§2), the multi-projectile clamp (§3.1), theMAX_DYNAMIC_ENVIRONMENTcap (§3.4), and the "too many visible lights" cap (§4). - Physics body limits exist but are Havok-internal, not id Tech-level
(§7, new this round): a "shape instances exceeded maximum" string and a
Havok motion-solver rotation/position-clamp-event pair were traced to
real call sites, but both numeric ceilings live inside Havok middleware's
own budget/threshold system rather than a single findable
idLib-style comparison — a structurally different, harder static-analysis target than every cap in points 1–5, and still numerically unresolved.
TNO (no runtime image, strings only — honestly limited): the exact same
MAX_* symbol family exists verbatim in .rdata (MAX_GENTITIES,
ENTITYNUM_MAX_NORMAL, MAX_PLAYERS, idAAS2File::MAX_AREAS, …, §6), and
the exact same "Too many AIs active!"/"Group '%s' has too many members. The
max is %d."/idGameLocal::RegisterEntity: spawn count overflow message
family exists too — strong evidence TNO and TNC are the same engine lineage
with the same limit architecture — but no numeric value, no cvar
min/max, no flag byte, and no fatal-vs-warning behaviour could be confirmed
for TNO this round: .text is encrypted, there is no runtime snapshot, and
this project's rule against unpacking SteamStub was respected. Everything
TNO-side in this report is string evidence only, marked INFERRED, never
PROVEN.
1. The one limit that is neither a cvar nor a MAX_* name: the registry arrays themselves (TNC)
From r4_consolecmds.md §1 (disassembled idCmdSystemLocal/idCVarSystemLocal,
re-verified against the live snapshot this run) and cvars_tnc_v2.json's own
runtime_lists block (machine-readable, produced by walking the same two
idList heads at runtime):
| registry | used | capacity | headroom |
|---|---|---|---|
all commands (idCmdSystemLocal+8, RVA 0x4104428) |
234 | 256 | 22 |
exposed commands (idCmdSystemLocal+0x28, RVA 0x4104448) |
34 | 64 | 30 |
all cvars (idCVarSystemLocal, array ptr 0x2f4f8c0) |
3256 | 4096 | 840 |
exposed cvars (array ptr 0x2f578f0) |
304 | 512 | 208 |
This is a hard, compiled array size — not a cvar, nothing in the CVAR_/CMD_
flag set changes it. It matters for modding because it is the ceiling on
adding new commands/cvars at all (e.g. via a hypothetical AddCommand/
RegisterStaticVars hook): retail already uses 79.5% of the cvar slots and
91.4% of the command slots, but there is real headroom (840 cvars, 22
commands) before the engine would need the backing array reallocated —
something no proven mechanism in this project currently does. Not tested in
this round (would need a working AddCommand/RegisterStaticVar injection
point, which no track has built); recorded as a hard ceiling to keep in mind,
not as "headroom available to mods today".
2. AI population: per-spawner data field, not a global constant (TNC)
idTarget_AISpawn's Exec/Think body at RVA 0x96d290
(tools/decomp.py 0x96d290) is the function behind the string
"idTarget_AISpawn: ( Spawn ScenePoint execution ) Too many AIs active!
Number of AIs active: %i, limit set to: %i, number shown: %i out of: %i "
(RVA 0x26a19c0, confirmed as this function's own referenced string via
--strings "too many"). The decompiled body reads three named fields off the
entity through the generic decl-parameter accessor (same reflection
mechanism r4_aispawnwaves.md §3 inferred from the lack of direct code
xrefs to the field name):
puVar10 = FUN_7ff62cb86060(param_1, s_m_maxActiveAIs_..., param_2);
uVar6 = *puVar10; // m_maxActiveAIs (per-entity, map-author-set)
puVar10 = FUN_7ff62cb86060(param_1, s_m_maxNumOfSpawn_..., param_2);
uVar8 = *puVar10; // m_maxNumOfSpawn
pcVar11 = FUN_7ff62cb85ec0(param_1, s_m_randomSpawn_..., param_2);
cVar4 = *pcVar11; // m_randomSpawn (bool)
...
if ((-1 < (int)uVar6) && ((int)uVar6 <= iVar17)) {
return; // cap not exceeded (or -1 = unlimited) -> fall through, no warning
}
iVar17 is read earlier from *(int*)(*(longlong*)(lVar12+8) + 0x56a20) —
a live counter field, not a constant. This confirms r4_aispawnwaves.md's
inference directly in code: maxActiveAIs is a per-idTarget_AISpawn
entity parameter (map/decl data), compared against a live count, with no
engine-wide ceiling anywhere in this function. A custom map setting
maxActiveAIs = -1 on every spawner is not fighting any compiled limit here
(it may still hit AI performance limits — pathfinding, nav-mesh job count,
ai_visAreaNodesPerFrame/ai_visAreaTracesPerFrame cvars §4 — but not this
hard-coded population gate). (Corpus distribution note, corrected from pass 1:
the value 12 quoted above is the mode — 177 of 504 spawners that set the
field — not the median, which is 8; see "Pass 1 errors" item 3.)
2.2 How to tell warn from fatal reliably: the logger's severity argument, not Ghidra's NORETURN tag
Pass 1 used Ghidra's /* WARNING: Subroutine does not return */ tag as a
rough fatal/warn signal and got it right by luck; the verifier caught that the
tag is also present on the confirmed-non-fatal 16-projectile clamp call site.
This round traced every warn- and fatal-style call found across the whole
track (§2.1, §3.1, §3.2, §3.4, §4) down one more level, into the two shared
logger trampolines they all call through:
void FUN_7ff62d4485c0(a,b,c,d) { ...; FUN_7ff62d446a60(5, a, &local); return; } // WARN path
void FUN_7ff62d4486b0(a,b,c,d) { ...; FUN_7ff62d4481e0(); // FATAL path
FUN_7ff62d446a60(7, a, &local); return; }
Both wrappers are tagged NORETURN by Ghidra at their call sites, and both
decompile to a normal return; themselves — the tag is simply wrong/
unreliable in this GZF snapshot (likely because FUN_7ff62d446a60 itself,
or its own leaf FUN_7ff62df567c0, genuinely never returns for some
severity level, and Ghidra's static analysis applied that NORETURN-ness to
the trampoline regardless of which severity it is called with). The real
discriminator is the first argument to FUN_7ff62d446a60: severity 5
is a log-and-continue warning (the wrapper always falls through to its own
return;); severity 7 first tail-calls a dedicated terminate chain
(FUN_7ff62d4481e0, itself genuinely NORETURN) before logging — i.e. the
process is already being torn down by the time the message prints. This
holds across every call site traced this track: fatal (severity 7,
FUN_7ff62d4486b0) — idGameLocal::RegisterManagedObject overflow (§2.1),
the Group-member cap (§3.2); warn (severity 5, FUN_7ff62d4485c0) — the
16-projectile clamp (§3.1), the idTarget_AISpawn AI-cap check (§2), the new
MAX_DYNAMIC_ENVIRONMENT cap (§3.4), and the new "too many visible lights"/
"max visible shadow slices" caps (§4). Recipe for future passes: when
classifying a new MAX_*/limit call site in this snapshot, find the call
into FUN_7ff62d446a60 (directly or through one of the two wrappers above)
and read its first argument — do not trust the NORETURN tag alone.
2.1 What overflow actually does: FATAL for entities, not for AI count
idGameLocal::RegisterManagedObject's overflow branch (RVA 0x11c8a91,
tools/decomp.py 0x11c8a91) is a one-instruction, no-return tail call:
void FUN_7ff62d3d8a91(void) {
/* WARNING: Subroutine does not return */
FUN_7ff62d4486b0(s_idGameLocal__RegisterManagedObje_7ff62eb80810);
// "idGameLocal::RegisterManagedObject: spawn count overflow"
}
FUN_7ff62d4486b0 is the engine's fatal-error entry point (same call shape
used by every other no-return assert-style error string found in this
track, e.g. s_Tried_to_spawn_too_many_projecti_... in §3.1, and matches the
idCommonLocal/idLib::FatalError idiom this project's other tracks already
established). Exceeding the managed-object/entity registration slot count
is FATAL, not a soft cull. The companion string, "Could not register
'%s'. Managed Object number %d already used." (RVA 0x2970880, function
0x11c8a9e), is a related but distinct error — a slot-reuse conflict, also a
no-return fatal call — not the same condition as the pure overflow. Open
question, not resolved this round: the actual numeric capacity behind
MAX_GENTITIES/MAX_G_MANAGED_OBJECTS (both exist as RTTI/debug-string
names, §6, in both games) was not extracted — no caller of the overflow
function was found by Ghidra's static xref scan (likely an indirect/
optimized call site), so the comparison that triggers it was not located.
3. Compiled numeric constants actually pulled out of disassembly (TNC)
Three cases where a MAX_*-style cap's literal value, not just its name,
was confirmed:
3.1 Multi-projectile spawn: hard-coded 16
tools/decomp.py 0x105a280 (the function behind
"Tried to spawn too many projectiles (%d). Max allowed is (%d)", RVA
0x2955758):
iVar1 = *(int *)(param_2[0x1d] + 0x3bc); // requested projectile count (weapon/ammo decl field)
if (iVar1 < 1) { iVar6 = 1; }
else {
iVar6 = iVar1;
if (0x10 < iVar1) { iVar6 = 0x10; } // clamp to 16
}
FUN_7ff62d448640(iVar1 != iVar6, s_Tried_to_spawn_too_many_projecti_...);
// assert(iVar1==iVar6) -- warns+continues (severity 5, see §2.2),
// despite Ghidra's misleading NORETURN tag on this call site
This clamps (not fatals) to 16 simultaneous projectiles per fire call — relevant to any weapon mod that sets a decl's pellet/burst count above 16 (e.g. a shotgun with more than 16 pellets per shot): the engine will silently cap it to 16 and print a warning, not crash.
3.2 AI/encounter "Group" size: hard-coded 64
tools/decomp.py 0xdae38b (behind "Group '%s' has too many members. The
max is %d.", RVA 0x28b9430):
FUN_7ff62d4486b0(s_Group___s__has_too_many_members__..., *(undefined8*)(unaff_RBP+0x20), 0x40);
The format's %d is filled from the literal 0x40 = 64 — a fatal
no-return call (same idiom as §2.1). This exact message string exists
verbatim in TNO ("Group '%s' has too many members. The max is %d.",
RVA 0xf327d0, found by tno_strings.py) — the number itself could not be
confirmed for TNO (encrypted .text), but the shared, byte-identical format
string is itself evidence the cap is the same architectural mechanism
(possibly the same 64) in both games.
3.3 Cuttable/fracture model geometry: a literal world-space bound
"DynamicFracture : Model '<name>' is too large to use as a cuttable, ensure
model is contained inside a [-16, 16] world boundary axis box." (RVA
0x26526c0, corrected from pass 1's 0x2652620 — see "Pass 1 errors" item 4)
— a plain-text literal bound, no disassembly needed: any cuttable/destructible
model's local geometry must fit inside a 32×32×32 world-unit cube or
DynamicFracture rejects it. Relevant to any mod adding a new breakable prop.
3.4 Dynamic environment probe population: hard-coded 128
tools/decomp.py --strings "MAX_DYNAMIC_ENVIRONMENT" finds the string
"MAX_DYNAMIC_ENVIRONMENT exceeded" (RVA 0x2605538) referenced from
FUN_7ff62c589120 (RVA 0x379120) — a function that registers/looks up a
dynamic-environment (reflection-probe-style) volume by its world-space
bounding box. A live population counter is read at the top of the function
(iVar1 = *(int*)(lVar2 + 0xc510)) and, further down, compared directly:
if ((param_3 != 0) && (param_6 < 0x11)) {
if (iVar1 == 0x80) { // 0x80 = 128
/* WARNING: Subroutine does not return */
FUN_7ff62d4485c0(s_MAX_DYNAMIC_ENVIRONMENT_exceeded_...); // severity 5 -- warn, not fatal (§2.2)
}
/* ... falls through and continues registering the probe ... */
A 4th confirmed hard numeric cap: 128 simultaneous dynamic-environment probes, warn-and-continue (same non-fatal severity-5 path as §3.1/§2), despite carrying the same misleading NORETURN tag. Relevant to any mod/map that spawns an unusually large number of reflection-probe-style volumes.
4. Cvar-governed limits: the full min/max-clamped set, plus named max* cvars (TNC)
re_probes/cvartable/cvars_tnc_v2.json (3256 cvars, each with default,
type, flags_decoded, exposed/settable_in_retail, and — for 131 of
them — an engine-enforced min/max). Flag meanings (from
tools/cvar_table.py's own cross-checked bit table, selbsttest()):
CVAR_CHEAT = reset-on-cheat-path candidate (TNC has no proven TNO-style
reset mechanism found so far, unlike the TNO behaviour docs/limits.md
documents); CVAR_INIT = only takes effect if set before that subsystem
initializes (startup .cfg/+set, not mid-session console); CVAR_ROM =
never settable; CVAR_EXPOSE = reachable by the normal, restricted (level-1)
console — everything not EXPOSE needs the resourceExec <file> -s /
Kiscule-Command-node route r4_consolecmds.md/r5_kisculecmd.md already
proved reaches non-exposed cvars with no code patch.
World geometry / joints (all CHEAT+INIT, none EXPOSE — startup-config only):
| cvar | default | notes |
|---|---|---|
r_worldGeometryManagerMaxVerts |
11 | million static vertices, engine-wide cap for mergedworld-class geometry |
r_worldGeometryManagerMaxIndices |
36 | million static indices (~12M triangles) |
r_worldGeometryVertexPaintMaxVerts |
8 | million, vertex-painted subset |
r_worldJointsManagerMaxJoints |
300000 | max joints usable for skinned geometry engine-wide |
r_LODSkipOnLargeModelsMaxVerts |
500000 | per-model; skip auto-LOD generation above this |
Collision/kd-tree (CHEAT, not EXPOSE; engine-clamped min/max):
| cvar | default | min–max |
|---|---|---|
kd_maxTrisPerLeaf |
8192 | 64–16384 |
kd_maxVertsPerLeaf |
8192 | 64–16384 |
kd_maxLeafSize |
800 | 200–2000 |
AI / entity population & physics (CHEAT, not EXPOSE):
| cvar | default | notes |
|---|---|---|
g_maxEffectPhysicsBodies |
64 | max active effect (non-gameplay) physics bodies at once |
g_maxSpawnLocations |
150 | approx. limit of generated MP-style spawn locations |
gore_maxLivingGoreEnts |
10 | max simultaneous gore-piece entities in the world |
ai_maxTurnRate |
360 | deg/s |
g_parallelMaxThreads |
4 | threads for parallel game jobs |
aimtarget_maxDistance |
500 | aim-assist target distance |
poi_PVPEnemyNameMaxAlpha/...Deg |
— | leftover PvP/MP scaffolding (TNC ships SP-only) |
poi_ArtificialPOILimit |
25 | CHEAT, NETWORKSYNC (not EXPOSE — needs resourceExec -s). Description verbatim: "Although we allocate space for additional POIs past this, we will throw an error if we get this many POIs — so we can evaluate how many we actually need." Directly backs the string "idPOIManager::UpdatePOIInfo had too many POIs - exceeded artificial limit of %d, had %d. Increase CVAR 'poi_ArtificialPOILimit' to raise this." — a named, raisable POI (point-of-interest / HUD marker) population cap, found this round (missed in pass 1 despite being reachable from the same --strings "exceeded" sweep) |
Streaming / memory pools (mixed EXPOSE status — check before assuming a route):
| cvar | default | min–max | exposed? |
|---|---|---|---|
is_poolSize |
1024 MiB | — | yes (NOCHEAT, LOCAL_SETTING, EXPOSE) — plain console-settable |
is_maxUploadsPerFrame |
24 | — | yes |
is_maxTextureSize |
32768 KB | — | no (CHEAT, INIT) |
is_maxAllocationsPerFrame |
32 | — | no |
fc_maxCacheMemoryMB |
1024 | 1–2048 | no |
fc_minCacheMemoryMB |
4 | 0–96 | no |
fc_leaveFreeMapMB |
128 | 1–1024 | no |
fc_leaveFreePersistMB |
8 | 1–1024 | no |
resource_diskStreamingBufferSize |
8 MiB | 1–1024 | no |
resource_diskStreamingScratchBufferSize |
4 MiB | 1–1024 | no |
s_initStreamPoolSizeMB |
40 | — | no (INIT — startup only) |
s_initApuMemoryHeapSize |
370 MB | — | no |
vt_maxLockedPagesPercent |
20% | 0–50 | yes |
vt_maxPPF (pages/frame, virtual texture) |
32 | 0–64 | yes |
Strings / names (cvar-level only — the C++ MAX_* string-length constants
in §6 have no retrievable numeric value this round):
| cvar | default | notes |
|---|---|---|
swf_textMaxInputLength |
512 | max chars in a UI text-input field |
snapEdit_scriptVarNameMaxLength |
20 | max script-variable name length in the SnapMap-derived editor |
Shadows / lights (compiled caps, CHEAT, not EXPOSE):
| cvar | default | notes |
|---|---|---|
r_staticShadowsMaxOpaqueVertices |
33 554 432 | 32M, static shadow cache |
r_staticShadowsMaxAlphaVertices |
1 048 576 | 1M, transparent subset |
r_umbraMaxShadowQueriesVisible/...Invisible |
6 / 1 | 0–512 clamp |
No cvar named "max visible lights"/"max active lights" exists. Pass 1 left both compiled caps unresolved; this round finished reading both to their comparison line:
- Visible lights per frame: hard-coded 8196. The light-gather function
(RVA
0x113549b) guards its "Too many visible lights.\n" warning with a direct literal comparison:if (unaff_R12 == 0x2004) { FUN_7ff62d4485c0(...) }—0x2004= 8196. Severity 5, i.e. warn-and-continue (§2.2), same misleading NORETURN tag.unaff_R12is a live visible-light counter (its increment site was not traced this round, but the comparison itself is a direct literal, not table- or cvar-driven). - "Max visible shadow slices" is a table, not one number.
GatherShadowCasters(RVA0x1136efd) reads its cap from*(uint*)(&DAT_7ff62e878100 + index*4)— a smallint32array indexed by a live value (uVar29, whose origin — most likely a quality-tier or light-type enum — was not traced back this round) — then warns (severity 5,"Max visible shadow slices exceeded") if the live count exceedstable[index] + some_float. Reading the raw bytes directly fromre_probes/runtime_87400/module.binat file offset0x2668100(= RVA ofDAT_7ff62e878100, same file-offset-as-RVA conventiontools/decomp.pyuses) gives the first 6int32entries as{6, 1, 4, 0, 6, 0}before the data starts looking like 8-byte-aligned pointers (plausible table end, not confirmed by a bounds check in code). So this is a per-context cap table with small values (0–6), not a single large constant — a materially different shape of limit than every other cap in this report, which is itself a useful finding: not everyMAX_*-adjacent check is a flat array bound.
5. Decls / resources: no fixed-array cap found — idList-shaped, not idArray-shaped
Unlike the cvar/command registries (§1, a genuine fixed C array), nothing in
this track found a compiled ceiling on decl or resource counts. Circumstantial
but consistent evidence: docs/engine-surface.md already measured TNC's live
install at 203 843 assets across 105 types, with single types running into
the tens of thousands (sample 61 318 in the TNO measurement in the same
doc, sound 43 782) — if a small fixed array like the cvar one existed here,
retail content would already be bumping into it, and it isn't. declManager
itself is referenced in TNC's strings only via generic "type already
registered"/"decl not found" messages (§6), never a capacity message, and its
RTTI name (idDeclManagerLocal) uses the generic idList-backed container
idiom this project's other tracks have repeatedly found to be dynamically
growable (unlike idCmdSystemLocal's/idCVarSystemLocal's static arrays).
Inferred, not proven: decl/resource counts are bounded by available
memory and by the archive/cfile container format (csize/offset invariants,
docs/limits.md §3–4), not by a small compiled array — the practical ceiling
for a modder is "the container format and streaming budget", not an
entity-style hard count.
6. Shared MAX_* symbol family: present (named) in both games, numeric value mostly out of reach
tools/decomp.py --strings "MAX_" (TNC, ~450 hits) and
re_probes/enginelimits/tno_strings.py "MAX_[A-Z_]+" (TNO, ~450 hits,
reproducible on demand) both surface the same C++-mangled static-const-member
symbol family, e.g. (TNC left / TNO right, both confirmed present in the
respective binary, values below are each binary's own string, not compared
numerically — neither is readable):
MAX_GENTITIES MAX_GENTITIES
ENTITYNUM_MAX_NORMAL ENTITYNUM_MAX_NORMAL
ENTITYNUM_WORLD / _NONE / _ALL_BUT_WORLD (TNO has the same trio)
MAX_PLAYERS / MAX_COOP_PLAYERS / MAX_BOTS / MAX_LOCAL_PLAYERS (both)
MAX_WORLD_AREAS / MAX_WORLD_VIEWS (both)
idCmdArgs::MAX_COMMAND_STRING / MAX_COMMAND_ARGS (both)
MAX_RENDER_LIGHTS / MAX_RENDER_MODELS (both — corrected from pass 1, which wrongly said "TNO only"; re-run `decomp.py --strings MAX_RENDER` confirms TNC RVA 0x279f100/0x279f0e8)
MAX_ENTITY_COORDINATE (both)
Corrected from pass 1 (see "Pass 1 errors" item 1): idAAS2File::MAX_AREAS
was wrongly claimed above and in PROVEN to exist in both games. Re-checked
three ways this round (--strings idAAS2File, --strings AAS2,
--strings MAX_AREAS) — zero hits in TNC. It exists only in TNO
(RVA 0xd95778, via tno_strings.py). Dropped from the "both" list.
Why the numbers can't be read even for TNC (where .text is decompilable
this round): almost all of these strings show "(no references found)" under
tools/decomp.py's xref scan — they are consumed by a reflection/RTTI name
table (likely per-type static-member descriptors for the save/serialization
or script-binding system), not by a single, findable comparison instruction.
The 3 exceptions in §3 were found precisely because their error-message
siblings (format strings passed to a fatal/warn call) sit right next to a
literal integer argument in the same short function — that pattern does not
exist for most of the MAX_* family, which are structural/reflection
metadata rather than runtime-checked limits with a logged error path.
TNO-specific finding: idGameLocal::RegisterEntity: spawn count
overflow (TNO's name for TNC's RegisterManagedObject, same message shape,
RVA 0xffeb60) confirms TNO has the identical entity-overflow mechanism —
further lineage evidence — but, again, no numeric value, no code path, and
no fatal-vs-warning confirmation is possible without a decompiled TNO image,
which does not exist in this project.
7. Physics body limits (TNC) — missed entirely in pass 1, traced further this round
The question explicitly asked about physics body limits; pass 1's report had
only the g_maxEffectPhysicsBodies=64 cvar (§4) and no mention of the
Havok (hknp) engine's own internal limits, despite the relevant strings
being directly reachable from the report's own --strings "exceeded" sweep.
This round traced two separate Havok-side limit families as far as static
analysis allows:
Shape-instance budget. "The number of shape instances (%i) exceeded the
maximum number (%i) by %i. Number of triangle meshes used: %i. Number of
polytopes used: %i" (RVA 0x26544e0). Pass 1's xref scan found only a raw
reference address (~0x7ff62c931487) that tools/decomp.py would not
resolve to a function start — reproduced here and confirmed still true. This
round went one level deeper with tools/tnc_runtime_code.py's Capstone
disassembler (reads module.bin directly, independent of Ghidra's function
boundaries) and disassembled the actual instructions at that address:
0x721487 lea rdx, [rip + 0x1f33052] ; -> 0x26544e0, the shape-instance string
0x72148e mov rcx, qword ptr [rbp+0x1e8]
0x721495 call 0x120fb40
FUN_7ff62d41fb40 (the call target) is a thin trampoline into
FUN_7ff62d41fa90 — a different logging path than §2.2's
FUN_7ff62d446a60/severity pair, consistent with this being a Havok
middleware-internal error reporter, not an id Tech-level MAX_* check. The
message's own two %i fields (current count, max count) are filled from
runtime arguments passed into this call, not from a literal immediate at
this site, so the numeric ceiling itself remains unresolved — but this
round establishes why: it is a Havok-internal configured budget (most
likely set once at physics-world init from a Havok config/tuning asset, not
a single compiled constant anywhere in idGameLocal-level code), which is a
materially different, harder static-analysis target than every idLib-level
cap in §2–§4. tools/decomp.py 0x7210c0-area and tools/tnc_runtime_code.py
disagree on this function's boundaries (the .pdata-derived range
0x720100–0x7218e0 that tnc_runtime_code.py reports contains the target
address, but tools/decomp.py's Ghidra-derived function at that same start
address decompiles to an unrelated 6-line stub) — recorded as a tooling
discrepancy for whoever picks this up next, not re-investigated further this
round (capped per this track's "~3 identical dead ends" rule).
Motion/constraint clamp events. "MimaxRotationExceeded" /
"MimaxPositionExceeded" (RVA 0x1f8b310/0x1f8b328) are referenced by
three real functions, not reflection metadata — decompiled one,
FUN_7ff62db9e250 (RVA 0x198e250): it is a debug-telemetry event
emitter (writes named events — YtDebris, YtGetClosestPoint,
YtCheckContactCache, then conditionally MimaxRotationExceeded/
MimaxPositionExceeded — into a ring buffer for an external physics
visual-debugger), not the threshold check itself. It forwards a live
exceeded-flag/magnitude (*(int*)(param_1+9) / *(int*)(param_1+0x4c),
non-zero = exceeded) that was computed earlier by Havok's own motion solver;
the actual rotation/position thresholds that set that flag are Havok-internal
and were not located this round.
Net assessment: physics body/shape limits are real, stringed, and now
traced one concrete level deeper than pass 1 (an actual disassembled call
site for the shape-instance cap, a decompiled telemetry emitter for the
motion-clamp events) — but, unlike §2–§4's id Tech-level caps, they live
inside Havok middleware's own internal budget/threshold system rather than a
single findable idLib-style comparison, so no numeric value could be
extracted this round either. This is a different kind of dead end than
§2.1's "no callers found" (an architectural one, not a search-coverage one),
worth recording as such for the next pass rather than re-attempting the same
xref search a 4th time.
PROVEN
- TNC's command/cvar registries are fixed-size C arrays with the exact
capacities in §1 (
r4_consolecmds.md's disassembly +cvars_tnc_v2.json's ownruntime_listsmetadata — two independent sources agreeing). idTarget_AISpawn's "Too many AIs active" check (RVA0x96d290) reads a per-entitym_maxActiveAIsfield, not a global constant, and the function returns normally (no fatal, no assert) when the cap is not exceeded or is-1— confirmsr4_aispawnwaves.md's data-level inference directly in disassembly (§2).idGameLocal::RegisterManagedObject's overflow path (RVA0x11c8a91) is an unconditional, no-return fatal-error call — exceeding it crashes, it does not cull or warn-and-continue (§2.1).- The multi-projectile spawn cap is a hard-coded 16 (RVA
0x105a280, §3.1) — a warn-and-clamp, not fatal. - The "Group has too many members" cap is a hard-coded 64 (RVA
0xdae38b, §3.2) — fatal, no-return. - The cuttable/fracture model bound is a literal
[-16, 16]world-unit cube per model (string, RVA0x26526c0, §3.3; corrected from pass 1's0x2652620, see "Pass 1 errors" item 4). MAX_DYNAMIC_ENVIRONMENTis a hard-coded 128 (RVA0x379120, §3.4) — warn-and-continue (severity 5, §2.2), not fatal.- The light-gather function's "too many visible lights" cap is a hard-coded
8196 (
0x2004, RVA0x113549b, §4 shadows/lights) — warn, not fatal.GatherShadowCasters's "max visible shadow slices" cap (RVA0x1136efd) is not a single literal but a smallint32lookup table atDAT_7ff62e878100, first 6 entries read directly frommodule.binas{6, 1, 4, 0, 6, 0}(§4). - The logger
FUN_7ff62d446a60's first argument is the real fatal/warn discriminator (5 = warn/returns, 7 = fatal/terminates first), independently reproducible across all 6 warn/fatal call sites traced this track (§2.2) — more reliable than Ghidra's own NORETURN tag, which is wrong on every one of the 4 warn-path call sites. poi_ArtificialPOILimit(default 25,CHEAT, NETWORKSYNC, notEXPOSE) exists incvars_tnc_v2.jsonexactly as described and backs theidPOIManager::UpdatePOIInfo"exceeded artificial limit" string (§4).- The 131-entry
min/max-clamped cvar table and the broadermax*-named cvar set in §4, with exact defaults, ranges andCVAR_flags, all read directly fromcvars_tnc_v2.json(produced by walking the live cvar registry, per that file's ownruntime_lists/hinweismetadata). - The
CVAR_flag bit table used to interpretexposed/INIT/CHEATabove istools/cvar_table.py's own cross-checked table (selbsttest()asserts it against two independently-read reflection tables in the EXE and confirms TNO uses a different bit layout — proof the table is measured, not assumed). - The
MAX_*/*_MAX_IDstring family exists, by name, in both TNC (tools/decomp.py --strings "MAX_") and TNO (re_probes/enginelimits/tno_strings.py "MAX_[A-Z_]+"), including several identical fully-qualified names (MAX_GENTITIES,ENTITYNUM_MAX_NORMAL,idCmdArgs::MAX_COMMAND_STRING,MAX_RENDER_LIGHTS,MAX_RENDER_MODELS, §6) — both runs are reproducible on demand, output included inline above. (idAAS2File::MAX_AREASis not in this "both" list — pass 1 wrongly included it; it is TNO-only, see "Pass 1 errors" item 1 and §6.) - The error-message strings
"Too many AIs active!"(asidTarget_AISpawnin TNC /idTarget_ShowAIin TNO) and"Group '%s' has too many members. The max is %d."exist verbatim in both binaries' string tables (§3.2, §6) — found independently viatools/decomp.py --stringsandtno_strings.pyrespectively.
INFERRED (plausible, not independently nailed down this round)
- That decl/resource counts have no small fixed-array ceiling (§5) — based
on the absence of any capacity-style error message for
declManagerand on retail's own very high per-type counts, not on a disassembled bounds check (none was found to disassemble). - That TNO's identical
MAX_*/error-string family implies the same numeric caps as TNC — plausible given shared code lineage but explicitly unconfirmed; TNO could easily have different compiled values even with identical symbol names (different game, different build). - That
MAX_GENTITIES/MAX_G_MANAGED_OBJECTS's actual numeric value governs the fatal overflow in §2.1 — the string names exist and the overflow function exists, but the comparison site tying the two together was not located (no callers found by Ghidra's static xref scan for the overflow function itself, §2.1). - That
GatherShadowCasters's shadow-slice cap table (DAT_7ff62e878100, §4) is indexed by a quality-tier or light-type enum — the index register (uVar29) is plausibly one of those given the surrounding light-culling code, but its origin was not traced back to a named source this round (resolved from pass 1's open question: the light-gather cap itself,0x2004, is now PROVEN, not inferred — only this table's index semantics remain inferred). - That the Havok shape-instance budget (§7) is a configured-at-init value rather than a compiled constant — based on the call site passing both the current count and the max as runtime arguments (not literals) into a distinct Havok-internal logging path, not on having located the actual init-time assignment.
Failed hypotheses / dead ends
- Tried to find a caller of
idGameLocal::RegisterManagedObject's overflow branch (tools/decomp.py 0x11c8a91 --callers) to recover the actualMAX_GENTITIES-equivalent numeric comparison — zero callers found by Ghidra's static xref scan (consistent with an optimized/indirect call, same class of dead endr4_aispawnwaves.md§3 already hit trying to xrefmaxActiveAIs). Not pursued further this round; would need either a broader caller search strategy or runtime breakpoint data this project's rules don't allow (no process attach). - Searched TNO's
.rdata/.datafor any literal small-integer array or table that might sit adjacent to theMAX_GENTITIESstring and encode its value directly — none found; C++static const intmembers at this era are near-universally inlined as immediate operands at each use site rather than stored as data, which is exactly why they show up as debug-string labels with "no references" in TNC's own decompiled xref scan too. Not a promising lead for either game; abandoned after one check. - Looked for a single
max visible lights/max active lightscvar by name pattern incvars_tnc_v2.json(ai_-,r_-,g_-prefixed candidates) — none exists; the only light-count-adjacent cvars found (r_umbraMaxShadowQueriesVisible/Invisible) govern occlusion-query job counts, not a light population cap. Confirmed by full-table grep, not abandoned early. - Tried to find the real function start for the Havok shape-instance-exceeded
call site (§7) three ways:
tools/decomp.py's own xref scan (pass 1, no callers resolved),tools/decomp.pyon the.pdata-derived containing range reported bytnc_runtime_code.py(decompiles to an unrelated 6-line stub — the two tools' function boundaries disagree at this address), and direct Capstone disassembly at the exact byte offset (succeeded at the instruction level, §7, but the call target is a thin trampoline whose numeric argument is a runtime value, not a literal). Capped here per this track's "~3 identical dead ends" rule; a working caller-boundary reconciliation between the two tools would be needed to go further. - Traced one of the three
MimaxRotationExceeded/MimaxPositionExceededreferencing functions (§7,FUN_7ff62db9e250) to completion — it is a debug-telemetry emitter, not a threshold check; did not decompile the other two referencing functions (FUN_7ff62db9e77a,FUN_7ff62db9e49a) since the first already established the pattern (event emission, not the solver threshold itself) and this track's dead-end cap applies.
Recipe: what a modder can actually do with this, today
- Raise streaming/cache pools with no code patch:
is_poolSize(image streaming) andvt_maxPPF/vt_maxLockedPagesPercent(virtual texture) are EXPOSEd — settable from the plain, unrestricted console or any.cfg, noresourceExec -sneeded.fc_maxCacheMemoryMB(file cache, 1–2048 MB) ands_initStreamPoolSizeMB(WWise sound pool) are not exposed but have no other barrier — theresourceExec <modded cfile> -sroute already proven inr4_consolecmds.md/r5_kisculecmd.mdreaches them the same way it reachedGod/g_infiniteAmmo.s_initStreamPoolSizeMBis additionallyCVAR_INIT— it must be set in the config that runs before the audio subsystem initializes (i.e. early indefault.cfg's own exec chain, not a late in-levelresourceExec), or it has no effect until the next full restart. - World-geometry ceilings are a startup-config matter, not a console
one:
r_worldGeometryManagerMaxVerts/...Indices/r_worldJointsManagerMaxJointsare allCHEAT+INIT, neverEXPOSE— raising them for an unusually large custom map means writing them into whatever.cfgthe loader gets exec'd before the renderer's world geometry manager initializes, not setting them mid-session. Retail's defaults (11M verts / 36M indices / 300k joints) are generous for a single custom map, so this only matters for an exceptionally large build. - Don't fight the per-spawner AI cap — it isn't the engine's. §2 proves
maxActiveAIsis authored peridTarget_AISpawnentity; a mod wanting "unlimited AI" just needsmaxActiveAIs = -1(or omit the field) on its own spawners, exactly as 83/504 retail spawners already do (r4_aispawnwaves.md). This is a data change, no cvar, no patch. - Respect the two confirmed hard numeric caps (§3): a weapon/ammo decl asking for more than 16 simultaneous projectiles gets silently clamped with a warning (not fatal) — if a mod wants a "32-pellet shotgun" effect, it needs 2 fire events or 2 weapon stages, not one 32-count decl. A single AI "Group" (encounter roster) is capped at 64 members and this one is fatal — a mod building an unusually large single encounter group should stay under 64 distinct group members.
- The cvar/command registry headroom (§1, 840 cvars / 22 commands) is
real but currently unreachable by any proven mechanism in this
project — no track has built a working
AddCommand/RegisterStaticVarinjection. Recorded for whoever picks that up next, not usable today. - New this round: a named, raisable POI cap.
poi_ArtificialPOILimit(default 25, §4) is notEXPOSEd but needs no code patch — theresourceExec -sroute raises it like any other non-exposed cvar; useful for a mod/map that places more than 25 simultaneous HUD points-of-interest (collectibles, objective markers) before hitting its logged "exceeded artificial limit" message. - New this round: two more hard numeric caps to design around.
MAX_DYNAMIC_ENVIRONMENT(§3.4) caps simultaneous reflection-probe-style volumes at 128, warn-and-continue. The light-gather function's "too many visible lights" check (§4) caps at 8196, also warn-and-continue — generous enough that no normal custom map should hit it, but a mod programmatically spawning dynamic lights should know the ceiling exists.GatherShadowCasters's shadow-slice cap (§4) is a small per-context table ({6, 1, 4, 0, 6, 0, ...}), not one number — a mod relying on many simultaneous shadow-casting slices in one context could plausibly hit a cap as low as single digits depending on which table index applies; worth keeping in mind for any "many dynamic shadows at once" effect.
Open questions
- The actual numeric value of
MAX_GENTITIES/MAX_G_MANAGED_OBJECTS(TNC) — blocked on finding the (apparently indirect/optimized) call site that compares the live count against it before calling the fatal overflow path (§2.1, "Failed hypotheses"). - The light-gather function's cap is now resolved (8196, PROVEN, §4).
Still open:
GatherShadowCasters's shadow-slice cap table's index semantics — which live value selects theDAT_7ff62e878100[index]entry (quality tier? light type? both were plausible from the surrounding code, neither confirmed) — and whether the 6 values read this round ({6, 1, 4, 0, 6, 0}) are the table's full extent or just its first 6 entries (the following bytes look like pointers, suggesting the table itself is that short, but no explicit bounds check was found to confirm it). - The Havok-internal shape-instance budget (§7) and the Havok motion-solver's
rotation/position-clamp thresholds behind
MimaxRotationExceeded/MimaxPositionExceeded(§7) — both are now traced one concrete level deeper than pass 1 (an actual call site for the former, a decompiled telemetry emitter for the latter) but neither numeric value was recovered; likely needs either locating Havok's own init-time config read or a different search strategy than the xref/disassembly approach this track used throughout. - Whether TNO's identical symbol/message family carries the same numeric
values as TNC — fundamentally unanswerable without a TNO runtime image,
which this project's rules (no process attach, no
.textunpacking) keep out of reach; flagging it as a standing limitation of any future TNO-side limits work, not something the next pass should expect to solve with static analysis alone. - The remaining ~440 unexplored
MAX_*names in TNC and TNO (§6) — most are reflection/debug metadata with "no references found", so a blanket attempt to resolve all of them is likely low-yield; worth doing only for specific ones a future track actually needs (e.g. if someone hitsMAX_PARMBLOCK_OPS/MAX_NODE_TYPESin practice while scripting).
Reproduction
# TNC — the exact queries this report's evidence came from, each ~15 s:
python tools/decomp.py --strings "exceeded"
python tools/decomp.py --strings "overflow"
python tools/decomp.py --strings "too many"
python tools/decomp.py --strings "MAX_"
python tools/decomp.py 0x96d290 # idTarget_AISpawn AI-cap check
python tools/decomp.py 0x11c8a91 --callers # RegisterManagedObject overflow (fatal, no callers found)
python tools/decomp.py 0x105a280 # multi-projectile clamp (16)
python tools/decomp.py 0xdae38b # Group member cap (64)
python tools/decomp.py --strings "16, 16" # cuttable-model bound, correct RVA 0x26526c0
python tools/decomp.py --strings "MAX_RENDER" # MAX_RENDER_LIGHTS/MODELS exist in TNC (pass 1 said no)
python tools/decomp.py --strings "idAAS2File" # zero hits in TNC (pass 1 wrongly said "both")
python tools/decomp.py --strings "MAX_DYNAMIC_ENVIRONMENT" # new this round
python tools/decomp.py 0x379120 # MAX_DYNAMIC_ENVIRONMENT cap (128), warn not fatal
python tools/decomp.py 0x113549b # light-gather "too many visible lights" cap (0x2004=8196)
python tools/decomp.py 0x1136efd # GatherShadowCasters shadow-slice cap table lookup
python tools/decomp.py 0x12385c0 # warn logger (severity 5) -- §2.2
python tools/decomp.py 0x12386b0 # fatal logger (severity 7) -- §2.2
python -c "import struct; d=open('re_probes/runtime_87400/module.bin','rb').read(); print(struct.unpack_from('<6i', d, 0x2668100))" # shadow-slice table raw values
# TNC — physics (§7), new this round:
python tools/decomp.py --strings "exceeded the maximum" # Havok shape-instance string, RVA 0x26544e0
python tools/decomp.py --strings "MimaxRotationExceeded|MimaxPositionExceeded"
python tools/decomp.py 0x198e250 # one of the 3 referencing functions (debug-telemetry emitter)
python -c "
import sys; sys.path.insert(0,'tools')
from tnc_runtime_code import Image
img = Image('re_probes/runtime_87400')
rva = 0x721487
for insn in img.cs.disasm(img.data[rva-32:rva+32], rva-32):
print(hex(insn.address), insn.mnemonic, insn.op_str)
" # direct Capstone disasm of the shape-instance call site
# TNC cvar table (no decompiler needed, pure data read):
python -c "import json; d=json.load(open('re_probes/cvartable/cvars_tnc_v2.json',encoding='utf-8')); print(d['runtime_lists'])"
python -c "import json; d=json.load(open('re_probes/cvartable/cvars_tnc_v2.json',encoding='utf-8')); print([c for c in d['cvars'] if c['name']=='poi_ArtificialPOILimit'])"
# TNO — read-only PE string scan, new this track:
python re_probes/enginelimits/tno_strings.py "MAX_[A-Z_]+"
python re_probes/enginelimits/tno_strings.py "exceeded|overflow"
python re_probes/enginelimits/tno_strings.py "too many"
python re_probes/enginelimits/tno_strings.py "idAAS2File" # TNO-only confirmation
Smallest in-game test
(Translated from German.)
The most robust, most easily checkable findings here are the
16-projectile clamp and the two free (EXPOSE'd) streaming cvars —
neither needs the resourceExec -s detour, only an own weapon decl
or the normal console, respectively:
- 16-projectile clamp (TNC, §3.1): In an own weapon/ammo decl
(e.g. a copy of
example_fast_shotgun), set the field that sets the projectile count per shot (find it by a decl diff against a shotgun decl, if the field name is not already known) experimentally to24or32and load it. Expectation according to this report: the weapon still fires only 16 projectiles per shot (visible from the spread/hit count on a wall at short distance), and in theqconsole.logequivalent (TNC log path not yet checked in this track) the warning"Tried to spawn too many projectiles"should appear. A decl with<=16as a control should fire exactly the set number, without a warning. - Free streaming cvar (TNC, §4/Recipe 1): Via the normal, unrestricted
console (no
resourceExec -sneeded, because of theEXPOSEflag) setis_poolSize 2048(is_restartafterwards, according to the cvar's description), then load a texture-heavy scene. Expectation: if the game noticeably reloaded/popped before, there should be less reloading/ popping with the larger pool — not a hard pass/fail test, but an immediately observable difference that shows the cvar actually takes effect and is not reset at the next start (unlike TNO's cheat-reset mechanism fromdocs/limits.md, which has not been shown for TNC in any track so far). - New this round —
poi_ArtificialPOILimit(TNC, §4/Recipe 6): Via the already-provenresourceExec <modded cfile> -sroute, setpoi_ArtificialPOILimit 5(instead of the default25), then load a scene with more than 5 simultaneous POI markers (e.g. several collectibles/objective markers in view at the same time). Expectation from the disassembly: from the 6th POI on, the engine should log the error message"idPOIManager::UpdatePOIInfo had too many POIs - exceeded artificial limit of %d, had %d."— the simplest evidence that theresourceExec -sroute also reaches non-EXPOSE'dNETWORKSYNCcvars like this one, not only the already-confirmedGod/g_infiniteAmmo.
Related: Research Report: R4 Console Commands, Research Report: Cmdtable, Research Report: Cvartable, Research Report: R4 Ai Spawn Waves, Research Report: R5 Kiscule Command, Research Report: R7 Tno Limits, Modding Limits, Engine Modding Surface, Ghidra Analysis.