From e55294af78c5c8e45f90d04ac99baba66efb1fc0 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Tue, 10 Feb 2026 14:18:42 -0500 Subject: [PATCH] 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 --- src/mudlib/embedded_if_session.py | 33 +++++++++++++------ src/mudlib/zmachine/zcpu.py | 53 ++++++++++++++++++++++++++++--- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/mudlib/embedded_if_session.py b/src/mudlib/embedded_if_session.py index 8a30d90..130c462 100644 --- a/src/mudlib/embedded_if_session.py +++ b/src/mudlib/embedded_if_session.py @@ -46,6 +46,14 @@ class EmbeddedIFSession: Must be called before the interpreter thread is launched. 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(): return False @@ -53,17 +61,24 @@ 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 - ): + mem = self._zmachine._mem + + if mem.version <= 3 and pc > 0 and 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) + 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 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 81e8a71..bff0b83 100644 --- a/src/mudlib/zmachine/zcpu.py +++ b/src/mudlib/zmachine/zcpu.py @@ -1034,12 +1034,57 @@ class ZCpu: ## EXT opcodes (opcodes 256-284) def op_save_v5(self, *args): - """TODO: Write docstring here.""" - raise ZCpuNotImplemented + """Save game state to file (V5+ - stores result). + + 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): - """TODO: Write docstring here.""" - raise ZCpuNotImplemented + """Restore game state from file (V5+ - stores result). + + 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): """Logical shift: positive places = left, negative = right (V5+).