Hot Reload Overview¶
Hot reload allows you to update content packs at runtime without restarting the MAID server. This is invaluable during development and for live updates to production servers.
What is Hot Reload?¶
Hot reload is a mechanism that:
- Unloads the current version of a content pack (cleaning up systems, commands, event handlers)
- Loads a new version of the pack (registering updated systems, commands, events)
- Migrates component data if the component schemas have changed
- Preserves system state when systems implement the
StatefulSystemprotocol
All of this happens while the game is running, with minimal disruption to connected players.
When to Use Hot Reload¶
Hot reload is useful for:
- Development: Iterate quickly on content pack code without restarting the server
- Bug fixes: Apply urgent fixes without disconnecting players
- Content updates: Add new rooms, items, or NPCs to a live game
- A/B testing: Swap content pack versions to test different gameplay
Hot Reload States¶
A hot reload operation progresses through several states:
PENDING -> VALIDATING -> PAUSING -> UNLOADING -> LOADING -> MIGRATING -> RESUMING -> COMPLETED
└-> FAILED
└-> ROLLED_BACK
| State | Description |
|---|---|
PENDING |
Operation queued, waiting to start |
VALIDATING |
Checking dependencies and compatibility |
PAUSING |
Pausing the game tick loop |
UNLOADING |
Unloading the old pack version |
LOADING |
Loading the new pack version |
MIGRATING |
Running component data migrations |
RESUMING |
Resuming the game tick loop |
COMPLETED |
Operation finished successfully |
FAILED |
Operation failed (may be partially applied) |
ROLLED_BACK |
Operation failed and was automatically rolled back |
Core Components¶
HotReloadManager¶
The HotReloadManager is the central class for hot reload operations:
from maid_engine.plugins.hot_reload import HotReloadManager
# Access via engine
manager = engine.hot_reload
# Load a new pack
result = await manager.load_pack(my_pack)
# Unload an existing pack
result = await manager.unload_pack("my-pack-name")
# Reload a pack (unload + load)
result = await manager.reload_pack("my-pack-name", new_pack)
# Reload with state preservation
result = await manager.reload_pack_with_state("my-pack-name", new_pack)
HotReloadResult¶
Every operation returns a HotReloadResult:
@dataclass
class HotReloadResult:
success: bool # Whether the operation completed successfully
state: HotReloadState # Final state of the operation
pack_name: str # Name of the content pack
error: str | None # Error message if failed
elapsed_time: float # Time taken in seconds
migrations_run: int # Number of migrations executed
HotReloadContext¶
During operations, a HotReloadContext tracks progress:
@dataclass
class HotReloadContext:
pack_name: str # Pack being operated on
state: HotReloadState # Current state
start_time: float # When the operation started
previous_pack: ContentPack | None # Old pack (for reload)
new_pack: ContentPack | None # New pack being loaded
Basic Usage¶
Loading a Pack at Runtime¶
from my_content_pack import MyContentPack
# Create pack instance
pack = MyContentPack()
# Load it
result = await engine.hot_reload.load_pack(pack)
if result.success:
print(f"Pack loaded in {result.elapsed_time:.2f}s")
else:
print(f"Load failed: {result.error}")
Reloading a Pack¶
# Reload an existing pack with a new instance
result = await engine.hot_reload.reload_pack(
"my-content-pack",
MyContentPack() # New instance with updated code
)
if result.success:
print(f"Reloaded with {result.migrations_run} migrations")
Unloading a Pack¶
result = await engine.hot_reload.unload_pack("my-content-pack")
if not result.success:
if "depends on it" in result.error:
print("Other packs depend on this one!")
Lifecycle Hooks¶
Register callbacks to run at specific points during hot reload:
def on_pre_unload(ctx: HotReloadContext) -> None:
print(f"About to unload: {ctx.pack_name}")
# Save any external state, notify players, etc.
def on_post_load(ctx: HotReloadContext) -> None:
print(f"Finished loading: {ctx.pack_name}")
# Reinitialize caches, restore state, etc.
manager.register_hook("pre_unload", on_pre_unload)
manager.register_hook("post_load", on_post_load)
Available hook types:
| Hook | When Called |
|---|---|
pre_unload |
Before a pack is unloaded |
post_unload |
After a pack is unloaded |
pre_load |
Before a pack is loaded |
post_load |
After a pack is loaded |
pre_migrate |
Before migrations are run |
post_migrate |
After migrations complete |
Error Handling and Rollback¶
Hot reload supports automatic rollback when operations fail:
# Enable rollback (on by default)
manager.enable_rollback = True
# Attempt reload
result = await manager.reload_pack("my-pack", new_pack)
if result.state == HotReloadState.ROLLED_BACK:
print("Reload failed but original state was restored")
elif result.state == HotReloadState.FAILED:
print("Reload failed and rollback also failed!")
The rollback mechanism:
- Captures a snapshot of the pack's state before the operation
- If the operation fails, restores the snapshot
- Re-registers systems, commands, and event handlers
- Calls the pack's
on_loadlifecycle hook
Dependency Management¶
Hot reload respects content pack dependencies:
# This will fail if other packs depend on "base-pack"
result = await manager.unload_pack("base-pack")
if not result.success:
print(result.error) # "Cannot unload: other packs depend on it: ['game-pack']"
# This will fail if dependencies aren't loaded
pack_with_deps = PackThatNeedsBasePack()
result = await manager.load_pack(pack_with_deps)
if not result.success:
print(result.error) # "Missing dependency: base-pack"
File Watching for Development¶
During development, use file watching for automatic hot reload:
Or programmatically:
from maid_engine.plugins import PackFileWatcher
watcher = PackFileWatcher(engine.hot_reload, debounce_delay=0.5)
watcher.watch("my-pack", Path("/path/to/my-pack/src"))
await watcher.start()
# Now any .py file changes in that directory trigger a reload
Events¶
Hot reload emits events you can subscribe to:
from maid_engine.plugins.events import (
PackLoadingEvent,
PackLoadedEvent,
PackUnloadingEvent,
PackUnloadedEvent,
HotReloadFailedEvent,
)
@engine.world.events.subscribe(PackLoadedEvent)
async def on_pack_loaded(event: PackLoadedEvent) -> None:
print(f"Pack {event.pack_name} loaded in {event.elapsed_time:.2f}s")
if event.is_reload:
print("This was a reload, not a fresh load")
Best Practices¶
- Test migrations thoroughly before deploying to production
- Use rollback - keep it enabled in production
- Watch for memory leaks - ensure old pack resources are properly cleaned up
- Notify players during hot reload operations that might cause brief pauses
- Keep packs small - smaller packs reload faster
- Implement StatefulSystem for systems with internal state
- Log hook callbacks to aid debugging
Next Steps¶
- Development Workflow - Using hot reload during development
- Data Migrations - Migrating component data between versions