.dfs — a system
System settings, user parameters, module calls and assignments in the six stacks, Defaults, renderers and bindings, and referencing a .dfe.
.dfs — a system
Produces one UNiagaraSystem.
Add Parent="../Base.dfs" to inherit another system's source. See
system source inheritance for merge, path, and rebuild rules.
System(Name="Effects/NS_Spark", Root="Game")
{
Settings = { ... } // system properties
Properties = { ... } // user parameters (User.*)
SystemSpawn = { ... } // system-scope stacks (L1)
SystemUpdate = { ... }
Emitter <Name> { ... } // inline
Emitter <Name> from "<path>" { ... } // copied from a .dfe, then overridden
}Settings
Schema-driven: the names are read off the live asset, so a misspelling reports the real list (DFX3020). Common ones:
Settings = {
EffectType = "/Niagara/Default/FX_Default.FX_Default";
WarmupTime = 0.0;
FixedBounds = box(-200, -200, -50, 200, 200, 300); // implies UseFixedBounds = true
UseFixedBounds = true; // the system's "Fixed Bounds" override flag (bFixedBounds)
FixedTickDelta = true; // substep at a fixed rate instead of once per frame
FixedTickDeltaTime = 0.01667; // ... of 60Hz. Changes simulation, not just smoothness
ModulePaths = ["/Niagara/Modules", "/Game/FX/Modules"];
}ModulePaths is DreamFX's own, not Niagara's: it adds search roots for resolving module short names
(L4). The engine defaults (/Niagara/Modules, /Niagara/DynamicInputs, /Niagara/Functions) stay on
the list, so declaring your own folder adds to them rather than replacing them.
Rebuilding resets omitted supported system and emitter settings to fresh-asset engine defaults, then applies the settings declared in the source. Removing a supported setting therefore clears its previous override; this reset covers the generator's mapped settings, not unrelated asset fields.
A declared FixedBounds box turns the system's override flag on, because a box under dynamic bounds
is inert. Write UseFixedBounds = false; to keep an authored box while the bounds stay dynamic; the
decompiler writes exactly that for an asset that holds a box with the flag off, so the round trip
keeps both.
Properties — user parameters
Properties = {
int SparkCount = 24 [ Group="Burst"; SortPriority=10 ];
float SparkSpeed = 450.0;
Color TintA = (1.0, 0.72, 0.25, 1.0);
Vector HitNormal = (0, 0, 1) [ Description="Impact normal from blueprint" ];
Texture2D NoiseTex = "Plugin.MoonToon:Textures/T_Noise01";
DI<SkeletalMesh> TargetMesh;
}Each becomes User.<Name>, settable from blueprint with SetNiagaraVariable*. The name is a stable
key across rebuilds (plan 4.5), so renaming one breaks every blueprint that referenced it.
Parameters can also be organized with Group("Name") { ... } scopes, matching DreamShader:
Properties = {
Group("Burst") {
int SparkCount = 24;
float SparkSpeed = 450.0;
}
Group("Look") {
Color TintA = (1.0, 0.72, 0.25, 1.0);
}
float Loose = 1.0;
}Every parameter inside a scope inherits the group name, and a SortPriority is stamped by
declaration order — step 10, one counter shared across the whole block, so groups and nesting
number their members in one continuous sequence. Nesting composes with |, Unreal's sub-category
syntax: Group("Outer") { Group("Inner") { … } } names its members Outer|Inner. An explicit
[ Group=… ] or [ SortPriority=… ] on the parameter itself wins and does not consume an auto
slot; loose top-level parameters carry neither. The keyword is case-insensitive.
Three diagnostics guard the form: DFX2027 (the name must be a quoted string), DFX2028 (it must be non-empty), and DFX2029 (no other form may open a brace inside a parameter block).
Description reaches the asset, and so do Group and SortPriority: the adapter rebuilds the
system's user parameter hierarchy (UNiagaraSystemEditorData::UserParameterHierarchy) from the
plan, so the details panel groups and orders parameters the way the source lays them out. UE 5.8's
per-variable CategoryName / EditorSortPriority metadata is deprecated engine-side — the
hierarchy is what the parameters panel actually reads. The plan is the whole truth about
organization: hand-made arrangement inside the editor does not survive a rebuild.
Data interface parameters carry their configuration, as the quoted JSON blob the exporter writes:
Properties = {
DI<RigidMeshCollisionQuery> Collide_StaticMesh = "{\"ActorTags\":[\"collider\"], … }";
DI<Curve> SizeCurve; // declared bare: a slot to fill at runtime
}Until 2026-08-12 this was declaration-only (plan 3.5, DFX5098 "feed it at runtime"). That was a deliberate scope cut and it aged badly: a collision source or a property reader is its configuration, so a rebuilt mirror default-constructed the objects the effect queries the world through — the smoke came out, and what it collided with did not. DFX5098 now means the value is the wrong shape, not that it will be ignored.
A declaration with no value is still a slot to fill at runtime, and stays one. curve { } remains
the readable spelling for a curve interface — see values.md.
Stacks
Six stacks, two at system scope and four per emitter (L1), plus a per-emitter event stack declared
with OnEvent and any number of simulation stage stacks declared with Stage (both below):
SystemSpawn SystemUpdate <- top level of the .dfs
EmitterSpawn EmitterUpdate <- inside an Emitter block
ParticleSpawn ParticleUpdateWriting order is module order. Two statement forms (L2):
ParticleUpdate = {
GravityForce(Gravity = (0, 0, -680)); // module call
Particles.Moon.Seed = 0.5; // assignment
Particles.Moon.Tint = User.TintA; // ...folds into the same Set Parameters module
SolveForcesAndVelocity(); // ...and this call ends the run
}Consecutive assignments fold into one Set Parameters module, and a module call breaks the run. That rule is what makes the round trip symmetric: one Set Parameters module exports as one block of assignments.
A stack you do not declare is left alone, and the build says what it kept (DFX5003). This matters for
SystemUpdate, which a new system gets a SystemState in — clearing every undeclared stack would
make each .dfs without an explicit SystemUpdate produce a system that never runs. To take a stack
over and empty it, declare it empty: SystemUpdate = { }.
An emitter's four stacks are not subject to this: DreamFX builds emitters with no default modules, so they start empty either way.
Module calls
ModuleName(Input = Value, Input = Value);
Spawn/Initialization/V2/InitializeParticle(...); // partial path, when the short name is ambiguous
ModuleName@1.2(...); // R7 version pin
disabled GravityForce(Gravity = (0, 0, -980)); // in the stack, not executed
Grid3D_ResampleFloat() as Grid3D_ResampleFloat003; // pin the node's name (see below)as <name> pins the function call node's name. It matters the moment anything links
Output.<node>.<value> — those links resolve by the node's display name, the engine numbers fresh
nodes by add order, and an original whose numbering carries years of editing history (a 003 whose
siblings are long deleted) would rebuild with different numbers, leaving every output link dangling
as "read before set". The decompiler emits as whenever a node's name is not simply the module
asset's; hand-written sources rarely need it. Names are unique per emitter (DFX5034).
Arguments are always named (DFX2008). Input names are normalised — Niagara's Loop Duration is
written LoopDuration.
disabled parks a module without deleting it: it stays in the stack, keeps its inputs, and does not
run. That is Niagara's own "keep it but turn it off" state. It also prefixes an assignment:
Color Particles.Color = (1, 1, 1, 1);
disabled Color Particles.Color = (1, 0, 0, 1);
float Particles.CustomValue = 2.0;Consecutive assignments fold only when their enabled state agrees. This example creates three Set
Parameters nodes; the middle node remains disabled and the two surrounding nodes remain enabled.
The decompiler preserves that state. A declaration in Defaults cannot be disabled (DFX2024), since
a default is parameter metadata rather than an executed stack node.
Static switches gate other inputs, and on a module source order is write order. An input that only exists once a switch is set has to be written after it:
EmitterState(
LifeCycleMode = Self, // gates everything below
LoopBehavior = Once,
LoopDuration = 0.15
);On a dynamic input the rule is relaxed: switches are hoisted and written first whatever order they appear in, so this works either way round.
VelocityStrength = RandomRangeFloat(
Minimum = User.Speed * 0.5,
Maximum = User.Speed,
RandomnessMode = SimulationDefaults // a switch, written before the two above
)dfx schema <Module> -Stack <Stack> prints the signature as the build sees it, static switches
included.
@1.2 records which version the source was written against, selects it on the module node, and
errors if the asset no longer exposes it (DFX3009). Selection is a graph-level operation, so it
works on any engine.
Assignments
Particles.Moon.Seed = 0.5; // first write declares the attribute (L2)
Color Particles.Color = hlsl { ... }; // a type, when the value carries none
Emitter.MyCounter = 0;The target is namespace-qualified (DFX4025). The type comes from the value; an hlsl block, a dynamic
input and an inline expression all have none, so those need the annotation (DFX4022).
OnEvent — reacting to another emitter's events
Emitter Receiver
{
ParticleSpawn = { … }
OnEvent(Source = Sparks, Event = "LocationEvent", Mode = SpawnedParticles, SpawnNumber = 2) = {
ReceiveLocationEvent(); // ordinary stack statements; runs per received event
}
}The header configures the handler, the block is a stack like any other. Source names an emitter in
this system, Event the event it generates (a GenerateLocationEvent() module sends
"LocationEvent"), and the rest are the handler's spend controls, written only when they differ from
the defaults: Mode (SpawnedParticles spawns SpawnNumber new particles per event and runs the
block on them; EveryParticle runs it on the existing ones), MaxEventsPerFrame,
UpdateAttributeInitialValues, RandomSpawnNumber with MinSpawnNumber. Source and Event are
required (DFX2025).
One OnEvent block per emitter (DFX5031): DreamFX addresses the event stack through the same rails as
the other four, and those can reach exactly one handler. An emitter that should react to two event
streams becomes two emitters.
Source is normally an emitter name. The decompiler also writes two bracketed tokens there, and a
text may carry them: `<none>`, the invalid id that means the handler reads from the emitter it
sits in, and `<unresolved GUID>`, an id no emitter in the system answers to — what a handler
carried into a copied system looks like when nothing remapped its source on the way, and several
systems then share one stale id. Both are written back verbatim, so such a text rebuilds and stays
faithful to the asset; the second is a warning (DFX5036), because a handler whose source is gone
never fires. The backticks are the lexer's rule for a name that is not bare, not a quoting choice.
An event generator usually needs RequiresPersistentIDs = true in the source emitter's Settings,
because it reads Particles.ID; without it the build reports the engine's own stack issue (DFX6003).
Stage — simulation stages
Emitter Fluid
{
Settings = { SimTarget = GPU; }
ParticleUpdate = { … }
Stage Settle = { // a bare stage: enabled, particle iteration, runs once
ScaleColor();
}
Stage Project(DataInterface = "Emitter.PressureGrid", NumIterations = 10) = {
… // ordinary stack statements; runs per grid cell, ten times
}
}A stage is a particle stack that runs after ParticleUpdate, on a GPU emitter (SimTarget = GPU;
a CPU emitter with a Stage block is DFX5033). Declaration order is run order, and each block is a
stack like any other — module calls, folded assignments, disabled, all of it.
The header holds the stage's own properties, written only when they differ from a freshly added
stage: DataInterface names the grid the stage iterates over (one thread per cell rather than per
particle) and already implies the iteration source; Iteration says it explicitly (Particles,
DataInterface, DirectSet) for the shapes that need it, such as a data-interface iteration with
nothing bound; NumIterations repeats the stage (default 1); Enabled = false parks it. Unknown
or misshapen arguments are DFX2026. Two Stage blocks with one name is DFX5032 — stages are
identified by name, and the build would otherwise quietly keep whichever stack ran last.
DataInterface is a name, and the name is the whole contract: the engine resolves the iteration
grid by name at compile time and never reads the binding's stored type (authored assets ship with
type handles that resolve to garbage in later sessions, and simulate fine). The grid itself is
usually materialized by a module's internal writes — it does not need to be a declared parameter
for the binding to work, and a misspelt name surfaces as the stage compiling against a grid that
does not exist.
NumIterations and Enabled each take a value or a parameter. The value position decides
which: a number, true or false is the literal, and anything that reads as a name is a binding.
Stage Project(NumIterations = 6, NumIterations = Emitter.OVERRIDE.SolveIterations) = { … }
Stage DebugSlice(Enabled = Grid3D_GAS_CONTROLS_SPAWN.RenderDebugSlice) = { … }Both may appear at once, as above, because the engine stores both: the number is the fallback the
binding overrides. That is worth writing rather than simplifying away — a stage whose Enabled is
bound runs when the effect says so, and a rebuild that kept only the literal flag ran it always,
which is how Ninja's debug slice came to draw over the fluid in every mirror.
A stage of a custom C++ stage class (anything that is not the engine's generic stage) stays a gap with its own header line (DFX8016).
Generic stages also preserve their dispatch and particle-filter configuration:
| Argument | Value |
|---|---|
DirectDispatchType | OneD, TwoD, ThreeD, or Custom |
DirectDispatchElementType | NumThreads, NumThreadsNoClipping, or NumGroups |
ElementCountX, ElementCountY, ElementCountZ | Non-negative integer fallback and/or a parameter name |
OverrideGpuDispatchNumThreads | true or false |
OverrideGpuDispatchNumThreadsX, OverrideGpuDispatchNumThreadsY, OverrideGpuDispatchNumThreadsZ | Non-negative integer fallback and/or a parameter name |
GpuDispatchForceLinear, DisablePartialParticleUpdate | true or false |
ParticleIterationStateEnabled | true or false |
ParticleIterationStateBinding | Particle attribute name, or None to clear |
ParticleIterationStateRange | Inclusive integer pair, such as (1, 3) |
Stage Dispatch(
Iteration = DirectSet,
DirectDispatchType = TwoD,
ElementCountX = 128, ElementCountX = Emitter.Width,
ElementCountY = 64,
OverrideGpuDispatchNumThreads = true,
OverrideGpuDispatchNumThreadsX = 8,
OverrideGpuDispatchNumThreadsY = 8
) = { ... }As with NumIterations, repeating a count with a number and a parameter preserves both halves of
the engine binding. Export writes values that differ from a fresh stage and preserves bindings.
Removing an option resets it to the fresh-stage default on the next build; old bindings do not linger.
Thread-group override parameters retain Niagara's static int type; element counts use ordinary
int. ParticleIterationStateBinding = None explicitly clears the default particle attribute binding.
Defaults — what a read produces when nothing wrote
Working since 2026-08-09. What looked like an API gap was an ordering bug: the writes were creating the very entry they then refused to fill, so defaults are now applied after the stacks with an implied Value pass. The decompiler exports a
Defaultsblock when an emitter graph carries one that differs from what a fresh build produces.
Emitter Sparks
{
Defaults = {
float Particles.MySize = 1.0; // a value
Vector Particles.Home = Engine.Owner.Position; // a binding
}
ParticleSpawn = { … }
}An assignment says this parameter is now this. A default says if nothing set it, reading it gives
this — Niagara's DefaultMode, and the difference between a read that compiles and one that does
not. Entries are assignments with a declared type (DFX4028); the value must be a literal, an enum or
another parameter, because a default cannot compute per particle (DFX4029).
Renderers
SpriteRenderer Core
{
Material = "Plugin.MoonToon:Materials/FX/M_SparkSprite";
Alignment = VelocityAligned;
FacingMode = FaceCamera;
SortMode = ViewDepth;
SubImageSize = (2, 2);
Bind SpriteSize -> Particles.SpriteSize;
Bind Color -> Particles.Color;
}Properties are schema-driven (L8): every renderer type gets its whole property set with no per-type syntax, and an unknown name reports the real list. Only the type keyword is a closed set (DFX3004).
A property that holds a list of assets — Meshes and OverrideMaterials on a mesh renderer — is
written as an array of paths:
MeshRenderer Body
{
Meshes = ["/Engine/BasicShapes/Cube"];
bOverrideMaterials = true;
OverrideMaterials = ["Plugin.MoonToon:Materials/FX/M_Chunk"];
}Mesh material overrides are disabled by default. A nonempty OverrideMaterials array needs
bOverrideMaterials = true;; otherwise DreamFX reports DFX7105 and the renderer uses the mesh's own
materials. Mesh renderers have no single Material property: writing one is an error (DFX3049).
Each element is really a struct with the asset as one field inside it; which field is found by reflection, so this works for renderer types that do not exist yet. The struct's other fields — a mesh's per-element pivot, scale and LOD range — have no syntax. An export that would have dropped one says so in the file header rather than flattening it away.
Bind is separate from property assignment because attribute bindings are not plain fields — the
binding struct caches a display name, a data-set name and source-mode flags that only its own
SetValue recomputes, so writing the serialised field would leave half a binding behind.
Other binding structures, including a Sprite or Ribbon renderer's MaterialUserParamBinding, are
carried as quoted JSON property values during export. User parameter bindings carry only their name,
such as "{\"parameter\":{\"name\":\"User.Material\"}}"; the renderer supplies the fixed parameter type.
This avoids serializing Niagara's process-local type index. A binding with an unexpected type reports
an export gap. These bindings are separate from attribute Bind statements.
Declaration order is renderer order, and there is no other addressing scheme. Reordering two renderer blocks repaints the effect.
Leaving Material out on a Sprite, Ribbon or Decal renderer applies the engine default (DFX5004).
Referencing a .dfe
Emitter Flash from "../Emitters/E_MoonFlashCard"
{
EmitterUpdate = {
EmitterState(LifeCycleMode = Self, LoopBehavior = Once, LoopDuration = 0.08);
}
}See dfe.md for what the merge does and does not do.