From c52e59c5d4d2587d5fdbd92dbfd63a29e5a2dcca Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Tue, 10 Feb 2026 12:56:39 -0500 Subject: [PATCH] Process V3 save branch on restore to advance PC past branch data The Quetzal spec stores the PC pointing at the save instruction's branch data. On restore, this branch must be processed as "save succeeded" to advance the PC to the actual next instruction. Without this, the branch bytes were decoded as an opcode, corrupting execution. Detect the save opcode (0xB5) immediately before the restored PC to distinguish in-game saves from out-of-band saves (which don't need branch processing). Also improve error diagnostics: pop_stack now raises ZStackPopError with frame context, and the instruction trace dumps on all exceptions. --- src/mudlib/embedded_if_session.py | 11 +++++++++++ src/mudlib/zmachine/zcpu.py | 3 +-- src/mudlib/zmachine/zstackmanager.py | 5 +++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/mudlib/embedded_if_session.py b/src/mudlib/embedded_if_session.py index 79aa40e..763de68 100644 --- a/src/mudlib/embedded_if_session.py +++ b/src/mudlib/embedded_if_session.py @@ -53,6 +53,17 @@ class EmbeddedIFSession: save_data = self.save_path.read_bytes() parser = QuetzalParser(self._zmachine) parser.load_from_bytes(save_data) + # In V1-3, the saved PC points to branch data after the save + # instruction. Process the branch as "save succeeded" so the + # PC advances past it. Detect by checking for save opcode (0xB5) + # immediately before the restored PC. + pc = self._zmachine._opdecoder.program_counter + if ( + self._zmachine._mem.version <= 3 + and pc > 0 + and self._zmachine._mem[pc - 1] == 0xB5 + ): + self._zmachine._cpu._branch(True) return True except Exception as e: logger.debug(f"Restore failed: {e}") diff --git a/src/mudlib/zmachine/zcpu.py b/src/mudlib/zmachine/zcpu.py index c6bbb09..4241189 100644 --- a/src/mudlib/zmachine/zcpu.py +++ b/src/mudlib/zmachine/zcpu.py @@ -236,8 +236,7 @@ class ZCpu: except (ZCpuQuit, ZCpuRestart): # Normal control flow - don't dump trace raise - except ZCpuError: - # All other ZCpu errors - dump trace for debugging + except Exception: self._dump_trace() raise return True diff --git a/src/mudlib/zmachine/zstackmanager.py b/src/mudlib/zmachine/zstackmanager.py index e874484..7960d77 100644 --- a/src/mudlib/zmachine/zstackmanager.py +++ b/src/mudlib/zmachine/zstackmanager.py @@ -153,6 +153,11 @@ class ZStackManager: "Remove and return value from the top of the data stack." current_routine = self._call_stack[-1] + if not current_routine.stack: + frame_idx = len(self._call_stack) - 1 + ra = getattr(current_routine, "return_addr", "N/A") + pc = getattr(current_routine, "program_counter", 0) + raise ZStackPopError(f"frame {frame_idx}, return_addr={ra}, pc=0x{pc:06x}") return current_routine.stack.pop() def get_stack_frame_index(self):