Add multiplayer zmachine design notes
This commit is contained in:
parent
1b3a3646d6
commit
8288b2535a
1 changed files with 622 additions and 0 deletions
622
docs/how/multiplayer-zmachine-design.rst
Normal file
622
docs/how/multiplayer-zmachine-design.rst
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
==========================================
|
||||
multiplayer z-machine — shared world design
|
||||
==========================================
|
||||
|
||||
level 4 means multiple MUD players sharing one z-machine instance. one game
|
||||
world, multiple explorers. MojoZork proved this works for Zork 1 with 4
|
||||
concurrent players over telnet. this doc designs how it works in our Python
|
||||
interpreter, using MojoZork's lessons but improving on its limitations.
|
||||
|
||||
see ``docs/how/mojozork-audit.rst`` for the full MojoZork technical analysis.
|
||||
see ``docs/how/if-journey.rst`` for the five-level vision (this is level 4).
|
||||
|
||||
|
||||
why this is hard
|
||||
================
|
||||
|
||||
the z-machine is a single-player virtual machine. one program counter, one
|
||||
stack, one set of global variables. the game assumes there's one "you" in
|
||||
the world. multiplayer means faking multiple players inside a system that
|
||||
was never designed for it.
|
||||
|
||||
MojoZork solved this with brute force: swap the entire execution context
|
||||
(PC, stack, globals) every time a different player takes a turn. it works,
|
||||
but it's hardcoded to one build of Zork 1 — 10 specific global variable
|
||||
indices, 6 bytecode patch addresses, one known object layout.
|
||||
|
||||
our design keeps the same core trick (context swapping) but wraps it in
|
||||
a config-driven system that can support multiple games.
|
||||
|
||||
|
||||
the core trick: context swapping
|
||||
================================
|
||||
|
||||
the z-machine blocks on input (``op_sread`` in V3, ``op_aread`` in V5+).
|
||||
when it blocks, the entire interpreter state is frozen: PC points past the
|
||||
read instruction, the stack is stable, globals hold current values. this is
|
||||
the natural swap point.
|
||||
|
||||
the multiplayer loop::
|
||||
|
||||
1. player sends a command
|
||||
2. restore that player's saved context (PC, stack, globals, object data)
|
||||
3. feed the command to the interpreter
|
||||
4. run until the next READ (interpreter blocks again)
|
||||
5. capture output, save context back out
|
||||
6. repeat for next player with input
|
||||
|
||||
this is what MojoZork does. it's also what we'll do. the z-machine is
|
||||
inherently single-threaded — no way around processing one player at a time.
|
||||
|
||||
|
||||
shared vs per-player state
|
||||
==========================
|
||||
|
||||
which z-machine state is shared, and which needs per-player copies?
|
||||
|
||||
======================== ========= ================ ================================
|
||||
state shared? per-player copy? notes
|
||||
======================== ========= ================ ================================
|
||||
static/high memory yes no code and strings, immutable
|
||||
dynamic memory yes no world state (object tree, etc)
|
||||
program counter no yes each player paused at own READ
|
||||
call stack + locals no yes different execution contexts
|
||||
data stack no yes operand stack diverges
|
||||
player-specific globals no yes location, deaths, flags, etc
|
||||
player object entry no yes parent/attrs/properties
|
||||
touchbits (visited rooms) no yes each player's exploration history
|
||||
PRNG state yes no shared randomness is fine
|
||||
======================== ========= ================ ================================
|
||||
|
||||
this matches MojoZork's split exactly. the key insight: dynamic memory is ONE
|
||||
copy shared by all players. most of the game world (objects, rooms, items,
|
||||
puzzle state) is shared. only the player's execution state and a handful of
|
||||
"this is about me specifically" globals need per-player copies.
|
||||
|
||||
the consequence: when player A picks up the brass lantern, player B can't pick
|
||||
it up either. the object tree is shared. this is correct — it's the same world.
|
||||
but when player A is in the kitchen and player B is in the cellar, they each
|
||||
have their own location global. the game "sees" each player in their own room
|
||||
during their turn.
|
||||
|
||||
|
||||
player object strategy
|
||||
======================
|
||||
|
||||
each player needs a z-machine object to exist in the world — to have a
|
||||
location, to hold inventory, to be visible to other players.
|
||||
|
||||
**V3 (Zork 1):** 255 max objects, Zork uses ~250. objects 251-255 are
|
||||
available as player slots. this limits V3 to ~5 concurrent players.
|
||||
|
||||
**V5/V8 (Lost Pig, etc):** 65535 max objects. even a large game uses a few
|
||||
thousand at most. tens of thousands of spare slots.
|
||||
|
||||
MojoZork virtualizes player objects — stores them outside the real object
|
||||
table and intercepts all object access with function pointer hooks. this is
|
||||
clever but adds complexity and tight coupling to every object opcode.
|
||||
|
||||
**our approach: use real object table slots.**
|
||||
|
||||
for V3, claim spare slots at the top of the range (251+). for V5/V8, claim
|
||||
a block starting well above the game's highest object. player objects live
|
||||
in the real object table, initialized with appropriate parent (starting room),
|
||||
attributes (visible), and properties (name, description).
|
||||
|
||||
advantages over MojoZork's virtualization:
|
||||
|
||||
- no function pointer hooks on every object access
|
||||
- standard object opcodes work unmodified
|
||||
- properties live in real memory (no fake address ranges)
|
||||
- simpler code, fewer places for bugs
|
||||
|
||||
disadvantages:
|
||||
|
||||
- requires knowing the game's max object count (to avoid collisions)
|
||||
- V3 is limited to ~5 players (real constraint, but acceptable)
|
||||
- must write object data into dynamic memory during initialization
|
||||
|
||||
to determine available slots, the game config specifies a safe range, or we
|
||||
scan the object table at startup to find the highest used object.
|
||||
|
||||
comparison with MojoZork::
|
||||
|
||||
MojoZork our approach
|
||||
───────────────────────────────── ──────────────────────────────────
|
||||
virtual objects outside table real objects in table
|
||||
function pointer hooks no hooks needed
|
||||
fake address range (0xFE00+) real memory addresses
|
||||
hardcoded to Zork objects 251-254 config-driven range per game
|
||||
4 player limit (hardcoded) V3: ~5, V8: effectively unlimited
|
||||
|
||||
|
||||
identifying player-specific state
|
||||
=================================
|
||||
|
||||
the hardest part of supporting a new game: figuring out which global variables
|
||||
need per-player copies.
|
||||
|
||||
MojoZork hardcodes 10 Zork 1 globals (location, deaths, lit, verbose, etc).
|
||||
that works for one game but doesn't scale.
|
||||
|
||||
**our approach: per-game TOML config files.**
|
||||
|
||||
each supported game gets a multiplayer config that maps globals to meanings::
|
||||
|
||||
# content/stories/zork1.multiplayer.toml
|
||||
|
||||
[game]
|
||||
name = "zork1"
|
||||
version = 3
|
||||
story_file = "zork1.z3"
|
||||
|
||||
[player_object]
|
||||
original_id = 4 # the game's built-in "player" object
|
||||
start_room = 180 # West of House (object ID)
|
||||
room_container = 82 # parent of all rooms
|
||||
|
||||
[player_globals]
|
||||
# global variable indices that need per-player copies
|
||||
# index = human-readable name (for debugging/logging)
|
||||
0 = "location"
|
||||
60 = "lucky"
|
||||
61 = "deaths"
|
||||
62 = "dead"
|
||||
66 = "lit"
|
||||
70 = "superbrief"
|
||||
71 = "verbose"
|
||||
72 = "alwayslit"
|
||||
133 = "loadallowed"
|
||||
139 = "coffin_held"
|
||||
|
||||
[attributes]
|
||||
invisible = 7 # attribute to hide player during swap
|
||||
touchbit = 3 # room-visited flag (per-player)
|
||||
|
||||
[player_slots]
|
||||
start = 251 # first available object ID for players
|
||||
max = 255 # last available (V3 ceiling)
|
||||
|
||||
[bytecode_patches]
|
||||
# addresses where the game hardcodes the player object ID.
|
||||
# during swap, these bytes get patched to the current player's object ID.
|
||||
# found by disassembling and searching for references to object 4.
|
||||
addresses = [0x6B3F, 0x93E4, 0x9411, 0xD748, 0xE1AF, 0x6B88]
|
||||
|
||||
how to create these configs for new games:
|
||||
|
||||
1. **location global** — always global 0 (``read_global(0x10)``). this is
|
||||
standard across all z-machine games.
|
||||
2. **other player globals** — requires disassembly and analysis. look for
|
||||
globals that change on room entry, death, or mode switches. the
|
||||
``scripts/zmachine_inspect.py`` tool can help, and we could build a
|
||||
tracing tool that logs global writes during single-player play.
|
||||
3. **bytecode patches** — search disassembly for literal references to the
|
||||
player object ID. not all games will need these (some reference the
|
||||
player via a global, not a literal).
|
||||
4. **object slots** — load the game, count objects, pick a safe range above
|
||||
the highest used object.
|
||||
|
||||
this is manual work per game. but multiplayer is inherently game-specific —
|
||||
each game makes different assumptions about the player. a config file is the
|
||||
honest way to capture those assumptions.
|
||||
|
||||
future tooling (not in scope for initial implementation):
|
||||
|
||||
- a tracing mode that logs all global writes during play, flagging candidates
|
||||
for per-player globals
|
||||
- a disassembler that searches for literal object ID references
|
||||
- a test harness that runs two contexts against the same game and detects
|
||||
state conflicts
|
||||
|
||||
|
||||
the turn model
|
||||
==============
|
||||
|
||||
MojoZork uses implicit round-robin: whoever sends input next gets their turn.
|
||||
there's no explicit queue or scheduling.
|
||||
|
||||
**our approach: input queue, process one at a time.**
|
||||
|
||||
::
|
||||
|
||||
SharedIFSession
|
||||
├── input_queue: asyncio.Queue[(player_name, command)]
|
||||
├── processing_lock: asyncio.Lock
|
||||
└── tick():
|
||||
while not input_queue.empty():
|
||||
player, command = input_queue.get()
|
||||
swap_in(player)
|
||||
feed(command)
|
||||
run_until_read()
|
||||
capture_output(player)
|
||||
swap_out(player)
|
||||
|
||||
when a MUD player types a command in IF mode, it goes into the shared input
|
||||
queue. the session processes queued commands one at a time in FIFO order.
|
||||
|
||||
latency: in a MUD, players expect immediate response. with context swapping,
|
||||
each player's command takes one z-machine "turn" to process (typically
|
||||
milliseconds — our interpreter runs thousands of instructions per ms). even
|
||||
with 4-5 players, the queue drains in well under 100ms. this is fast enough
|
||||
that players won't notice the serialization.
|
||||
|
||||
if latency becomes a problem (unlikely), we could process the queue on every
|
||||
MUD tick rather than waiting for it to drain, giving each player one turn per
|
||||
tick in round-robin order.
|
||||
|
||||
comparison with MojoZork::
|
||||
|
||||
MojoZork our approach
|
||||
───────────────────────────────── ──────────────────────────────────
|
||||
implicit (whoever sends input) explicit FIFO queue
|
||||
blocking telnet read per player async queue, non-blocking
|
||||
single-threaded C async Python (event loop)
|
||||
no fairness guarantee FIFO ordering
|
||||
|
||||
|
||||
data structures
|
||||
===============
|
||||
|
||||
the core types for multiplayer::
|
||||
|
||||
@dataclass
|
||||
class PlayerContext:
|
||||
"""per-player execution state for a shared z-machine."""
|
||||
player_name: str
|
||||
mud_player: Player # reference to MUD player entity
|
||||
|
||||
# execution state (saved when swapped out)
|
||||
program_counter: int
|
||||
call_stack: list[ZRoutine] # full call frame chain
|
||||
data_stack: list[int] # operand stack
|
||||
|
||||
# player-specific z-machine state
|
||||
player_globals: dict[int, int] # global index -> value
|
||||
player_object_id: int # our object in the z-machine
|
||||
object_entry: bytes # saved object table entry
|
||||
property_data: bytes # saved property table
|
||||
touchbits: bytearray # visited-room flags
|
||||
|
||||
# i/o
|
||||
output_buffer: list[str] # accumulated output this turn
|
||||
|
||||
# session
|
||||
hash_code: str # for reconnection (6 chars)
|
||||
joined_at: float # timestamp
|
||||
|
||||
|
||||
class SharedIFSession:
|
||||
"""multiplayer z-machine session — one game, many players."""
|
||||
zmachine: ZMachine # single shared interpreter
|
||||
story_name: str
|
||||
game_config: GameMultiplayerConfig
|
||||
|
||||
contexts: dict[str, PlayerContext] # player_name -> context
|
||||
current_player: str | None # who's executing right now
|
||||
input_queue: asyncio.Queue # (player_name, command) pairs
|
||||
spectators: set[Player] # watchers who aren't playing
|
||||
|
||||
# persistence
|
||||
save_path: Path
|
||||
moves_since_save: int
|
||||
|
||||
the ``GameMultiplayerConfig`` is loaded from the TOML file described above.
|
||||
it holds the per-game mappings (player globals, object slots, patches, etc).
|
||||
|
||||
|
||||
the swap in detail
|
||||
==================
|
||||
|
||||
**swap_in(player_name)** — restore a player's context before their turn::
|
||||
|
||||
1. load PlayerContext for this player
|
||||
2. restore PC:
|
||||
opdecoder.program_counter = ctx.program_counter
|
||||
3. restore stack:
|
||||
stackmanager.restore(ctx.call_stack, ctx.data_stack)
|
||||
4. restore player-specific globals:
|
||||
for idx, value in ctx.player_globals.items():
|
||||
memory.write_global(idx + 0x10, value)
|
||||
5. restore player object entry:
|
||||
write ctx.object_entry to object table at ctx.player_object_id
|
||||
write ctx.property_data to property table address
|
||||
6. restore touchbits:
|
||||
for each room object, set/clear touchbit attribute from ctx.touchbits
|
||||
7. apply bytecode patches:
|
||||
for each patch address in game_config:
|
||||
memory[addr] = ctx.player_object_id (low byte)
|
||||
8. mark other players' objects as invisible (set invisible attribute)
|
||||
|
||||
**swap_out(player_name)** — save a player's context after their turn::
|
||||
|
||||
1. save PC:
|
||||
ctx.program_counter = opdecoder.program_counter
|
||||
2. save stack:
|
||||
ctx.call_stack, ctx.data_stack = stackmanager.snapshot()
|
||||
3. save player-specific globals:
|
||||
for idx in game_config.player_globals:
|
||||
ctx.player_globals[idx] = memory.read_global(idx + 0x10)
|
||||
4. save player object entry:
|
||||
ctx.object_entry = read object table at ctx.player_object_id
|
||||
ctx.property_data = read property table
|
||||
5. save touchbits:
|
||||
for each room, ctx.touchbits[i] = get_attr(room, touchbit_attr)
|
||||
6. clear visibility hacks (remove invisible from other players)
|
||||
|
||||
the swap is fast. it's mostly memcpy-equivalent operations. the touchbit
|
||||
loop is O(rooms) but rooms are typically < 300.
|
||||
|
||||
|
||||
output routing
|
||||
==============
|
||||
|
||||
during a player's turn, the z-machine generates text via the UI callback.
|
||||
this text should go to that player. but other players might also need to
|
||||
see certain things (someone entering their room, items being taken, etc).
|
||||
|
||||
**basic output routing:** all z-machine output during player A's turn goes
|
||||
to player A's output buffer. after the turn, flush the buffer to their MUD
|
||||
connection.
|
||||
|
||||
**broadcast messages:** when a player takes an action visible to others
|
||||
(enters a room, picks up an item), the MUD layer generates broadcast messages
|
||||
for nearby players. this happens at the MUD level, not the z-machine level.
|
||||
|
||||
::
|
||||
|
||||
after swap_out(player_a):
|
||||
old_room = previous_location
|
||||
new_room = ctx.player_globals[0] # location
|
||||
if old_room != new_room:
|
||||
broadcast_to_room(old_room, f"{player_a} leaves.")
|
||||
broadcast_to_room(new_room, f"{player_a} arrives.")
|
||||
|
||||
**spectators:** non-playing watchers see all output from all players,
|
||||
prefixed with the player's name. same pattern as current single-player
|
||||
spectator support, extended to multiplex output streams.
|
||||
|
||||
|
||||
MUD integration
|
||||
===============
|
||||
|
||||
how does a multiplayer IF session look from the MUD side?
|
||||
|
||||
**entering a shared session:**
|
||||
|
||||
a terminal object in the MUD world hosts the shared game. players "use" the
|
||||
terminal to join. the first player creates the session; subsequent players
|
||||
join the existing one::
|
||||
|
||||
> use terminal
|
||||
[joining Zork I — 2 players already exploring]
|
||||
[you are player 3, starting at West of House]
|
||||
|
||||
the player's mode stack pushes "shared_if". input routes to the shared
|
||||
session's input queue instead of the MUD command dispatcher.
|
||||
|
||||
**leaving and rejoining:**
|
||||
|
||||
players can leave with ``::quit`` (same as single-player). their context is
|
||||
preserved — the player object stays in the world (marked inactive/invisible),
|
||||
and their state is saved. they can rejoin later and resume where they left off.
|
||||
|
||||
MojoZork uses a 6-char hash code for reconnection. we can do the same, or
|
||||
just use the player's MUD identity (they're already authenticated).
|
||||
|
||||
**when the last player leaves:**
|
||||
|
||||
the session enters a dormant state. the shared z-machine state (dynamic
|
||||
memory) is saved to disk along with all player contexts. when someone
|
||||
rejoins, the session is restored.
|
||||
|
||||
option: keep the session alive for some grace period (5 minutes?) before
|
||||
saving and tearing down, in case players are just briefly disconnecting.
|
||||
|
||||
**spectators:**
|
||||
|
||||
work the same as single-player spectators. watchers see multiplexed output
|
||||
from all active players, prefixed with player names. spectators can't send
|
||||
commands to the z-machine.
|
||||
|
||||
|
||||
persistence
|
||||
===========
|
||||
|
||||
MojoZork saves to SQLite (instance state + per-player state + transcripts).
|
||||
we already have Quetzal save/restore for single-player. for multiplayer
|
||||
we need both the shared state and per-player contexts.
|
||||
|
||||
**what to save:**
|
||||
|
||||
- shared dynamic memory (one copy, same as Quetzal CMem/UMem)
|
||||
- per-player: PC, call stack, data stack, player globals, object data,
|
||||
touchbits
|
||||
- session metadata: game name, player list, creation time
|
||||
|
||||
**format options:**
|
||||
|
||||
1. **extend Quetzal** — add custom chunks for per-player data. Quetzal
|
||||
is extensible (IFF format). but it's designed for single-player saves.
|
||||
2. **separate files** — one Quetzal-like file for shared state, one file
|
||||
per player for their context. simpler but more files to manage.
|
||||
3. **SQLite** — like MojoZork. one row per session, BLOBs for state.
|
||||
integrates with our existing player database.
|
||||
|
||||
**recommendation:** SQLite, following MojoZork's proven approach. we already
|
||||
use SQLite for player accounts. add tables for multiplayer sessions and
|
||||
player contexts. BLOBs for binary state (memory, stacks).
|
||||
|
||||
**autosave:** every N moves (MojoZork uses 30). also save on player
|
||||
join/leave and session dormancy.
|
||||
|
||||
|
||||
game candidates
|
||||
===============
|
||||
|
||||
not all IF games make sense for multiplayer. criteria:
|
||||
|
||||
- exploration-heavy (many rooms to discover)
|
||||
- items and puzzles that can be shared or divided
|
||||
- enough physical space for multiple players
|
||||
- not deeply single-protagonist (tight narrative, personal journey)
|
||||
|
||||
**good candidates:**
|
||||
|
||||
1. **Zork I** (V3) — the original. large map, many items, exploration-focused.
|
||||
MojoZork already proved this works. our first target.
|
||||
|
||||
2. **Lost Pig** (V8) — single location focus (farm/caves), but funny and
|
||||
interactive. orc protagonist gives natural "party of adventurers" vibe.
|
||||
V8 format means plenty of object slots.
|
||||
|
||||
3. **Zork II / Zork III** (V3) — similar to Zork I. would need their own
|
||||
multiplayer configs but the gameplay pattern works.
|
||||
|
||||
4. **Enchanter trilogy** (V3/V5) — magic system could make cooperative play
|
||||
interesting (different players learn different spells).
|
||||
|
||||
**poor candidates:**
|
||||
|
||||
- tightly scripted narrative (Photopia, Shade)
|
||||
- real-time or timing-sensitive games
|
||||
- games with one-protagonist arcs (most modern literary IF)
|
||||
- Glulx games (out of scope — different VM entirely)
|
||||
|
||||
**what we have now:** ``content/stories/zork1.z3`` and
|
||||
``content/stories/LostPig.z8``. Zork I is the obvious first target since
|
||||
MojoZork provides a reference implementation to validate against.
|
||||
|
||||
|
||||
implementation plan
|
||||
===================
|
||||
|
||||
in rough dependency order:
|
||||
|
||||
**phase 1: game config and analysis tools**
|
||||
|
||||
- define the multiplayer TOML config format
|
||||
- write ``content/stories/zork1.multiplayer.toml`` (translating MojoZork's
|
||||
hardcoded values)
|
||||
- extend ``scripts/zmachine_inspect.py`` with object counting and
|
||||
global-tracing modes
|
||||
- analyze Lost Pig for multiplayer config (object count, candidate globals)
|
||||
|
||||
**phase 2: context swapping**
|
||||
|
||||
- implement ``PlayerContext`` dataclass
|
||||
- implement ``swap_in()`` / ``swap_out()`` on the interpreter
|
||||
- add ``stackmanager.snapshot()`` and ``stackmanager.restore()`` methods
|
||||
- add player object initialization (write new objects into table)
|
||||
- test: two contexts alternating on Zork 1, verify independent locations
|
||||
|
||||
**phase 3: shared session**
|
||||
|
||||
- implement ``SharedIFSession`` (the multiplayer equivalent of
|
||||
``EmbeddedIFSession``)
|
||||
- input queue and FIFO processing
|
||||
- output routing (per-player buffers)
|
||||
- broadcast messages for cross-player events (room entry/exit)
|
||||
|
||||
**phase 4: MUD integration**
|
||||
|
||||
- shared terminal object that hosts multiplayer sessions
|
||||
- join/leave/rejoin flow
|
||||
- mode stack integration (``shared_if`` mode)
|
||||
- spectator support (multiplexed output)
|
||||
|
||||
**phase 5: persistence**
|
||||
|
||||
- SQLite schema for multiplayer sessions
|
||||
- save/restore shared state + per-player contexts
|
||||
- autosave logic
|
||||
- dormant session management (save on last leave, restore on rejoin)
|
||||
|
||||
**phase 6: Lost Pig support**
|
||||
|
||||
- analyze Lost Pig for multiplayer config
|
||||
- write ``LostPig.multiplayer.toml``
|
||||
- test with V8 object slots (high-range allocation)
|
||||
- validate context swapping works for V8/V5 (aread vs sread differences)
|
||||
|
||||
|
||||
comparison table
|
||||
================
|
||||
|
||||
================================ ============================== ==============================
|
||||
concern MojoZork our design
|
||||
================================ ============================== ==============================
|
||||
player objects virtual (outside object table) real (in object table)
|
||||
object access function pointer hooks standard opcodes, no hooks
|
||||
player globals 10 hardcoded indices TOML config per game
|
||||
bytecode patches 6 hardcoded addresses TOML config per game
|
||||
player limit 4 (hardcoded) V3: ~5, V8: hundreds
|
||||
game support one build of Zork 1 config-driven, multiple games
|
||||
turn model implicit (whoever sends input) explicit FIFO queue
|
||||
output routing callback to current connection per-player buffer + broadcast
|
||||
persistence SQLite (memory BLOBs) SQLite (same approach)
|
||||
reconnection 6-char hash code MUD identity (already authed)
|
||||
error recovery longjmp + instance destroy exception handling + save state
|
||||
language C (single file, 2500 lines) Python (multiple modules)
|
||||
================================ ============================== ==============================
|
||||
|
||||
|
||||
known limitations
|
||||
=================
|
||||
|
||||
things we're explicitly not solving in the initial implementation:
|
||||
|
||||
- **no generic multiplayer** — each game needs manual analysis and a config
|
||||
file. there's no way around this; games make game-specific assumptions.
|
||||
|
||||
- **no concurrent execution** — one player at a time. the z-machine is
|
||||
single-threaded. serialized turns are the only safe model.
|
||||
|
||||
- **V3 player limit** — roughly 5 players max due to the 255-object ceiling.
|
||||
acceptable for the MUD use case (small group exploration).
|
||||
|
||||
- **no cross-player item interaction** — if player A drops an item in a room
|
||||
and player B is in that room, B won't see it until their next turn (when
|
||||
the shared object tree is visible to their context). this is a fundamental
|
||||
consequence of turn-based context swapping.
|
||||
|
||||
- **game-specific bytecode patches** — some games hardcode the player object
|
||||
ID in z-code instructions. these patch addresses must be found manually
|
||||
via disassembly. not all games will need them.
|
||||
|
||||
- **save_undo** — currently stubbed as "not available" in our interpreter.
|
||||
multiplayer makes undo even harder (whose undo?). punting entirely.
|
||||
|
||||
- **dictionary injection** — level 3 feature (adding new words to the game).
|
||||
orthogonal to multiplayer but would enhance it. not in scope here.
|
||||
|
||||
- **Glulx games** — different VM, out of scope. only z-machine V3/V5/V8.
|
||||
|
||||
|
||||
open questions
|
||||
==============
|
||||
|
||||
things to resolve during implementation:
|
||||
|
||||
1. **stackmanager snapshot format** — do we deep-copy ZRoutine objects, or
|
||||
serialize to bytes? deep copy is simpler; bytes are more compact for
|
||||
persistence. probably start with deep copy, optimize later.
|
||||
|
||||
2. **initial player setup** — when a new player joins mid-game, where do
|
||||
they start? MojoZork puts everyone at West of House. we could let the
|
||||
config specify a start room, or always use the game's default start.
|
||||
|
||||
3. **player object properties** — what name and description does a player
|
||||
object get? MojoZork uses "Adventurer" plus a number. we could use the
|
||||
MUD player's name, but that requires writing a z-machine string into
|
||||
the property table.
|
||||
|
||||
4. **item conflict** — if player A is carrying an item and player B tries
|
||||
to take it during their turn (shared object tree), what happens? the
|
||||
game will say "you can't see that here" because the item's parent is
|
||||
player A's object, not the room. this is probably fine — the z-machine
|
||||
handles it naturally.
|
||||
|
||||
5. **scoring** — most games track score in a global. is score per-player
|
||||
or shared? MojoZork doesn't address this. probably per-player (add to
|
||||
player_globals config).
|
||||
Loading…
Reference in a new issue