step_fast() never recorded trace entries, so crash dumps always showed an empty trace. Now records PC + opcode info in the same deque as step(). Also includes exception type in player-facing error messages when the exception string is empty.
1446 lines
51 KiB
Python
1446 lines
51 KiB
Python
#
|
|
# A class which represents the CPU itself, the brain of the virtual
|
|
# machine. It ties all the systems together and runs the story.
|
|
#
|
|
# For the license of this file, please consult the LICENSE file in the
|
|
# root directory of this distribution.
|
|
#
|
|
|
|
import random
|
|
import time
|
|
from collections import deque
|
|
|
|
from . import zopdecoder, zscreen
|
|
from .zlogging import log, log_disasm
|
|
|
|
|
|
class ZCpuError(Exception):
|
|
"General exception for Zcpu class"
|
|
|
|
|
|
class ZCpuOpcodeOverlap(ZCpuError):
|
|
"Overlapping opcodes registered"
|
|
|
|
|
|
class ZCpuIllegalInstruction(ZCpuError):
|
|
"Illegal instruction encountered"
|
|
|
|
|
|
class ZCpuDivideByZero(ZCpuError):
|
|
"Divide by zero error"
|
|
|
|
|
|
class ZCpuNotImplemented(ZCpuError):
|
|
"Opcode not yet implemented"
|
|
|
|
|
|
class ZCpuQuit(ZCpuError):
|
|
"Quit opcode executed"
|
|
|
|
|
|
class ZCpuRestart(ZCpuError):
|
|
"Restart opcode executed"
|
|
|
|
|
|
class ZCpu:
|
|
def __init__(
|
|
self,
|
|
zmem,
|
|
zopdecoder,
|
|
zstack,
|
|
zobjects,
|
|
zstring,
|
|
zstreammanager,
|
|
zui,
|
|
zlexer,
|
|
zmachine=None,
|
|
):
|
|
self._memory = zmem
|
|
self._opdecoder = zopdecoder
|
|
self._stackmanager = zstack
|
|
self._objects = zobjects
|
|
self._string = zstring
|
|
self._streammanager = zstreammanager
|
|
self._ui = zui
|
|
self._lexer = zlexer
|
|
self._zmachine = zmachine
|
|
self._trace = deque(maxlen=20)
|
|
self._undo_snapshot = None
|
|
self._dispatch = self._build_dispatch_table()
|
|
|
|
@property
|
|
def _program_counter(self):
|
|
"""Return the current program counter value."""
|
|
return self._opdecoder.program_counter
|
|
|
|
def _get_handler(self, opcode_class, opcode_number):
|
|
try:
|
|
opcode_decl = self.opcodes[opcode_class][opcode_number]
|
|
except IndexError:
|
|
opcode_decl = None
|
|
if not opcode_decl:
|
|
raise ZCpuIllegalInstruction
|
|
|
|
# If the opcode declaration is a sequence, we have extra
|
|
# thinking to do.
|
|
if not isinstance(opcode_decl, (list, tuple)):
|
|
opcode_func = opcode_decl
|
|
else:
|
|
# We have several different implementations for the
|
|
# opcode, and we need to select the right one based on
|
|
# version.
|
|
if isinstance(opcode_decl[0], (list, tuple)):
|
|
for func, version in opcode_decl:
|
|
if version <= self._memory.version:
|
|
opcode_func = func
|
|
break
|
|
# Only one implementation, check that our machine is
|
|
# recent enough.
|
|
elif opcode_decl[1] <= self._memory.version:
|
|
opcode_func = opcode_decl[0]
|
|
else:
|
|
raise ZCpuIllegalInstruction
|
|
|
|
# The following is a hack, based on our policy of only
|
|
# documenting opcodes we implement. If we ever hit an
|
|
# undocumented opcode, we crash with a not implemented
|
|
# error.
|
|
if not opcode_func.__doc__:
|
|
return False, opcode_func
|
|
else:
|
|
return True, opcode_func
|
|
|
|
def _make_signed(self, a):
|
|
"""Turn the given 16-bit value into a signed integer."""
|
|
assert a < (1 << 16)
|
|
if (a >> 15) & 1:
|
|
a = a - (1 << 16)
|
|
return a
|
|
|
|
def _unmake_signed(self, a):
|
|
"""Turn the given signed integer into a 16-bit value ready for
|
|
storage."""
|
|
if a < 0:
|
|
a = (1 << 16) + a
|
|
return a
|
|
|
|
def _read_variable(self, addr):
|
|
"""Return the value of the given variable, which can come from
|
|
the stack, or from a local/global variable. If it comes from
|
|
the stack, the value is popped from the stack."""
|
|
if addr == 0x0:
|
|
return self._stackmanager.pop_stack()
|
|
elif 0x0 < addr < 0x10:
|
|
return self._stackmanager.get_local_variable(addr - 1)
|
|
else:
|
|
return self._memory.read_global(addr)
|
|
|
|
def _write_result(self, result_value, store_addr=None):
|
|
"""Write the given result value to the stack or to a
|
|
local/global variable. Write result_value to the store_addr
|
|
variable, or if None, extract the destination variable from
|
|
the opcode."""
|
|
if store_addr is None:
|
|
result_addr = self._opdecoder.get_store_address()
|
|
else:
|
|
result_addr = store_addr
|
|
|
|
if result_addr is not None:
|
|
if result_addr == 0x0:
|
|
log(f"Push {result_value} to stack")
|
|
self._stackmanager.push_stack(result_value)
|
|
elif 0x0 < result_addr < 0x10:
|
|
log(f"Local variable {result_addr - 1} = {result_value}")
|
|
self._stackmanager.set_local_variable(result_addr - 1, result_value)
|
|
else:
|
|
log(f"Global variable {result_addr} = {result_value}")
|
|
self._memory.write_global(result_addr, result_value)
|
|
|
|
def _call(self, routine_address, args, store_return_value):
|
|
"""Set up a function call to the given routine address,
|
|
passing the given arguments. If store_return_value is True,
|
|
the routine's return value will be stored."""
|
|
# Calling address 0 is a no-op that returns false (0).
|
|
if routine_address == 0:
|
|
if store_return_value:
|
|
self._write_result(0)
|
|
return
|
|
addr = self._memory.packed_address(routine_address)
|
|
if store_return_value:
|
|
return_value = self._opdecoder.get_store_address()
|
|
else:
|
|
return_value = None
|
|
current_addr = self._opdecoder.program_counter
|
|
new_addr = self._stackmanager.start_routine(
|
|
addr, return_value, current_addr, args
|
|
)
|
|
self._opdecoder.program_counter = new_addr
|
|
|
|
def _branch(self, test_result):
|
|
"""Retrieve the branch information, and set the instruction
|
|
pointer according to the type of branch and the test_result."""
|
|
branch_cond, branch_offset = self._opdecoder.get_branch_offset()
|
|
|
|
if test_result == branch_cond:
|
|
if branch_offset == 0 or branch_offset == 1:
|
|
log(f"Return from routine with {branch_offset}")
|
|
addr = self._stackmanager.finish_routine(branch_offset)
|
|
self._opdecoder.program_counter = addr
|
|
else:
|
|
log(f"Jump to offset {branch_offset:+d}")
|
|
self._opdecoder.program_counter += branch_offset - 2
|
|
|
|
def _dump_trace(self):
|
|
"""Print the last N instructions for debugging."""
|
|
print("\n=== INSTRUCTION TRACE (last 20) ===")
|
|
for entry in self._trace:
|
|
print(entry)
|
|
print("===================================\n")
|
|
|
|
def _build_dispatch_table(self):
|
|
"""Pre-resolve all opcode handlers for current version."""
|
|
table = {}
|
|
for opcode_class, opcode_class_list in self.opcodes.items():
|
|
class_table = []
|
|
for opcode_decl in opcode_class_list:
|
|
if not opcode_decl:
|
|
class_table.append(None)
|
|
continue
|
|
if not isinstance(opcode_decl, (list, tuple)):
|
|
func = opcode_decl
|
|
else:
|
|
func = None
|
|
if isinstance(opcode_decl[0], (list, tuple)):
|
|
for f, version in opcode_decl: # type: ignore
|
|
if version <= self._memory.version:
|
|
func = f
|
|
break
|
|
elif opcode_decl[1] <= self._memory.version:
|
|
func = opcode_decl[0]
|
|
if func is None:
|
|
class_table.append(None)
|
|
continue
|
|
implemented = bool(func.__doc__)
|
|
class_table.append((implemented, func))
|
|
table[opcode_class] = class_table
|
|
return table
|
|
|
|
def step_fast(self):
|
|
"""Execute a single instruction with lightweight tracing.
|
|
|
|
Returns True if execution should continue.
|
|
"""
|
|
current_pc = self._opdecoder.program_counter
|
|
(opcode_class, opcode_number, operands) = self._opdecoder.get_next_instruction()
|
|
entry = self._dispatch[opcode_class][opcode_number]
|
|
if entry is None:
|
|
self._trace.append(f" {current_pc:06x} ILLEGAL")
|
|
self._dump_trace()
|
|
raise ZCpuIllegalInstruction(
|
|
f"illegal opcode class={opcode_class} num={opcode_number}"
|
|
f" at PC={current_pc:#x}"
|
|
)
|
|
implemented, func = entry
|
|
if not implemented:
|
|
return False
|
|
self._trace.append(
|
|
f" {current_pc:06x} {func.__name__}"
|
|
f"({', '.join(str(x) for x in operands)})"
|
|
)
|
|
try:
|
|
func(self, *operands)
|
|
except (ZCpuQuit, ZCpuRestart):
|
|
raise
|
|
except Exception:
|
|
self._dump_trace()
|
|
raise
|
|
return True
|
|
|
|
def step(self):
|
|
"""Execute a single instruction. Returns True if execution should continue."""
|
|
current_pc = self._opdecoder.program_counter
|
|
log(f"Reading next opcode at address {current_pc:x}")
|
|
(opcode_class, opcode_number, operands) = self._opdecoder.get_next_instruction()
|
|
|
|
# Record raw byte at this PC for trace
|
|
cls_str = zopdecoder.OPCODE_STRINGS.get(opcode_class, f"?{opcode_class}")
|
|
trace_entry = f" {current_pc:06x} {cls_str}:{opcode_number:02x}"
|
|
|
|
try:
|
|
implemented, func = self._get_handler(opcode_class, opcode_number)
|
|
except ZCpuIllegalInstruction:
|
|
trace_entry += f" ILLEGAL (raw byte: {self._memory[current_pc]:02x})"
|
|
self._trace.append(trace_entry)
|
|
self._dump_trace()
|
|
raise
|
|
|
|
trace_entry += f" {func.__name__}({', '.join(str(x) for x in operands)})"
|
|
self._trace.append(trace_entry)
|
|
|
|
log_disasm(
|
|
current_pc,
|
|
zopdecoder.OPCODE_STRINGS[opcode_class],
|
|
opcode_number,
|
|
func.__name__,
|
|
", ".join([str(x) for x in operands]),
|
|
)
|
|
if not implemented:
|
|
log(f"Unimplemented opcode {func.__name__}, halting execution")
|
|
return False
|
|
|
|
# The returned function is unbound, so we must pass
|
|
# self to it ourselves.
|
|
try:
|
|
func(self, *operands)
|
|
except (ZCpuQuit, ZCpuRestart):
|
|
# Normal control flow - don't dump trace
|
|
raise
|
|
except Exception:
|
|
self._dump_trace()
|
|
raise
|
|
return True
|
|
|
|
def run(self):
|
|
"""The Magic Function that takes little bits and bytes, twirls
|
|
them around, and brings the magic to your screen!"""
|
|
log("Execution started")
|
|
while self.step_fast():
|
|
pass
|
|
|
|
##
|
|
## Opcode implementation functions start here.
|
|
##
|
|
|
|
## 2OP opcodes (opcodes 1-127 and 192-223)
|
|
def op_je(self, a, *others):
|
|
"""Branch if the first argument is equal to any subsequent
|
|
arguments. Note that the second operand may be absent, in
|
|
which case there is no jump."""
|
|
for b in others:
|
|
if a == b:
|
|
self._branch(True)
|
|
return
|
|
|
|
# Fallthrough: No args were equal to a.
|
|
self._branch(False)
|
|
|
|
def op_jl(self, a, b):
|
|
"""Branch if the first argument is less than the second."""
|
|
a = self._make_signed(a)
|
|
b = self._make_signed(b)
|
|
if a < b:
|
|
self._branch(True)
|
|
else:
|
|
self._branch(False)
|
|
|
|
def op_jg(self, a, b):
|
|
"""Branch if the first argument is greater than the second."""
|
|
a = self._make_signed(a)
|
|
b = self._make_signed(b)
|
|
if a > b:
|
|
self._branch(True)
|
|
else:
|
|
self._branch(False)
|
|
|
|
def op_dec_chk(self, variable, test_value):
|
|
"""Decrement the variable, and branch if the value becomes
|
|
less than test_value."""
|
|
val = self._read_variable(variable)
|
|
val = (val - 1) % 65536
|
|
self._write_result(val, store_addr=variable)
|
|
self._branch(self._make_signed(val) < self._make_signed(test_value))
|
|
|
|
def op_inc_chk(self, variable, test_value):
|
|
"""Increment the variable, and branch if the value becomes
|
|
greater than the test value."""
|
|
val = self._read_variable(variable)
|
|
val = (val + 1) % 65536
|
|
self._write_result(val, store_addr=variable)
|
|
self._branch(self._make_signed(val) > self._make_signed(test_value))
|
|
|
|
def op_jin(self, obj1, obj2):
|
|
"""Branch if obj1's parent equals obj2."""
|
|
parent = self._objects.get_parent(obj1)
|
|
self._branch(parent == obj2)
|
|
|
|
def op_test(self, bitmap, flags):
|
|
"""Test if all bits in flags are set in bitmap, branch if true."""
|
|
self._branch((bitmap & flags) == flags)
|
|
|
|
def op_or(self, a, b):
|
|
"""Bitwise OR between the two arguments."""
|
|
self._write_result(a | b)
|
|
|
|
def op_and(self, a, b):
|
|
"""Bitwise AND between the two arguments."""
|
|
self._write_result(a & b)
|
|
|
|
def op_test_attr(self, obj, attr):
|
|
"""Test if object has attribute, branch if true."""
|
|
has_attr = self._objects.get_attribute(obj, attr)
|
|
self._branch(has_attr != 0)
|
|
|
|
def op_set_attr(self, obj, attr):
|
|
"""Set attribute on object."""
|
|
self._objects.set_attribute(obj, attr)
|
|
|
|
def op_clear_attr(self, obj, attr):
|
|
"""Clear attribute on object."""
|
|
self._objects.clear_attribute(obj, attr)
|
|
|
|
def op_store(self, variable, value):
|
|
"""Store the given value to the given variable."""
|
|
self._write_result(value, store_addr=variable)
|
|
|
|
def op_insert_obj(self, object, dest):
|
|
"""Move object OBJECT to become the first child of object
|
|
DEST. After the move, the prior first child of DEST is now
|
|
the OBJECT's sibling."""
|
|
self._objects.insert_object(dest, object)
|
|
|
|
def op_loadw(self, base, offset):
|
|
"""Store in the given result register the word value at
|
|
(base+2*offset)."""
|
|
val = self._memory.read_word(base + 2 * offset)
|
|
self._write_result(val)
|
|
|
|
def op_loadb(self, base, offset):
|
|
"""Store in the given result register the byte value at
|
|
(base+offset)."""
|
|
val = self._memory[base + offset]
|
|
self._write_result(val)
|
|
|
|
def op_get_prop(self, objectnum, propnum):
|
|
"""Store in the given result an object's property value
|
|
(either a byte or word)."""
|
|
val = self._objects.get_prop(objectnum, propnum)
|
|
self._write_result(val)
|
|
|
|
def op_get_prop_addr(self, obj, prop):
|
|
"""Get property data address, store 0 if not found."""
|
|
addr = self._objects.get_property_data_address(obj, prop)
|
|
self._write_result(addr)
|
|
|
|
def op_get_next_prop(self, obj, prop):
|
|
"""Get next property number, store result."""
|
|
next_prop = self._objects.get_next_property(obj, prop)
|
|
self._write_result(next_prop)
|
|
|
|
def op_add(self, a, b):
|
|
"""Signed 16-bit addition."""
|
|
result = self._unmake_signed(self._make_signed(a) + self._make_signed(b))
|
|
self._write_result(result)
|
|
|
|
def op_sub(self, a, b):
|
|
"""Signed 16-bit subtraction"""
|
|
result = self._unmake_signed(self._make_signed(a) - self._make_signed(b))
|
|
self._write_result(result)
|
|
|
|
def op_mul(self, a, b):
|
|
"""Signed 16-bit multiplication."""
|
|
result = self._unmake_signed(self._make_signed(a) * self._make_signed(b))
|
|
self._write_result(result)
|
|
|
|
def op_div(self, a, b):
|
|
"""Signed 16-bit division."""
|
|
a = self._make_signed(a)
|
|
b = self._make_signed(b)
|
|
if b == 0:
|
|
raise ZCpuDivideByZero
|
|
self._write_result(self._unmake_signed(a // b))
|
|
|
|
def op_mod(self, a, b):
|
|
"""Signed 16-bit modulo (remainder after division)."""
|
|
a = self._make_signed(a)
|
|
b = self._make_signed(b)
|
|
if b == 0:
|
|
raise ZCpuDivideByZero
|
|
# Z-machine uses truncation toward zero, not Python's floor division
|
|
quotient = int(a / b)
|
|
if quotient < 0 and quotient < a / b:
|
|
quotient += 1
|
|
if quotient > 0 and quotient > a / b:
|
|
quotient -= 1
|
|
remainder = a - (quotient * b)
|
|
self._write_result(self._unmake_signed(remainder))
|
|
|
|
def op_call_2s(self, routine_addr, arg1):
|
|
"""Call routine(arg1) and store the result."""
|
|
self._call(routine_addr, [arg1], True)
|
|
|
|
def op_call_2n(self, routine_addr, arg1):
|
|
"""Call routine(arg1) and throw away the result."""
|
|
self._call(routine_addr, [arg1], False)
|
|
|
|
def op_set_colour(self, *args):
|
|
"""Set foreground and background colors (no-op for text MUD)."""
|
|
pass
|
|
|
|
def op_throw(self, *args):
|
|
"""TODO: Write docstring here."""
|
|
raise ZCpuNotImplemented
|
|
|
|
## 1OP opcodes (opcodes 128-175)
|
|
|
|
def op_jz(self, val):
|
|
"""Branch if the val is zero."""
|
|
self._branch(val == 0)
|
|
|
|
def op_get_sibling(self, obj):
|
|
"""Get sibling of object, store it, branch if nonzero."""
|
|
sibling = self._objects.get_sibling(obj)
|
|
self._write_result(sibling)
|
|
self._branch(sibling != 0)
|
|
|
|
def op_get_child(self, object_num):
|
|
"""Get first child of object, store it, branch if nonzero."""
|
|
child = self._objects.get_child(object_num)
|
|
self._write_result(child)
|
|
self._branch(child != 0)
|
|
|
|
def op_get_parent(self, object_num):
|
|
"""Get and store the parent of the given object."""
|
|
self._write_result(self._objects.get_parent(object_num))
|
|
|
|
def op_get_prop_len(self, data_addr):
|
|
"""Get property data length from data address, store result."""
|
|
length = self._objects.get_property_length(data_addr)
|
|
self._write_result(length)
|
|
|
|
def op_inc(self, variable):
|
|
"""Increment the given value."""
|
|
val = self._read_variable(variable)
|
|
val = (val + 1) % 65536
|
|
self._write_result(val, store_addr=variable)
|
|
|
|
def op_dec(self, variable):
|
|
"""Decrement the given variable."""
|
|
val = self._read_variable(variable)
|
|
val = self._make_signed(val)
|
|
val = val - 1
|
|
val = self._unmake_signed(val)
|
|
self._write_result(val, store_addr=variable)
|
|
|
|
def op_print_addr(self, string_byte_address):
|
|
"""Print the z-encoded string at the given byte address."""
|
|
text = self._string.get(string_byte_address)
|
|
self._ui.screen.write(text)
|
|
|
|
def op_call_1s(self, routine_address):
|
|
"""Call the given routine and store the return value."""
|
|
self._call(routine_address, [], True)
|
|
|
|
def op_remove_obj(self, obj):
|
|
"""Remove object from its parent."""
|
|
self._objects.remove_object(obj)
|
|
|
|
def op_print_obj(self, obj):
|
|
"""Print object's short name."""
|
|
shortname = self._objects.get_shortname(obj)
|
|
self._ui.screen.write(shortname)
|
|
|
|
def op_ret(self, value):
|
|
"""Return from the current routine with the given value."""
|
|
pc = self._stackmanager.finish_routine(value)
|
|
self._opdecoder.program_counter = pc
|
|
|
|
def op_jump(self, offset):
|
|
"""Jump unconditionally to the given branch offset. This
|
|
opcode does not follow the usual branch decision algorithm,
|
|
and so we do not call the _branch method to dispatch the call."""
|
|
|
|
old_pc = self._opdecoder.program_counter
|
|
|
|
# The offset to the jump instruction is known to be a 2-byte
|
|
# signed integer. We need to make it signed before applying
|
|
# the offset.
|
|
if offset >= (1 << 15):
|
|
offset = -(1 << 16) + offset
|
|
log(f"Jump unconditionally to relative offset {offset}")
|
|
|
|
# Apparently reading the 2 bytes of operand *isn't* supposed
|
|
# to increment the PC, thus we need to apply this offset to PC
|
|
# that's still pointing at the 'jump' opcode. Hence the -2
|
|
# modifier below.
|
|
new_pc = self._opdecoder.program_counter + offset - 2
|
|
self._opdecoder.program_counter = new_pc
|
|
log(f"PC has changed from from {old_pc:x} to {new_pc:x}")
|
|
|
|
def op_print_paddr(self, string_paddr):
|
|
"""Print the string at the given packed address."""
|
|
zstr_address = self._memory.packed_address(string_paddr)
|
|
text = self._string.get(zstr_address)
|
|
self._ui.screen.write(text)
|
|
|
|
def op_load(self, variable):
|
|
"""Load the value of the given variable and store it."""
|
|
value = self._read_variable(variable)
|
|
self._write_result(value)
|
|
|
|
def op_not(self, value):
|
|
"""Bitwise NOT of the given value."""
|
|
result = ~value & 0xFFFF
|
|
self._write_result(result)
|
|
|
|
def op_call_1n(self, routine_addr):
|
|
"""Call the given routine, and discard the return value."""
|
|
self._call(routine_addr, [], False)
|
|
|
|
## 0OP opcodes (opcodes 176-191)
|
|
|
|
def op_rtrue(self, *args):
|
|
"""Make the current routine return true (1)."""
|
|
pc = self._stackmanager.finish_routine(1)
|
|
self._opdecoder.program_counter = pc
|
|
|
|
def op_rfalse(self, *args):
|
|
"""Make the current routine return false (0)."""
|
|
pc = self._stackmanager.finish_routine(0)
|
|
self._opdecoder.program_counter = pc
|
|
|
|
def op_print(self):
|
|
"""Print the embedded ZString."""
|
|
zstr_address = self._opdecoder.get_zstring()
|
|
text = self._string.get(zstr_address)
|
|
self._ui.screen.write(text)
|
|
|
|
def op_print_ret(self):
|
|
"""TODO: Write docstring here."""
|
|
self.op_print()
|
|
self.op_rtrue()
|
|
|
|
def op_nop(self, *args):
|
|
"""Do nothing."""
|
|
pass
|
|
|
|
def op_save(self, *args):
|
|
"""Save game state to file (V3 - branch on success).
|
|
|
|
Uses QuetzalWriter to generate save data in IFF/FORM/IFZS format,
|
|
then calls the filesystem to write it. Branches true on success,
|
|
false on failure.
|
|
"""
|
|
if self._zmachine is None:
|
|
# Can't save without zmachine reference
|
|
self._branch(False)
|
|
return
|
|
|
|
from .quetzal import QuetzalWriter
|
|
|
|
try:
|
|
writer = QuetzalWriter(self._zmachine)
|
|
save_data = writer.generate_save_data()
|
|
success = self._ui.filesystem.save_game(save_data)
|
|
self._branch(success)
|
|
except Exception as e:
|
|
# Any error during save process = failure
|
|
log(f"Save failed with exception: {e}")
|
|
self._branch(False)
|
|
|
|
def op_save_v4(self, *args):
|
|
"""TODO: Write docstring here."""
|
|
raise ZCpuNotImplemented
|
|
|
|
def op_restore(self, *args):
|
|
"""Restore game state from file (V3 - branch on success).
|
|
|
|
Uses QuetzalParser to load save data from filesystem,
|
|
validates it matches current story, and restores memory/stack/PC.
|
|
Branches true on success, false on failure.
|
|
"""
|
|
if self._zmachine is None:
|
|
# Can't restore without zmachine reference
|
|
self._branch(False)
|
|
return
|
|
|
|
from .quetzal import QuetzalParser
|
|
|
|
try:
|
|
# Get save data from filesystem
|
|
save_data = self._ui.filesystem.restore_game()
|
|
if save_data is None:
|
|
# User cancelled or no save file available
|
|
self._branch(False)
|
|
return
|
|
|
|
# Parse the save data
|
|
parser = QuetzalParser(self._zmachine)
|
|
parser.load_from_bytes(save_data)
|
|
|
|
# QuetzalParser already:
|
|
# - Validated IFhd matches current story (release/serial/checksum)
|
|
# - Replaced dynamic memory via _parse_cmem or _parse_umem
|
|
# - Replaced stack manager via _parse_stks
|
|
# - Set program counter via _parse_ifhd
|
|
|
|
# Success!
|
|
self._branch(True)
|
|
except Exception as e:
|
|
# Any error during restore process = failure
|
|
log(f"Restore failed with exception: {e}")
|
|
self._branch(False)
|
|
|
|
def op_restore_v4(self, *args):
|
|
"""TODO: Write docstring here."""
|
|
raise ZCpuNotImplemented
|
|
|
|
def op_restart(self, *args):
|
|
"""Restart the game from the beginning.
|
|
|
|
Raises ZCpuRestart exception for the run loop to handle.
|
|
The ZMachine should catch this and reset to initial state.
|
|
"""
|
|
raise ZCpuRestart
|
|
|
|
def op_ret_popped(self, *args):
|
|
"""Pop a value from the stack and return it from the current routine."""
|
|
value = self._stackmanager.pop_stack()
|
|
pc = self._stackmanager.finish_routine(value)
|
|
self._opdecoder.program_counter = pc
|
|
|
|
def op_pop(self, *args):
|
|
"""Pop and discard the top value from the stack."""
|
|
self._stackmanager.pop_stack()
|
|
|
|
def op_catch(self, *args):
|
|
"""Store the current stack frame index (for throw opcode)."""
|
|
frame_index = self._stackmanager.get_stack_frame_index()
|
|
self._write_result(frame_index)
|
|
|
|
def op_quit(self, *args):
|
|
"""Quit the game."""
|
|
raise ZCpuQuit
|
|
|
|
def op_new_line(self, *args):
|
|
"""Print a newline."""
|
|
self._ui.screen.write("\n")
|
|
|
|
def op_show_status(self, *args):
|
|
"""Update status line (V3 only). No-op in this implementation."""
|
|
pass
|
|
|
|
def op_verify(self, *args):
|
|
"""Verify story file checksum. Branch if checksum matches."""
|
|
expected_checksum = self._memory.read_word(0x1C)
|
|
actual_checksum = self._memory.generate_checksum()
|
|
self._branch(expected_checksum == actual_checksum)
|
|
|
|
def op_piracy(self, *args):
|
|
"""Anti-piracy check. Always branches true (all interpreters pass this)."""
|
|
self._branch(True)
|
|
|
|
## VAR opcodes (opcodes 224-255)
|
|
|
|
# call in v1-3
|
|
def op_call(self, routine_addr, *args):
|
|
"""Call the routine r1, passing it any of r2, r3, r4 if defined."""
|
|
self._call(routine_addr, args, True)
|
|
|
|
def op_call_vs(self, routine_addr, *args):
|
|
"""See op_call."""
|
|
self.op_call(routine_addr, *args)
|
|
|
|
def op_storew(self, array, offset, value):
|
|
"""Store the given 16-bit value at array+2*byte_index."""
|
|
store_address = array + 2 * offset
|
|
self._memory.write_word(store_address, value)
|
|
|
|
def op_storeb(self, array, byte_index, value):
|
|
"""Store the given byte value at array+byte_index."""
|
|
self._memory[array + byte_index] = value & 0xFF
|
|
|
|
def op_put_prop(self, object_number, property_number, value):
|
|
"""Set an object's property to the given value."""
|
|
self._objects.set_property(object_number, property_number, value)
|
|
|
|
def op_sread(self, *args):
|
|
"""Read text input from keyboard (V3).
|
|
|
|
Args:
|
|
args[0]: text buffer address (byte 0 = max length, text starts at byte 1)
|
|
args[1]: parse buffer address (optional, 0 = no parsing)
|
|
"""
|
|
text_buffer_addr = args[0]
|
|
parse_buffer_addr = args[1] if len(args) > 1 else 0
|
|
|
|
# V3: show status line first
|
|
if self._memory.version <= 3:
|
|
self.op_show_status()
|
|
|
|
# Read input from keyboard
|
|
text = self._ui.keyboard_input.read_line()
|
|
text = text.lower().strip("\n\r")
|
|
|
|
# Store in text buffer
|
|
max_len = self._memory[text_buffer_addr]
|
|
text = text[:max_len]
|
|
for i, ch in enumerate(text):
|
|
self._memory[text_buffer_addr + 1 + i] = ord(ch)
|
|
self._memory[text_buffer_addr + 1 + len(text)] = 0
|
|
|
|
# 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]
|
|
# Find word position in text
|
|
pos = text.find(word_str, offset)
|
|
if pos == -1:
|
|
pos = offset # fallback: best guess position
|
|
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 + 1 # 1-indexed from start of text buffer
|
|
offset = pos + word_len
|
|
|
|
def op_sread_v4(self, *args):
|
|
"""TODO: Write docstring here."""
|
|
raise ZCpuNotImplemented
|
|
|
|
def op_aread(self, *args):
|
|
"""Read text input from keyboard (V5+).
|
|
|
|
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
|
|
|
|
# Consume store byte BEFORE blocking in read_line(). This ensures
|
|
# the PC is past the entire instruction when MUD-level saves capture
|
|
# state during read_line(). Without this, saves point PC at the store
|
|
# byte, which gets misinterpreted as an opcode on restore.
|
|
store_addr = self._opdecoder.get_store_address()
|
|
|
|
# Read input from keyboard (blocks until player types something)
|
|
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, store_addr=store_addr)
|
|
|
|
def op_print_char(self, char):
|
|
"""Output the given ZSCII character."""
|
|
self._ui.screen.write(self._string.zscii.get([char]))
|
|
|
|
def op_print_num(self, value):
|
|
"""Print a signed 16-bit number as text."""
|
|
signed_value = self._make_signed(value)
|
|
self._ui.screen.write(str(signed_value))
|
|
|
|
def op_random(self, n):
|
|
"""Generate a random number, or seed the PRNG.
|
|
|
|
If the input is positive, generate a uniformly random number
|
|
in the range [1:input]. If the input is negative, seed the
|
|
PRNG with that value. If the input is zero, seed the PRNG with
|
|
the current time.
|
|
"""
|
|
result = 0
|
|
if n > 0:
|
|
log(f"Generate random number in [1:{n}]")
|
|
result = random.randint(1, n)
|
|
elif n < 0:
|
|
log(f"Seed PRNG with {n}")
|
|
random.seed(n)
|
|
else:
|
|
log("Seed PRNG with time")
|
|
random.seed(time.time())
|
|
self._write_result(result)
|
|
|
|
def op_push(self, value):
|
|
"""Push a value onto the current routine's game stack."""
|
|
self._stackmanager.push_stack(value)
|
|
|
|
def op_pull(self, variable):
|
|
"""Pop a value from the stack and store it in the given variable."""
|
|
value = self._stackmanager.pop_stack()
|
|
self._write_result(value, store_addr=variable)
|
|
|
|
def op_split_window(self, height):
|
|
"""Split or unsplit the window horizontally."""
|
|
self._ui.screen.split_window(height)
|
|
|
|
def op_set_window(self, window_num):
|
|
"""Set the given window as the active window."""
|
|
self._ui.screen.select_window(window_num)
|
|
|
|
def op_call_vs2(self, routine_addr, *args):
|
|
"""Call routine with up to 7 arguments and store the result."""
|
|
self._call(routine_addr, args, True)
|
|
|
|
def op_erase_window(self, window_number):
|
|
"""Clear the window with the given number. If # is -1, unsplit
|
|
all and clear (full reset). If # is -2, clear all but don't
|
|
unsplit."""
|
|
if window_number == -1:
|
|
self.op_split_window(0)
|
|
self._ui.screen.erase_window(zscreen.WINDOW_LOWER)
|
|
elif window_number == -2:
|
|
self._ui.screen.erase_window(zscreen.WINDOW_LOWER)
|
|
self._ui.screen.erase_window(zscreen.WINDOW_UPPER)
|
|
else:
|
|
self._ui.screen.erase_window(window_number)
|
|
|
|
def op_erase_line(self, *args):
|
|
"""Erase current line on screen (no-op for text MUD)."""
|
|
pass
|
|
|
|
def op_set_cursor(self, x, y):
|
|
"""Set the cursor position within the active window."""
|
|
self._ui.screen.set_cursor_position(x, y)
|
|
|
|
def op_get_cursor(self, table_addr):
|
|
"""Get cursor position into table. For MUD, always write row=1, col=1."""
|
|
self._memory.write_word(table_addr, 1) # row
|
|
self._memory.write_word(table_addr + 2, 1) # col
|
|
|
|
def op_set_text_style(self, text_style):
|
|
"""Set the text style."""
|
|
self._ui.screen.set_text_style(text_style)
|
|
|
|
def op_buffer_mode(self, flag):
|
|
"""If set to 1, text output on the lower window in stream 1 is
|
|
buffered up so that it can be word-wrapped properly. If set to
|
|
0, it isn't."""
|
|
|
|
self._ui.screen.buffer_mode = bool(flag)
|
|
|
|
def op_output_stream(self, stream_num):
|
|
"""Enable or disable the given stream.
|
|
|
|
This is the v3/4 implementation of the opcode, which just
|
|
delegates to the backwards compatible v5 implementation.
|
|
"""
|
|
self.op_output_stream_v5(stream_num)
|
|
|
|
def op_output_stream_v5(self, stream_num, table=None):
|
|
"""Enable or disable the given output stream."""
|
|
stream_num = self._make_signed(stream_num)
|
|
if stream_num < 0:
|
|
self._streammanager.output.unselect(-stream_num)
|
|
else:
|
|
self._streammanager.output.select(stream_num)
|
|
|
|
def op_input_stream(self, stream_num):
|
|
"""Select input stream (stub - not yet implemented for MUD integration)."""
|
|
pass
|
|
|
|
# This one may have been used prematurely in v3 stories. Keep an
|
|
# eye out for it if we ever get bug reports.
|
|
def op_sound_effect(self, *args):
|
|
"""Play sound effect (no-op - sound not supported in text MUD)."""
|
|
pass
|
|
|
|
def op_read_char(self, unused, time=0, input_routine=0):
|
|
"""Read a single character from input stream 0 (keyboard).
|
|
|
|
Optionally, call a routine periodically to decide whether or
|
|
not to interrupt user input.
|
|
"""
|
|
# According to the spec, the first argument is always one, and
|
|
# exists only for Historical Reasons(tm)
|
|
assert unused == 1
|
|
|
|
# TODO: shiny timer stuff not implemented yet.
|
|
if time != 0 or input_routine != 0:
|
|
raise ZCpuNotImplemented
|
|
|
|
# Consume store byte BEFORE blocking in read_char() — same reason
|
|
# as op_aread: PC must be past the full instruction for MUD saves.
|
|
store_addr = self._opdecoder.get_store_address()
|
|
char = self._ui.keyboard_input.read_char()
|
|
self._write_result(char, store_addr=store_addr)
|
|
|
|
def op_scan_table(self, x, table, length, *args):
|
|
"""Search a table for a value, branch if found, store address (V4+).
|
|
|
|
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, value):
|
|
"""Bitwise NOT (VAR form). Same as op_not."""
|
|
result = ~value & 0xFFFF
|
|
self._write_result(result)
|
|
|
|
def op_call_vn(self, routine_addr, *args):
|
|
"""Call routine with up to 3 arguments and discard the result."""
|
|
self._call(routine_addr, args, False)
|
|
|
|
def op_call_vn2(self, routine_addr, *args):
|
|
"""Call routine with up to 7 arguments and discard the result."""
|
|
self._call(routine_addr, args, False)
|
|
|
|
def op_tokenize(self, text_buffer, parse_buffer, *args):
|
|
"""Tokenize text in text_buffer into parse_buffer (V5+).
|
|
|
|
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):
|
|
"""Encode ZSCII text to Z-encoded string (V5+).
|
|
|
|
This opcode converts ZSCII text into Z-machine's packed text format
|
|
(3 characters per 2 bytes). Complex operation, rarely used.
|
|
Not implemented - will raise ZCpuNotImplemented if any game calls it.
|
|
"""
|
|
raise ZCpuNotImplemented
|
|
|
|
def op_copy_table(self, first, second, size):
|
|
"""Copy a block of memory, or zero-fill (V5+).
|
|
|
|
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, zscii_text, width, height=1, skip=0):
|
|
"""Formatted table printing (no-op for text MUD).
|
|
|
|
Spec: print width chars per line for height lines from zscii_text.
|
|
Skip bytes between rows. For now, no-op to avoid crashes.
|
|
"""
|
|
pass
|
|
|
|
def op_check_arg_count(self, arg_number):
|
|
"""Branch if the Nth argument was passed to the current routine."""
|
|
current_frame = self._stackmanager._call_stack[-1]
|
|
self._branch(arg_number <= current_frame.arg_count)
|
|
|
|
## EXT opcodes (opcodes 256-284)
|
|
|
|
def op_save_v5(self, *args):
|
|
"""Save game state to file (V5+ - stores result).
|
|
|
|
Generates Quetzal save data and writes via filesystem.
|
|
Stores 1 on success, 0 on failure. On restore, the game
|
|
will see 2 stored in the same variable.
|
|
"""
|
|
if self._zmachine is None:
|
|
self._write_result(0)
|
|
return
|
|
|
|
from .quetzal import QuetzalWriter
|
|
|
|
try:
|
|
writer = QuetzalWriter(self._zmachine)
|
|
save_data = writer.generate_save_data()
|
|
success = self._ui.filesystem.save_game(save_data)
|
|
self._write_result(1 if success else 0)
|
|
except Exception as e:
|
|
log(f"Save failed with exception: {e}")
|
|
self._write_result(0)
|
|
|
|
def op_restore_v5(self, *args):
|
|
"""Restore game state from file (V5+ - stores result).
|
|
|
|
Loads Quetzal save data and restores memory/stack/PC.
|
|
The restored PC points at the store byte of the original save
|
|
instruction. We read it and write 2 (meaning "restored") to
|
|
the indicated variable.
|
|
Stores 0 on failure (in the current, un-restored state).
|
|
"""
|
|
if self._zmachine is None:
|
|
self._write_result(0)
|
|
return
|
|
|
|
from .quetzal import QuetzalParser
|
|
|
|
try:
|
|
save_data = self._ui.filesystem.restore_game()
|
|
if save_data is None:
|
|
self._write_result(0)
|
|
return
|
|
|
|
parser = QuetzalParser(self._zmachine)
|
|
parser.load_from_bytes(save_data)
|
|
|
|
# Restored PC points at the store byte of the save instruction.
|
|
# Read it and write 2 ("restored") to that variable.
|
|
self._write_result(2)
|
|
except Exception as e:
|
|
log(f"Restore failed with exception: {e}")
|
|
self._write_result(0)
|
|
|
|
def op_log_shift(self, number, places):
|
|
"""Logical shift: positive places = left, negative = right (V5+).
|
|
|
|
Right shift fills with zeros (unsigned/logical shift).
|
|
"""
|
|
places = self._make_signed(places)
|
|
result = (
|
|
(number << places) & 0xFFFF
|
|
if places >= 0
|
|
else (number >> (-places)) & 0xFFFF
|
|
)
|
|
self._write_result(result)
|
|
|
|
def op_art_shift(self, number, places):
|
|
"""Arithmetic shift: positive places = left, negative = right (V5+).
|
|
|
|
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):
|
|
"""Save undo state (V5+, EXT:9).
|
|
|
|
Captures a snapshot of dynamic memory, call stack, and PC.
|
|
Stores 1 on success. After restore_undo, execution resumes
|
|
here with result 2 (like fork() returning different values).
|
|
"""
|
|
from .zstackmanager import ZRoutine, ZStackBottom
|
|
|
|
# Read store address first — advances PC past the store byte
|
|
store_addr = self._opdecoder.get_store_address()
|
|
|
|
# Capture dynamic memory
|
|
mem = self._memory
|
|
dynamic_copy = bytearray(mem._memory[mem._dynamic_start : mem._dynamic_end + 1])
|
|
|
|
# Deep copy call stack
|
|
stack_copy = []
|
|
for frame in self._stackmanager._call_stack:
|
|
if isinstance(frame, ZStackBottom):
|
|
bottom = ZStackBottom()
|
|
bottom.program_counter = frame.program_counter
|
|
bottom.stack = frame.stack[:]
|
|
bottom.local_vars = frame.local_vars[:]
|
|
stack_copy.append(bottom)
|
|
else:
|
|
new_frame = ZRoutine(
|
|
frame.start_addr,
|
|
frame.return_addr,
|
|
self._memory,
|
|
[],
|
|
local_vars=frame.local_vars[:],
|
|
stack=frame.stack[:],
|
|
)
|
|
new_frame.program_counter = frame.program_counter
|
|
new_frame.arg_count = frame.arg_count
|
|
stack_copy.append(new_frame)
|
|
|
|
self._undo_snapshot = (
|
|
self._opdecoder.program_counter,
|
|
store_addr,
|
|
dynamic_copy,
|
|
stack_copy,
|
|
)
|
|
|
|
# Store 1 = success
|
|
self._write_result(1, store_addr=store_addr)
|
|
|
|
def op_restore_undo(self, *args):
|
|
"""Restore undo state (V5+, EXT:10).
|
|
|
|
Restores dynamic memory, call stack, and PC from snapshot.
|
|
Stores 0 on failure. On success, execution resumes at the
|
|
save_undo call site with result 2.
|
|
"""
|
|
if self._undo_snapshot is None:
|
|
self._write_result(0)
|
|
return
|
|
|
|
pc, store_addr, dynamic_copy, stack_copy = self._undo_snapshot
|
|
self._undo_snapshot = None
|
|
|
|
# Restore dynamic memory
|
|
mem = self._memory
|
|
mem._memory[mem._dynamic_start : mem._dynamic_end + 1] = dynamic_copy
|
|
|
|
# Restore call stack: keep the live ZStackBottom identity,
|
|
# but restore its state from the snapshot
|
|
live_bottom = self._stackmanager._stackbottom
|
|
saved_bottom = stack_copy[0]
|
|
live_bottom.program_counter = saved_bottom.program_counter
|
|
live_bottom.stack = saved_bottom.stack[:]
|
|
live_bottom.local_vars = saved_bottom.local_vars[:]
|
|
self._stackmanager._call_stack[:] = [live_bottom] + stack_copy[1:]
|
|
|
|
# Restore PC
|
|
self._opdecoder.program_counter = pc
|
|
|
|
# Store 2 at save_undo's store location (not restore_undo's)
|
|
self._write_result(2, store_addr=store_addr)
|
|
|
|
def op_print_unicode(self, char_code):
|
|
"""Print a Unicode character (V5+, EXT:11)."""
|
|
self._ui.screen.write(chr(char_code))
|
|
|
|
def op_check_unicode(self, char_code):
|
|
"""Check if Unicode character can be printed/read (V5+, EXT:12).
|
|
|
|
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
|
|
# divided into tables based on the operand type. Within each
|
|
# table, the operand is then indexed by its number. We preserve
|
|
# that organization in this opcode table.
|
|
#
|
|
# The opcode table is a dictionary mapping an operand type to a
|
|
# list of opcodes definitions. Each opcode definition's index in
|
|
# the table is the opcode number within that opcode table.
|
|
#
|
|
# The opcodes are in one of three forms:
|
|
#
|
|
# - If the opcode is available and unchanging in all versions,
|
|
# then the definition is simply the function implementing the
|
|
# opcode.
|
|
#
|
|
# - If the opcode is only available as of a certain version
|
|
# upwards, then the definition is the tuple (opcode_func,
|
|
# first_version), where first_version is the version of the
|
|
# Z-machine where the opcode appeared.
|
|
#
|
|
# - If the opcode changes meaning with successive revisions of the
|
|
# Z-machine, then the definition is a list of the above tuples,
|
|
# sorted in descending order (tuple with the highest
|
|
# first_version comes first). If an instruction became illegal
|
|
# after a given version, it should have a tuple with the opcode
|
|
# function set to None.
|
|
|
|
opcodes = {
|
|
# 2OP opcodes
|
|
zopdecoder.OPCODE_2OP: [
|
|
None,
|
|
op_je,
|
|
op_jl,
|
|
op_jg,
|
|
op_dec_chk,
|
|
op_inc_chk,
|
|
op_jin,
|
|
op_test,
|
|
op_or,
|
|
op_and,
|
|
op_test_attr,
|
|
op_set_attr,
|
|
op_clear_attr,
|
|
op_store,
|
|
op_insert_obj,
|
|
op_loadw,
|
|
op_loadb,
|
|
op_get_prop,
|
|
op_get_prop_addr,
|
|
op_get_next_prop,
|
|
op_add,
|
|
op_sub,
|
|
op_mul,
|
|
op_div,
|
|
op_mod,
|
|
(op_call_2s, 4),
|
|
(op_call_2n, 5),
|
|
(op_set_colour, 5),
|
|
(op_throw, 5),
|
|
],
|
|
# 1OP opcodes
|
|
zopdecoder.OPCODE_1OP: [
|
|
op_jz,
|
|
op_get_sibling,
|
|
op_get_child,
|
|
op_get_parent,
|
|
op_get_prop_len,
|
|
op_inc,
|
|
op_dec,
|
|
op_print_addr,
|
|
(op_call_1s, 4),
|
|
op_remove_obj,
|
|
op_print_obj,
|
|
op_ret,
|
|
op_jump,
|
|
op_print_paddr,
|
|
op_load,
|
|
[(op_call_1n, 5), (op_not, 1)],
|
|
],
|
|
# 0OP opcodes
|
|
zopdecoder.OPCODE_0OP: [
|
|
op_rtrue,
|
|
op_rfalse,
|
|
op_print,
|
|
op_print_ret,
|
|
op_nop,
|
|
[(None, 5), (op_save_v4, 4), (op_save, 1)],
|
|
[(None, 5), (op_restore_v4, 4), (op_restore, 1)],
|
|
op_restart,
|
|
op_ret_popped,
|
|
[(op_catch, 5), (op_pop, 1)],
|
|
op_quit,
|
|
op_new_line,
|
|
[(None, 4), (op_show_status, 3)],
|
|
(op_verify, 3),
|
|
None, # Padding. Opcode 0OP:E is the extended opcode marker.
|
|
(op_piracy, 5),
|
|
],
|
|
# VAR opcodes
|
|
zopdecoder.OPCODE_VAR: [
|
|
[(op_call_vs, 4), (op_call, 1)],
|
|
op_storew,
|
|
op_storeb,
|
|
op_put_prop,
|
|
[(op_aread, 5), (op_sread_v4, 4), (op_sread, 1)],
|
|
op_print_char,
|
|
op_print_num,
|
|
op_random,
|
|
op_push,
|
|
op_pull,
|
|
(op_split_window, 3),
|
|
(op_set_window, 3),
|
|
(op_call_vs2, 4),
|
|
(op_erase_window, 4),
|
|
(op_erase_line, 4),
|
|
(op_set_cursor, 4),
|
|
(op_get_cursor, 4),
|
|
(op_set_text_style, 4),
|
|
(op_buffer_mode, 4),
|
|
[(op_output_stream_v5, 5), (op_output_stream, 3)],
|
|
(op_input_stream, 3),
|
|
(op_sound_effect, 5),
|
|
(op_read_char, 4),
|
|
(op_scan_table, 4),
|
|
(op_not, 5),
|
|
(op_call_vn, 5),
|
|
(op_call_vn2, 5),
|
|
(op_tokenize, 5),
|
|
(op_encode_text, 5),
|
|
(op_copy_table, 5),
|
|
(op_print_table, 5),
|
|
(op_check_arg_count, 5),
|
|
],
|
|
# EXT opcodes
|
|
zopdecoder.OPCODE_EXT: [
|
|
(op_save_v5, 5),
|
|
(op_restore_v5, 5),
|
|
(op_log_shift, 5),
|
|
(op_art_shift, 5),
|
|
(op_set_font, 5),
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
(op_save_undo, 5),
|
|
(op_restore_undo, 5),
|
|
(op_print_unicode, 5),
|
|
(op_check_unicode, 5),
|
|
],
|
|
}
|