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,167 @@
|
||||
# Architettura di `progetto2`
|
||||
|
||||
> Versione: v0.2.x — client MUD CLI asincrono in Python ≥ 3.10 basato su
|
||||
> `prompt_toolkit`. Niente dipendenze pesanti: solo la libreria del
|
||||
> prompt + `pytest/ruff` per la dev.
|
||||
|
||||
## Visione d'insieme
|
||||
|
||||
Il client è organizzato in **cinque pilastri** indipendenti, composti
|
||||
da `cli.py` che fa da orchestratore asincrono:
|
||||
|
||||
| Pilastro | Modulo | Ruolo |
|
||||
| --- | --- | --- |
|
||||
| **Trasporto** | `telnet.py` | State machine IAC + asyncio TCP + negoziazione opzioni (ECHO/NAWS/TTYPE/GMCP a livello di byte‑option; non parsa i payload). Nessuna conoscenza del MUD. |
|
||||
| **Euristiche di testo** | `deadsouls.py` | Parsing delle stanze Dead Souls / LP‑MUD: canonicalizzazione direzioni, rilevazione blocchi stanza, regex uscite. |
|
||||
| **Payload semantici** | `gmcp.py` | Decoder GMCP: validazione *di forma* del module name + estrazione tollerante `Char.Vitals` / `Room.Info`. |
|
||||
| **Grafo & layout** | `mapper.py` | Database stanze (id‑stable), BFS multi‑source per coordinate, render ASCII del grafo. |
|
||||
| **Osservabilità** | `logger.py` | Sink JSONL append‑only + Markdown streamed; redazione password; lock difensivo. |
|
||||
| **UI** | `cli.py` | Orchestrazione: prompt async, status bar, dispatcher telnet, auto‑login, comandi slash, terminazione pulita. |
|
||||
|
||||
A parte `ansi.py` (foglia pura: solo regex + `re.sub`), tutti gli altri
|
||||
moduli sono foglie funzionali tranne `cli.py`.
|
||||
|
||||
## Grafo delle dipendenze
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ cli.py │ (orchestratore)
|
||||
└────────┬────────────┘
|
||||
│ importa
|
||||
┌───────────────┬─────┴──────┬───────────────┬──────────────┐
|
||||
│ │ │ │ │
|
||||
┌──▼─────┐ ┌─────▼─────┐ ┌───▼──────┐ ┌───────▼──────┐ ┌─────▼──────┐
|
||||
│ logger │ │ telnet │ │ gmcp │ │ mapper │ │ deadsouls │
|
||||
└────────┘ └─────┬─────┘ └─────┬────┘ └───────┬──────┘ └─────┬──────┘
|
||||
│ │ │ │
|
||||
│ SubnegEvent │ │ import │
|
||||
│ (OPT_GMCP) │ │ EXIT_ALIASES │
|
||||
│ │ EXIT_ALIASES│ + offsets │
|
||||
│ ├──┐ ├──────────────┘
|
||||
│ │ └───────────┘
|
||||
│ │
|
||||
│ ascii parser (IACParser)
|
||||
└─ nessuna dipendenza da altri moduli MUD
|
||||
│
|
||||
┌───▼──────┐
|
||||
│ ansi.py │ (foglia pura)
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
**Regola di import**: `gmcp.py` e `mapper.py` **non** dovrebbero
|
||||
importare da `cli.py` (e non lo fanno). `telnet.py` non sa nulla di
|
||||
GMCP: emette solo `SubnegEvent(OPT_GMCP, data)` e lascia che `cli.py`
|
||||
lo smisti alla callback di `_handle_gmcp`.
|
||||
|
||||
## Data flow di un frame in ingresso
|
||||
|
||||
```
|
||||
server TCP
|
||||
│
|
||||
│ bytes grezzi
|
||||
▼
|
||||
┌──────────────────────────────────────────┐
|
||||
│ TelnetConnection.read() │ chunk = 4096 byte
|
||||
└──────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────┐
|
||||
│ IACParser.feed(chunk) │ state machine
|
||||
│ • S.NORMAL → accumula testo │
|
||||
│ • S.IAC → DO/DONT/WILL/WONT/SB │
|
||||
│ • S.SB_* → accumulator dati subneg │
|
||||
│ • EOR/GA → flush + PromptEvent │
|
||||
└──────┬───────────────────────────────────┘
|
||||
│ list[TelnetEvent]
|
||||
▼
|
||||
┌──────────────────────────────────────────┐
|
||||
│ conn.read_loop(handler) │ smistamento (TUTTI gli eventi
|
||||
│ • tutti gli eventi passano anche per │ passano anche per `handler`):
|
||||
│ l'handler; Negotiate/Subneg vengono │ • NegotiateEvent → _on_control
|
||||
│ PRIMA processati da _on_control, │ (risposta WILL/WONT/DONT)
|
||||
│ POI inoltrati all'utente │ • SubnegEvent → handler → cli
|
||||
│ │ → _handle_gmcp (se OPT_GMCP)
|
||||
│ │ • TextEvent → print + log
|
||||
│ │ • PromptEvent → marker (no‑op) │
|
||||
└──────┬───────────────────────────────────┘
|
||||
│ TextEvent | SubnegEvent
|
||||
▼
|
||||
┌──────────────────────────────────────────┐
|
||||
│ _on_telnet_event (cli.py) │
|
||||
│ • TextEvent → MAPPER (via deadsouls) │
|
||||
│ • TextEvent → auto‑login (regex) │
|
||||
│ • SubnegEvent(GMCP) → gmcp.parse + │
|
||||
│ state.update │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Nessun modulo a valle dell'IAC si preoccupa di IAC: è un boundary
|
||||
pulito. `gmcp.py` riceve **solo** i `bytes` della subneg, non il
|
||||
flusso completo. `deadsouls.RollingRoomParser` riceve **solo testo
|
||||
plain** (ANSI già strippato da `cli.py`).
|
||||
|
||||
## Boundary concerns (punti deboli noti)
|
||||
|
||||
1. **Canonicalizzazione direzioni duplicata.**
|
||||
`deadsouls.EXIT_ALIASES` è la sorgente canonica. Tuttavia
|
||||
`mapper.py`, `gmcp.py` e `cli.py` (via `_handle_gmcp:exits_list`)
|
||||
chiamano tutti `EXIT_ALIASES.get(d, d)` su ogni uscita. **Rischio
|
||||
reale**: se domani aggiungi un alias in `EXIT_ALIASES` e dimentichi
|
||||
di aggiornare la logica del consumer, ottieni nomi non canonicali.
|
||||
- *Mitigazione breve*: aggiungere `deadsouls.normalize_exits(list)`
|
||||
e farla usare ovunque.
|
||||
- *Mitigazione strutturale*: promuovere `EXIT_ALIASES` +
|
||||
`DIRECTION_OFFSETS` in un modulo `navigation.py` da cui
|
||||
`deadsouls` *re‑importi* per retro‑compatibilità.
|
||||
|
||||
2. **Doppia fonte "camera corrente".**
|
||||
`mapper.Mapper._current` è la verità. Ma `cli.ClientState` espone
|
||||
`room_name` come property che rilancia su `mapper.current_room`. Non
|
||||
c'è cache in `cli`, quindi sono sincronizzati, ma l'oggetto
|
||||
`ClientState` ha più campi di quanti `_run()` ne inizializzi alcuni
|
||||
(`auto_password_prompted`, `vitals`) — è verboso ma non buggy.
|
||||
|
||||
3. **Re‑entrata del logger in `__init__`.**
|
||||
`EventLogger.__init__` apre due file in scrittura + scrive l'header
|
||||
MD; un errore sulla seconda operazione lascia il primo file aperto
|
||||
fino al GC. È mitigato da `try/except OSError` + `_close_files()`,
|
||||
ma l'eccezione rilanciata può lasciare un file orfano sul disco se
|
||||
il kernel non chiude immediatamente. In pratica: il `finally` di
|
||||
`_run` chiama `state.logger.close()` e quindi GC non è mai
|
||||
l'unico meccanismo.
|
||||
|
||||
4. **Validazione GMCP solo di *shape*, non di *semantica*.**
|
||||
La regex `_MODULE_NAME_RE = ^[A-Za-z][A-Za-z0-9_.]*$` è ancorata
|
||||
e rifiuta correttamente nomi malformati (`.bad`, `1Room`,
|
||||
`Room..Info`). Tuttavia non valida la *semantica* del payload:
|
||||
un `Room.Info` con `exits` come stringa invece di `dict`/`list`
|
||||
produce silenziosamente una lista uscite vuota. Documentato ma non
|
||||
ancora controllato.
|
||||
|
||||
5. **`refresh_interval=0.5` su `PromptSession`.**
|
||||
Anche in idle ridisegna la toolbar due volte al secondo. Costo CPU
|
||||
trascurabile su laptop moderne ma visibile su terminali via SSH su
|
||||
linee lente. Accettabile per v0.2; documentato come noto.
|
||||
|
||||
## Estensioni future raccomandate
|
||||
|
||||
| Estensione | Impatto | Dove intervenire |
|
||||
| --- | --- | --- |
|
||||
| `directions.py` (alias + offset + normalize) | alto (boundary) | spostare `EXIT_ALIASES`/`DIRECTION_OFFSETS` da `deadsouls.py`, re‑importare per back‑compat |
|
||||
| MSDP / MCCP / MXP | medio | aggiungere costanti + handling in `telnet._negotiate`, modulo parser parallelo a `gmcp.py` |
|
||||
| Persistenza stanze su disco | medio | `mapper.Mapper.to_json()` / `from_json()`, hook in `_run`/`_maybe_autologin` fine sessione |
|
||||
| `EventLogger` su sink syslog/journald | basso | aggiungere `journald_path` a `__init__` + dispatcher |
|
||||
| Multi‑sessione (multi `Mapper` + tab) | alto | refactoring `ClientState` in `Session`, lista in `cli` |
|
||||
| mappa HTML/SVG | medio | nuovo `mapper.render_svg()` + renderer HTML minimale |
|
||||
|
||||
## Decisioni di design (perché così)
|
||||
|
||||
| Decisione | Alternativa scartata | Motivo |
|
||||
| --- | --- | --- |
|
||||
| `telnet.py` non importa `gmcp.py` | metodo `parse_gmcp(self, ev)` | injection di dipendenza esplicita + testing più semplice |
|
||||
| Mappa lazy: coordinate solo a `render()` | coordinate in tempo reale | permette backtracking senza snapshot, layout deterministico al render |
|
||||
| Logger append‑only senza buffering | buffer batch + flush periodico | `tail -f` immediato è OK per volumi MUD (kB/s non MB/s) |
|
||||
| Lock difensivo nel logger | single‑thread assumption | costo trascurabile, protegge da futuri worker thread |
|
||||
| Auto‑login regex multi‑linea | stateful prompt matcher | regex robusta + documentata + testabile |
|
||||
| NAWS solo iniziale (no SIGWINCH handler) | re‑invio su cambio finestra | première itération: il client non intercetta segnali; documentato come gap |
|
||||
| `EventLogger` come sink opt‑in (no default) | always‑on log | sovraccarico inutile su sessioni interattive |
|
||||
Reference in new issue
Block a user