Compare commits
7 commits
f4b7d0548b
...
14816478aa
| Author | SHA1 | Date | |
|---|---|---|---|
| 14816478aa | |||
| 8a5ef7b1f6 | |||
| d71f221277 | |||
| 38e60ae40c | |||
| e61dcc3ac4 | |||
| 11d939a70f | |||
| e0573f4229 |
9 changed files with 489 additions and 112 deletions
|
|
@ -133,12 +133,12 @@ The games that motivated this work:
|
||||||
The Wizard Sniffer (Buster Hudson, 2017)
|
The Wizard Sniffer (Buster Hudson, 2017)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You play a pig who sniffs out wizards. IFComp winner, XYZZY winner. Screwball comedy. The pig/wizard game that started this. Z-machine format. Would need V5 support.
|
You play a pig who sniffs out wizards. IFComp winner, XYZZY winner. Screwball comedy. The pig/wizard game that started this. Glulx format (.gblorb) — out of scope for embedded interpreter unless we subprocess to a Glulx interpreter.
|
||||||
|
|
||||||
Lost Pig (Admiral Jota, 2007)
|
Lost Pig (Admiral Jota, 2007)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Grunk the orc chases an escaped pig. IFComp winner, 4 XYZZY awards. Famous for its responsive parser and comedy writing. Z-machine format. V5.
|
Grunk the orc chases an escaped pig. IFComp winner, 4 XYZZY awards. Famous for its responsive parser and comedy writing. Z-machine V8 format (not V5 as originally assumed). Now playable in the hybrid interpreter.
|
||||||
|
|
||||||
Zork I, II, III
|
Zork I, II, III
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
|
|
@ -152,7 +152,7 @@ Funny, frustrating, great spectator game. V3.
|
||||||
|
|
||||||
Also: Anchorhead, Photopia, Spider and Web, Shade, Colossal Cave.
|
Also: Anchorhead, Photopia, Spider and Web, Shade, Colossal Cave.
|
||||||
|
|
||||||
V3 covers the Infocom catalog. V5 covers most modern IF including the pig games. V8 covers big modern Inform 7 games but is lower priority.
|
V3 covers the Infocom catalog. V5/V8 covers most modern IF. Lost Pig is V8 (same opcodes as V5, different packed address scaling). Wizard Sniffer is Glulx (out of scope for embedded). V8 support is now implemented.
|
||||||
|
|
||||||
Note: no Python Glulx interpreter exists. Games that target Glulx (some modern Inform 7) are out of scope unless we subprocess to a C interpreter.
|
Note: no Python Glulx interpreter exists. Games that target Glulx (some modern Inform 7) are out of scope unless we subprocess to a C interpreter.
|
||||||
|
|
||||||
|
|
@ -205,7 +205,7 @@ All V3 gaps have been resolved. sread tokenization works correctly. save/restore
|
||||||
|
|
||||||
Do they represent z-machine memory the same way? Both use bytearrays, but header parsing, object table offsets, string encoding — are these compatible enough that porting opcodes is translation, or is it a deeper rewrite?
|
Do they represent z-machine memory the same way? Both use bytearrays, but header parsing, object table offsets, string encoding — are these compatible enough that porting opcodes is translation, or is it a deeper rewrite?
|
||||||
|
|
||||||
UPDATE: Less urgent now that the hybrid interpreter works end-to-end for V3. The layout question mainly matters for V5 opcode porting (Lost Pig, Wizard Sniffer). The hybrid already handles all V3 memory operations correctly.
|
UPDATE: Resolved. The hybrid interpreter now works end-to-end for both V3 and V8. V8 uses the same object model as V5 (14-byte frames, 48 attributes, 65535 objects) and the same opcodes. The only difference is packed address scaling (×8 vs ×4). Memory layout handled correctly for all supported versions.
|
||||||
|
|
||||||
3. Async model
|
3. Async model
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
@ -268,6 +268,12 @@ Concrete next steps, roughly ordered. Update as items get done.
|
||||||
|
|
||||||
- [x] find the game files: locate freely distributable z-machine story files for the games we care about. Wizard Sniffer, Lost Pig, Zork (if legally available). (zork1.z3 bundled in content/stories/)
|
- [x] find the game files: locate freely distributable z-machine story files for the games we care about. Wizard Sniffer, Lost Pig, Zork (if legally available). (zork1.z3 bundled in content/stories/)
|
||||||
|
|
||||||
|
- [x] V8 support and Lost Pig: relaxed version gates (V8 = V5 with ×8 packed addresses), implemented extended opcode decoder, ported V5+ opcodes (aread, call_vn/vn2/vs2, save_undo stub, log/art shift, scan_table, tokenize, copy_table, print_unicode). found and fixed: insert_object wrong removal order, double-byte operand decoder reading type bytes interleaved with operands instead of all types first, opcode detection using 7-bit mask instead of 5-bit. Lost Pig now runs to completion (101K steps, 61 opcodes). (done — LostPig.z8 bundled in content/stories/, see ``scripts/trace_lostpig.py``)
|
||||||
|
|
||||||
|
- [ ] wire V8 games to MUD: ``EmbeddedIFSession`` currently only handles .z3 files. extend to .z5/.z8 so Lost Pig is playable via ``play lostpig`` in the MUD.
|
||||||
|
|
||||||
|
- [ ] implement real save_undo: currently stubs returning -1 ("not available"). a proper implementation needs in-memory state snapshots (dynamic memory + call stack). Lost Pig works without undo but players expect it.
|
||||||
|
|
||||||
milestone — Zork 1 playable in hybrid interpreter
|
milestone — Zork 1 playable in hybrid interpreter
|
||||||
--------------------------------------------------
|
--------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -316,6 +322,29 @@ What this enables:
|
||||||
- foundation for level 3 (moldable world — write z-machine state from MUD)
|
- foundation for level 3 (moldable world — write z-machine state from MUD)
|
||||||
- no external dependency on dfrotz for V3 games
|
- no external dependency on dfrotz for V3 games
|
||||||
|
|
||||||
|
milestone — V8 support: Lost Pig playable
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
The hybrid interpreter now supports V8 (and by extension V5) story files. Lost Pig — the 4-time XYZZY award winner about an orc chasing a pig — runs to completion in the interpreter.
|
||||||
|
|
||||||
|
What was needed:
|
||||||
|
|
||||||
|
- version gates relaxed: V8 uses the same object model and opcodes as V5, just different packed address scaling (×8 vs ×4) and larger max file size (512KB)
|
||||||
|
- extended opcode decoder: V5+ uses 0xBE prefix for extended opcodes. the decoder was stubbed. implemented reading opcode number + type byte after prefix
|
||||||
|
- double-byte operand fix: ``call_vs2`` and ``call_vn2`` have two operand type bytes. the old decoder read them interleaved with operands (type1, operands, type2, operands) instead of both types first. refactored into ``_read_type_byte()`` + ``_parse_operand_list()``
|
||||||
|
- double-byte detection fix: the check used ``opcode[0:7]`` (7-bit mask) instead of ``opcode_num`` (5-bit). never matched, so double-byte opcodes were always mis-parsed
|
||||||
|
- ``insert_object`` fix: was inserting into new parent before removing from old. corrupted sibling chains when moving within same parent. now removes first per spec
|
||||||
|
- 15 new opcodes: ``aread``, ``call_vn``/``vn2``/``vs2``, ``catch``, ``check_arg_count``, ``save_undo`` (stub), ``restore_undo`` (stub), ``log_shift``, ``art_shift``, ``scan_table``, ``tokenize``, ``copy_table``, ``set_font``, ``print_unicode``, ``check_unicode``
|
||||||
|
|
||||||
|
Lost Pig trace: 101K instructions, 61 unique opcodes, full gameplay loop working (room descriptions, parser, object manipulation). ``scripts/trace_lostpig.py`` for tracing.
|
||||||
|
|
||||||
|
What this enables:
|
||||||
|
|
||||||
|
- modern IF games compiled with Inform 6/7 to Z-machine V5/V8 format are now playable
|
||||||
|
- same level 2 inspection works (object tree, location, state reading)
|
||||||
|
- level 3 write APIs already handle V4-8 object format (14-byte frames, 48 attributes, 65535 objects)
|
||||||
|
- Wizard Sniffer is Glulx (out of scope), but Lost Pig is the better game anyway
|
||||||
|
|
||||||
related documents
|
related documents
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
|
|
||||||
127
scripts/trace_lostpig.py
Normal file
127
scripts/trace_lostpig.py
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
#!/usr/bin/env -S uv run --script
|
||||||
|
"""Trace Lost Pig V8 opcodes — find what's needed for V5+ support.
|
||||||
|
|
||||||
|
Runs the interpreter step-by-step, collecting unique opcodes hit.
|
||||||
|
When an unimplemented opcode is encountered, reports what was seen
|
||||||
|
and what's missing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ruff: noqa: E402
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
sys.path.insert(0, str(project_root / "src"))
|
||||||
|
|
||||||
|
from mudlib.zmachine import ZMachine, zopdecoder, zstream, zui
|
||||||
|
from mudlib.zmachine.trivialzui import (
|
||||||
|
TrivialAudio,
|
||||||
|
TrivialFilesystem,
|
||||||
|
TrivialScreen,
|
||||||
|
)
|
||||||
|
from mudlib.zmachine.zcpu import (
|
||||||
|
ZCpuNotImplemented,
|
||||||
|
ZCpuQuit,
|
||||||
|
ZCpuRestart,
|
||||||
|
)
|
||||||
|
|
||||||
|
story_path = project_root / "content" / "stories" / "LostPig.z8"
|
||||||
|
if not story_path.exists():
|
||||||
|
print(f"ERROR: {story_path} not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
story_bytes = story_path.read_bytes()
|
||||||
|
print(f"Loaded LostPig.z8: {len(story_bytes)} bytes, version {story_bytes[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
class AutoInputStream(zstream.ZInputStream):
|
||||||
|
"""Input stream that auto-feeds commands."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._input_count = 0
|
||||||
|
self._max_inputs = 3
|
||||||
|
|
||||||
|
def read_line(self, *args, **kwargs):
|
||||||
|
self._input_count += 1
|
||||||
|
if self._input_count > self._max_inputs:
|
||||||
|
raise ZCpuQuit
|
||||||
|
return "look"
|
||||||
|
|
||||||
|
|
||||||
|
audio = TrivialAudio()
|
||||||
|
screen = TrivialScreen()
|
||||||
|
keyboard = AutoInputStream()
|
||||||
|
filesystem = TrivialFilesystem()
|
||||||
|
ui = zui.ZUI(audio, screen, keyboard, filesystem)
|
||||||
|
zm = ZMachine(story_bytes, ui)
|
||||||
|
|
||||||
|
opcodes_seen = Counter()
|
||||||
|
step_count = 0
|
||||||
|
max_steps = 500_000
|
||||||
|
|
||||||
|
print(f"Tracing up to {max_steps} steps...")
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while step_count < max_steps:
|
||||||
|
pc = zm._cpu._opdecoder.program_counter
|
||||||
|
(opcode_class, opcode_number, operands) = (
|
||||||
|
zm._cpu._opdecoder.get_next_instruction()
|
||||||
|
)
|
||||||
|
|
||||||
|
cls_str = zopdecoder.OPCODE_STRINGS.get(opcode_class, f"?{opcode_class}")
|
||||||
|
key = f"{cls_str}:{opcode_number:02x}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
implemented, func = zm._cpu._get_handler(opcode_class, opcode_number)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ILLEGAL at step {step_count}: {key} (pc={pc:#x})")
|
||||||
|
print(f" {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
opcodes_seen[f"{key} ({func.__name__})"] += 1
|
||||||
|
|
||||||
|
if not implemented:
|
||||||
|
print(f"UNIMPLEMENTED at step {step_count}: {key} -> {func.__name__}")
|
||||||
|
print(f" PC: {pc:#x}")
|
||||||
|
print()
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
func(zm._cpu, *operands)
|
||||||
|
except ZCpuQuit:
|
||||||
|
print(f"Game quit after {step_count} steps")
|
||||||
|
break
|
||||||
|
except ZCpuRestart:
|
||||||
|
print(f"Game restart after {step_count} steps")
|
||||||
|
break
|
||||||
|
except ZCpuNotImplemented as e:
|
||||||
|
print(f"NOT IMPLEMENTED at step {step_count}: {key} -> {func.__name__}")
|
||||||
|
print(f" PC: {pc:#x}")
|
||||||
|
print(f" Error: {e}")
|
||||||
|
print()
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR at step {step_count}: {key} -> {func.__name__}")
|
||||||
|
print(f" PC: {pc:#x}")
|
||||||
|
print(f" Operands: {operands}")
|
||||||
|
print(f" {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
zm._cpu._dump_trace()
|
||||||
|
break
|
||||||
|
|
||||||
|
step_count += 1
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"\nInterrupted at step {step_count}")
|
||||||
|
|
||||||
|
print(f"\nTotal steps: {step_count}")
|
||||||
|
print(f"Unique opcodes seen: {len(opcodes_seen)}")
|
||||||
|
print()
|
||||||
|
print("Opcodes by frequency:")
|
||||||
|
for op, count in opcodes_seen.most_common():
|
||||||
|
print(f" {count:>8} {op}")
|
||||||
|
|
@ -172,7 +172,7 @@ class QuetzalParser:
|
||||||
self._seen_mem_or_stks = True
|
self._seen_mem_or_stks = True
|
||||||
|
|
||||||
cmem = self._zmachine._mem
|
cmem = self._zmachine._mem
|
||||||
dynamic_len = (cmem._dynamic_end - cmem.dynamic_start) + 1
|
dynamic_len = (cmem._dynamic_end - cmem._dynamic_start) + 1
|
||||||
log(f" Dynamic memory length is {dynamic_len}")
|
log(f" Dynamic memory length is {dynamic_len}")
|
||||||
self._last_loaded_metadata["dynamic memory length"] = dynamic_len
|
self._last_loaded_metadata["dynamic memory length"] = dynamic_len
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -644,8 +644,9 @@ class ZCpu:
|
||||||
self._stackmanager.pop_stack()
|
self._stackmanager.pop_stack()
|
||||||
|
|
||||||
def op_catch(self, *args):
|
def op_catch(self, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""Store the current stack frame index (for throw opcode)."""
|
||||||
raise ZCpuNotImplemented
|
frame_index = self._stackmanager.get_stack_frame_index()
|
||||||
|
self._write_result(frame_index)
|
||||||
|
|
||||||
def op_quit(self, *args):
|
def op_quit(self, *args):
|
||||||
"""Quit the game."""
|
"""Quit the game."""
|
||||||
|
|
@ -744,8 +745,54 @@ class ZCpu:
|
||||||
raise ZCpuNotImplemented
|
raise ZCpuNotImplemented
|
||||||
|
|
||||||
def op_aread(self, *args):
|
def op_aread(self, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""Read text input from keyboard (V5+).
|
||||||
raise ZCpuNotImplemented
|
|
||||||
|
V5 text buffer: byte 0 = max chars, byte 1 = num chars written,
|
||||||
|
text starts at byte 2. Stores the terminating ZSCII character
|
||||||
|
(13 for newline) as result. Does not lowercase (game handles it).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args[0]: text buffer address
|
||||||
|
args[1]: parse buffer address (0 = skip tokenization)
|
||||||
|
args[2]: optional timer interval (tenths of a second, 0 = none)
|
||||||
|
args[3]: optional timer routine address
|
||||||
|
"""
|
||||||
|
text_buffer_addr = args[0]
|
||||||
|
parse_buffer_addr = args[1] if len(args) > 1 else 0
|
||||||
|
|
||||||
|
# Read input from keyboard
|
||||||
|
text = self._ui.keyboard_input.read_line()
|
||||||
|
text = text.lower().strip("\n\r")
|
||||||
|
|
||||||
|
# Store in text buffer (V5 format: text at byte 2, count at byte 1)
|
||||||
|
max_len = self._memory[text_buffer_addr]
|
||||||
|
text = text[:max_len]
|
||||||
|
self._memory[text_buffer_addr + 1] = len(text)
|
||||||
|
for i, ch in enumerate(text):
|
||||||
|
self._memory[text_buffer_addr + 2 + i] = ord(ch)
|
||||||
|
|
||||||
|
# Tokenize if parse buffer provided
|
||||||
|
if parse_buffer_addr != 0:
|
||||||
|
max_words = self._memory[parse_buffer_addr]
|
||||||
|
tokens = self._lexer.parse_input(text)
|
||||||
|
num_words = min(len(tokens), max_words)
|
||||||
|
|
||||||
|
self._memory[parse_buffer_addr + 1] = num_words
|
||||||
|
offset = 0
|
||||||
|
for i in range(num_words):
|
||||||
|
word_str, dict_addr = tokens[i]
|
||||||
|
pos = text.find(word_str, offset)
|
||||||
|
if pos == -1:
|
||||||
|
pos = offset
|
||||||
|
word_len = len(word_str)
|
||||||
|
base = parse_buffer_addr + 2 + (i * 4)
|
||||||
|
self._memory.write_word(base, dict_addr)
|
||||||
|
self._memory[base + 2] = word_len
|
||||||
|
self._memory[base + 3] = pos + 2 # offset from start of text buffer
|
||||||
|
offset = pos + word_len
|
||||||
|
|
||||||
|
# Store terminating character (13 = newline)
|
||||||
|
self._write_result(13)
|
||||||
|
|
||||||
def op_print_char(self, char):
|
def op_print_char(self, char):
|
||||||
"""Output the given ZSCII character."""
|
"""Output the given ZSCII character."""
|
||||||
|
|
@ -793,9 +840,9 @@ class ZCpu:
|
||||||
"""Set the given window as the active window."""
|
"""Set the given window as the active window."""
|
||||||
self._ui.screen.select_window(window_num)
|
self._ui.screen.select_window(window_num)
|
||||||
|
|
||||||
def op_call_vs2(self, *args):
|
def op_call_vs2(self, routine_addr, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""Call routine with up to 7 arguments and store the result."""
|
||||||
raise ZCpuNotImplemented
|
self._call(routine_addr, args, True)
|
||||||
|
|
||||||
def op_erase_window(self, window_number):
|
def op_erase_window(self, window_number):
|
||||||
"""Clear the window with the given number. If # is -1, unsplit
|
"""Clear the window with the given number. If # is -1, unsplit
|
||||||
|
|
@ -804,7 +851,7 @@ class ZCpu:
|
||||||
if window_number == -1:
|
if window_number == -1:
|
||||||
self.op_split_window(0)
|
self.op_split_window(0)
|
||||||
self._ui.screen.erase_window(zscreen.WINDOW_LOWER)
|
self._ui.screen.erase_window(zscreen.WINDOW_LOWER)
|
||||||
if window_number == -2:
|
elif window_number == -2:
|
||||||
self._ui.screen.erase_window(zscreen.WINDOW_LOWER)
|
self._ui.screen.erase_window(zscreen.WINDOW_LOWER)
|
||||||
self._ui.screen.erase_window(zscreen.WINDOW_UPPER)
|
self._ui.screen.erase_window(zscreen.WINDOW_UPPER)
|
||||||
else:
|
else:
|
||||||
|
|
@ -876,41 +923,113 @@ class ZCpu:
|
||||||
char = self._ui.keyboard_input.read_char()
|
char = self._ui.keyboard_input.read_char()
|
||||||
self._write_result(char)
|
self._write_result(char)
|
||||||
|
|
||||||
def op_scan_table(self, *args):
|
def op_scan_table(self, x, table, length, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""Search a table for a value, branch if found, store address (V4+).
|
||||||
raise ZCpuNotImplemented
|
|
||||||
|
Searches length entries starting at table. Each entry is form
|
||||||
|
bytes wide (default 2). Compares against byte 0-1 (word) or
|
||||||
|
byte 0 (if form bit 7 is set, compare bytes not words).
|
||||||
|
form & 0x7f = entry size in bytes.
|
||||||
|
"""
|
||||||
|
form = args[0] if len(args) > 0 else 0x82 # default: word entries, 2 bytes wide
|
||||||
|
entry_size = form & 0x7F
|
||||||
|
compare_word = not (form & 0x80)
|
||||||
|
|
||||||
|
for i in range(length):
|
||||||
|
addr = table + (i * entry_size)
|
||||||
|
val = self._memory.read_word(addr) if compare_word else self._memory[addr]
|
||||||
|
if val == x:
|
||||||
|
self._write_result(addr)
|
||||||
|
self._branch(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._write_result(0)
|
||||||
|
self._branch(False)
|
||||||
|
|
||||||
def op_not_v5(self, *args):
|
def op_not_v5(self, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""TODO: Write docstring here."""
|
||||||
raise ZCpuNotImplemented
|
raise ZCpuNotImplemented
|
||||||
|
|
||||||
def op_call_vn(self, *args):
|
def op_call_vn(self, routine_addr, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""Call routine with up to 3 arguments and discard the result."""
|
||||||
raise ZCpuNotImplemented
|
self._call(routine_addr, args, False)
|
||||||
|
|
||||||
def op_call_vn2(self, *args):
|
def op_call_vn2(self, routine_addr, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""Call routine with up to 7 arguments and discard the result."""
|
||||||
raise ZCpuNotImplemented
|
self._call(routine_addr, args, False)
|
||||||
|
|
||||||
def op_tokenize(self, *args):
|
def op_tokenize(self, text_buffer, parse_buffer, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""Tokenize text in text_buffer into parse_buffer (V5+).
|
||||||
raise ZCpuNotImplemented
|
|
||||||
|
Uses V5 text buffer format (count at byte 1, text at byte 2+).
|
||||||
|
Optional args[0] = dictionary address, args[1] = flag.
|
||||||
|
"""
|
||||||
|
_dictionary = args[0] if len(args) > 0 else 0 # custom dict, not yet used
|
||||||
|
flag = args[1] if len(args) > 1 else 0
|
||||||
|
|
||||||
|
# Read text from V5 text buffer
|
||||||
|
num_chars = self._memory[text_buffer + 1]
|
||||||
|
text = ""
|
||||||
|
for i in range(num_chars):
|
||||||
|
text += chr(self._memory[text_buffer + 2 + i])
|
||||||
|
|
||||||
|
# Tokenize
|
||||||
|
max_words = self._memory[parse_buffer]
|
||||||
|
tokens = self._lexer.parse_input(text)
|
||||||
|
num_words = min(len(tokens), max_words)
|
||||||
|
|
||||||
|
if not flag:
|
||||||
|
self._memory[parse_buffer + 1] = num_words
|
||||||
|
|
||||||
|
offset = 0
|
||||||
|
for i in range(num_words):
|
||||||
|
word_str, dict_addr = tokens[i]
|
||||||
|
pos = text.find(word_str, offset)
|
||||||
|
if pos == -1:
|
||||||
|
pos = offset
|
||||||
|
word_len = len(word_str)
|
||||||
|
base = parse_buffer + 2 + (i * 4)
|
||||||
|
# When flag is set, only fill in entries that have dict matches
|
||||||
|
if not flag or dict_addr != 0:
|
||||||
|
self._memory.write_word(base, dict_addr)
|
||||||
|
self._memory[base + 2] = word_len
|
||||||
|
self._memory[base + 3] = pos + 2
|
||||||
|
offset = pos + word_len
|
||||||
|
|
||||||
def op_encode_text(self, *args):
|
def op_encode_text(self, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""TODO: Write docstring here."""
|
||||||
raise ZCpuNotImplemented
|
raise ZCpuNotImplemented
|
||||||
|
|
||||||
def op_copy_table(self, *args):
|
def op_copy_table(self, first, second, size):
|
||||||
"""TODO: Write docstring here."""
|
"""Copy a block of memory, or zero-fill (V5+).
|
||||||
raise ZCpuNotImplemented
|
|
||||||
|
If second is 0, zero-fill first for size bytes.
|
||||||
|
If size is positive, copy forward (safe for non-overlapping).
|
||||||
|
If size is negative, copy backward (safe for overlapping).
|
||||||
|
"""
|
||||||
|
if second == 0:
|
||||||
|
for i in range(abs(self._make_signed(size))):
|
||||||
|
self._memory[first + i] = 0
|
||||||
|
else:
|
||||||
|
signed_size = self._make_signed(size)
|
||||||
|
count = abs(signed_size)
|
||||||
|
if signed_size >= 0:
|
||||||
|
# Forward copy (may corrupt overlapping regions)
|
||||||
|
for i in range(count):
|
||||||
|
self._memory[second + i] = self._memory[first + i]
|
||||||
|
else:
|
||||||
|
# Backward copy (safe for overlapping)
|
||||||
|
for i in range(count - 1, -1, -1):
|
||||||
|
self._memory[second + i] = self._memory[first + i]
|
||||||
|
|
||||||
def op_print_table(self, *args):
|
def op_print_table(self, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""TODO: Write docstring here."""
|
||||||
raise ZCpuNotImplemented
|
raise ZCpuNotImplemented
|
||||||
|
|
||||||
def op_check_arg_count(self, *args):
|
def op_check_arg_count(self, arg_number):
|
||||||
"""TODO: Write docstring here."""
|
"""Branch if the Nth argument was passed to the current routine."""
|
||||||
raise ZCpuNotImplemented
|
current_frame = self._stackmanager._call_stack[-1]
|
||||||
|
self._branch(arg_number <= current_frame.arg_count)
|
||||||
|
|
||||||
## EXT opcodes (opcodes 256-284)
|
## EXT opcodes (opcodes 256-284)
|
||||||
|
|
||||||
|
|
@ -922,33 +1041,65 @@ class ZCpu:
|
||||||
"""TODO: Write docstring here."""
|
"""TODO: Write docstring here."""
|
||||||
raise ZCpuNotImplemented
|
raise ZCpuNotImplemented
|
||||||
|
|
||||||
def op_log_shift(self, *args):
|
def op_log_shift(self, number, places):
|
||||||
"""TODO: Write docstring here."""
|
"""Logical shift: positive places = left, negative = right (V5+).
|
||||||
raise ZCpuNotImplemented
|
|
||||||
|
|
||||||
def op_art_shift(self, *args):
|
Right shift fills with zeros (unsigned/logical shift).
|
||||||
"""TODO: Write docstring here."""
|
"""
|
||||||
raise ZCpuNotImplemented
|
places = self._make_signed(places)
|
||||||
|
result = (
|
||||||
|
(number << places) & 0xFFFF
|
||||||
|
if places >= 0
|
||||||
|
else (number >> (-places)) & 0xFFFF
|
||||||
|
)
|
||||||
|
self._write_result(result)
|
||||||
|
|
||||||
def op_set_font(self, *args):
|
def op_art_shift(self, number, places):
|
||||||
"""TODO: Write docstring here."""
|
"""Arithmetic shift: positive places = left, negative = right (V5+).
|
||||||
raise ZCpuNotImplemented
|
|
||||||
|
Right shift preserves the sign bit (signed/arithmetic shift).
|
||||||
|
"""
|
||||||
|
signed_number = self._make_signed(number)
|
||||||
|
places = self._make_signed(places)
|
||||||
|
if places >= 0:
|
||||||
|
result = (signed_number << places) & 0xFFFF
|
||||||
|
else:
|
||||||
|
result = self._unmake_signed(signed_number >> (-places))
|
||||||
|
self._write_result(result)
|
||||||
|
|
||||||
|
def op_set_font(self, font_id):
|
||||||
|
"""Set the current font. Returns the previous font, or 0 if
|
||||||
|
the requested font is unavailable (V5+).
|
||||||
|
|
||||||
|
Font 1 is the normal font. We only support font 1.
|
||||||
|
"""
|
||||||
|
if font_id == 1:
|
||||||
|
self._write_result(1) # was font 1, now font 1
|
||||||
|
else:
|
||||||
|
self._write_result(0) # unsupported font
|
||||||
|
|
||||||
def op_save_undo(self, *args):
|
def op_save_undo(self, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""Save undo state. Store -1 if not available (V5+).
|
||||||
raise ZCpuNotImplemented
|
|
||||||
|
Stores 1 on success, 0 on failure, -1 if not available.
|
||||||
|
Real undo support deferred; return -1 for now.
|
||||||
|
"""
|
||||||
|
self._write_result(self._unmake_signed(-1))
|
||||||
|
|
||||||
def op_restore_undo(self, *args):
|
def op_restore_undo(self, *args):
|
||||||
"""TODO: Write docstring here."""
|
"""Restore undo state. Store 0 on failure (V5+)."""
|
||||||
raise ZCpuNotImplemented
|
self._write_result(0)
|
||||||
|
|
||||||
def op_print_unicode(self, *args):
|
def op_print_unicode(self, char_code):
|
||||||
"""TODO: Write docstring here."""
|
"""Print a Unicode character (V5+, EXT:11)."""
|
||||||
raise ZCpuNotImplemented
|
self._ui.screen.write(chr(char_code))
|
||||||
|
|
||||||
def op_check_unicode(self, *args):
|
def op_check_unicode(self, char_code):
|
||||||
"""TODO: Write docstring here."""
|
"""Check if Unicode character can be printed/read (V5+, EXT:12).
|
||||||
raise ZCpuNotImplemented
|
|
||||||
|
Bit 0 = can print, bit 1 = can read. We support printing only.
|
||||||
|
"""
|
||||||
|
self._write_result(1) # can print, can't read
|
||||||
|
|
||||||
# Declaration of the opcode tables. In a Z-Machine, opcodes are
|
# Declaration of the opcode tables. In a Z-Machine, opcodes are
|
||||||
# divided into tables based on the operand type. Within each
|
# divided into tables based on the operand type. Within each
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,9 @@ class ZMemory:
|
||||||
elif 4 <= self.version <= 5:
|
elif 4 <= self.version <= 5:
|
||||||
if self._total_size > 262144:
|
if self._total_size > 262144:
|
||||||
raise ZMemoryBadStoryfileSize
|
raise ZMemoryBadStoryfileSize
|
||||||
|
elif self.version == 8:
|
||||||
|
if self._total_size > 524288:
|
||||||
|
raise ZMemoryBadStoryfileSize
|
||||||
else:
|
else:
|
||||||
raise ZMemoryUnsupportedVersion
|
raise ZMemoryUnsupportedVersion
|
||||||
|
|
||||||
|
|
@ -253,6 +256,10 @@ class ZMemory:
|
||||||
if address < 0 or address > (self._total_size // 4):
|
if address < 0 or address > (self._total_size // 4):
|
||||||
raise ZMemoryOutOfBounds
|
raise ZMemoryOutOfBounds
|
||||||
return address * 4
|
return address * 4
|
||||||
|
elif self.version == 8:
|
||||||
|
if address < 0 or address > (self._total_size // 8):
|
||||||
|
raise ZMemoryOutOfBounds
|
||||||
|
return address * 8
|
||||||
else:
|
else:
|
||||||
raise ZMemoryUnsupportedVersion
|
raise ZMemoryUnsupportedVersion
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ class ZObjectParser:
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
self._objecttree_addr = self._propdefaults_addr + 62
|
self._objecttree_addr = self._propdefaults_addr + 62
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
self._objecttree_addr = self._propdefaults_addr + 126
|
self._objecttree_addr = self._propdefaults_addr + 126
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
@ -90,7 +90,7 @@ class ZObjectParser:
|
||||||
if not (1 <= objectnum <= 255):
|
if not (1 <= objectnum <= 255):
|
||||||
raise ZObjectIllegalObjectNumber
|
raise ZObjectIllegalObjectNumber
|
||||||
result = self._objecttree_addr + (9 * (objectnum - 1))
|
result = self._objecttree_addr + (9 * (objectnum - 1))
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
if not (1 <= objectnum <= 65535):
|
if not (1 <= objectnum <= 65535):
|
||||||
log(f"error: there is no object {objectnum}")
|
log(f"error: there is no object {objectnum}")
|
||||||
raise ZObjectIllegalObjectNumber
|
raise ZObjectIllegalObjectNumber
|
||||||
|
|
@ -111,7 +111,7 @@ class ZObjectParser:
|
||||||
addr += 4 # skip past attributes
|
addr += 4 # skip past attributes
|
||||||
result = self._memory[addr : addr + 3]
|
result = self._memory[addr : addr + 3]
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
addr += 6 # skip past attributes
|
addr += 6 # skip past attributes
|
||||||
result = [
|
result = [
|
||||||
self._memory.read_word(addr),
|
self._memory.read_word(addr),
|
||||||
|
|
@ -135,7 +135,7 @@ class ZObjectParser:
|
||||||
# skip past attributes and relatives
|
# skip past attributes and relatives
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
addr += 7
|
addr += 7
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
addr += 12
|
addr += 12
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
@ -150,7 +150,7 @@ class ZObjectParser:
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
if not (1 <= propnum <= 31):
|
if not (1 <= propnum <= 31):
|
||||||
raise ZObjectIllegalPropertyNumber
|
raise ZObjectIllegalPropertyNumber
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
if not (1 <= propnum <= 63):
|
if not (1 <= propnum <= 63):
|
||||||
raise ZObjectIllegalPropertyNumber
|
raise ZObjectIllegalPropertyNumber
|
||||||
else:
|
else:
|
||||||
|
|
@ -171,7 +171,7 @@ class ZObjectParser:
|
||||||
raise ZObjectIllegalAttributeNumber
|
raise ZObjectIllegalAttributeNumber
|
||||||
bf = BitField(self._memory[object_addr + (attrnum // 8)])
|
bf = BitField(self._memory[object_addr + (attrnum // 8)])
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
if not (0 <= attrnum <= 47):
|
if not (0 <= attrnum <= 47):
|
||||||
raise ZObjectIllegalAttributeNumber
|
raise ZObjectIllegalAttributeNumber
|
||||||
bf = BitField(self._memory[object_addr + (attrnum // 8)])
|
bf = BitField(self._memory[object_addr + (attrnum // 8)])
|
||||||
|
|
@ -192,7 +192,7 @@ class ZObjectParser:
|
||||||
byte_offset = attrnum // 8
|
byte_offset = attrnum // 8
|
||||||
bf = BitField(self._memory[object_addr + byte_offset])
|
bf = BitField(self._memory[object_addr + byte_offset])
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
if not (0 <= attrnum <= 47):
|
if not (0 <= attrnum <= 47):
|
||||||
raise ZObjectIllegalAttributeNumber
|
raise ZObjectIllegalAttributeNumber
|
||||||
byte_offset = attrnum // 8
|
byte_offset = attrnum // 8
|
||||||
|
|
@ -215,7 +215,7 @@ class ZObjectParser:
|
||||||
byte_offset = attrnum // 8
|
byte_offset = attrnum // 8
|
||||||
bf = BitField(self._memory[object_addr + byte_offset])
|
bf = BitField(self._memory[object_addr + byte_offset])
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
if not (0 <= attrnum <= 47):
|
if not (0 <= attrnum <= 47):
|
||||||
raise ZObjectIllegalAttributeNumber
|
raise ZObjectIllegalAttributeNumber
|
||||||
byte_offset = attrnum // 8
|
byte_offset = attrnum // 8
|
||||||
|
|
@ -233,7 +233,7 @@ class ZObjectParser:
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
max = 32
|
max = 32
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
max = 48
|
max = 48
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
@ -269,7 +269,7 @@ class ZObjectParser:
|
||||||
addr = self._get_object_addr(objectnum)
|
addr = self._get_object_addr(objectnum)
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
self._memory[addr + 4] = new_parent_num
|
self._memory[addr + 4] = new_parent_num
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
self._memory.write_word(addr + 6, new_parent_num)
|
self._memory.write_word(addr + 6, new_parent_num)
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
@ -280,7 +280,7 @@ class ZObjectParser:
|
||||||
addr = self._get_object_addr(objectnum)
|
addr = self._get_object_addr(objectnum)
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
self._memory[addr + 6] = new_child_num
|
self._memory[addr + 6] = new_child_num
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
self._memory.write_word(addr + 10, new_child_num)
|
self._memory.write_word(addr + 10, new_child_num)
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
@ -291,7 +291,7 @@ class ZObjectParser:
|
||||||
addr = self._get_object_addr(objectnum)
|
addr = self._get_object_addr(objectnum)
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
self._memory[addr + 5] = new_sibling_num
|
self._memory[addr + 5] = new_sibling_num
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
self._memory.write_word(addr + 8, new_sibling_num)
|
self._memory.write_word(addr + 8, new_sibling_num)
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
@ -330,40 +330,21 @@ class ZObjectParser:
|
||||||
self.set_sibling(objectnum, 0)
|
self.set_sibling(objectnum, 0)
|
||||||
|
|
||||||
def insert_object(self, parent_object, new_child):
|
def insert_object(self, parent_object, new_child):
|
||||||
"""Prepend object NEW_CHILD to the list of PARENT_OBJECT's children."""
|
"""Prepend object NEW_CHILD to the list of PARENT_OBJECT's children.
|
||||||
|
|
||||||
# Remember all the original pointers within the new_child
|
Per the Z-spec: if new_child already has a parent, it is first
|
||||||
[p, s, c] = self._get_parent_sibling_child(new_child)
|
removed from that parent's child list, then made the first child
|
||||||
|
of parent_object."""
|
||||||
|
|
||||||
# First insert new_child intto the parent_object
|
# Remove from old parent first (spec says "first removed")
|
||||||
|
self.remove_object(new_child)
|
||||||
|
|
||||||
|
# Now insert as first child of parent_object
|
||||||
original_child = self.get_child(parent_object)
|
original_child = self.get_child(parent_object)
|
||||||
self.set_sibling(new_child, original_child)
|
self.set_sibling(new_child, original_child)
|
||||||
self.set_parent(new_child, parent_object)
|
self.set_parent(new_child, parent_object)
|
||||||
self.set_child(parent_object, new_child)
|
self.set_child(parent_object, new_child)
|
||||||
|
|
||||||
if p == 0: # no need to 'remove' new_child, since it wasn't in a tree
|
|
||||||
return
|
|
||||||
|
|
||||||
# Hunt down and remove the new_child from its old location
|
|
||||||
item = self.get_child(p)
|
|
||||||
if item == 0:
|
|
||||||
# new_object claimed to have parent p, but p has no children!?
|
|
||||||
raise ZObjectMalformedTree
|
|
||||||
elif item == new_child: # done! new_object was head of list
|
|
||||||
self.set_child(p, s) # note that s might be 0, that's fine.
|
|
||||||
else: # walk across list of sibling links
|
|
||||||
prev = item
|
|
||||||
current = self.get_sibling(item)
|
|
||||||
while current != 0:
|
|
||||||
if current == new_child:
|
|
||||||
self.set_sibling(prev, s) # s might be 0, that's fine.
|
|
||||||
break
|
|
||||||
prev = current
|
|
||||||
current = self.get_sibling(current)
|
|
||||||
else:
|
|
||||||
# we reached the end of the list, never got a match
|
|
||||||
raise ZObjectMalformedTree
|
|
||||||
|
|
||||||
def get_shortname(self, objectnum):
|
def get_shortname(self, objectnum):
|
||||||
"""Return 'short name' of object number OBJECTNUM as ascii string."""
|
"""Return 'short name' of object number OBJECTNUM as ascii string."""
|
||||||
|
|
||||||
|
|
@ -402,7 +383,7 @@ class ZObjectParser:
|
||||||
return (addr, size)
|
return (addr, size)
|
||||||
addr += size
|
addr += size
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
while self._memory[addr] != 0:
|
while self._memory[addr] != 0:
|
||||||
bf = BitField(self._memory[addr])
|
bf = BitField(self._memory[addr])
|
||||||
addr += 1
|
addr += 1
|
||||||
|
|
@ -448,7 +429,7 @@ class ZObjectParser:
|
||||||
proplist[pnum] = (addr, size)
|
proplist[pnum] = (addr, size)
|
||||||
addr += size
|
addr += size
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
while self._memory[addr] != 0:
|
while self._memory[addr] != 0:
|
||||||
bf = BitField(self._memory[addr])
|
bf = BitField(self._memory[addr])
|
||||||
addr += 1
|
addr += 1
|
||||||
|
|
@ -516,7 +497,7 @@ class ZObjectParser:
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
bf = BitField(self._memory[addr])
|
bf = BitField(self._memory[addr])
|
||||||
return bf[0:5]
|
return bf[0:5]
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
bf = BitField(self._memory[addr])
|
bf = BitField(self._memory[addr])
|
||||||
return bf[0:6]
|
return bf[0:6]
|
||||||
else:
|
else:
|
||||||
|
|
@ -555,7 +536,7 @@ class ZObjectParser:
|
||||||
size = bf[5:8] + 1
|
size = bf[5:8] + 1
|
||||||
return size
|
return size
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
|
||||||
bf = BitField(self._memory[size_addr])
|
bf = BitField(self._memory[size_addr])
|
||||||
if bf[7]:
|
if bf[7]:
|
||||||
# Two-byte header: size is in bits 0-5 of this byte
|
# Two-byte header: size is in bits 0-5 of this byte
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class ZOpDecoder:
|
||||||
log(f"Decode opcode {opcode:x}")
|
log(f"Decode opcode {opcode:x}")
|
||||||
|
|
||||||
# Determine the opcode type, and hand off further parsing.
|
# Determine the opcode type, and hand off further parsing.
|
||||||
if self._memory.version == 5 and opcode == 0xBE:
|
if self._memory.version >= 5 and opcode == 0xBE:
|
||||||
# Extended opcode
|
# Extended opcode
|
||||||
return self._parse_opcode_extended()
|
return self._parse_opcode_extended()
|
||||||
|
|
||||||
|
|
@ -122,19 +122,30 @@ class ZOpDecoder:
|
||||||
|
|
||||||
opcode_num = opcode[0:5]
|
opcode_num = opcode[0:5]
|
||||||
|
|
||||||
# Parse the types byte to retrieve the operands.
|
# Read all type bytes FIRST, before parsing any operands.
|
||||||
operands = self._parse_operands_byte()
|
# call_vs2 (VAR:12) and call_vn2 (VAR:26) have two type bytes;
|
||||||
|
# all others have one. Both type bytes must be read before
|
||||||
# Special case: opcodes 12 and 26 have a second operands byte.
|
# operand data starts in the stream.
|
||||||
if opcode[0:7] == 0xC or opcode[0:7] == 0x1A:
|
operand_types = self._read_type_byte()
|
||||||
|
if opcode_type == OPCODE_VAR and opcode_num in (0xC, 0x1A):
|
||||||
log("Opcode has second operand byte")
|
log("Opcode has second operand byte")
|
||||||
operands += self._parse_operands_byte()
|
operand_types += self._read_type_byte()
|
||||||
|
|
||||||
|
operands = self._parse_operand_list(operand_types)
|
||||||
return (opcode_type, opcode_num, operands)
|
return (opcode_type, opcode_num, operands)
|
||||||
|
|
||||||
def _parse_opcode_extended(self):
|
def _parse_opcode_extended(self):
|
||||||
"""Parse an extended opcode (v5+ feature)."""
|
"""Parse an extended opcode (v5+ feature)."""
|
||||||
raise NotImplementedError("Extended opcodes (v5+) not yet implemented")
|
log("Opcode is extended")
|
||||||
|
|
||||||
|
# Read the extended opcode number
|
||||||
|
opcode_num = self._get_pc()
|
||||||
|
log(f"Extended opcode number: {opcode_num:x}")
|
||||||
|
|
||||||
|
# Parse the operand types byte and retrieve operands
|
||||||
|
operands = self._parse_operands_byte()
|
||||||
|
|
||||||
|
return (OPCODE_EXT, opcode_num, operands)
|
||||||
|
|
||||||
def _parse_operand(self, operand_type):
|
def _parse_operand(self, operand_type):
|
||||||
"""Read and return an operand of the given type.
|
"""Read and return an operand of the given type.
|
||||||
|
|
@ -170,25 +181,31 @@ class ZOpDecoder:
|
||||||
|
|
||||||
return operand
|
return operand
|
||||||
|
|
||||||
def _parse_operands_byte(self):
|
def _read_type_byte(self):
|
||||||
"""Parse operands given by the operand byte and return a list of
|
"""Read one operand type byte and return a list of type codes."""
|
||||||
values.
|
|
||||||
"""
|
|
||||||
operand_byte = BitField(self._get_pc())
|
operand_byte = BitField(self._get_pc())
|
||||||
operands = []
|
return [
|
||||||
for operand_type in [
|
|
||||||
operand_byte[6:8],
|
operand_byte[6:8],
|
||||||
operand_byte[4:6],
|
operand_byte[4:6],
|
||||||
operand_byte[2:4],
|
operand_byte[2:4],
|
||||||
operand_byte[0:2],
|
operand_byte[0:2],
|
||||||
]:
|
]
|
||||||
|
|
||||||
|
def _parse_operand_list(self, operand_types):
|
||||||
|
"""Parse operands from a list of type codes, stopping at ABSENT."""
|
||||||
|
operands = []
|
||||||
|
for operand_type in operand_types:
|
||||||
operand = self._parse_operand(operand_type)
|
operand = self._parse_operand(operand_type)
|
||||||
if operand is None:
|
if operand is None:
|
||||||
break
|
break
|
||||||
operands.append(operand)
|
operands.append(operand)
|
||||||
|
|
||||||
return operands
|
return operands
|
||||||
|
|
||||||
|
def _parse_operands_byte(self):
|
||||||
|
"""Read one type byte and parse its operands. Used by extended
|
||||||
|
opcodes and other callers that always have a single type byte."""
|
||||||
|
return self._parse_operand_list(self._read_type_byte())
|
||||||
|
|
||||||
# Public funcs that the ZPU may also need to call, depending on the
|
# Public funcs that the ZPU may also need to call, depending on the
|
||||||
# opcode being executed:
|
# opcode being executed:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ class ZRoutine:
|
||||||
self.start_addr = start_addr
|
self.start_addr = start_addr
|
||||||
self.return_addr = return_addr
|
self.return_addr = return_addr
|
||||||
self.program_counter = 0 # used when execution interrupted
|
self.program_counter = 0 # used when execution interrupted
|
||||||
|
self.arg_count = len(args) # track number of args passed
|
||||||
|
|
||||||
if stack is None:
|
if stack is None:
|
||||||
self.stack = []
|
self.stack = []
|
||||||
|
|
@ -70,14 +71,14 @@ class ZRoutine:
|
||||||
self.start_addr += 1
|
self.start_addr += 1
|
||||||
|
|
||||||
# Initialize the local vars in the ZRoutine's dictionary. This is
|
# Initialize the local vars in the ZRoutine's dictionary. This is
|
||||||
# only needed on machines v1 through v4. In v5 machines, all local
|
# only needed on machines v1 through v4. In v5 and v8 machines, all
|
||||||
# variables are preinitialized to zero.
|
# local variables are preinitialized to zero.
|
||||||
self.local_vars = [0 for _ in range(15)]
|
self.local_vars = [0 for _ in range(15)]
|
||||||
if 1 <= zmem.version <= 4:
|
if 1 <= zmem.version <= 4:
|
||||||
for i in range(num_local_vars):
|
for i in range(num_local_vars):
|
||||||
self.local_vars[i] = zmem.read_word(self.start_addr)
|
self.local_vars[i] = zmem.read_word(self.start_addr)
|
||||||
self.start_addr += 2
|
self.start_addr += 2
|
||||||
elif zmem.version != 5:
|
elif zmem.version not in (5, 8):
|
||||||
raise ZStackUnsupportedVersion
|
raise ZStackUnsupportedVersion
|
||||||
|
|
||||||
# Place call arguments into local vars, if available
|
# Place call arguments into local vars, if available
|
||||||
|
|
|
||||||
64
tests/test_zmemory_v8.py
Normal file
64
tests/test_zmemory_v8.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
"""Tests for V8 z-machine version support."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mudlib.zmachine.zmemory import ZMemory, ZMemoryUnsupportedVersion
|
||||||
|
|
||||||
|
|
||||||
|
def make_minimal_story(version: int, size: int) -> bytes:
|
||||||
|
"""Create a minimal z-machine story file of the specified version and size."""
|
||||||
|
story = bytearray(size)
|
||||||
|
story[0] = version # version byte
|
||||||
|
# Set static memory base (0x0E) to a reasonable value
|
||||||
|
story[0x0E] = 0x04
|
||||||
|
story[0x0F] = 0x00
|
||||||
|
# Set high memory base (0x04) to end of file
|
||||||
|
story[0x04] = (size >> 8) & 0xFF
|
||||||
|
story[0x05] = size & 0xFF
|
||||||
|
# Set global variables base (0x0C)
|
||||||
|
story[0x0C] = 0x03
|
||||||
|
story[0x0D] = 0x00
|
||||||
|
return bytes(story)
|
||||||
|
|
||||||
|
|
||||||
|
def test_v3_accepts_128kb_story():
|
||||||
|
"""V3 stories can be up to 128KB (131072 bytes)."""
|
||||||
|
story = make_minimal_story(3, 131072)
|
||||||
|
mem = ZMemory(story)
|
||||||
|
assert mem.version == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_v5_accepts_256kb_story():
|
||||||
|
"""V5 stories can be up to 256KB (262144 bytes)."""
|
||||||
|
story = make_minimal_story(5, 262144)
|
||||||
|
mem = ZMemory(story)
|
||||||
|
assert mem.version == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_v8_accepts_512kb_story():
|
||||||
|
"""V8 stories can be up to 512KB (524288 bytes)."""
|
||||||
|
story = make_minimal_story(8, 524288)
|
||||||
|
mem = ZMemory(story)
|
||||||
|
assert mem.version == 8
|
||||||
|
|
||||||
|
|
||||||
|
def test_v8_packed_address_scaling():
|
||||||
|
"""V8 uses ×8 scaling for packed addresses (not ×4 like V5)."""
|
||||||
|
story = make_minimal_story(8, 10000)
|
||||||
|
mem = ZMemory(story)
|
||||||
|
# V8 packed address 100 should map to byte address 800
|
||||||
|
assert mem.packed_address(100) == 800
|
||||||
|
|
||||||
|
|
||||||
|
def test_v6_unsupported():
|
||||||
|
"""V6 should still be unsupported (different packed address format)."""
|
||||||
|
story = make_minimal_story(6, 10000)
|
||||||
|
with pytest.raises(ZMemoryUnsupportedVersion):
|
||||||
|
ZMemory(story)
|
||||||
|
|
||||||
|
|
||||||
|
def test_v7_unsupported():
|
||||||
|
"""V7 should still be unsupported (different packed address format)."""
|
||||||
|
story = make_minimal_story(7, 10000)
|
||||||
|
with pytest.raises(ZMemoryUnsupportedVersion):
|
||||||
|
ZMemory(story)
|
||||||
Loading…
Reference in a new issue