from __future__ import annotations import asyncio import logging from typing import * from hippolyzer.lib.base import llsd from hippolyzer.lib.base.message.message import Message from hippolyzer.lib.client.namecache import NameCache from hippolyzer.lib.client.object_manager import ( ClientObjectManager, UpdateType, ClientWorldObjectManager, ) from hippolyzer.lib.base.objects import Object from hippolyzer.lib.proxy.addons import AddonManager from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow from hippolyzer.lib.proxy.settings import ProxySettings from hippolyzer.lib.proxy.vocache import RegionViewerObjectCacheChain if TYPE_CHECKING: from hippolyzer.lib.proxy.region import ProxiedRegion from hippolyzer.lib.proxy.sessions import Session LOG = logging.getLogger(__name__) class ProxyObjectManager(ClientObjectManager): """ Object manager for a specific region """ _region: ProxiedRegion def __init__( self, region: ProxiedRegion, may_use_vo_cache: bool = False ): super().__init__(region) self.may_use_vo_cache = may_use_vo_cache self.cache_loaded = False self.object_cache = RegionViewerObjectCacheChain([]) self._cache_miss_timer: Optional[asyncio.TimerHandle] = None self.queued_cache_misses: Set[int] = set() region.message_handler.subscribe( "RequestMultipleObjects", self._handle_request_multiple_objects, ) def load_cache(self): if not self.may_use_vo_cache or self.cache_loaded: return handle = self._region.handle if not handle: LOG.warning(f"Tried to load cache for {self._region} without a handle") return self.cache_loaded = True self.object_cache = RegionViewerObjectCacheChain.for_region(handle, self._region.cache_id) def request_missed_cached_objects_soon(self): if self._cache_miss_timer: self._cache_miss_timer.cancel() # Basically debounce. Will only trigger 0.2 seconds after the last time it's invoked to # deal with the initial flood of ObjectUpdateCached and the natural lag time between that # and the viewers' RequestMultipleObjects messages self._cache_miss_timer = asyncio.get_event_loop().call_later( 0.2, self._request_missed_cached_objects) def _request_missed_cached_objects(self): self._cache_miss_timer = None self.request_objects(self.queued_cache_misses) self.queued_cache_misses.clear() def clear(self): super().clear() self.object_cache = RegionViewerObjectCacheChain([]) self.cache_loaded = False self.queued_cache_misses.clear() if self._cache_miss_timer: self._cache_miss_timer.cancel() self._cache_miss_timer = None def _is_localid_selected(self, localid: int): return localid in self._region.session().selected.object_locals def _handle_request_multiple_objects(self, msg: Message): # Remove any queued cache misses that the viewer just requested for itself self.queued_cache_misses -= {b["ID"] for b in msg["ObjectData"]} class ProxyWorldObjectManager(ClientWorldObjectManager): _session: Session _settings: ProxySettings def __init__(self, session: Session, settings: ProxySettings, name_cache: Optional[NameCache]): super().__init__(session, settings, name_cache) session.http_message_handler.subscribe( "GetObjectCost", self._handle_get_object_cost ) def _handle_object_update_cached_misses(self, region_handle: int, missing_locals: Set[int]): if self._settings.AUTOMATICALLY_REQUEST_MISSING_OBJECTS: # Schedule these local IDs to be requested soon if the viewer doesn't request # them itself. Ideally we could just mutate the CRC of the ObjectUpdateCached # to force a CRC cache miss in the viewer, but that appears to cause the viewer # to drop the resulting ObjectUpdateCompressed when the CRC doesn't match? # It was causing all objects to go missing even though the ObjectUpdateCompressed # was received. region_mgr: Optional[ProxyObjectManager] = self._get_region_manager(region_handle) region_mgr.queued_cache_misses |= missing_locals region_mgr.request_missed_cached_objects_soon() def _run_object_update_hooks(self, obj: Object, updated_props: Set[str], update_type: UpdateType): super()._run_object_update_hooks(obj, updated_props, update_type) region = self._session.region_by_handle(obj.RegionHandle) AddonManager.handle_object_updated(self._session, region, obj, updated_props) def _run_kill_object_hooks(self, obj: Object): super()._run_kill_object_hooks(obj) region = self._session.region_by_handle(obj.RegionHandle) AddonManager.handle_object_killed(self._session, region, obj) def _lookup_cache_entry(self, region_handle: int, local_id: int, crc: int) -> Optional[bytes]: region_mgr: Optional[ProxyObjectManager] = self._get_region_manager(region_handle) return region_mgr.object_cache.lookup_object_data(local_id, crc) def _handle_get_object_cost(self, flow: HippoHTTPFlow): parsed = llsd.parse_xml(flow.response.content) self._process_get_object_cost_response(parsed)