Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Latest research roundsNeueste Forschungsrunden

FX, Particle, Impact and Gore Effect Decls: Format, Reference Chains, and Two Decl-Only Mods

Partly solvedTeilweise gelöst Original page in the GitHub wikiOriginalseite im GitHub-Wiki

In plain wordsIn einfachen Worten

This report surveys the effect definitions of Wolfenstein II: The New Colossus: particles, muzzle flashes, bullet impacts, blood streaks, gore wounds and explosions. A fresh count found 1,781 definitions in 12 types, all readable, and size, colour, particle count and lifetime are plain editable values; the chain from weapon to ammo, projectile, impact, effect and particle was followed on the pistol. Two small text-only mods, bigger grenade explosions and a blue muzzle flash, passed the loader's real patch step. All new work here is static: whether the game actually uses an edited particle or effect definition is still open and was not tested in game, while in The New Order such particle edits were already measured to be ignored.

  • 1,781 definitions in 12 effect types (corrected from 1,861) all parse without error; 797 of them are particles and 265 are effect compositions.
  • Size, colour, count and lifetime edits read back correctly after editing, and whole sub-blocks can be replaced to add fields the original lacks.
  • Retail already mixes weapons: the pistol's muzzle flash uses a battle-rifle texture and the shotgun uses SMG and battle-rifle particles, so new looks can reuse existing textures.
  • Most typical edits (22 of 25) fit into the existing archive space without a rebuild, and both mods ran through the loader's real patch path with 0 errors.
  • Open: whether the game loads edited particle and effect definitions; the code structure points to yes, but nothing was tested in game, and in The New Order particle edits are proven to be ignored.

Dieser Bericht untersucht die Effekt-Definitionen von Wolfenstein II: The New Colossus: Partikel, Mündungsfeuer, Einschläge, Blutspuren, Gore-Wunden und Explosionen. Eine frische Zählung ergab 1.781 Definitionen in 12 Typen, alle lesbar, und Größe, Farbe, Partikelanzahl und Lebensdauer sind einfache, änderbare Werte; die Kette von Waffe über Munition, Projektil, Einschlag und Effekt bis zum Partikel wurde an der Pistole nachverfolgt. Zwei kleine reine Text-Mods, größere Granatenexplosionen und ein blaues Mündungsfeuer, haben den echten Patch-Schritt des Loaders bestanden. Alle neue Arbeit hier ist statisch: Ob das Spiel eine geänderte Partikel- oder Effekt-Definition wirklich verwendet, ist noch offen und wurde nicht im Spiel getestet; in The New Order wurde dagegen schon gemessen, dass solche Partikeländerungen ignoriert werden.

  • 1.781 Definitionen in 12 Effekttypen (korrigiert von 1.861) lassen sich alle fehlerfrei einlesen; 797 davon sind Partikel, 265 Effekt-Zusammenstellungen.
  • Änderungen an Größe, Farbe, Anzahl und Lebensdauer werden nach dem Bearbeiten korrekt zurückgelesen, und ganze Unterblöcke lassen sich ersetzen, um Felder hinzuzufügen, die im Original fehlen.
  • Das Originalspiel mischt schon Waffen: Das Mündungsfeuer der Pistole nutzt eine Sturmgewehr-Textur, die Schrotflinte nutzt MP- und Sturmgewehr-Partikel; neue Looks können also vorhandene Texturen wiederverwenden.
  • Die meisten typischen Änderungen (22 von 25) passen ohne Neuaufbau in den vorhandenen Archivplatz, und beide Mods liefen mit 0 Fehlern durch den echten Patch-Weg des Loaders.
  • Offen: ob das Spiel geänderte Partikel- und Effekt-Definitionen lädt; die Codestruktur spricht dafür, aber im Spiel wurde nichts getestet, und in The New Order werden Partikeländerungen nachweislich ignoriert.

Technical detailsTechnische Details

Der technische Teil ist auf Englisch – so, wie er im Wiki steht. Unterstrichene Begriffe haben eine Erklärung auf Deutsch.

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:

  1. 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 via census.py types --all: confirmed 1,781. Fixed everywhere this number appeared.
  2. projectileImpactEffect:default's per-surface table was undercounted. Pass 1 said "28 named blocks"; a fresh count via Decl.paths() on the live decl this round finds 39 top-level edit.* blocks, 35 of which literally end in Effect (the other 4 are hazardFire, hazardSteam, ricketyMetal, ricketyWood). Fixed in §4.
  3. "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 total damage entries 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.
  4. 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_particleFadeQualityMultiplierDurango and r_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 per M), 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, 0 DeclError (fresh census, not cited from a prior round; 1,812 incl. goreGraph/goreMannequin, also 0 DeclError, §9).
  • particle's size/color/count/lifetime fields and fx's light intensity/color/radius fields are Decl.set()-able scalars that roundtrip (parse → edit → render → reparse → value matches) cleanly.
  • Decl.set() works at block granularity, not just scalar leaves — proven by replacing fx:weapons/handgun's editEvents.item[8] wholesale and having the added intensity field 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 particle decl's systemProperties. material field 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/impactSounds are absent from TNC's type list (checked against the unfiltered census, not a name guess).
  • The fx:weapons/shotgun cross-weapon reuse (§3) — three of its own fx events point at smg/battlerifle particle 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 (passes REQUIRED=('id','name'), spiel='tnc' is in SUPPORTED_GAMES), and wolfsdk.decl.apply_patch() — the exact function mod.py's build_payloads() calls — applies every "set" edit in both recipes against the live retail decl text with 0 DeclError: 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: particle payload edits are discarded at runtime (idFile_RingBuffer skip, 35 vs 0 measured — cited from docs/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 both fx, md6Def, and breakable decls there.
  • 14 TNC cvars under r_decal*/r_particle* are exposed=true (directly console-settable, no resourceExec -s unlock needed), exact registered names, defaults, flags and clamps (re-read from re_probes/cvartable/cvars_tnc_v2.json this round; min/max blank 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/fx decl 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 (generic idDeclInfoTemplate RTTI finding, no generated/*.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's particle/fx decls are unambiguously TEXT and parse cleanly, which TNO's binary .bin streams never were; they go through the same generic C++ decl template as already-runtime-proven types like weapon/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 a gorewounds decl 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 --strings attempt at related FX_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 direct lea+cmp call site — not pursued further, see §7).

7. Failed hypotheses / dead ends (recorded, not repeated)

  1. 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 way r4_materials.md found one for ambientProgram (RVA 0x2635db0, read at 0x5B1D61). Every FX_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 inline lea+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.
  2. Naive explosion-decl name lookup. envexplosion:weapons/grenades/ frag_surface_explosion's effects.fuelCoreImpact = "grenades/ frag_explosion" is not an fx decl name — fx:grenades/ frag_explosion does not exist. It is a particle decl name with a weapons/ prefix the field value itself omits (particle:weapons/grenades/frag/frag_explosion is the real target). One lookup miss, resolved immediately by checking the particle sample list instead of guessing — recorded so a future generic envexplosion→target resolver doesn't repeat the same wrong assumption.
  3. glass/destructiblePiece/impactParticles/impactSounds as TNC decl types. Expected by analogy with TNO's docs/engine-surface.md listing; 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/fx decl at all? The one question that decides whether §2–§4's entire lever works (§6). Narrowed this round (generic idDeclInfoTemplate RTTI 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) and goreMannequin (10) — field-censused this round (python re_probes/fxdecls/census.py fields goreGraph/ goreMannequin, §1): both 0 DeclError. goreGraph is a per-wound gore-hit-decal table, structurally close to gorewounds' own bloodPool block — 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. goreMannequin is not an effect-size lever: edit.model (an .lwo gib-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 (surgical set() on goreBehavior:standard/pistol's two existing probability scalars, mods/tnc_gore_probe already 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:

  1. Mod B first (wolfsdk.fx_blaues_muendungsfeuer): apply the loader with this one mod (plus tnc_erste as 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 edited fx/particle decl (the open question from section 6), or the pistol in the kit level uses a different fx/weaponFX assignment than weapons/handgun — then search the log for impactEffectTable/weaponFX lines for the weapon actually equipped there.
  2. 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.
  3. With no visible change in either case: that confirms the open question in section 6 (TNC possibly does not load edited particle/fx decls at runtime) and makes it the next priority question for the next round — with the same diagnostic path as explosivpistole_diag.md (rule out load-order/inherit traps 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/fields for goreGraph/goreMannequin, plus a standalone recount of projectileImpactEffect:default's blocks and the damage/ declGoreBehavior raw-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 scratch mod.json fixtures under re_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.