Initial commit: MUD client v0.2
This commit is contained in:
commit
0030fd7d82
26 files changed
+4421
No files matched your search
@@ -0,0 +1,177 @@
|
||||
# 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:
|
||||
|
||||
```regex
|
||||
\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 |
|
||||
Reference in new issue
Block a user