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.
This commit is contained in:
Jared Miller 2026-02-10 12:56:39 -05:00
parent 8526e48247
commit c52e59c5d4
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 17 additions and 2 deletions

View file

@ -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}")

View file

@ -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

View file

@ -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):