Python 2 `/` did integer division, Python 3 returns float. Changed to `//` in zobjectparser (attribute byte offset) and zmemory (address bounds checks). Zork 1 hit this on the first test_attr opcode.
592 lines
20 KiB
Python
592 lines
20 KiB
Python
#
|
|
# A class which knows how to parse objects in the object tree.
|
|
# Implements section 12 of Z-code specification.
|
|
#
|
|
# For the license of this file, please consult the LICENSE file in the
|
|
# root directory of this distribution.
|
|
#
|
|
|
|
# This part of of the z-machine is where it becomes really clear that
|
|
# the original authoris were MIT Lisp-heads. :-) They've got a tree
|
|
# of objects going, where each object is basically a linked list of
|
|
# siblings. Specifically, each object contains a pointer to a parent,
|
|
# a pointer to its "next sibling" in the list, and a pointer to the
|
|
# head of its own children-list.
|
|
|
|
from .bitfield import BitField
|
|
from .zlogging import log
|
|
from .zstring import ZStringFactory
|
|
|
|
|
|
class ZObjectError(Exception):
|
|
"General exception for ZObject class"
|
|
|
|
pass
|
|
|
|
|
|
class ZObjectIllegalObjectNumber(ZObjectError):
|
|
"Illegal object number given."
|
|
|
|
pass
|
|
|
|
|
|
class ZObjectIllegalAttributeNumber(ZObjectError):
|
|
"Illegal attribute number given."
|
|
|
|
pass
|
|
|
|
|
|
class ZObjectIllegalPropertyNumber(ZObjectError):
|
|
"Illegal property number given."
|
|
|
|
pass
|
|
|
|
|
|
class ZObjectIllegalPropertySet(ZObjectError):
|
|
"Illegal set of a property whose size is not 1 or 2."
|
|
|
|
pass
|
|
|
|
|
|
class ZObjectIllegalVersion(ZObjectError):
|
|
"Unsupported z-machine version."
|
|
|
|
pass
|
|
|
|
|
|
class ZObjectIllegalPropLength(ZObjectError):
|
|
"Illegal property length."
|
|
|
|
pass
|
|
|
|
|
|
class ZObjectMalformedTree(ZObjectError):
|
|
"Object tree is malformed."
|
|
|
|
pass
|
|
|
|
|
|
# The interpreter should only need exactly one instance of this class.
|
|
|
|
|
|
class ZObjectParser:
|
|
def __init__(self, zmem):
|
|
self._memory = zmem
|
|
self._propdefaults_addr = zmem.read_word(0x0A)
|
|
self._stringfactory = ZStringFactory(self._memory)
|
|
|
|
if 1 <= self._memory.version <= 3:
|
|
self._objecttree_addr = self._propdefaults_addr + 62
|
|
elif 4 <= self._memory.version <= 5:
|
|
self._objecttree_addr = self._propdefaults_addr + 126
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
def _get_object_addr(self, objectnum):
|
|
"""Return address of object number OBJECTNUM."""
|
|
|
|
result = 0
|
|
if 1 <= self._memory.version <= 3:
|
|
if not (1 <= objectnum <= 255):
|
|
raise ZObjectIllegalObjectNumber
|
|
result = self._objecttree_addr + (9 * (objectnum - 1))
|
|
elif 4 <= self._memory.version <= 5:
|
|
if not (1 <= objectnum <= 65535):
|
|
log(f"error: there is no object {objectnum}")
|
|
raise ZObjectIllegalObjectNumber
|
|
result = self._objecttree_addr + (14 * (objectnum - 1))
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
log(f"address of object {objectnum} is {result}")
|
|
return result
|
|
|
|
def _get_parent_sibling_child(self, objectnum):
|
|
"""Return [parent, sibling, child] object numbers of object OBJECTNUM."""
|
|
|
|
addr = self._get_object_addr(objectnum)
|
|
|
|
result = 0
|
|
if 1 <= self._memory.version <= 3:
|
|
addr += 4 # skip past attributes
|
|
result = self._memory[addr : addr + 3]
|
|
|
|
elif 4 <= self._memory.version <= 5:
|
|
addr += 6 # skip past attributes
|
|
result = [
|
|
self._memory.read_word(addr),
|
|
self._memory.read_word(addr + 2),
|
|
self._memory.read_word(addr + 4),
|
|
]
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
log(
|
|
f"parent/sibling/child of object {objectnum} is "
|
|
f"{result[0]}, {result[1]}, {result[2]}"
|
|
)
|
|
return result
|
|
|
|
def _get_proptable_addr(self, objectnum):
|
|
"""Return address of property table of object OBJECTNUM."""
|
|
|
|
addr = self._get_object_addr(objectnum)
|
|
|
|
# skip past attributes and relatives
|
|
if 1 <= self._memory.version <= 3:
|
|
addr += 7
|
|
elif 4 <= self._memory.version <= 5:
|
|
addr += 12
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
return self._memory.read_word(addr)
|
|
|
|
def _get_default_property_addr(self, propnum):
|
|
"""Return address of default value for property PROPNUM."""
|
|
|
|
addr = self._propdefaults_addr
|
|
|
|
if 1 <= self._memory.version <= 3:
|
|
if not (1 <= propnum <= 31):
|
|
raise ZObjectIllegalPropertyNumber
|
|
elif 4 <= self._memory.version <= 5:
|
|
if not (1 <= propnum <= 63):
|
|
raise ZObjectIllegalPropertyNumber
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
return addr + (2 * (propnum - 1))
|
|
|
|
# --------- Public APIs -----------
|
|
|
|
def get_attribute(self, objectnum, attrnum):
|
|
"""Return value (0 or 1) of attribute number ATTRNUM of object
|
|
number OBJECTNUM."""
|
|
|
|
object_addr = self._get_object_addr(objectnum)
|
|
|
|
if 1 <= self._memory.version <= 3:
|
|
if not (0 <= attrnum <= 31):
|
|
raise ZObjectIllegalAttributeNumber
|
|
bf = BitField(self._memory[object_addr + (attrnum // 8)])
|
|
|
|
elif 4 <= self._memory.version <= 5:
|
|
if not (0 <= attrnum <= 47):
|
|
raise ZObjectIllegalAttributeNumber
|
|
bf = BitField(self._memory[object_addr + (attrnum // 8)])
|
|
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
return bf[7 - (attrnum % 8)]
|
|
|
|
def set_attribute(self, objectnum, attrnum):
|
|
"""Set attribute number ATTRNUM of object number OBJECTNUM to 1."""
|
|
|
|
object_addr = self._get_object_addr(objectnum)
|
|
|
|
if 1 <= self._memory.version <= 3:
|
|
if not (0 <= attrnum <= 31):
|
|
raise ZObjectIllegalAttributeNumber
|
|
byte_offset = attrnum // 8
|
|
bf = BitField(self._memory[object_addr + byte_offset])
|
|
|
|
elif 4 <= self._memory.version <= 5:
|
|
if not (0 <= attrnum <= 47):
|
|
raise ZObjectIllegalAttributeNumber
|
|
byte_offset = attrnum // 8
|
|
bf = BitField(self._memory[object_addr + byte_offset])
|
|
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
bf[7 - (attrnum % 8)] = 1
|
|
self._memory[object_addr + byte_offset] = int(bf)
|
|
|
|
def clear_attribute(self, objectnum, attrnum):
|
|
"""Clear attribute number ATTRNUM of object number OBJECTNUM to 0."""
|
|
|
|
object_addr = self._get_object_addr(objectnum)
|
|
|
|
if 1 <= self._memory.version <= 3:
|
|
if not (0 <= attrnum <= 31):
|
|
raise ZObjectIllegalAttributeNumber
|
|
byte_offset = attrnum // 8
|
|
bf = BitField(self._memory[object_addr + byte_offset])
|
|
|
|
elif 4 <= self._memory.version <= 5:
|
|
if not (0 <= attrnum <= 47):
|
|
raise ZObjectIllegalAttributeNumber
|
|
byte_offset = attrnum // 8
|
|
bf = BitField(self._memory[object_addr + byte_offset])
|
|
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
bf[7 - (attrnum % 8)] = 0
|
|
self._memory[object_addr + byte_offset] = int(bf)
|
|
|
|
def get_all_attributes(self, objectnum):
|
|
"""Return a list of all attribute numbers that are set on object
|
|
OBJECTNUM"""
|
|
|
|
if 1 <= self._memory.version <= 3:
|
|
max = 32
|
|
elif 4 <= self._memory.version <= 5:
|
|
max = 48
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
# really inefficient, but who cares?
|
|
attrs = []
|
|
for i in range(0, max):
|
|
if self.get_attribute(objectnum, i):
|
|
attrs.append(i)
|
|
return attrs
|
|
|
|
def get_parent(self, objectnum):
|
|
"""Return object number of parent of object number OBJECTNUM."""
|
|
|
|
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
|
return parent
|
|
|
|
def get_child(self, objectnum):
|
|
"""Return object number of child of object number OBJECTNUM."""
|
|
|
|
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
|
return child
|
|
|
|
def get_sibling(self, objectnum):
|
|
"""Return object number of sibling of object number OBJECTNUM."""
|
|
|
|
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
|
return sibling
|
|
|
|
def set_parent(self, objectnum, new_parent_num):
|
|
"""Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM."""
|
|
|
|
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:
|
|
self._memory.write_word(addr + 6, new_parent_num)
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
def set_child(self, objectnum, new_child_num):
|
|
"""Make OBJECTNUM's child pointer point to NEW_PARENT_NUM."""
|
|
|
|
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:
|
|
self._memory.write_word(addr + 10, new_child_num)
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
def set_sibling(self, objectnum, new_sibling_num):
|
|
"""Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM."""
|
|
|
|
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:
|
|
self._memory.write_word(addr + 8, new_sibling_num)
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
def remove_object(self, objectnum):
|
|
"""Detach object OBJECTNUM from its parent (unlink from sibling chain)."""
|
|
|
|
parent = self.get_parent(objectnum)
|
|
if parent == 0:
|
|
# Object has no parent, nothing to remove
|
|
return
|
|
|
|
sibling = self.get_sibling(objectnum)
|
|
|
|
# Check if this object is the first child
|
|
if self.get_child(parent) == objectnum:
|
|
# Make sibling the new first child
|
|
self.set_child(parent, sibling)
|
|
else:
|
|
# Walk the sibling chain to find the object before this one
|
|
prev = self.get_child(parent)
|
|
current = self.get_sibling(prev)
|
|
while current != 0:
|
|
if current == objectnum:
|
|
# Link prev to our sibling, removing us from chain
|
|
self.set_sibling(prev, sibling)
|
|
break
|
|
prev = current
|
|
current = self.get_sibling(current)
|
|
else:
|
|
# Shouldn't happen - object claimed parent but not in chain
|
|
raise ZObjectMalformedTree
|
|
|
|
# Clear this object's parent
|
|
self.set_parent(objectnum, 0)
|
|
self.set_sibling(objectnum, 0)
|
|
|
|
def insert_object(self, parent_object, new_child):
|
|
"""Prepend object NEW_CHILD to the list of PARENT_OBJECT's children."""
|
|
|
|
# Remember all the original pointers within the new_child
|
|
[p, s, c] = self._get_parent_sibling_child(new_child)
|
|
|
|
# First insert new_child intto the parent_object
|
|
original_child = self.get_child(parent_object)
|
|
self.set_sibling(new_child, original_child)
|
|
self.set_parent(new_child, parent_object)
|
|
self.set_child(parent_object, new_child)
|
|
|
|
if p == 0: # no need to 'remove' new_child, since it wasn't in a tree
|
|
return
|
|
|
|
# Hunt down and remove the new_child from its old location
|
|
item = self.get_child(p)
|
|
if item == 0:
|
|
# new_object claimed to have parent p, but p has no children!?
|
|
raise ZObjectMalformedTree
|
|
elif item == new_child: # done! new_object was head of list
|
|
self.set_child(p, s) # note that s might be 0, that's fine.
|
|
else: # walk across list of sibling links
|
|
prev = item
|
|
current = self.get_sibling(item)
|
|
while current != 0:
|
|
if current == new_child:
|
|
self.set_sibling(prev, s) # s might be 0, that's fine.
|
|
break
|
|
prev = current
|
|
current = self.get_sibling(current)
|
|
else:
|
|
# we reached the end of the list, never got a match
|
|
raise ZObjectMalformedTree
|
|
|
|
def get_shortname(self, objectnum):
|
|
"""Return 'short name' of object number OBJECTNUM as ascii string."""
|
|
|
|
addr = self._get_proptable_addr(objectnum)
|
|
return self._stringfactory.get(addr + 1)
|
|
|
|
def get_prop(self, objectnum, propnum):
|
|
"""Return either a byte or word value of property PROPNUM of
|
|
object OBJECTNUM."""
|
|
(addr, size) = self.get_prop_addr_len(objectnum, propnum)
|
|
if size == 1:
|
|
return self._memory[addr]
|
|
elif size == 2:
|
|
return self._memory.read_word(addr)
|
|
else:
|
|
raise ZObjectIllegalPropLength
|
|
|
|
def get_prop_addr_len(self, objectnum, propnum):
|
|
"""Return address & length of value for property number PROPNUM of
|
|
object number OBJECTNUM. If object has no such property, then
|
|
return the address & length of the 'default' value for the property."""
|
|
|
|
# start at the beginning of the object's proptable
|
|
addr = self._get_proptable_addr(objectnum)
|
|
# skip past the shortname of the object
|
|
addr += 2 * self._memory[addr]
|
|
pnum = 0
|
|
|
|
if 1 <= self._memory.version <= 3:
|
|
while self._memory[addr] != 0:
|
|
bf = BitField(self._memory[addr])
|
|
addr += 1
|
|
pnum = bf[4:0]
|
|
size = bf[7:5] + 1
|
|
if pnum == propnum:
|
|
return (addr, size)
|
|
addr += size
|
|
|
|
elif 4 <= self._memory.version <= 5:
|
|
while self._memory[addr] != 0:
|
|
bf = BitField(self._memory[addr])
|
|
addr += 1
|
|
pnum = bf[5:0]
|
|
if bf[7]:
|
|
bf2 = BitField(self._memory[addr])
|
|
addr += 1
|
|
size = bf2[5:0]
|
|
else:
|
|
size = 2 if bf[6] else 1
|
|
if pnum == propnum:
|
|
return (addr, size)
|
|
addr += size
|
|
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
# property list ran out, so return default propval instead.
|
|
default_value_addr = self._get_default_property_addr(propnum)
|
|
return (default_value_addr, 2)
|
|
|
|
def get_all_properties(self, objectnum):
|
|
"""Return a dictionary of all properties listed in the property
|
|
table of object OBJECTNUM. (Obviously, this discounts 'default'
|
|
property values.). The dictionary maps property numbers to (addr,
|
|
len) propval tuples."""
|
|
|
|
proplist = {}
|
|
|
|
# start at the beginning of the object's proptable
|
|
addr = self._get_proptable_addr(objectnum)
|
|
# skip past the shortname of the object
|
|
shortname_length = self._memory[addr]
|
|
addr += 1
|
|
addr += 2 * shortname_length
|
|
|
|
if 1 <= self._memory.version <= 3:
|
|
while self._memory[addr] != 0:
|
|
bf = BitField(self._memory[addr])
|
|
addr += 1
|
|
pnum = bf[4:0]
|
|
size = bf[7:5] + 1
|
|
proplist[pnum] = (addr, size)
|
|
addr += size
|
|
|
|
elif 4 <= self._memory.version <= 5:
|
|
while self._memory[addr] != 0:
|
|
bf = BitField(self._memory[addr])
|
|
addr += 1
|
|
pnum = bf[0:6]
|
|
if bf[7]:
|
|
bf2 = BitField(self._memory[addr])
|
|
addr += 1
|
|
size = bf2[0:6]
|
|
if size == 0:
|
|
size = 64
|
|
else:
|
|
size = 2 if bf[6] else 1
|
|
proplist[pnum] = (addr, size)
|
|
addr += size
|
|
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
return proplist
|
|
|
|
def set_property(self, objectnum, propnum, value):
|
|
"""Set a property on an object."""
|
|
proplist = self.get_all_properties(objectnum)
|
|
if propnum not in proplist:
|
|
raise ZObjectIllegalPropertyNumber
|
|
|
|
addr, size = proplist[propnum]
|
|
if size == 1:
|
|
self._memory[addr] = value & 0xFF
|
|
elif size == 2:
|
|
self._memory.write_word(addr, value)
|
|
else:
|
|
raise ZObjectIllegalPropertySet
|
|
|
|
def get_property_data_address(self, objectnum, propnum):
|
|
"""Return the address of property PROPNUM's data bytes for object
|
|
OBJECTNUM. Return 0 if the object doesn't have that property."""
|
|
|
|
try:
|
|
addr, size = self.get_prop_addr_len(objectnum, propnum)
|
|
# get_prop_addr_len returns default property addr if not found
|
|
# We need to check if this is the actual property or default
|
|
proplist = self.get_all_properties(objectnum)
|
|
if propnum in proplist:
|
|
return addr
|
|
else:
|
|
return 0
|
|
except ZObjectIllegalPropLength:
|
|
return 0
|
|
|
|
def get_next_property(self, objectnum, propnum):
|
|
"""If PROPNUM is 0, return the first property number of object OBJECTNUM.
|
|
Otherwise, return the property number after PROPNUM in the property list.
|
|
Return 0 if there are no more properties."""
|
|
|
|
if propnum == 0:
|
|
# Return first property number
|
|
addr = self._get_proptable_addr(objectnum)
|
|
# Skip past the shortname
|
|
addr += 2 * self._memory[addr]
|
|
# Read first property number
|
|
if self._memory[addr] == 0:
|
|
return 0
|
|
|
|
if 1 <= self._memory.version <= 3:
|
|
bf = BitField(self._memory[addr])
|
|
return bf[4:0]
|
|
elif 4 <= self._memory.version <= 5:
|
|
bf = BitField(self._memory[addr])
|
|
return bf[5:0]
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
else:
|
|
# Find the property after propnum
|
|
proplist = self.get_all_properties(objectnum)
|
|
if propnum not in proplist:
|
|
raise ZObjectIllegalPropertyNumber
|
|
|
|
# Properties are stored in descending order
|
|
# Find the next lower property number
|
|
sorted_props = sorted(proplist.keys(), reverse=True)
|
|
try:
|
|
idx = sorted_props.index(propnum)
|
|
if idx + 1 < len(sorted_props):
|
|
return sorted_props[idx + 1]
|
|
else:
|
|
return 0
|
|
except ValueError:
|
|
return 0
|
|
|
|
def get_property_length(self, data_address):
|
|
"""Given a property DATA address, return the length of that property's data.
|
|
Return 0 if data_address is 0."""
|
|
|
|
if data_address == 0:
|
|
return 0
|
|
|
|
# The size byte is just before the data address
|
|
size_addr = data_address - 1
|
|
|
|
if 1 <= self._memory.version <= 3:
|
|
bf = BitField(self._memory[size_addr])
|
|
size = bf[7:5] + 1
|
|
return size
|
|
|
|
elif 4 <= self._memory.version <= 5:
|
|
bf = BitField(self._memory[size_addr])
|
|
if bf[7]:
|
|
# Two size bytes, look at second byte
|
|
bf2 = BitField(self._memory[data_address - 2])
|
|
size = bf2[5:0]
|
|
if size == 0:
|
|
size = 64
|
|
return size
|
|
else:
|
|
# One size byte
|
|
return 2 if bf[6] else 1
|
|
|
|
else:
|
|
raise ZObjectIllegalVersion
|
|
|
|
def describe_object(self, objectnum):
|
|
"""For debugging purposes, pretty-print everything known about
|
|
object OBJECTNUM."""
|
|
|
|
print("Object number:", objectnum)
|
|
print(" Short name:", self.get_shortname(objectnum))
|
|
print(" Parent:", self.get_parent(objectnum), end=" ")
|
|
print(" Sibling:", self.get_sibling(objectnum), end=" ")
|
|
print(" Child:", self.get_child(objectnum))
|
|
print(" Attributes:", self.get_all_attributes(objectnum))
|
|
print(" Properties:")
|
|
|
|
proplist = self.get_all_properties(objectnum)
|
|
for key in list(proplist.keys()):
|
|
(addr, len) = proplist[key]
|
|
print(f" [{key:2d}] :", end=" ")
|
|
for i in range(0, len):
|
|
print(f"{self._memory[addr + i]:02X}", end=" ")
|
|
print()
|