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 by | DreamFXEditor at module startup, through UToolMenus::RegisterStartupCallback |
| ToolMenu owner | DreamFXEditor |
| Suppressed by | -NoDreamFXEditor, and by every commandlet run |
| Settings | Project 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 name | Label | Icon | Effect |
|---|---|---|---|
DreamFX.RebuildAll | Rebuild DFX | Icons.Refresh | Queues 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.VerifyAll | Verify DFX | Icons.Adjust | Verifies every source against its generated asset. Writes nothing. One toast: clean, or a drift count pointing at the Output Log |
DreamFX.OpenWorkspace | Open DreamFX Workspace (VSCode) | Icons.OpenInExternalEditor | Rewrites DFX/DreamFX.code-workspace from the current source roots, then launches it |
Level Editor toolbar
Extending LevelEditor.LevelEditorToolBar.AssetsToolBar, section DreamFX.
| Entry name | Label | Icon | Effect |
|---|---|---|---|
DreamFX.RebuildAllToolbar | DFX | Icons.Refresh | Identical to Rebuild DFX |
DreamFX.OpenWorkspaceToolbar | Open DreamFX Workspace (VSCode) | Icons.OpenInExternalEditor | Identical 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 class | Dynamic entry | What appears |
|---|---|---|
UNiagaraSystem | DreamFX.SystemAssetActions | submenu DreamFX — System submenu |
UNiagaraEmitter | DreamFX.EmitterAssetActions | submenu DreamFX — Emitter submenu |
UNiagaraScript | DreamFX.ScriptAssetActions | submenu 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:
| Entry | Label | Icon | Effect |
|---|---|---|---|
DreamFX.ExportDfs | Export .dfs | Icons.Save | Decompiles to <DecompiledOutputDirectory>/<package path>.dfs and opens it. The file rebuilds a mirror under Decompiled/; the asset is never modified |
DreamFX.Adopt | Adopt (take over as a DFX source) | Icons.Import | below |
With provenance, section DreamFX.SourceActions, titled Source:
| Entry | Label | Icon | Effect |
|---|---|---|---|
DreamFX.OpenSource | Open Source | Icons.OpenInExternalEditor | Opens the .dfs named in the stamp |
DreamFX.RebuildFromSource | Rebuild from Source | Icons.Refresh | Queues that one file and builds it at once — below |
DreamFX.PullIntoSource | Write Back to Source | Icons.Save | Writes the values tuned in the asset back into the literals of its .dfs — below |
DreamFX.VerifyAsset | Verify | Icons.Adjust | Checks 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.
| Entry | Label | Icon | Effect |
|---|---|---|---|
DreamFX.ExportDfe | Export .dfe | Icons.Save | Copies 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.ToolBaris shared.FNiagaraSystemToolkit,FNiagaraSimCacheToolkit,FNiagaraParameterDefinitionsToolkit,FNiagaraParameterCollectionToolkitandFNiagaraStatelessEmitterTemplateToolkitall return the toolkit name"Niagara". So the entry first looks for aUNiagaraSystemamong 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.
| Toast | Condition |
|---|---|
DreamFX: {Built} built, {Skipped} up to date. | nothing failed |
DreamFX build failed: {FirstError} + an Open in VSCode link | one 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_Handlestarts 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.
| Toast | Condition |
|---|---|
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 .dfs | Adopt | |
|---|---|---|
| Writes to | DecompiledOutputDirectory (default DFX/Decompiled) | the real source root — <Project>/DFX/… or <Plugin>/DFX/… |
Name= names | Decompiled/<original directory>/<asset> — a mirror | the original asset |
| Picked up by the build | yes — saving one rebuilds its mirror | yes |
| Modifies the asset | never | yes: rebuilds it from the new source and stamps provenance |
| Unrepresentable features | warns, and lists them in the file | refuses |
| 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
- 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. - Refuse if that destination already exists, including incomplete or invalid source, or another
.dfsalready declares the same target asset. - Decompile. Refuse if anything is unrepresentable, listing what.
- Confirm, listing the file to write and the asset to rebuild.
- 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.
- Re-decompile the rebuilt asset and compare byte for byte.
| Diagnostic | Cause |
|---|---|
DFX8010 | the asset has features DreamFXLang cannot express |
DFX8011 | the source destination already exists, or another source already generates this asset |
DFX8012 | adopted, 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.
| Toast | Condition |
|---|---|
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} |
| 9 | every 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.
| Setting | Default | Effect |
|---|---|---|
| Open Workspace In New Window | true | Only the workspace launcher reads it |
| Incremental Build on Save (Workspace) | true | The 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) | false | Saving 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) | false | Let 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 Directory | DFX/Decompiled | Where 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 confirmationbSkipConfirmation 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" -NoDreamFXEditorParsed as a bare command-line parameter, so -NoDreamFXEditor is the only accepted spelling.
| Disabled | Still active |
|---|---|
| Every menu, toolbar and context-menu entry on this page | The -run=DreamFX commandlet, which never uses any of it |
| The source watcher and save-to-rebuild | The parser, generator, decompiler and settings object |
| The generated-asset guard | Assets 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 indexWrites 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 — makesUNiagaraGraph::ReferencesStaticVariablerecurse without bound, and the stack overflow ends the process.dfx schemaon 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.ps1re-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 |
-NoInputs | Skip the probe. Fast, and enough to answer "what exists" |
-Retry | Clear 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, includingDFX8010–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