Skip to content

Hot Reload for Development

Hot reload dramatically speeds up the development cycle by allowing you to see changes instantly without restarting the server. This guide covers development workflows and best practices.

Quick Start

Start the server with file watching enabled:

maid server start --watch --debug

The --watch flag enables automatic hot reload when source files change. The --debug flag provides detailed logging.

What Gets Watched

By default, the file watcher monitors:

  • Python files (.py) in loaded content pack source directories
  • Changes are debounced (default 0.5 seconds) to handle rapid saves

Customizing the Watch

# Custom debounce delay
maid server start --watch --watch-debounce 1.0

# Load specific packs to watch
maid server start --watch --content my-pack --content other-pack

Development Workflow

1. Initial Setup

Structure your content pack for easy development:

my-content-pack/
├── src/
│   └── my_content_pack/
│       ├── __init__.py
│       ├── pack.py           # ContentPack implementation
│       ├── systems/
│       │   ├── __init__.py
│       │   └── combat.py
│       ├── commands/
│       │   ├── __init__.py
│       │   └── attack.py
│       └── components/
│           ├── __init__.py
│           └── health.py
├── tests/
│   └── test_pack.py
└── pyproject.toml

2. Start Development Server

# In your MAID project directory
maid server start --watch --debug --content my-content-pack

3. Edit and Save

Make changes to your pack's code. When you save a file:

  1. The file watcher detects the change
  2. Hot reload is triggered for the pack
  3. Old systems, commands, and handlers are unregistered
  4. New versions are registered
  5. Migrations run if needed

You'll see output like:

[INFO] File changed: combat.py
[INFO] Triggering reload for pack: my-content-pack
[INFO] Successfully reloaded pack 'my-content-pack' in 0.125s

4. Test in Real-Time

Connect to your development server and test changes immediately:

telnet localhost 4000

Or use a MUD client pointed at localhost:4000.

Manual Hot Reload

You can trigger hot reload manually via CLI or API:

CLI

# Reload a specific module
maid dev reload my_content_pack.systems.combat --type module

# Reload an entire pack
maid dev reload my-content-pack --type pack

# Watch mode with manual control
maid dev reload my-content-pack --watch --watch-path src/

Programmatic

# In a dev shell or script
import asyncio
from maid_engine.core.engine import GameEngine
from maid_engine.config import get_settings
from my_content_pack import MyContentPack

async def reload_pack():
    settings = get_settings()
    engine = GameEngine(settings)

    # Load initial pack
    engine.load_content_pack(MyContentPack())
    await engine.start()

    # Later, reload with updated code
    # (after reimporting the module)
    import importlib
    import my_content_pack
    importlib.reload(my_content_pack)

    result = await engine.hot_reload.reload_pack(
        "my-content-pack",
        my_content_pack.MyContentPack()
    )
    print(f"Reload: {result}")

asyncio.run(reload_pack())

Interactive Development Shell

Use the development shell for interactive testing:

maid dev shell

This gives you an IPython shell with MAID loaded:

# In the shell
from maid_engine.core.engine import GameEngine
from maid_engine.config import get_settings
from my_content_pack import MyContentPack

settings = get_settings()
engine = GameEngine(settings)
engine.load_content_pack(MyContentPack())

# Start the engine
import asyncio
asyncio.get_event_loop().run_until_complete(engine.start())

# Test commands, systems, etc.
world = engine.world
entity = world.create_entity()

# Reload after making changes
result = asyncio.get_event_loop().run_until_complete(
    engine.hot_reload.reload_pack("my-content-pack", MyContentPack())
)

Debugging Hot Reload Issues

Enable Verbose Logging

# Via environment variable
MAID_LOG_LEVEL=DEBUG maid server start --watch

# Or in your pack's settings
settings.log_level = "DEBUG"

Common Issues

Pack Not Reloading

Check that:

  1. The file watcher is running (Watching: my-pack at /path/to/src)
  2. You're saving files in the watched directory
  3. The file has a .py extension
# Verify what's being watched
watcher = engine.hot_reload._file_watcher  # If accessible
print(watcher.watched_packs)

Import Errors After Reload

If you see import errors, the module cache may be stale:

import importlib
import sys

# Clear cached modules before reload
modules_to_clear = [
    key for key in sys.modules
    if key.startswith("my_content_pack")
]
for mod in modules_to_clear:
    del sys.modules[mod]

# Now reload
result = await engine.hot_reload.reload_pack("my-pack", MyContentPack())

State Lost After Reload

If your system loses state, implement StatefulSystem:

from maid_engine.plugins.migration import StatefulSystem

class CombatSystem(System):
    priority = 50

    def __init__(self, world: World) -> None:
        super().__init__(world)
        self._active_combats: dict[UUID, Combat] = {}
        self._cooldowns: dict[UUID, float] = {}

    def capture_state(self) -> dict[str, Any]:
        """Capture state before hot reload."""
        return {
            "active_combats": {
                str(k): v.to_dict()
                for k, v in self._active_combats.items()
            },
            "cooldowns": {
                str(k): v
                for k, v in self._cooldowns.items()
            },
        }

    def restore_state(self, state: dict[str, Any]) -> None:
        """Restore state after hot reload."""
        self._active_combats = {
            UUID(k): Combat.from_dict(v)
            for k, v in state.get("active_combats", {}).items()
        }
        self._cooldowns = {
            UUID(k): v
            for k, v in state.get("cooldowns", {}).items()
        }

    async def update(self, delta: float) -> None:
        # Normal system logic
        pass

Hot Reload Hooks for Development

Use hooks to add development-specific behavior:

def dev_pre_reload(ctx: HotReloadContext) -> None:
    """Called before reload - save dev state."""
    print(f"\n{'='*50}")
    print(f"Reloading: {ctx.pack_name}")
    print(f"{'='*50}")

def dev_post_reload(ctx: HotReloadContext) -> None:
    """Called after reload - report status."""
    print(f"Reload completed in {ctx.elapsed_time:.3f}s")
    print(f"{'='*50}\n")

# Register development hooks
if settings.debug:
    engine.hot_reload.register_hook("pre_load", dev_pre_reload)
    engine.hot_reload.register_hook("post_load", dev_post_reload)

Testing During Development

Run Tests Without Restarting

# In another terminal while server is running
uv run pytest packages/my-content-pack/tests/ -v

# Or run specific tests
uv run pytest packages/my-content-pack/tests/test_combat.py -v

Test Pack Protocol Compliance

maid plugin compliance my-content-pack

Test Pack Lifecycle

maid plugin test my-content-pack --ticks 10 --verbose

Performance Considerations

Development vs Production

In development:

  • Enable verbose logging
  • Use shorter debounce delays (0.3-0.5s)
  • Keep rollback enabled

In production:

  • Minimize logging
  • Use longer debounce delays (1-2s)
  • Plan reload windows during low activity

Measuring Reload Time

# Get reload history
history = engine.hot_reload.get_reload_history()

for result in history:
    print(f"{result.pack_name}: {result.elapsed_time:.3f}s")
    if result.migrations_run > 0:
        print(f"  Migrations: {result.migrations_run}")

Optimizing Reload Speed

  1. Keep packs focused - Smaller packs reload faster
  2. Minimize on_load work - Defer expensive initialization
  3. Use lazy imports - Import only when needed
  4. Profile migrations - Ensure migrations are efficient

Next Steps