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:
| 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)¶
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¶
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:
- 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 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¶
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¶
- Command Overview - Understanding the command system
- Implementing Command Handlers - Write effective handlers