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:
parent
602da45ac2
commit
e55294af78
2 changed files with 73 additions and 13 deletions
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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+).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue