Commands Guide¶
Commands are how players interact with your content pack. This guide covers everything from basic command creation to advanced features like hooks, locks, and layered command resolution.
Command Basics¶
Creating a Command Handler¶
Command handlers are async functions that receive a CommandContext:
from maid_engine.commands.registry import CommandContext
async def hello_command(ctx: CommandContext) -> bool:
"""A simple hello command."""
await ctx.session.send("Hello, world!")
return True # Command handled successfully
Command Context¶
The CommandContext provides everything you need:
@dataclass
class CommandContext:
session: Any # Player's network session
player_id: UUID # UUID of the player entity
command: str # The command name executed
args: list[str] # Command arguments
raw_input: str # Full raw input string
world: World # The game world
document_store: DocumentStore | None # For persistence
metadata: dict # Additional data
Registering Commands¶
Register commands through the command registry:
from maid_engine.commands.registry import AccessLevel, LayeredCommandRegistry
def register_commands(registry: LayeredCommandRegistry, pack_name: str) -> None:
registry.register(
name="hello",
handler=hello_command,
pack_name=pack_name,
aliases=["hi", "greet"],
category="social",
description="Say hello",
usage="hello [name]",
access_level=AccessLevel.PLAYER,
hidden=False,
)
Command Parameters¶
Access Levels¶
Commands can require specific access levels:
from maid_engine.commands.registry import AccessLevel
class AccessLevel(IntEnum):
PLAYER = 0 # Regular players
HELPER = 1 # Helpers/guides
BUILDER = 2 # World builders
ADMIN = 3 # Administrators
IMPLEMENTOR = 4 # Full access
Usage:
registry.register(
name="ban",
handler=ban_command,
pack_name=pack_name,
access_level=AccessLevel.ADMIN, # Only admins can use
)
Command Categories¶
Categories organize commands for help listings:
registry.register(
name="attack",
handler=attack_command,
pack_name=pack_name,
category="combat",
)
registry.register(
name="cast",
handler=cast_command,
pack_name=pack_name,
category="magic",
)
Hidden Commands¶
Hide commands from help listings:
registry.register(
name="debug",
handler=debug_command,
pack_name=pack_name,
hidden=True, # Won't appear in help
)
Argument Handling¶
Basic Arguments¶
Arguments are provided as a list of strings:
async def give_command(ctx: CommandContext) -> bool:
"""Give an item to another player."""
if len(ctx.args) < 2:
await ctx.session.send("Usage: give <player> <item>")
return True
target_name = ctx.args[0]
item_name = " ".join(ctx.args[1:]) # Item name may have spaces
# Find target...
return True
Parsing Complex Arguments¶
For complex argument parsing, consider using a helper:
def parse_args(args: list[str], pattern: str) -> dict | None:
"""Parse arguments based on a pattern.
Pattern: "target:str count:int? force:bool?"
"""
result = {}
# Implementation...
return result
async def complex_command(ctx: CommandContext) -> bool:
parsed = parse_args(ctx.args, "target:str amount:int")
if not parsed:
await ctx.session.send("Usage: command <target> <amount>")
return True
target = parsed["target"]
amount = parsed["amount"]
# Process...
return True
Layered Command System¶
MAID's command registry supports layered resolution, allowing content packs to override commands.
Setting Pack Priorities¶
Higher priority packs override lower ones:
# In your engine setup
registry.set_pack_priority("my-game", 100) # Highest
registry.set_pack_priority("classic-rpg", 50) # Middle
registry.set_pack_priority("stdlib", 0) # Lowest
Overriding Commands¶
To override a command from a lower-priority pack:
# stdlib registers a basic "look" command at priority 0
# Your pack can override it at priority 100
async def custom_look(ctx: CommandContext) -> bool:
"""Enhanced look command with weather and time."""
# Your custom implementation
await ctx.session.send("You look around...")
return True
def register_commands(registry: LayeredCommandRegistry, pack_name: str) -> None:
registry.register(
name="look", # Same name as stdlib's
handler=custom_look,
pack_name=pack_name, # Your pack's higher priority wins
)
Viewing All Layers¶
You can inspect all registered versions of a command:
layers = registry.get_all_layers("look")
for layer in layers:
print(f"{layer.pack_name}: priority {layer.priority}")
Command Hooks¶
Hooks allow you to run code before or after command execution.
Pre-Hooks¶
Pre-hooks run before the command handler and can cancel execution:
from maid_engine.commands.hooks import PreHookContext
async def rate_limit_hook(ctx: PreHookContext) -> bool:
"""Limit command frequency."""
player_id = ctx.player_entity_id
if is_rate_limited(player_id):
await ctx.session.send("Please wait before using another command.")
return False # Cancel execution
return True # Continue
registry.register_pre_hook(
name="rate_limiter",
handler=rate_limit_hook,
commands=["attack", "cast"], # Only these commands
)
Post-Hooks¶
Post-hooks run after the command completes:
from maid_engine.commands.hooks import PostHookContext
async def command_logger(ctx: PostHookContext) -> None:
"""Log command execution."""
log.info(
f"Command: {ctx.command_name} by {ctx.player_entity_id} "
f"took {ctx.execution_time_ms:.2f}ms"
)
registry.register_post_hook(
name="logger",
handler=command_logger,
)
Hook Priorities¶
Hooks run in priority order:
from maid_engine.commands.hooks import HookPriority
registry.register_pre_hook(
name="authentication",
handler=auth_hook,
priority=HookPriority.HIGHEST, # Runs first
)
registry.register_pre_hook(
name="rate_limiter",
handler=rate_limit_hook,
priority=HookPriority.NORMAL, # Runs after auth
)
Filtering Hooks¶
Hooks can filter by command name or category:
# Only for specific commands
registry.register_pre_hook(
name="combat_check",
handler=combat_hook,
commands=["attack", "flee", "defend"],
)
# Only for a category
registry.register_pre_hook(
name="magic_check",
handler=magic_hook,
categories=["magic"],
)
Lock System¶
Locks provide fine-grained permission control using expressions.
Lock Expressions¶
Locks are string expressions evaluated at runtime:
registry.register(
name="godmode",
handler=godmode_command,
pack_name=pack_name,
locks="perm(admin)", # Requires admin permission
)
registry.register(
name="guild_bank",
handler=guild_bank_command,
pack_name=pack_name,
locks="perm(guild_leader) OR perm(admin)", # Either works
)
Custom Lock Functions¶
Register custom lock functions:
from maid_engine.commands.locks import LockContext
def check_gold(ctx: LockContext, args: list[str]) -> bool:
"""Check if player has enough gold."""
player = ctx.world.entities.get(ctx.player_entity_id)
if not player:
return False
inventory = player.try_get(InventoryComponent)
if not inventory:
return False
required = int(args[0]) if args else 0
return inventory.gold >= required
registry.register_lock_function("has_gold", check_gold)
# Use in command registration
registry.register(
name="expensive_action",
handler=expensive_command,
pack_name=pack_name,
locks="has_gold(1000)", # Requires 1000 gold
)
Lock Expression Syntax¶
| Expression | Meaning |
|---|---|
perm(name) |
Has permission "name" |
func(arg) |
Custom function with argument |
A AND B |
Both A and B must be true |
A OR B |
Either A or B must be true |
NOT A |
A must be false |
(A AND B) OR C |
Grouping with parentheses |
Command Metadata¶
Add custom metadata for hooks and other systems:
registry.register(
name="fireball",
handler=fireball_command,
pack_name=pack_name,
metadata={
"cooldown": 5.0, # 5 second cooldown
"mana_cost": 50, # Costs 50 mana
"requires_target": True,
"combat_only": True,
},
)
Access metadata in hooks:
async def cooldown_hook(ctx: PreHookContext) -> bool:
cooldown = ctx.metadata.get("cooldown", 0)
if cooldown > 0:
if is_on_cooldown(ctx.player_entity_id, ctx.command_name):
await ctx.session.send("That ability is on cooldown.")
return False
return True
Complete Example¶
Here's a complete example of a sophisticated command:
from uuid import UUID
from maid_engine.commands.registry import (
AccessLevel,
CommandContext,
LayeredCommandRegistry,
)
from maid_stdlib.components import (
DescriptionComponent,
HealthComponent,
InventoryComponent,
PositionComponent,
)
async def heal_command(ctx: CommandContext) -> bool:
"""Heal yourself or another player.
Usage:
heal - Heal yourself
heal <target> - Heal another player
"""
player = ctx.world.entities.get(ctx.player_id)
if not player:
await ctx.session.send("Error: Could not find your character.")
return False
# Determine target
if ctx.args:
# Heal another player
target_name = ctx.args[0].lower()
target = await find_player_in_room(ctx, target_name)
if not target:
await ctx.session.send(f"You don't see '{ctx.args[0]}' here.")
return True
else:
# Heal self
target = player
# Check target has health
health = target.try_get(HealthComponent)
if not health:
await ctx.session.send("That target cannot be healed.")
return True
# Check already at full health
if health.current >= health.maximum:
if target == player:
await ctx.session.send("You are already at full health.")
else:
target_desc = target.get(DescriptionComponent)
await ctx.session.send(f"{target_desc.name} is already at full health.")
return True
# Calculate healing amount
heal_amount = min(50, health.maximum - health.current)
health.heal(heal_amount)
# Send messages
if target == player:
await ctx.session.send(f"You heal yourself for {heal_amount} health.")
else:
target_desc = target.get(DescriptionComponent)
await ctx.session.send(
f"You heal {target_desc.name} for {heal_amount} health."
)
return True
async def find_player_in_room(ctx: CommandContext, name: str):
"""Find a player by name in the same room."""
player = ctx.world.entities.get(ctx.player_id)
if not player:
return None
player_pos = player.try_get(PositionComponent)
if not player_pos:
return None
for entity in ctx.world.entities.with_components(
PositionComponent, DescriptionComponent
):
if entity.id == ctx.player_id:
continue
pos = entity.get(PositionComponent)
if pos.room_id != player_pos.room_id:
continue
desc = entity.get(DescriptionComponent)
if name in str(desc.name).lower():
return entity
return None
def register_commands(registry: LayeredCommandRegistry, pack_name: str) -> None:
registry.register(
name="heal",
handler=heal_command,
pack_name=pack_name,
category="magic",
description="Heal yourself or another player",
usage="heal [target]",
access_level=AccessLevel.PLAYER,
metadata={
"cooldown": 10.0,
"mana_cost": 25,
},
)
Best Practices¶
Validate Input Early¶
async def my_command(ctx: CommandContext) -> bool:
if not ctx.args:
await ctx.session.send("Usage: command <required_arg>")
return True # Still return True - command was handled
Return Appropriate Values¶
- Return
Truewhen the command was handled (even if the action failed) - Return
Falseonly when the command wasn't applicable
async def my_command(ctx: CommandContext) -> bool:
if not ctx.args:
await ctx.session.send("Usage: command <arg>")
return True # Handled - showed usage
if not is_valid(ctx.args[0]):
await ctx.session.send("Invalid argument.")
return True # Handled - showed error
# Do the actual thing
return True # Handled - did the action
Use Descriptive Categories¶
Standard categories:
movement- look, go, enter, exitcombat- attack, flee, defendmagic- cast, channel, dispelsocial- say, tell, emoteinventory- get, drop, wear, removeinformation- who, score, helpadmin- ban, kick, reload
Provide Clear Usage Messages¶
async def complex_command(ctx: CommandContext) -> bool:
if len(ctx.args) < 2:
await ctx.session.send("Usage: command <target> <action> [options]")
await ctx.session.send("Actions: add, remove, list")
await ctx.session.send("Example: command player1 add --force")
return True
Next Steps¶
- Events Guide - Working with the event bus
- Persistence Guide - Storing command-related data
- Testing Guide - Testing your commands