Fix code review issues for phase 14

This commit is contained in:
Jared Miller 2026-02-14 12:12:23 -05:00
parent 71f4ae4ec4
commit e6ca4dc6b1
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
8 changed files with 121 additions and 13 deletions

View file

@ -23,6 +23,7 @@ class CommandDefinition:
mode: str = "normal" mode: str = "normal"
help: str = "" help: str = ""
hidden: bool = False hidden: bool = False
admin: bool = False
# Registry maps command names to definitions # Registry maps command names to definitions
@ -204,5 +205,11 @@ async def dispatch(player: Player, raw_input: str) -> None:
await player.writer.drain() await player.writer.drain()
return return
# Check admin permission
if defn.admin and not getattr(player, "is_admin", False):
player.writer.write("You don't have permission to do that.\r\n")
await player.writer.drain()
return
# Execute the handler # Execute the handler
await defn.handler(player, args) await defn.handler(player, args)

View file

@ -54,6 +54,10 @@ async def cmd_dig(player: Player, args: str) -> None:
await player.send("Width and height must be numbers.\r\n") await player.send("Width and height must be numbers.\r\n")
return return
if width < 1 or height < 1:
await player.send("Zone dimensions must be at least 1x1.\r\n")
return
if get_zone(name) is not None: if get_zone(name) is not None:
await player.send(f"Zone '{name}' already exists.\r\n") await player.send(f"Zone '{name}' already exists.\r\n")
return return
@ -110,7 +114,7 @@ async def cmd_place(player: Player, args: str) -> None:
await player.send(f"Placed {thing_name} at ({player.x}, {player.y}).\r\n") await player.send(f"Placed {thing_name} at ({player.x}, {player.y}).\r\n")
register(CommandDefinition("@goto", cmd_goto, help="Teleport to a zone")) register(CommandDefinition("@goto", cmd_goto, admin=True, help="Teleport to a zone"))
register(CommandDefinition("@dig", cmd_dig, help="Create a new zone")) register(CommandDefinition("@dig", cmd_dig, admin=True, help="Create a new zone"))
register(CommandDefinition("@save", cmd_save, help="Save current zone")) register(CommandDefinition("@save", cmd_save, admin=True, help="Save current zone"))
register(CommandDefinition("@place", cmd_place, help="Place a thing")) register(CommandDefinition("@place", cmd_place, admin=True, help="Place a thing"))

View file

@ -53,6 +53,6 @@ async def cmd_set_brush(player: Player, args: str) -> None:
# Register paint mode commands # Register paint mode commands
register(CommandDefinition("@paint", cmd_paint)) register(CommandDefinition("@paint", cmd_paint, admin=True))
register(CommandDefinition("p", cmd_toggle_painting)) register(CommandDefinition("p", cmd_toggle_painting, admin=True))
register(CommandDefinition("brush", cmd_set_brush)) register(CommandDefinition("brush", cmd_set_brush, admin=True))

View file

@ -10,7 +10,7 @@ def _find_readable(player: Player, name: str) -> Thing | None:
name_lower = name.lower() name_lower = name.lower()
# Check inventory first # Check inventory first
for obj in player._contents: for obj in player.contents:
if not isinstance(obj, Thing): if not isinstance(obj, Thing):
continue continue
if obj.name.lower() == name_lower: if obj.name.lower() == name_lower:

View file

@ -84,7 +84,10 @@ def export_zone(zone: Zone) -> str:
lines.append(f"respawn_seconds = {spawn_rule.respawn_seconds}") lines.append(f"respawn_seconds = {spawn_rule.respawn_seconds}")
if spawn_rule.home_region is not None: if spawn_rule.home_region is not None:
hr = spawn_rule.home_region hr = spawn_rule.home_region
lines.append(f"home_region = {{ x = {hr['x']}, y = {hr['y']} }}") lines.append(
f"home_region = {{ x = [{hr['x'][0]}, {hr['x'][1]}], "
f"y = [{hr['y'][0]}, {hr['y'][1]}] }}"
)
lines.append("") lines.append("")
return "\n".join(lines) return "\n".join(lines)

View file

@ -1,5 +1,6 @@
"""Mob template loading, global registry, and spawn/despawn/query.""" """Mob template loading, global registry, and spawn/despawn/query."""
import logging
import tomllib import tomllib
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
@ -8,6 +9,8 @@ from mudlib.entity import Mob
from mudlib.loot import LootEntry from mudlib.loot import LootEntry
from mudlib.zone import Zone from mudlib.zone import Zone
logger = logging.getLogger(__name__)
@dataclass @dataclass
class MobTemplate: class MobTemplate:
@ -91,10 +94,26 @@ def spawn_mob(
moves=list(template.moves), moves=list(template.moves),
) )
if home_region is not None: if home_region is not None:
# Validate home_region structure
if (
isinstance(home_region.get("x"), list)
and isinstance(home_region.get("y"), list)
and len(home_region["x"]) == 2
and len(home_region["y"]) == 2
and all(isinstance(v, int) for v in home_region["x"])
and all(isinstance(v, int) for v in home_region["y"])
):
mob.home_x_min = home_region["x"][0] mob.home_x_min = home_region["x"][0]
mob.home_x_max = home_region["x"][1] mob.home_x_max = home_region["x"][1]
mob.home_y_min = home_region["y"][0] mob.home_y_min = home_region["y"][0]
mob.home_y_max = home_region["y"][1] mob.home_y_max = home_region["y"][1]
else:
logger.warning(
"Malformed home_region for mob %s: %s. Expected "
"{x: [int, int], y: [int, int]}. Skipping home region.",
template.name,
home_region,
)
mobs.append(mob) mobs.append(mob)
return mob return mob

View file

@ -42,6 +42,7 @@ class Player(Entity):
play_time_seconds: float = 0.0 play_time_seconds: float = 0.0
unlocked_moves: set[str] = field(default_factory=set) unlocked_moves: set[str] = field(default_factory=set)
session_start: float = 0.0 session_start: float = 0.0
is_admin: bool = False
@property @property
def mode(self) -> str: def mode(self) -> str:

View file

@ -48,6 +48,7 @@ def player(zone, mock_writer, mock_reader):
writer=mock_writer, writer=mock_writer,
reader=mock_reader, reader=mock_reader,
location=zone, location=zone,
is_admin=True,
) )
zone._contents.append(p) zone._contents.append(p)
players["builder"] = p players["builder"] = p
@ -172,6 +173,37 @@ async def test_dig_bad_args(player, mock_writer):
assert "usage" in written.lower() or "dig" in written.lower() assert "usage" in written.lower() or "dig" in written.lower()
@pytest.mark.asyncio
async def test_dig_non_numeric_dimensions(player, mock_writer):
"""@dig with non-numeric dimensions shows error."""
from mudlib.commands.build import cmd_dig
await cmd_dig(player, "cave abc def")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0]
assert "numbers" in written.lower()
@pytest.mark.asyncio
async def test_dig_zero_or_negative_dimensions(player, mock_writer):
"""@dig with zero or negative dimensions shows error."""
from mudlib.commands.build import cmd_dig
await cmd_dig(player, "cave 0 5")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0]
assert "at least 1x1" in written.lower()
mock_writer.write.reset_mock()
await cmd_dig(player, "cave 5 -2")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0]
assert "at least 1x1" in written.lower()
# --- @save --- # --- @save ---
@ -251,3 +283,45 @@ async def test_place_no_args(player, mock_writer):
mock_writer.write.assert_called() mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0] written = mock_writer.write.call_args_list[0][0][0]
assert "usage" in written.lower() or "place" in written.lower() assert "usage" in written.lower() or "place" in written.lower()
# --- Permission checks ---
@pytest.mark.asyncio
async def test_builder_commands_require_admin(zone, mock_writer, mock_reader):
"""Non-admin players cannot use builder commands."""
from mudlib.commands import dispatch
non_admin = Player(
name="player",
x=5,
y=5,
writer=mock_writer,
reader=mock_reader,
location=zone,
is_admin=False,
)
await dispatch(non_admin, "@goto hub")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "permission" in written.lower()
mock_writer.write.reset_mock()
await dispatch(non_admin, "@dig test 5 5")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "permission" in written.lower()
mock_writer.write.reset_mock()
await dispatch(non_admin, "@save")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "permission" in written.lower()
mock_writer.write.reset_mock()
await dispatch(non_admin, "@place thing")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "permission" in written.lower()