Runtime Hot Reload Reference
SkillFiles & storageRuntime hot reload, FileSystemWatcher rebuilds the Gum element tree when .gumx/.gusx/.gucx/.gutx/.fnt files change. Triggers: GumHotReloadManager, IGumHotReloadManager, GumService.EnableHotReload, debounce, font cache eviction during reload.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Runtime Hot Reload Reference skill
What this skill tells your AI
The instructions your AI receives, as published by vchelaru/gum in .claude/skills/gum-runtime-hot-reload/SKILL.md and read by ahel’s review.
What It Is
Hot reload lets a running game pick up changes saved in the Gum tool without restarting. GumService.EnableHotReload(absoluteGumxSourcePath) starts a FileSystemWatcher on the source project directory; file changes trigger a debounced rebuild of GumService.Default.Root's children from freshly loaded save data.
User-facing docs: docs/code/hot-reload.md. User docs are the source of truth for the public API; keep them in sync when behavior changes.
Key Files
| File | Purpose |
|---|---|
MonoGameGum/GumHotReloadManager.cs | IGumHotReloadManager + GumHotReloadManager |
MonoGameGum/GumService.cs | EnableHotReload, per-frame Update, Uninitialize stop |
docs/code/hot-reload.md | Public documentation |
Platform Gating
The entire file is wrapped in #if !IOS && !ANDROID. The EnableHotReload method on GumService is likewise gated. File compiles for MonoGame, KNI, FNA (under XNALIKE) and Raylib — namespace switches via #if. Any new API surface must respect both gates.
Source vs Bin Paths — Critical Distinction
The watched path is the source .gumx (the file the Gum tool edits), not the bin/Content copy. Two directories matter:
_projectSourcePath/sourceDirectory— where files change, whereFontCache/is read from_binGumDirectory— snapshot ofFileManager.RelativeDirectoryatStart(); where fonts are copied to and where the runtime loads from
During PerformReload, animations are reloaded by enumerating *Animations.ganx under the source directory through a LooseFileGumFileProvider rooted there (via GumService.LoadAnimationsFromProvider). FileManager.RelativeDirectory is not swapped — the provider's root is the source directory. (This replaced an older approach that swapped RelativeDirectory and probed FileManager.FileExists per element.) If you add asset-resolving logic to the reload path, root it at the source directory yourself rather than relying on a global-state swap.
Reload Pipeline
FileSystemWatcher event
→ HandleFileChange filters by extension (.gumx/.gucx/.gusx/.gutx/.fnt/.ganx)
→ sets _pendingReload + _lastChangeTime
→ (.fnt paths also appended to _changedFontFiles under _fontFileLock)
GumService.Update → _hotReloadManager.Update(Root)
→ if _pendingReload && 200ms elapsed since last change → PerformReload
The 200 ms debounce coalesces the Gum tool's multi-file save burst into one reload. Don't shorten it without testing against a real tool save — partial saves will otherwise rebuild against an inconsistent on-disk state.
PerformReload — What Actually Happens
CopyAndUnloadChangedFonts()— copies changed.fntfiles plus matching<basename>*.pngtexture pages from sourceFontCache/to binFontCache/, thenLoaderManager.Disposes both so they reload from disk.GumProjectSave.Load+Initialize; swap intoObjectFinder.Self.GumProjectSave.- Reload animations: enumerate
*Animations.ganxunder the source directory via aLooseFileGumFileProviderand load each throughGumService.LoadAnimationsFromProviderinto the new project (noRelativeDirectoryswap — the provider is rooted at the source dir). ApplyDiff(roots, newProject, SystemManagers.Default)— applies the in-place reconciliation walk described below.- Fire
ReloadCompleted.
ApplyDiff — In-Place Reconciliation Walk
Defined on GumHotReloadManager and exposed public static for direct test use. Walks every root and, at each visual whose ElementSave.Name matches an element in the new project:
- Re-point
element.ElementSaveto the new project's element. - Structural diff against the new
Instanceslist (DiffDesignTimeChildren):- Partition the visual's children into design-time (those with
Tag is InstanceSave) and everything else (runtime-added or Tag-cleared). - For each existing design-time child not present in the new
Instances:Parent = null+RemoveFromManagers(). - For each new
InstanceSave:- If a matching design-time child exists, compare its visual's
ElementSave.Nameagainst the newBaseType. Mismatch → remove+recreate (retype). Match → refresh theTagto point at the newInstanceSaveinstance. - If a same-named non-design-time child exists, leave it alone (runtime owns that slot). Do not create a duplicate.
- Otherwise call
instance.ToGraphicalUiElement(systemManagers)and attach viaParent = parent+ElementGueContainingThis = parent.
- If a matching design-time child exists, compare its visual's
ReorderDesignTimeChildrenwalks the design-time slots inChildrenandMoves items so the design-time subsequence matchesnewEs.Instancesorder. Non-design-time children keep their slots.
- Partition the visual's children into design-time (those with
SetVariablesRecursively(newEs, newEs.DefaultState)— re-applies the new default-state values. Qualified-name variables (MyInstance.X,MyInstance.Parent, etc.) flow into the children byName, which also handles reparenting and animates new instances into position.- Recurse into runtime-added children only. Design-time children are skipped — their variables were already set via the parent's qualified-name walk.
Non-Obvious Behaviors / Gotchas
Tagis the design-time marker.InstanceSave.ToGraphicalUiElementsetsTag = instanceSave(GumRuntime/InstanceSaveExtensionMethods.GumRuntime.cs:39). If user code nulls or replaces thatTag, the diff treats the visual as runtime-owned: not removed, not retyped, not duplicated. Variable application viaNamestill works. Documented limitation, seedocs/code/hot-reload.md.- Retype detection uses
ElementSave.Name, not the oldTag.BaseType. This is the visual's actual built-from type, which stays stable across diffs whether the caller passed a fresh project or mutated the existing one in place — important for tests. Root.Childrenand any roots passed toUpdate(IEnumerable<GraphicalUiElement>)are the reload surface.PopupRoot/ModalRootare untouched.- Runtime state on design-time visuals is overwritten by
SetVariablesRecursively. Games that mutate UI in code need to rerun that logic onReloadCompleted. - Children added with no
Tag(or with aTagthat isn't anInstanceSave) are runtime-owned and preserved. This includes ItemsControl-generated rows and anything the user constructed programmatically. - Reparenting flows through the qualified
<instance>.Parentvariable — there is no explicit reparent step in the diff. The parent state'sFoo.Parent = "Bar"line is what re-attachesFoounderBarduringSetVariablesRecursively. This is why both old and new parent visuals must exist before that step runs. - Textures (non-font
.png) and.ganxare watched but not reloaded in the cache-eviction sense..ganxis only re-read by enumerating the source directory into the new project'sElementAnimations(viaGumService.LoadAnimationsFromProvider);.pngedits require a restart. _changedFontFilesis mutated off the game thread (watcher callback). It's protected by_fontFileLock; any new shared state added must be similarly synchronized.Stop()is idempotent-safe but does not reset other state —GumService.Uninitializejust nulls the manager reference afterward.- Font cache path is built with
FileManager.Standardize(..., preserveCase: true, makeAbsolute: true). The loader's cache keys are case-preserving absolute paths; any eviction added must match that exact shape or it will silently miss.
Extending
- For new watched extensions: add to
HandleFileChange's extension check. If the asset type has a cache, add eviction toPerformReload(follow the font pattern — copy source→bin, thenLoaderManager.Disposethe standardized path). - Prefer injecting a custom
IGumHotReloadManagerrather than adding game-specific logic toGumHotReloadManager. (CurrentlyEnableHotReloadhardcodesnew GumHotReloadManager()— if a test/custom manager seam is needed, add an overload accepting anIGumHotReloadManager.) - Subscribe to
ReloadCompletedfrom game code to reapply runtime state after a rebuild.
Signals
- GitHub stars
- 620
- Forks
- 80
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
gum-runtime-hot-reload- Source
- github.com/vchelaru/gum