Skip to content

Hot Reload Guide

Hot reload allows you to update Python code, content packs, and ECS systems without restarting the MAID server. This is invaluable during development for rapid iteration and testing.

Table of Contents


Overview

What Can Be Hot Reloaded?

Type Description Reload Command
Python Modules Any Python module in the codebase @reload module <name>
Content Packs Entire content packs with their systems @reload pack <name>
ECS Systems Individual game systems @reload system <name>
All Tracked All modules being watched @reload all

How It Works

  1. Snapshot: Before reloading, the system takes a snapshot of the current module state
  2. Unload: The old module/pack is properly unloaded
  3. Reimport: Python's importlib.reload() is called
  4. Reinitialize: Systems are re-registered with the game engine
  5. Verify: Health checks confirm the reload was successful

Limitations

  • State Loss: In-memory state is lost during reload (use persistent storage)
  • Active References: Old object references may persist in some cases
  • Database Connections: Connection pools may need to be re-established
  • Running Tasks: Async tasks from the old module continue until complete

CLI Commands

Start Server with Watch Mode

# Start server with file watching enabled
uv run maid server start --watch

# Watch specific directories
uv run maid server start --watch --watch-dir packages/maid-stdlib

# Watch with specific patterns
uv run maid server start --watch --watch-pattern "**/*.py"

Manual Reload via CLI

# Reload a specific module
uv run maid dev reload module maid_stdlib.commands.building

# Reload a content pack
uv run maid dev reload pack maid-classic-rpg

# Reload all tracked modules
uv run maid dev reload all

# List currently tracked modules
uv run maid dev reload list

CLI Reload Options

# Reload with verbose output
uv run maid dev reload module maid_stdlib.systems --verbose

# Reload without creating snapshot (faster, no rollback)
uv run maid dev reload module maid_stdlib.systems --no-snapshot

# Force reload even if checks fail
uv run maid dev reload module maid_stdlib.systems --force

# Dry run - show what would be reloaded
uv run maid dev reload module maid_stdlib.systems --dry-run

In-Game Commands

@reload

Reload modules, packs, or systems from within the game.

Syntax:

@reload <type> <target>

Types: - module - Python module by import path - pack - Content pack by name - system - ECS system by name - all - All tracked modules

Examples:

@reload module maid_stdlib.commands.building.create
@reload pack maid-classic-rpg
@reload system CombatSystem
@reload all

@reload module

Reload a Python module by its import path.

@reload module maid_stdlib.components.health

Output:

Reloading module: maid_stdlib.components.health
  Snapshot created: #3
  Module unloaded
  Module reimported
  Health checks passed
  Reload complete (125ms)

@reload pack

Reload an entire content pack.

@reload pack maid-stdlib

Output:

Reloading content pack: maid-stdlib
  Checking dependencies...
  Unloading pack...
    - Unregistered 5 systems
    - Unregistered 12 event handlers
  Reloading pack...
    - Registered 5 systems
    - Registered 12 event handlers
  Pack reload complete (450ms)

Note: Packs with dependent packs loaded cannot be reloaded. You must reload dependents first or use @reload all.

@reload system

Reload a specific ECS system.

@reload system CombatSystem

Output:

Reloading system: CombatSystem
  Removing from world...
  Reimporting module: maid_classic_rpg.systems.combat
  Re-registering system...
  System reload complete (75ms)

Reload Status

Check the status of reloadable components:

@reload status

Output:

Hot Reload Status:
  Watch Mode: Active
  Tracked Modules: 45
  Last Reload: 2 minutes ago
  Snapshots: 5 available

  Recently Modified:
    - maid_stdlib.commands.building.create (modified 30s ago)
    - maid_classic_rpg.systems.combat (modified 5m ago)


Watch Mode

Watch mode automatically reloads modules when file changes are detected.

Enabling Watch Mode

Via CLI:

uv run maid server start --watch

Via Configuration:

# .env file
MAID_DEV__WATCH_MODE=true
MAID_DEV__WATCH_DIRECTORIES=packages/
MAID_DEV__WATCH_PATTERNS=**/*.py

Watch Mode Behavior

  1. File Change Detected: inotify/fsevents detects a file save
  2. Debounce: Waits 500ms for additional changes (configurable)
  3. Module Mapping: Determines which module(s) the file belongs to
  4. Auto Reload: Triggers reload for affected modules
  5. Notification: Sends message to connected admins

Watch Configuration

# In your settings or .env

# Enable/disable watch mode
MAID_DEV__WATCH_MODE = true

# Directories to watch (comma-separated)
MAID_DEV__WATCH_DIRECTORIES = packages/,content/

# File patterns to watch
MAID_DEV__WATCH_PATTERNS = **/*.py,**/*.yaml

# Patterns to ignore
MAID_DEV__WATCH_IGNORE = **/__pycache__/**,**/*.pyc,**/test_*

# Debounce delay in milliseconds
MAID_DEV__WATCH_DEBOUNCE_MS = 500

# Auto-reload on change (false = notify only)
MAID_DEV__WATCH_AUTO_RELOAD = true

Watch Mode In-Game Control

@watch status           # Show watch mode status
@watch start            # Start watching
@watch stop             # Stop watching
@watch add <path>       # Add directory to watch
@watch remove <path>    # Remove directory from watch
@watch ignore <pattern> # Add ignore pattern

Module Tracking

The hot reload system tracks modules for reloading.

Automatic Tracking

By default, these modules are automatically tracked: - All modules in packages/maid-stdlib/ - All modules in packages/maid-classic-rpg/ - Content pack entry points

Manual Tracking

Track additional modules:

In-Game:

@reload track maid_engine.core.events
@reload untrack maid_engine.core.events
@reload list                              # List tracked modules

Programmatically:

from maid_engine.plugins.reload import get_reload_manager

manager = get_reload_manager()
manager.track_module("my_module.path")
manager.untrack_module("my_module.path")

Track Status

@reload list

Output:

Tracked Modules (45):
  maid_stdlib.commands.building.create     [modified 2m ago]
  maid_stdlib.commands.building.destroy    [clean]
  maid_stdlib.components.health            [clean]
  maid_stdlib.systems.combat               [modified 30s ago]
  ...


Rollback System

The rollback system allows reverting to previous module states.

How Rollback Works

  1. Before each reload, a snapshot is created
  2. Snapshots store serialized module state
  3. Rollback restores the previous state
  4. Limited number of snapshots are kept (default: 10)

@rollback

Rollback to the previous snapshot.

@rollback

Output:

Rolling back to snapshot #3...
  Restored: maid_stdlib.commands.building.create
  Rollback complete (95ms)

List Snapshots

@rollback list

Output:

Available Snapshots:
  #5  2024-01-15 10:35:00  maid_stdlib.commands.building.create
  #4  2024-01-15 10:30:00  maid_classic_rpg.systems.combat
  #3  2024-01-15 10:25:00  maid_stdlib.components.health
  #2  2024-01-15 10:20:00  [multiple modules]
  #1  2024-01-15 10:15:00  maid_stdlib.systems.movement

Rollback to Specific Snapshot

@rollback 3

Clear Snapshots

@rollback clear         # Clear all snapshots
@rollback clear 3       # Clear snapshots older than #3

Snapshot Configuration

# Maximum snapshots to keep
MAID_DEV__MAX_SNAPSHOTS = 10

# Snapshot storage location
MAID_DEV__SNAPSHOT_DIR = .maid/snapshots

# Auto-snapshot before reload
MAID_DEV__AUTO_SNAPSHOT = true

State Checkpointing

The state checkpoint system preserves combat and session state during hot reload operations, ensuring players in active combat don't end up in undefined states.

Overview

When a reload is triggered, the checkpoint system:

  1. Captures combat state - Active fights, targets, cooldowns, PvP flags
  2. Captures session state - Player connections, authentication, metadata
  3. Pauses combat (optional) - Prevents race conditions during reload
  4. Restores state - After successful reload, state is restored
  5. Handles failures - On reload failure, checkpoint is discarded and rollback handles module state

Using Checkpoints

Automatic (Recommended):

The checkpoint manager integrates with ReloadManager via hooks:

from maid_engine.reload import ReloadManager, CheckpointManager

# Create managers
reload_manager = ReloadManager(root_packages=["maid_engine", "maid_stdlib"])
checkpoint_manager = CheckpointManager(
    world=engine.world,
    session_manager=engine.session_manager,
    pause_combat=True,  # Pause combat during reload
)

# Register as hooks - checkpoints are created/restored automatically
reload_manager.add_async_pre_reload_hook(checkpoint_manager.pre_reload_hook)
reload_manager.add_async_post_reload_hook(checkpoint_manager.post_reload_hook)

Manual (Advanced):

from maid_engine.reload import CheckpointManager, ReloadScope

checkpoint_manager = CheckpointManager(world, session_manager)

# Before reload
checkpoint = await checkpoint_manager.create_checkpoint(
    scope=ReloadScope.MODULE,
    modules=["maid_classic_rpg.systems.combat"],
)

# ... perform reload ...

# After successful reload
await checkpoint_manager.restore_checkpoint(checkpoint)

# Or on failure
await checkpoint_manager.discard_checkpoint(checkpoint)

What Gets Checkpointed

Combat State (per entity): - in_combat - Whether entity is in active combat - target_id - Current combat target - cooldowns - Ability cooldown timers - pvp_flag_state - PvP flags, protection, kill streaks - active_effects - Status effects with remaining durations - tactical_position - Grid position for tactical combat

Session State (per session): - player_id - Associated player entity - account_id - Authenticated account - state - Connection state (PLAYING, etc.) - metadata - Session-specific data - last_activity - Activity timestamp

System State (StatefulSystem): Systems implementing the StatefulSystem protocol have their state captured:

from maid_engine.plugins.migration import StatefulSystem

class CombatQueueSystem(System, StatefulSystem):
    def __init__(self, world: World):
        super().__init__(world)
        self._pending_attacks: list[Attack] = []
        self._cooldowns: dict[UUID, float] = {}

    def capture_state(self) -> dict[str, Any]:
        """Called before reload."""
        return {
            "pending_attacks": [a.to_dict() for a in self._pending_attacks],
            "cooldowns": dict(self._cooldowns),
        }

    def restore_state(self, state: dict[str, Any]) -> None:
        """Called after reload."""
        self._pending_attacks = [
            Attack.from_dict(a) for a in state.get("pending_attacks", [])
        ]
        self._cooldowns = state.get("cooldowns", {})

Combat Pause During Reload

When pause_combat=True, the checkpoint manager:

  1. Sets world.get_data("combat_paused_for_reload") to True
  2. Combat systems should check this flag and skip processing
  3. After reload, the flag is cleared

In your combat system:

class CombatSystem(System):
    async def update(self, delta: float) -> None:
        # Skip if paused for reload
        if self.world.get_data("combat_paused_for_reload"):
            return

        # Normal combat processing...

Configuration

checkpoint_manager = CheckpointManager(
    world=engine.world,
    session_manager=engine.session_manager,
    pause_combat=True,      # Pause combat during reload (default: True)
    max_checkpoints=5,      # Keep last 5 checkpoints (default: 5)
)

Checkpoint History

Access previous checkpoints for debugging:

# Get checkpoint history (most recent first)
history = checkpoint_manager.checkpoint_history

for checkpoint in history:
    print(f"Checkpoint {checkpoint.checkpoint_id}")
    print(f"  Age: {checkpoint.age:.2f}s")
    print(f"  Combat states: {checkpoint.combat_count}")
    print(f"  Sessions: {checkpoint.session_count}")
    print(f"  In combat: {checkpoint.entities_in_combat}")

Serialization

Checkpoints can be serialized for debugging or persistence:

# Serialize
data = checkpoint.to_dict()

# Deserialize
from maid_engine.reload import StateCheckpoint
restored = StateCheckpoint.from_dict(data)

Best Practices

1. Design for Reloadability

Do:

# Store state externally
class CombatSystem(System):
    def __init__(self, world: World):
        self.world = world
        # State in world, not in system

    def get_combatants(self):
        # Query world each time
        return self.world.query(CombatComponent)

Don't:

# State stored in module/class
_combatants = {}  # Module-level state is lost!

class CombatSystem(System):
    combatants = []  # Class-level state is lost!

2. Use Lazy Initialization

_cached_data = None

def get_data():
    global _cached_data
    if _cached_data is None:
        _cached_data = load_expensive_data()
    return _cached_data

def on_reload():
    """Called during reload to reset state."""
    global _cached_data
    _cached_data = None

3. Implement Reload Hooks

# In your module
def __reload_prepare__():
    """Called before module is reloaded."""
    save_state_to_persistent_storage()

def __reload_complete__():
    """Called after module is reloaded."""
    restore_state_from_persistent_storage()

4. Test Reloads

# In tests
async def test_combat_system_reload():
    engine = create_test_engine()

    # Initial state
    system = engine.world.get_system(CombatSystem)
    initial_state = system.get_state()

    # Reload
    await engine.reload_system("CombatSystem")

    # Verify state preserved
    new_system = engine.world.get_system(CombatSystem)
    assert new_system.get_state() == initial_state

5. Use Version Checks

# For data migrations during reload
__version__ = "2.0.0"

def migrate_from_v1(old_data):
    """Migrate data from v1 format."""
    return {
        "new_field": old_data.get("old_field", "default"),
        **old_data
    }

Troubleshooting

"Module not found" Error

Problem: Module import path is incorrect.

Solution:

# Verify the import path
@reload module --verify maid_stdlib.commands.building

# Check if module is tracked
@reload list | grep building

"Cannot reload: dependent packs loaded"

Problem: Trying to reload a pack that other packs depend on.

Solution:

# Check dependents
@reload deps maid-stdlib

# Reload all packs in order
@reload all

# Or reload the dependent pack first
@reload pack maid-classic-rpg
@reload pack maid-stdlib

"State lost after reload"

Problem: Module-level state was lost.

Solution: - Store state in the World or document store - Implement __reload_prepare__ and __reload_complete__ hooks - Use the rollback system to recover

"Old code still running"

Problem: References to old objects persist.

Solution:

# Use weak references
import weakref
_handlers = weakref.WeakValueDictionary()

# Or clear registries on reload
def __reload_complete__():
    event_bus.clear_handlers(prefix="my_module")

"Watch mode not detecting changes"

Problem: File changes not triggering reload.

Solution:

# Check watch status
@watch status

# Verify directory is watched
@watch list

# Check ignore patterns
@watch ignore list

# Manual trigger
@reload module <module_name>

Performance Issues After Reload

Problem: Server slower after multiple reloads.

Solution:

# Clear accumulated state
@reload clear-cache

# Restart with fresh state (when convenient)
uv run maid server restart

# Check for memory leaks
@memory compare


See Also