Add protocol negotiation doc
This commit is contained in:
parent
bd5f83e890
commit
fb758c8f36
2 changed files with 170 additions and 0 deletions
169
docs/how/protocols.rst
Normal file
169
docs/how/protocols.rst
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
protocol negotiation — GMCP, MSDP, and client detection
|
||||||
|
========================================================
|
||||||
|
|
||||||
|
how the server offers GMCP and MSDP to connecting clients, how clients
|
||||||
|
accept or reject them, and how the game guards against sending data to
|
||||||
|
clients that don't support it.
|
||||||
|
|
||||||
|
see also: ``mud.tin`` (tintin++ client config), ``docs/lessons/charset-vs-mtts.txt``
|
||||||
|
(related telnet negotiation gotcha).
|
||||||
|
|
||||||
|
|
||||||
|
the lifecycle
|
||||||
|
-------------
|
||||||
|
|
||||||
|
1. client connects via telnet
|
||||||
|
2. telnetlib3 calls ``begin_negotiation()`` on our ``MudTelnetServer``
|
||||||
|
3. server sends ``IAC WILL GMCP`` and ``IAC WILL MSDP``
|
||||||
|
4. client replies ``IAC DO <option>`` (accept) or ``IAC DONT <option>`` (reject)
|
||||||
|
5. telnetlib3 updates its internal option state tables
|
||||||
|
6. ``Player.gmcp_enabled`` / ``Player.msdp_enabled`` check those tables
|
||||||
|
7. every send function guards on the relevant property before transmitting
|
||||||
|
|
||||||
|
all of this happens during the telnet handshake, before the player sees the
|
||||||
|
name prompt. by the time game code runs, the protocol state is settled.
|
||||||
|
|
||||||
|
|
||||||
|
server side — MudTelnetServer
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
``server.py`` subclasses telnetlib3's ``TelnetServer``::
|
||||||
|
|
||||||
|
class MudTelnetServer(TelnetServer):
|
||||||
|
def begin_negotiation(self) -> None:
|
||||||
|
super().begin_negotiation()
|
||||||
|
self.writer.iac(WILL, GMCP)
|
||||||
|
self.writer.iac(WILL, MSDP)
|
||||||
|
|
||||||
|
``begin_negotiation()`` is called once by telnetlib3 right after the TCP
|
||||||
|
connection is established. the parent handles baseline telnet options (TTYPE,
|
||||||
|
NAWS, etc). we add GMCP and MSDP offers on top.
|
||||||
|
|
||||||
|
there's no ``begin_advanced_negotiation()`` override — GMCP/MSDP offers go
|
||||||
|
in the initial phase because clients need to know about them before the game
|
||||||
|
starts sending data.
|
||||||
|
|
||||||
|
|
||||||
|
player properties
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
``player.py`` exposes two boolean properties::
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gmcp_enabled(self) -> bool:
|
||||||
|
if self.writer is None:
|
||||||
|
return False
|
||||||
|
return bool(
|
||||||
|
self.writer.local_option.enabled(GMCP)
|
||||||
|
or self.writer.remote_option.enabled(GMCP)
|
||||||
|
)
|
||||||
|
|
||||||
|
checks both ``local_option`` and ``remote_option`` because telnet negotiation
|
||||||
|
is symmetric — either side can initiate. ``local_option`` reflects our ``WILL``
|
||||||
|
offer being accepted (client sent ``DO``). ``remote_option`` would reflect a
|
||||||
|
client-initiated ``DO`` that we accepted with ``WILL``. in practice the server
|
||||||
|
always offers first, but checking both is correct per the telnet spec.
|
||||||
|
|
||||||
|
same pattern for ``msdp_enabled``.
|
||||||
|
|
||||||
|
|
||||||
|
the guard pattern
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
every function in ``gmcp.py`` that sends data checks the property first::
|
||||||
|
|
||||||
|
def send_char_vitals(player: Player) -> None:
|
||||||
|
if not player.gmcp_enabled:
|
||||||
|
return
|
||||||
|
player.send_gmcp("Char.Vitals", { ... })
|
||||||
|
|
||||||
|
def send_msdp_vitals(player: Player) -> None:
|
||||||
|
if not player.msdp_enabled:
|
||||||
|
return
|
||||||
|
player.send_msdp("HEALTH", ...)
|
||||||
|
|
||||||
|
this is the only guard needed. the game loop calls these functions
|
||||||
|
unconditionally for all players; the functions themselves decide whether to
|
||||||
|
actually send. simple clients that don't negotiate GMCP/MSDP just never
|
||||||
|
receive the data.
|
||||||
|
|
||||||
|
|
||||||
|
what gets sent over each protocol
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
**GMCP** (Generic MUD Communication Protocol) — structured JSON packages:
|
||||||
|
|
||||||
|
- ``Char.Vitals`` — hp, max_hp, stamina, max_stamina
|
||||||
|
- ``Char.Status`` — name, level, location name
|
||||||
|
- ``Room.Info`` — room id, coordinates, exits
|
||||||
|
- ``Room.Map`` — ASCII map fragment for client-side rendering
|
||||||
|
|
||||||
|
sent on-demand when state changes (room enter, damage taken, etc).
|
||||||
|
|
||||||
|
**MSDP** (MUD Server Data Protocol) — key/value variable updates:
|
||||||
|
|
||||||
|
- ``HEALTH``, ``HEALTH_MAX``, ``STAMINA``, ``STAMINA_MAX``
|
||||||
|
|
||||||
|
sent on a timer, once per second (every ``TICK_RATE`` ticks in the game loop).
|
||||||
|
intended for status bar gauges.
|
||||||
|
|
||||||
|
|
||||||
|
client side — tintin++ example
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
tintin++ does NOT auto-negotiate GMCP or MSDP. it sends ``IAC DONT`` by
|
||||||
|
default, rejecting the server's offer. the ``CATCH`` event intercepts the
|
||||||
|
incoming ``IAC WILL`` before tintin's default handler runs::
|
||||||
|
|
||||||
|
#EVENT {CATCH IAC WILL GMCP} {#send {\xFF\xFD\xC9\}}
|
||||||
|
#EVENT {CATCH IAC WILL MSDP} {#send {\xFF\xFD\x45\}}
|
||||||
|
|
||||||
|
the hex bytes:
|
||||||
|
|
||||||
|
- ``\xFF\xFD\xC9`` = ``IAC DO GMCP`` (255, 253, 201)
|
||||||
|
- ``\xFF\xFD\x45`` = ``IAC DO MSDP`` (255, 253, 69)
|
||||||
|
|
||||||
|
MSDP variables arrive as subnegotiation events and get stored in tintin
|
||||||
|
variables for the status bar::
|
||||||
|
|
||||||
|
#EVENT {IAC SB MSDP} {
|
||||||
|
#variable {MSDP_%0} {%1};
|
||||||
|
#showme {[HP: $MSDP_HEALTH/$MSDP_HEALTH_MAX] ...} {-2}
|
||||||
|
}
|
||||||
|
|
||||||
|
``{-2}`` puts the status bar on the split line at the bottom of the terminal.
|
||||||
|
|
||||||
|
this was the trickiest part of the integration — the CATCH mechanism is
|
||||||
|
not well-documented in tintin++ and the raw byte sending syntax changed
|
||||||
|
across versions.
|
||||||
|
|
||||||
|
|
||||||
|
the client command
|
||||||
|
------------------
|
||||||
|
|
||||||
|
players can type ``client`` in-game to see what got negotiated::
|
||||||
|
|
||||||
|
client protocols
|
||||||
|
GMCP: active
|
||||||
|
MSDP: active
|
||||||
|
terminal
|
||||||
|
type: xterm-256color
|
||||||
|
size: 120x40
|
||||||
|
color: 256
|
||||||
|
...
|
||||||
|
|
||||||
|
useful for debugging connection issues — if a player reports missing status
|
||||||
|
bars, ``client`` shows whether MSDP actually negotiated.
|
||||||
|
|
||||||
|
|
||||||
|
adding a protocol
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
to add support for another telnet option:
|
||||||
|
|
||||||
|
1. import the option byte constant (or define it)
|
||||||
|
2. add ``self.writer.iac(WILL, NEW_OPTION)`` in ``MudTelnetServer.begin_negotiation()``
|
||||||
|
3. add a ``new_option_enabled`` property on ``Player``
|
||||||
|
4. guard all send functions on that property
|
||||||
|
5. add it to the ``client`` command output
|
||||||
|
6. add CATCH handler in ``mud.tin`` if tintin++ doesn't auto-negotiate it
|
||||||
|
|
@ -27,6 +27,7 @@ mostly standalone subsystems.
|
||||||
- ``docs/how/terrain-generation.txt`` — perlin noise, elevation thresholds, river tracing
|
- ``docs/how/terrain-generation.txt`` — perlin noise, elevation thresholds, river tracing
|
||||||
- ``docs/how/persistence.txt`` — SQLite storage, what's persisted vs runtime
|
- ``docs/how/persistence.txt`` — SQLite storage, what's persisted vs runtime
|
||||||
- ``docs/how/prompt-system.txt`` — modal prompts, color markup, per-player customization
|
- ``docs/how/prompt-system.txt`` — modal prompts, color markup, per-player customization
|
||||||
|
- ``docs/how/protocols.rst`` — GMCP/MSDP negotiation, client detection, guard pattern
|
||||||
|
|
||||||
combat
|
combat
|
||||||
------
|
------
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue