6.2 KiB
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:
data.decode('utf-8', errors='replace')(tollerante a byte non validi).lstrip()per edge case con junk iniziale.- Split sul primo spazio:
head, _, rest = text.partition(' '). - Validazione
headcon_MODULE_NAME_RE = ^[A-Za-z][A-Za-z0-9_.]*$: rifiuta.bad,1Room,Room..Info, ecc. 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:
- Nome: prima riga non vuota del blocco corrente.
- Body: tutte le righe tra nome e uscite (incluso l'eventuale
riga
[ Exits: ... ]nel body delRoomBlock). - Exits: l'ultima riga del blocco riconosciuta dal regex
_EXITS_RE(bracketed/unbracketed,:o-). No exits: la regex_NO_EXITS_RErende 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 |