mud/src/mudlib/zmachine/zobjectparser.py
Jared Miller e1c6a92368
Fix Python 3 integer division in zmachine modules
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.
2026-02-09 21:07:16 -05:00

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