Relax version gates to accept V8 story files

V8 uses the same format as V5 (object model, opcodes, stack) with
two differences: packed address scaling (×8 instead of ×4) and max
file size (512KB instead of 256KB).

zmemory: add V8 size validation and packed_address case
zobjectparser: accept version 8 alongside 4-5 in all checks
zstackmanager: allow V8 stack initialization
V6-7 remain unsupported (different packed address format with offsets).
This commit is contained in:
Jared Miller 2026-02-10 13:37:22 -05:00
parent e0573f4229
commit 11d939a70f
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
4 changed files with 90 additions and 19 deletions

View file

@ -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

View file

@ -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

View file

@ -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

64
tests/test_zmemory_v8.py Normal file
View file

@ -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)