Files

6.2 KiB
Raw Permalink Blame History

Protocolli gestiti

Il client parla IAOC telnet (RFC 854) con un subset scelto di opzioni e GMCP come estensione JSON. Niente MSDP/MCCP/MXP in v0.2 (WONT su richiesta server).


Telnet — state machine

I byte IAC (0xFF) sono interpretati come command o iniziatore di subneg. Tutto il resto è testo che viene accumulato e poi emesso come un unico TextEvent per chunk letto.

byte          new state                  effetto
─────────────────────────────────────────────────────────────────────
0xFF          IAC                        nessuno
0xFF 0xFF     NORMAL                     literal 0xFF nel testo
0xFF DO|DONT|WILL|WONT  → NEGOTIATE     parser aspetta option byte
0xFF SB       → SB_OPTION                parser aspetta option byte
0xFF SE       NORMAL                     drop silenzioso
0xFF EOR      NORMAL + emit PromptEvent
0xFF GA       NORMAL + emit PromptEvent
0xFF + altro  NORMAL                     drop silent (NOP/DM/AO/AYT…)

In subneg:

byte                new state                  effetto
─────────────────────────────────────────────────────────────────────
SB_OPTION: <opt>    SB_DATA
SB_DATA: non IAC    accumula in _sb_buf
SB_DATA: IAC        SB_IAC
SB_IAC : SE         NORMAL + emit SubnegEvent(opt, _sb_buf)
SB_IAC : IAC        SB_DATA + literal 0xFF in _sb_buf
SB_IAC : altro      NORMAL                     abort della subneg

EOR (End Of Record, 239) e GA (Go Ahead, 249) sono entrambi interpretati come prompt boundary. Dead Souls li usa per delimitare l'output tra una stanza e l'altra. Il client li propaga come PromptEvent. Nella v0.2 nessun consumer in cli.py lo ascolta (elif isinstance(ev, PromptEvent): return None è un no‑op esplicito): è un forward hook per future euristiche di stanza basate sul confine EOR/GA.

Negoziazione opzioni

Server chiede Client risponde Side effect
DO ECHO WILL ECHO il prompt locale provvede l'eco
DO NAWS (con naws_size) WILL NAWS + SB NAWS w h SE dimensione iniziale
DO TTYPE (con ttype) WILL TTYPE set _expects_ttype_send
DO TTYPE (SB SEND) SB TTYPE "XTERM-256COLOR" SE risposta
DO GMCP (con want_gmcp) WILL GMCP abilita dispatch eventi GMCP
DO altro WONT rifiuto gentile
DONT X WONT X sempre
WILL X DONT X server offers non accettati
WONT X DONT X sempre

GMCP — WILL proattivo

TelnetConnection(want_gmcp=True) invia IAC WILL GMCP al connect(). Questo è richiesto da alcuni MUD (Dead Souls incluso) che aspettano l'offerta del client invece di un DO GMCP server‑driven.


GMCP — wire format

GMCP è un subneg con option code 201. Il payload è testo UTF‑8:

IAC SB GMCP <module.path> [' ' <json>] IAC SE

Esempio:

IAC SB GMCP Char.Vitals {"hp":100,"max_hp":120,"sp":42,"mv":80} IAC SE

Parsing

In gmcp.parse_gmcp:

  1. data.decode('utf-8', errors='replace') (tollerante a byte non validi).
  2. lstrip() per edge case con junk iniziale.
  3. Split sul primo spazio: head, _, rest = text.partition(' ').
  4. Validazione head con _MODULE_NAME_RE = ^[A-Za-z][A-Za-z0-9_.]*$: rifiuta .bad, 1Room, Room..Info, ecc.
  5. json.loads(rest) se presente. Decode error → data = None (tolleranza: server potrebbe dribblare payload parziali).

Moduli supportati out‑of‑the‑box

Modulo Effetto
Char.Vitals aggiorna state.vitals con sinonimi (hp/health/life)
Room.Info mapper.visit() con id_override = "vnum{int(num)}" se num/vnum/id è numerico
Char.Login, Char.Logout, Char.Status, Char.Info loggati (futuri hook)
Room.WalkDir loggato (futuro hook per movimento automatico)

Vedi docs/events.md per i payload registrati.


Dead Souls — euristiche di stanza

Dead Souls (driver FluffOS) usa convenzione di stanza testuale:

Inn Hall
A warm fire crackles in the hearth.

[ Exits: n e ]

Il parser RollingRoomParser riconosce:

  1. Nome: prima riga non vuota del blocco corrente.
  2. Body: tutte le righe tra nome e uscite (incluso l'eventuale riga [ Exits: ... ] nel body del RoomBlock).
  3. Exits: l'ultima riga del blocco riconosciuta dal regex _EXITS_RE (bracketed/unbracketed, : o -).
  4. No exits: la regex _NO_EXITS_RE rende la stanza "isolata" senza abortire.

parse_exits(line) è tollerante:

Exits: north, south
[ Exits: n e ]
Exit -
[ Exit: none ]

Tutti questi formati producono la stessa list[str] canonicalizzata.


ANSI — stripper

ansi.strip_ansi(text) rimuove sequenze CSI, OSC, charset designation e DEC private mode. Sequenze non riconosciute vengono lasciate nel testo (meglio un carattere spurio che un evento perso).

Pattern coperti:

\x1B\[[0-9;?]*[ -/]*[@-~]   # CSI sequences (SGR, cursor, mode)
| \x1B\][^\x07\x1B]*(?:\x07|\x1B\\)    # OSC sequences (title, etc.)
| \x1B[()][A-Za-z0-9]                   # Charset designation
| \x1B\[?[0-9;]*[hl]                    # DEC private mode set/reset

Single ESC byte senza [ successivo è lasciato nel testo. I log JSON e MD sono protetti da un try/except difensivo nel logger che, in caso di crash dello stripper, registra il raw (meglio spurio che event perso).


Error handling sui byte non validi

Input Comportamento
0xFF in mezzo al testo (literal) preservato nel testo
Subneg interrotta SubnegEvent mai emesso, stato torna a NORMAL; ok
IAC con comando sconosciuto drop silent
UTF‑8 non valido errors='replace' su tutto il testo
GMCP JSON malformato GmcpEvent.data = None (non abort)
GMCP module name malformato parse_gmcp ritorna None (non propagato)
GMCP module name non‑riconosciuto loggato come evento "gmcp" ma non dispatch
Stdout errno dopo read IncompleteReadError / ConnectionResetError → chiusura pulita