Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
927a353dec | ||
|
|
bc68eeb7d2 | ||
|
|
de79f42aa6 | ||
|
|
e138ae88a1 | ||
|
|
e20a4a01ad | ||
|
|
a2b49fdc44 | ||
|
|
988a82179e | ||
|
|
4eb97b5958 |
2
.github/workflows/bundle_windows.yml
vendored
2
.github/workflows/bundle_windows.yml
vendored
@@ -18,7 +18,7 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
|
||||
@@ -74,7 +74,7 @@ class AnimTrackerAddon(BaseAddon):
|
||||
# We don't care about other messages, we're just interested in distinguishing cases where the viewer
|
||||
# specifically requested something vs something being done by the server on its own.
|
||||
return
|
||||
av = region.objects.lookup_avatar(session.agent_id)
|
||||
av = session.objects.lookup_avatar(session.agent_id)
|
||||
if not av or not av.Object:
|
||||
print("Somehow didn't know about our own av object?")
|
||||
return
|
||||
|
||||
44
addon_examples/create_shape.py
Normal file
44
addon_examples/create_shape.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Demonstrates item creation as well as bodypart / clothing upload
|
||||
"""
|
||||
|
||||
from hippolyzer.lib.base.datatypes import UUID
|
||||
from hippolyzer.lib.base.templates import WearableType, Permissions
|
||||
from hippolyzer.lib.base.wearables import Wearable, VISUAL_PARAMS
|
||||
from hippolyzer.lib.proxy.addon_utils import BaseAddon
|
||||
from hippolyzer.lib.proxy.commands import handle_command
|
||||
from hippolyzer.lib.proxy.region import ProxiedRegion
|
||||
from hippolyzer.lib.proxy.sessions import Session
|
||||
|
||||
|
||||
class ShapeCreatorAddon(BaseAddon):
|
||||
@handle_command()
|
||||
async def create_shape(self, session: Session, region: ProxiedRegion):
|
||||
"""Make a shape with pre-set parameters and place it in the body parts folder"""
|
||||
|
||||
wearable = Wearable.make_default(WearableType.SHAPE)
|
||||
# Max out the jaw jut param
|
||||
jaw_param = VISUAL_PARAMS.by_name("Jaw Jut")
|
||||
wearable.parameters[jaw_param.id] = jaw_param.value_max
|
||||
wearable.name = "Cool Shape"
|
||||
|
||||
# A unique transaction ID is needed to tie the item creation to the following asset upload.
|
||||
transaction_id = UUID.random()
|
||||
item = await session.inventory.create_item(
|
||||
UUID.ZERO, # This will place it in the default folder for the type
|
||||
name=wearable.name,
|
||||
type=wearable.wearable_type.asset_type,
|
||||
inv_type=wearable.wearable_type.asset_type.inventory_type,
|
||||
wearable_type=wearable.wearable_type,
|
||||
next_mask=Permissions.MOVE | Permissions.MODIFY | Permissions.COPY | Permissions.TRANSFER,
|
||||
transaction_id=transaction_id,
|
||||
)
|
||||
print(f"Created {item!r}")
|
||||
await region.xfer_manager.upload_asset(
|
||||
wearable.wearable_type.asset_type,
|
||||
wearable.to_str(),
|
||||
transaction_id=transaction_id,
|
||||
)
|
||||
|
||||
|
||||
addons = [ShapeCreatorAddon()]
|
||||
@@ -8,7 +8,7 @@ applied to the mesh before upload.
|
||||
I personally use manglers to strip bounding box materials you need
|
||||
to add to give a mesh an arbitrary center of rotation / scaling.
|
||||
"""
|
||||
|
||||
from hippolyzer.lib.base.helpers import reorient_coord
|
||||
from hippolyzer.lib.base.mesh import MeshAsset
|
||||
from hippolyzer.lib.proxy.addons import AddonManager
|
||||
|
||||
@@ -16,23 +16,8 @@ import local_mesh
|
||||
AddonManager.hot_reload(local_mesh, require_addons_loaded=True)
|
||||
|
||||
|
||||
def _reorient_coord(coord, orientation, normals=False):
|
||||
coords = []
|
||||
for axis in orientation:
|
||||
axis_idx = abs(axis) - 1
|
||||
if normals:
|
||||
# Normals have a static domain from -1.0 to 1.0, just negate.
|
||||
new_coord = coord[axis_idx] if axis >= 0 else -coord[axis_idx]
|
||||
else:
|
||||
new_coord = coord[axis_idx] if axis >= 0 else 1.0 - coord[axis_idx]
|
||||
coords.append(new_coord)
|
||||
if coord.__class__ in (list, tuple):
|
||||
return coord.__class__(coords)
|
||||
return coord.__class__(*coords)
|
||||
|
||||
|
||||
def _reorient_coord_list(coord_list, orientation, normals=False):
|
||||
return [_reorient_coord(x, orientation, normals) for x in coord_list]
|
||||
def _reorient_coord_list(coord_list, orientation, min_val: int | float = 0):
|
||||
return [reorient_coord(x, orientation, min_val) for x in coord_list]
|
||||
|
||||
|
||||
def reorient_mesh(orientation):
|
||||
@@ -47,7 +32,7 @@ def reorient_mesh(orientation):
|
||||
# flipping the axes around.
|
||||
material["Position"] = _reorient_coord_list(material["Position"], orientation)
|
||||
# Are you even supposed to do this to the normals?
|
||||
material["Normal"] = _reorient_coord_list(material["Normal"], orientation, normals=True)
|
||||
material["Normal"] = _reorient_coord_list(material["Normal"], orientation, min_val=-1)
|
||||
return mesh
|
||||
return _reorienter
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ Assorted utilities to make creating animations from scratch easier
|
||||
"""
|
||||
|
||||
import copy
|
||||
from typing import List, Union
|
||||
from typing import List, Union, Mapping
|
||||
|
||||
from hippolyzer.lib.base.datatypes import Vector3, Quaternion
|
||||
from hippolyzer.lib.base.llanim import PosKeyframe, RotKeyframe
|
||||
from hippolyzer.lib.base.llanim import PosKeyframe, RotKeyframe, JOINTS_DICT, Joint
|
||||
from hippolyzer.lib.base.mesh_skeleton import AVATAR_SKELETON
|
||||
from hippolyzer.lib.base.multidict import OrderedMultiDict
|
||||
|
||||
|
||||
def smooth_step(t: float):
|
||||
@@ -89,3 +91,35 @@ def smooth_rot(start: Quaternion, end: Quaternion, inter_frames: int, time: floa
|
||||
smooth_t = smooth_step(t)
|
||||
frames.append(RotKeyframe(time=time + (t * duration), rot=rot_interp(start, end, smooth_t)))
|
||||
return frames + [RotKeyframe(time=time + duration, rot=end)]
|
||||
|
||||
|
||||
def mirror_joints(joints_dict: Mapping[str, Joint]) -> JOINTS_DICT:
|
||||
"""Mirror a joints dict so left / right are swapped, including transformations"""
|
||||
new_joints: JOINTS_DICT = OrderedMultiDict()
|
||||
|
||||
for joint_name, joint in joints_dict.items():
|
||||
inverse_joint_node = AVATAR_SKELETON[joint_name].inverse
|
||||
if not inverse_joint_node:
|
||||
new_joints[joint_name] = joint
|
||||
continue
|
||||
|
||||
# Okay, this is one we have to actually mirror
|
||||
new_joint = Joint(joint.priority, [], [])
|
||||
|
||||
for rot_keyframe in joint.rot_keyframes:
|
||||
new_joint.rot_keyframes.append(RotKeyframe(
|
||||
time=rot_keyframe.time,
|
||||
# Just need to mirror on yaw and roll
|
||||
rot=Quaternion.from_euler(*(rot_keyframe.rot.to_euler() * Vector3(-1, 1, -1)))
|
||||
))
|
||||
|
||||
for pos_keyframe in joint.pos_keyframes:
|
||||
new_joint.pos_keyframes.append(PosKeyframe(
|
||||
time=pos_keyframe.time,
|
||||
# Y is left / right so just negate it.
|
||||
pos=pos_keyframe.pos * Vector3(1, -1, 1)
|
||||
))
|
||||
|
||||
new_joints[inverse_joint_node.name] = new_joint
|
||||
|
||||
return new_joints
|
||||
|
||||
@@ -429,8 +429,8 @@ class GLTFBuilder:
|
||||
|
||||
# Add each joint to the child list of their respective parent
|
||||
for joint_name, joint_ctx in built_joints.items():
|
||||
if parent := AVATAR_SKELETON[joint_name].parent:
|
||||
built_joints[parent().name].node.children.append(self.model.nodes.index(joint_ctx.node))
|
||||
if parent_name := AVATAR_SKELETON[joint_name].parent_name:
|
||||
built_joints[parent_name].node.children.append(self.model.nodes.index(joint_ctx.node))
|
||||
return built_joints
|
||||
|
||||
def _fix_blender_joint(self, joint_matrix: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
||||
|
||||
@@ -195,3 +195,21 @@ def create_logged_task(
|
||||
task = asyncio.create_task(coro, name=name)
|
||||
add_future_logger(task, name, logger)
|
||||
return task
|
||||
|
||||
|
||||
def reorient_coord(coord, new_orientation, min_val: int | float = 0):
|
||||
"""
|
||||
Reorient a coordinate instance such that its components are negated and transposed appropriately.
|
||||
|
||||
For ex:
|
||||
reorient_coord((1,2,3), (3,-2,-1)) == (3,-2,-1)
|
||||
"""
|
||||
min_val = abs(min_val)
|
||||
coords = []
|
||||
for axis in new_orientation:
|
||||
axis_idx = abs(axis) - 1
|
||||
new_coord = coord[axis_idx] if axis >= 0 else min_val - coord[axis_idx]
|
||||
coords.append(new_coord)
|
||||
if coord.__class__ in (list, tuple):
|
||||
return coord.__class__(coords)
|
||||
return coord.__class__(*coords)
|
||||
|
||||
@@ -15,6 +15,8 @@ CONSTRAINT_DATACLASS = se.ForwardSerializable(lambda: se.Dataclass(Constraint))
|
||||
POSKEYFRAME_DATACLASS = se.ForwardSerializable(lambda: se.Dataclass(PosKeyframe))
|
||||
ROTKEYFRAME_DATACLASS = se.ForwardSerializable(lambda: se.Dataclass(RotKeyframe))
|
||||
|
||||
JOINTS_DICT = OrderedMultiDict[str, "Joint"]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Animation:
|
||||
@@ -29,7 +31,7 @@ class Animation:
|
||||
ease_in_duration: float = se.dataclass_field(se.F32)
|
||||
ease_out_duration: float = se.dataclass_field(se.F32)
|
||||
hand_pose: HandPose = se.dataclass_field(lambda: se.IntEnum(HandPose, se.U32), default=0)
|
||||
joints: OrderedMultiDict[str, Joint] = se.dataclass_field(se.MultiDictAdapter(
|
||||
joints: JOINTS_DICT = se.dataclass_field(se.MultiDictAdapter(
|
||||
se.Collection(se.U32, se.Tuple(se.CStr(), JOINT_DATACLASS)),
|
||||
))
|
||||
constraints: List[Constraint] = se.dataclass_field(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
import re
|
||||
import weakref
|
||||
from typing import *
|
||||
|
||||
@@ -9,22 +11,23 @@ from lxml import etree
|
||||
|
||||
from hippolyzer.lib.base.datatypes import Vector3, RAD_TO_DEG
|
||||
from hippolyzer.lib.base.helpers import get_resource_filename
|
||||
from hippolyzer.lib.base.mesh import MeshAsset, SkinSegmentDict, llsd_to_mat4
|
||||
|
||||
|
||||
MAYBE_JOINT_REF = Optional[Callable[[], "JointNode"]]
|
||||
MAYBE_JOINT_REF = Optional[str]
|
||||
SKELETON_REF = Optional[Callable[[], "Skeleton"]]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class JointNode:
|
||||
name: str
|
||||
parent: MAYBE_JOINT_REF
|
||||
parent_name: MAYBE_JOINT_REF
|
||||
skeleton: SKELETON_REF
|
||||
translation: Vector3
|
||||
pivot: Vector3 # pivot point for the joint, generally the same as translation
|
||||
rotation: Vector3 # Euler rotation in degrees
|
||||
scale: Vector3
|
||||
type: str # bone or collision_volume
|
||||
support: str
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.type))
|
||||
@@ -37,6 +40,12 @@ class JointNode:
|
||||
translate=tuple(self.translation),
|
||||
)
|
||||
|
||||
@property
|
||||
def parent(self) -> Optional[JointNode]:
|
||||
if self.parent_name:
|
||||
return self.skeleton()[self.parent_name]
|
||||
return None
|
||||
|
||||
@property
|
||||
def index(self) -> int:
|
||||
bone_idx = 0
|
||||
@@ -51,57 +60,109 @@ class JointNode:
|
||||
@property
|
||||
def ancestors(self) -> Sequence[JointNode]:
|
||||
joint_node = self
|
||||
ancestors = []
|
||||
while joint_node.parent:
|
||||
joint_node = joint_node.parent()
|
||||
skeleton = self.skeleton()
|
||||
ancestors: List[JointNode] = []
|
||||
while joint_node.parent_name:
|
||||
joint_node = skeleton.joint_dict.get(joint_node.parent_name)
|
||||
ancestors.append(joint_node)
|
||||
return ancestors
|
||||
|
||||
@property
|
||||
def children(self) -> Sequence[JointNode]:
|
||||
children = []
|
||||
children: List[JointNode] = []
|
||||
for node in self.skeleton().joint_dict.values():
|
||||
if node.parent and node.parent() == self:
|
||||
if node.parent_name and node.parent_name == self.name:
|
||||
children.append(node)
|
||||
return children
|
||||
|
||||
@property
|
||||
def inverse(self) -> Optional[JointNode]:
|
||||
l_re = re.compile(r"(.*?(?:_|\b))L((?:_|\b).*)")
|
||||
r_re = re.compile(r"(.*?(?:_|\b))R((?:_|\b).*)")
|
||||
|
||||
inverse_name = None
|
||||
if "Left" in self.name:
|
||||
inverse_name = self.name.replace("Left", "Right")
|
||||
elif "LEFT" in self.name:
|
||||
inverse_name = self.name.replace("LEFT", "RIGHT")
|
||||
elif l_re.match(self.name):
|
||||
inverse_name = re.sub(l_re, r"\1R\2", self.name)
|
||||
elif "Right" in self.name:
|
||||
inverse_name = self.name.replace("Right", "Left")
|
||||
elif "RIGHT" in self.name:
|
||||
inverse_name = self.name.replace("RIGHT", "LEFT")
|
||||
elif r_re.match(self.name):
|
||||
inverse_name = re.sub(r_re, r"\1L\2", self.name)
|
||||
|
||||
if inverse_name:
|
||||
return self.skeleton().joint_dict.get(inverse_name)
|
||||
return None
|
||||
|
||||
@property
|
||||
def descendents(self) -> Set[JointNode]:
|
||||
descendents = set()
|
||||
ancestors = {self}
|
||||
last_ancestors = set()
|
||||
descendents: Set[JointNode] = set()
|
||||
ancestors: Set[str] = {self.name}
|
||||
last_ancestors: Set[str] = set()
|
||||
while last_ancestors != ancestors:
|
||||
last_ancestors = ancestors
|
||||
last_ancestors = ancestors.copy()
|
||||
for node in self.skeleton().joint_dict.values():
|
||||
if node.parent and node.parent() in ancestors:
|
||||
ancestors.add(node)
|
||||
if node.parent_name and node.parent_name in ancestors:
|
||||
ancestors.add(node.name)
|
||||
descendents.add(node)
|
||||
return descendents
|
||||
|
||||
|
||||
class Skeleton:
|
||||
def __init__(self, root_node: etree.ElementBase):
|
||||
def __init__(self, root_node: Optional[etree.ElementBase] = None):
|
||||
self.joint_dict: Dict[str, JointNode] = {}
|
||||
self._parse_node_children(root_node, None)
|
||||
if root_node is not None:
|
||||
self._parse_node_children(root_node, None)
|
||||
|
||||
def __getitem__(self, item: str) -> JointNode:
|
||||
return self.joint_dict[item]
|
||||
|
||||
def _parse_node_children(self, node: etree.ElementBase, parent: MAYBE_JOINT_REF):
|
||||
def clone(self) -> Self:
|
||||
val = copy.deepcopy(self)
|
||||
skel_ref = weakref.ref(val)
|
||||
for joint in val.joint_dict.values():
|
||||
joint.skeleton = skel_ref
|
||||
return val
|
||||
|
||||
def _parse_node_children(self, node: etree.ElementBase, parent_name: MAYBE_JOINT_REF):
|
||||
name = node.get('name')
|
||||
joint = JointNode(
|
||||
name=name,
|
||||
parent=parent,
|
||||
parent_name=parent_name,
|
||||
skeleton=weakref.ref(self),
|
||||
translation=_get_vec_attr(node, "pos", Vector3()),
|
||||
pivot=_get_vec_attr(node, "pivot", Vector3()),
|
||||
rotation=_get_vec_attr(node, "rot", Vector3()),
|
||||
scale=_get_vec_attr(node, "scale", Vector3(1, 1, 1)),
|
||||
support=node.get('support', 'base'),
|
||||
type=node.tag,
|
||||
)
|
||||
self.joint_dict[name] = joint
|
||||
for child in node.iterchildren():
|
||||
self._parse_node_children(child, weakref.ref(joint))
|
||||
self._parse_node_children(child, joint.name)
|
||||
|
||||
def merge_mesh_skeleton(self, mesh: MeshAsset) -> None:
|
||||
"""Update this skeleton with a skeleton definition from a mesh asset"""
|
||||
skin_seg: Optional[SkinSegmentDict] = mesh.segments.get('skin')
|
||||
if not skin_seg:
|
||||
return
|
||||
|
||||
for joint_name, matrix in zip(skin_seg['joint_names'], skin_seg.get('alt_inverse_bind_matrix', [])):
|
||||
# We're only meant to use the translation component from the alt inverse bind matrix.
|
||||
joint_decomp = transformations.decompose_matrix(llsd_to_mat4(matrix))
|
||||
joint_node = self.joint_dict.get(joint_name)
|
||||
if not joint_node:
|
||||
continue
|
||||
joint_node.translation = Vector3(*joint_decomp[3])
|
||||
|
||||
if pelvis_offset := skin_seg.get('pelvis_offset'):
|
||||
# TODO: Should we even do this?
|
||||
pelvis_node = self["mPelvis"]
|
||||
pelvis_node.translation += Vector3(0, 0, pelvis_offset)
|
||||
|
||||
|
||||
def _get_vec_attr(node, attr_name: str, default: Vector3) -> Vector3:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,7 +42,7 @@ class Object(recordclass.RecordClass, use_weakref=True): # type: ignore
|
||||
CRC: Optional[int] = None
|
||||
PCode: Optional[tmpls.PCode] = None
|
||||
Material: Optional[tmpls.MCode] = None
|
||||
ClickAction: Optional[int] = None
|
||||
ClickAction: Optional[tmpls.ClickAction] = None
|
||||
Scale: Optional[Vector3] = None
|
||||
ParentID: Optional[int] = None
|
||||
# Actually contains a weakref proxy
|
||||
@@ -243,6 +243,7 @@ def normalize_object_update(block: Block, handle: int):
|
||||
"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 {},
|
||||
"ClickAction": block.deserialize_var("ClickAction", make_copy=False),
|
||||
"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),
|
||||
@@ -435,8 +436,8 @@ class FastObjectUpdateCompressedDataDeserializer:
|
||||
"PCode": pcode,
|
||||
"State": state,
|
||||
"CRC": crc,
|
||||
"Material": material,
|
||||
"ClickAction": click_action,
|
||||
"Material": tmpls.MCode(material),
|
||||
"ClickAction": tmpls.ClickAction(click_action),
|
||||
"Scale": scale,
|
||||
"Position": pos,
|
||||
"Rotation": rot,
|
||||
|
||||
@@ -980,6 +980,7 @@ class MCode(IntEnum):
|
||||
# What's in the high nybble, anything?
|
||||
STONE = 0
|
||||
METAL = 1
|
||||
GLASS = 2
|
||||
WOOD = 3
|
||||
FLESH = 4
|
||||
PLASTIC = 5
|
||||
@@ -1688,6 +1689,24 @@ class SoundFlags(IntFlag):
|
||||
STOP = 1 << 5
|
||||
|
||||
|
||||
@se.enum_field_serializer("ObjectClickAction", "ObjectData", "ClickAction")
|
||||
@se.enum_field_serializer("ObjectUpdate", "ObjectData", "ClickAction")
|
||||
class ClickAction(IntEnum):
|
||||
# "NONE" is also used as an alias for "TOUCH"
|
||||
TOUCH = 0
|
||||
SIT = 1
|
||||
BUY = 2
|
||||
PAY = 3
|
||||
OPEN = 4
|
||||
PLAY = 5
|
||||
OPEN_MEDIA = 6
|
||||
ZOOM = 7
|
||||
DISABLED = 8
|
||||
IGNORE = 9
|
||||
# I've seen this in practice, not clear what it is.
|
||||
UNKNOWN = 255
|
||||
|
||||
|
||||
class CompressedFlags(IntFlag):
|
||||
SCRATCHPAD = 1
|
||||
TREE = 1 << 1
|
||||
@@ -1722,7 +1741,7 @@ class ObjectUpdateCompressedDataSerializer(se.SimpleSubfieldSerializer):
|
||||
"State": ObjectStateAdapter(se.U8),
|
||||
"CRC": se.U32,
|
||||
"Material": se.IntEnum(MCode, se.U8),
|
||||
"ClickAction": se.U8,
|
||||
"ClickAction": se.IntEnum(ClickAction, se.U8),
|
||||
"Scale": se.Vector3,
|
||||
"Position": se.Vector3,
|
||||
"Rotation": se.PackedQuat(se.Vector3),
|
||||
|
||||
@@ -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,17 +667,43 @@ 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}")
|
||||
return
|
||||
|
||||
obj.Animations.clear()
|
||||
for block in message["AnimationList"]:
|
||||
for block in message.blocks.get("AnimationList", []):
|
||||
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"]
|
||||
|
||||
@@ -4,6 +4,7 @@ import unittest
|
||||
|
||||
from hippolyzer.lib.base.datatypes import *
|
||||
from hippolyzer.lib.base.inventory import InventoryModel, SaleType, InventoryItem
|
||||
from hippolyzer.lib.base.message.message import Block, Message
|
||||
from hippolyzer.lib.base.wearables import Wearable, VISUAL_PARAMS
|
||||
|
||||
SIMPLE_INV = """\tinv_object\t0
|
||||
@@ -323,6 +324,270 @@ parameters 82
|
||||
textures 0
|
||||
"""
|
||||
|
||||
# TODO: Move appearance-related stuff elsewhere.
|
||||
|
||||
GIRL_NEXT_DOOR_APPEARANCE_MSG = Message(
|
||||
'AvatarAppearance',
|
||||
Block('Sender', ID=UUID(int=1), IsTrial=0),
|
||||
# We don't care about the value of this.
|
||||
Block('ObjectData', TextureEntry=b""),
|
||||
Block('VisualParam', ParamValue=9),
|
||||
Block('VisualParam', ParamValue=30),
|
||||
Block('VisualParam', ParamValue=71),
|
||||
Block('VisualParam', ParamValue=32),
|
||||
Block('VisualParam', ParamValue=51),
|
||||
Block('VisualParam', ParamValue=132),
|
||||
Block('VisualParam', ParamValue=10),
|
||||
Block('VisualParam', ParamValue=76),
|
||||
Block('VisualParam', ParamValue=84),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=43),
|
||||
Block('VisualParam', ParamValue=83),
|
||||
Block('VisualParam', ParamValue=113),
|
||||
Block('VisualParam', ParamValue=68),
|
||||
Block('VisualParam', ParamValue=73),
|
||||
Block('VisualParam', ParamValue=43),
|
||||
Block('VisualParam', ParamValue=35),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=7),
|
||||
Block('VisualParam', ParamValue=132),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=76),
|
||||
Block('VisualParam', ParamValue=91),
|
||||
Block('VisualParam', ParamValue=129),
|
||||
Block('VisualParam', ParamValue=106),
|
||||
Block('VisualParam', ParamValue=76),
|
||||
Block('VisualParam', ParamValue=58),
|
||||
Block('VisualParam', ParamValue=99),
|
||||
Block('VisualParam', ParamValue=73),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=203),
|
||||
Block('VisualParam', ParamValue=48),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=150),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=114),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=76),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=40),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=140),
|
||||
Block('VisualParam', ParamValue=86),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=99),
|
||||
Block('VisualParam', ParamValue=84),
|
||||
Block('VisualParam', ParamValue=53),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=66),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=100),
|
||||
Block('VisualParam', ParamValue=216),
|
||||
Block('VisualParam', ParamValue=214),
|
||||
Block('VisualParam', ParamValue=204),
|
||||
Block('VisualParam', ParamValue=204),
|
||||
Block('VisualParam', ParamValue=204),
|
||||
Block('VisualParam', ParamValue=51),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=89),
|
||||
Block('VisualParam', ParamValue=109),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=61),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=115),
|
||||
Block('VisualParam', ParamValue=76),
|
||||
Block('VisualParam', ParamValue=91),
|
||||
Block('VisualParam', ParamValue=158),
|
||||
Block('VisualParam', ParamValue=102),
|
||||
Block('VisualParam', ParamValue=109),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=193),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=132),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=68),
|
||||
Block('VisualParam', ParamValue=35),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=97),
|
||||
Block('VisualParam', ParamValue=92),
|
||||
Block('VisualParam', ParamValue=79),
|
||||
Block('VisualParam', ParamValue=107),
|
||||
Block('VisualParam', ParamValue=160),
|
||||
Block('VisualParam', ParamValue=112),
|
||||
Block('VisualParam', ParamValue=63),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=159),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=73),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=102),
|
||||
Block('VisualParam', ParamValue=158),
|
||||
Block('VisualParam', ParamValue=145),
|
||||
Block('VisualParam', ParamValue=153),
|
||||
Block('VisualParam', ParamValue=163),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=122),
|
||||
Block('VisualParam', ParamValue=43),
|
||||
Block('VisualParam', ParamValue=94),
|
||||
Block('VisualParam', ParamValue=135),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=214),
|
||||
Block('VisualParam', ParamValue=204),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=56),
|
||||
Block('VisualParam', ParamValue=30),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=204),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=112),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=25),
|
||||
Block('VisualParam', ParamValue=100),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=84),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=51),
|
||||
Block('VisualParam', ParamValue=94),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=255),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=25),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=25),
|
||||
Block('VisualParam', ParamValue=23),
|
||||
Block('VisualParam', ParamValue=51),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=25),
|
||||
Block('VisualParam', ParamValue=23),
|
||||
Block('VisualParam', ParamValue=51),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=25),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=25),
|
||||
Block('VisualParam', ParamValue=23),
|
||||
Block('VisualParam', ParamValue=51),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=25),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=25),
|
||||
Block('VisualParam', ParamValue=23),
|
||||
Block('VisualParam', ParamValue=51),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=25),
|
||||
Block('VisualParam', ParamValue=23),
|
||||
Block('VisualParam', ParamValue=51),
|
||||
Block('VisualParam', ParamValue=0),
|
||||
Block('VisualParam', ParamValue=25),
|
||||
Block('VisualParam', ParamValue=23),
|
||||
Block('VisualParam', ParamValue=51),
|
||||
Block('VisualParam', ParamValue=1),
|
||||
Block('VisualParam', ParamValue=127),
|
||||
Block('AppearanceData', AppearanceVersion=1, CofVersion=100, Flags=0),
|
||||
Block('AppearanceHover', HoverHeight=Vector3(0.0, 0.0, 0.0))
|
||||
)
|
||||
|
||||
|
||||
class TestWearable(unittest.TestCase):
|
||||
def test_parse(self):
|
||||
@@ -338,3 +603,17 @@ class TestWearable(unittest.TestCase):
|
||||
def test_visual_params(self):
|
||||
param = VISUAL_PARAMS.by_name("Eyelid_Inner_Corner_Up")
|
||||
self.assertEqual(param.value_max, 1.2)
|
||||
|
||||
def test_message_equivalent(self):
|
||||
wearable = Wearable.from_str(GIRL_NEXT_DOOR_SHAPE)
|
||||
parsed = VISUAL_PARAMS.parse_appearance_message(GIRL_NEXT_DOOR_APPEARANCE_MSG)
|
||||
|
||||
for i, (param_id, param_val) in enumerate(parsed.items()):
|
||||
param = VISUAL_PARAMS.by_id(param_id)
|
||||
if param.wearable != "shape":
|
||||
continue
|
||||
# A parameter may legitimately be missing from the shape depending on its age,
|
||||
# just assume it's the default value.
|
||||
expected_val = wearable.parameters.get(param_id, param.value_default)
|
||||
# This seems like quite a large delta. Maybe we should be using different quantization here.
|
||||
self.assertAlmostEqual(expected_val, param_val, delta=0.015)
|
||||
|
||||
@@ -20,7 +20,7 @@ class TestSkeleton(unittest.TestCase):
|
||||
self.assertEqual(113, self.skeleton["mKneeLeft"].index)
|
||||
|
||||
def test_get_joint_parent(self):
|
||||
self.assertEqual("mChest", self.skeleton["mNeck"].parent().name)
|
||||
self.assertEqual("mChest", self.skeleton["mNeck"].parent.name)
|
||||
|
||||
def test_get_joint_matrix(self):
|
||||
expected_mat = np.array([
|
||||
@@ -30,3 +30,9 @@ class TestSkeleton(unittest.TestCase):
|
||||
[0., 0., 0., 1.]
|
||||
])
|
||||
np.testing.assert_equal(expected_mat, self.skeleton["mNeck"].matrix)
|
||||
|
||||
def test_get_inverse_joint(self):
|
||||
self.assertEqual("R_CLAVICLE", self.skeleton["L_CLAVICLE"].inverse.name)
|
||||
self.assertEqual(None, self.skeleton["mChest"].inverse)
|
||||
self.assertEqual("mHandMiddle1Right", self.skeleton["mHandMiddle1Left"].inverse.name)
|
||||
self.assertEqual("RIGHT_HANDLE", self.skeleton["LEFT_HANDLE"].inverse.name)
|
||||
|
||||
61
tests/client/test_inventory_manager.py
Normal file
61
tests/client/test_inventory_manager.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import unittest
|
||||
|
||||
from hippolyzer.lib.base.datatypes import UUID
|
||||
from hippolyzer.lib.client.inventory_manager import InventoryManager
|
||||
from tests.client import MockClientRegion
|
||||
|
||||
CREATE_FOLDER_PAYLOAD = {
|
||||
'_base_uri': 'slcap://InventoryAPIv3',
|
||||
'_created_categories': [
|
||||
UUID(int=2),
|
||||
],
|
||||
'_created_items': [],
|
||||
'_embedded': {
|
||||
'categories': {
|
||||
f'{UUID(int=2)}': {
|
||||
'_embedded': {'categories': {}, 'items': {}, 'links': {}},
|
||||
'_links': {
|
||||
'parent': {'href': f'/category/{UUID(int=1)}'},
|
||||
'self': {'href': f'/category/{UUID(int=2)}'}
|
||||
},
|
||||
'agent_id': f'{UUID(int=9)}',
|
||||
'category_id': f'{UUID(int=2)}',
|
||||
'name': 'New Folder',
|
||||
'parent_id': f'{UUID(int=1)}',
|
||||
'type_default': -1,
|
||||
'version': 1
|
||||
}
|
||||
},
|
||||
'items': {}, 'links': {}
|
||||
},
|
||||
'_links': {
|
||||
'categories': {'href': f'/category/{UUID(int=1)}/categories'},
|
||||
'category': {'href': f'/category/{UUID(int=1)}', 'name': 'self'},
|
||||
'children': {'href': f'/category/{UUID(int=1)}/children'},
|
||||
'items': {'href': f'/category/{UUID(int=1)}/items'},
|
||||
'links': {'href': f'/category/{UUID(int=1)}/links'},
|
||||
'parent': {'href': '/category/00000000-0000-0000-0000-000000000000'},
|
||||
'self': {'href': f'/category/{UUID(int=1)}/children'}
|
||||
},
|
||||
'_updated_category_versions': {str(UUID(int=1)): 27},
|
||||
'agent_id': UUID(int=9),
|
||||
'category_id': UUID(int=1),
|
||||
'name': 'My Inventory',
|
||||
'parent_id': UUID.ZERO,
|
||||
'type_default': 8,
|
||||
'version': 27,
|
||||
}
|
||||
|
||||
|
||||
class TestParcelOverlay(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.region = MockClientRegion()
|
||||
self.session = self.region.session()
|
||||
self.inv_manager = InventoryManager(self.session)
|
||||
self.model = self.inv_manager.model
|
||||
self.handler = self.region.message_handler
|
||||
|
||||
def test_create_folder_response(self):
|
||||
self.inv_manager.process_aisv3_response(CREATE_FOLDER_PAYLOAD)
|
||||
self.assertIsNotNone(self.model.get_category(UUID(int=1)))
|
||||
self.assertIsNotNone(self.model.get_category(UUID(int=2)))
|
||||
Reference in New Issue
Block a user