diff --git a/hippolyzer/lib/base/objects.py b/hippolyzer/lib/base/objects.py index 5d9531c..06213c0 100644 --- a/hippolyzer/lib/base/objects.py +++ b/hippolyzer/lib/base/objects.py @@ -125,12 +125,14 @@ class Object(recordclass.RecordClass, use_weakref=True): # type: ignore SitName: Optional[str] = None TextureID: Optional[List[UUID]] = None RegionHandle: Optional[int] = None + Animations: Optional[List[UUID]] = None def __init__(self, **_kwargs): """ set up the object attributes """ self.ExtraParams = self.ExtraParams or {} # Variable 1 self.ObjectCosts = self.ObjectCosts or {} self.ChildIDs = [] + self.Animations = self.Animations or [] # Same as parent, contains weakref proxies. self.Children: List[Object] = [] @@ -253,7 +255,7 @@ def normalize_object_update(block: Block, handle: int): # OwnerID is only set in this packet if a sound is playing. Don't allow # ObjectUpdates to clobber _real_ OwnerIDs we had from ObjectProperties # with a null UUID. - if object_data["OwnerID"] == UUID(): + if object_data["OwnerID"] == UUID.ZERO: del object_data["OwnerID"] del object_data["Flags"] del object_data["Gain"] @@ -309,7 +311,7 @@ def normalize_object_update_compressed_data(data: bytes): compressed["SoundFlags"] = 0 compressed["SoundGain"] = 0.0 compressed["SoundRadius"] = 0.0 - compressed["Sound"] = UUID() + compressed["Sound"] = UUID.ZERO if compressed["TextureEntry"] is None: compressed["TextureEntry"] = tmpls.TextureEntryCollection() @@ -323,7 +325,7 @@ def normalize_object_update_compressed_data(data: bytes): # Don't clobber OwnerID in case the object has a proper one from # a previous ObjectProperties. OwnerID isn't expected to be populated # on ObjectUpdates unless an attached sound is playing. - if object_data["OwnerID"] == UUID(): + if object_data["OwnerID"] == UUID.ZERO: del object_data["OwnerID"] return object_data diff --git a/hippolyzer/lib/client/object_manager.py b/hippolyzer/lib/client/object_manager.py index c4911f8..dab2e69 100644 --- a/hippolyzer/lib/client/object_manager.py +++ b/hippolyzer/lib/client/object_manager.py @@ -45,6 +45,7 @@ class ObjectUpdateType(enum.IntEnum): FAMILY = enum.auto() COSTS = enum.auto() KILL = enum.auto() + ANIMATIONS = enum.auto() class ClientObjectManager: @@ -132,7 +133,7 @@ class ClientObjectManager: # Need to wait until we get our reply fut = self.state.register_future(local_id, ObjectUpdateType.PROPERTIES) else: - # This was selected so we should already have up to date info + # This was selected so we should already have up-to-date info fut = asyncio.Future() fut.set_result(self.lookup_localid(local_id)) futures.append(fut) @@ -261,6 +262,10 @@ class ClientWorldObjectManager: self._handle_object_properties_generic) message_handler.subscribe("ObjectPropertiesFamily", self._handle_object_properties_generic) + message_handler.subscribe("AvatarAnimation", + self._handle_animation_message) + message_handler.subscribe("ObjectAnimation", + self._handle_animation_message) def lookup_fullid(self, full_id: UUID) -> Optional[Object]: return self._fullid_lookup.get(full_id, None) @@ -274,7 +279,7 @@ class ClientWorldObjectManager: @property def all_avatars(self) -> Iterable[Avatar]: - return tuple(self._avatars.values()) + return list(self._avatars.values()) def __len__(self): return len(self._fullid_lookup) @@ -293,7 +298,7 @@ class ClientWorldObjectManager: def untrack_region_objects(self, handle: int): """Handle signal that a region object manager was just cleared""" # Make sure they're gone from our lookup table - for obj in tuple(self._fullid_lookup.values()): + for obj in list(self._fullid_lookup.values()): if obj.RegionHandle == handle: del self._fullid_lookup[obj.FullID] if handle in self._region_managers: @@ -609,6 +614,33 @@ class ClientWorldObjectManager: region_state.coarse_locations.update(coarse_locations) self._rebuild_avatar_objects() + def _handle_animation_message(self, message: Message): + sender_id = message["Sender"]["ID"] + if message.name == "AvatarAnimation": + avatar = self._avatars.get(sender_id) + if not avatar: + LOG.warning(f"Received AvatarAnimation for unknown avatar {sender_id}") + return + + if not avatar.Object: + LOG.warning(f"Received AvatarAnimation for avatar with no object {sender_id}") + return + + obj = avatar.Object + elif message.name == "ObjectAnimation": + obj = self.lookup_fullid(sender_id) + if not obj: + LOG.warning(f"Received AvatarAnimation for avatar with no object {sender_id}") + return + else: + LOG.error(f"Unknown animation message type: {message.name}") + return + + obj.Animations.clear() + for block in message["AnimationList"]: + obj.Animations.append(block["AnimID"]) + self._run_object_update_hooks(obj, {"Animations"}, ObjectUpdateType.ANIMATIONS, message) + def _process_get_object_cost_response(self, parsed: dict): if "error" in parsed: return @@ -887,8 +919,6 @@ class Avatar: self.FullID: UUID = full_id self.Object: Optional["Object"] = obj self.RegionHandle: int = region_handle - # TODO: Allow hooking into getZOffsets FS bridge response - # to fill in the Z axis if it's infinite self.CoarseLocation = coarse_location self.Valid = True self.GuessedZ: Optional[float] = None