Skip to content

Add hot reload for live mod code reloading#92

Open
elliotttate wants to merge 5 commits intoAlchyr:developfrom
elliotttate:feature/hot-reload
Open

Add hot reload for live mod code reloading#92
elliotttate wants to merge 5 commits intoAlchyr:developfrom
elliotttate:feature/hot-reload

Conversation

@elliotttate
Copy link
Copy Markdown
Contributor

Summary

Adds a built-in hot reload system that lets modders reload their mod's code while the game is running — no restart needed. Change a card's damage, fix a relic's effect, add a new entity, rebuild, and type hotreload MyMod in the dev console. Changes take effect in seconds.

  • 12-step reload pipeline: load assembly → swap Harmony patches → replace entities in ModelDb → refresh pools → reload localization → update live scene tree nodes
  • Incremental reload: compares IL signature hashes between old and new types — unchanged entities are skipped entirely
  • Auto pool registration: BaseLib entity constructors (CustomCardModel, etc.) auto-register in pools during Activator.CreateInstance(), so no explicit pool re-registration is needed
  • Assembly stamping via BaseLib.props: Debug builds automatically get a unique assembly name suffix (e.g. MyMod_hr143052789), solving both ALC name conflicts and file locking. Mods using the BaseLib NuGet package get this for free.
  • BaseLib subsystem refresh: re-processes SavedSpireField, ModInterop, custom enums, custom characters, and custom orbs on reload
  • 73 startup self-tests verify every reflection target (ModelDb fields, ModManager, pool structures, serialization cache) on game launch so incompatible game updates are caught immediately
  • File watcher (optional): automatically reloads when a stamped DLL appears in the mods folder
  • Console commands: hotreload, hotreload_list, hotreload_status, hotreload_test
  • Transactional rollback: if entity creation fails, ModelDb, Mod.assembly references, Harmony patches, and serialization cache are all restored to their pre-reload state

Key technical findings during development

  • Godot 4.5 uses IsolatedComponentLoadContext, not AssemblyLoadContext.Default, for mod assemblies. Hot-reloaded DLLs must be loaded into the same ALC as sts2.dll or IsSubclassOf / is checks fail.
  • AbstractModel constructor throws DuplicateModelException if a canonical model already exists. Old entities must be removed from _contentById before Activator.CreateInstance().
  • MSBuild PropertyGroup stamping (not Target) is needed for assembly name changes to affect both the internal name and output filename.
  • ModManager._mods is the current field name (not _loadedMods).

What modders need to do

Nothing special if they already use the BaseLib NuGet package and have Sts2Path set in their .csproj. Build in Debug, run hotreload ModName in the console. Full guide in docs/hot_reload.md.

Files

Area Files Purpose
Core HotReload/HotReloadPipeline.cs 12-step reload pipeline
API HotReload/HotReloadEngine.cs Public Reload() / ReloadByModId() API
Result HotReload/HotReloadResult.cs Structured reload result with timings
Helpers TypeSignatureHasher.cs, AssemblyStamper.cs, SerializationCacheSnapshot.cs, HotReloadSession.cs Hash comparison, name normalization, rollback snapshots
Live refresh LiveInstanceRefresher.cs Scene tree walk + run state refresh (deck, piles, powers)
File watcher ModFileWatcher.cs FSW + polling fallback for auto-reload
Commands Commands/HotReloadCommand.cs Console commands (hotreload, hotreload_list, hotreload_status, hotreload_test)
Tests HotReloadSelfTests.cs 73 startup tests + on-demand integration tests
Build build/BaseLib.props Assembly stamping + CopyToModsFolder targets
Docs docs/hot_reload.md Full guide with quick start, troubleshooting, API reference
Modified ContentPatches, PostModInitPatch, ModelLocPatch, CustomEnums, SavedSpireFieldPatch, CustomCharacterModel, CustomOrbModel Added RemoveByAssembly() hooks and extracted reusable methods

Test plan

  • 73 startup self-tests pass (reflection targets verified on game boot)
  • Assembly stamping produces unique filenames per build
  • Stamped DLLs deploy alongside locked originals without conflict
  • Entity injection succeeds (2 entities: card + relic, 0 errors)
  • Incremental reload skips unchanged entities
  • Repeated successive reloads work
  • hotreload console command works by mod folder name
  • Code changes (damage value modification) take effect after reload

Reload mod assemblies at runtime without restarting the game. Change a card's
damage, fix a relic's effect, or add new entities — rebuild and type
`hotreload MyMod` in the dev console.

The system loads the new assembly into Godot's ALC, removes old entities from
ModelDb, creates fresh instances (which auto-register in pools via BaseLib's
constructors), reloads localization, and refreshes live card/relic/power nodes
in the scene tree. Unchanged entities are detected via IL signature hashing
and skipped for speed.

Build integration via BaseLib.props stamps each Debug build with a unique
assembly name (e.g. MyMod_hr143052789) so the ALC accepts it alongside the
previous version. The stamped filename also avoids file lock conflicts with
the running game. Mods using the BaseLib NuGet package get this automatically.

Console commands: hotreload, hotreload_list, hotreload_status, hotreload_test.
Optional file watcher for automatic reload on build (enable in BaseLib settings).
73 startup self-tests verify all reflection targets on every game launch.
- Fix CTS leak in ModFileWatcher (old CancellationTokenSource was never disposed)
- Add logging to LiveInstanceRefresher catch blocks (was silently swallowing errors)
- Expand rollback to clear ModelDb caches and pool instance caches on entity failure
- Skip types without parameterless constructors instead of crashing
- Guard against null from Activator.CreateInstance
- Protect file watcher against file deletion between debounce and read
- Warn when old assembly count exceeds 10 (memory accumulation)
- Log stale patch removal failures instead of silently ignoring
- Add 11 new integration tests: bad path/mod ID error handling, InheritsFromByName
  vs IsSubclassOf consistency, hash stability, assembly count health check,
  RemoveByAssembly safety with unknown assemblies, edge case mod names, and more
@Alchyr Alchyr deleted the branch Alchyr:develop March 31, 2026 10:31
@Alchyr Alchyr closed this Mar 31, 2026
@Alchyr Alchyr reopened this Mar 31, 2026
Resolve conflicts in BaseLibMain.cs, BaseLibConfig.cs, ContentPatches.cs,
and NodeFactory.cs. Adopt upstream's _convertingNodes HashSet tracking and
CreateFromNode rename while preserving hot reload additions (HotReloadEngine,
RemoveByAssembly, self-tests, CustomModelCounts, EnableFileWatcher config).
@elliotttate
Copy link
Copy Markdown
Contributor Author

I've been using this for a while now without any issues, but please let me know if there's anything that could be improved or some edge case that isn't working right

RunSelfTests() was called from NodeFactory.Init() which runs before
harmony.PatchAll(). The 3 tests that call scene.Instantiate() need
the SceneConversionPatch postfix to be in place. Move the call to
BaseLibMain after PatchAll() so all 17 tests can pass.
…zation

Upstream renamed Patches/Utils/ModelLocPatch.cs to Patches/Localization/ModelLocPatch.cs,
changing its namespace. Add the new using to HotReloadPipeline.cs so it compiles.
@erasels
Copy link
Copy Markdown
Contributor

erasels commented Apr 1, 2026

I'd like to know how big of an impact on performance this has and if it breaks mod interoperability. This seems like a pretty invasive change

@elliotttate
Copy link
Copy Markdown
Contributor Author

I haven't noticed any, but I have a pretty powerful PC and it needs more testing from others. It's more intended for development though than something you use for released mods.

@GraysonnG
Copy link
Copy Markdown
Contributor

GraysonnG commented Apr 6, 2026

ngl this should probably be a separate mod, live reloading is completely out of the scope of an end user. baselib is not just for developing mods and it seems like less impact if this is a modding tool for modders and not a part of the baselib every mod on relies upon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants