Implement QuetzalWriter._generate_ifhd_chunk()

The IFhd chunk contains 13 bytes of metadata identifying the story
and current execution state:
- Release number (2 bytes) from header
- Serial number (6 bytes) from header
- Checksum (2 bytes) from header
- Program counter (3 bytes) from CPU state

This allows save files to be validated against the story file.
This commit is contained in:
Jared Miller 2026-02-10 09:47:24 -05:00
parent 8097bbcf55
commit 0c6eadb0da
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 83 additions and 7 deletions

View file

@ -377,15 +377,40 @@ class QuetzalWriter:
"""Return a chunk of type IFhd, containing metadata about the
zmachine and story being played."""
### TODO: write this. payload must be *exactly* 13 bytes, even if
### it means padding the program counter.
mem = self._zmachine._mem
### Some old infocom games don't have checksums stored in header.
### If not, generate it from the *original* story file memory
### image and put it into this chunk. See ZMemory.generate_checksum().
pass
# Release number (2 bytes, big-endian) from header bytes 2-3
release = mem.read_word(2)
return "0"
# Serial number (6 bytes) from header bytes 0x12-0x17
serial = bytes(mem[0x12:0x18])
# Checksum (2 bytes, big-endian) from header bytes 0x1C-0x1D
checksum = mem.read_word(0x1C)
# Program counter (3 bytes, big-endian) - current PC
pc = self._zmachine._cpu._program_counter
# Build the 13-byte chunk
chunk_data = bytearray(13)
# Bytes 0-1: Release number
chunk_data[0] = (release >> 8) & 0xFF
chunk_data[1] = release & 0xFF
# Bytes 2-7: Serial number
chunk_data[2:8] = serial
# Bytes 8-9: Checksum
chunk_data[8] = (checksum >> 8) & 0xFF
chunk_data[9] = checksum & 0xFF
# Bytes 10-12: Program counter (24-bit)
chunk_data[10] = (pc >> 16) & 0xFF
chunk_data[11] = (pc >> 8) & 0xFF
chunk_data[12] = pc & 0xFF
return bytes(chunk_data)
def _generate_cmem_chunk(self):
"""Return a compressed chunk of data representing the compressed

View file

@ -784,6 +784,57 @@ class ZMachineComplexOpcodeTests(TestCase):
self.cpu.op_restart()
class QuetzalWriterTests(TestCase):
"""Test suite for QuetzalWriter save functionality."""
def test_generate_ifhd_chunk(self):
"""Test _generate_ifhd_chunk() produces correct 13-byte IFhd chunk."""
from mudlib.zmachine.quetzal import QuetzalWriter
# Create a mock zmachine with known header values
mock_zmachine = Mock()
mock_zmachine._mem = MockMemory()
# Set header values in memory:
# Bytes 2-3: Release number (0x1234)
mock_zmachine._mem.write_word(0x02, 0x1234)
# Bytes 0x12-0x17: Serial number (6 bytes: "860509")
serial = b"860509"
for i, byte in enumerate(serial):
mock_zmachine._mem[0x12 + i] = byte
# Bytes 0x1C-0x1D: Checksum (0xABCD)
mock_zmachine._mem.write_word(0x1C, 0xABCD)
# Set program counter
mock_cpu = Mock()
mock_cpu._program_counter = 0x123456 # 24-bit PC
mock_zmachine._cpu = mock_cpu
# Create writer and generate chunk
writer = QuetzalWriter(mock_zmachine)
chunk_data = writer._generate_ifhd_chunk()
# Verify chunk is exactly 13 bytes
self.assertEqual(len(chunk_data), 13)
# Verify release number (bytes 0-1)
self.assertEqual(chunk_data[0], 0x12)
self.assertEqual(chunk_data[1], 0x34)
# Verify serial number (bytes 2-7)
for i, expected in enumerate(serial):
self.assertEqual(chunk_data[2 + i], expected)
# Verify checksum (bytes 8-9)
self.assertEqual(chunk_data[8], 0xAB)
self.assertEqual(chunk_data[9], 0xCD)
# Verify program counter (bytes 10-12)
self.assertEqual(chunk_data[10], 0x12)
self.assertEqual(chunk_data[11], 0x34)
self.assertEqual(chunk_data[12], 0x56)
# Note: ZObjectParser methods are tested through integration tests
# with real story files, not unit tests with mock memory, as the
# interaction with ZStringFactory makes mocking complex.