Skip to content

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 StatefulSystem protocol

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:

  1. Captures a snapshot of the pack's state before the operation
  2. If the operation fails, restores the snapshot
  3. Re-registers systems, commands, and event handlers
  4. Calls the pack's on_load lifecycle 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:

# Start server with file watching
maid server start --watch

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

  1. Test migrations thoroughly before deploying to production
  2. Use rollback - keep it enabled in production
  3. Watch for memory leaks - ensure old pack resources are properly cleaned up
  4. Notify players during hot reload operations that might cause brief pauses
  5. Keep packs small - smaller packs reload faster
  6. Implement StatefulSystem for systems with internal state
  7. Log hook callbacks to aid debugging

Next Steps