Custom Command¶
Problem¶
You want to create a brand-new player command from scratch — with argument parsing, permission checks, and proper registration in a content pack.
Solution¶
Step 1: Define the Command Handler¶
from maid_engine.commands.decorators import command, arguments
from maid_engine.commands.arguments import (
ArgumentSpec,
ArgumentType,
ParsedArguments,
SearchScope,
)
from maid_engine.commands import CommandContext, AccessLevel
from maid_stdlib.components import (
DescriptionComponent,
HealthComponent,
InventoryComponent,
)
@command(
name="heal",
aliases=["cure"],
category="skills",
help_text="Heal yourself or a target with a potion",
access_level=AccessLevel.PLAYER,
locks="NOT in_combat()",
)
@arguments(
ArgumentSpec(
"target",
ArgumentType.ENTITY,
required=False,
default=None,
description="Who to heal (default: yourself)",
search_scope=SearchScope.ROOM,
),
)
async def cmd_heal(ctx: CommandContext, args: ParsedArguments) -> bool:
"""Use a healing potion on yourself or an ally."""
world = ctx.world
player = world.get_entity(ctx.player_id)
if not player:
return False
# Determine target (self if none specified)
target_ref = args["target"]
if target_ref and target_ref.found:
target = target_ref.entity
else:
target = player
if not target:
await ctx.session.send("Heal who?\n")
return False
# Check for healing potion in inventory
inv = player.try_get(InventoryComponent)
if not inv:
await ctx.session.send("You have no inventory.\n")
return False
potion_id = None
potion_weight = 0.0
for item_id in inv.items:
item = world.get_entity(item_id)
if not item:
continue
desc = item.try_get(DescriptionComponent)
if desc and desc.matches_keyword("healing_potion"):
from maid_stdlib.components import ItemComponent
item_comp = item.try_get(ItemComponent)
potion_weight = item_comp.weight if item_comp else 0.0
potion_id = item_id
break
if not potion_id:
await ctx.session.send("You don't have any healing potions.\n")
return False
# Apply healing
health = target.try_get(HealthComponent)
if not health:
target_desc = target.try_get(DescriptionComponent)
name = target_desc.name if target_desc else "That"
await ctx.session.send(f"{name} can't be healed.\n")
return False
heal_amount = 25
actual = health.heal(heal_amount)
# Consume the potion
inv.remove_item(potion_id, potion_weight)
world.destroy_entity(potion_id)
target_desc = target.try_get(DescriptionComponent)
name = target_desc.name if target_desc else "them"
if target == player:
await ctx.session.send(
f"You drink a healing potion and recover {actual} HP.\n"
)
else:
await ctx.session.send(
f"You give a healing potion to {name}. "
f"They recover {actual} HP.\n"
)
return True
Step 2: Register in Your Content Pack¶
from maid_engine.plugins.protocol import ContentPack
from maid_engine.plugins import ContentPackManifest
from maid_engine.commands.registry import CommandRegistry, LayeredCommandRegistry
from maid_engine.core.world import World
from maid_engine.core.ecs import System
from maid_engine.core.events import Event
from maid_engine.core.engine import GameEngine
class MyContentPack:
"""Example content pack with a custom command."""
@property
def manifest(self) -> ContentPackManifest:
return ContentPackManifest(
name="my-pack",
version="1.0.0",
description="My custom content pack",
)
def get_dependencies(self) -> list[str]:
return ["stdlib"] # Depends on standard library
def get_systems(self, world: World) -> list[System]:
return []
def get_events(self) -> list[type[Event]]:
return []
def register_commands(self, registry: CommandRegistry | LayeredCommandRegistry) -> None:
registry.register(
"heal",
cmd_heal,
self.manifest.name,
aliases=["cure"],
category="skills",
)
async def on_load(self, engine: GameEngine) -> None:
pass
async def on_unload(self, engine: GameEngine) -> None:
pass
Step 3: Pattern-Based Command (Alternative)¶
For commands with complex syntax like give <item> to <target>:
from maid_engine.commands.decorators import command, pattern
from maid_engine.commands.arguments import (
ArgumentSpec,
ArgumentType,
ParsedArguments,
SearchScope,
)
from maid_engine.commands import CommandContext
from maid_stdlib.components import DescriptionComponent, InventoryComponent, ItemComponent
@command(name="give", category="items", help_text="Give an item to someone")
@pattern(
"<item> to <target>",
item=ArgumentSpec(
"item",
ArgumentType.ENTITY,
search_scope=SearchScope.INVENTORY,
),
target=ArgumentSpec(
"target",
ArgumentType.ENTITY,
search_scope=SearchScope.ROOM,
),
)
async def cmd_give(ctx: CommandContext, args: ParsedArguments) -> bool:
"""Give an item from your inventory to another entity."""
item_ref = args["item"]
target_ref = args["target"]
if not item_ref.found:
await ctx.session.send("You don't have that item.\n")
return False
if not target_ref.found:
await ctx.session.send("Give it to whom?\n")
return False
# Transfer item between inventories
giver = ctx.world.get_entity(ctx.player_id)
giver_inv = giver.get(InventoryComponent)
target_inv = target_ref.entity.get(InventoryComponent)
if not target_inv:
await ctx.session.send("They can't carry items.\n")
return False
item_comp = item_ref.entity.try_get(ItemComponent)
item_weight = item_comp.weight if item_comp else 0.0
giver_inv.remove_item(item_ref.entity.id, item_weight)
target_inv.add_item(item_ref.entity.id, item_weight)
# Get display names from DescriptionComponent
item_desc = item_ref.entity.try_get(DescriptionComponent)
item_name = item_desc.name if item_desc else item_ref.keyword
target_desc = target_ref.entity.try_get(DescriptionComponent)
target_name = target_desc.name if target_desc else target_ref.keyword
await ctx.session.send(f"You give {item_name} to {target_name}.\n")
return True
Step 4: Command with Hooks¶
Add pre/post execution hooks for cross-cutting concerns:
from maid_engine.commands.hooks import HookPriority, HookResult, PreHookContext, PostHookContext
async def cooldown_pre_hook(ctx: PreHookContext) -> HookResult:
"""Block command if on cooldown. Return CANCEL to block."""
# Check cooldown logic...
if False: # Replace with actual cooldown check
ctx.cancel("That command is on cooldown!")
return HookResult.CANCEL
return HookResult.CONTINUE # Allow execution
async def log_post_hook(ctx: PostHookContext) -> None:
"""Log command usage after execution."""
# ctx.result contains the handler return value
# ctx.execution_time_ms contains timing info
pass
# In register_commands:
def register_commands(self, registry: LayeredCommandRegistry) -> None:
registry.register("heal", cmd_heal, "my-pack")
registry.register_pre_hook(
"heal_cooldown", cooldown_pre_hook, HookPriority.NORMAL
)
registry.register_post_hook(
"heal_log", log_post_hook, HookPriority.LOW
)
How It Works¶
@commanddecorator attaches metadata (name, aliases, locks, access level) to the function@argumentsdecorator wraps the handler with automatic argument parsing and validation@patterndecorator parses structured input like"X to Y"into named argumentsCommandContextprovides access toworld,session,player_id, and raw inputParsedArgumentsgives dict-like access to parsed and validated argument valuesregistry.register()inregister_commands()adds the command to the layered registry- Lock expressions like
NOT in_combat()are checked automatically before the handler runs - Return
Trueif the command succeeded,Falseif it failed
Argument Type Quick Reference¶
| Type | Resolves To | Example Input |
|---|---|---|
STRING |
str |
"hello world" |
INTEGER |
int |
"42" |
FLOAT |
float |
"3.14" |
BOOLEAN |
bool |
"yes", "true", "on" |
ENTITY |
EntityReference |
"goblin", "2.sword" |
ENTITY_LIST |
EntityReference (multiple) |
"all.potion" |
DIRECTION |
str (normalized) |
"n", "north", "ne" |
PLAYER |
EntityReference |
"PlayerName" |
REST |
str (remaining input) |
anything after other args |
Variations¶
- Admin command: Set
access_level=AccessLevel.ADMINto restrict to admins - Subcommands: Parse
ctx.args[0]to dispatch to sub-handlers (heal self,heal other) - Confirmation: Store pending state and require a second command to confirm (
heal --confirm) - Target self shortcut: Check
if ctx.args and ctx.args[0] == "self"as a convenience alias
See Also¶
- Commands Guide — Full command system overview
- Command System Guide — Advanced features (hooks, locks, patterns)
- Content Packs — Pack structure and registration