From c1c2a96295f6dce5defa314ea1598d5b2fbfd9fb Mon Sep 17 00:00:00 2001 From: Salad Dais Date: Wed, 11 Dec 2024 22:56:50 +0000 Subject: [PATCH] Fix some event handling quirks --- hippolyzer/lib/base/events.py | 23 ++++++----- hippolyzer/lib/base/helpers.py | 2 +- hippolyzer/lib/base/templates.py | 13 ++++++- hippolyzer/lib/base/wearables.py | 55 +++++++++++++++++++++++++++ hippolyzer/lib/client/hippo_client.py | 48 ++++++++++++++--------- tests/base/test_events.py | 12 ++++++ tests/base/test_message_wrapper.py | 11 +++++- 7 files changed, 132 insertions(+), 32 deletions(-) diff --git a/hippolyzer/lib/base/events.py b/hippolyzer/lib/base/events.py index c4f8b91..ae15347 100644 --- a/hippolyzer/lib/base/events.py +++ b/hippolyzer/lib/base/events.py @@ -41,7 +41,8 @@ class Event: return self - def _handler_key(self, handler): + @staticmethod + def _handler_key(handler): return handler[:3] def unsubscribe(self, handler, *args, **kwargs): @@ -55,21 +56,23 @@ class Event: raise ValueError(f"Handler {handler!r} is not subscribed to this event.") return self + def _create_async_wrapper(self, handler, args, inner_args, kwargs): + # Note that unsubscription may be delayed due to asyncio scheduling :) + async def _run_handler_wrapper(): + unsubscribe = await handler(args, *inner_args, **kwargs) + if unsubscribe: + _ = self.unsubscribe(handler, *inner_args, **kwargs) + return _run_handler_wrapper + def notify(self, args): - for handler in self.subscribers[:]: - handler, inner_args, kwargs, one_shot, predicate = handler + for subscriber in self.subscribers[:]: + handler, inner_args, kwargs, one_shot, predicate = subscriber if predicate and not predicate(args): continue if one_shot: self.unsubscribe(handler, *inner_args, **kwargs) if asyncio.iscoroutinefunction(handler): - # Note that unsubscription may be delayed due to asyncio scheduling :) - - async def _run_handler_wrapper(): - unsubscribe = await handler(args, *inner_args, **kwargs) - if unsubscribe: - _ = self.unsubscribe(handler, *inner_args, **kwargs) - create_logged_task(_run_handler_wrapper(), self.name, LOG) + create_logged_task(self._create_async_wrapper(handler, args, inner_args, kwargs)(), self.name, LOG) else: try: if handler(args, *inner_args, **kwargs) and not one_shot: diff --git a/hippolyzer/lib/base/helpers.py b/hippolyzer/lib/base/helpers.py index 2366c11..77c8444 100644 --- a/hippolyzer/lib/base/helpers.py +++ b/hippolyzer/lib/base/helpers.py @@ -171,7 +171,7 @@ def get_mtime(path): def fut_logger(name: str, logger: logging.Logger, fut: asyncio.Future, *args) -> None: """Callback suitable for exception logging in `Future.add_done_callback()`""" - if fut.exception(): + if not fut.cancelled() and fut.exception(): if isinstance(fut.exception(), asyncio.CancelledError): # Don't really care if the task was just cancelled return diff --git a/hippolyzer/lib/base/templates.py b/hippolyzer/lib/base/templates.py index 21522ad..d5c719c 100644 --- a/hippolyzer/lib/base/templates.py +++ b/hippolyzer/lib/base/templates.py @@ -1822,9 +1822,20 @@ class ChatSourceType(IntEnum): UNKNOWN = 3 +@dataclasses.dataclass +class ThrottleData: + resend: float = se.dataclass_field(se.F32) + land: float = se.dataclass_field(se.F32) + wind: float = se.dataclass_field(se.F32) + cloud: float = se.dataclass_field(se.F32) + task: float = se.dataclass_field(se.F32) + texture: float = se.dataclass_field(se.F32) + asset: float = se.dataclass_field(se.F32) + + @se.subfield_serializer("AgentThrottle", "Throttle", "Throttles") class AgentThrottlesSerializer(se.SimpleSubfieldSerializer): - TEMPLATE = se.Collection(None, se.F32) + TEMPLATE = se.Dataclass(ThrottleData) @se.subfield_serializer("ObjectUpdate", "ObjectData", "NameValue") diff --git a/hippolyzer/lib/base/wearables.py b/hippolyzer/lib/base/wearables.py index 6d44317..d2a25f1 100644 --- a/hippolyzer/lib/base/wearables.py +++ b/hippolyzer/lib/base/wearables.py @@ -5,6 +5,7 @@ Body parts and linden clothing layers from __future__ import annotations import dataclasses +import enum import logging from io import StringIO from typing import * @@ -21,6 +22,60 @@ LOG = logging.getLogger(__name__) _T = TypeVar("_T") WEARABLE_VERSION = "LLWearable version 22" +DEFAULT_WEARABLE_TEX = UUID("c228d1cf-4b5d-4ba8-84f4-899a0796aa97") + + +class AvatarTEIndex(enum.IntEnum): + """From llavatarappearancedefines.h""" + HEAD_BODYPAINT = 0 + UPPER_SHIRT = enum.auto() + LOWER_PANTS = enum.auto() + EYES_IRIS = enum.auto() + HAIR = enum.auto() + UPPER_BODYPAINT = enum.auto() + LOWER_BODYPAINT = enum.auto() + LOWER_SHOES = enum.auto() + HEAD_BAKED = enum.auto() + UPPER_BAKED = enum.auto() + LOWER_BAKED = enum.auto() + EYES_BAKED = enum.auto() + LOWER_SOCKS = enum.auto() + UPPER_JACKET = enum.auto() + LOWER_JACKET = enum.auto() + UPPER_GLOVES = enum.auto() + UPPER_UNDERSHIRT = enum.auto() + LOWER_UNDERPANTS = enum.auto() + SKIRT = enum.auto() + SKIRT_BAKED = enum.auto() + HAIR_BAKED = enum.auto() + LOWER_ALPHA = enum.auto() + UPPER_ALPHA = enum.auto() + HEAD_ALPHA = enum.auto() + EYES_ALPHA = enum.auto() + HAIR_ALPHA = enum.auto() + HEAD_TATTOO = enum.auto() + UPPER_TATTOO = enum.auto() + LOWER_TATTOO = enum.auto() + HEAD_UNIVERSAL_TATTOO = enum.auto() + UPPER_UNIVERSAL_TATTOO = enum.auto() + LOWER_UNIVERSAL_TATTOO = enum.auto() + SKIRT_TATTOO = enum.auto() + HAIR_TATTOO = enum.auto() + EYES_TATTOO = enum.auto() + LEFT_ARM_TATTOO = enum.auto() + LEFT_LEG_TATTOO = enum.auto() + AUX1_TATTOO = enum.auto() + AUX2_TATTOO = enum.auto() + AUX3_TATTOO = enum.auto() + LEFTARM_BAKED = enum.auto() + LEFTLEG_BAKED = enum.auto() + AUX1_BAKED = enum.auto() + AUX2_BAKED = enum.auto() + AUX3_BAKED = enum.auto() + + @property + def is_baked(self) -> bool: + return self.name.endswith("_BAKED") @dataclasses.dataclass diff --git a/hippolyzer/lib/client/hippo_client.py b/hippolyzer/lib/client/hippo_client.py index ed55506..f9df5eb 100644 --- a/hippolyzer/lib/client/hippo_client.py +++ b/hippolyzer/lib/client/hippo_client.py @@ -23,7 +23,7 @@ from hippolyzer.lib.base.message.udpdeserializer import UDPMessageDeserializer from hippolyzer.lib.base.network.caps_client import CapsClient, CAPS_DICT from hippolyzer.lib.base.network.transport import ADDR_TUPLE, Direction, SocketUDPTransport, AbstractUDPTransport from hippolyzer.lib.base.settings import Settings, SettingDescriptor -from hippolyzer.lib.base.templates import RegionHandshakeReplyFlags, ChatType +from hippolyzer.lib.base.templates import RegionHandshakeReplyFlags, ChatType, ThrottleData from hippolyzer.lib.base.transfer_manager import TransferManager from hippolyzer.lib.base.xfer_manager import XferManager from hippolyzer.lib.client.asset_uploader import AssetUploader @@ -190,7 +190,7 @@ class HippoClientRegion(BaseClientRegion): "RegionInfo", Flags=( RegionHandshakeReplyFlags.SUPPORTS_SELF_APPEARANCE - | RegionHandshakeReplyFlags.VOCACHE_IS_EMPTY + | RegionHandshakeReplyFlags.VOCACHE_CULLING_ENABLED ) ) ) @@ -208,7 +208,15 @@ class HippoClientRegion(BaseClientRegion): "Throttle", GenCounter=0, # Reasonable defaults, I guess - Throttles_=[207360.0, 165376.0, 33075.19921875, 33075.19921875, 682700.75, 682700.75, 269312.0], + Throttles_=ThrottleData( + resend=207360.0, + land=165376.0, + wind=33075.19921875, + cloud=33075.19921875, + task=682700.75, + texture=682700.75, + asset=269312.0 + ), ) ) ) @@ -277,21 +285,25 @@ class HippoClientRegion(BaseClientRegion): ack: Optional[int] = None while True: payload = {"ack": ack, "done": False} - async with self.caps_client.post("EventQueueGet", llsd=payload) as resp: - if resp.status != 200: - await asyncio.sleep(0.1) - continue - polled = await resp.read_llsd() - for event in polled["events"]: - if self._llsd_serializer.can_handle(event["message"]): - msg = self._llsd_serializer.deserialize(event) - else: - msg = Message.from_eq_event(event) - msg.sender = self.circuit_addr - msg.direction = Direction.IN - self.session().message_handler.handle(msg) - self.message_handler.handle(msg) - ack = polled["id"] + try: + async with self.caps_client.post("EventQueueGet", llsd=payload) as resp: + if resp.status != 200: + await asyncio.sleep(0.1) + continue + polled = await resp.read_llsd() + for event in polled["events"]: + if self._llsd_serializer.can_handle(event["message"]): + msg = self._llsd_serializer.deserialize(event) + else: + msg = Message.from_eq_event(event) + msg.sender = self.circuit_addr + msg.direction = Direction.IN + self.session().message_handler.handle(msg) + self.message_handler.handle(msg) + ack = polled["id"] + await asyncio.sleep(0.001) + except aiohttp.client_exceptions.ServerDisconnectedError: + # This is expected to happen during long-polling, just pick up again where we left off. await asyncio.sleep(0.001) async def _handle_ping_check(self, message: Message): diff --git a/tests/base/test_events.py b/tests/base/test_events.py index 4079cfa..67f9c29 100644 --- a/tests/base/test_events.py +++ b/tests/base/test_events.py @@ -49,3 +49,15 @@ class TestEvents(unittest.IsolatedAsyncioTestCase): await called.wait() mock.assert_called_with("foo") self.assertNotIn(_mock_wrapper, [x[0] for x in self.event.subscribers]) + + async def test_multiple_subscribers(self): + called = asyncio.Event() + called2 = asyncio.Event() + + self.event.subscribe(lambda *args: called.set()) + self.event.subscribe(lambda *args: called2.set()) + + self.event.notify(None) + + self.assertTrue(called.is_set()) + self.assertTrue(called2.is_set()) diff --git a/tests/base/test_message_wrapper.py b/tests/base/test_message_wrapper.py index 447ff4e..14b3367 100644 --- a/tests/base/test_message_wrapper.py +++ b/tests/base/test_message_wrapper.py @@ -181,6 +181,8 @@ class TestMessageHandlers(unittest.IsolatedAsyncioTestCase): self.message_handler.handle(msg) async def test_subscription(self): + called = asyncio.Event() + called2 = asyncio.Event() with self.message_handler.subscribe_async( message_names=("Foo",), predicate=lambda m: m["Bar"]["Baz"] == 1, @@ -192,6 +194,10 @@ class TestMessageHandlers(unittest.IsolatedAsyncioTestCase): msg3 = Message("Foo", Block("Bar", Baz=1, Biz=3)) self._fake_received_message(msg1) self._fake_received_message(msg2) + + self.message_handler.subscribe("Foo", lambda *args: called.set()) + self.message_handler.subscribe("Foo", lambda *args: called2.set()) + self._fake_received_message(msg3) received = [] while True: @@ -199,14 +205,15 @@ class TestMessageHandlers(unittest.IsolatedAsyncioTestCase): received.append(await asyncio.wait_for(get_msg(), 0.001)) except asyncio.exceptions.TimeoutError: break - self.assertEqual(len(foo_handlers), 1) + self.assertEqual(len(foo_handlers), 3) self.assertListEqual(received, [msg1, msg3]) # The message should have been take()n, making a copy self.assertIsNot(msg1, received[0]) # take() was called, so this should have been marked queued self.assertTrue(msg1.queued) # Leaving the block should have unsubscribed automatically - self.assertEqual(len(foo_handlers), 0) + self.assertEqual(len(foo_handlers), 2) + self.assertTrue(called.is_set()) async def test_subscription_no_take(self): with self.message_handler.subscribe_async(("Foo",), take=False) as get_msg: