Implement V5+ save/restore opcodes and handle in-game saves on restore

- op_save_v5: generates Quetzal save, stores 1 on success / 0 on failure
- op_restore_v5: loads Quetzal save, stores 2 ("restored") via store byte
- _try_restore: detect V5+ in-game saves (0xBE 0x00 before PC) and process
  the store byte with result 2, matching the V3 branch-on-restore pattern
This commit is contained in:
Jared Miller 2026-02-10 14:18:42 -05:00
parent 602da45ac2
commit e55294af78
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 73 additions and 13 deletions

View file

@ -46,6 +46,14 @@ class EmbeddedIFSession:
Must be called before the interpreter thread is launched. Must be called before the interpreter thread is launched.
Returns True if state was restored successfully. Returns True if state was restored successfully.
Handles two save origins:
- In-game save (V3: opcode 0xB5, V5+: EXT 0xBE/0x00): PC points at
branch data (V3) or store byte (V5+). Process them so execution
resumes at the next instruction.
- MUD-level _do_save during sread/aread: PC points past the read
instruction. No post-processing needed (phantom output suppressed
in start()).
""" """
if not self.save_path.exists(): if not self.save_path.exists():
return False return False
@ -53,17 +61,24 @@ class EmbeddedIFSession:
save_data = self.save_path.read_bytes() save_data = self.save_path.read_bytes()
parser = QuetzalParser(self._zmachine) parser = QuetzalParser(self._zmachine)
parser.load_from_bytes(save_data) 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 pc = self._zmachine._opdecoder.program_counter
if ( mem = self._zmachine._mem
self._zmachine._mem.version <= 3
and pc > 0 if mem.version <= 3 and pc > 0 and mem[pc - 1] == 0xB5:
and self._zmachine._mem[pc - 1] == 0xB5 # V3 in-game save: PC at branch data after save opcode (0xB5).
): # Process the branch as "save succeeded".
self._zmachine._cpu._branch(True) self._zmachine._cpu._branch(True)
elif (
mem.version >= 5
and pc >= 3
and mem[pc - 3] == 0xBE
and mem[pc - 2] == 0x00
):
# V5+ in-game save: PC at store byte after EXT save opcode.
# Read store byte and write 2 ("restored") to that variable.
self._zmachine._cpu._write_result(2)
return True return True
except Exception as e: except Exception as e:
logger.debug(f"Restore failed: {e}") logger.debug(f"Restore failed: {e}")

View file

@ -1034,12 +1034,57 @@ class ZCpu:
## EXT opcodes (opcodes 256-284) ## EXT opcodes (opcodes 256-284)
def op_save_v5(self, *args): def op_save_v5(self, *args):
"""TODO: Write docstring here.""" """Save game state to file (V5+ - stores result).
raise ZCpuNotImplemented
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): def op_restore_v5(self, *args):
"""TODO: Write docstring here.""" """Restore game state from file (V5+ - stores result).
raise ZCpuNotImplemented
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): def op_log_shift(self, number, places):
"""Logical shift: positive places = left, negative = right (V5+). """Logical shift: positive places = left, negative = right (V5+).