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.

Pack Priority System

Commands are registered by content packs. Each pack has a numeric priority set via set_pack_priority(). When multiple packs register the same command name, the highest priority wins:

from maid_engine.commands import LayeredCommandRegistry

registry = LayeredCommandRegistry()

# Set pack priorities (higher number = higher priority)
registry.set_pack_priority("engine", 0)
registry.set_pack_priority("stdlib", 50)
registry.set_pack_priority("my-game", 100)
Priority Use Case
0 Core engine commands (quit, help)
50 Standard library commands
100 Content pack commands
200+ Explicit overrides

Higher priority packs override lower priority packs.

How Layering Works

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

# Engine pack (priority 0)
registry.register("attack", basic_attack, pack_name="engine")

# Content pack (priority 100)
registry.register("attack", rpg_attack, pack_name="my-game")

# When player types "attack":
# rpg_attack runs (pack priority 100 > 0)
# basic_attack is ignored

Registering Commands

Default Priority (from pack)

# Priority is inherited from the pack's set_pack_priority() value
registry.register("attack", attack_handler, pack_name="my-game")

Explicit Priority Override

# Override the pack-level priority for a specific command
registry.register(
    "attack",
    attack_handler,
    pack_name="my-game",
    priority=200,  # Higher than the pack's default
)

Via Content Pack

class MyCombatPack:
    def register_commands(self, registry: CommandRegistry) -> None:
        # Priority comes from set_pack_priority("my-combat-pack", ...)
        registry.register(
            "attack",
            self.attack_handler,
            pack_name="my-combat-pack",
        )

Overriding Commands

Override from Content Pack

To override a stdlib command, give your pack a higher priority:

# During engine setup:
registry.set_pack_priority("stdlib", 50)
registry.set_pack_priority("custom-movement", 100)

class CustomMovementPack:
    def register_commands(self, registry: CommandRegistry) -> None:
        # This overrides stdlib's "look" because custom-movement has priority 100 > 50
        registry.register(
            "look",
            self.enhanced_look,
            pack_name="custom-movement",
        )

    async def enhanced_look(self, ctx: CommandContext) -> bool:
        # 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("attack")
        if base_cmd:
            self._base_attack = base_cmd.handler

        # Register override (use explicit high priority)
        registry.register(
            "attack",
            self.enhanced_attack,
            pack_name="enhanced-combat",
            priority=200,  # Higher than the base pack
        )

    async def enhanced_attack(self, ctx: CommandContext) -> bool:
        player = ctx.world.get_entity(ctx.player_id)

        # Add pre-attack check
        if player.has_tag("pacifist"):
            await send_to_session(ctx.session, "Your pacifist vows prevent violence.")
            return True

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

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

            return result

        await send_to_session(ctx.session, "Base attack not found.")
        return True

Priority Resolution

Same Command, Different Packs

registry.set_pack_priority("engine", 0)
registry.set_pack_priority("stdlib", 50)
registry.set_pack_priority("magic-pack", 100)
registry.set_pack_priority("override-pack", 200)

registry.register("cast", basic_cast, pack_name="engine")        # Priority 0
registry.register("cast", stdlib_cast, pack_name="stdlib")        # Priority 50
registry.register("cast", magic_cast, pack_name="magic-pack")     # Priority 100
registry.register("cast", custom_cast, pack_name="override-pack") # Priority 200

# Result: custom_cast (200) wins
handled = await registry.execute(ctx)

Same Command, Same Priority

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

registry.register("attack", pack_a_attack, pack_name="pack-a")
registry.register("attack", pack_b_attack, pack_name="pack-b")

# If both packs have the same priority, pack_b_attack (registered last) wins

To avoid conflicts, use different pack priorities or explicit per-command priorities.

Checking Command Registration

Query Current Registration

cmd = registry.get("attack")
if cmd:
    print(f"Command: {cmd.name}")
    print(f"Priority: {cmd.priority}")
    print(f"Handler: {cmd.handler}")
    print(f"Pack: {cmd.pack_name}")

List All Registrations

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

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:
            packs = [c.pack_name for c in cmds]
            conflicts.append(f"{name}: {packs}")

    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 Priorities

# Engine commands - rarely override
registry.set_pack_priority("engine", 0)

# Standard library - base implementations
registry.set_pack_priority("stdlib", 50)

# Your content pack - normal usage
registry.set_pack_priority("my-game", 100)

# Explicit overrides - when you need to take control
registry.set_pack_priority("my-override", 200)

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(
            "attack",
            self.critical_attack,
            pack_name="combat-override",
            priority=200,
            description="Attack with critical hit chance.",
        )

3. Avoid Priority Conflicts

# Bad - two packs at same priority for same command
# Pack A
registry.register("craft", craft_a, pack_name="pack-a")
# Pack B
registry.register("craft", craft_b, pack_name="pack-b")
# If both packs have the same priority: unpredictable

# Good - explicit priorities
registry.set_pack_priority("pack-a", 100)
registry.set_pack_priority("pack-b", 200)  # pack-b overrides pack-a

4. Preserve Aliases When Overriding

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

# Override should preserve aliases
registry.register(
    "inventory",
    enhanced_inventory,
    pack_name="my-game",
    aliases=["i", "inv"],  # Keep the same aliases!
)

5. Test Override Behavior

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

    # Set up pack priorities
    registry.set_pack_priority("base-pack", 50)
    registry.set_pack_priority("override-pack", 100)

    # Register base
    registry.register("test", base_handler, pack_name="base-pack")

    # Verify base is used
    handled = await registry.execute(ctx)
    assert handled is True

    # Register override (higher priority pack)
    registry.register("test", override_handler, pack_name="override-pack")

    # Verify override is used
    handled = await registry.execute(ctx)
    assert handled is True

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_name)
    if cmd:
        print(f"Command: {cmd.name}")
        print(f"Priority: {cmd.priority}")
        print(f"Pack: {cmd.pack_name}")
        print(f"Aliases: {cmd.aliases}")
    else:
        print(f"Command not found: {command_name}")

# Usage
await debug_command(registry, "attack")

Next Steps