diff --git a/hippolyzer/lib/client/material_manager.py b/hippolyzer/lib/client/material_manager.py deleted file mode 100644 index ca7af51..0000000 --- a/hippolyzer/lib/client/material_manager.py +++ /dev/null @@ -1,71 +0,0 @@ -import asyncio -from typing import Dict, Sequence - -from hippolyzer.lib.base.datatypes import UUID -from hippolyzer.lib.base.helpers import proxify -from hippolyzer.lib.base import llsd -from hippolyzer.lib.client.state import BaseClientRegion - - -MATERIAL_MAP_TYPE = Dict[UUID, dict] - - -class ClientMaterialManager: - """ - Material manager for a specific region - """ - - def __init__(self, region: BaseClientRegion): - self._region = proxify(region) - self.materials: MATERIAL_MAP_TYPE = {} - self._requesting_all_lock = asyncio.Lock() - - def clear(self): - self.materials.clear() - - async def request_all_materials(self) -> MATERIAL_MAP_TYPE: - """ - Request all materials within the sim - - Sigh, yes, this is best practice per indra :( - """ - if self._requesting_all_lock.locked(): - # We're already requesting all materials, wait until the lock is free - # and just return what was returned. - async with self._requesting_all_lock: - return self.materials - - async with self._requesting_all_lock: - async with self._region.caps_client.get("RenderMaterials") as resp: - resp.raise_for_status() - # Clear out all previous materials, this is a complete response. - self.materials.clear() - self._process_materials_response(await resp.read()) - return self.materials - - async def request_materials(self, material_ids: Sequence[UUID]) -> MATERIAL_MAP_TYPE: - if self._requesting_all_lock.locked(): - # Just wait for the in-flight request for all materials to complete - # if we have one in flight. - async with self._requesting_all_lock: - # Wait for the lock to be released - pass - - not_found = set(x for x in material_ids if (x not in self.materials)) - if not_found: - # Request any materials we don't already have, if there were any - data = {"Zipped": llsd.zip_llsd([x.bytes for x in material_ids])} - async with self._region.caps_client.post("RenderMaterials", data=data) as resp: - resp.raise_for_status() - self._process_materials_response(await resp.read()) - - # build up a dict of just the requested mats - mats = {} - for mat_id in material_ids: - mats[mat_id] = self.materials[mat_id] - return mats - - def _process_materials_response(self, response: bytes): - entries = llsd.unzip_llsd(llsd.parse_xml(response)["Zipped"]) - for entry in entries: - self.materials[UUID(bytes=entry["ID"])] = entry["Material"] diff --git a/hippolyzer/lib/client/object_manager.py b/hippolyzer/lib/client/object_manager.py index 528f6b7..c4911f8 100644 --- a/hippolyzer/lib/client/object_manager.py +++ b/hippolyzer/lib/client/object_manager.py @@ -28,6 +28,7 @@ from hippolyzer.lib.base.objects import ( from hippolyzer.lib.base.settings import Settings from hippolyzer.lib.client.namecache import NameCache, NameCacheEntry from hippolyzer.lib.base.templates import PCode, ObjectStateSerializer +from hippolyzer.lib.base import llsd if TYPE_CHECKING: from hippolyzer.lib.client.state import BaseClientRegion, BaseClientSession @@ -35,6 +36,7 @@ if TYPE_CHECKING: LOG = logging.getLogger(__name__) OBJECT_OR_LOCAL = Union[Object, int] +MATERIAL_MAP_TYPE = Dict[UUID, dict] class ObjectUpdateType(enum.IntEnum): @@ -50,12 +52,13 @@ class ClientObjectManager: Object manager for a specific region """ - __slots__ = ("_region", "_world_objects", "state", "__weakref__") + __slots__ = ("_region", "_world_objects", "state", "__weakref__", "_requesting_all_mats_lock") def __init__(self, region: BaseClientRegion): self._region: BaseClientRegion = proxify(region) self._world_objects: ClientWorldObjectManager = proxify(region.session().objects) self.state: RegionObjectsState = RegionObjectsState() + self._requesting_all_mats_lock = asyncio.Lock() def __len__(self): return len(self.state.localid_lookup) @@ -166,6 +169,53 @@ class ClientObjectManager: futures.append(self.state.register_future(local_id, ObjectUpdateType.UPDATE)) return futures + async def request_all_materials(self) -> MATERIAL_MAP_TYPE: + """ + Request all materials within the sim + + Sigh, yes, this is best practice per indra :( + """ + if self._requesting_all_mats_lock.locked(): + # We're already requesting all materials, wait until the lock is free + # and just return what was returned. + async with self._requesting_all_mats_lock: + return self.state.materials + + async with self._requesting_all_mats_lock: + async with self._region.caps_client.get("RenderMaterials") as resp: + resp.raise_for_status() + # Clear out all previous materials, this is a complete response. + self.state.materials.clear() + self._process_materials_response(await resp.read()) + return self.state.materials + + async def request_materials(self, material_ids: Sequence[UUID]) -> MATERIAL_MAP_TYPE: + if self._requesting_all_mats_lock.locked(): + # Just wait for the in-flight request for all materials to complete + # if we have one in flight. + async with self._requesting_all_mats_lock: + # Wait for the lock to be released + pass + + not_found = set(x for x in material_ids if (x not in self.state.materials)) + if not_found: + # Request any materials we don't already have, if there were any + data = {"Zipped": llsd.zip_llsd([x.bytes for x in material_ids])} + async with self._region.caps_client.post("RenderMaterials", data=data) as resp: + resp.raise_for_status() + self._process_materials_response(await resp.read()) + + # build up a dict of just the requested mats + mats = {} + for mat_id in material_ids: + mats[mat_id] = self.state.materials[mat_id] + return mats + + def _process_materials_response(self, response: bytes): + entries = llsd.unzip_llsd(llsd.parse_xml(response)["Zipped"]) + for entry in entries: + self.state.materials[UUID(bytes=entry["ID"])] = entry["Material"] + class ObjectEvent: __slots__ = ("object", "updated", "update_type") @@ -654,13 +704,14 @@ class RegionObjectsState: __slots__ = ( "handle", "missing_locals", "_orphans", "localid_lookup", "coarse_locations", - "_object_futures" + "_object_futures", "materials" ) def __init__(self): self.missing_locals = set() self.localid_lookup: Dict[int, Object] = {} self.coarse_locations: Dict[UUID, Vector3] = {} + self.materials: MATERIAL_MAP_TYPE = {} self._object_futures: Dict[Tuple[int, int], List[asyncio.Future]] = {} self._orphans: Dict[int, List[int]] = collections.defaultdict(list) @@ -673,6 +724,7 @@ class RegionObjectsState: self.coarse_locations.clear() self.missing_locals.clear() self.localid_lookup.clear() + self.materials.clear() def lookup_localid(self, localid: int) -> Optional[Object]: return self.localid_lookup.get(localid) diff --git a/tests/client/__init__.py b/tests/client/__init__.py index d7c11e2..8d4ef1d 100644 --- a/tests/client/__init__.py +++ b/tests/client/__init__.py @@ -7,6 +7,8 @@ from hippolyzer.lib.base.message.message import Message from hippolyzer.lib.base.message.message_handler import MessageHandler from hippolyzer.lib.base.network.caps_client import CapsClient from hippolyzer.lib.base.test_utils import MockHandlingCircuit +from hippolyzer.lib.client.hippo_client import ClientSettings +from hippolyzer.lib.client.object_manager import ClientWorldObjectManager from hippolyzer.lib.client.state import BaseClientRegion, BaseClientSession, BaseClientSessionManager @@ -34,3 +36,4 @@ class MockClientSession(BaseClientSession): def __init__(self, id, secure_session_id, agent_id, circuit_code, session_manager: Optional[BaseClientSessionManager]): super().__init__(id, secure_session_id, agent_id, circuit_code, session_manager) + self.objects = ClientWorldObjectManager(self, ClientSettings(), None) diff --git a/tests/client/test_material_manager.py b/tests/client/test_material_manager.py index 64ee98e..7a1876c 100644 --- a/tests/client/test_material_manager.py +++ b/tests/client/test_material_manager.py @@ -5,7 +5,7 @@ import aioresponses from hippolyzer.lib.base.datatypes import UUID from hippolyzer.lib.base import llsd -from hippolyzer.lib.client.material_manager import ClientMaterialManager +from hippolyzer.lib.client.object_manager import ClientObjectManager from . import MockClientRegion @@ -54,16 +54,16 @@ class MaterialManagerTest(unittest.IsolatedAsyncioTestCase): body=self._make_rendermaterials_resp([self.GET_RENDERMATERIALS_BODY[0]]) ) self.region = MockClientRegion(self.FAKE_CAPS) - self.manager = ClientMaterialManager(self.region) + self.manager = ClientObjectManager(self.region) async def asyncTearDown(self): self.aio_mock.stop() async def test_fetch_all_materials(self): await self.manager.request_all_materials() - self.assertListEqual([UUID(int=1), UUID(int=2), UUID(int=3)], list(self.manager.materials.keys())) + self.assertListEqual([UUID(int=1), UUID(int=2), UUID(int=3)], list(self.manager.state.materials.keys())) async def test_fetch_some_materials(self): mats = await self.manager.request_materials((UUID(int=1),)) self.assertListEqual([UUID(int=1)], list(mats.keys())) - self.assertListEqual([UUID(int=1)], list(self.manager.materials.keys())) + self.assertListEqual([UUID(int=1)], list(self.manager.state.materials.keys()))