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.
445 lines
17 KiB
Python
445 lines
17 KiB
Python
"""
|
|
Copyright 2009, Linden Research, Inc.
|
|
See NOTICE.md for previous contributors
|
|
Copyright 2021, Salad Dais
|
|
All Rights Reserved.
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 3 of the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public License
|
|
along with this program; if not, write to the Free Software Foundation,
|
|
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import logging
|
|
import struct
|
|
from typing import *
|
|
|
|
import lazy_object_proxy
|
|
import recordclass
|
|
|
|
from hippolyzer.lib.base.datatypes import Vector3, Quaternion, Vector4, UUID, TaggedUnion
|
|
from hippolyzer.lib.base.message.message import Block
|
|
from hippolyzer.lib.base.namevalue import NameValueCollection
|
|
import hippolyzer.lib.base.serialization as se
|
|
import hippolyzer.lib.base.templates as tmpls
|
|
|
|
|
|
class Object(recordclass.datatuple): # type: ignore
|
|
__options__ = {
|
|
"use_weakref": True,
|
|
}
|
|
__weakref__: Any
|
|
|
|
LocalID: Optional[int] = None
|
|
State: Optional[int] = None
|
|
FullID: Optional[UUID] = None
|
|
CRC: Optional[int] = None
|
|
PCode: Optional[int] = None
|
|
Material: Optional[int] = None
|
|
ClickAction: Optional[int] = None
|
|
Scale: Optional[Vector3] = None
|
|
ParentID: Optional[int] = None
|
|
# Actually contains a weakref proxy
|
|
Parent: Optional[Object] = None
|
|
UpdateFlags: Optional[int] = None
|
|
PathCurve: Optional[int] = None
|
|
ProfileCurve: Optional[int] = None
|
|
PathBegin: Optional[int] = None
|
|
PathEnd: Optional[int] = None
|
|
PathScaleX: Optional[int] = None
|
|
PathScaleY: Optional[int] = None
|
|
PathShearX: Optional[int] = None
|
|
PathShearY: Optional[int] = None
|
|
PathTwist: Optional[int] = None
|
|
PathTwistBegin: Optional[int] = None
|
|
PathRadiusOffset: Optional[int] = None
|
|
PathTaperX: Optional[int] = None
|
|
PathTaperY: Optional[int] = None
|
|
PathRevolutions: Optional[int] = None
|
|
PathSkew: Optional[int] = None
|
|
ProfileBegin: Optional[int] = None
|
|
ProfileEnd: Optional[int] = None
|
|
ProfileHollow: Optional[int] = None
|
|
TextureEntry: Optional[tmpls.TextureEntry] = None
|
|
TextureAnim: Optional[tmpls.TextureAnim] = None
|
|
NameValue: Optional[Any] = None
|
|
Data: Optional[Any] = None
|
|
Text: Optional[str] = None
|
|
TextColor: Optional[bytes] = None
|
|
MediaURL: Optional[str] = None
|
|
PSBlock: Optional[Dict] = None
|
|
ExtraParams: Optional[Dict[tmpls.ExtraParamType, Any]] = None
|
|
Sound: Optional[UUID] = None
|
|
OwnerID: Optional[UUID] = None
|
|
SoundGain: Optional[float] = None
|
|
SoundFlags: Optional[int] = None
|
|
SoundRadius: Optional[float] = None
|
|
JointType: Optional[int] = None
|
|
JointPivot: Optional[int] = None
|
|
JointAxisOrAnchor: Optional[int] = None
|
|
TreeSpecies: Optional[int] = None
|
|
ScratchPad: Optional[bytes] = None
|
|
ObjectCosts: Optional[Dict] = None
|
|
ChildIDs: Optional[List[int]] = None
|
|
# Same as parent, contains weakref proxies.
|
|
Children: Optional[List[Object]] = None
|
|
|
|
FootCollisionPlane: Optional[Vector4] = None
|
|
Position: Optional[Vector3] = None
|
|
Velocity: Optional[Vector3] = None
|
|
Acceleration: Optional[Vector3] = None
|
|
Rotation: Optional[Quaternion] = None
|
|
AngularVelocity: Optional[Vector3] = None
|
|
|
|
# from ObjectProperties
|
|
CreatorID: Optional[UUID] = None
|
|
GroupID: Optional[UUID] = None
|
|
CreationDate: Optional[int] = None
|
|
BaseMask: Optional[int] = None
|
|
OwnerMask: Optional[int] = None
|
|
GroupMask: Optional[int] = None
|
|
EveryoneMask: Optional[int] = None
|
|
NextOwnerMask: Optional[int] = None
|
|
OwnershipCost: Optional[int] = None
|
|
# TaxRate
|
|
SaleType: Optional[int] = None
|
|
SalePrice: Optional[int] = None
|
|
AggregatePerms: Optional[int] = None
|
|
AggregatePermTextures: Optional[int] = None
|
|
AggregatePermTexturesOwner: Optional[int] = None
|
|
Category: Optional[int] = None
|
|
InventorySerial: Optional[int] = None
|
|
ItemID: Optional[UUID] = None
|
|
FolderID: Optional[UUID] = None
|
|
FromTaskID: Optional[UUID] = None
|
|
LastOwnerID: Optional[UUID] = None
|
|
Name: Optional[str] = None
|
|
Description: Optional[str] = None
|
|
TouchName: Optional[str] = None
|
|
SitName: Optional[str] = None
|
|
TextureID: Optional[List[UUID]] = None
|
|
RegionHandle: Optional[int] = None
|
|
|
|
def __init__(self, **_kwargs):
|
|
""" set up the object attributes """
|
|
self.ExtraParams = self.ExtraParams or {} # Variable 1
|
|
self.ObjectCosts = self.ObjectCosts or {}
|
|
self.ChildIDs = []
|
|
# Same as parent, contains weakref proxies.
|
|
self.Children: List[Object] = []
|
|
|
|
@property
|
|
def GlobalPosition(self) -> Vector3:
|
|
return handle_to_global_pos(self.RegionHandle) + self.RegionPosition
|
|
|
|
@property
|
|
def RegionPosition(self) -> Vector3:
|
|
if not self.ParentID:
|
|
return self.Position
|
|
|
|
if not self.Parent:
|
|
raise ValueError("Can't calculate an orphan's RegionPosition")
|
|
|
|
# TODO: Cache this and dirty cache if ancestor updates pos?
|
|
return self.Parent.RegionPosition + (self.Position.rotated(self.Parent.RegionRotation))
|
|
|
|
@property
|
|
def RegionRotation(self) -> Quaternion:
|
|
if not self.ParentID:
|
|
return self.Rotation
|
|
|
|
if not self.Parent:
|
|
raise ValueError("Can't calculate an orphan's RegionRotation")
|
|
|
|
# TODO: Cache this and dirty cache if ancestor updates rot?
|
|
return self.Rotation * self.Parent.RegionRotation
|
|
|
|
@property
|
|
def AncestorsKnown(self) -> bool:
|
|
obj = self
|
|
while obj.ParentID:
|
|
if not obj.Parent:
|
|
return False
|
|
obj = obj.Parent
|
|
return True
|
|
|
|
def update_properties(self, properties: Dict[str, Any]) -> Set[str]:
|
|
""" takes a dictionary of attribute:value and makes it so """
|
|
updated_properties = set()
|
|
for key, val in properties.items():
|
|
if hasattr(self, key):
|
|
old_val = getattr(self, key, dataclasses.MISSING)
|
|
# Don't check equality if we're using a lazy proxy,
|
|
# parsing is deferred until we actually use it.
|
|
if any(isinstance(x, lazy_object_proxy.Proxy) for x in (old_val, val)):
|
|
# TODO: be smarter about this. Can we store the raw bytes and
|
|
# compare those if it's an unparsed object?
|
|
is_updated = old_val is not val
|
|
else:
|
|
is_updated = old_val != val
|
|
if is_updated:
|
|
updated_properties.add(key)
|
|
setattr(self, key, val)
|
|
return updated_properties
|
|
|
|
def to_dict(self):
|
|
val = recordclass.asdict(self)
|
|
del val["Children"]
|
|
del val["Parent"]
|
|
return val
|
|
|
|
|
|
def handle_to_gridxy(handle: int) -> Tuple[int, int]:
|
|
return (handle >> 32) // 256, (handle & 0xFFffFFff) // 256
|
|
|
|
|
|
def gridxy_to_handle(x: int, y: int):
|
|
return ((x * 256) << 32) | (y * 256)
|
|
|
|
|
|
def handle_to_global_pos(handle: int) -> Vector3:
|
|
return Vector3(handle >> 32, handle & 0xFFffFFff)
|
|
|
|
|
|
def normalize_object_update(block: Block, handle: int):
|
|
object_data = {
|
|
"RegionHandle": handle,
|
|
"FootCollisionPlane": None,
|
|
"SoundFlags": block["Flags"],
|
|
"SoundGain": block["Gain"],
|
|
"SoundRadius": block["Radius"],
|
|
**dict(block.items()),
|
|
"TextureEntry": block.deserialize_var("TextureEntry", make_copy=False),
|
|
"NameValue": block.deserialize_var("NameValue", make_copy=False),
|
|
"TextureAnim": block.deserialize_var("TextureAnim", make_copy=False),
|
|
"ExtraParams": block.deserialize_var("ExtraParams", make_copy=False) or {},
|
|
"PSBlock": block.deserialize_var("PSBlock", make_copy=False).value,
|
|
"UpdateFlags": block.deserialize_var("UpdateFlags", make_copy=False),
|
|
"State": block.deserialize_var("State", make_copy=False),
|
|
**block.deserialize_var("ObjectData", make_copy=False).value,
|
|
}
|
|
object_data["LocalID"] = object_data.pop("ID")
|
|
# Empty == not updated
|
|
if not object_data["TextureEntry"]:
|
|
object_data.pop("TextureEntry")
|
|
# OwnerID is only set in this packet if a sound is playing. Don't allow
|
|
# ObjectUpdates to clobber _real_ OwnerIDs we had from ObjectProperties
|
|
# with a null UUID.
|
|
if object_data["OwnerID"] == UUID():
|
|
del object_data["OwnerID"]
|
|
del object_data["Flags"]
|
|
del object_data["Gain"]
|
|
del object_data["Radius"]
|
|
del object_data["ObjectData"]
|
|
return object_data
|
|
|
|
|
|
def normalize_terse_object_update(block: Block, handle: int):
|
|
object_data = {
|
|
**block.deserialize_var("Data", make_copy=False),
|
|
**dict(block.items()),
|
|
"TextureEntry": block.deserialize_var("TextureEntry", make_copy=False),
|
|
"RegionHandle": handle,
|
|
}
|
|
object_data["LocalID"] = object_data.pop("ID")
|
|
object_data.pop("Data")
|
|
# Empty == not updated
|
|
if object_data["TextureEntry"] is None:
|
|
object_data.pop("TextureEntry")
|
|
return object_data
|
|
|
|
|
|
def normalize_object_update_compressed_data(data: bytes):
|
|
# Shared by ObjectUpdateCompressed and VOCache case
|
|
compressed = FastObjectUpdateCompressedDataDeserializer.read(data)
|
|
# TODO: ObjectUpdateCompressed doesn't provide a default value for unused
|
|
# fields, whereas ObjectUpdate and friends do (TextColor, etc.)
|
|
# need some way to normalize ObjectUpdates so they won't appear to have
|
|
# changed just because an ObjectUpdate got sent with a default value
|
|
# Only used for determining which sections are present
|
|
del compressed["Flags"]
|
|
|
|
ps_block = compressed.pop("PSBlockNew", None)
|
|
if ps_block is None:
|
|
ps_block = compressed.pop("PSBlock", None)
|
|
if ps_block is None:
|
|
ps_block = TaggedUnion(0, None)
|
|
compressed.pop("PSBlock", None)
|
|
if compressed["NameValue"] is None:
|
|
compressed["NameValue"] = NameValueCollection()
|
|
|
|
object_data = {
|
|
"PSBlock": ps_block.value,
|
|
# Parent flag not set means explicitly un-parented
|
|
"ParentID": compressed.pop("ParentID", None) or 0,
|
|
"LocalID": compressed.pop("ID"),
|
|
**compressed,
|
|
}
|
|
if object_data["TextureEntry"] is None:
|
|
object_data.pop("TextureEntry")
|
|
# Don't clobber OwnerID in case the object has a proper one.
|
|
if object_data["OwnerID"] == UUID():
|
|
del object_data["OwnerID"]
|
|
return object_data
|
|
|
|
|
|
def normalize_object_update_compressed(block: Block, handle: int):
|
|
compressed = normalize_object_update_compressed_data(block["Data"])
|
|
compressed["UpdateFlags"] = block.deserialize_var("UpdateFlags", make_copy=False)
|
|
compressed["RegionHandle"] = handle
|
|
return compressed
|
|
|
|
|
|
class SimpleStructReader(se.BufferReader):
|
|
def read_struct(self, spec: struct.Struct, peek=False) -> Tuple[Any, ...]:
|
|
val = spec.unpack_from(self._buffer, self._pos)
|
|
if not peek:
|
|
self._pos += spec.size
|
|
return val
|
|
|
|
def read_bytes_null_term(self) -> bytes:
|
|
old_offset = self._pos
|
|
while self._buffer[self._pos] != 0:
|
|
self._pos += 1
|
|
val = self._buffer[old_offset:self._pos]
|
|
self._pos += 1
|
|
return val
|
|
|
|
|
|
class FastObjectUpdateCompressedDataDeserializer:
|
|
HEADER_STRUCT = struct.Struct("<16sIBBIBB3f3f3fI16s")
|
|
ANGULAR_VELOCITY_STRUCT = struct.Struct("<3f")
|
|
PARENT_ID_STRUCT = struct.Struct("<I")
|
|
TREE_SPECIES_STRUCT = struct.Struct("<B")
|
|
DATAPACKER_LEN = struct.Struct("<I")
|
|
COLOR_ADAPTER = tmpls.Color4()
|
|
PARTICLES_OLD = se.TypedBytesFixed(86, tmpls.PSBLOCK_TEMPLATE)
|
|
SOUND_STRUCT = struct.Struct("<16sfBf")
|
|
PRIM_PARAMS_STRUCT = struct.Struct("<BBHHBBBBbbbbbBbHHH")
|
|
ATTACHMENT_STATE_ADAPTER = tmpls.AttachmentStateAdapter(None)
|
|
|
|
@classmethod
|
|
def read(cls, data: bytes) -> Dict:
|
|
reader = SimpleStructReader("<", data)
|
|
foo = reader.read_struct(cls.HEADER_STRUCT)
|
|
full_id, local_id, pcode, state, crc, material, click_action, \
|
|
scalex, scaley, scalez, posx, posy, posz, rotx, roty, rotz, \
|
|
flags, owner_id = foo
|
|
scale = Vector3(scalex, scaley, scalez)
|
|
full_id = UUID(bytes=full_id)
|
|
pcode = tmpls.PCode(pcode)
|
|
if pcode == tmpls.PCode.AVATAR:
|
|
state = tmpls.AgentState(state)
|
|
elif pcode == tmpls.PCode.PRIMITIVE:
|
|
state = cls.ATTACHMENT_STATE_ADAPTER.decode(state, None)
|
|
pos = Vector3(posx, posy, posz)
|
|
rot = Quaternion(rotx, roty, rotz)
|
|
owner_id = UUID(bytes=owner_id)
|
|
ang_vel = None
|
|
if flags & tmpls.CompressedFlags.ANGULAR_VELOCITY.value:
|
|
ang_vel = Vector3(*reader.read_struct(cls.ANGULAR_VELOCITY_STRUCT))
|
|
parent_id = None
|
|
if flags & tmpls.CompressedFlags.PARENT_ID.value:
|
|
parent_id = reader.read_struct(cls.PARENT_ID_STRUCT)[0]
|
|
tree_species = None
|
|
if flags & tmpls.CompressedFlags.TREE.value:
|
|
tree_species = reader.read_struct(cls.TREE_SPECIES_STRUCT)[0]
|
|
scratchpad = None
|
|
if flags & tmpls.CompressedFlags.SCRATCHPAD.value:
|
|
scratchpad = reader.read_bytes(reader.read_struct(cls.DATAPACKER_LEN)[0])
|
|
text = None
|
|
text_color = None
|
|
if flags & tmpls.CompressedFlags.TEXT.value:
|
|
text = reader.read_bytes_null_term().decode("utf8")
|
|
text_color = cls.COLOR_ADAPTER.decode(reader.read_bytes(4), ctx=None)
|
|
media_url = None
|
|
if flags & tmpls.CompressedFlags.MEDIA_URL.value:
|
|
media_url = reader.read_bytes_null_term().decode("utf8")
|
|
psblock = None
|
|
if flags & tmpls.CompressedFlags.PARTICLES.value:
|
|
psblock = reader.read(cls.PARTICLES_OLD)
|
|
extra_params = reader.read(tmpls.EXTRA_PARAM_COLLECTION)
|
|
sound, sound_gain, sound_flags, sound_radius = None, None, None, None
|
|
if flags & tmpls.CompressedFlags.SOUND.value:
|
|
sound, sound_gain, sound_flags, sound_radius = reader.read_struct(cls.SOUND_STRUCT)
|
|
sound = UUID(bytes=sound)
|
|
sound_flags = tmpls.SoundFlags(sound_flags)
|
|
name_value = None
|
|
if flags & tmpls.CompressedFlags.NAME_VALUES.value:
|
|
name_value = reader.read(tmpls.NAMEVALUES_TERMINATED_TEMPLATE)
|
|
path_curve, profile_curve, path_begin, path_end, path_scale_x, path_scale_y, \
|
|
path_shear_x, path_shear_y, path_twist, path_twist_begin, path_radius_offset, \
|
|
path_taper_x, path_taper_y, path_revolutions, path_skew, profile_begin, \
|
|
profile_end, profile_hollow = reader.read_struct(cls.PRIM_PARAMS_STRUCT)
|
|
texture_entry = reader.read(tmpls.DATA_PACKER_TE_TEMPLATE)
|
|
texture_anim = None
|
|
if flags & tmpls.CompressedFlags.TEXTURE_ANIM.value:
|
|
texture_anim = reader.read(se.TypedByteArray(se.U32, tmpls.TA_TEMPLATE))
|
|
psblock_new = None
|
|
if flags & tmpls.CompressedFlags.PARTICLES_NEW.value:
|
|
psblock_new = reader.read(tmpls.PSBLOCK_TEMPLATE)
|
|
|
|
if len(reader):
|
|
logging.warning(f"{len(reader)} bytes left at end of buffer for compressed {data!r}")
|
|
|
|
return {
|
|
"FullID": full_id,
|
|
"ID": local_id,
|
|
"PCode": pcode,
|
|
"State": state,
|
|
"CRC": crc,
|
|
"Material": material,
|
|
"ClickAction": click_action,
|
|
"Scale": scale,
|
|
"Position": pos,
|
|
"Rotation": rot,
|
|
"Flags": flags,
|
|
"OwnerID": owner_id,
|
|
"AngularVelocity": ang_vel,
|
|
"ParentID": parent_id,
|
|
"TreeSpecies": tree_species,
|
|
"ScratchPad": scratchpad,
|
|
"Text": text,
|
|
"TextColor": text_color,
|
|
"MediaURL": media_url,
|
|
"PSBlock": psblock,
|
|
"ExtraParams": extra_params,
|
|
"Sound": sound,
|
|
"SoundGain": sound_gain,
|
|
"SoundFlags": sound_flags,
|
|
"SoundRadius": sound_radius,
|
|
"NameValue": name_value,
|
|
"PathCurve": path_curve,
|
|
"ProfileCurve": profile_curve,
|
|
"PathBegin": path_begin, # 0 to 1, quanta = 0.01
|
|
"PathEnd": path_end, # 0 to 1, quanta = 0.01
|
|
"PathScaleX": path_scale_x, # 0 to 1, quanta = 0.01
|
|
"PathScaleY": path_scale_y, # 0 to 1, quanta = 0.01
|
|
"PathShearX": path_shear_x, # -.5 to .5, quanta = 0.01
|
|
"PathShearY": path_shear_y, # -.5 to .5, quanta = 0.01
|
|
"PathTwist": path_twist, # -1 to 1, quanta = 0.01
|
|
"PathTwistBegin": path_twist_begin, # -1 to 1, quanta = 0.01
|
|
"PathRadiusOffset": path_radius_offset, # -1 to 1, quanta = 0.01
|
|
"PathTaperX": path_taper_x, # -1 to 1, quanta = 0.01
|
|
"PathTaperY": path_taper_y, # -1 to 1, quanta = 0.01
|
|
"PathRevolutions": path_revolutions, # 0 to 3, quanta = 0.015
|
|
"PathSkew": path_skew, # -1 to 1, quanta = 0.01
|
|
"ProfileBegin": profile_begin, # 0 to 1, quanta = 0.01
|
|
"ProfileEnd": profile_end, # 0 to 1, quanta = 0.01
|
|
"ProfileHollow": profile_hollow, # 0 to 1, quanta = 0.01
|
|
"TextureEntry": texture_entry,
|
|
"TextureAnim": texture_anim,
|
|
"PSBlockNew": psblock_new,
|
|
}
|