Round-6 research pass on TNC's effect decls — fx, particle, projectileImpactEffect, ribbon, gorewounds, goreBehavior and six smaller types — their text format, the reference chain from weapon to ammo, projectile, impact, fx and particle, and whether size, colour, count and lifetime can be changed with decl edits alone. A fresh census counts 1,781 decls in 12 types, all of which parse with decl.py, and the relevant values are plain editable fields; two decl-only mods (bigger grenade explosions, a recoloured muzzle flash) are designed and run through the loader's real patch path. Whether TNC actually loads an edited particle/fx decl at runtime is still open — nothing was tested in game.
Track fxdecls, round 6, 2026-10-01. Read-only against
<Steam library>/steamapps/common/Wolfenstein.II.The.New.Colossus (TNC) and
the project's existing static findings for
<Steam library>/steamapps/common/Wolfenstein.The.New.Order (TNO, no runtime
image available — see "TNO" section). All new code lives under
re_probes/fxdecls/; nothing under wolfsdk/, tools/, docs/, or any
other track's directory was touched. No game launch, no process attach.
wolfsdk/decl.py, wolfsdk/idcl.py, wolfsdk/mapcatalog.py,
tools/extract_tnc.py are imported and reused exactly as they are.
Builds on, does not repeat: docs/moddingflaeche-tnc.md (the TNC
type census, the decl.py dialect/vector-bug measurement, the csize+gap
budget numbers), docs/moddingflaeche-tno.md (the TNO type census and the
proven "particle payload is frozen, particle references are not"
result), docs/engine-surface.md (TNO per-type field summaries),
re_probes/agent_reports/r4_materials.md (material/decal-family anatomy,
reused verbatim), re_probes/agent_reports/r3_newweapon.md and
explosivpistole_diag.md (the weapon→ammo→projectile reference chain and
the fx/fxDecl field names), re_probes/agent_reports/gore_runtime.md
(the one runtime data point this project has for a gore decl: full-file
replacement of 24 goreBehavior decls parses but crashes the game; a
surgical set() on existing scalar fields is the recommended next probe —
not retested this round, no game launch allowed), r2_newassets.md /
r3_newassetsv.md (the proven-but-prototype "append a brand-new
(type,name) entry" technique, re_probes/newassets/append_entry.py).
Pass 1 errors (fixed this round)
A verifier re-ran every claim independently and refuted four of them — all arithmetic/labeling mistakes in an otherwise-reproducible pass 1, not wrong methodology. Fixed below, each re-derived independently this round, not just copied from the verifier's numbers:
- Headline total was wrong. "1,861 decls total" didn't match pass 1's
own per-type table, which sums to 1,781 (the 12 types in §1) or
1,812 (14, including
goreGraph/goreMannequin). Re-summed independently this round viacensus.py types --all: confirmed 1,781. Fixed everywhere this number appeared. projectileImpactEffect:default's per-surface table was undercounted. Pass 1 said "28 named blocks"; a fresh count viaDecl.paths()on the live decl this round finds 39 top-leveledit.*blocks, 35 of which literally end inEffect(the other 4 arehazardFire,hazardSteam,ricketyMetal,ricketyWood). Fixed in §4.- "63/154 damage decls carry
declGoreBehavior" was an unlabeled raw count. Re-verified independently: 63/154 is correct as raw, non- deduplicated archive entries (154 totaldamageentries incl. patch-layer duplicates), but the unique-name figure is 62/151. Both numbers now stated explicitly with their methodology in §4, so they no longer read as (and don't numerically collide with)projectileImpactEffect's unrelated 156/154 row. - Two cvars were displayed as one name with a parenthetical.
"r_particleFadeQualityMultiplier(Durango)" and
"r_particlesMinAlpha(Durango)" are two independently-registered cvar
names with no space or parens:
r_particleFadeQualityMultiplierDurangoandr_particlesMinAlphaDurango. Fixed in §6's cvar list, now with every name spelled exactly as registered plus its flags/default/range.
Also closed this round, raised by the verifier as "missed" rather than
"wrong": the two §8 recipes are now run through the actual production
code path (wolfsdk.mod.Mod.load + wolfsdk.decl.apply_patch, not just
Decl.set() on a standalone script); a second, stronger cross-weapon
particle-reuse example was found (fx:weapons/shotgun); a negative
control for the TNO-style "frozen precompiled particle stream" hypothesis
was run against the TNC install tree; goreGraph/goreMannequin got a
field census; and a new static (Ghidra RTTI) data point was found for the
central open question. All in §3/§4/§6/§8/§9 below.
Answer in one paragraph
TNC's FX surface is twelve TEXT decl types, all in the key = value;
dialect (none affected by decl.py's one known weakness, the "key value"
list-as-punctuation misparse that only hits material/renderParm/
table/env/json/articulatedFigure): fx (265, an event
composition — FX_PARTICLE/FX_DECAL/FX_LIGHT/FX_SOUND/FX_RIBBON/
FX_BLUR_GAUSSIAN events keyed to trigger strings like
FX_WEAPON_START_FIRE, each with its own size/duration/tagNames and a
reference to the next layer), particle (797, the actual multi-stage
particle system — size, color, count and lifetime are direct scalar/vector
fields on every stage), projectileImpactEffect (156, the per-surface
material impact table — concrete/metal/wood/flesh/glass/... each with its
own decal-texture array, decal size, particle refs and sound), ribbon
(101, blood/smoke streaks — material, lifespan, two colors, two widths),
gorewounds (383, inline RGB color + decal size + size + per-piece
spawn/joint data for a wound type), goreBehavior (41, the probability
table that picks a gorewounds entry), plus explosion (11),
envexplosion (11, explosion spread composition), propExplode (4),
particleStage (1), flare (9) and ebolt (2). Fresh census this
round, all 12 types, 1,781 decls total (1,812 incl. the two
not-fully-censused goreGraph/goreMannequin types, now field-censused
too, §1/§9), 0 DeclError under decl.py.
Size, color, particle/spawn count and lifetime are all directly
editable scalar fields in particle and gorewounds (proven this round
by parse→edit→reparse roundtrips, not just inspection); the whole
weapon→projectile→impact→fx→particle chain is walkable string references
(projectileDecl, impactEffectTable, particle, declPrt), confirmed
end to end on the pistol. New effects from existing textures: yes, today,
without the still-prototype "new decl name" tool — any particle/fx
stage's material/decalMtr/lightMtr field is a plain string naming
any already-loaded texture/material, and retail itself already
cross-references weapons this way (the handgun's own muzzle flash reuses
textures/effects/particles/weapons/battleriflemuzzleflash_front), so a
full-body rewrite of an existing decl's content — the same "file"
full-replace mechanism gore_realistisch already ships — can recompose any
combination of existing materials into a "new" effect under a reused name.
Not proven this round: that TNC actually loads an edited particle/fx
decl at runtime (no game launch; docs/moddingflaeche-tnc.md already
flagged this as open) — narrowed, not closed, by two new static findings
(§6): TNC's RTTI shows particle/fx/gorewounds/etc. are handled by
the same generic C++ decl template as already-runtime-proven types
like weapon/damage, and TNC's install has no TNO-style precompiled
generated/*.bin shadow tree that could explain a silent discard.
Proven NOT to work in TNO: particle payload
edits are silently discarded there (measured: idFile_RingBuffer
skip-messages, 35 vs 0 baseline) — only the reference to which particle
plays is editable in TNO.
1. The twelve FX-relevant decl types (fresh census, this round)
python re_probes/fxdecls/census.py types --all
mounts all 24 .resources archives (base/chunks/chunkbase_6/
patch_1..4 + their chunkbase_6 variants — the same "common" pool
r3_newweapon.md's open_common() and docs/moddingflaeche-tnc.md use;
dlc archives live under a separate folder tree not covered by this pool,
same scope limitation every prior TNC round has had):
| type | n | unique | DeclError |
|---|---|---|---|
fx |
265 | 263 | 0 (1 entry is non-UTF8, matches moddingflaeche-tnc.md §2.1's "ein fx") |
particle |
797 | 777 | 0 |
projectileImpactEffect |
156 | 154 | 0 |
gorewounds |
383 | 383 | 0 |
goreBehavior |
41 | 40 | 0 |
ribbon |
101 | 101 | 0 |
explosion |
11 | 11 | 0 |
envexplosion |
11 | 11 | 0 |
propExplode |
4 | 4 | 0 |
particleStage |
1 | 1 | 0 |
flare |
9 | 9 | 0 |
ebolt |
2 | 2 | 0 |
Sum of these 12: 1,781 — pass 1 misstated this as "1,861"; see "Pass 1 errors" above.
Two more gore-adjacent types, named in pass 1 but not field-censused there — field-censused this round (§9 has the field summary):
| type | n | unique | DeclError |
|---|---|---|---|
goreGraph |
21 | 20 | 0 (fresh this round) |
goreMannequin |
10 | 10 | 0 (fresh this round) |
(14-type sum incl. these two: 1,812.)
Binary/not-editable neighbors, confirmed present but out of scope:
prtmeshdist (483, binary particle mesh-distribution data),
staticParticleModel (7, binary), decalatlas (903, binary — the baked
decal texture atlas), destructionmodeldata (177, binary),
glassmodel (136, binary).
Negative result: glass, destructiblePiece, impactParticles and
impactSounds — all TEXT decl types in TNO (docs/engine-surface.md) —
do not exist as decl types in TNC's common archive pool (checked via
the full, unfiltered type listing, not a name guess). TNC's equivalent
surface lives inside breakable.edit.breakable.fx.*
(brokenConnection/vaporize/vaporizeFire/vaporizeLaser, each a
particleSystem + soundEvent pair, 175 TNC breakable decls) and inside
projectileImpactEffect's per-material table — TNC folded TNO's several
small impact/glass/destructible decl types into fewer, bigger ones.
2. particle: size, color, count, lifetime are direct fields
python re_probes/fxdecls/census.py fields particle (797 decls, every
scalar leaf, index positions normalized) shows two parallel particle
systems living in the same decl type — an older CPU system that
dominates the corpus, and a newer GPU system:
Legacy CPU system (edit.stages.item[N].*, by frequency):
- Size:
size.size.size[M].val0/val1(+variance,calcType),size.aspectRatio.val0/val1 - Color:
colorAttributes.baseColor.baseColor[M].val0/val1(RGB, one channel perM),colorAttributes.fadeColor.{x,y,z},colorAttributes.brightness.val0/val1 - Count:
systemProperties.totalParticles,systemProperties.cycles - Lifetime:
systemProperties.particleLife.val0/val1(+.variance),systemProperties.deadTime.val0/val1,maxSystemDuration(top-level, the whole system's cap) - Texture:
systemProperties.material— a plain string naming any already-loaded texture/material, e.g."textures/effects/particles/liquid/splash_lit" - Also present:
gravity.gravity.val0/val1,acceleration.*,speed.*,offset.*,texAnimation.{numColumns,numRows,rate,type}(sprite-sheet flipbook playback),orientation.type(POR_AIMED/POR_X/…),bounds.b.b[0/1].{x,y,z}(the system's reported AABB — grown bounds with grown size, else particles get frustum-culled early, a real failure mode to watch for in a "bigger" mod).
Newer GPU system (systemPropertiesNEW/colorNEW/sizeNEW/
gravityNEW/velocityFieldNEW/collisionNEW, co-existing in the same
corpus, example textures/effects/particles/gpu_spark_add):
systemPropertiesNEW.maxParticles/.maxGpuSpawnedParticles/
.maxSpawnedParticles/.cpuSpawnRate/.lifeTime.val0/val1/.emissive/
.bounds.{x,y,z}, colorNEW.initialColor/.finalColor/.opacity/
.overbright, sizeNEW.size.val0/val1.
Proven this round (re_probes/fxdecls/census.py + an ad-hoc roundtrip
script, not kept as a separate file beyond the scratchpad): edited
particle:weapons/handgun/handgun_muzzleflash — size ×3
(edit.stages.item[0].size.size.size[0].val0/val1 → 0.6),
brightened color (colorAttributes.baseColor.baseColor[1].val0 → 1.0),
tripled count (systemProperties.totalParticles → 3) — Decl.set() then
.render() then a fresh Decl() on the output: re-parses without
error, every edited value reads back correctly. Same for
particle:weapons/grenades/frag/frag_explosion's maxSystemDuration.
This proves the edits are structurally safe (same class of proof as the
project's existing "1,432/1,432 roundtrip" test in
docs/moddingflaeche-tnc.md §2.2, just scoped to this round's 3 types) —
it does not prove the game loads them, see §6.
3. fx: the event composition, and a proven block-level insert trick
fx decls (edit.editEvents.item[N]) are the layer every weapon/breakable/
cutscene points at. Fields seen across 264 parsed decls: name, group,
type (FX_PARTICLE/FX_DECAL/FX_LIGHT/FX_SOUND/FX_RIBBON/
FX_BLUR_GAUSSIAN/FX_SCREEN_SHAKE per TNO's moddingflaeche-tno.md
§2.9), duration, delay, fadeInTime/fadeOutTime, size (yes — fx
events carry their own size override, independent of the referenced
particle's own size), looping, tagNames (joint names on the owning
.md6 model, e.g. "muzzle", "fx_righthand"), multiTagUseType,
startCondition/stopCondition/suppressCondition (arrays of trigger
strings — FX_WEAPON_START_FIRE, FX_WEAPON_START_FIRE_UPGRADE_1,
FX_CINEMATIC_N, FX_GENERIC_N, FX_NONE), originType/rotationType/
rotOffsetAngles/offset, and one of four typed parameter blocks:
particleParms.declPrt (→ particle decl), decalParms.{decalMtr,depth,
angle,isPersistent,priority} (→ a decals/... material, per
r4_materials.md), lightParms.{radius,intensity,lightMtr} (→ a
lights/... material), ribbonParms.{declRibbon,widthOverTime,
alphaMultiplier,...} (→ ribbon decl), soundParms.startSoundEvent.
Concrete, fully walked example — the pistol's muzzle flash:
fx:weapons/handgun
editEvents.item[0] name=muzzleflash type=FX_PARTICLE tagNames=[muzzle]
startCondition=[FX_WEAPON_START_FIRE]
particleParms.declPrt = "weapons/handgun/handgun_muzzleflash"
editEvents.item[8] name=muzzlelight type=FX_LIGHT tagNames=[muzzle]
startCondition=[FX_WEAPON_START_FIRE]
color = { y=0.521 z=0.184 } // x omitted (defaulted)
lightParms.radius = { x=2 y=2 z=2 }
lightParms.lightMtr = "lights/gaus" // no explicit intensity
editEvents.item[9] same, group="elephant" (upgrade variant), has lightParms.intensity=3
particle:weapons/handgun/handgun_muzzleflash itself is two stages
("stage"/"stage1") whose systemProperties.material fields are
"textures/effects/particles/weapons/handgunmuzzleflash_side" and —
reused from a different weapon entirely —
"textures/effects/particles/weapons/battleriflemuzzleflash_front".
This is organic, in-retail proof that cross-weapon material reuse in a
particle stage is normal and safe, not a modded hack.
A stronger example, found this round: fx:weapons/shotgun's own
editEvents reference whole particle decls that belong to other
weapons, not just a texture buried inside one particle's own material
field —
fx:weapons/shotgun
editEvents.item[5].particleParms.declPrt = "weapons/smg/smg_shell_ejection_smoke"
editEvents.item[6].particleParms.declPrt = "weapons/battlerifle/battlerifle_shell_ejection_sparks"
editEvents.item[8].particleParms.declPrt = "weapons/smg/overheat/shroud_heat_1"
three of the shotgun's own fx events point at smg/battlerifle
particle decls by name. Confirmed by reading the live decl's
particleParms.declPrt paths directly this round. This is free,
in-retail corroboration at the fx-layer (whole-decl reuse) on top of
the particle-layer example above (one texture string inside a decl) —
the engine itself never treats "weapon X's effect" as owned exclusively
by weapon X.
New technical finding this round: wolfsdk/decl.py's Decl.set()
works on any addressable path, including a whole nested block, not
just scalar leaves — because every path (scalar or {...} block) is
recorded as a Value with its own byte span. Proven: replacing
fx:weapons/handgun's edit.editEvents.item[8] (the standard muzzle
light, which has no intensity field and only a partial color) with
a new block that adds intensity = 40, a full color, and radius = 6
re-parses cleanly and reads back every new field correctly. This matters
because decl.py's surgical editor cannot otherwise add a field that
isn't already present in the retail text (set() raises DeclError if
the target path doesn't exist) — block-level replacement is the one way
around that without falling back to a full-file "file" replace of the
entire (possibly much larger) decl. It is the same "file" mechanism
gore_realistisch already uses, just scoped to one sub-block instead of
the whole document, which keeps the edit far inside the csize+gap
budget (§5).
4. The full chain: weapon → ammo → projectile → impact → fx → particle
Walked on the pistol, every field read directly from the live archive:
weapon:weapon/player/sp/handgun_nomesh
initialAmmoDecl = "ammo/player/sp/handgun"
ammo:ammo/player/sp/handgun
projectileDecl = "projectile/player/pistol"
projectile:projectile/player/pistol (inherits projectile/player/default)
impactEffectTable = "handgun"
damageDecl = "damage/firearm/bullet_pistol"
projectileImpactEffect:handgun (inherits bullet_base -> default)
fleshEffect.ribbons.item[0].ribbon = "weapon/battlerifle/impact/battlerifle_blood"
(inherited from "default"): fleshEffect.particles.item[0].particle =
"weapons/base/impacts/bullet/bullet_flesh"; concreteEffect.decalMaterial =
[4 existing textures]; concreteEffect.decalSize = 0.2; ... (28 surface types total)
ribbon:weapon/battlerifle/impact/battlerifle_blood
material = "textures/effects/ribbons/weapons/blood_impact"
lifespan = 0.5
ribbonSegment.colorOnSpawn/.colorOnDeath = { r g b } // RGB
ribbonSegment.widthOnSpawn/.widthOnDeath = { min max } // size
fx:weapons/handgun
editEvents.item[0].particleParms.declPrt = "weapons/handgun/handgun_muzzleflash"
particle:weapons/handgun/handgun_muzzleflash
stages.item[0/1].systemProperties.{material,totalParticles,particleLife}
stages.item[0/1].{size,colorAttributes}
Every link in this chain is a plain string field, independently
set()-able. The projectileImpactEffect per-surface table
(defaultEffect/metalEffect/stoneEffect/fleshEffect/glassEffect/
...39 top-level edit.* blocks in default, re-counted this round
with Decl.paths() rather than eyeballed — pass 1 said "28", see "Pass 1
errors" above; 35 of the 39 literally end in Effect, the rest are
hazardFire/hazardSteam/ricketyMetal/ricketyWood) is the concrete
"impact effect decl" the task asked for: each surface's decalMaterial
is an array of existing decal textures the engine already picks
randomly from, decalSize is a bare float, and particles/ribbons are
arrays of {groupName, particle|ribbon, translucencyScale, bind}.
Blood/gore chain, walked on a disintegration wound
(gorewounds:fullbody/humanoid/disintegrate):
damage:<any firearm decl>.declGoreBehavior = "standard/pistol"
(63/154 raw damage archive entries carry this field, 62/151 by unique
decl name — both re-verified this round; pass 1's bare "63/154" read
as, and numerically collided with, projectileImpactEffect's unrelated
156/154 row above, see "Pass 1 errors")
goreBehavior:standard/pistol
probability.item[N].{woundType, probability} // picks a GORETYPE_*
gorewounds:<decl matching that woundType>
visuals.goreSpray.sprayFX = "FX_GORE_DISINTEGRATE" // engine enum string, not a decl ref — open, see §7
visuals.bloodPool.decalParms.item[0] = { decalMat, size={x,y}, priority } // existing decal texture + direct size
visuals.disintegrate.color/.colorScale/.pieceInitialColor/.pieceFadeColor/.pieceColorScale // inline RGB, no fx/particle indirection
spawning.spawnEntities.item[N].fxTagJoint = "fx_hips" // joint tag the spawned gore piece's own fx binds to
So gorewounds carries its own inline color/size fields for disintegration
effects — no separate fx/particle decl to edit for that specific visual;
bloodPool's decal (a literal blood-pool texture) is both size- and
texture-swappable directly.
5. Budget: does an edit fit without a 3.4 GiB archive rebuild?
Not re-measured this round — cited from the project's own prior,
reproducible measurement (docs/moddingflaeche-tnc.md §4.1,
python tools/survey_tnc.py --budget), because it already covers exactly
these two types with a real test-edit (every integer in the decl ×1000,
re-Kraken'd):
| type | csize median |
gap median | fits in csize alone |
fits in csize+gap |
|---|---|---|---|---|
fx |
852 B | 48 B | 4/25 | 22/25 |
particle |
2,633 B | 205 B | 0/25 | 22/25 |
So ~88% of realistic size/color/count/lifetime edits land inside the
existing payload slot + its zero-filled gap and patch in place with no
archive rebuild (idcl.plan_in_slot()/write_in_place(), already the
project's production writer). The ~3/25 oversized ones fall back to a
Kraken recompress (§4.3: byte-identical to retail, 401/401 and 194/194
measured) or a mode-0→2 switch (§4.2) — both already proven, not new work
needed for this track. The decl.py "vector dialect" weakness (§2.3,
2,242 decls project-wide, only material/renderParm/table/env/
json/articulatedFigure/one renderLayerDefinition/one entityDef)
does not include any of the 12 types in §1 — consistent with this
round's own 0-DeclError census.
6. PROVEN vs. INFERRED
PROVEN (this round, static/structural):
- All 1,781 decls across the 12 FX-relevant TNC types parse with
decl.py, 0DeclError(fresh census, not cited from a prior round; 1,812 incl.goreGraph/goreMannequin, also 0DeclError, §9). particle's size/color/count/lifetime fields andfx's light intensity/color/radius fields areDecl.set()-able scalars that roundtrip (parse → edit → render → reparse → value matches) cleanly.Decl.set()works at block granularity, not just scalar leaves — proven by replacingfx:weapons/handgun'seditEvents.item[8]wholesale and having the addedintensityfield read back correctly after reparse.- The weapon→ammo→projectile→impact→fx→particle chain (§4) is real, not inferred — every field and value was read directly off the live pistol decls in this round, not copied from a prior report.
- Cross-weapon material reuse inside a
particledecl'ssystemProperties. materialfield is retail-native (handgun's own muzzleflash uses a battlerifle texture), so authoring a "new" effect from only existing textures needs no new tooling, just a full-body rewrite of an existing decl's content. glass/destructiblePiece/impactParticles/impactSoundsare absent from TNC's type list (checked against the unfiltered census, not a name guess).- The
fx:weapons/shotguncross-weapon reuse (§3) — three of its ownfxevents point atsmg/battlerifleparticle decls by name, read directly off the live decl this round. - Both §8 mod recipes run cleanly through the real production code
path, not just a standalone
Decl.set()script:wolfsdk.mod.Mod. load()accepts both manifests (passesREQUIRED=('id','name'),spiel='tnc'is inSUPPORTED_GAMES), andwolfsdk.decl.apply_patch()— the exact functionmod.py'sbuild_payloads()calls — applies every"set"edit in both recipes against the live retail decl text with 0DeclError:particle:weapons/grenades/frag/frag_explosion(19,922→19,874 B),envexplosion:weapons/grenades/frag_surface_ explosion(308→309 B),fx:weapons/handgun(8,872→8,849 B),particle:weapons/handgun/handgun_muzzleflash(4,454→4,406 B). Script:re_probes/fxdecls/verify_recipes.py. - TNO:
particlepayload edits are discarded at runtime (idFile_RingBufferskip, 35 vs 0 measured — cited fromdocs/moddingflaeche-tno.md§6.1, not re-measured, no TNO runtime image exists to re-measure against); the reference to a particle name is editable in bothfx,md6Def, andbreakabledecls there. - 14 TNC cvars under
r_decal*/r_particle*areexposed=true(directly console-settable, noresourceExec -sunlock needed), exact registered names, defaults, flags and clamps (re-read fromre_probes/cvartable/cvars_tnc_v2.jsonthis round;min/maxblank means the game enforces no explicit clamp beyond the type's own range):
| cvar (exact name) | default | flags | min/max |
|---|---|---|---|
r_decalDistanceFadeMultiplier |
1.0 | FLOAT,NOCHEAT,LOCAL_SETTING,EXPOSE (0x220014) |
— |
r_decalFadeCulling |
2 | INTEGER,NOCHEAT,LOCAL_SETTING,EXPOSE (0x220012) |
— |
r_decalFilteringQuality |
2 | INTEGER,NOCHEAT,LOCAL_SETTING,EXPOSE (0x220012) |
— |
r_decalLifetimeMultiplier |
1.0 | FLOAT,NOCHEAT,LOCAL_SETTING,EXPOSE (0x220014) |
— |
r_imageAtlasDecalHeight |
8 | INTEGER,NOCHEAT,EXPOSE (0x200012) |
1–16 |
r_imageAtlasDecalWidth |
8 | INTEGER,NOCHEAT,EXPOSE (0x200012) |
1–16 |
r_particleFadeQualityMultiplier |
1.0 | FLOAT,NOCHEAT,LOCAL_SETTING,EXPOSE (0x220014) |
— |
r_particleFadeQualityMultiplierDurango |
0.8 | FLOAT,NOCHEAT,EXPOSE (0x200014) |
— |
r_particlesLightAtlasHeight |
2048 | INTEGER,NOCHEAT,LOCAL_SETTING,EXPOSE (0x220012) |
— |
r_particlesLightAtlasQuality |
1 | INTEGER,NOCHEAT,LOCAL_SETTING,EXPOSE (0x220012) |
— |
r_particlesLightAtlasWidth |
4096 | INTEGER,NOCHEAT,LOCAL_SETTING,EXPOSE (0x220012) |
— |
r_particlesMinAlpha |
16 | FLOAT,NOCHEAT,EXPOSE (0x200014) |
— |
r_particlesMinAlphaDurango |
32 | FLOAT,NOCHEAT,EXPOSE (0x200014) |
— |
r_explosivesDebug |
0 (false) | BOOL,NOCHEAT,ARCHIVE,EXPOSE (0x210011) |
— |
r_particleFadeQualityMultiplierDurango and r_particlesMinAlphaDurango
are two fully separate, independently-registered cvars (pass 1 wrote
them as one name with a "(Durango)" qualifier — fixed, see "Pass 1
errors"), not platform-conditional aliases of the base name; both are
set the same way (SetCvar in console, or resourceExec for the
exposed=false ones below), global quality/distance multipliers, not
per-effect size/color controls — a coarse, zero-risk complement to the
decl-only levers, not a substitute.
- TestFx/TestParticle/StepParticleTime/ai_startfx/ai_stopfx
console commands are registered but not exposed (exposed: false in
re_probes/cmdtable/commands.json, re-confirmed this round) — same
resourceExec <file> -s unlock the TNC console-unlock memory already
proved would be needed to reach them.
- New static data point on the central open question (below): TNC's
RTTI type-descriptor table (docs/ghidra/rtti_tnc.json, 5,783 classes)
has a C++ class for every one of 12 of these 14 decl types, and every
one is an instance of the same generic template the engine uses for
every other ordinary TEXT decl (idDeclDamage, idDeclAmmo,
idDeclWeapon, ...): idDeclInfoTemplate<VidDeclParticle>,
<VidDeclFX>, <VidDeclParticleStage>,
<VidDeclProjectileImpactEffect>, <VidDeclGoreWound>,
<VidDeclGoreBehavior>, <VidDeclGoreGraph>, <VidDeclGoreMannequin>,
<VidDeclRibbon>, <VidDeclExplosion>, <VidDeclEnvExplosion>,
<VidDeclFlare> are all present (propExplode/ebolt, the two
smallest types at 4 and 2 decls, have no matching RTTI entry — not
investigated further, possibly inlined/devirtualized for such a tiny
type). This is read-only, no game launch. It does not prove the engine
re-reads a decl's text after a mod edits it, but it is concrete
evidence against the one alternative this round could not otherwise
rule out — that TNC quietly routes particle/fx through a
TNO-style special-cased binary/precompiled path instead of the generic
decl pipeline every already-runtime-proven edit (weapon, damage, ammo —
see memory tnc-decl-edits.md) goes through.
- Negative control for the "precompiled shadow stream" hypothesis.
TNO has base/generated/decls/particle/*.bin, a 1:1 compiled mirror of
every TNO particle decl, which is the actual mechanism that discards
edits there (§6, TNO row above). Checked this round whether TNC's
install tree has anything analogous: find ... -iname '*generated*' to
depth 4 under the TNC install root finds nothing (only
base/{bink,maps,sound,_wolfsdk_tnc}, dlc/dlc_{0,1,2,3,5},
virtualtextures/maps). TNC's closest binary neighbors
(prtmeshdist 479 unique, staticParticleModel 7 unique, decalatlas
864 unique) don't 1:1-match particle's 777 unique names the way
TNO's generated/particle/*.bin count matches TNO's particle decl
count. Doesn't prove TNC loads edits, but removes the one concrete
alternative mechanism this round could check for without a game
launch.
INFERRED, not proven (needs a real game launch this round could not do):
- That TNC actually loads an edited
particle/fxdecl at runtime and the visual changes.docs/moddingflaeche-tnc.md§7 already flagged this exact gap ("particle... ob das Laden auch funktioniert: nicht geprüft" — "particle... whether loading also works: not checked"), and this round narrows it (genericidDeclInfoTemplateRTTI finding, nogenerated/*.bin-style shadow directory, both above) without closing it — this is still the single open item that decides whether §2's entire size/color/count/lifetime lever works at all, or whether TNC's particle system turns out to have the same frozen-binary-stream behavior TNO's does. The balance of static evidence now leans toward "loads like any other decl" (TNC'sparticle/fxdecls are unambiguously TEXT and parse cleanly, which TNO's binary.binstreams never were; they go through the same generic C++ decl template as already-runtime-proven types likeweapon/damage/ammo; and TNC has no TNO-style precompiled shadow tree) — but none of this is a load-path trace, only structural analogy. The in-game test below is still the only thing that actually answers it. - That
visuals.goreSpray.sprayFX = "FX_GORE_DISINTEGRATE"in agorewoundsdecl is a closed engine enum (not further editable/ listable) rather than a decl reference — read as a bare string, not traced into compiled code this round (one--stringsattempt at relatedFX_WEAPON_START_FIRE*enum strings came back "no references found" in the Ghidra xref search, likely because these are read from a jump/string table rather than a directlea+cmpcall site — not pursued further, see §7).
7. Failed hypotheses / dead ends (recorded, not repeated)
- Runtime xref corroboration for the FX event dispatcher. Tried
python tools/decomp.py --strings "FX_WEAPON_START_FIRE"hoping for a direct call-site xref the wayr4_materials.mdfound one forambientProgram(RVA0x2635db0, read at0x5B1D61). EveryFX_WEAPON_START_FIRE*variant string came back "no references found" — these enum-name strings are very likely read through an indirect table (a string→enum lookup built once at startup) rather than inlinelea+cmp, so Ghidra's static xref pass doesn't see the use site. One attempt, not repeated; recorded as a dead end rather than retried with a different enum family, since it would cost ~15s × N without a clear next string to try. - Naive explosion-decl name lookup.
envexplosion:weapons/grenades/ frag_surface_explosion'seffects.fuelCoreImpact = "grenades/ frag_explosion"is not anfxdecl name —fx:grenades/ frag_explosiondoes not exist. It is aparticledecl name with aweapons/prefix the field value itself omits (particle:weapons/grenades/frag/frag_explosionis the real target). One lookup miss, resolved immediately by checking theparticlesample list instead of guessing — recorded so a future generic envexplosion→target resolver doesn't repeat the same wrong assumption. glass/destructiblePiece/impactParticles/impactSoundsas TNC decl types. Expected by analogy with TNO'sdocs/engine-surface.mdlisting; absent from TNC's fresh, unfiltered type census (§1). Not a search failure — the full type list was printed and checked, not grepped for a guessed name.
8. Two concrete decl-only effect mods
Both use the project's existing mod.json schema
("<type>:<name>": {"set": {<path>: <value>}} or {"file": <path>},
exactly as gore_realistisch/tnc_gore_probe already do) and both stay
inside the §5 budget (scalar edits of existing fields keep the text the
same length or shorter; the one block-replace in Mod B is ~140 bytes
smaller than the original block, see §3). Both recipes below were run
this round through the real production path — wolfsdk.mod.Mod.load()
(manifest validation) then wolfsdk.decl.apply_patch() (the exact
function mod.py's build_payloads() calls) against the live retail
decl text, 0 errors on all four target assets, see §6's new PROVEN entry
and re_probes/fxdecls/verify_recipes.py. That is as far as this track
can go without a game launch — neither has been applied to an
installation or tested in-game this round.
Mod A — "Größere Explosionen" (bigger grenade explosions)
Target: particle:weapons/grenades/frag/frag_explosion (11 stages: light
flash, fireball, smoke, sparks, ...). Scale every stage's size up and
stretch the system's lifetime; grow the top-level bounds to match (else
the bigger particles risk getting frustum-culled, §2):
{
"id": "wolfsdk.fx_groessere_explosion",
"name": "Groessere Granatenexplosion",
"spiel": "tnc",
"priority": 160,
"assets": {
"particle:weapons/grenades/frag/frag_explosion": {
"set": {
"edit.maxSystemDuration": "4200",
"edit.bounds.b.b[0].x": "-24.25",
"edit.bounds.b.b[0].y": "-24.25",
"edit.bounds.b.b[0].z": "-24.25",
"edit.bounds.b.b[1].x": "24.25",
"edit.bounds.b.b[1].y": "24.25",
"edit.bounds.b.b[1].z": "24.25"
}
},
"envexplosion:weapons/grenades/frag_surface_explosion": {
"set": {
"edit.effectSpreadRadius": "9",
"edit.numEffectsSpawned": "12",
"edit.maxLifespan": "6000"
}
}
}
}
(Per-stage size.size.size[0].val0/val1 and systemProperties.
totalParticles paths exist too but differ by stage index per decl —
dump the specific decl with python re_probes/fxdecls/census.py dump
particle weapons/grenades/frag/frag_explosion first and pick the stage(s)
to scale; left out of this snippet to keep it copy-pasteable without a
stage-by-stage dump in this report.)
Mod B — "Andere Mündungsfeuerfarbe" (a differently-colored, bigger muzzle flash + light)
Target: the pistol's fx:weapons/handgun (block-replace the muzzle light
event to add intensity it doesn't have in retail, §3) and
particle:weapons/handgun/handgun_muzzleflash (scale + recolor the flash
sprite itself):
{
"id": "wolfsdk.fx_blaues_muendungsfeuer",
"name": "Blaues Muendungsfeuer (Pistole)",
"spiel": "tnc",
"priority": 160,
"assets": {
"fx:weapons/handgun": {
"set": {
"edit.editEvents.item[8]": "{\n\t\t\t\tname = \"muzzlelight\";\n\t\t\t\tgroup = \"standard\";\n\t\t\t\ttype = \"FX_LIGHT\";\n\t\t\t\tduration = 0.05;\n\t\t\t\tcolor = {\n\t\t\t\t\tx = 0.2;\n\t\t\t\t\ty = 0.4;\n\t\t\t\t\tz = 1.0;\n\t\t\t\t}\n\t\t\t\ttagNames = {\n\t\t\t\t\tnum = 1;\n\t\t\t\t\titem[0] = \"muzzle\";\n\t\t\t\t}\n\t\t\t\tstartCondition = {\n\t\t\t\t\tnum = 1;\n\t\t\t\t\titem[0] = \"FX_WEAPON_START_FIRE\";\n\t\t\t\t}\n\t\t\t\tstopCondition = {\n\t\t\t\t\tnum = 1;\n\t\t\t\t\titem[0] = \"FX_NONE\";\n\t\t\t\t}\n\t\t\t\toffset = {\n\t\t\t\t\ty = 0.0299999993;\n\t\t\t\t}\n\t\t\t\tlightParms = {\n\t\t\t\t\tradius = {\n\t\t\t\t\t\tx = 6;\n\t\t\t\t\t\ty = 6;\n\t\t\t\t\t\tz = 6;\n\t\t\t\t\t}\n\t\t\t\t\tintensity = 40;\n\t\t\t\t\tlightMtr = \"lights/gaus\";\n\t\t\t\t}\n\t\t\t}"
}
},
"particle:weapons/handgun/handgun_muzzleflash": {
"set": {
"edit.stages.item[0].size.size.size[0].val0": "0.6",
"edit.stages.item[0].size.size.size[0].val1": "0.6",
"edit.stages.item[0].colorAttributes.baseColor.baseColor[1].val0": "0.2",
"edit.stages.item[0].colorAttributes.baseColor.baseColor[1].val1": "0.2",
"edit.stages.item[0].colorAttributes.baseColor.baseColor[2].val0": "1.0",
"edit.stages.item[0].colorAttributes.baseColor.baseColor[2].val1": "1.0"
}
}
}
}
This block-replace pattern (editing edit.editEvents.item[8] wholesale
with a hand-written new block) is this round's own proven technique (§3) —
the color/lightParms.intensity/lightParms.radius values above are
exactly the values that round-tripped correctly in the test script.
9. Open questions
- Does TNC load an edited
particle/fxdecl at all? The one question that decides whether §2–§4's entire lever works (§6). Narrowed this round (genericidDeclInfoTemplateRTTI match, no precompiled shadow tree, §6) but not closed — still needs the in-game test below. sprayFX = "FX_GORE_DISINTEGRATE"-style enum strings in gorewounds — closed vocabulary or further-editable? Not traced into compiled code this round (§6/§7.1).goreGraph(21) andgoreMannequin(10) — field-censused this round (python re_probes/fxdecls/census.py fields goreGraph/goreMannequin, §1): both 0DeclError.goreGraphis a per-wound gore-hit-decal table, structurally close togorewounds' ownbloodPoolblock —edit.object.impactSprayDecals.item[N]. {maxDistance, decals.item[N].{decalMat, dripMat, depth, size.{x,y}, dripSize.{x,y}, dripOffset.{y}, fadeInEndTime}}— directly size/texture-editable scalars, same lever as §4's gore chain.goreMannequinis not an effect-size lever:edit.model(an.lwogib-piece mesh path) +edit.partsSetup.item[N].{name, poseTag, massProperties.{type,value}}— a per-enemy gib-rig/physics skeleton definition (which mesh part binds to which animation joint, with what mass), no color/size/lifetime fields of the kind this track is about.- TNO's
fx/ribbon/gorewounds-equivalent budget numbers — not re-measured this round;docs/moddingflaeche-tno.md§4 has the general method (tools/survey_modding_surface.py --budget-equivalent) but this round didn't rerun it for the FX-specific types. gore_runtime.md's open probe (surgicalset()ongoreBehavior:standard/pistol's two existingprobabilityscalars,mods/tnc_gore_probealready built) is still the right first in-game test for the gore side specifically — this round didn't duplicate or advance it, only connected it into the wider fx/particle picture.
Smallest in-game test for the maintainer
(Translated from German.)
Order, because Mod B needs the least preparation and is the most conspicuous:
- Mod B first (
wolfsdk.fx_blaues_muendungsfeuer): apply the loader with this one mod (plustnc_ersteas the base), do not let the game start automatically — start it yourself via Steam as always. Use the already-proven kit map (dlc_0,c2v1— the same one on which the 30-round magazine test and the explosive pistol ran), draw the pistol and shoot once into a wall. Expectation on success: the muzzle flash now briefly casts blue instead of orange light, and the flash itself is visibly bigger. If nothing looks different: either TNC does not load the editedfx/particledecl (the open question from section 6), or the pistol in the kit level uses a differentfx/weaponFXassignment thanweapons/handgun— then search the log forimpactEffectTable/weaponFXlines for the weapon actually equipped there. - Then Mod A (
wolfsdk.fx_groessere_explosion): throw a frag grenade (should be available in the kit level) and watch the duration/size of the fireball. - With no visible change in either case: that confirms the
open question in section 6 (TNC possibly does not load edited
particle/fxdecls at runtime) and makes it the next priority question for the next round — with the same diagnostic path asexplosivpistole_diag.md(rule out load-order/inherittraps first, only then doubt the decl itself).
Files touched this round
re_probes/fxdecls/census.py— the census/field-frequency/dump/chain tool from pass 1, reused as-is this round (types/fieldsforgoreGraph/goreMannequin, plus a standalone recount ofprojectileImpactEffect:default's blocks and thedamage/declGoreBehaviorraw-vs-unique counts, both done inline rather than as new subcommands).re_probes/fxdecls/verify_recipes.py— new this round: runs both §8 mod recipes through the real production path (wolfsdk.mod.Mod.load+wolfsdk.decl.apply_patch) against live retail decl text; writes its two scratchmod.jsonfixtures underre_probes/fxdecls/tmp/(gitignored scratch, like every other track's folder), reads the retail archives, never writes to them.re_probes/agent_reports/r6_fxdecls.md— this report, pass 2.
No file under wolfsdk/, tools/, docs/, mods/, or any other track's
re_probes/ subfolder was modified. No file under the two game install
directories or <user folder>/Saved Games was written. The find
-iname '*generated*' negative control (§6) only read directory names
under the TNC install root, nothing was written there.
Related: Research Report: R4 Materials, Research Report: R3 Newweapon, Research Report: Explosivpistole Diag, Research Report: Gore Runtime, Research Report: R2 Newassets, Research Report: R3 Newassetsv, TNC Modding Surface, TNO Modding Surface, Engine Modding Surface.