Editor Plugin Hot Reload
This guide covers hot reloading for editor plugins, including requirements, limitations, state preservation, and debugging tips.
Overview
Hot reload allows editor plugins to be unloaded and reloaded without restarting the editor. This enables rapid iteration during plugin development.
How Hot Reload Works
- Plugin disabled - The plugin's
Shutdown()method is called - State captured - For
IStatefulPluginimplementations,SaveState()is called - Assembly unloaded - The plugin's assembly is unloaded via
AssemblyLoadContext - Assembly reloaded - The updated assembly is loaded into a new context
- Plugin enabled -
Initialize()is called on the new instance - State restored - For
IStatefulPlugin,RestoreState()is called
Requirements for Hot-Reloadable Plugins
Manifest Configuration
Set supportsHotReload: true in your plugin manifest:
{
"id": "com.example.my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"capabilities": {
"supportsHotReload": true,
"supportsDisable": true
}
}
Resource Cleanup
Plugins must properly clean up all resources in Shutdown():
public class MyPlugin : EditorPluginBase
{
private EventSubscription? selectionSub;
public override void Initialize(IEditorContext context)
{
selectionSub = context.OnSelectionChanged(OnSelectionChanged);
}
public override void Shutdown()
{
// CRITICAL: Dispose all subscriptions
selectionSub?.Dispose();
}
}
Avoid Static State
Static fields survive assembly unload and can cause memory leaks:
// ❌ BAD: Static state prevents unload
public class BadPlugin : EditorPluginBase
{
private static List<Entity> cachedEntities = [];
}
// ✅ GOOD: Instance state is properly managed
public class GoodPlugin : EditorPluginBase
{
private readonly List<Entity> cachedEntities = [];
}
State Preservation
Implement IStatefulPlugin to preserve state across hot reloads:
public class MyPlugin : EditorPluginBase, IStatefulPlugin
{
private bool showOverlay = true;
private int selectedTabIndex = 0;
public object? SaveState()
{
// Return a simple dictionary - NOT plugin-defined types!
return new Dictionary<string, object>
{
["ShowOverlay"] = showOverlay,
["SelectedTab"] = selectedTabIndex
};
}
public void RestoreState(object? state)
{
if (state is Dictionary<string, object> dict)
{
showOverlay = dict.GetValueOrDefault("ShowOverlay") as bool? ?? true;
selectedTabIndex = dict.GetValueOrDefault("SelectedTab") as int? ?? 0;
}
}
}
State Object Guidelines
The state object must not reference types from your plugin's assembly:
| Safe Types | Unsafe Types |
|---|---|
| Primitives (int, float, string) | Plugin-defined classes |
| Collections of primitives | Plugin-defined structs |
Dictionary<string, object> |
Types referencing plugin types |
| Types from KeenEyes.Editor.Abstractions | Entity handles (stale after reload) |
Why? Types from the old assembly cannot be deserialized into the new assembly after reload.
Panel State Preservation
Panel positions and sizes are automatically preserved by the editor's PanelStateStore. No additional code is needed for basic panel layout preservation.
For custom panel state, use IStatefulPlugin:
public class MyPanelPlugin : EditorPluginBase, IStatefulPlugin
{
private MyPanel? panel;
public object? SaveState()
{
return new Dictionary<string, object>
{
["ScrollPosition"] = panel?.ScrollY ?? 0f,
["ExpandedNodes"] = panel?.GetExpandedNodeIds() ?? []
};
}
public void RestoreState(object? state)
{
// State will be applied after panel is recreated
if (state is Dictionary<string, object> dict && panel != null)
{
panel.ScrollY = dict.GetValueOrDefault("ScrollPosition") as float? ?? 0f;
var expanded = dict.GetValueOrDefault("ExpandedNodes") as List<string>;
if (expanded != null) panel.SetExpandedNodes(expanded);
}
}
}
Debugging Hot Reload Issues
Unload Diagnostics
When a plugin fails to fully unload, the editor provides diagnostics:
Plugin 'com.example.my-plugin' may not have fully unloaded.
There may be lingering references.
Potential holders:
- 2 event subscription(s)
- Panel: MyCustomPanel
- PropertyDrawer: ColorDrawer
Resource counts:
- Subscriptions: 2
- Panels: 1
- PropertyDrawers: 1
Common Causes of Unload Failure
- Undisposed subscriptions - Event handlers keeping references
- Static fields - Static collections referencing plugin types
- Closures - Lambdas capturing plugin instances
- Cached types - Reflection-based caches holding Type references
Type Cache Integration
The editor clears type caches when plugins unload. If you maintain type caches in your plugin, register a clear callback:
// In your capability implementation
public class MyCapability : IEditorCapability
{
private readonly Dictionary<Type, object> typeCache = [];
private readonly TypeCacheManager cacheManager;
public MyCapability(TypeCacheManager cacheManager)
{
this.cacheManager = cacheManager;
cacheManager.RegisterClearCallback(OnPluginUnloading);
}
private void OnPluginUnloading(string pluginId)
{
// Clear any cached types from this plugin
var keysToRemove = typeCache.Keys
.Where(t => t.Assembly.GetName().Name?.Contains(pluginId) ?? false)
.ToList();
foreach (var key in keysToRemove)
{
typeCache.Remove(key);
}
}
}
Limitations
Type Identity
After reload, types from your plugin are new types, even if they have the same name:
// Before reload: typeof(MyComponent)
// After reload: typeof(MyComponent) <- Different Type object!
// This means:
// - Type comparisons fail across reload
// - Generic type caches become invalid
// - Reflection-based registrations need refresh
Entity References
Entity handles captured before reload remain valid, but component references may need refresh:
// ❌ BAD: Stale reference after reload
private ref Position posRef; // Invalid after reload!
// ✅ GOOD: Re-fetch reference each use
private Entity trackedEntity;
// In update: ref var pos = ref world.Get<Position>(trackedEntity);
Services and Singletons
Services obtained from IEditorContext are stable across reloads, but your plugin's references to them must be re-established in Initialize().
Best Practices Checklist
- [ ] Set
supportsHotReload: truein manifest - [ ] Dispose all event subscriptions in
Shutdown() - [ ] Avoid static fields that reference plugin types
- [ ] Implement
IStatefulPluginfor UI state preservation - [ ] Use only primitive types in state objects
- [ ] Don't cache
refreturns across frames - [ ] Re-establish service references in
Initialize() - [ ] Test hot reload during development