Without this we end up in weird cases where the viewer gets a cache hit and never request the object data, creating link heirarchies where the viewer knows about all the prims but Hippolyzer only knows some of them and orphans them. Since we don't know what viewer the user is using, we scan around the disk for object caches and try to use those. 99% of the time the connection will be coming from localhost so this is fine. Fixes #11
379 lines
15 KiB
Python
379 lines
15 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
|
|
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
|
|
from hippolyzer.lib.base.templates import ObjectUpdateCompressedDataSerializer
|
|
|
|
|
|
class Object(recordclass.datatuple): # type: ignore
|
|
__options__ = {
|
|
"fast_new": False,
|
|
"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[Any] = None
|
|
TextureAnim: Optional[Any] = None
|
|
NameValue: Optional[Any] = None
|
|
Data: Optional[Any] = None
|
|
Text: Optional[str] = None
|
|
TextColor: Optional[bytes] = None
|
|
MediaURL: Optional[Any] = None
|
|
PSBlock: Optional[Any] = None
|
|
ExtraParams: Optional[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[Any] = None
|
|
|
|
def __init__(self, *, LocalID=None, State=None, FullID=None, CRC=None, PCode=None, Material=None,
|
|
ClickAction=None, Scale=None, ParentID=None, UpdateFlags=None, PathCurve=None, ProfileCurve=None,
|
|
PathBegin=None, PathEnd=None, PathScaleX=None, PathScaleY=None, PathShearX=None, PathShearY=None,
|
|
PathTwist=None, PathTwistBegin=None, PathRadiusOffset=None, PathTaperX=None, PathTaperY=None,
|
|
PathRevolutions=None, PathSkew=None, ProfileBegin=None, ProfileEnd=None, ProfileHollow=None,
|
|
TextureEntry=None, TextureAnim=None, NameValue=None, Data=None, Text=None, TextColor=None,
|
|
MediaURL=None, PSBlock=None, ExtraParams=None, Sound=None, OwnerID=None, SoundGain=None,
|
|
SoundFlags=None, SoundRadius=None, JointType=None, JointPivot=None, JointAxisOrAnchor=None,
|
|
FootCollisionPlane=None, Position=None, Velocity=None, Acceleration=None, Rotation=None,
|
|
AngularVelocity=None, TreeSpecies=None, ObjectCosts=None, ScratchPad=None):
|
|
""" set up the object attributes """
|
|
|
|
self.LocalID = LocalID # U32
|
|
self.State = State # U8
|
|
self.FullID = FullID # LLUUID
|
|
self.CRC = CRC # U32 // TEMPORARY HACK FOR JAMES
|
|
self.PCode = PCode # U8
|
|
self.Material = Material # U8
|
|
self.ClickAction = ClickAction # U8
|
|
self.Scale = Scale # LLVector3
|
|
self.ParentID = ParentID # U32
|
|
# Actually contains a weakref proxy
|
|
self.Parent: Optional[Object] = None
|
|
self.UpdateFlags = UpdateFlags # U32 // U32, see object_flags.h
|
|
self.PathCurve = PathCurve # U8
|
|
self.ProfileCurve = ProfileCurve # U8
|
|
self.PathBegin = PathBegin # U16 // 0 to 1, quanta = 0.01
|
|
self.PathEnd = PathEnd # U16 // 0 to 1, quanta = 0.01
|
|
self.PathScaleX = PathScaleX # U8 // 0 to 1, quanta = 0.01
|
|
self.PathScaleY = PathScaleY # U8 // 0 to 1, quanta = 0.01
|
|
self.PathShearX = PathShearX # U8 // -.5 to .5, quanta = 0.01
|
|
self.PathShearY = PathShearY # U8 // -.5 to .5, quanta = 0.01
|
|
self.PathTwist = PathTwist # S8 // -1 to 1, quanta = 0.01
|
|
self.PathTwistBegin = PathTwistBegin # S8 // -1 to 1, quanta = 0.01
|
|
self.PathRadiusOffset = PathRadiusOffset # S8 // -1 to 1, quanta = 0.01
|
|
self.PathTaperX = PathTaperX # S8 // -1 to 1, quanta = 0.01
|
|
self.PathTaperY = PathTaperY # S8 // -1 to 1, quanta = 0.01
|
|
self.PathRevolutions = PathRevolutions # U8 // 0 to 3, quanta = 0.015
|
|
self.PathSkew = PathSkew # S8 // -1 to 1, quanta = 0.01
|
|
self.ProfileBegin = ProfileBegin # U16 // 0 to 1, quanta = 0.01
|
|
self.ProfileEnd = ProfileEnd # U16 // 0 to 1, quanta = 0.01
|
|
self.ProfileHollow = ProfileHollow # U16 // 0 to 1, quanta = 0.01
|
|
self.TextureEntry = TextureEntry # Variable 2
|
|
self.TextureAnim = TextureAnim # Variable 1
|
|
self.NameValue = NameValue # Variable 2
|
|
self.Data = Data # Variable 2
|
|
self.Text = Text # Variable 1 // llSetText() hovering text
|
|
self.TextColor = TextColor # Fixed 4 // actually, a LLColor4U
|
|
self.MediaURL = MediaURL # Variable 1 // URL for web page, movie, etc.
|
|
self.PSBlock = PSBlock # Variable 1
|
|
self.ExtraParams = ExtraParams or {} # Variable 1
|
|
self.Sound = Sound # LLUUID
|
|
self.OwnerID = OwnerID # LLUUID // HACK object's owner id, only set if non-null sound, for muting
|
|
self.SoundGain = SoundGain # F32
|
|
self.SoundFlags = SoundFlags # U8
|
|
self.SoundRadius = SoundRadius # F32 // cutoff radius
|
|
self.JointType = JointType # U8
|
|
self.JointPivot = JointPivot # LLVector3
|
|
self.JointAxisOrAnchor = JointAxisOrAnchor # LLVector3
|
|
self.TreeSpecies = TreeSpecies
|
|
self.ScratchPad = ScratchPad
|
|
self.ObjectCosts = ObjectCosts or {}
|
|
self.ChildIDs = []
|
|
# Same as parent, contains weakref proxies.
|
|
self.Children: List[Object] = []
|
|
|
|
# from ObjectUpdateCompressed
|
|
self.FootCollisionPlane: Optional[Vector4] = FootCollisionPlane
|
|
self.Position: Optional[Vector3] = Position
|
|
self.Velocity: Optional[Vector3] = Velocity
|
|
self.Acceleration: Optional[Vector3] = Acceleration
|
|
self.Rotation: Optional[Quaternion] = Rotation
|
|
self.AngularVelocity: Optional[Vector3] = AngularVelocity
|
|
|
|
# from ObjectProperties
|
|
self.CreatorID = None
|
|
self.GroupID = None
|
|
self.CreationDate = None
|
|
self.BaseMask = None
|
|
self.OwnerMask = None
|
|
self.GroupMask = None
|
|
self.EveryoneMask = None
|
|
self.NextOwnerMask = None
|
|
self.OwnershipCost = None
|
|
# TaxRate
|
|
self.SaleType = None
|
|
self.SalePrice = None
|
|
self.AggregatePerms = None
|
|
self.AggregatePermTextures = None
|
|
self.AggregatePermTexturesOwner = None
|
|
self.Category = None
|
|
self.InventorySerial = None
|
|
self.ItemID = None
|
|
self.FolderID = None
|
|
self.FromTaskID = None
|
|
self.LastOwnerID = None
|
|
self.Name = None
|
|
self.Description = None
|
|
self.TouchName = None
|
|
self.SitName = None
|
|
self.TextureID = None
|
|
|
|
@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 isinstance(val, lazy_object_proxy.Proxy):
|
|
# TODO: be smarter about this. Can we store the raw bytes and
|
|
# compare those if it's an unparsed object?
|
|
if old_val is not val:
|
|
updated_properties.add(key)
|
|
else:
|
|
if old_val != val:
|
|
updated_properties.add(key)
|
|
setattr(self, key, val)
|
|
return updated_properties
|
|
|
|
def to_dict(self):
|
|
return recordclass.asdict(self)
|
|
|
|
|
|
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 normalize_object_update(block: Block):
|
|
object_data = {
|
|
"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):
|
|
object_data = {
|
|
**block.deserialize_var("Data", make_copy=False),
|
|
**dict(block.items()),
|
|
"TextureEntry": block.deserialize_var("TextureEntry", make_copy=False),
|
|
}
|
|
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 = ObjectUpdateCompressedDataSerializer.deserialize(None, 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):
|
|
compressed = normalize_object_update_compressed_data(block["Data"])
|
|
compressed["UpdateFlags"] = block.deserialize_var("UpdateFlags", make_copy=False)
|
|
return compressed
|