if terminal — how the interactive fiction system works ====================================================== what it is ========== players can play z-machine interactive fiction games (zork, etc) from inside the mud. input routes to a dfrotz subprocess instead of the command dispatcher. other players in the room see the output scrolling on a virtual terminal. this is level 1 from if-journey.rst — the IF world is a black box. we pipe text in and out. no interpreter embedding, no VM introspection. how it works ============ three pieces: the session, the command, and the server integration. IFSession (if_session.py) ------------------------- manages a single dfrotz subprocess. one session per player. spawns dfrotz with: dfrotz -p -w 80 -m -p plain ASCII (no formatting escapes) -w 80-column width -m no MORE prompts (continuous output) IFResponse dataclass carries output + done flag back to caller. input flow: player types something -> server routes to if_session.handle_input() -> escape commands (::quit, ::save, ::help) handled at mud layer -> everything else written to dfrotz stdin -> response read from dfrotz stdout -> IFResponse returned to server reading output: dfrotz writes text then shows a ">" prompt and waits for input. _read_response() reads in 1024-byte chunks until it detects the prompt. prompt detection checks for "> ", ">\n", ">\r\n" at end of raw bytes, and "\n>" in the stripped text. bare ">" with no preceding newline is only stripped when the raw bytes confirm a prompt pattern. idle timeout: 100ms (returns once data stops flowing) overall deadline: 5 seconds per response this was tricky to get right — see docs/lessons/dfrotz-prompt-detection.txt escape commands --------------- prefixed with :: to avoid conflicting with game input. ::quit exit game (auto-saves first) ::save force save to disk ::help show available escape commands play command (commands/play.py) ------------------------------- entry point. registered as a normal-mode command. play list available stories play zork start zork (exact or prefix match) story discovery: scans content/stories/ for .z3, .z5, .z8, .zblorb files. prefix matching: "zork" matches "zork1.z3". on start: 1. create IFSession with story path 2. spawn dfrotz, read intro text 3. push "if" onto player.mode_stack 4. if save file exists, auto-restore 5. broadcast intro/restored text to spectators server integration (server.py) ------------------------------ the shell loop checks player.mode and routes input: if player.mode == "if" and player.if_session: route to if_session.handle_input() on done (::quit): stop session, pop mode stack, send "you leave the terminal." on disconnect: session cleaned up in finally block (auto-saves) spectator broadcasting ====================== broadcast_to_spectators() in if_session.py iterates the global players dict, finds everyone at the same (x,y) who isn't the playing player, sends them formatted output: [Jared's terminal] > open mailbox Opening the small mailbox reveals a leaflet. triggered on: game start/restore, every game output, and when the player leaves the terminal. save/restore ============ saves stored at: data/if_saves/{player_name}/{game_name}.qzl player names sanitized (non-alphanumeric chars replaced with _). quetzal format (.qzl) — z-machine standard. save flow: send "save\n" to dfrotz -> read filename prompt -> send path -> read confirmation. auto-confirms "overwrite" if file exists. checks for "Failed" in response. restore flow: on "play zork" with existing save file, sends "restore\n" to dfrotz, provides save path, reads restored game text. double-save prevention: _saved flag prevents saving twice (::quit triggers save, then stop() would save again). flag resets on regular game input. file layout =========== src/mudlib/if_session.py IFSession class + broadcast_to_spectators src/mudlib/commands/play.py play command content/stories/zork1.z3 bundled story file data/if_saves/ per-player save files (gitignored) what's not here (yet) ===================== - terminal room objects (currently just the "play" command) - embedded interpreter (levels 2-5) - multiplayer z-machine - ghost presence in IF rooms see if-journey.rst for the bigger picture.