Gum gum-runtime-hot-reload
Reference guide for runtime hot reload — the FileSystemWatcher-based system that rebuilds the Gum element tree at runtime when .gumx/.gusx/.gucx/.gutx/.fnt files change on disk. Load this when working on GumHotReloadManager, IGumHotReloadManager, GumService.EnableHotReload, the hot reload debounce, or font cache eviction during reload.
install
source · Clone the upstream repo
git clone https://github.com/vchelaru/Gum
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/vchelaru/Gum "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/gum-runtime-hot-reload" ~/.claude/skills/vchelaru-gum-gum-runtime-hot-reload && rm -rf "$T"
manifest:
.claude/skills/gum-runtime-hot-reload/SKILL.mdsource content
Runtime Hot Reload Reference
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 |
|---|---|
| + |
| , per-frame , stop |
| 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
— where files change, wheresourceDirectory
is read fromFontCache/
— snapshot of_binGumDirectory
atFileManager.RelativeDirectory
; where fonts are copied to and where the runtime loads fromStart()
During
PerformReload, FileManager.RelativeDirectory is temporarily swapped to the source directory so TryLoadAnimation resolves .ganx files against the live source tree, then restored. If you add any asset-resolving logic to the reload path, respect this swap — or you will read from the wrong directory.
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
— copies changedCopyAndUnloadChangedFonts()
files plus matching.fnt
texture pages from source<basename>*.png
to binFontCache/
, thenFontCache/
s both so they reload from disk.LoaderManager.Dispose
+GumProjectSave.Load
; swap intoInitialize
.ObjectFinder.Self.GumProjectSave- Temporarily point
at the source directory, callFileManager.RelativeDirectory
for every element, restore.GumService.TryLoadAnimation - Snapshot
, callroot.Children[*].ElementSave.Name
+ null parent on each.RemoveFromManagers() - Look up each snapshotted name in the new project and
to rebuild in original order.ToGraphicalUiElement(..., addToManagers: false) - Fire
.ReloadCompleted
Non-Obvious Behaviors / Gotchas
- Only
are rebuilt.Root.Children
andPopupRoot
are untouched. Anything the game attached elsewhere will not be refreshed.ModalRoot - Runtime state is lost. Every rebuilt element comes back with Gum-project values only — code-set properties (
,Text
, etc.) disappear. Games that populate UI in code must rerun that logic onWidth
.ReloadCompleted - Children added with no
(pure runtime instances withElementSave
) are silently dropped — the snapshot stores their name slot but the lookup fails.name == null - Textures (non-font
) and.png
are watched but not reloaded in the cache-eviction sense..ganx
is only re-read via.ganx
on the new project;TryLoadAnimation
edits require a restart..png
is mutated off the game thread (watcher callback). It's protected by_changedFontFiles
; any new shared state added must be similarly synchronized._fontFileLock
is idempotent-safe but does not reset other state —Stop()
just nulls the manager reference afterward.GumService.Uninitialize- Font cache path is built with
. The loader's cache keys are case-preserving absolute paths; any eviction added must match that exact shape or it will silently miss.FileManager.Standardize(..., preserveCase: true, makeAbsolute: true)
Extending
- For new watched extensions: add to
's extension check. If the asset type has a cache, add eviction toHandleFileChange
(follow the font pattern — copy source→bin, thenPerformReload
the standardized path).LoaderManager.Dispose - Prefer injecting a custom
rather than adding game-specific logic toIGumHotReloadManager
. (CurrentlyGumHotReloadManager
hardcodesEnableHotReload
— if a test/custom manager seam is needed, add an overload accepting annew GumHotReloadManager()
.)IGumHotReloadManager - Subscribe to
from game code to reapply runtime state after a rebuild.ReloadCompleted