Compare commits

..

7 commits

Author SHA1 Message Date
305f25e77a
Add spawn command and wire mobs into server
Phase 6: spawn command creates mobs at player position from loaded
templates. Server loads mob templates from content/mobs/ at startup,
injects world into combat/commands module, and runs process_mobs()
each game loop tick after process_combat().
2026-02-09 11:43:37 -05:00
42bb3fa046
Add mob AI for combat decisions
Phase 5: process_mobs() runs each tick, handling mob attack and defense
decisions. Mobs pick random attacks from their move list when IDLE,
swap roles if needed, and attempt defense during TELEGRAPH/WINDOW with
a 40% chance of correct counter. 1-second cooldown between actions.
Training dummies with empty moves never fight back.
2026-02-09 11:43:37 -05:00
86c2c46dfa
Handle mob defeat in combat resolution
Phase 4: when combat ends, determine winner/loser. If the loser is a
Mob, despawn it and send a victory message to the winner. If the loser
is a Player fighting a Mob, send a defeat message instead.
2026-02-09 11:43:37 -05:00
f2d52c4936
Render mobs as * in viewport
Phase 3: look command now collects alive mob positions using the same
wrapping-aware relative position calc as players, and renders them as *
with the same priority as other players (after @ but before effects).
2026-02-09 11:43:37 -05:00
1fa21e20b0
Add mob target resolution in combat commands
Phase 2: do_attack now searches the mobs registry after players dict
when resolving a target name. Players always take priority over mobs
with the same name. World instance injected into combat/commands module
for wrapping-aware mob proximity checks.
2026-02-09 11:43:37 -05:00
2bab61ef8c
Add mob templates, registry, and spawn/despawn/query
Phase 1 of fightable mobs: MobTemplate dataclass loaded from TOML,
global mobs list, spawn_mob/despawn_mob/get_nearby_mob with
wrapping-aware distance. Mob entity gets moves and next_action_at fields.
2026-02-09 11:43:37 -05:00
ed6ffbdc5d
Add research on IF possibilities 2026-02-09 11:38:12 -05:00

View file

@ -1,378 +0,0 @@
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