diff --git a/src/mudlib/zmachine/quetzal.py b/src/mudlib/zmachine/quetzal.py index a8eb872..05e714f 100644 --- a/src/mudlib/zmachine/quetzal.py +++ b/src/mudlib/zmachine/quetzal.py @@ -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 diff --git a/tests/test_zmachine_opcodes.py b/tests/test_zmachine_opcodes.py index db15c19..1c6bf76 100644 --- a/tests/test_zmachine_opcodes.py +++ b/tests/test_zmachine_opcodes.py @@ -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.