Compare commits
No commits in common. "4279186eca893c5ea0e1a44a107faf85fe1360ed" and "f418805b78777af9cd2ea86a6b3689747e7c3d41" have entirely different histories.
4279186eca
...
f418805b78
10 changed files with 9 additions and 326 deletions
30
mud.tin
30
mud.tin
|
|
@ -1,28 +1,8 @@
|
|||
#NOP TinTin++ config for the MUD server
|
||||
#NOP usage: tt++ mud.tin
|
||||
#NOP TinTin++ config for connecting to the MUD server
|
||||
#split 0 1
|
||||
|
||||
#NOP initialize MSDP variables for status bar (before connection)
|
||||
#variable {MSDP_HEALTH} {-}
|
||||
#variable {MSDP_HEALTH_MAX} {-}
|
||||
#variable {MSDP_STAMINA} {-}
|
||||
#variable {MSDP_STAMINA_MAX} {-}
|
||||
|
||||
#NOP MSDP: tintin++ auto-negotiates (responds DO to server's WILL).
|
||||
#NOP store variables as they arrive and refresh the status bar.
|
||||
|
||||
#EVENT {IAC SB MSDP} {
|
||||
#variable {MSDP_%0} {%1};
|
||||
#showme {[HP: $MSDP_HEALTH/$MSDP_HEALTH_MAX] [ST: $MSDP_STAMINA/$MSDP_STAMINA_MAX]} {-2}
|
||||
}
|
||||
|
||||
#NOP reconnect after disconnect. defined before #session so it
|
||||
#NOP persists in the startup session when the mud session dies.
|
||||
#alias {reconnect} {#session mud localhost 6789}
|
||||
|
||||
#session mud localhost 6789
|
||||
|
||||
#NOP fly shortcuts: f + direction
|
||||
#NOP fly aliases: f<direction> = fly 5 in that direction
|
||||
#alias {fn} {fly north}
|
||||
#alias {fs} {fly south}
|
||||
#alias {fe} {fly east}
|
||||
|
|
@ -32,6 +12,10 @@
|
|||
#alias {fse} {fly southeast}
|
||||
#alias {fsw} {fly southwest}
|
||||
|
||||
#NOP combat shortcuts
|
||||
#alias {o} {sweep}
|
||||
#alias {r} {roundhouse}
|
||||
|
||||
#alias {reconnect} {
|
||||
#zap mud;
|
||||
#session mud localhost 6789;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,69 +260,6 @@ async def cmd_skills(player: Player, args: str) -> None:
|
|||
await player.send(" ".join(names) + "\r\n")
|
||||
|
||||
|
||||
async def cmd_client(player: Player, args: str) -> None:
|
||||
"""Show client protocol negotiation and terminal capabilities."""
|
||||
lines = ["client protocols"]
|
||||
|
||||
# Protocol status
|
||||
gmcp = "active" if player.gmcp_enabled else "not active"
|
||||
msdp = "active" if player.msdp_enabled else "not active"
|
||||
lines.append(f" GMCP: {gmcp}")
|
||||
lines.append(f" MSDP: {msdp}")
|
||||
|
||||
# Terminal info
|
||||
lines.append("terminal")
|
||||
|
||||
# Terminal type from TTYPE negotiation
|
||||
ttype = None
|
||||
if player.writer is not None:
|
||||
ttype = player.writer.get_extra_info("TERM") or None
|
||||
lines.append(f" type: {ttype or 'unknown'}")
|
||||
|
||||
# Terminal size from NAWS
|
||||
cols, rows = 80, 24
|
||||
if player.writer is not None:
|
||||
cols = player.writer.get_extra_info("cols") or 80
|
||||
rows = player.writer.get_extra_info("rows") or 24
|
||||
lines.append(f" size: {cols}x{rows}")
|
||||
|
||||
# Color depth
|
||||
lines.append(f" colors: {player.color_depth}")
|
||||
|
||||
# MTTS capabilities
|
||||
caps = player.caps
|
||||
mtts_flags = []
|
||||
if caps.ansi:
|
||||
mtts_flags.append("ANSI")
|
||||
if caps.vt100:
|
||||
mtts_flags.append("VT100")
|
||||
if caps.utf8:
|
||||
mtts_flags.append("UTF-8")
|
||||
if caps.colors_256:
|
||||
mtts_flags.append("256 colors")
|
||||
if caps.truecolor:
|
||||
mtts_flags.append("truecolor")
|
||||
if caps.mouse_tracking:
|
||||
mtts_flags.append("mouse tracking")
|
||||
if caps.screen_reader:
|
||||
mtts_flags.append("screen reader")
|
||||
if caps.proxy:
|
||||
mtts_flags.append("proxy")
|
||||
if caps.mnes:
|
||||
mtts_flags.append("MNES")
|
||||
if caps.mslp:
|
||||
mtts_flags.append("MSLP")
|
||||
if caps.ssl:
|
||||
mtts_flags.append("SSL")
|
||||
|
||||
if mtts_flags:
|
||||
lines.append(f" MTTS: {', '.join(mtts_flags)}")
|
||||
else:
|
||||
lines.append(" MTTS: none detected")
|
||||
|
||||
await player.send("\r\n".join(lines) + "\r\n")
|
||||
|
||||
|
||||
# Register the commands command
|
||||
register(
|
||||
CommandDefinition(
|
||||
|
|
@ -345,17 +282,6 @@ register(
|
|||
)
|
||||
)
|
||||
|
||||
# Register the client command
|
||||
register(
|
||||
CommandDefinition(
|
||||
"client",
|
||||
cmd_client,
|
||||
aliases=[],
|
||||
mode="*",
|
||||
help="show client protocol and terminal info",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def cmd_help(player: Player, args: str) -> None:
|
||||
"""Show help for a command or skill.
|
||||
|
|
@ -367,7 +293,7 @@ async def cmd_help(player: Player, args: str) -> None:
|
|||
args = args.strip()
|
||||
if not args:
|
||||
await player.send(
|
||||
"type help <command> for details. see also: commands, skills, client\r\n"
|
||||
"type help <command> for details. see also: commands, skills\r\n"
|
||||
)
|
||||
return
|
||||
await _show_command_detail(player, args)
|
||||
|
|
|
|||
|
|
@ -108,8 +108,6 @@ def send_map_data(player: Player) -> None:
|
|||
|
||||
def send_msdp_vitals(player: Player) -> None:
|
||||
"""Send MSDP variable updates for real-time gauges."""
|
||||
if not player.msdp_enabled:
|
||||
return
|
||||
player.send_msdp(
|
||||
{
|
||||
"HEALTH": str(round(player.pl, 1)),
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ from __future__ import annotations
|
|||
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
|
||||
|
||||
|
|
@ -56,26 +54,6 @@ class Player(Entity):
|
|||
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] = {}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import tomllib
|
|||
from typing import cast
|
||||
|
||||
import telnetlib3
|
||||
from telnetlib3 import GMCP, MSDP, WILL
|
||||
from telnetlib3.server_shell import readline2
|
||||
|
||||
import mudlib.combat.commands
|
||||
|
|
@ -239,10 +238,6 @@ async def shell(
|
|||
|
||||
log.debug("new connection from %s", _writer.get_extra_info("peername"))
|
||||
|
||||
# Offer GMCP and MSDP so clients can negotiate
|
||||
_writer.iac(WILL, GMCP)
|
||||
_writer.iac(WILL, MSDP)
|
||||
|
||||
_writer.write("Welcome to the MUD!\r\n")
|
||||
_writer.write("What is your name? ")
|
||||
await _writer.drain()
|
||||
|
|
|
|||
|
|
@ -17,15 +17,6 @@ requires_zork = pytest.mark.skipif(not ZORK_PATH.exists(), reason="zork1.z3 not
|
|||
|
||||
@dataclass
|
||||
class MockWriter:
|
||||
def __post_init__(self):
|
||||
# Mock option negotiation state
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
self.local_option = MagicMock()
|
||||
self.local_option.enabled = MagicMock(return_value=False)
|
||||
self.remote_option = MagicMock()
|
||||
self.remote_option.enabled = MagicMock(return_value=False)
|
||||
|
||||
def write(self, data):
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -47,11 +47,6 @@ def mock_writer():
|
|||
writer.drain = AsyncMock()
|
||||
writer.send_gmcp = MagicMock()
|
||||
writer.send_msdp = MagicMock()
|
||||
# Option negotiation state (for gmcp_enabled/msdp_enabled checks)
|
||||
writer.local_option = MagicMock()
|
||||
writer.local_option.enabled = MagicMock(return_value=True)
|
||||
writer.remote_option = MagicMock()
|
||||
writer.remote_option.enabled = MagicMock(return_value=True)
|
||||
return writer
|
||||
|
||||
|
||||
|
|
@ -507,45 +502,3 @@ async def test_char_status_sent_on_rest_complete(player):
|
|||
]
|
||||
assert len(status_calls) == 1
|
||||
assert status_calls[0][0][1]["resting"] is False
|
||||
|
||||
|
||||
def test_msdp_vitals_skipped_when_not_negotiated(player):
|
||||
"""Test send_msdp_vitals skips when MSDP is not negotiated."""
|
||||
player.writer.local_option.enabled.return_value = False
|
||||
player.writer.remote_option.enabled.return_value = False
|
||||
|
||||
send_msdp_vitals(player)
|
||||
|
||||
player.writer.send_msdp.assert_not_called()
|
||||
|
||||
|
||||
def test_msdp_enabled_property(player):
|
||||
"""Test msdp_enabled reflects negotiation state."""
|
||||
player.writer.local_option.enabled.return_value = False
|
||||
player.writer.remote_option.enabled.return_value = False
|
||||
assert not player.msdp_enabled
|
||||
|
||||
player.writer.local_option.enabled.return_value = True
|
||||
assert player.msdp_enabled
|
||||
|
||||
|
||||
def test_gmcp_enabled_property(player):
|
||||
"""Test gmcp_enabled reflects negotiation state."""
|
||||
player.writer.local_option.enabled.return_value = False
|
||||
player.writer.remote_option.enabled.return_value = False
|
||||
assert not player.gmcp_enabled
|
||||
|
||||
player.writer.remote_option.enabled.return_value = True
|
||||
assert player.gmcp_enabled
|
||||
|
||||
|
||||
def test_msdp_enabled_no_writer():
|
||||
"""Test msdp_enabled with no writer returns False."""
|
||||
p = Player(name="NoWriter", writer=None)
|
||||
assert not p.msdp_enabled
|
||||
|
||||
|
||||
def test_gmcp_enabled_no_writer():
|
||||
"""Test gmcp_enabled with no writer returns False."""
|
||||
p = Player(name="NoWriter", writer=None)
|
||||
assert not p.gmcp_enabled
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
"""Tests for client command."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from mudlib.caps import ClientCaps
|
||||
from mudlib.commands.help import cmd_client
|
||||
from mudlib.player import Player, players
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_state():
|
||||
players.clear()
|
||||
yield
|
||||
players.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_writer():
|
||||
writer = MagicMock()
|
||||
writer.write = MagicMock()
|
||||
writer.drain = AsyncMock()
|
||||
writer.send_gmcp = MagicMock()
|
||||
writer.send_msdp = MagicMock()
|
||||
writer.local_option = MagicMock()
|
||||
writer.local_option.enabled = MagicMock(return_value=True)
|
||||
writer.remote_option = MagicMock()
|
||||
writer.remote_option.enabled = MagicMock(return_value=True)
|
||||
writer.get_extra_info = MagicMock(
|
||||
side_effect=lambda key: {
|
||||
"TERM": "TINTIN",
|
||||
"cols": 191,
|
||||
"rows": 54,
|
||||
}.get(key)
|
||||
)
|
||||
return writer
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_shows_protocols(mock_writer):
|
||||
"""Test client command shows GMCP and MSDP status."""
|
||||
p = Player(name="Test", writer=mock_writer)
|
||||
p.caps = ClientCaps(ansi=True, truecolor=True)
|
||||
await cmd_client(p, "")
|
||||
|
||||
output = mock_writer.write.call_args[0][0]
|
||||
assert "GMCP: active" in output
|
||||
assert "MSDP: active" in output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_shows_terminal_info(mock_writer):
|
||||
"""Test client command shows terminal type and size."""
|
||||
p = Player(name="Test", writer=mock_writer)
|
||||
p.caps = ClientCaps(ansi=True)
|
||||
await cmd_client(p, "")
|
||||
|
||||
output = mock_writer.write.call_args[0][0]
|
||||
assert "type: TINTIN" in output
|
||||
assert "size: 191x54" in output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_shows_color_depth(mock_writer):
|
||||
"""Test client command shows color depth."""
|
||||
p = Player(name="Test", writer=mock_writer)
|
||||
p.caps = ClientCaps(ansi=True, truecolor=True, colors_256=True)
|
||||
await cmd_client(p, "")
|
||||
|
||||
output = mock_writer.write.call_args[0][0]
|
||||
assert "colors: truecolor" in output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_shows_mtts_flags(mock_writer):
|
||||
"""Test client command shows MTTS capabilities."""
|
||||
p = Player(name="Test", writer=mock_writer)
|
||||
p.caps = ClientCaps(ansi=True, vt100=True, utf8=True, colors_256=True)
|
||||
await cmd_client(p, "")
|
||||
|
||||
output = mock_writer.write.call_args[0][0]
|
||||
assert "ANSI" in output
|
||||
assert "VT100" in output
|
||||
assert "UTF-8" in output
|
||||
assert "256 colors" in output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_not_negotiated(mock_writer):
|
||||
"""Test client command when protocols not negotiated."""
|
||||
mock_writer.local_option.enabled.return_value = False
|
||||
mock_writer.remote_option.enabled.return_value = False
|
||||
|
||||
p = Player(name="Test", writer=mock_writer)
|
||||
p.caps = ClientCaps()
|
||||
await cmd_client(p, "")
|
||||
|
||||
output = mock_writer.write.call_args[0][0]
|
||||
assert "GMCP: not active" in output
|
||||
assert "MSDP: not active" in output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_client_no_mtts():
|
||||
"""Test client command with no MTTS capabilities."""
|
||||
writer = MagicMock()
|
||||
writer.write = MagicMock()
|
||||
writer.drain = AsyncMock()
|
||||
writer.local_option = MagicMock()
|
||||
writer.local_option.enabled = MagicMock(return_value=False)
|
||||
writer.remote_option = MagicMock()
|
||||
writer.remote_option.enabled = MagicMock(return_value=False)
|
||||
writer.get_extra_info = MagicMock(return_value=None)
|
||||
|
||||
p = Player(name="Test", writer=writer)
|
||||
p.caps = ClientCaps()
|
||||
await cmd_client(p, "")
|
||||
|
||||
output = writer.write.call_args[0][0]
|
||||
assert "type: unknown" in output
|
||||
assert "MTTS: none detected" in output
|
||||
|
|
@ -35,13 +35,6 @@ class MockWriter:
|
|||
|
||||
def __init__(self):
|
||||
self.messages = []
|
||||
# Mock option negotiation state
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
self.local_option = MagicMock()
|
||||
self.local_option.enabled = MagicMock(return_value=False)
|
||||
self.remote_option = MagicMock()
|
||||
self.remote_option.enabled = MagicMock(return_value=False)
|
||||
|
||||
def write(self, message: str):
|
||||
self.messages.append(message)
|
||||
|
|
|
|||
15
uv.lock
15
uv.lock
|
|
@ -245,11 +245,8 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "telnetlib3"
|
||||
version = "2.4.0"
|
||||
version = "2.3.0"
|
||||
source = { directory = "../../src/telnetlib3" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
|
|
@ -258,7 +255,6 @@ requires-dist = [
|
|||
{ name = "sphinx-autodoc-typehints", marker = "extra == 'docs'" },
|
||||
{ name = "sphinx-rtd-theme", marker = "extra == 'docs'" },
|
||||
{ name = "ucs-detect", marker = "extra == 'extras'", specifier = ">=2" },
|
||||
{ name = "wcwidth", specifier = ">=0.2.13" },
|
||||
]
|
||||
provides-extras = ["docs", "extras"]
|
||||
|
||||
|
|
@ -270,12 +266,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8
|
|||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue