Fix some event handling quirks

This commit is contained in:
Salad Dais
2024-12-11 22:56:50 +00:00
parent b4be9fa757
commit c1c2a96295
7 changed files with 132 additions and 32 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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,6 +285,7 @@ class HippoClientRegion(BaseClientRegion):
ack: Optional[int] = None
while True:
payload = {"ack": ack, "done": False}
try:
async with self.caps_client.post("EventQueueGet", llsd=payload) as resp:
if resp.status != 200:
await asyncio.sleep(0.1)
@@ -293,6 +302,9 @@ class HippoClientRegion(BaseClientRegion):
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):
self.circuit.send(

View File

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

View File

@@ -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: