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: elif 4 <= self.version <= 5:
if self._total_size > 262144: if self._total_size > 262144:
raise ZMemoryBadStoryfileSize raise ZMemoryBadStoryfileSize
elif self.version == 8:
if self._total_size > 524288:
raise ZMemoryBadStoryfileSize
else: else:
raise ZMemoryUnsupportedVersion raise ZMemoryUnsupportedVersion
@ -253,6 +256,10 @@ class ZMemory:
if address < 0 or address > (self._total_size // 4): if address < 0 or address > (self._total_size // 4):
raise ZMemoryOutOfBounds raise ZMemoryOutOfBounds
return address * 4 return address * 4
elif self.version == 8:
if address < 0 or address > (self._total_size // 8):
raise ZMemoryOutOfBounds
return address * 8
else: else:
raise ZMemoryUnsupportedVersion raise ZMemoryUnsupportedVersion

View file

@ -77,7 +77,7 @@ class ZObjectParser:
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
self._objecttree_addr = self._propdefaults_addr + 62 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 self._objecttree_addr = self._propdefaults_addr + 126
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
@ -90,7 +90,7 @@ class ZObjectParser:
if not (1 <= objectnum <= 255): if not (1 <= objectnum <= 255):
raise ZObjectIllegalObjectNumber raise ZObjectIllegalObjectNumber
result = self._objecttree_addr + (9 * (objectnum - 1)) 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): if not (1 <= objectnum <= 65535):
log(f"error: there is no object {objectnum}") log(f"error: there is no object {objectnum}")
raise ZObjectIllegalObjectNumber raise ZObjectIllegalObjectNumber
@ -111,7 +111,7 @@ class ZObjectParser:
addr += 4 # skip past attributes addr += 4 # skip past attributes
result = self._memory[addr : addr + 3] 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 addr += 6 # skip past attributes
result = [ result = [
self._memory.read_word(addr), self._memory.read_word(addr),
@ -135,7 +135,7 @@ class ZObjectParser:
# skip past attributes and relatives # skip past attributes and relatives
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
addr += 7 addr += 7
elif 4 <= self._memory.version <= 5: elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
addr += 12 addr += 12
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
@ -150,7 +150,7 @@ class ZObjectParser:
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
if not (1 <= propnum <= 31): if not (1 <= propnum <= 31):
raise ZObjectIllegalPropertyNumber raise ZObjectIllegalPropertyNumber
elif 4 <= self._memory.version <= 5: elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
if not (1 <= propnum <= 63): if not (1 <= propnum <= 63):
raise ZObjectIllegalPropertyNumber raise ZObjectIllegalPropertyNumber
else: else:
@ -171,7 +171,7 @@ class ZObjectParser:
raise ZObjectIllegalAttributeNumber raise ZObjectIllegalAttributeNumber
bf = BitField(self._memory[object_addr + (attrnum // 8)]) 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): if not (0 <= attrnum <= 47):
raise ZObjectIllegalAttributeNumber raise ZObjectIllegalAttributeNumber
bf = BitField(self._memory[object_addr + (attrnum // 8)]) bf = BitField(self._memory[object_addr + (attrnum // 8)])
@ -192,7 +192,7 @@ class ZObjectParser:
byte_offset = attrnum // 8 byte_offset = attrnum // 8
bf = BitField(self._memory[object_addr + byte_offset]) 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): if not (0 <= attrnum <= 47):
raise ZObjectIllegalAttributeNumber raise ZObjectIllegalAttributeNumber
byte_offset = attrnum // 8 byte_offset = attrnum // 8
@ -215,7 +215,7 @@ class ZObjectParser:
byte_offset = attrnum // 8 byte_offset = attrnum // 8
bf = BitField(self._memory[object_addr + byte_offset]) 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): if not (0 <= attrnum <= 47):
raise ZObjectIllegalAttributeNumber raise ZObjectIllegalAttributeNumber
byte_offset = attrnum // 8 byte_offset = attrnum // 8
@ -233,7 +233,7 @@ class ZObjectParser:
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
max = 32 max = 32
elif 4 <= self._memory.version <= 5: elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
max = 48 max = 48
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
@ -269,7 +269,7 @@ class ZObjectParser:
addr = self._get_object_addr(objectnum) addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
self._memory[addr + 4] = new_parent_num 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) self._memory.write_word(addr + 6, new_parent_num)
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
@ -280,7 +280,7 @@ class ZObjectParser:
addr = self._get_object_addr(objectnum) addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
self._memory[addr + 6] = new_child_num 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) self._memory.write_word(addr + 10, new_child_num)
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
@ -291,7 +291,7 @@ class ZObjectParser:
addr = self._get_object_addr(objectnum) addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
self._memory[addr + 5] = new_sibling_num 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) self._memory.write_word(addr + 8, new_sibling_num)
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
@ -402,7 +402,7 @@ class ZObjectParser:
return (addr, size) return (addr, size)
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: while self._memory[addr] != 0:
bf = BitField(self._memory[addr]) bf = BitField(self._memory[addr])
addr += 1 addr += 1
@ -448,7 +448,7 @@ class ZObjectParser:
proplist[pnum] = (addr, size) proplist[pnum] = (addr, size)
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: while self._memory[addr] != 0:
bf = BitField(self._memory[addr]) bf = BitField(self._memory[addr])
addr += 1 addr += 1
@ -516,7 +516,7 @@ class ZObjectParser:
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
bf = BitField(self._memory[addr]) bf = BitField(self._memory[addr])
return bf[0:5] 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]) bf = BitField(self._memory[addr])
return bf[0:6] return bf[0:6]
else: else:
@ -555,7 +555,7 @@ class ZObjectParser:
size = bf[5:8] + 1 size = bf[5:8] + 1
return size 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]) bf = BitField(self._memory[size_addr])
if bf[7]: if bf[7]:
# Two-byte header: size is in bits 0-5 of this byte # Two-byte header: size is in bits 0-5 of this byte

View file

@ -70,14 +70,14 @@ class ZRoutine:
self.start_addr += 1 self.start_addr += 1
# Initialize the local vars in the ZRoutine's dictionary. This is # Initialize the local vars in the ZRoutine's dictionary. This is
# only needed on machines v1 through v4. In v5 machines, all local # only needed on machines v1 through v4. In v5 and v8 machines, all
# variables are preinitialized to zero. # local variables are preinitialized to zero.
self.local_vars = [0 for _ in range(15)] self.local_vars = [0 for _ in range(15)]
if 1 <= zmem.version <= 4: if 1 <= zmem.version <= 4:
for i in range(num_local_vars): for i in range(num_local_vars):
self.local_vars[i] = zmem.read_word(self.start_addr) self.local_vars[i] = zmem.read_word(self.start_addr)
self.start_addr += 2 self.start_addr += 2
elif zmem.version != 5: elif zmem.version not in (5, 8):
raise ZStackUnsupportedVersion raise ZStackUnsupportedVersion
# Place call arguments into local vars, if available # 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)