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
- CLI Commands
- In-Game Commands
- Watch Mode
- Module Tracking
- Rollback System
- State Checkpointing
- Best Practices
- Troubleshooting
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¶
- Snapshot: Before reloading, the system takes a snapshot of the current module state
- Unload: The old module/pack is properly unloaded
- Reimport: Python's
importlib.reload()is called - Reinitialize: Systems are re-registered with the game engine
- 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:
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.
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.
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.
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:
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:
Via Configuration:
# .env file
MAID_DEV__WATCH_MODE=true
MAID_DEV__WATCH_DIRECTORIES=packages/
MAID_DEV__WATCH_PATTERNS=**/*.py
Watch Mode Behavior¶
- File Change Detected: inotify/fsevents detects a file save
- Debounce: Waits 500ms for additional changes (configurable)
- Module Mapping: Determines which module(s) the file belongs to
- Auto Reload: Triggers reload for affected modules
- 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¶
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¶
- Before each reload, a snapshot is created
- Snapshots store serialized module state
- Rollback restores the previous state
- Limited number of snapshots are kept (default: 10)
@rollback¶
Rollback to the previous snapshot.
Output:
Rolling back to snapshot #3...
Restored: maid_stdlib.commands.building.create
Rollback complete (95ms)
List Snapshots¶
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¶
Clear Snapshots¶
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:
- Captures combat state - Active fights, targets, cooldowns, PvP flags
- Captures session state - Player connections, authentication, metadata
- Pauses combat (optional) - Prevents race conditions during reload
- Restores state - After successful reload, state is restored
- 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:
- Sets
world.get_data("combat_paused_for_reload")toTrue - Combat systems should check this flag and skip processing
- 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¶
- Profiling Tutorial - Measure reload performance impact
- Building Commands Reference - Related admin commands
- Admin API Reference - API reload endpoints