mojozork audit
===============

technical audit of icculus/mojozork, a C z-machine interpreter with multiplayer
telnet server. this sits alongside the viola and zvm audits as reference for our
level 4 (shared world) integration plans.

project facts
=============

author: Ryan C. Gordon (icculus)

language: C, single-file per target

files::

    mojozork.c (1,800 lines)        — standalone single-player V3 z-machine
    multizorkd.c (2,784 lines)      — multiplayer telnet server
    mojozork-libretro.c             — RetroArch core frontend
    mojozork-sdl3.c                 — SDL3 graphical frontend
    CMakeLists.txt                  — build system, four optional targets

version support: V3 only. covers most of the infocom catalog. V4/V5/V6+ opcodes
are stubbed but unimplemented.

includes zork1.dat (92KB story file) — Activision released Zork I-III for free.

only external dependency: sqlite3 (multizork server only).

what matters for us
===================

this is NOT a candidate for direct use (it's C, we're Python). what matters:

1. the multiplayer architecture — MultiZork proves you can have multiple
   players in one z-machine instance. this is our Level 4 (shared world) from
   if-journey.rst.

2. the step execution model — runInstruction() executes one opcode at a time.
   step_completed flag breaks out of the loop at READ/QUIT. this is the async
   pattern we want in our interpreter.

3. the memory virtualization trick — two function pointers
   (get_virtualized_mem_ptr and remap_objectid) redirect memory access for
   multiplayer objects. clean separation of concern.

4. the persistence model — SQLite schema for saving complete z-machine state,
   player state, transcripts. same pattern we'd use.

5. the game-specific problem — MultiZork works for Zork 1 only. the multiplayer
   code is deeply hardcoded to Zork 1's specific object IDs, global variable
   indices, and memory layout. Ryan himself says it won't even work with other
   builds of Zork 1.

core interpreter
================

ZMachineState struct
--------------------

mojozork.c:88-131::

    story: uint8*               — game file loaded into memory
    header: ZHeader             — parsed 40-byte z-machine header
    logical_pc / pc             — program counter (offset and pointer)
    sp / bp                     — stack and base pointer into uint16 stack[2048]
    operands[8] + operand_count — current instruction arguments
    opcodes[256]                — function pointer dispatch table
    step_completed              — flag set by READ/QUIT to break execution
    quit                        — flag for game exit
    writestr callback           — function pointer for output
    die callback                — function pointer for fatal errors

execution model
---------------

mojozork.c main loop::

    while (!GState->quit) {
        runInstruction();
    }

runInstruction() (line ~1611-1676):

1. fetch opcode byte from PC
2. parse operand types from encoding bits
3. read operand values (large constant, small constant, variable, omitted)
4. dispatch: opcodes[opcode_number].fn()
5. handler modifies state (PC, stack, memory)

the step_completed pattern (used by MultiZork)::

    GState->step_completed = 0;
    while (!GState->step_completed) {
        runInstruction();
    }
    // READ or QUIT set step_completed = 1, breaking out

this is the async pattern we want. execute until natural yield point (input
needed), then return control to the server.

object tree (V3)
================

object entry structure
----------------------

V3 object entry is 9 bytes::

    bytes 0-3: attributes (32 boolean flags)
    byte 4: parent object ID (0=none)
    byte 5: sibling object ID (0=none)
    byte 6: first child object ID (0=none)
    bytes 7-8: address of property table

V3 allows 255 objects (8-bit IDs). Zork 1 uses 250. the 5 unused slots are
where MultiZork puts player objects.

object tree functions
---------------------

- getObjectPtr(objid) — returns pointer to 9-byte entry in object table
- getObjectProperty(objid, propid, *size) — walks property list, returns data
- unparentObject(objid) — removes from parent's child list (linear sibling walk)
- opcode_insert_obj — unparent, then insert as first child of destination
- opcode_remove_obj — unparent, clear parent/sibling fields
- opcode_get_parent/get_sibling/get_child — tree traversal
- opcode_set_attr/clear_attr/test_attr — bit manipulation on attribute flags

property tables: variable-length entries in descending ID order. each entry has
a size/ID byte (3 bits size, 5 bits propid) followed by data. 31 default
property values stored at start of object table.

multiplayer architecture
========================

the interesting part.

player state
------------

Player struct (multizorkd.c:96-127) stores per-player snapshots::

    PC, SP, BP + full stack copy (uint16[2048])
    object_table_data[9]        — the player's object (parent/child/sibling/attrs)
    property_table_data[32]     — player name in ZSCII + properties
    touchbits[32]               — per-player room-visited flags (256 bits for 250 objects)

    10 player-specific global variables:
        location, deaths, dead, lit, verbose, superbrief,
        alwayslit, lucky, loadallowed, coffin_held

    connection pointer, username, rejoin hash
    againbuf (last command for "again"/"g")

Instance struct wraps ZMachineState + up to 4 Players + database IDs + jmpbuf
for error recovery.

the 5-slot trick
----------------

Zork 1 uses objects 1-250 of 255 possible. objects 251-254 become player
objects. BUT: no free space in the object table — Infocom's tools pack it
tight, with property data immediately after object 250.

solution: virtual memory. player object data lives outside z-machine address
space. when the VM requests object 251-254:

- getObjectPtr() returns a pointer to player.object_table_data (external buffer)
- property table addresses map to 0xFE00-0xFFFF (high memory, unreachable by
  game code)
- get_virtualized_mem_ptr() intercepts these fake addresses and redirects to
  player.property_table_data

this works because Zork 1 never directly accesses the object table by address —
it always uses the z-machine object opcodes. the override hooks intercept at
the opcode level.

the player swap
---------------

step_instance() (multizorkd.c:1308-1507) — the core multiplayer function.

before running:

1. restore player's PC/SP/BP/stack from snapshot
2. swap in player-specific globals (globals[0]=location, globals[60]=lucky, etc)
3. patch 6 hardcoded "object #4" references in Zork's bytecode to point at this
   player's object
4. set INVISIBLE and NDESCBIT on the player object (so they don't see themselves)
5. if input provided, write it into z-machine's input buffer and tokenize

execute:

6. while (!step_completed) { runInstruction(); }

after running:

7. save PC/SP/BP/stack to player snapshot
8. save player-specific globals back to player struct
9. save touchbits (room visited flags) for all rooms
10. check for win condition (globals[140] != 0), reset touchbits for endgame room
11. clear INVISIBLE/NDESCBIT on player object
12. if QUIT opcode hit, flag player as game_over, drop connection

error recovery: setjmp/longjmp around the execution loop. if die() is called,
longjmp back, broadcast error, free instance.

chat system
-----------

- "!message" — say to players in same room (room = same parent object)
- "!!message" — broadcast to all players in instance
- regular text — z-machine command for current player

the game-specific problem
--------------------------

the multiplayer code is riddled with "ZORK 1 SPECIFIC MAGIC" comments.

hardcoded::

    object 4 = player
    object 82 = rooms container
    object 180 = West of House (start room)
    6 bytecode locations where Zork compares against literal 4 instead of using PLAYER global
    10 specific global variable indices for player state
    attribute 3 = TOUCHBIT, attribute 7 = INVISIBLE, attribute 14 = NDESCBIT

Ryan's own words: "there's an enormous amount of hardcoded tapdancing in
multizork to work with this build of Zork 1. It won't even work with other
versions of Zork 1, as the global variables will be in a different order at the
least."

for other games, you'd need to:

1. identify which globals are player-specific (game analysis required)
2. find hardcoded player object references (disassembly required)
3. determine room structure (game-specific object tree analysis)
4. identify visibility/description attributes (game-specific)

this is fundamentally a per-game effort, not a generic system.

persistence
===========

SQLite schema in multizorkd.c (lines 170-250)::

    instances:
        hash, num_players, start/save times, dynamic_memory (BLOB), story filename

    players:
        hash, username, PC/SP/BP, stack (BLOB), object/property/touchbits (BLOBs),
        all player-specific globals, game_over flag

    transcripts:
        player FK, texttype (output/input/system), content text

    crashes:
        instance FK, timestamp, current_player, PC, error string

    blocked:
        IP address blocking for failed logins

    used_hashes:
        tracks issued rejoin codes

auto-save every 30 moves. full dynamic memory snapshot + all player state.

reconnection: players get a 6-char hash code at game start. typing the code at
login restores their session, even if all players disconnected and the game was
archived.

transcripts saved for every session — accessible on the web
(multizork-transcripts.php).

telnet server
=============

simple custom telnet implementation (not a library):

- select()-based I/O loop (no threads)
- newline conversion (\n -> \r\n)
- dynamic output buffers (realloc on growth)
- connection state machine: READY -> DRAINING -> CLOSING
- input function pointers per connection (login/lobby/ingame) — state machine
  pattern
- IP-based rate limiting (blocked table, 24h timeout)

no telnet option negotiation (no NAWS, no GMCP, no MSDP). raw text in/out.

what we learn from this
========================

step execution works
--------------------

runInstruction() + step_completed flag = clean pause/resume at input prompts.
this is the model we want. whether we use viola, zvm hybrid, or write our own,
this pattern is proven.

memory virtualization is elegant
---------------------------------

two function pointer overrides (get_virtualized_mem_ptr, remap_objectid) let
one codebase serve both single-player and multiplayer. no opcode changes
needed. the principle: intercept at the memory access level, not the
instruction level.

multiplayer z-machine is possible but game-specific
----------------------------------------------------

MultiZork proves the concept works. multiple players, independent inventory,
shared world. but the implementation requires deep knowledge of the specific
game's internals. a generic solution would need:

- static analysis tools to identify player-specific globals
- disassembly tools to find hardcoded player references
- per-game configuration files mapping globals, objects, attributes

this is not impossible but it's a significant tooling investment.

the object table is more hackable than expected
------------------------------------------------

adding objects to a running game works IF the game uses opcodes (not direct
memory access) for object operations. the virtual memory trick is clever — fake
addresses that the VM can't distinguish from real ones.

persistence patterns transfer directly
---------------------------------------

the SQLite schema maps cleanly to our needs: quetzal blobs (they use raw memory
snapshots), player state, transcripts. the reconnection hash is a good UX
pattern.

V3 vs V5 matters for multiplayer
---------------------------------

V3: 255 objects, 8-bit IDs, 9-byte entries. tight but hackable.

V5: 65535 objects, 16-bit IDs, 14-byte entries. much more room for player
objects but also much more complex object tree. V5 games may use objects more
aggressively (modern IF is larger). the "5 spare slots" trick is V3-specific.

comparison with viola and zvm
==============================

::

    aspect          | mojozork           | viola                | zvm
    ----------------|--------------------|----------------------|--------------------
    language        | C                  | Python               | Python
    version support | V3 only            | V1-V5 (V6-V8 partial)| V1-V5 (structural)
    can run games   | yes                | yes                  | no (~46/108 opcodes)
    global state    | GState pointer     | 13 modules w/globals | instance-based (clean)
    IO abstraction  | function pointers  | adapter pattern      | abstract ZUI
    step execution  | runInstruction()   | feasible (5 lines)   | feasible (5 lines)
    multiplayer     | working (Zork 1)   | not supported        | not supported
    save/restore    | custom format      | quetzal              | stubbed
    error handling  | die() + longjmp    | sys.exit() (8+ locs) | exceptions (clean)
    object tree     | complete V3        | complete V3-V5       | read complete, write partial
    test suite      | script-based       | none                 | minimal
    usable by us    | reference only (C) | embeddable with work | skeleton needs finishing

the verdict from if-journey.rst still holds: "zvm has the architecture you'd
want. viola has the implementation you'd need." MojoZork adds: "and mojozork
has the multiplayer proof you need to believe level 4 is achievable."

related documents
=================

- ``docs/how/if-journey.rst`` — integration vision and roadmap
- ``docs/how/viola-embedding-audit.rst`` — viola architecture audit
- ``docs/how/zvm-embedding-audit.rst`` — zvm architecture audit
- ``docs/how/if-integration.txt`` — original integration plan
- Ryan's Patreon post (Aug 17, 2021) — motivation and technical narrative
  behind MultiZork
