diff --git a/addon_examples/greetings.py b/addon_examples/greetings.py index 0885ac2..3bca108 100644 --- a/addon_examples/greetings.py +++ b/addon_examples/greetings.py @@ -24,9 +24,9 @@ class GreetingAddon(BaseAddon): dist = Vector3.dist(our_avatar.GlobalPosition, other_avatar.GlobalPosition) if dist >= 19.0: continue - if other_avatar.Name is None: + if other_avatar.PreferredName is None: continue - send_chat(f"Greetings, {other_avatar.Name}!") + send_chat(f"Greetings, {other_avatar.PreferredName}!") addons = [GreetingAddon()] diff --git a/hippolyzer/apps/proxy.py b/hippolyzer/apps/proxy.py index 172e126..ee4d735 100644 --- a/hippolyzer/apps/proxy.py +++ b/hippolyzer/apps/proxy.py @@ -112,6 +112,7 @@ def start_proxy(extra_addons: Optional[list] = None, extra_addon_paths: Optional session_manager = session_manager or SessionManager() flow_context = session_manager.flow_context + session_manager.name_cache.load_viewer_caches() # TODO: argparse if len(sys.argv) == 3: diff --git a/hippolyzer/lib/proxy/namecache.py b/hippolyzer/lib/proxy/namecache.py index cd9a636..bfb97c6 100644 --- a/hippolyzer/lib/proxy/namecache.py +++ b/hippolyzer/lib/proxy/namecache.py @@ -1,41 +1,106 @@ from __future__ import annotations import dataclasses +import logging from typing import * +from hippolyzer.lib.base import llsd from hippolyzer.lib.base.datatypes import UUID +from hippolyzer.lib.base.message.message_handler import MessageHandler +from hippolyzer.lib.proxy.viewer_settings import iter_viewer_cache_dirs if TYPE_CHECKING: + from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow from hippolyzer.lib.proxy.message import ProxiedMessage @dataclasses.dataclass class NameCacheEntry: - FirstName: Optional[str] = None - LastName: Optional[str] = None - DisplayName: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None + display_name: Optional[str] = None + + def __str__(self): + if self.display_name: + return f"{self.display_name} ({self.legacy_name})" + return self.legacy_name + + @property + def legacy_name(self): + return f"{self.first_name} {self.last_name}" + + @property + def preferred_name(self): + if self.display_name: + return self.display_name + return self.legacy_name class NameCache: - # TODO: persist this somewhere across runs def __init__(self): self._cache: Dict[UUID, NameCacheEntry] = {} + def create_subscriptions( + self, + message_handler: MessageHandler[ProxiedMessage], + http_message_handler: MessageHandler[HippoHTTPFlow], + ): + message_handler.subscribe("UUIDNameReply", self._handle_uuid_name_reply) + http_message_handler.subscribe("GetDisplayNames", self._handle_get_display_names) + + def load_viewer_caches(self): + for cache_dir in iter_viewer_cache_dirs(): + try: + namecache_file = cache_dir / "avatar_name_cache.xml" + if namecache_file.exists(): + with open(namecache_file, "rb") as f: + namecache_bytes = f.read() + agents = llsd.parse_xml(namecache_bytes)["agents"] + for agent_id, agent_data in agents.items(): + # Don't set display name if they just have the default + display_name = None + if not agent_data["is_display_name_default"]: + display_name = agent_data["display_name"] + self.update(UUID(agent_id), { + "FirstName": agent_data["legacy_first_name"], + "LastName": agent_data["legacy_last_name"], + "DisplayName": display_name, + }) + except: + logging.exception(f"Failed to load namecache from {cache_dir}") + def lookup(self, uuid: UUID) -> Optional[NameCacheEntry]: return self._cache.get(uuid) def update(self, uuid: UUID, vals: dict): # upsert the cache entry entry = self._cache.get(uuid) or NameCacheEntry() - entry.LastName = vals.get("LastName") or entry.LastName - entry.FirstName = vals.get("FirstName") or entry.FirstName - entry.DisplayName = vals.get("DisplayName") or entry.DisplayName + if "FirstName" in vals: + entry.first_name = vals["FirstName"] + if "LastName" in vals: + entry.last_name = vals["LastName"] + if "DisplayName" in vals: + entry.display_name = vals["DisplayName"] if vals["DisplayName"] else None self._cache[uuid] = entry - def handle_uuid_name_reply(self, msg: ProxiedMessage): - """UUID lookup reply handler to be registered by regions""" + def _handle_uuid_name_reply(self, msg: ProxiedMessage): for block in msg.blocks["UUIDNameBlock"]: self.update(block["ID"], { "FirstName": block["FirstName"], "LastName": block["LastName"], }) + + def _handle_get_display_names(self, flow: HippoHTTPFlow): + if flow.response.status_code != 200: + return + parsed = llsd.parse_xml(flow.response.content) + for agent in parsed["agents"]: + # Don't set display name if they just have the default + display_name = None + if not agent["is_display_name_default"]: + display_name = agent["display_name"] + self.update(agent["id"], { + "FirstName": agent["legacy_first_name"], + "LastName": agent["legacy_last_name"], + "DisplayName": display_name, + }) diff --git a/hippolyzer/lib/proxy/objects.py b/hippolyzer/lib/proxy/objects.py index 32b86dc..5a4fb74 100644 --- a/hippolyzer/lib/proxy/objects.py +++ b/hippolyzer/lib/proxy/objects.py @@ -25,7 +25,7 @@ from hippolyzer.lib.base.objects import ( from hippolyzer.lib.proxy.addons import AddonManager from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow from hippolyzer.lib.proxy.message import ProxiedMessage -from hippolyzer.lib.proxy.namecache import NameCache +from hippolyzer.lib.proxy.namecache import NameCache, NameCacheEntry from hippolyzer.lib.proxy.templates import PCode, ObjectStateSerializer from hippolyzer.lib.proxy.vocache import RegionViewerObjectCacheChain @@ -91,7 +91,7 @@ class Avatar: region_handle: int, obj: Optional["Object"] = None, coarse_location: Optional[Vector3] = None, - resolved_name: Optional[str] = None, + resolved_name: Optional[NameCacheEntry] = None, ): self.FullID: UUID = full_id self.Object: Optional["Object"] = obj @@ -119,10 +119,15 @@ class Avatar: @property def Name(self) -> Optional[str]: - if self.Object: - nv: Dict[str, str] = self.Object.NameValue.to_dict() - return f"{nv['FirstName']} {nv['LastName']}" - return self._resolved_name + if not self._resolved_name: + return None + return str(self._resolved_name) + + @property + def PreferredName(self) -> Optional[str]: + if not self._resolved_name: + return None + return self._resolved_name.preferred_name class ObjectManager: @@ -143,9 +148,7 @@ class ObjectManager: self.missing_locals = set() self._orphan_manager = OrphanManager() self._world_objects: WorldObjectManager = region.session().objects - name_cache = region.session().session_manager.name_cache - # Use a local namecache if we don't have a session manager - self.name_cache: Optional[NameCache] = name_cache or NameCache() + self.name_cache: NameCache = region.session().session_manager.name_cache message_handler = region.message_handler message_handler.subscribe("CoarseLocationUpdate", @@ -176,15 +179,12 @@ class ObjectManager: av_obj = av_objects.get(av_id) coarse_location = self._coarse_locations.get(av_id) - resolved_name = None - if namecache_entry := self.name_cache.lookup(av_id): - resolved_name = f"{namecache_entry.FirstName} {namecache_entry.LastName}" avatars.append(Avatar( full_id=av_id, region_handle=self._region.handle, coarse_location=coarse_location, obj=av_obj, - resolved_name=resolved_name, + resolved_name=self.name_cache.lookup(av_id), )) return avatars diff --git a/hippolyzer/lib/proxy/region.py b/hippolyzer/lib/proxy/region.py index 4b184aa..004f382 100644 --- a/hippolyzer/lib/proxy/region.py +++ b/hippolyzer/lib/proxy/region.py @@ -15,7 +15,6 @@ from hippolyzer.lib.base.message.message_handler import MessageHandler from hippolyzer.lib.base.objects import handle_to_global_pos from hippolyzer.lib.proxy.caps_client import CapsClient from hippolyzer.lib.proxy.circuit import ProxiedCircuit -from hippolyzer.lib.proxy.namecache import NameCache from hippolyzer.lib.proxy.objects import ObjectManager from hippolyzer.lib.proxy.transfer_manager import TransferManager from hippolyzer.lib.proxy.xfer_manager import XferManager @@ -65,9 +64,6 @@ class ProxiedRegion: self.transfer_manager = TransferManager(self) self.caps_client = CapsClient(self) self.objects = ObjectManager(self, use_vo_cache=True) - if session: - name_cache: NameCache = session.session_manager.name_cache - self.message_handler.subscribe("UUIDNameReply", name_cache.handle_uuid_name_reply) self._recalc_caps() @property diff --git a/hippolyzer/lib/proxy/sessions.py b/hippolyzer/lib/proxy/sessions.py index baf4c87..8b1054c 100644 --- a/hippolyzer/lib/proxy/sessions.py +++ b/hippolyzer/lib/proxy/sessions.py @@ -171,6 +171,10 @@ class SessionManager: def create_session(self, login_data) -> Session: session = Session.from_login_data(login_data, self) + self.name_cache.create_subscriptions( + session.message_handler, + session.http_message_handler, + ) self.sessions.append(session) logging.info("Created %r" % session) return session diff --git a/tests/proxy/test_object_manager.py b/tests/proxy/test_object_manager.py index fb658b4..f9f8ccd 100644 --- a/tests/proxy/test_object_manager.py +++ b/tests/proxy/test_object_manager.py @@ -356,9 +356,10 @@ class RegionObjectManagerTests(ObjectManagerTestMixin, unittest.IsolatedAsyncioT b'LastName STRING RW DS Resident\n' b'Title STRING RW DS foo', ) - self.assertEqual(self.object_manager.name_cache.lookup(obj.FullID).FirstName, "firstname") + self.assertEqual(self.object_manager.name_cache.lookup(obj.FullID).first_name, "firstname") av = self.object_manager.lookup_avatar(obj.FullID) - self.assertEqual(av.Name, "firstname Resident") + self.assertEqual(av.Name, "unicodename (firstname Resident)") + self.assertEqual(av.PreferredName, "unicodename") def test_normalize_cache_data(self): normalized = normalize_object_update_compressed_data(OBJECT_UPDATE_COMPRESSED_DATA)