Command System Overview¶
The command system in MAID handles player input, routing text commands to appropriate handlers. It supports aliases, layered overrides, and content pack integration.
What is a Command?¶
A command is a text instruction from a player, like:
Commands consist of a name and optional arguments.
Command Registry¶
The LayeredCommandRegistry manages all commands:
from maid_engine.commands import LayeredCommandRegistry, CommandHandler
registry = LayeredCommandRegistry()
# Register a command
registry.register(
"look",
look_handler,
pack_name="my-pack",
aliases=["l", "examine"],
description="Look at your surroundings",
)
# Execute a command
handled = await registry.execute(player_context)
Command Structure¶
Command Handler¶
A command handler is an async function:
from maid_engine.commands import CommandContext
async def look_handler(ctx: CommandContext) -> bool:
"""Look at surroundings or a specific target.
Returns True if handled, False if not recognized.
"""
if ctx.args:
# look <target>
await look_at(ctx.player, ctx.args[0])
else:
# look (room description)
await look_room(ctx.player)
return True
CommandContext¶
Contains all information about the command invocation:
@dataclass
class CommandContext:
player_id: UUID # Player executing command
command: str # Command name ("look")
args: list[str] # Arguments (["sword"])
raw_input: str # Full input ("look at sword")
world: World # Game world reference
session: Session | None # Network session (if any)
Return Value¶
Command handlers return bool:
True— the command was handled successfullyFalse— the command was not recognized or could not be handled
Send messages to the player directly via the session (e.g., await send_to_session(ctx.session, "message")) rather than through a return value.
Built-in Commands¶
MAID provides core commands in maid-stdlib:
Movement Commands¶
north, south, east, west, up, down # Move in a direction
n, s, e, w, u, d # Direction aliases
go <direction> # Explicit movement
Look Commands¶
look # Look at current room
look <target> # Look at entity or item
examine <target> # Alias for look <target>
Inventory Commands¶
inventory # List carried items
i # Alias for inventory
get <item> # Pick up an item
drop <item> # Drop an item
give <item> <target> # Give item to someone
Communication Commands¶
say <message> # Speak to the room
tell <player> <message> # Private message
shout <message> # Broadcast to area
System Commands¶
Command Layers¶
Commands can be registered at different layers, allowing content packs to override base functionality:
# 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)
# Register from different packs — priority comes from pack priority
registry.register("attack", basic_attack, pack_name="engine")
registry.register("attack", stdlib_attack, pack_name="stdlib")
registry.register("attack", rpg_attack, pack_name="my-game")
When attack is executed, rpg_attack runs (pack priority 100 is highest).
Command Aliases¶
Commands can have multiple names:
registry.register(
name="inventory",
handler=inventory_handler,
aliases=["i", "inv", "items"],
)
# All of these work:
# inventory
# i
# inv
# items
Command Categories¶
Organize commands by category:
from maid_classic_rpg.commands.registry import CommandCategory
registry.register(
name="attack",
handler=attack_handler,
category=CommandCategory.COMBAT,
)
registry.register(
name="cast",
handler=cast_handler,
category=CommandCategory.MAGIC,
)
# List commands by category
combat_commands = registry.get_by_category(CommandCategory.COMBAT)
Note
CommandCategory is defined in maid_classic_rpg.commands.registry, not in maid_engine.commands. The engine's category field is a plain str; CommandCategory is a game-level enum provided by the classic RPG content pack.
Content Pack Integration¶
Content packs register commands through their interface:
class MyCombatPack:
def register_commands(self, registry: CommandRegistry) -> None:
registry.register(
"attack",
self.attack_handler,
pack_name="my-combat-pack",
aliases=["a", "hit"],
description="Attack a target",
category="combat",
)
registry.register(
"flee",
self.flee_handler,
pack_name="my-combat-pack",
description="Attempt to flee from combat",
category="combat",
)
Command Discovery¶
List All Commands¶
# Get all registered commands
for cmd in registry.all_commands():
print(f"{cmd.name}: {cmd.description}")
# Get command by name
cmd = registry.get("attack")
if cmd:
print(f"Found: {cmd.name} (aliases: {cmd.aliases})")
Help System¶
# Get a specific command definition
cmd = registry.get("attack")
if cmd:
print(cmd.description)
print(cmd.usage)
Command Execution Flow¶
1. Player types: "attack goblin"
2. Parse input -> command="attack", args=["goblin"]
3. Find handler via LayeredCommandRegistry
4. Create CommandContext
5. Execute handler
6. Handler returns bool (True = handled, False = not handled)
7. Send response to player (done inside the handler)
async def handle_player_input(input_text: str, player: Entity) -> None:
# Parse command and args
parts = input_text.strip().split()
if not parts:
return
command = parts[0].lower()
args = parts[1:]
# Create context
ctx = CommandContext(
player_id=player.id,
command=command,
args=args,
raw_input=input_text,
world=world,
session=session,
)
# Execute — returns True if handled, False if unknown command
handled = await registry.execute(ctx)
if not handled:
await send_to_player(player, f"Unknown command: {command}")
Error Handling¶
Unknown Commands¶
Command Errors¶
async def attack_handler(ctx: CommandContext) -> bool:
if not ctx.args:
await send_to_session(ctx.session, "Attack what? Usage: attack <target>")
return True # Command was recognized, even though args were wrong
target = find_target(ctx.args[0], ctx.player_id)
if not target:
await send_to_session(ctx.session, f"You don't see '{ctx.args[0]}' here.")
return True
# Perform attack
await do_attack(ctx.player_id, target.id)
await send_to_session(ctx.session, f"You attack {target.name}!")
return True
Best Practices¶
1. Validate Early¶
async def handler(ctx: CommandContext) -> bool:
# Validate arguments first
if len(ctx.args) < 2:
await send_to_session(ctx.session, "Usage: give <item> <target>")
return True # Recognized command, bad args
# Then process
...
2. Provide Clear Feedback¶
async def handler(ctx: CommandContext) -> bool:
item = find_item(ctx.args[0])
if not item:
await send_to_session(
ctx.session,
f"You don't have '{ctx.args[0]}'.\n"
f"Check your inventory with 'inventory'.",
)
return True
...
3. Use Categories¶
# Makes help more organized
registry.register(
"sneak",
sneak_handler,
pack_name="my-pack",
category="movement", # Not "combat"
)
4. Document Commands¶
registry.register(
"cast",
cast_handler,
pack_name="my-pack",
description="Cast a spell.\nUsage: cast <spell> [target]",
aliases=["c"],
)
Next Steps¶
- Implementing Command Handlers - Write effective handlers
- Command Priority and Layering - Override commands properly