Handle object 0 (nothing) gracefully in object parser

Per the Z-machine spec, object 0 means "nothing" and operations on it
should be safe no-ops: get_child/get_sibling/get_parent return 0,
test_attr returns false, set/clear_attr are no-ops, etc. Previously
these threw ZObjectIllegalObjectNumber, crashing on games like Curses
that pass object 0 to get_child during room transitions.
This commit is contained in:
Jared Miller 2026-02-10 18:32:36 -05:00
parent 5a98adb6ee
commit 74538756d5
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C

View file

@ -163,6 +163,8 @@ class ZObjectParser:
def get_attribute(self, objectnum, attrnum): def get_attribute(self, objectnum, attrnum):
"""Return value (0 or 1) of attribute number ATTRNUM of object """Return value (0 or 1) of attribute number ATTRNUM of object
number OBJECTNUM.""" number OBJECTNUM."""
if objectnum == 0:
return 0
object_addr = self._get_object_addr(objectnum) object_addr = self._get_object_addr(objectnum)
@ -183,6 +185,8 @@ class ZObjectParser:
def set_attribute(self, objectnum, attrnum): def set_attribute(self, objectnum, attrnum):
"""Set attribute number ATTRNUM of object number OBJECTNUM to 1.""" """Set attribute number ATTRNUM of object number OBJECTNUM to 1."""
if objectnum == 0:
return
object_addr = self._get_object_addr(objectnum) object_addr = self._get_object_addr(objectnum)
@ -206,6 +210,8 @@ class ZObjectParser:
def clear_attribute(self, objectnum, attrnum): def clear_attribute(self, objectnum, attrnum):
"""Clear attribute number ATTRNUM of object number OBJECTNUM to 0.""" """Clear attribute number ATTRNUM of object number OBJECTNUM to 0."""
if objectnum == 0:
return
object_addr = self._get_object_addr(objectnum) object_addr = self._get_object_addr(objectnum)
@ -247,18 +253,24 @@ class ZObjectParser:
def get_parent(self, objectnum): def get_parent(self, objectnum):
"""Return object number of parent of object number OBJECTNUM.""" """Return object number of parent of object number OBJECTNUM."""
if objectnum == 0:
return 0
[parent, sibling, child] = self._get_parent_sibling_child(objectnum) [parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return parent return parent
def get_child(self, objectnum): def get_child(self, objectnum):
"""Return object number of child of object number OBJECTNUM.""" """Return object number of child of object number OBJECTNUM."""
if objectnum == 0:
return 0
[parent, sibling, child] = self._get_parent_sibling_child(objectnum) [parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return child return child
def get_sibling(self, objectnum): def get_sibling(self, objectnum):
"""Return object number of sibling of object number OBJECTNUM.""" """Return object number of sibling of object number OBJECTNUM."""
if objectnum == 0:
return 0
[parent, sibling, child] = self._get_parent_sibling_child(objectnum) [parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return sibling return sibling
@ -298,6 +310,8 @@ class ZObjectParser:
def remove_object(self, objectnum): def remove_object(self, objectnum):
"""Detach object OBJECTNUM from its parent (unlink from sibling chain).""" """Detach object OBJECTNUM from its parent (unlink from sibling chain)."""
if objectnum == 0:
return
parent = self.get_parent(objectnum) parent = self.get_parent(objectnum)
if parent == 0: if parent == 0:
@ -335,6 +349,8 @@ class ZObjectParser:
Per the Z-spec: if new_child already has a parent, it is first Per the Z-spec: if new_child already has a parent, it is first
removed from that parent's child list, then made the first child removed from that parent's child list, then made the first child
of parent_object.""" of parent_object."""
if parent_object == 0 or new_child == 0:
return
# Remove from old parent first (spec says "first removed") # Remove from old parent first (spec says "first removed")
self.remove_object(new_child) self.remove_object(new_child)
@ -347,6 +363,8 @@ class ZObjectParser:
def get_shortname(self, objectnum): def get_shortname(self, objectnum):
"""Return 'short name' of object number OBJECTNUM as ascii string.""" """Return 'short name' of object number OBJECTNUM as ascii string."""
if objectnum == 0:
return ""
addr = self._get_proptable_addr(objectnum) addr = self._get_proptable_addr(objectnum)
return self._stringfactory.get(addr + 1) return self._stringfactory.get(addr + 1)
@ -354,6 +372,8 @@ class ZObjectParser:
def get_prop(self, objectnum, propnum): def get_prop(self, objectnum, propnum):
"""Return either a byte or word value of property PROPNUM of """Return either a byte or word value of property PROPNUM of
object OBJECTNUM.""" object OBJECTNUM."""
if objectnum == 0:
return 0
(addr, size) = self.get_prop_addr_len(objectnum, propnum) (addr, size) = self.get_prop_addr_len(objectnum, propnum)
if size == 1: if size == 1:
return self._memory[addr] return self._memory[addr]
@ -467,6 +487,8 @@ class ZObjectParser:
def get_property_data_address(self, objectnum, propnum): def get_property_data_address(self, objectnum, propnum):
"""Return the address of property PROPNUM's data bytes for object """Return the address of property PROPNUM's data bytes for object
OBJECTNUM. Return 0 if the object doesn't have that property.""" OBJECTNUM. Return 0 if the object doesn't have that property."""
if objectnum == 0:
return 0
try: try:
addr, size = self.get_prop_addr_len(objectnum, propnum) addr, size = self.get_prop_addr_len(objectnum, propnum)
@ -484,6 +506,8 @@ class ZObjectParser:
"""If PROPNUM is 0, return the first property number of object OBJECTNUM. """If PROPNUM is 0, return the first property number of object OBJECTNUM.
Otherwise, return the property number after PROPNUM in the property list. Otherwise, return the property number after PROPNUM in the property list.
Return 0 if there are no more properties.""" Return 0 if there are no more properties."""
if objectnum == 0:
return 0
if propnum == 0: if propnum == 0:
# Return first property number # Return first property number