diff --git a/src/mudlib/zmachine/zmemory.py b/src/mudlib/zmachine/zmemory.py index 897819a..5c2ea7d 100644 --- a/src/mudlib/zmachine/zmemory.py +++ b/src/mudlib/zmachine/zmemory.py @@ -172,6 +172,9 @@ class ZMemory: elif 4 <= self.version <= 5: if self._total_size > 262144: raise ZMemoryBadStoryfileSize + elif self.version == 8: + if self._total_size > 524288: + raise ZMemoryBadStoryfileSize else: raise ZMemoryUnsupportedVersion @@ -253,6 +256,10 @@ class ZMemory: if address < 0 or address > (self._total_size // 4): raise ZMemoryOutOfBounds return address * 4 + elif self.version == 8: + if address < 0 or address > (self._total_size // 8): + raise ZMemoryOutOfBounds + return address * 8 else: raise ZMemoryUnsupportedVersion diff --git a/src/mudlib/zmachine/zobjectparser.py b/src/mudlib/zmachine/zobjectparser.py index 816e87f..c0f6ad5 100644 --- a/src/mudlib/zmachine/zobjectparser.py +++ b/src/mudlib/zmachine/zobjectparser.py @@ -77,7 +77,7 @@ class ZObjectParser: if 1 <= self._memory.version <= 3: self._objecttree_addr = self._propdefaults_addr + 62 - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: self._objecttree_addr = self._propdefaults_addr + 126 else: raise ZObjectIllegalVersion @@ -90,7 +90,7 @@ class ZObjectParser: if not (1 <= objectnum <= 255): raise ZObjectIllegalObjectNumber result = self._objecttree_addr + (9 * (objectnum - 1)) - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: if not (1 <= objectnum <= 65535): log(f"error: there is no object {objectnum}") raise ZObjectIllegalObjectNumber @@ -111,7 +111,7 @@ class ZObjectParser: addr += 4 # skip past attributes result = self._memory[addr : addr + 3] - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: addr += 6 # skip past attributes result = [ self._memory.read_word(addr), @@ -135,7 +135,7 @@ class ZObjectParser: # skip past attributes and relatives if 1 <= self._memory.version <= 3: addr += 7 - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: addr += 12 else: raise ZObjectIllegalVersion @@ -150,7 +150,7 @@ class ZObjectParser: if 1 <= self._memory.version <= 3: if not (1 <= propnum <= 31): raise ZObjectIllegalPropertyNumber - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: if not (1 <= propnum <= 63): raise ZObjectIllegalPropertyNumber else: @@ -171,7 +171,7 @@ class ZObjectParser: raise ZObjectIllegalAttributeNumber bf = BitField(self._memory[object_addr + (attrnum // 8)]) - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: if not (0 <= attrnum <= 47): raise ZObjectIllegalAttributeNumber bf = BitField(self._memory[object_addr + (attrnum // 8)]) @@ -192,7 +192,7 @@ class ZObjectParser: byte_offset = attrnum // 8 bf = BitField(self._memory[object_addr + byte_offset]) - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: if not (0 <= attrnum <= 47): raise ZObjectIllegalAttributeNumber byte_offset = attrnum // 8 @@ -215,7 +215,7 @@ class ZObjectParser: byte_offset = attrnum // 8 bf = BitField(self._memory[object_addr + byte_offset]) - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: if not (0 <= attrnum <= 47): raise ZObjectIllegalAttributeNumber byte_offset = attrnum // 8 @@ -233,7 +233,7 @@ class ZObjectParser: if 1 <= self._memory.version <= 3: max = 32 - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: max = 48 else: raise ZObjectIllegalVersion @@ -269,7 +269,7 @@ class ZObjectParser: addr = self._get_object_addr(objectnum) if 1 <= self._memory.version <= 3: self._memory[addr + 4] = new_parent_num - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: self._memory.write_word(addr + 6, new_parent_num) else: raise ZObjectIllegalVersion @@ -280,7 +280,7 @@ class ZObjectParser: addr = self._get_object_addr(objectnum) if 1 <= self._memory.version <= 3: self._memory[addr + 6] = new_child_num - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: self._memory.write_word(addr + 10, new_child_num) else: raise ZObjectIllegalVersion @@ -291,7 +291,7 @@ class ZObjectParser: addr = self._get_object_addr(objectnum) if 1 <= self._memory.version <= 3: self._memory[addr + 5] = new_sibling_num - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: self._memory.write_word(addr + 8, new_sibling_num) else: raise ZObjectIllegalVersion @@ -402,7 +402,7 @@ class ZObjectParser: return (addr, size) addr += size - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: while self._memory[addr] != 0: bf = BitField(self._memory[addr]) addr += 1 @@ -448,7 +448,7 @@ class ZObjectParser: proplist[pnum] = (addr, size) addr += size - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: while self._memory[addr] != 0: bf = BitField(self._memory[addr]) addr += 1 @@ -516,7 +516,7 @@ class ZObjectParser: if 1 <= self._memory.version <= 3: bf = BitField(self._memory[addr]) return bf[0:5] - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: bf = BitField(self._memory[addr]) return bf[0:6] else: @@ -555,7 +555,7 @@ class ZObjectParser: size = bf[5:8] + 1 return size - elif 4 <= self._memory.version <= 5: + elif 4 <= self._memory.version <= 5 or self._memory.version == 8: bf = BitField(self._memory[size_addr]) if bf[7]: # Two-byte header: size is in bits 0-5 of this byte diff --git a/src/mudlib/zmachine/zstackmanager.py b/src/mudlib/zmachine/zstackmanager.py index 7960d77..277ca71 100644 --- a/src/mudlib/zmachine/zstackmanager.py +++ b/src/mudlib/zmachine/zstackmanager.py @@ -70,14 +70,14 @@ class ZRoutine: self.start_addr += 1 # Initialize the local vars in the ZRoutine's dictionary. This is - # only needed on machines v1 through v4. In v5 machines, all local - # variables are preinitialized to zero. + # only needed on machines v1 through v4. In v5 and v8 machines, all + # local variables are preinitialized to zero. self.local_vars = [0 for _ in range(15)] if 1 <= zmem.version <= 4: for i in range(num_local_vars): self.local_vars[i] = zmem.read_word(self.start_addr) self.start_addr += 2 - elif zmem.version != 5: + elif zmem.version not in (5, 8): raise ZStackUnsupportedVersion # Place call arguments into local vars, if available diff --git a/tests/test_zmemory_v8.py b/tests/test_zmemory_v8.py new file mode 100644 index 0000000..e217bb2 --- /dev/null +++ b/tests/test_zmemory_v8.py @@ -0,0 +1,64 @@ +"""Tests for V8 z-machine version support.""" + +import pytest + +from mudlib.zmachine.zmemory import ZMemory, ZMemoryUnsupportedVersion + + +def make_minimal_story(version: int, size: int) -> bytes: + """Create a minimal z-machine story file of the specified version and size.""" + story = bytearray(size) + story[0] = version # version byte + # Set static memory base (0x0E) to a reasonable value + story[0x0E] = 0x04 + story[0x0F] = 0x00 + # Set high memory base (0x04) to end of file + story[0x04] = (size >> 8) & 0xFF + story[0x05] = size & 0xFF + # Set global variables base (0x0C) + story[0x0C] = 0x03 + story[0x0D] = 0x00 + return bytes(story) + + +def test_v3_accepts_128kb_story(): + """V3 stories can be up to 128KB (131072 bytes).""" + story = make_minimal_story(3, 131072) + mem = ZMemory(story) + assert mem.version == 3 + + +def test_v5_accepts_256kb_story(): + """V5 stories can be up to 256KB (262144 bytes).""" + story = make_minimal_story(5, 262144) + mem = ZMemory(story) + assert mem.version == 5 + + +def test_v8_accepts_512kb_story(): + """V8 stories can be up to 512KB (524288 bytes).""" + story = make_minimal_story(8, 524288) + mem = ZMemory(story) + assert mem.version == 8 + + +def test_v8_packed_address_scaling(): + """V8 uses ×8 scaling for packed addresses (not ×4 like V5).""" + story = make_minimal_story(8, 10000) + mem = ZMemory(story) + # V8 packed address 100 should map to byte address 800 + assert mem.packed_address(100) == 800 + + +def test_v6_unsupported(): + """V6 should still be unsupported (different packed address format).""" + story = make_minimal_story(6, 10000) + with pytest.raises(ZMemoryUnsupportedVersion): + ZMemory(story) + + +def test_v7_unsupported(): + """V7 should still be unsupported (different packed address format).""" + story = make_minimal_story(7, 10000) + with pytest.raises(ZMemoryUnsupportedVersion): + ZMemory(story)