Skip to content

Command Priority and Layering

MAID's command system supports layered command registration, allowing content packs to override base functionality. This guide explains how command layering works.

Command Layers

Commands are registered at specific layers, which determine priority:

from maid_engine.commands import CommandLayer
Layer Priority Use Case
ENGINE 0 Core engine commands (quit, help)
STDLIB 100 Standard library commands
CONTENT 200 Content pack commands
OVERRIDE 300 Explicit overrides
CUSTOM 400 Custom/dynamic commands

Higher priority layers override lower priority layers.

How Layering Works

When multiple handlers exist for the same command, only the highest priority one executes:

# Engine layer (priority 0)
registry.register(
    name="attack",
    handler=basic_attack,
    layer=CommandLayer.ENGINE,
)

# Content pack layer (priority 200)
registry.register(
    name="attack",
    handler=rpg_attack,
    layer=CommandLayer.CONTENT,
)

# When player types "attack":
# rpg_attack runs (higher priority)
# basic_attack is ignored

Registering at Layers

Default Layer (CONTENT)

# No layer specified = CONTENT
registry.register(
    name="attack",
    handler=attack_handler,
)

Explicit Layer

registry.register(
    name="attack",
    handler=attack_handler,
    layer=CommandLayer.OVERRIDE,  # High priority
)

Via Content Pack

class MyCombatPack:
    def register_commands(self, registry: CommandRegistry) -> None:
        # Registered at CONTENT layer by default
        registry.register(
            name="attack",
            handler=self.attack_handler,
            source_pack="my-combat-pack",
        )

Overriding Commands

Override from Content Pack

To override a stdlib command:

class CustomMovementPack:
    def register_commands(self, registry: CommandRegistry) -> None:
        # Override the standard 'look' command
        registry.register(
            name="look",
            handler=self.enhanced_look,
            layer=CommandLayer.OVERRIDE,  # Higher than STDLIB
            source_pack="custom-movement",
        )

    async def enhanced_look(self, ctx: CommandContext) -> CommandResult:
        # Custom look implementation with extra features
        ...

Partial Override with Fallback

Sometimes you want to enhance, not replace:

class EnhancedCombatPack:
    def __init__(self):
        self._base_attack = None

    def register_commands(self, registry: CommandRegistry) -> None:
        # Get reference to base handler
        base_cmd = registry.get_command("attack")
        if base_cmd:
            self._base_attack = base_cmd.handler

        # Register override
        registry.register(
            name="attack",
            handler=self.enhanced_attack,
            layer=CommandLayer.OVERRIDE,
            source_pack="enhanced-combat",
        )

    async def enhanced_attack(self, ctx: CommandContext) -> CommandResult:
        player = ctx.get_player()

        # Add pre-attack check
        if player.has_tag("pacifist"):
            return CommandResult(False, "Your pacifist vows prevent violence.")

        # Call base handler
        if self._base_attack:
            result = await self._base_attack(ctx)

            # Add post-attack enhancement
            if result.success:
                await self.check_combo(ctx)

            return result

        return CommandResult(False, "Base attack not found.")

Layer Resolution

Same Command, Different Layers

# All registered for "cast"
registry.register("cast", basic_cast, layer=CommandLayer.ENGINE)      # Priority 0
registry.register("cast", stdlib_cast, layer=CommandLayer.STDLIB)      # Priority 100
registry.register("cast", magic_cast, layer=CommandLayer.CONTENT)      # Priority 200
registry.register("cast", custom_cast, layer=CommandLayer.OVERRIDE)    # Priority 300

# Result: custom_cast (300) wins
result = await registry.execute("cast", ctx)

Same Command, Same Layer

When commands are at the same layer, the last registered wins:

# Both at CONTENT layer
registry.register("attack", pack_a_attack, layer=CommandLayer.CONTENT)
registry.register("attack", pack_b_attack, layer=CommandLayer.CONTENT)

# Result: pack_b_attack (registered last) wins

To avoid conflicts, use explicit layers or ensure packs don't overlap.

Checking Command Registration

Query Current Registration

cmd = registry.get_command("attack")
if cmd:
    print(f"Command: {cmd.name}")
    print(f"Layer: {cmd.layer}")
    print(f"Handler: {cmd.handler}")
    print(f"Source: {cmd.source_pack}")

List All Registrations

for cmd in registry.all_commands():
    print(f"{cmd.name} @ {cmd.layer.name} from {cmd.source_pack}")

Check for Conflicts

def check_conflicts(registry: CommandRegistry) -> list[str]:
    """Find commands with multiple registrations."""
    conflicts = []
    command_counts = {}

    for cmd in registry.all_commands():
        if cmd.name not in command_counts:
            command_counts[cmd.name] = []
        command_counts[cmd.name].append(cmd)

    for name, cmds in command_counts.items():
        if len(cmds) > 1:
            sources = [c.source_pack for c in cmds]
            conflicts.append(f"{name}: {sources}")

    return conflicts

Content Pack Priority

Content packs load in dependency order. Later packs can override earlier ones:

# Load order affects CONTENT layer priority
engine.load_content_pack(BasePack())      # Loaded first
engine.load_content_pack(EnhancedPack())  # Loaded second

# EnhancedPack's commands override BasePack's at same layer

Explicit Dependencies

class EnhancedPack:
    @property
    def manifest(self) -> ContentPackManifest:
        return ContentPackManifest(
            name="enhanced-pack",
            version="1.0.0",
            dependencies=["base-pack"],  # Load after base-pack
        )

Hot Reload Considerations

When a pack is hot reloaded:

  1. Old commands from that pack are unregistered
  2. New commands are registered
  3. Override relationships are recalculated
# Before reload:
# "attack" -> rpg_pack handler (CONTENT)

# After rpg_pack reload with modified handler:
# "attack" -> rpg_pack new handler (CONTENT)

# Commands from other packs are unaffected

Best Practices

1. Use Appropriate Layers

# Engine commands - rarely override
layer=CommandLayer.ENGINE

# Standard library - base implementations
layer=CommandLayer.STDLIB

# Your content pack - normal usage
layer=CommandLayer.CONTENT

# Explicit overrides - when you need to take control
layer=CommandLayer.OVERRIDE

2. Document Overrides

class CombatOverridePack:
    """Overrides standard combat commands.

    This pack replaces:
    - attack: Adds critical hit system
    - defend: Adds parry mechanics

    Requires: maid-stdlib (provides base commands)
    """

    def register_commands(self, registry):
        registry.register(
            name="attack",
            handler=self.critical_attack,
            layer=CommandLayer.OVERRIDE,
            help_text="Attack with critical hit chance.",
        )

3. Avoid Layer Conflicts

# Bad - two packs at same layer for same command
# Pack A
registry.register("craft", craft_a, layer=CommandLayer.CONTENT)
# Pack B
registry.register("craft", craft_b, layer=CommandLayer.CONTENT)
# Result: Unpredictable

# Good - explicit layers
# Pack A (base crafting)
registry.register("craft", craft_a, layer=CommandLayer.CONTENT)
# Pack B (enhanced crafting, explicitly overrides)
registry.register("craft", craft_b, layer=CommandLayer.OVERRIDE)

4. Preserve Aliases When Overriding

# Original registration
registry.register(
    name="inventory",
    handler=basic_inventory,
    aliases=["i", "inv"],
)

# Override should preserve aliases
registry.register(
    name="inventory",
    handler=enhanced_inventory,
    aliases=["i", "inv"],  # Keep the same aliases!
    layer=CommandLayer.OVERRIDE,
)

5. Test Override Behavior

@pytest.mark.asyncio
async def test_command_override():
    registry = LayeredCommandRegistry()

    # Register base
    registry.register("test", base_handler, layer=CommandLayer.CONTENT)

    # Verify base is used
    result = await registry.execute("test", ctx)
    assert "base" in result.message

    # Register override
    registry.register("test", override_handler, layer=CommandLayer.OVERRIDE)

    # Verify override is used
    result = await registry.execute("test", ctx)
    assert "override" in result.message

Debugging Layer Issues

Enable Debug Logging

import logging
logging.getLogger("maid_engine.commands").setLevel(logging.DEBUG)

Inspect Command Resolution

async def debug_command(registry, command_name):
    cmd = registry.get_command(command_name)
    if cmd:
        print(f"Command: {cmd.name}")
        print(f"Layer: {cmd.layer.name} ({cmd.layer.value})")
        print(f"Source: {cmd.source_pack}")
        print(f"Aliases: {cmd.aliases}")
    else:
        print(f"Command not found: {command_name}")

# Usage
await debug_command(registry, "attack")

Next Steps