diff --git a/src/mudlib/zmachine/trivialzui.py b/src/mudlib/zmachine/trivialzui.py index 43ed118..4f3934c 100644 --- a/src/mudlib/zmachine/trivialzui.py +++ b/src/mudlib/zmachine/trivialzui.py @@ -115,6 +115,7 @@ class TrivialScreen(zscreen.ZScreen): for char in string: newline_printed = False sys.stdout.write(char) + sys.stdout.flush() if char == "\n": newline_printed = True @@ -292,6 +293,11 @@ def _unix_read_char(): import tty fd = sys.stdin.fileno() + + # Check if stdin is a TTY - if not, use simple read + if not sys.stdin.isatty(): + return sys.stdin.read(1) + old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) @@ -333,6 +339,14 @@ def _read_line(original_text=None, terminating_characters=None): assert isinstance(original_text, str) assert isinstance(terminating_characters, str) + # If stdin is not a TTY, use simple line reading + if not sys.stdin.isatty(): + line = sys.stdin.readline() + if not line: # EOF + raise EOFError("End of input") + # Strip newline but keep the content + return line.rstrip("\n\r") + chars_entered = len(original_text) sys.stdout.write(original_text) string = original_text diff --git a/src/mudlib/zmachine/zcpu.py b/src/mudlib/zmachine/zcpu.py index 2be4445..e96febf 100644 --- a/src/mudlib/zmachine/zcpu.py +++ b/src/mudlib/zmachine/zcpu.py @@ -8,6 +8,7 @@ import random import time +from collections import deque from . import bitfield, zopdecoder, zscreen from .zlogging import log, log_disasm @@ -53,6 +54,7 @@ class ZCpu: self._streammanager = zstreammanager self._ui = zui self._lexer = zlexer + self._trace = deque(maxlen=20) def _get_handler(self, opcode_class, opcode_number): try: @@ -60,6 +62,8 @@ class ZCpu: except IndexError: opcode_decl = None if not opcode_decl: + cls, num = opcode_class, opcode_number + print(f"DEBUG: Illegal opcode class={cls} num={num}") raise ZCpuIllegalInstruction # If the opcode declaration is a sequence, we have extra @@ -80,6 +84,8 @@ class ZCpu: elif opcode_decl[1] <= self._memory.version: opcode_func = opcode_decl[0] else: + cls, num = opcode_class, opcode_number + print(f"DEBUG: Illegal opcode class={cls} num={num}") raise ZCpuIllegalInstruction # The following is a hack, based on our policy of only @@ -143,6 +149,11 @@ class ZCpu: """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() @@ -168,12 +179,34 @@ class ZCpu: 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 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() - implemented, func = self._get_handler(opcode_class, opcode_number) + + # 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], @@ -256,9 +289,9 @@ class ZCpu: parent = self._objects.get_parent(obj1) self._branch(parent == obj2) - def op_test(self, *args): - """TODO: Write docstring here.""" - raise ZCpuNotImplemented + 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.""" @@ -386,8 +419,10 @@ class ZCpu: self._branch(sibling != 0) def op_get_child(self, object_num): - """Get and store the first child of the given object.""" - self._write_result(self._objects.get_child(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.""" @@ -574,6 +609,10 @@ class ZCpu: # call in v1-3 def op_call(self, routine_addr, *args): """Call the routine r1, passing it any of r2, r3, r4 if defined.""" + # Calling address 0 is a no-op that returns false (0). + if routine_addr == 0: + self._write_result(0) + return addr = self._memory.packed_address(routine_addr) return_addr = self._opdecoder.get_store_address() current_addr = self._opdecoder.program_counter @@ -614,7 +653,7 @@ class ZCpu: self.op_show_status() # Read input from keyboard - text = self._ui.keyboard.readline() + text = self._ui.keyboard_input.read_line() text = text.lower().strip("\n\r") # Store in text buffer diff --git a/src/mudlib/zmachine/zstring.py b/src/mudlib/zmachine/zstring.py index a0e1217..5291a9c 100644 --- a/src/mudlib/zmachine/zstring.py +++ b/src/mudlib/zmachine/zstring.py @@ -442,7 +442,14 @@ class ZsciiTranslator: try: return self._output_table[index] except KeyError: - raise IndexError("No such ZSCII character") from None + # Handle undefined ZSCII characters + # 0-31 (except 0, 10): control characters, return empty string + # 128-154, 252-254: undefined, return placeholder + # 155-251: extended characters, should have Unicode table but don't + if index < 32: + return "" + # For undefined or unmapped characters, return a placeholder + return "?" def utoz(self, char): """Translate the given Unicode code into the corresponding diff --git a/tests/test_zmachine_opcodes.py b/tests/test_zmachine_opcodes.py index 015e84a..524c669 100644 --- a/tests/test_zmachine_opcodes.py +++ b/tests/test_zmachine_opcodes.py @@ -85,7 +85,8 @@ class MockUI: def __init__(self): self.screen = Mock() self.screen.write = Mock() - self.keyboard = Mock() + self.keyboard_input = Mock() + self.keyboard_input.read_line = Mock() class ZMachineOpcodeTests(TestCase): @@ -548,7 +549,7 @@ class ZMachineComplexOpcodeTests(TestCase): self.memory[text_buffer_addr] = 20 # max length # Mock keyboard input - self.ui.keyboard.readline = Mock(return_value="hello world\n") + self.ui.keyboard_input.read_line = Mock(return_value="hello world\n") # Call sread with text buffer only (no parse buffer) self.cpu.op_sread(text_buffer_addr, 0) @@ -566,7 +567,7 @@ class ZMachineComplexOpcodeTests(TestCase): text_buffer_addr = 0x1000 self.memory[text_buffer_addr] = 5 # max length of 5 - self.ui.keyboard.readline = Mock(return_value="hello world\n") + self.ui.keyboard_input.read_line = Mock(return_value="hello world\n") self.cpu.op_sread(text_buffer_addr, 0) @@ -581,7 +582,7 @@ class ZMachineComplexOpcodeTests(TestCase): text_buffer_addr = 0x1000 self.memory[text_buffer_addr] = 20 - self.ui.keyboard.readline = Mock(return_value="HELLO World\n") + self.ui.keyboard_input.read_line = Mock(return_value="HELLO World\n") self.cpu.op_sread(text_buffer_addr, 0) @@ -594,7 +595,7 @@ class ZMachineComplexOpcodeTests(TestCase): text_buffer_addr = 0x1000 self.memory[text_buffer_addr] = 20 - self.ui.keyboard.readline = Mock(return_value="test\r\n") + self.ui.keyboard_input.read_line = Mock(return_value="test\r\n") self.cpu.op_sread(text_buffer_addr, 0) @@ -607,7 +608,7 @@ class ZMachineComplexOpcodeTests(TestCase): """Test sread calls op_show_status for V3.""" text_buffer_addr = 0x1000 self.memory[text_buffer_addr] = 20 - self.ui.keyboard.readline = Mock(return_value="test\n") + self.ui.keyboard_input.read_line = Mock(return_value="test\n") # Track if show_status was called call_count = [0]