From dcc8b961bbfde4c4dfdc5fa93932918828a12112 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 7 Feb 2026 16:15:21 -0500 Subject: [PATCH] Add CommandDefinition and migrate command registry --- docs/how/commands.txt | 25 +++++++++++++------ src/mudlib/commands/__init__.py | 43 +++++++++++++++++++-------------- src/mudlib/commands/fly.py | 4 +-- src/mudlib/commands/look.py | 4 +-- src/mudlib/commands/movement.py | 18 +++++++------- src/mudlib/commands/quit.py | 4 +-- tests/test_commands.py | 13 +++++----- 7 files changed, 65 insertions(+), 46 deletions(-) diff --git a/docs/how/commands.txt b/docs/how/commands.txt index 232e7f8..2221f97 100644 --- a/docs/how/commands.txt +++ b/docs/how/commands.txt @@ -1,15 +1,25 @@ command system ============== -commands are registered in a simple dict mapping names to async handlers. +commands are registered as CommandDefinition objects with metadata. + + from mudlib.commands import CommandDefinition, register async def my_command(player: Player, args: str) -> None: player.writer.write("hello\r\n") await player.writer.drain() - register("mycommand", my_command, aliases=["mc"]) + register(CommandDefinition("mycommand", my_command, aliases=["mc"])) -dispatch parses input, looks up the handler, calls it: +CommandDefinition fields: + + name primary command name + handler async function(player, args) + aliases alternative names (default: []) + mode required player mode (default: "normal", "*" = any mode) + help help text (default: "") + +dispatch parses input, looks up the definition, calls its handler: await dispatch(player, "mycommand some args") @@ -43,16 +53,17 @@ adding commands --------------- 1. create src/mudlib/commands/yourcommand.py -2. import register from mudlib.commands +2. import CommandDefinition and register from mudlib.commands 3. define async handler(player, args) -4. call register() with name and aliases +4. call register(CommandDefinition(...)) 5. import the module in server.py so registration runs at startup code ---- -src/mudlib/commands/__init__.py registry + dispatch +src/mudlib/commands/__init__.py registry + dispatch + CommandDefinition src/mudlib/commands/movement.py direction commands src/mudlib/commands/look.py look/l -src/mudlib/commands/quit.py quit/q +src/mudlib/commands/fly.py fly +src/mudlib/commands/quit.py quit/q (mode="*") src/mudlib/player.py Player dataclass + registry diff --git a/src/mudlib/commands/__init__.py b/src/mudlib/commands/__init__.py index b357bd4..a3865d4 100644 --- a/src/mudlib/commands/__init__.py +++ b/src/mudlib/commands/__init__.py @@ -1,31 +1,38 @@ """Command registry and dispatcher.""" from collections.abc import Awaitable, Callable +from dataclasses import dataclass, field from mudlib.player import Player # Type alias for command handlers CommandHandler = Callable[[Player, str], Awaitable[None]] -# Registry maps command names to handler functions -_registry: dict[str, CommandHandler] = {} + +@dataclass +class CommandDefinition: + """Metadata wrapper for a registered command.""" + + name: str + handler: CommandHandler + aliases: list[str] = field(default_factory=list) + mode: str = "normal" + help: str = "" -def register( - name: str, handler: CommandHandler, aliases: list[str] | None = None -) -> None: - """Register a command handler with optional aliases. +# Registry maps command names to definitions +_registry: dict[str, CommandDefinition] = {} + + +def register(defn: CommandDefinition) -> None: + """Register a command definition with its aliases. Args: - name: The primary command name - handler: Async function that handles the command - aliases: Optional list of alternative names for the command + defn: The command definition to register """ - _registry[name] = handler - - if aliases: - for alias in aliases: - _registry[alias] = handler + _registry[defn.name] = defn + for alias in defn.aliases: + _registry[alias] = defn async def dispatch(player: Player, raw_input: str) -> None: @@ -45,13 +52,13 @@ async def dispatch(player: Player, raw_input: str) -> None: command = parts[0].lower() args = parts[1] if len(parts) > 1 else "" - # Look up the handler - handler = _registry.get(command) + # Look up the definition + defn = _registry.get(command) - if handler is None: + if defn is None: player.writer.write(f"Unknown command: {command}\r\n") await player.writer.drain() return # Execute the handler - await handler(player, args) + await defn.handler(player, args) diff --git a/src/mudlib/commands/fly.py b/src/mudlib/commands/fly.py index 9f76243..aafbdec 100644 --- a/src/mudlib/commands/fly.py +++ b/src/mudlib/commands/fly.py @@ -2,7 +2,7 @@ from typing import Any -from mudlib.commands import register +from mudlib.commands import CommandDefinition, register from mudlib.commands.movement import DIRECTIONS, send_nearby_message from mudlib.effects import add_effect from mudlib.player import Player @@ -91,4 +91,4 @@ async def cmd_fly(player: Player, args: str) -> None: await cmd_look(player, "") -register("fly", cmd_fly) +register(CommandDefinition("fly", cmd_fly)) diff --git a/src/mudlib/commands/look.py b/src/mudlib/commands/look.py index 3347095..580cb46 100644 --- a/src/mudlib/commands/look.py +++ b/src/mudlib/commands/look.py @@ -2,7 +2,7 @@ from typing import Any -from mudlib.commands import register +from mudlib.commands import CommandDefinition, register from mudlib.effects import get_effects_at from mudlib.player import Player, players from mudlib.render.ansi import RESET, colorize_terrain @@ -89,4 +89,4 @@ async def cmd_look(player: Player, args: str) -> None: # Register the look command with its alias -register("look", cmd_look, aliases=["l"]) +register(CommandDefinition("look", cmd_look, aliases=["l"])) diff --git a/src/mudlib/commands/movement.py b/src/mudlib/commands/movement.py index e3dc767..44d32a8 100644 --- a/src/mudlib/commands/movement.py +++ b/src/mudlib/commands/movement.py @@ -2,7 +2,7 @@ from typing import Any -from mudlib.commands import register +from mudlib.commands import CommandDefinition, register from mudlib.player import Player, players # World instance will be injected by the server @@ -154,11 +154,11 @@ async def move_southwest(player: Player, args: str) -> None: # Register all movement commands with their aliases -register("north", move_north, aliases=["n"]) -register("south", move_south, aliases=["s"]) -register("east", move_east, aliases=["e"]) -register("west", move_west, aliases=["w"]) -register("northeast", move_northeast, aliases=["ne"]) -register("northwest", move_northwest, aliases=["nw"]) -register("southeast", move_southeast, aliases=["se"]) -register("southwest", move_southwest, aliases=["sw"]) +register(CommandDefinition("north", move_north, aliases=["n"])) +register(CommandDefinition("south", move_south, aliases=["s"])) +register(CommandDefinition("east", move_east, aliases=["e"])) +register(CommandDefinition("west", move_west, aliases=["w"])) +register(CommandDefinition("northeast", move_northeast, aliases=["ne"])) +register(CommandDefinition("northwest", move_northwest, aliases=["nw"])) +register(CommandDefinition("southeast", move_southeast, aliases=["se"])) +register(CommandDefinition("southwest", move_southwest, aliases=["sw"])) diff --git a/src/mudlib/commands/quit.py b/src/mudlib/commands/quit.py index 862a7c6..535cfab 100644 --- a/src/mudlib/commands/quit.py +++ b/src/mudlib/commands/quit.py @@ -1,6 +1,6 @@ """Quit command for disconnecting from the server.""" -from mudlib.commands import register +from mudlib.commands import CommandDefinition, register from mudlib.player import Player, players @@ -21,4 +21,4 @@ async def cmd_quit(player: Player, args: str) -> None: # Register the quit command with its aliases -register("quit", cmd_quit, aliases=["q"]) +register(CommandDefinition("quit", cmd_quit, aliases=["q"], mode="*")) diff --git a/tests/test_commands.py b/tests/test_commands.py index 4581121..2ee868b 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest from mudlib import commands -from mudlib.commands import look, movement +from mudlib.commands import CommandDefinition, look, movement from mudlib.effects import active_effects, add_effect from mudlib.player import Player from mudlib.render.ansi import RESET @@ -49,8 +49,9 @@ def test_register_command(): async def test_handler(player, args): pass - commands.register("test", test_handler) + commands.register(CommandDefinition("test", test_handler)) assert "test" in commands._registry + assert commands._registry["test"].handler is test_handler def test_register_command_with_aliases(): @@ -59,12 +60,12 @@ def test_register_command_with_aliases(): async def test_handler(player, args): pass - commands.register("testcmd", test_handler, aliases=["tc", "t"]) + commands.register(CommandDefinition("testcmd", test_handler, aliases=["tc", "t"])) assert "testcmd" in commands._registry assert "tc" in commands._registry assert "t" in commands._registry - assert commands._registry["testcmd"] == commands._registry["tc"] - assert commands._registry["testcmd"] == commands._registry["t"] + assert commands._registry["testcmd"] is commands._registry["tc"] + assert commands._registry["testcmd"] is commands._registry["t"] @pytest.mark.asyncio @@ -78,7 +79,7 @@ async def test_dispatch_routes_to_handler(player): called = True received_args = args - commands.register("testcmd", test_handler) + commands.register(CommandDefinition("testcmd", test_handler)) await commands.dispatch(player, "testcmd arg1 arg2") assert called