mud/src/mudlib/player.py
Jared Miller 5a0c1b2151
Fix dataclass equality causing duplicate items in move_to
Objects were comparing by value instead of identity, causing
list.remove() to remove the wrong object when moving items with
identical attributes. Set eq=False on all dataclasses to use
identity-based comparison.
2026-02-14 01:39:45 -05:00

88 lines
2.8 KiB
Python

"""Player state and registry."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from telnetlib3 import GMCP, MSDP
from mudlib.caps import ClientCaps
from mudlib.entity import Entity
if TYPE_CHECKING:
from mudlib.editor import Editor
from mudlib.embedded_if_session import EmbeddedIFSession
from mudlib.if_session import IFSession
@dataclass(eq=False)
class Player(Entity):
"""Represents a connected player."""
writer: Any = None # telnetlib3 TelnetWriter for sending output
reader: Any = None # telnetlib3 TelnetReader for reading input
flying: bool = False
mode_stack: list[str] = field(default_factory=lambda: ["normal"])
caps: ClientCaps = field(default_factory=lambda: ClientCaps(ansi=True))
editor: Editor | None = None
if_session: IFSession | EmbeddedIFSession | None = None
paint_mode: bool = False
painting: bool = False
paint_brush: str = "."
prompt_template: str | None = None
aliases: dict[str, str] = field(default_factory=dict)
_last_msdp: dict = field(default_factory=dict, repr=False)
_power_task: asyncio.Task | None = None
@property
def mode(self) -> str:
"""Current mode is the top of the stack."""
return self.mode_stack[-1]
@property
def color_depth(self) -> str | None:
"""Best available color mode: truecolor, 256, 16, or None if no ANSI."""
if not self.caps.ansi:
return None
return self.caps.color_depth
async def send(self, message: str) -> None:
"""Send a message to the player via their telnet writer."""
self.writer.write(message)
await self.writer.drain()
def send_gmcp(self, package: str, data: Any = None) -> None:
"""Send a GMCP message to the client (no-op if unsupported)."""
if self.writer is not None:
self.writer.send_gmcp(package, data)
def send_msdp(self, variables: dict) -> None:
"""Send MSDP variables to the client (no-op if unsupported)."""
if self.writer is not None:
self.writer.send_msdp(variables)
@property
def gmcp_enabled(self) -> bool:
"""Whether this client has GMCP negotiated."""
if self.writer is None:
return False
return bool(
self.writer.local_option.enabled(GMCP)
or self.writer.remote_option.enabled(GMCP)
)
@property
def msdp_enabled(self) -> bool:
"""Whether this client has MSDP negotiated."""
if self.writer is None:
return False
return bool(
self.writer.local_option.enabled(MSDP)
or self.writer.remote_option.enabled(MSDP)
)
# Global registry of connected players
players: dict[str, Player] = {}