Start handling AvatarAppearance messages
This commit is contained in:
@@ -267,7 +267,7 @@ class Message:
|
||||
block.message_name = self.name
|
||||
block.finalize()
|
||||
|
||||
def get_block(self, block_name: str, default=None, /) -> Optional[Block]:
|
||||
def get_blocks(self, block_name: str, default=None, /) -> Optional[MsgBlockList]:
|
||||
return self.blocks.get(block_name, default)
|
||||
|
||||
@property
|
||||
|
||||
@@ -16,6 +16,8 @@ from hippolyzer.lib.base.datatypes import UUID
|
||||
from hippolyzer.lib.base.helpers import get_resource_filename
|
||||
from hippolyzer.lib.base.inventory import InventorySaleInfo, InventoryPermissions
|
||||
from hippolyzer.lib.base.legacy_schema import SchemaBase, parse_schema_line, SchemaParsingError
|
||||
import hippolyzer.lib.base.serialization as se
|
||||
from hippolyzer.lib.base.message.message import Message
|
||||
from hippolyzer.lib.base.templates import WearableType
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@@ -78,6 +80,13 @@ class AvatarTEIndex(enum.IntEnum):
|
||||
return self.name.endswith("_BAKED")
|
||||
|
||||
|
||||
class VisualParamGroup(enum.IntEnum):
|
||||
TWEAKABLE = 0
|
||||
ANIMATABLE = 1
|
||||
TWEAKABLE_NO_TRANSMIT = 2
|
||||
TRANSMIT_NOT_TWEAKABLE = 3
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class VisualParam:
|
||||
id: int
|
||||
@@ -85,26 +94,47 @@ class VisualParam:
|
||||
value_min: float
|
||||
value_max: float
|
||||
value_default: float
|
||||
group: VisualParamGroup
|
||||
# These might be `None` if the param isn't meant to be directly edited
|
||||
edit_group: Optional[str]
|
||||
wearable: Optional[str]
|
||||
|
||||
def dequantize_val(self, val: int) -> float:
|
||||
"""Dequantize U8 values from AvatarAppearance messages"""
|
||||
spec = se.QuantizedFloat(se.U8, self.value_min, self.value_max, False)
|
||||
return spec.decode(val, None)
|
||||
|
||||
|
||||
class VisualParams(List[VisualParam]):
|
||||
def __init__(self, lad_path):
|
||||
super().__init__()
|
||||
with open(lad_path, "rb") as f:
|
||||
doc = parse_etree(f)
|
||||
|
||||
temp_params = []
|
||||
for param in doc.findall(".//param"):
|
||||
self.append(VisualParam(
|
||||
temp_params.append(VisualParam(
|
||||
id=int(param.attrib["id"]),
|
||||
name=param.attrib["name"],
|
||||
group=VisualParamGroup(int(param.get("group", "0"))),
|
||||
edit_group=param.get("edit_group"),
|
||||
wearable=param.get("wearable"),
|
||||
value_min=float(param.attrib["value_min"]),
|
||||
value_max=float(param.attrib["value_max"]),
|
||||
value_default=float(param.attrib.get("value_default", 0.0))
|
||||
))
|
||||
# Some functionality relies on the list being sorted by ID, though there may be holes.
|
||||
temp_params.sort(key=lambda x: x.id)
|
||||
# Remove dupes, only using the last value present (matching indra behavior)
|
||||
# This is necessary to remove the duplicate eye pop entry...
|
||||
self.extend({x.id: x for x in temp_params}.values())
|
||||
|
||||
@property
|
||||
def appearance_params(self) -> Iterator[VisualParam]:
|
||||
for param in self:
|
||||
if param.group not in (VisualParamGroup.TWEAKABLE, VisualParamGroup.TRANSMIT_NOT_TWEAKABLE):
|
||||
continue
|
||||
yield param
|
||||
|
||||
def by_name(self, name: str) -> VisualParam:
|
||||
return [x for x in self if x.name == name][0]
|
||||
@@ -118,6 +148,12 @@ class VisualParams(List[VisualParam]):
|
||||
def by_id(self, vparam_id: int) -> VisualParam:
|
||||
return [x for x in self if x.id == vparam_id][0]
|
||||
|
||||
def parse_appearance_message(self, message: Message) -> Dict[int, float]:
|
||||
params = {}
|
||||
for param, value_block in zip(self.appearance_params, message["VisualParam"]):
|
||||
params[param.id] = param.dequantize_val(value_block["ParamValue"])
|
||||
return params
|
||||
|
||||
|
||||
VISUAL_PARAMS = VisualParams(get_resource_filename("lib/base/data/avatar_lad.xml"))
|
||||
|
||||
|
||||
@@ -378,7 +378,7 @@ class HippoClientSession(BaseClientSession):
|
||||
sim_seed = msg["EventData"]["seed-capability"]
|
||||
# We teleported or cross region, opening comms to new sim
|
||||
elif msg.name in ("TeleportFinish", "CrossedRegion"):
|
||||
sim_block = msg.get_block("RegionData", msg.get_block("Info"))[0]
|
||||
sim_block = msg.get_blocks("RegionData", msg.get_blocks("Info"))[0]
|
||||
sim_addr = (sim_block["SimIP"], sim_block["SimPort"])
|
||||
sim_handle = sim_block["RegionHandle"]
|
||||
sim_seed = sim_block["SeedCapability"]
|
||||
|
||||
@@ -27,6 +27,7 @@ from hippolyzer.lib.base.objects import (
|
||||
Object, handle_to_global_pos,
|
||||
)
|
||||
from hippolyzer.lib.base.settings import Settings
|
||||
from hippolyzer.lib.base.wearables import VISUAL_PARAMS
|
||||
from hippolyzer.lib.client.namecache import NameCache, NameCacheEntry
|
||||
from hippolyzer.lib.base.templates import PCode, ObjectStateSerializer, XferFilePath
|
||||
from hippolyzer.lib.base import llsd
|
||||
@@ -47,6 +48,7 @@ class ObjectUpdateType(enum.IntEnum):
|
||||
COSTS = enum.auto()
|
||||
KILL = enum.auto()
|
||||
ANIMATIONS = enum.auto()
|
||||
APPEARANCE = enum.auto()
|
||||
|
||||
|
||||
class ClientObjectManager:
|
||||
@@ -299,6 +301,8 @@ class ClientWorldObjectManager:
|
||||
self._handle_animation_message)
|
||||
message_handler.subscribe("ObjectAnimation",
|
||||
self._handle_animation_message)
|
||||
message_handler.subscribe("AvatarAppearance",
|
||||
self._handle_avatar_appearance_message)
|
||||
|
||||
def lookup_fullid(self, full_id: UUID) -> Optional[Object]:
|
||||
return self._fullid_lookup.get(full_id, None)
|
||||
@@ -663,7 +667,8 @@ class ClientWorldObjectManager:
|
||||
elif message.name == "ObjectAnimation":
|
||||
obj = self.lookup_fullid(sender_id)
|
||||
if not obj:
|
||||
LOG.warning(f"Received AvatarAnimation for avatar with no object {sender_id}")
|
||||
# This is only a debug message in the viewer, but let's be louder.
|
||||
LOG.warning(f"Received ObjectAnimation for animesh with no object {sender_id}")
|
||||
return
|
||||
else:
|
||||
LOG.error(f"Unknown animation message type: {message.name}")
|
||||
@@ -674,6 +679,31 @@ class ClientWorldObjectManager:
|
||||
obj.Animations.append(block["AnimID"])
|
||||
self._run_object_update_hooks(obj, {"Animations"}, ObjectUpdateType.ANIMATIONS, message)
|
||||
|
||||
def _handle_avatar_appearance_message(self, message: Message):
|
||||
sender_id: UUID = message["Sender"]["ID"]
|
||||
if message["Sender"]["IsTrial"]:
|
||||
return
|
||||
av = self.lookup_avatar(sender_id)
|
||||
if not av:
|
||||
LOG.warning(f"Received AvatarAppearance with no avatar {sender_id}")
|
||||
return
|
||||
|
||||
version = message["AppearanceData"]["CofVersion"]
|
||||
if version < av.COFVersion:
|
||||
LOG.warning(f"Ignoring stale appearance for {sender_id}, {version} < {av.COFVersion}")
|
||||
return
|
||||
|
||||
if not message.get_blocks("VisualParam"):
|
||||
LOG.warning(f"No visual params in AvatarAppearance for {sender_id}")
|
||||
return
|
||||
|
||||
av.COFVersion = version
|
||||
av.Appearance = VISUAL_PARAMS.parse_appearance_message(message)
|
||||
|
||||
av_obj = av.Object
|
||||
if av_obj:
|
||||
self._run_object_update_hooks(av_obj, set(), ObjectUpdateType.APPEARANCE, message)
|
||||
|
||||
def _process_get_object_cost_response(self, parsed: dict):
|
||||
if "error" in parsed:
|
||||
return
|
||||
@@ -953,6 +983,8 @@ class Avatar:
|
||||
self.Object: Optional["Object"] = obj
|
||||
self.RegionHandle: int = region_handle
|
||||
self.CoarseLocation = coarse_location
|
||||
self.Appearance: Dict[int, float] = {}
|
||||
self.COFVersion: int = -1
|
||||
self.Valid = True
|
||||
self.GuessedZ: Optional[float] = None
|
||||
self._resolved_name = resolved_name
|
||||
|
||||
@@ -360,7 +360,7 @@ class MITMProxyEventManager:
|
||||
sim_seed = event["body"]["seed-capability"]
|
||||
# We teleported or cross region, opening comms to new sim
|
||||
elif msg and msg.name in ("TeleportFinish", "CrossedRegion"):
|
||||
sim_block = msg.get_block("RegionData", msg.get_block("Info"))[0]
|
||||
sim_block = msg.get_blocks("RegionData", msg.get_blocks("Info"))[0]
|
||||
sim_addr = (sim_block["SimIP"], sim_block["SimPort"])
|
||||
sim_handle = sim_block["RegionHandle"]
|
||||
sim_seed = sim_block["SeedCapability"]
|
||||
|
||||
Reference in New Issue
Block a user