DreamFXLang
Tooling

Editor integration

The Tools menu, the level toolbar, Content Browser actions, the Niagara editor toolbar, the toasts, the file watcher and the settings.

Editor integration

DreamFX » Editor integration

Every place DreamFX attaches itself to the Unreal editor UI: the Tools menu, the Level Editor toolbar, the Content Browser asset context menus, and the Niagara system and script editor toolbars.

Registered byDreamFXEditor at module startup, through UToolMenus::RegisterStartupCallback
ToolMenu ownerDreamFXEditor
Suppressed by-NoDreamFXEditor, and by every commandlet run
SettingsProject Settings ▸ Plugins ▸ DreamFX

Registration is idempotent — a second pass adds nothing — and is skipped while the editor is shutting down. Every entry calls the same code the commandlet calls. A menu is an entry point, never a second implementation: if Rebuild DFX works, dfx build -All works, and vice versa.

Tools menu

Tools ▸ DreamFX, extending LevelEditor.MainMenu.Tools, section DreamFX.

Entry nameLabelIconEffect
DreamFX.RebuildAllRebuild DFXIcons.RefreshQueues every .dfs / .dfe / .dfm into the watcher's build queue, exactly as if each had been saved. One summary toast when the queue drains
DreamFX.VerifyAllVerify DFXIcons.AdjustVerifies every source against its generated asset. Writes nothing. One toast: clean, or a drift count pointing at the Output Log
DreamFX.OpenWorkspaceOpen DreamFX Workspace (VSCode)Icons.OpenInExternalEditorRewrites DFX/DreamFX.code-workspace from the current source roots, then launches it

Level Editor toolbar

Extending LevelEditor.LevelEditorToolBar.AssetsToolBar, section DreamFX.

Entry nameLabelIconEffect
DreamFX.RebuildAllToolbarDFXIcons.RefreshIdentical to Rebuild DFX
DreamFX.OpenWorkspaceToolbarOpen DreamFX Workspace (VSCode)Icons.OpenInExternalEditorIdentical to Open DreamFX Workspace

Content Browser context menus

Added into the stock GetAssetActions section of the per-class asset context menu.

[!WARNING] Every DreamFX context-menu entry requires exactly one selected asset. With two or more selected the section is empty and nothing says why. Right-click a single asset.

Asset classDynamic entryWhat appears
UNiagaraSystemDreamFX.SystemAssetActionssubmenu DreamFX — System submenu
UNiagaraEmitterDreamFX.EmitterAssetActionssubmenu DreamFX — Emitter submenu
UNiagaraScriptDreamFX.ScriptAssetActionssubmenu DreamFX — Script submenu, on a script DreamFX generated only

System submenu

DreamFX.SystemActions — label DreamFX, icon Icons.Settings. Its contents depend on whether the asset carries a DreamFX provenance stamp, because a generated asset and an imported one need opposite things.

Without provenance (an asset DreamFX did not make), section DreamFX.DecompileActions, titled Decompiler:

EntryLabelIconEffect
DreamFX.ExportDfsExport .dfsIcons.SaveDecompiles to <DecompiledOutputDirectory>/<package path>.dfs and opens it. The file rebuilds a mirror under Decompiled/; the asset is never modified
DreamFX.AdoptAdopt (take over as a DFX source)Icons.Importbelow

With provenance, section DreamFX.SourceActions, titled Source:

EntryLabelIconEffect
DreamFX.OpenSourceOpen SourceIcons.OpenInExternalEditorOpens the .dfs named in the stamp
DreamFX.RebuildFromSourceRebuild from SourceIcons.RefreshQueues that one file and builds it at once — below
DreamFX.PullIntoSourceWrite Back to SourceIcons.SaveWrites the values tuned in the asset back into the literals of its .dfs — below
DreamFX.VerifyAssetVerifyIcons.AdjustChecks this asset against its source

Emitter submenu

DreamFX.EmitterActions — one entry, because a standalone emitter has no round trip: a .dfe generates no asset of its own, so there is nothing to stamp, rebuild or verify.

EntryLabelIconEffect
DreamFX.ExportDfeExport .dfeIcons.SaveCopies the emitter into a throwaway /Temp host system, reads it there, and writes <DecompiledOutputDirectory>/<package path>.dfe, named into the Decompiled/ namespace like Export .dfs

The host system is what makes this honest rather than a guess: every reader in the Niagara external edit API addresses through an owning system, so what comes back is exactly what the emitter contributes when a system uses it.

Script submenu

DreamFX.ScriptActions — label DreamFX, icon Icons.Settings. For standalone UNiagaraScript assets — the asset kind .dfm builds. Only the Source state exists, because there is no script decompiler: the submenu appears only when the asset carries a provenance stamp, so a stock module or a hand-made script gets no DreamFX entry at all, and it offers the system menu's three Source commands — Open Source, Rebuild from Source, Verify — against the .dfm recorded in the stamp.

Niagara system editor toolbar

DreamFX.SystemEditorToolbarActions, a dynamic entry in section DreamFX of AssetEditor.Niagara.ToolBar. Adds a combo button labelled DreamFX whose content is the same two-state System submenu.

[!NOTE] AssetEditor.Niagara.ToolBar is shared. FNiagaraSystemToolkit, FNiagaraSimCacheToolkit, FNiagaraParameterDefinitionsToolkit, FNiagaraParameterCollectionToolkit and FNiagaraStatelessEmitterTemplateToolkit all return the toolkit name "Niagara". So the entry first looks for a UNiagaraSystem among the objects being edited and adds nothing when there is none — opening a Sim Cache shows no DreamFX button.

Niagara script editor toolbar

DreamFX.ScriptEditorToolbarActions, a dynamic entry in section DreamFX of AssetEditor.NiagaraScriptEditor.ToolBar — the editor a standalone UNiagaraScript opens in. Adds a combo button labelled DreamFX whose content is the Script submenu, and only when the script carries a provenance stamp: the same editor opens every stock module and dynamic input, and none of them has anything in that menu.

Command semantics

Rebuild DFX

Invalidates the source-root cache (so a newly enabled plugin is picked up), then stamps every source file into the watcher's pending queue. The debounce ticker builds them modules-first, then emitters, then systems — the same order the commandlet uses, and load-bearing when a .dfm and the .dfs that calls it are queued together.

Decompiled exports are included, like any other source. See Export vs Adopt.

Rebuild DFX always rebuilds in full: it is the escape hatch from the incremental build a save uses.

The watcher also follows .dfs Parent chains and emitter from references outside the DFX roots. Only the DFX roots are watched recursively. The directory holding each root, the directory of each external reference and that directory's parent are watched shallowly — direct children only — so a root or dependency directory being renamed, removed or replaced is seen without receiving every write under the project (Saved/, Intermediate/, DerivedDataCache/). A missing reference is watched through its nearest existing ancestor, so creating the missing parent or emitter source resumes rebuilds. Directory moves, renames, removals and rescan events invalidate both the previous dependency graph and the newly discovered sources; a watched directory that was itself replaced is subscribed again. External sources trigger their in-root dependents; they do not acquire standalone build targets merely by being watched.

A source's location is its DFX root plus its path inside that root. Moving or renaming a source without changing its contents makes an ordinary build regenerate the asset and update its provenance. The same tree checked out at another absolute path — a teammate's machine, CI, the test host — is the same location, so it is not rebuilt; commands that open or rebuild from the stamp resolve the recorded absolute path first and the root-relative path second. Verify remains read-only.

ToastCondition
DreamFX: {Built} built, {Skipped} up to date.nothing failed
DreamFX build failed: {FirstError} + an Open in VSCode linkone or more failed
DreamFX found no .dfs/.dfe/.dfm sources to rebuild.no sources at all

The Open in VSCode link jumps to the first error's file, line and column. The diagnostic has carried a position all along; before this it only reached the log.

Rebuild from Source

Queues the source named in the asset's stamp into the same watcher queue a save uses, and drains the queue at once instead of after the debounce. So it is the same forced rebuild as a save, with the same toasts as Rebuild DFX, and it cannot succeed where saving the file would fail. It rebuilds in full, never incrementally, so it is also the way to get the reference answer for an asset that looks wrong after a save.

A standalone script (.dfm) whose editor is open gets one more step. The Niagara script editor edits a copy of the asset and copies it back on Apply, so a rebuild under it would not show, and the next Apply would overwrite it. The editor is closed first — its own prompt decides what happens to edits not yet applied — and reopened after the build, whether the build worked or not. Cancel on that prompt keeps the editor and skips the rebuild, with a toast saying so. A script whose editor is not open is rebuilt without opening one. System editors work on the asset itself and are left open.

Incremental build on save

A save does not clear and rewrite the whole system. Each successful build stores a plan digest in the asset's package metadata (DreamFX.PlanDigest, next to the provenance stamp), and the watcher's next build compares the new plan with it and writes only the difference: changed argument values, a stack whose modules changed, an emitter that was added — and nothing else. On a 1230-line system that is about a second for a one-value edit, against 17 s for the full rebuild on the MoonEngine build and 56 s on the stock prebuilt UE 5.8.

It is controlled by Project Settings ▸ Plugins ▸ DreamFX ▸ Workspace ▸ Incremental Build on Save (bIncrementalBuildOnSave, default on) and applies to the watcher's save-triggered rebuilds only. Rebuild DFX, Rebuild from Source and the bulk batch's Rebuild them now link always rebuild in full.

The build falls back to a full rebuild by itself whenever the digest cannot be trusted — a new asset, an asset edited outside DreamFX, an older DreamFX's build, reordered emitters — and the Output Log says which, one line per file:

incremental: 0 emitter(s) and 1 stack(s) rebuilt, 3 input(s) written, 1 reset, nothing to recompile
full rebuild (the asset changed since its last build)

The result is required to be the same asset a full rebuild of the same text produces. What each edit costs, every fallback reason and how the guarantee is tested are in Incremental builds.

Write Back to Source

The editor-side entry to dfx pull -Apply: the values an asset stores — what a slider in the Niagara editor leaves behind — go back into the literals its .dfs already declares, so the next rebuild does not overwrite them. It is in the System submenu, which both the Content Browser and the Niagara system editor toolbar show, and is offered for any system with a provenance stamp. The source is resolved from the stamp exactly as Open Source and Rebuild from Source do it.

Only the literal's own bytes are replaced, so comments, indentation and line endings stay. A value the text changed since the last write-back is left alone (the text is newer), values the text does not declare are reported and never added, and a stack whose shape the asset and the text disagree on is refused whole. The toast gives the counts — written, withheld, not declared, refused stacks, and statements added or removed — and its link opens the report file under Saved/DreamFX/Pull/. The same numbers go to the Output Log with the pull's DFX71xx diagnostics. Unsaved changes in the open asset are included: the pull reads the asset in memory.

Two settings, both under Project Settings ▸ Plugins ▸ DreamFX ▸ Workspace and both off by default:

  • Write Back Structural Edits (bPullStructuralEdits) — also add and remove statements. With it off a module the editor added or removed is reported (DFX7113) and the whole stack is left untouched; with it on it is written, but only where the correspondence between asset and text is unique (pull.md §4d). The command and the save hook below both read it.
  • Write Back to Source on Asset Save (bPullOnAssetSave) — run the same write-back whenever the author saves a generated system. Saving the asset is the trigger, the work is done on the next editor tick, never inside the save, and the toast appears only when something was written or a stack was refused.

Write-back on save starts only from the author's own save. It starts nothing for a save DreamFX makes itself: every build ends by saving the system it generated, and a pull after that would write the values the build had just applied back into the file the build had just read. The generator's save goes through one function, which opens a scope while it runs; the hook declines while the scope is open (and the package-saved event is raised synchronously inside the save, so the scope covers exactly that save). It also declines for an autosave, a cook or any procedural save, a commandlet run, an automation run, and while the editor is playing.

Loop guard. Write-back rewrites a file the source watcher is watching, and the watcher cannot tell that from the author saving: it would rebuild the asset the values had just come from. So the command tells the watcher what it wrote — FSourceWatcher::IgnoreOwnWrite(File) records a hash of the file as it stands right after the write — and the watcher drops that file's own rebuild while the file still hashes to it. Anything that depends on the file (a child .dfs that inherits from it) is still rebuilt. The rules are deliberately about the content, not about notifications:

  • the author editing the file again changes the hash, so that save rebuilds as usual, and the record is discarded;
  • several notifications for one write (the OS reports more than one, and the watcher batches them by tick) are all skipped, and none at all costs nothing — the record expires on its own after 10 seconds;
  • a rescan or a directory move says nothing about who wrote the content, so it still queues the file;
  • Rebuild from Source and Rebuild DFX queue the file directly and are never skipped.

The asset is not touched by a write-back, so its provenance stamp still names the text as it was before the write. The next full build sees the new text, finds the stamp stale, and rebuilds to the same values.

The bulk batch

Re-exporting a content pack, switching branch, or running a scripted rewrite changes many sources at once, and rebuilding each one queues a Niagara compile per system. On this project that killed the editor: 24 systems rebuilding together put 237 jobs into the compile queue.

So a batch holding more than Bulk Rebuild Threshold files (Project Settings → Plugins → DreamFX, default 8) is offered rather than built:

DreamFX: N source files changed at once. Rebuilding them all now would queue hundreds of Niagara compiles. — Rebuild them now

The files are kept, not dropped: dropping them would leave the assets stale with nothing to notice it, which is the same failure the gate exists to prevent, only quieter. If the toast expires, Tools → DreamFX → Rebuild DFX does the same thing.

An ordinary save is never gated — one file, or any batch at or under the threshold, rebuilds immediately (incrementally, with the setting on), because that path is the iteration loop. An explicit Rebuild DFX is never gated either: it was asked for.

[!NOTE] This gate was first written against a different theory — that the danger was the watcher replaying everything changed while the editor was shut down — and measurement contradicted it. RegisterDirectoryChangedCallback_Handle starts watching at registration and reports no backlog: twelve files changed with the editor closed produced nothing at all on reopen. The batch that actually hurts arrives while the editor is open. Measured with twelve files before the fix: 54 Niagara system compiles and eight compile-pool saturations. After: the toast, and nothing built until asked.

Verify DFX

Runs the generator in verify mode over every source. Nothing is written — not the asset, not the provenance stamp. .dfe files are skipped: they generate no asset, so there is nothing to compare.

ToastCondition
DreamFX: {N} source(s) verified, all assets in step.clean
DreamFX: {Drifted} of {N} source(s) out of step ({Failed} failed). See the Output Log.drift or failure

Export vs Adopt

Both decompile. They differ in what happens next, and that difference is the whole point.

Export .dfsAdopt
Writes toDecompiledOutputDirectory (default DFX/Decompiled)the real source root — <Project>/DFX/… or <Plugin>/DFX/…
Name= namesDecompiled/<original directory>/<asset> — a mirrorthe original asset
Picked up by the buildyes — saving one rebuilds its mirroryes
Modifies the assetneveryes: rebuilds it from the new source and stamps provenance
Unrepresentable featureswarns, and lists them in the filerefuses
Meaning"let me read and play with this as text""from now on this text is the only truth"

What keeps the two apart is structural, not procedural: an export cannot name the asset it came from. /Game/FX/NS_X exports to DFX/Decompiled/Game/FX/NS_X.dfs, whose Name= is Decompiled/FX/NS_X, so a build writes /Game/Decompiled/FX/NS_X and the original is untouched however the file is edited. The whole decompiled tree is therefore ordinary source — watched, built on save, linted and CI'd.

Exports written before this arrangement still name the original. They are refused with DFX8013 rather than obeyed; re-exporting replaces them.

Exporting an asset that is already a mirror is refused too — it would leave two sources claiming one asset. The bridge's decompile action reports ok: false for this refusal, a failed decompile or a failed file write. Successful responses include the written outputPath; bridge requests do not launch a text editor.

Adopt

  1. Work out the source path from the asset's mount point: /Game/FX/NS_X → <Project>/DFX/FX/NS_X.dfs, /MoonToon/FX/NS_X → <MoonToon>/DFX/FX/NS_X.dfs.
  2. Refuse if that destination already exists, including incomplete or invalid source, or another .dfs already declares the same target asset.
  3. Decompile. Refuse if anything is unrepresentable, listing what.
  4. Confirm, listing the file to write and the asset to rebuild.
  5. Check the destination again and publish a new file without replacing an existing destination, including a file created while the confirmation was open. Rebuild the asset and stamp provenance.
  6. Re-decompile the rebuilt asset and compare byte for byte.
DiagnosticCause
DFX8010the asset has features DreamFXLang cannot express
DFX8011the source destination already exists, or another source already generates this asset
DFX8012adopted, but the rebuilt asset does not re-export to the same text

Step 3 is what makes step 6 meaningful: with no known gaps, a mismatch is a real defect rather than an expected loss. DFX8012 leaves the source file in place — the asset was rebuilt from it, so the text is authoritative either way — and logs the first differing line.

Open DreamFX Workspace (VSCode)

Rewrites DFX/DreamFX.code-workspace, then launches it through VSCode → the OS default editor → Notepad. The file is fully rewritten every time, never merged: hand-added launch or tasks blocks are lost. Per-user configuration belongs in DFX/.vscode/.

{
  "folders": [
    { "name": "DreamFX Source", "path": "." },
    { "name": "Plugin: DreamFX", "path": "../Plugins/DreamFX/DFX" }
  ],
  "settings": {
    "files.associations": { "*.dfs": "dreamfxlang", "*.dfe": "dreamfxlang", "*.dfm": "dreamfxlang" }
  },
  "extensions": {
    "recommendations": ["typedreammoon.dreamfxlang-language-support"]
  }
}

The project root is always first and always "." — the workspace file lives inside it, and a stable folder identity keeps VSCode's per-folder state attached across a rewrite. A plugin root on another drive has no relative form on Windows and gets an absolute path.

dreamfxlang is the language id the DreamFXLang extension registers. The association is written whether or not the extension is installed — without it VSCode falls back to plain text, which is harmless — and the recommendation is a prompt VSCode shows once and never again if dismissed.

ToastCondition
DreamFX failed to create workspace: {Error}the file could not be written
Opened DreamFX workspace in VSCode: {Path}VSCode launched
Opened DreamFX workspace: {Path}the OS default editor launched
Opened DreamFX workspace in Notepad: {Path}Notepad launched
DreamFX could not open workspace: {Path}every launcher failed

VSCode discovery

Windows only, most specific first. The first candidate that exists on disk wins.

#Location
1–2%LOCALAPPDATA%\Programs\Microsoft VS Code\{Code.exe, bin\code.cmd}
3–4%LOCALAPPDATA%\Programs\Microsoft VS Code Insiders\{Code - Insiders.exe, bin\code-insiders.cmd}
5–6%ProgramFiles%\Microsoft VS Code\{Code.exe, bin\code.cmd}
7–8%ProgramFiles(x86)%\Microsoft VS Code\{Code.exe, bin\code.cmd}
9every PATH entry, checked for code.cmd, code.exe, Code.exe, code-insiders.cmd, Code - Insiders.exe

Opening a workspace honours Open Workspace In New Window. Opening a file always passes --reuse-window -g <path>:<line>:<col> — a new window per diagnostic jump would be unusable.

Settings

Project Settings ▸ Plugins ▸ DreamFX, stored in Config/DefaultEditor.ini.

SettingDefaultEffect
Open Workspace In New WindowtrueOnly the workspace launcher reads it
Incremental Build on Save (Workspace)trueThe watcher's save-triggered rebuilds write only what changed since the last build, and fall back to a full rebuild when they cannot. Rebuild DFX, Rebuild from Source and Rebuild them now always rebuild in full. See Incremental builds
Write Back to Source on Asset Save (Workspace)falseSaving a generated system writes its stored values back into its .dfs, as Write Back to Source does. Never for DreamFX's own saves, autosaves, cooks, commandlets or while playing. See Write Back to Source
Write Back Structural Edits (Workspace)falseLet a write-back add and remove statements as well as rewrite values; off means they are reported and the stack is left alone. Read by the command and by the save hook
Decompiled Output DirectoryDFX/DecompiledWhere Export .dfs / Export .dfe write, relative to the project directory. Ordinary source: discovered, watched and built. Files here must name an asset in the Decompiled/ namespace (DFX8013)

Scripting the same commands

UDreamFXEditorLibrary exposes every command to Python and Blueprint, forwarding to the same bodies the menus call. A Slate entry cannot be invoked from a script, and a command that only a human click can reach is a command that never gets regression tested.

import unreal
lib = unreal.DreamFXEditorLibrary
lib.rebuild_all_sources()
lib.verify_all_sources()
print(lib.write_workspace_file())          # writes, does not launch
lib.export_system(unreal.load_asset("/Game/FX/NS_Spark"))
lib.adopt_system(unreal.load_asset("/Game/FX/NS_Spark"), True)   # True = skip the confirmation

bSkipConfirmation suppresses the modal dialog only. Both refusals still apply, reported as a toast and a log line instead of a dialog — a modal with no one to click it would block the calling thread.

Disabling the integration

UnrealEditor.exe "<Project>.uproject" -NoDreamFXEditor

Parsed as a bare command-line parameter, so -NoDreamFXEditor is the only accepted spelling.

DisabledStill active
Every menu, toolbar and context-menu entry on this pageThe -run=DreamFX commandlet, which never uses any of it
The source watcher and save-to-rebuildThe parser, generator, decompiler and settings object
The generated-asset guardAssets already generated and saved on disk

A commandlet run reaches the same state by a different route: IsRunningCommandlet() returns true and the module registers nothing.

The module index

pwsh -File .skill/dfx.ps1 index

Writes DFX/.dfx-index.json: every module and dynamic input the search paths expose, with its declared stacks, category, description and input signature. The editor extension reads that file for completion and hover — it cannot ask the engine, because every dfx call boots it and nothing that happens while someone is typing may cost tens of seconds.

Two halves with very different costs. The stacks and metadata come from the asset's usage bitmask, which is free — the same list the engine's own stack UI filters by. The input signature is probed, because the asset-level schema misses inline edit conditions and static switches entirely, and a completion list with no enum-shaped inputs in it would be worse than none. Measured here: 571 modules, 2.9s without the probe, 8.1s with it.

[!WARNING] Some module graphs cannot be walked. /Niagara/Modules/Masks/ConeMask — stock engine content — makes UNiagaraGraph::ReferencesStaticVariable recurse without bound, and the stack overflow ends the process. dfx schema on that module has always done this; the index simply walks into it 45 modules in. A plugin cannot add a visited-set to engine code and a stack overflow is not worth catching, so the walk is resumable instead: the module about to be probed is recorded first, and whatever is still recorded when the next run starts is quarantined. dfx.ps1 re-runs until the walk completes, so a handful of bad graphs costs a few extra boots rather than the feature. A quarantined module keeps its name, path and stacks; only its inputs are missing, and it says so.

Flag
-Out <path>Write somewhere other than DFX/.dfx-index.json
-NoInputsSkip the probe. Fast, and enough to answer "what exists"
-RetryClear the quarantine and try every module again — for after an engine upgrade

The index records the engine path and the enabled plugin list, because those are what invalidate it: a different engine is a different module set, and enabling a content plugin adds a whole family. Nothing compares them yet — that needs a live editor to compare against — so rebuilding is a command rather than something guessed at.

See also

  • Getting started — the editor-side workflow in order
  • Language reference — what the exported text means
  • Incremental builds — what a save writes, and when it falls back to a full rebuild
  • pull — the write-back behind Write Back to Source: what it may touch and what it refuses
  • Asset comparison and export — comparison failures and export coverage limits
  • Diagnostics — every DFXnnnn, including DFX8010–DFX8018, the gap codes that say what a decompile could not carry
  • dfx.ps1 coverage — the same question asked of a whole content tree, bucketed by feature

On this page