From d71f2212771b4d6921a6fa84128fefd77cfa61ae Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Tue, 10 Feb 2026 13:48:08 -0500 Subject: [PATCH] Implement V5+ call variants and fix double-byte operand decoding New opcodes: op_call_vn, op_call_vn2, op_call_vs2, op_catch, op_check_arg_count. All call variants delegate to existing _call(). ZRoutine now tracks arg_count for check_arg_count. Fixed zopdecoder double-byte operand parsing for call_vs2/call_vn2: the old code called _parse_operands_byte() twice, but this method reads both type byte AND operands together. The second call would read operand data as a type byte. Refactored into _read_type_byte() + _parse_operand_list() so both type bytes are read before any operand data. Also fixed the double-byte detection: was checking opcode[0:7] (7 bits = 0x7A for call_vn2) instead of opcode_num (5 bits = 0x1A). The check never matched, so double-byte opcodes were always mis-parsed. --- src/mudlib/zmachine/zcpu.py | 30 ++++++++++++----------- src/mudlib/zmachine/zopdecoder.py | 36 +++++++++++++++++----------- src/mudlib/zmachine/zstackmanager.py | 1 + 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/mudlib/zmachine/zcpu.py b/src/mudlib/zmachine/zcpu.py index 21f5da7..0c568e1 100644 --- a/src/mudlib/zmachine/zcpu.py +++ b/src/mudlib/zmachine/zcpu.py @@ -644,8 +644,9 @@ class ZCpu: self._stackmanager.pop_stack() def op_catch(self, *args): - """TODO: Write docstring here.""" - raise ZCpuNotImplemented + """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.""" @@ -793,9 +794,9 @@ class ZCpu: """Set the given window as the active window.""" self._ui.screen.select_window(window_num) - def op_call_vs2(self, *args): - """TODO: Write docstring here.""" - raise ZCpuNotImplemented + 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 @@ -884,13 +885,13 @@ class ZCpu: """TODO: Write docstring here.""" raise ZCpuNotImplemented - def op_call_vn(self, *args): - """TODO: Write docstring here.""" - raise ZCpuNotImplemented + 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, *args): - """TODO: Write docstring here.""" - raise ZCpuNotImplemented + 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, *args): """TODO: Write docstring here.""" @@ -908,9 +909,10 @@ class ZCpu: """TODO: Write docstring here.""" raise ZCpuNotImplemented - def op_check_arg_count(self, *args): - """TODO: Write docstring here.""" - raise ZCpuNotImplemented + 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) diff --git a/src/mudlib/zmachine/zopdecoder.py b/src/mudlib/zmachine/zopdecoder.py index 60613f9..213b153 100644 --- a/src/mudlib/zmachine/zopdecoder.py +++ b/src/mudlib/zmachine/zopdecoder.py @@ -122,14 +122,16 @@ class ZOpDecoder: opcode_num = opcode[0:5] - # Parse the types byte to retrieve the operands. - operands = self._parse_operands_byte() - - # Special case: opcodes 12 and 26 have a second operands byte. - if opcode[0:7] == 0xC or opcode[0:7] == 0x1A: + # Read all type bytes FIRST, before parsing any operands. + # call_vs2 (VAR:12) and call_vn2 (VAR:26) have two type bytes; + # all others have one. Both type bytes must be read before + # operand data starts in the stream. + operand_types = self._read_type_byte() + if opcode_type == OPCODE_VAR and opcode_num in (0xC, 0x1A): 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) def _parse_opcode_extended(self): @@ -179,25 +181,31 @@ class ZOpDecoder: return operand - def _parse_operands_byte(self): - """Parse operands given by the operand byte and return a list of - values. - """ + def _read_type_byte(self): + """Read one operand type byte and return a list of type codes.""" operand_byte = BitField(self._get_pc()) - operands = [] - for operand_type in [ + return [ operand_byte[6:8], operand_byte[4:6], operand_byte[2:4], 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) if operand is None: break operands.append(operand) - 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 # opcode being executed: diff --git a/src/mudlib/zmachine/zstackmanager.py b/src/mudlib/zmachine/zstackmanager.py index 277ca71..18ea0fb 100644 --- a/src/mudlib/zmachine/zstackmanager.py +++ b/src/mudlib/zmachine/zstackmanager.py @@ -54,6 +54,7 @@ class ZRoutine: self.start_addr = start_addr self.return_addr = return_addr self.program_counter = 0 # used when execution interrupted + self.arg_count = len(args) # track number of args passed if stack is None: self.stack = []