DreamFXLang
Language

.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    ParticleUpdate

Writing 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:

ArgumentValue
DirectDispatchTypeOneD, TwoD, ThreeD, or Custom
DirectDispatchElementTypeNumThreads, NumThreadsNoClipping, or NumGroups
ElementCountX, ElementCountY, ElementCountZNon-negative integer fallback and/or a parameter name
OverrideGpuDispatchNumThreadstrue or false
OverrideGpuDispatchNumThreadsX, OverrideGpuDispatchNumThreadsY, OverrideGpuDispatchNumThreadsZNon-negative integer fallback and/or a parameter name
GpuDispatchForceLinear, DisablePartialParticleUpdatetrue or false
ParticleIterationStateEnabledtrue or false
ParticleIterationStateBindingParticle attribute name, or None to clear
ParticleIterationStateRangeInclusive 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 Defaults block 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.

On this page