We need to know about an avatar's parent to get their exact position due to the Object.Position field always being relative to the parent.
140 lines
6.0 KiB
Python
140 lines
6.0 KiB
Python
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.base.templates import PCode
|
|
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)
|
|
if obj.PCode == PCode.AVATAR and "ParentID" in updated_props:
|
|
if obj.ParentID and not region.objects.lookup_localid(obj.ParentID):
|
|
# If an avatar just sat on an object we don't know about, add it to the queued
|
|
# cache misses and request if if the viewer doesn't. This should happen
|
|
# regardless of the auto-request object setting because otherwise we have no way
|
|
# to get a sitting agent's true region location, even if it's ourself.
|
|
region.objects.queued_cache_misses.add(obj.ParentID)
|
|
region.objects.request_missed_cached_objects_soon()
|
|
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)
|