From 4cff1475c38c1c341c71d7e323fe0214cb85efa6 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Thu, 12 Feb 2026 16:41:04 -0500 Subject: [PATCH] Remove player from zone contents on disconnect Player objects were removed from the players dict on quit/disconnect but never removed from zone._contents, leaving ghost * markers on other players' maps. --- src/mudlib/commands/quit.py | 3 ++- src/mudlib/server.py | 1 + tests/test_commands.py | 38 +++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/mudlib/commands/quit.py b/src/mudlib/commands/quit.py index 54699ce..68bb816 100644 --- a/src/mudlib/commands/quit.py +++ b/src/mudlib/commands/quit.py @@ -19,7 +19,8 @@ async def cmd_quit(player: Player, args: str) -> None: await player.writer.drain() player.writer.close() - # Remove from player registry + # Remove from zone and player registry + player.move_to(None) if player.name in players: del players[player.name] diff --git a/src/mudlib/server.py b/src/mudlib/server.py index a4bad4e..418163a 100644 --- a/src/mudlib/server.py +++ b/src/mudlib/server.py @@ -450,6 +450,7 @@ async def shell( # Save player state on disconnect (if not already saved by quit command) if player_name in players: save_player(player) + player.move_to(None) del players[player_name] log.info("%s disconnected", player_name) diff --git a/tests/test_commands.py b/tests/test_commands.py index 4bf419b..5c00a4a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -432,3 +432,41 @@ async def test_look_ignores_items_at_other_positions(player, test_zone): output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "far_rock" not in output.lower() + + +@pytest.mark.asyncio +async def test_quit_removes_player_from_zone(monkeypatch): + """Quitting removes player from zone contents.""" + from mudlib.commands import quit as quit_mod + from mudlib.player import players + + monkeypatch.setattr(quit_mod, "save_player", lambda p: None) + + terrain = [["." for _ in range(10)] for _ in range(10)] + zone = Zone( + name="qz", + width=10, + height=10, + toroidal=True, + terrain=terrain, + impassable=set(), + ) + writer = MagicMock() + writer.write = MagicMock() + writer.drain = AsyncMock() + writer.close = MagicMock() + p = Player( + name="quitter", + location=zone, + x=5, + y=5, + reader=MagicMock(), + writer=writer, + ) + players.clear() + players["quitter"] = p + + assert p in zone._contents + await quit_mod.cmd_quit(p, "") + assert p not in zone._contents + assert "quitter" not in players