Files
Hippolyzer/hippolyzer/lib/base/objects.py
Salad Dais 2608a02d5c Use viewer's object cache to better handle ObjectUpdateCached hits
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
2021-05-28 02:18:20 +00:00

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