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:
- Old commands from that pack are unregistered
- New commands are registered
- 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¶
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¶
- Command Overview - Understanding the command system
- Implementing Command Handlers - Write effective handlers