Files
Hippolyzer/hippolyzer/lib/base/templates.py
2025-06-18 20:44:11 +00:00

2437 lines
81 KiB
Python

"""
Serialization templates for structures used in LLUDP and HTTP bodies.
"""
import abc
import collections
import copy
import dataclasses
import datetime
import enum
import math
import zlib
from typing import *
import numpy as np
import hippolyzer.lib.base.serialization as se
from hippolyzer.lib.base import llsd
from hippolyzer.lib.base.datatypes import UUID, IntEnum, IntFlag, Vector3
from hippolyzer.lib.base.helpers import BiDiDict
from hippolyzer.lib.base.namevalue import NameValuesSerializer
from hippolyzer.lib.base.serialization import ParseContext
class LookupIntEnum(IntEnum):
"""
Used for enums that have legacy string names, may be used in the legacy schema
Generally this is the string returned by `LLWhateverType::lookup()` in indra
"""
@abc.abstractmethod
def to_lookup_name(self) -> str:
raise NotImplementedError()
@classmethod
def from_lookup_name(cls, legacy_name: str):
raise NotImplementedError()
_ASSET_TYPE_BIDI: BiDiDict[str] = BiDiDict({
"animation": "animatn",
"callingcard": "callcard",
"lsl_text": "lsltext",
"lsl_bytecode": "lslbyte",
"texture_tga": "txtr_tga",
"image_tga": "img_tga",
"image_jpeg": "jpg",
"sound_wav": "snd_wav",
"folder_link": "link_f",
"unknown": "invalid",
"none": "-1",
})
@se.enum_field_serializer("RequestXfer", "XferID", "VFileType")
@se.enum_field_serializer("AssetUploadRequest", "AssetBlock", "Type")
@se.enum_field_serializer("AssetUploadComplete", "AssetBlock", "Type")
@se.enum_field_serializer("UpdateCreateInventoryItem", "InventoryData", "Type")
@se.enum_field_serializer("CreateInventoryItem", "InventoryBlock", "Type")
@se.enum_field_serializer("RezObject", "InventoryData", "Type")
@se.enum_field_serializer("RezScript", "InventoryBlock", "Type")
@se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "Type")
@se.enum_field_serializer("BulkUpdateInventory", "ItemData", "Type")
class AssetType(LookupIntEnum):
TEXTURE = 0
SOUND = 1
CALLINGCARD = 2
LANDMARK = 3
SCRIPT = 4
CLOTHING = 5
OBJECT = 6
NOTECARD = 7
CATEGORY = 8
LSL_TEXT = 10
LSL_BYTECODE = 11
TEXTURE_TGA = 12
BODYPART = 13
SOUND_WAV = 17
IMAGE_TGA = 18
IMAGE_JPEG = 19
ANIMATION = 20
GESTURE = 21
SIMSTATE = 22
LINK = 24
FOLDER_LINK = 25
MARKETPLACE_FOLDER = 26
WIDGET = 40
PERSON = 45
MESH = 49
RESERVED_1 = 50
RESERVED_2 = 51
RESERVED_3 = 52
RESERVED_4 = 53
RESERVED_5 = 54
RESERVED_6 = 55
SETTINGS = 56
MATERIAL = 57
UNKNOWN = 255
NONE = -1
def to_lookup_name(self) -> str:
lower = self.name.lower()
return _ASSET_TYPE_BIDI.forward.get(lower, lower)
@classmethod
def from_lookup_name(cls, legacy_name: str):
reg_name = _ASSET_TYPE_BIDI.backward.get(legacy_name, legacy_name).upper()
return cls[reg_name]
@property
def inventory_type(self):
return {
AssetType.TEXTURE: InventoryType.TEXTURE,
AssetType.SOUND: InventoryType.SOUND,
AssetType.CALLINGCARD: InventoryType.CALLINGCARD,
AssetType.LANDMARK: InventoryType.LANDMARK,
AssetType.SCRIPT: InventoryType.LSL,
AssetType.CLOTHING: InventoryType.WEARABLE,
AssetType.OBJECT: InventoryType.OBJECT,
AssetType.NOTECARD: InventoryType.NOTECARD,
AssetType.CATEGORY: InventoryType.CATEGORY,
AssetType.LSL_TEXT: InventoryType.LSL,
AssetType.LSL_BYTECODE: InventoryType.LSL,
AssetType.TEXTURE_TGA: InventoryType.TEXTURE,
AssetType.BODYPART: InventoryType.WEARABLE,
AssetType.SOUND_WAV: InventoryType.SOUND,
AssetType.ANIMATION: InventoryType.ANIMATION,
AssetType.GESTURE: InventoryType.GESTURE,
AssetType.WIDGET: InventoryType.WIDGET,
AssetType.PERSON: InventoryType.PERSON,
AssetType.MESH: InventoryType.MESH,
AssetType.SETTINGS: InventoryType.SETTINGS,
AssetType.MATERIAL: InventoryType.MATERIAL,
}.get(self, AssetType.NONE)
_INV_TYPE_BIDI: BiDiDict[str] = BiDiDict({
"callingcard": "callcard",
"attachment": "attach",
"none": "-1",
})
@se.enum_field_serializer("UpdateCreateInventoryItem", "InventoryData", "InvType")
@se.enum_field_serializer("CreateInventoryItem", "InventoryBlock", "InvType")
@se.enum_field_serializer("RezObject", "InventoryData", "InvType")
@se.enum_field_serializer("RezScript", "InventoryBlock", "InvType")
@se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "InvType")
@se.enum_field_serializer("BulkUpdateInventory", "ItemData", "InvType")
@se.enum_field_serializer("BulkUpdateInventory", "FolderData", "Type")
class InventoryType(LookupIntEnum):
TEXTURE = 0
SOUND = 1
CALLINGCARD = 2
LANDMARK = 3
SCRIPT = 4
CLOTHING = 5
OBJECT = 6
NOTECARD = 7
CATEGORY = 8
ROOT_CATEGORY = 9
LSL = 10
LSL_BYTECODE = 11
TEXTURE_TGA = 12
BODYPART = 13
TRASH = 14
SNAPSHOT = 15
LOST_AND_FOUND = 16
ATTACHMENT = 17
WEARABLE = 18
ANIMATION = 19
GESTURE = 20
MESH = 22
WIDGET = 23
PERSON = 24
SETTINGS = 25
MATERIAL = 26
UNKNOWN = 255
NONE = -1
def to_lookup_name(self) -> str:
lower = self.name.lower()
return _INV_TYPE_BIDI.forward.get(lower, lower)
@classmethod
def from_lookup_name(cls, legacy_name: str):
reg_name = _INV_TYPE_BIDI.backward.get(legacy_name, legacy_name).upper()
return cls[reg_name]
_FOLDER_TYPE_BIDI: BiDiDict[str] = BiDiDict({
"callingcard": "callcard",
"lsl_text": "lsltext",
"animation": "animatn",
"snapshot_category": "snapshot",
"lost_and_found": "lstndfnd",
"ensemble_start": "ensemble",
"ensemble_end": "ensemble",
"current_outfit": "current",
"my_outfits": "my_otfts",
"basic_root": "basic_rt",
"marketplace_listings": "merchant",
"marketplace_stock": "stock",
"marketplace_version": "version",
"my_suitcase": "suitcase",
"root_inventory": "root_inv",
"none": "-1",
})
class FolderType(LookupIntEnum):
TEXTURE = 0
SOUND = 1
CALLINGCARD = 2
LANDMARK = 3
CLOTHING = 5
OBJECT = 6
NOTECARD = 7
# We'd really like to change this to 9 since AT_CATEGORY is 8,
# but "My Inventory" has been type 8 for a long time.
ROOT_INVENTORY = 8
LSL_TEXT = 10
BODYPART = 13
TRASH = 14
SNAPSHOT_CATEGORY = 15
LOST_AND_FOUND = 16
ANIMATION = 20
GESTURE = 21
FAVORITE = 23
# The "ensemble" values aren't used, no idea what they were for.
ENSEMBLE_START = 26
ENSEMBLE_END = 45
# This range is reserved for special clothing folder types.
CURRENT_OUTFIT = 46
OUTFIT = 47
MY_OUTFITS = 48
MESH = 49
# "received items" for MP
INBOX = 50
OUTBOX = 51
BASIC_ROOT = 52
MARKETPLACE_LISTINGS = 53
MARKETPLACE_STOCK = 54
# Note: We actually *never* create folders with that type. This is used for icon override only.
MARKETPLACE_VERSION = 55
SETTINGS = 56
MATERIAL = 57
# Firestorm folders, may not actually exist in legacy schema
FIRESTORM = 58
PHOENIX = 59
RLV = 60
# Opensim folders
MY_SUITCASE = 100
NONE = -1
def to_lookup_name(self) -> str:
lower = self.name.lower()
return _FOLDER_TYPE_BIDI.forward.get(lower, lower)
@classmethod
def from_lookup_name(cls, legacy_name: str):
reg_name = _FOLDER_TYPE_BIDI.backward.get(legacy_name, legacy_name).upper()
return cls[reg_name]
@se.enum_field_serializer("AgentIsNowWearing", "WearableData", "WearableType")
@se.enum_field_serializer("AgentWearablesUpdate", "WearableData", "WearableType")
@se.enum_field_serializer("CreateInventoryItem", "InventoryBlock", "WearableType")
class WearableType(IntEnum):
SHAPE = 0
SKIN = 1
HAIR = 2
EYES = 3
SHIRT = 4
PANTS = 5
SHOES = 6
SOCKS = 7
JACKET = 8
GLOVES = 9
UNDERSHIRT = 10
UNDERPANTS = 11
SKIRT = 12
ALPHA = 13
TATTOO = 14
PHYSICS = 15
UNIVERSAL = 16
@property
def asset_type(self) -> AssetType:
if self in (WearableType.HAIR, WearableType.SKIN, WearableType.EYES, WearableType.SHAPE):
return AssetType.BODYPART
return AssetType.CLOTHING
def _register_permissions_flags(message_name, block_name):
def _wrapper(flag_cls):
for flag_type in {"EveryoneMask", "BaseMask", "OwnerMask", "GroupMask", "NextOwnerMask"}:
se.flag_field_serializer(message_name, block_name, flag_type)(flag_cls)
return flag_cls
return _wrapper
@se.flag_field_serializer("ObjectPermissions", "ObjectData", "Mask")
@_register_permissions_flags("ObjectProperties", "ObjectData")
@_register_permissions_flags("ObjectPropertiesFamily", "ObjectData")
@_register_permissions_flags("UpdateCreateInventoryItem", "InventoryData")
@_register_permissions_flags("UpdateTaskInventory", "InventoryData")
@_register_permissions_flags("CreateInventoryItem", "InventoryBlock")
@_register_permissions_flags("RezObject", "RezData")
@_register_permissions_flags("RezObject", "InventoryData")
@_register_permissions_flags("RezScript", "InventoryBlock")
@_register_permissions_flags("RezMultipleAttachmentsFromInv", "ObjectData")
@_register_permissions_flags("BulkUpdateInventory", "ItemData")
class Permissions(IntFlag):
TRANSFER = (1 << 13)
MODIFY = (1 << 14)
COPY = (1 << 15)
# OpenSim export permission, per Firestorm. Deprecated parcel entry flag
EXPORT_OR_PARCEL_ENTER = (1 << 16)
# parcels, allow terraform, deprecated
TERRAFORM = (1 << 17)
# deprecated
OWNER_DEBIT = (1 << 18)
# objects, can grab/translate/rotate
MOVE = (1 << 19)
# parcels, avatars take damage, deprecated
DAMAGE = (1 << 20)
RESERVED = 1 << 31
_SALE_TYPE_LEGACY_NAMES = ("not", "orig", "copy", "cntn")
@se.enum_field_serializer("ObjectSaleInfo", "ObjectData", "SaleType")
@se.enum_field_serializer("BulkUpdateInventory", "ItemData", "SaleType")
@se.enum_field_serializer("ObjectProperties", "ObjectData", "SaleType")
@se.enum_field_serializer("ObjectPropertiesFamily", "ObjectData", "SaleType")
@se.enum_field_serializer("ObjectBuy", "ObjectData", "SaleType")
@se.enum_field_serializer("RezScript", "InventoryBlock", "SaleType")
@se.enum_field_serializer("RezObject", "InventoryData", "SaleType")
@se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "SaleType")
@se.enum_field_serializer("UpdateCreateInventoryItem", "InventoryData", "SaleType")
class SaleType(LookupIntEnum):
NOT = 0
ORIGINAL = 1
COPY = 2
CONTENTS = 3
@classmethod
def from_lookup_name(cls, legacy_name: str):
return cls(_SALE_TYPE_LEGACY_NAMES.index(legacy_name))
def to_lookup_name(self) -> str:
return _SALE_TYPE_LEGACY_NAMES[int(self.value)]
class AggregatePermissionType(IntEnum):
EMPTY = 0
NONE = 1
SOME = 2
ALL = 3
def _make_agg_perms_field():
return se.bitfield_field(bits=2, adapter=se.IntEnum(AggregatePermissionType))
@dataclasses.dataclass
class AggregatePerms(se.BitfieldDataclass):
Copy: AggregatePermissionType = _make_agg_perms_field()
Modify: AggregatePermissionType = _make_agg_perms_field()
Transfer: AggregatePermissionType = _make_agg_perms_field()
@se.subfield_serializer("ObjectProperties", "ObjectData", "AggregatePerms")
@se.subfield_serializer("ObjectProperties", "ObjectData", "AggregatePermTextures")
@se.subfield_serializer("ObjectProperties", "ObjectData", "AggregatePermTexturesOwner")
class AggregatePermsSerializer(se.AdapterSubfieldSerializer):
ORIG_INLINE = True
ADAPTER = se.BitfieldDataclass(AggregatePerms)
@se.flag_field_serializer("ParcelInfoReply", "Data", "Flags")
class ParcelInfoFlags(IntFlag):
MATURE = 1 << 0
# You should never see adult without mature
ADULT = 1 << 1
GROUP_OWNED = 1 << 2
@se.flag_field_serializer("MapItemRequest", "AgentData", "Flags")
@se.flag_field_serializer("MapNameRequest", "AgentData", "Flags")
@se.flag_field_serializer("MapBlockRequest", "AgentData", "Flags")
@se.flag_field_serializer("MapItemReply", "AgentData", "Flags")
@se.flag_field_serializer("MapNameReply", "AgentData", "Flags")
@se.flag_field_serializer("MapBlockReply", "AgentData", "Flags")
class MapImageFlags(IntFlag):
# No clue, honestly. I guess there's potentially different image types you could request.
LAYER = 1 << 1
RETURN_NONEXISTENT = 0x10000
@se.enum_field_serializer("MapBlockReply", "Data", "Access")
@se.enum_field_serializer("RegionInfo", "RegionInfo", "SimAccess")
@se.enum_field_serializer("RegionHandshake", "RegionInfo", "SimAccess")
class SimAccess(IntEnum):
# Treated as 'unknown', usually ends up being SIM_ACCESS_PG
MIN = 0
PG = 13
MATURE = 21
ADULT = 42
DOWN = 254
@se.enum_field_serializer("MapItemRequest", "RequestData", "ItemType")
@se.enum_field_serializer("MapItemReply", "RequestData", "ItemType")
class MapItemType(IntEnum):
TELEHUB = 0x01
PG_EVENT = 0x02
MATURE_EVENT = 0x03
# No longer supported, 2009-03-02 KLW
DEPRECATED_POPULAR = 0x04
DEPRECATED_AGENT_COUNT = 0x05
AGENT_LOCATIONS = 0x06
LAND_FOR_SALE = 0x07
CLASSIFIED = 0x08
ADULT_EVENT = 0x09
LAND_FOR_SALE_ADULT = 0x0a
@se.flag_field_serializer("RezObject", "RezData", "ItemFlags")
@se.flag_field_serializer("RezMultipleAttachmentsFromInv", "ObjectData", "ItemFlags")
@se.flag_field_serializer("RezObject", "InventoryData", "Flags")
@se.flag_field_serializer("RezScript", "InventoryBlock", "Flags")
@se.flag_field_serializer("UpdateCreateInventoryItem", "InventoryData", "Flags")
@se.flag_field_serializer("UpdateTaskInventory", "InventoryData", "Flags")
@se.flag_field_serializer("ChangeInventoryItemFlags", "InventoryData", "Flags")
class InventoryItemFlags(IntFlag):
# The asset has only one reference in the system. If the
# inventory item is deleted, or the assetid updated, then we
# can remove the old reference.
SHARED_SINGLE_REFERENCE = 0x40000000
# Object permissions should have next owner perm be more
# restrictive on rez. We bump this into the second byte of the
# flags since the low byte is used to track attachment points.
OBJECT_SLAM_PERM = 0x100
# The object sale information has been changed.
OBJECT_SLAM_SALE = 0x1000
# Specify which permissions masks to overwrite
# upon rez. Normally, if no permissions slam (above) or
# overwrite flags are set, the asset's permissions are
# used and the inventory's permissions are ignored. If
# any of these flags are set, the inventory's permissions
# take precedence.
OBJECT_PERM_OVERWRITE_BASE = 0x010000
OBJECT_PERM_OVERWRITE_OWNER = 0x020000
OBJECT_PERM_OVERWRITE_GROUP = 0x040000
OBJECT_PERM_OVERWRITE_EVERYONE = 0x080000
OBJECT_PERM_OVERWRITE_NEXT_OWNER = 0x100000
# Whether a returned object is composed of multiple items.
OBJECT_HAS_MULTIPLE_ITEMS = 0x200000
@property
def subtype(self):
"""Subtype of the given item type, could be an attachment point or setting type, etc."""
return self & 0xFF
@se.enum_field_serializer("ObjectPermissions", "ObjectData", "Field")
class PermissionType(IntEnum):
BASE = 0x01
OWNER = 0x02
GROUP = 0x04
EVERYONE = 0x08
NEXT_OWNER = 0x10
@se.enum_field_serializer("TransferRequest", "TransferInfo", "SourceType")
class TransferSourceType(IntEnum):
UNKNOWN = 0
FILE = 1
ASSET = 2
SIM_INV_ITEM = 3
SIM_ESTATE = 4
class EstateAssetType(IntEnum):
NONE = -1
COVENANT = 0
@dataclasses.dataclass
class TransferRequestParamsBase(abc.ABC):
pass
@dataclasses.dataclass
class TransferRequestParamsFile(TransferRequestParamsBase):
FileName: str = se.dataclass_field(se.CStr())
Delete: bool = se.dataclass_field(se.BOOL)
@dataclasses.dataclass
class TransferRequestParamsAsset(TransferRequestParamsFile):
AssetID: UUID = se.dataclass_field(se.UUID)
AssetType: "AssetType" = se.dataclass_field(se.IntEnum(AssetType, se.U32))
@dataclasses.dataclass
class TransferRequestParamsSimInvItem(TransferRequestParamsBase):
# Will never be None on the wire, just set this way so
# code can fill them in when creating these ourselves.
AgentID: Optional[UUID] = se.dataclass_field(se.UUID, default=None)
SessionID: Optional[UUID] = se.dataclass_field(se.UUID, default=None)
OwnerID: UUID = se.dataclass_field(se.UUID)
TaskID: UUID = se.dataclass_field(se.UUID)
ItemID: UUID = se.dataclass_field(se.UUID)
AssetID: UUID = se.dataclass_field(se.UUID)
AssetType: "AssetType" = se.dataclass_field(se.IntEnum(AssetType, se.U32))
@dataclasses.dataclass
class TransferRequestParamsSimEstate(TransferRequestParamsBase):
# See above RE Optional
AgentID: Optional[UUID] = se.dataclass_field(se.UUID, default=None)
SessionID: Optional[UUID] = se.dataclass_field(se.UUID, default=None)
EstateAssetType: "EstateAssetType" = se.dataclass_field(se.IntEnum(EstateAssetType, se.U32))
@se.subfield_serializer("TransferRequest", "TransferInfo", "Params")
class TransferParamsSerializer(se.EnumSwitchedSubfieldSerializer):
ENUM_FIELD = "SourceType"
TEMPLATES = {
TransferSourceType.FILE: se.Dataclass(TransferRequestParamsFile),
TransferSourceType.ASSET: se.Dataclass(TransferRequestParamsAsset),
TransferSourceType.SIM_INV_ITEM: se.Dataclass(TransferRequestParamsSimInvItem),
TransferSourceType.SIM_ESTATE: se.Dataclass(TransferRequestParamsSimEstate),
}
@se.enum_field_serializer("TransferAbort", "TransferInfo", "ChannelType")
@se.enum_field_serializer("TransferPacket", "TransferData", "ChannelType")
@se.enum_field_serializer("TransferRequest", "TransferInfo", "ChannelType")
@se.enum_field_serializer("TransferInfo", "TransferInfo", "ChannelType")
class TransferChannelType(IntEnum):
UNKNOWN = 0
MISC = 1
ASSET = 2
@se.enum_field_serializer("TransferInfo", "TransferInfo", "TargetType")
class TransferTargetType(IntEnum):
UNKNOWN = 0
FILE = 1
VFILE = 2
@se.enum_field_serializer("TransferInfo", "TransferInfo", "Status")
@se.enum_field_serializer("TransferPacket", "TransferData", "Status")
class TransferStatus(IntEnum):
OK = 0
DONE = 1
SKIP = 2
ABORT = 3
ERROR = -1 # generic error
UNKNOWN_SOURCE = -2 # not found
INSUFFICIENT_PERMISSIONS = -3
@se.subfield_serializer("TransferInfo", "TransferInfo", "Params")
class TransferInfoSerializer(se.BaseSubfieldSerializer):
TEMPLATES = {
TransferTargetType.FILE: se.Template({
"FileName": se.CStr(),
"Delete": se.BOOL,
}),
TransferTargetType.VFILE: se.Template({
"AgentID": se.UUID,
"SessionID": se.UUID,
"OwnerID": se.UUID,
"TaskID": se.UUID,
"ItemID": se.UUID,
"AssetID": se.UUID,
"AssetType": se.IntEnum(AssetType, se.U32),
}),
}
@classmethod
def _get_target_template(cls, block, val):
target_type = block["TargetType"]
if target_type != TransferTargetType.UNKNOWN:
return cls.TEMPLATES[target_type]
# Hard to tell what format the payload uses without tracking
# which request this info message corresponds to. Brute force it.
templates = TransferParamsSerializer.TEMPLATES.values()
return cls._fuzzy_guess_template(templates, val)
@classmethod
def deserialize(cls, ctx_obj, val, pod=False):
if not val:
return se.UNSERIALIZABLE
template = cls._get_target_template(ctx_obj, val)
if not template:
return se.UNSERIALIZABLE
return cls._deserialize_template(template, val, pod)
@classmethod
def serialize(cls, ctx_obj, vals):
template = cls._get_target_template(ctx_obj, vals)
if not template:
return se.UNSERIALIZABLE
return cls._serialize_template(template, vals)
@se.enum_field_serializer("RequestXfer", "XferID", "FilePath")
class XferFilePath(IntEnum):
NONE = 0
USER_SETTINGS = 1
APP_SETTINGS = 2
PER_SL_ACCOUNT = 3
CACHE = 4
CHARACTER = 5
HELP = 6
LOGS = 7
TEMP = 8
SKINS = 9
TOP_SKIN = 10
CHAT_LOGS = 11
PER_ACCOUNT_CHAT_LOGS = 12
USER_SKIN = 14
LOCAL_ASSETS = 15
EXECUTABLE = 16
DEFAULT_SKIN = 17
FONTS = 18
DUMP = 19
@se.enum_field_serializer("AbortXfer", "XferID", "Result")
class XferError(IntEnum):
FILE_EMPTY = -44
FILE_NOT_FOUND = -43
CANNOT_OPEN_FILE = -42
EOF = -39
@dataclasses.dataclass
class XferPacket(se.BitfieldDataclass):
PacketID: int = se.bitfield_field(bits=31)
IsEOF: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter())
@se.subfield_serializer("SendXferPacket", "XferID", "Packet")
class SendXferPacketIDSerializer(se.AdapterSubfieldSerializer):
ORIG_INLINE = True
ADAPTER = se.BitfieldDataclass(XferPacket)
@se.enum_field_serializer("ViewerEffect", "Effect", "Type")
class ViewerEffectType(IntEnum):
TEXT = 0
ICON = 1
CONNECTOR = 2
FLEXIBLE_OBJECT = 3
ANIMAL_CONTROLS = 4
LOCAL_ANIMATION_OBJECT = 5
CLOTH = 6
EFFECT_BEAM = 7
EFFECT_GLOW = 8
EFFECT_POINT = 9
EFFECT_TRAIL = 10
EFFECT_SPHERE = 11
EFFECT_SPIRAL = 12
EFFECT_EDIT = 13
EFFECT_LOOKAT = 14
EFFECT_POINTAT = 15
EFFECT_VOICE_VISUALIZER = 16
NAME_TAG = 17
EFFECT_BLOB = 18
class LookAtTarget(IntEnum):
NONE = 0
IDLE = 1
AUTO_LISTEN = 2
FREELOOK = 3
RESPOND = 4
HOVER = 5
CONVERSATION = 6
SELECT = 7
FOCUS = 8
MOUSELOOK = 9
CLEAR = 10
class PointAtTarget(IntEnum):
NONE = 0
SELECT = 1
GRAB = 2
CLEAR = 3
@se.subfield_serializer("ViewerEffect", "Effect", "TypeData")
class ViewerEffectDataSerializer(se.EnumSwitchedSubfieldSerializer):
ENUM_FIELD = "Type"
# A lot of effect types that seem to have their own payload
# actually use spiral's. Weird.
SPIRAL_TEMPLATE = se.Template({
"SourceID": se.UUID,
"TargetID": se.UUID,
"TargetPos": se.Vector3D,
})
TEMPLATES = {
ViewerEffectType.EFFECT_POINTAT: se.Template({
"SourceID": se.UUID,
"TargetID": se.UUID,
"TargetPos": se.Vector3D,
"PointTargetType": se.IntEnum(PointAtTarget, se.U8),
}),
ViewerEffectType.EFFECT_BEAM: SPIRAL_TEMPLATE,
ViewerEffectType.EFFECT_EDIT: SPIRAL_TEMPLATE,
ViewerEffectType.EFFECT_SPHERE: SPIRAL_TEMPLATE,
ViewerEffectType.EFFECT_POINT: SPIRAL_TEMPLATE,
ViewerEffectType.EFFECT_LOOKAT: se.Template({
"SourceID": se.UUID,
"TargetID": se.UUID,
"TargetPos": se.Vector3D,
"LookTargetType": se.IntEnum(LookAtTarget, se.U8),
}),
ViewerEffectType.EFFECT_SPIRAL: SPIRAL_TEMPLATE,
}
@se.enum_field_serializer("MoneyTransferRequest", "MoneyData", "TransactionType")
@se.enum_field_serializer("MoneyBalanceReply", "TransactionInfo", "TransactionType")
class MoneyTransactionType(IntEnum):
# _many_ of these codes haven't been used in decades.
# Money transaction failure codes
NULL = 0
# Error conditions
FAIL_SIMULATOR_TIMEOUT = 1
FAIL_DATASERVER_TIMEOUT = 2
FAIL_APPLICATION = 3
# Everything past this is actual transaction types
OBJECT_CLAIM = 1000
LAND_CLAIM = 1001
GROUP_CREATE = 1002
OBJECT_PUBLIC_CLAIM = 1003
GROUP_JOIN = 1004
TELEPORT_CHARGE = 1100
UPLOAD_CHARGE = 1101
LAND_AUCTION = 1102
CLASSIFIED_CHARGE = 1103
OBJECT_TAX = 2000
LAND_TAX = 2001
LIGHT_TAX = 2002
PARCEL_DIR_FEE = 2003
GROUP_TAX = 2004
CLASSIFIED_RENEW = 2005
RECURRING_GENERIC = 2100
GIVE_INVENTORY = 3000
OBJECT_SALE = 5000
GIFT = 5001
LAND_SALE = 5002
REFER_BONUS = 5003
INVENTORY_SALE = 5004
REFUND_PURCHASE = 5005
LAND_PASS_SALE = 5006
DWELL_BONUS = 5007
PAY_OBJECT = 5008
OBJECT_PAYS = 5009
RECURRING_GENERIC_USER = 5100
GROUP_JOIN_RESERVED = 6000
GROUP_LAND_DEED = 6001
GROUP_OBJECT_DEED = 6002
GROUP_LIABILITY = 6003
GROUP_DIVIDEND = 6004
MEMBERSHIP_DUES = 6005
OBJECT_RELEASE = 8000
LAND_RELEASE = 8001
OBJECT_DELETE = 8002
OBJECT_PUBLIC_DECAY = 8003
OBJECT_PUBLIC_DELETE = 8004
LINDEN_ADJUSTMENT = 9000
LINDEN_GRANT = 9001
LINDEN_PENALTY = 9002
EVENT_FEE = 9003
EVENT_PRIZE = 9004
STIPEND_BASIC = 10000
STIPEND_DEVELOPER = 10001
STIPEND_ALWAYS = 10002
STIPEND_DAILY = 10003
STIPEND_RATING = 10004
STIPEND_DELTA = 10005
@se.flag_field_serializer("MoneyTransferRequest", "MoneyData", "Flags")
class MoneyTransactionFlags(IntFlag):
SOURCE_GROUP = 1
DEST_GROUP = 1 << 1
OWNER_GROUP = 1 << 2
SIMULTANEOUS_CONTRIBUTION = 1 << 3
SIMULTANEOUS_CONTRIBUTION_REMOVAL = 1 << 4
@se.enum_field_serializer("ImprovedInstantMessage", "MessageBlock", "Dialog")
class IMDialogType(IntEnum):
NOTHING_SPECIAL = 0
MESSAGEBOX = 1
GROUP_INVITATION = 3
INVENTORY_OFFERED = 4
INVENTORY_ACCEPTED = 5
INVENTORY_DECLINED = 6
GROUP_VOTE = 7
GROUP_MESSAGE_DEPRECATED = 8
TASK_INVENTORY_OFFERED = 9
TASK_INVENTORY_ACCEPTED = 10
TASK_INVENTORY_DECLINED = 11
NEW_USER_DEFAULT = 12
SESSION_INVITE = 13
SESSION_P2P_INVITE = 14
SESSION_GROUP_START = 15
SESSION_CONFERENCE_START = 16
SESSION_SEND = 17
SESSION_LEAVE = 18
FROM_TASK = 19
DO_NOT_DISTURB_AUTO_RESPONSE = 20
CONSOLE_AND_CHAT_HISTORY = 21
LURE_USER = 22
LURE_ACCEPTED = 23
LURE_DECLINED = 24
GODLIKE_LURE_USER = 25
TELEPORT_REQUEST = 26
GROUP_ELECTION_DEPRECATED = 27
GOTO_URL = 28
FROM_TASK_AS_ALERT = 31
GROUP_NOTICE = 32
GROUP_NOTICE_INVENTORY_ACCEPTED = 33
GROUP_NOTICE_INVENTORY_DECLINED = 34
GROUP_INVITATION_ACCEPT = 35
GROUP_INVITATION_DECLINE = 36
GROUP_NOTICE_REQUESTED = 37
FRIENDSHIP_OFFERED = 38
FRIENDSHIP_ACCEPTED = 39
FRIENDSHIP_DECLINED_DEPRECATED = 40
TYPING_START = 41
TYPING_STOP = 42
@se.subfield_serializer("ImprovedInstantMessage", "MessageBlock", "BinaryBucket")
class InstantMessageBucketSerializer(se.EnumSwitchedSubfieldSerializer):
ENUM_FIELD = "Dialog"
# Aliases for clarity
EMPTY_BUCKET = se.UNSERIALIZABLE
PLAIN_STRING = se.UNSERIALIZABLE
ACCEPTED_INVENTORY = se.Template({
"TargetCategoryID": se.UUID,
})
# This is in a different, string format if it comes in through the
# ReadOfflineMsgs Cap but we don't parse those.
GROUP_NOTICE = se.Template({
"HasInventory": se.BOOL,
"AssetType": se.IntEnum(AssetType, se.U8),
"GroupID": se.UUID,
"ItemName": se.CStr(),
})
TEMPLATES = {
IMDialogType.SESSION_GROUP_START: EMPTY_BUCKET,
IMDialogType.INVENTORY_OFFERED: se.Template({
"AssetType": se.IntEnum(AssetType, se.U8),
"ItemID": se.UUID,
# Greedy, no length field.
"Children": se.Collection(
None,
se.Template({
"AssetType": se.IntEnum(AssetType, se.U8),
"ItemID": se.UUID,
}),
),
}),
# Either binary AssetType or serialized string if from when offline. WTF?
IMDialogType.TASK_INVENTORY_OFFERED: se.Template({
"AssetType": se.IntEnum(AssetType, se.U8),
}),
IMDialogType.TASK_INVENTORY_ACCEPTED: ACCEPTED_INVENTORY,
IMDialogType.GROUP_NOTICE_INVENTORY_ACCEPTED: ACCEPTED_INVENTORY,
IMDialogType.INVENTORY_ACCEPTED: ACCEPTED_INVENTORY,
IMDialogType.INVENTORY_DECLINED: EMPTY_BUCKET,
IMDialogType.GROUP_NOTICE_INVENTORY_DECLINED: EMPTY_BUCKET,
IMDialogType.TASK_INVENTORY_DECLINED: EMPTY_BUCKET,
IMDialogType.NOTHING_SPECIAL: EMPTY_BUCKET,
IMDialogType.TYPING_START: EMPTY_BUCKET,
IMDialogType.TYPING_STOP: EMPTY_BUCKET,
# It's a string, just read it as-is.
IMDialogType.LURE_USER: PLAIN_STRING,
IMDialogType.TELEPORT_REQUEST: PLAIN_STRING,
IMDialogType.GODLIKE_LURE_USER: PLAIN_STRING,
IMDialogType.SESSION_LEAVE: EMPTY_BUCKET,
IMDialogType.DO_NOT_DISTURB_AUTO_RESPONSE: EMPTY_BUCKET,
IMDialogType.FRIENDSHIP_OFFERED: EMPTY_BUCKET,
IMDialogType.FRIENDSHIP_ACCEPTED: EMPTY_BUCKET,
IMDialogType.FRIENDSHIP_DECLINED_DEPRECATED: EMPTY_BUCKET,
IMDialogType.GROUP_NOTICE: GROUP_NOTICE,
IMDialogType.GROUP_NOTICE_REQUESTED: GROUP_NOTICE,
IMDialogType.GROUP_INVITATION: se.Template({
"JoinCost": se.S32,
"RoleID": se.UUID,
}),
# Just a string
IMDialogType.FROM_TASK: PLAIN_STRING,
# Session name
IMDialogType.SESSION_SEND: PLAIN_STRING,
# What? Is this even used?
IMDialogType.GOTO_URL: PLAIN_STRING,
}
@se.subfield_serializer("ObjectUpdate", "ObjectData", "ObjectData")
class ObjectUpdateDataSerializer(se.SimpleSubfieldSerializer):
# http://wiki.secondlife.com/wiki/ObjectUpdate#ObjectData_Format
REGION_SIZE = 256.0
MIN_HEIGHT = -REGION_SIZE
MAX_HEIGHT = 4096.0
POSITION_COMPONENT_SCALES = (
(-0.5 * REGION_SIZE, 1.5 * REGION_SIZE),
(-0.5 * REGION_SIZE, 1.5 * REGION_SIZE),
(MIN_HEIGHT, MAX_HEIGHT),
)
FLOAT_TEMPLATE = {
"Position": se.Vector3,
"Velocity": se.Vector3,
"Acceleration": se.Vector3,
"Rotation": se.PackedQuat(se.Vector3),
"AngularVelocity": se.Vector3,
}
HALF_PRECISION_TEMPLATE = {
"Position": se.Vector3U16(component_scales=POSITION_COMPONENT_SCALES),
"Velocity": se.Vector3U16(-REGION_SIZE, REGION_SIZE),
"Acceleration": se.Vector3U16(-REGION_SIZE, REGION_SIZE),
"Rotation": se.PackedQuat(se.Vector4U16(-1.0, 1.0)),
"AngularVelocity": se.Vector3U16(-REGION_SIZE, REGION_SIZE),
}
LOW_PRECISION_TEMPLATE = {
"Position": se.Vector3U8(component_scales=POSITION_COMPONENT_SCALES),
"Velocity": se.Vector3U8(-REGION_SIZE, REGION_SIZE),
"Acceleration": se.Vector3U8(-REGION_SIZE, REGION_SIZE),
"Rotation": se.PackedQuat(se.Vector4U8(-1.0, 1.0)),
"AngularVelocity": se.Vector3U8(-REGION_SIZE, REGION_SIZE),
}
TEMPLATE = se.LengthSwitch({
76: se.Template({"FootCollisionPlane": se.Vector4, **FLOAT_TEMPLATE}),
60: se.Template(FLOAT_TEMPLATE),
48: se.Template({"FootCollisionPlane": se.Vector4, **HALF_PRECISION_TEMPLATE}),
32: se.Template(HALF_PRECISION_TEMPLATE),
16: se.Template(LOW_PRECISION_TEMPLATE),
})
@se.enum_field_serializer("ObjectUpdate", "ObjectData", "PCode")
@se.enum_field_serializer("ObjectAdd", "ObjectData", "PCode")
class PCode(IntEnum):
# Should actually be a bitmask, these are just some common ones.
PRIMITIVE = 9
AVATAR = 47
GRASS = 95
NEW_TREE = 111
PARTICLE_SYSTEM = 143
TREE = 255
@se.enum_field_serializer("ObjectUpdate", "ObjectData", "Material")
@se.enum_field_serializer("ObjectAdd", "ObjectData", "Material")
@se.enum_field_serializer("ObjectMaterial", "ObjectData", "Material")
class MCode(IntEnum):
# Seems like this is normally stored in a U8 with the high nybble masked off?
# What's in the high nybble, anything?
STONE = 0
METAL = 1
GLASS = 2
WOOD = 3
FLESH = 4
PLASTIC = 5
RUBBER = 6
LIGHT = 7
@se.flag_field_serializer("ObjectUpdate", "ObjectData", "UpdateFlags")
@se.flag_field_serializer("ObjectUpdateCompressed", "ObjectData", "UpdateFlags")
@se.flag_field_serializer("ObjectUpdateCached", "ObjectData", "UpdateFlags")
@se.flag_field_serializer("ObjectAdd", "ObjectData", "AddFlags")
@se.flag_field_serializer("ObjectDuplicate", "SharedData", "DuplicateFlags")
class ObjectUpdateFlags(IntFlag):
USE_PHYSICS = 1 << 0
CREATE_SELECTED = 1 << 1
OBJECT_MODIFY = 1 << 2
OBJECT_COPY = 1 << 3
OBJECT_ANY_OWNER = 1 << 4
OBJECT_YOU_OWNER = 1 << 5
SCRIPTED = 1 << 6
HANDLE_TOUCH = 1 << 7
OBJECT_MOVE = 1 << 8
TAKES_MONEY = 1 << 9
PHANTOM = 1 << 10
INVENTORY_EMPTY = 1 << 11
AFFECTS_NAVMESH = 1 << 12
CHARACTER = 1 << 13
VOLUME_DETECT = 1 << 14
INCLUDE_IN_SEARCH = 1 << 15
ALLOW_INVENTORY_DROP = 1 << 16
OBJECT_TRANSFER = 1 << 17
OBJECT_GROUP_OWNED = 1 << 18
OBJECT_YOU_OFFICER_DEPRECATED = 1 << 19
CAMERA_DECOUPLED = 1 << 20
ANIM_SOURCE = 1 << 21
CAMERA_SOURCE = 1 << 22
CAST_SHADOWS_DEPRECATED = 1 << 23
UNUSED_002 = 1 << 24
UNUSED_003 = 1 << 25
UNUSED_004 = 1 << 26
UNUSED_005 = 1 << 27
OBJECT_OWNER_MODIFY = 1 << 28
TEMPORARY_ON_REZ = 1 << 29
TEMPORARY_DEPRECATED = 1 << 30
ZLIB_COMPRESSED_REPRECATED = 1 << 31
JUST_CREATED_FLAGS = (ObjectUpdateFlags.CREATE_SELECTED | ObjectUpdateFlags.OBJECT_YOU_OWNER)
class AttachmentStateAdapter(se.Adapter):
# Encoded attachment point ID for attached objects
# nibbles are swapped around because old attachment nums only used to live
# in the high bits, I guess.
OFFSET = 4
MASK = 0xF << OFFSET
def _rotate_nibbles(self, val: int):
return ((val & self.MASK) >> self.OFFSET) | ((val & ~self.MASK) << self.OFFSET)
def decode(self, val: Any, ctx: Optional[se.ParseContext], pod: bool = False) -> Any:
return self._rotate_nibbles(val)
def encode(self, val: Any, ctx: Optional[se.ParseContext]) -> Any:
# f(f(x)) = x
return self._rotate_nibbles(val)
@se.flag_field_serializer("AgentUpdate", "AgentData", "State")
class AgentState(IntFlag):
TYPING = 1 << 2
EDITING = 1 << 4
class ObjectStateAdapter(se.ContextAdapter):
# State has a different meaning depending on PCode
def __init__(self, child_spec: Optional[se.SERIALIZABLE_TYPE]):
super().__init__(
lambda ctx: ctx.PCode, child_spec, {
PCode.AVATAR: se.IntFlag(AgentState),
PCode.PRIMITIVE: AttachmentStateAdapter(None),
# Other cases are probably just a number (tree species ID or something.)
se.MISSING: se.IdentityAdapter(),
}
)
@se.subfield_serializer("ObjectUpdate", "ObjectData", "State")
@se.subfield_serializer("ObjectAdd", "ObjectData", "State")
class ObjectStateSerializer(se.AdapterSubfieldSerializer):
ADAPTER = ObjectStateAdapter(None)
ORIG_INLINE = True
@se.subfield_serializer("ObjectUpdate", "RegionData", "TimeDilation")
@se.subfield_serializer("ObjectUpdateCompressed", "RegionData", "TimeDilation")
@se.subfield_serializer("ObjectUpdateCached", "RegionData", "TimeDilation")
@se.subfield_serializer("ImprovedTerseObjectUpdate", "RegionData", "TimeDilation")
class TimeDilationSerializer(se.AdapterSubfieldSerializer):
ADAPTER = se.QuantizedFloat(se.U16, 0.0, 1.0, False)
ORIG_INLINE = True
@se.subfield_serializer("ImprovedTerseObjectUpdate", "ObjectData", "Data")
class ImprovedTerseObjectUpdateDataSerializer(se.SimpleSubfieldSerializer):
TEMPLATE = se.Template({
"ID": se.U32,
# No inline PCode, so can't make sense of State at the message level
"State": se.U8,
"FootCollisionPlane": se.OptionalPrefixed(se.Vector4),
"Position": se.Vector3,
"Velocity": se.Vector3U16(-128.0, 128.0),
"Acceleration": se.Vector3U16(-64.0, 64.0),
"Rotation": se.PackedQuat(se.Vector4U16(-1.0, 1.0)),
"AngularVelocity": se.Vector3U16(-64.0, 64.0),
})
class ShineLevel(IntEnum):
OFF = 0
LOW = 1
MEDIUM = 2
HIGH = 3
@dataclasses.dataclass(unsafe_hash=True)
class BasicMaterials:
# Meaning is technically implementation-dependent, these are in LL data files
Bump: int = se.bitfield_field(bits=5, default=0)
FullBright: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter(), default=False)
Shiny: int = se.bitfield_field(bits=2, adapter=se.IntEnum(ShineLevel), default=0)
BUMP_SHINY_FULLBRIGHT = se.BitfieldDataclass(BasicMaterials, se.U8)
class TexGen(IntEnum):
DEFAULT = 0
PLANAR = 0x2
# These are unused / not supported
SPHERICAL = 0x4
CYLINDRICAL = 0x6
@dataclasses.dataclass(unsafe_hash=True)
class MediaFlags:
WebPage: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter(), default=False)
TexGen: "TexGen" = se.bitfield_field(bits=2, adapter=se.IntEnum(TexGen), default=TexGen.DEFAULT)
# Probably unused but show it just in case
_Unused: int = se.bitfield_field(bits=5, default=0)
# Not shifted so enum definitions can match indra
MEDIA_FLAGS = se.BitfieldDataclass(MediaFlags, se.U8, shift=False)
class Color4(se.Adapter):
def __init__(self, invert_bytes=False, invert_alpha=False):
# There's several different ways of representing colors, presumably
# to allow for more efficient zerocoding in common cases.
self.invert_bytes = invert_bytes
self.invert_alpha = invert_alpha
super().__init__(se.BytesFixed(4))
def _invert(self, val: bytes) -> bytes:
if self.invert_bytes:
val = bytes(~x & 0xFF for x in val)
if self.invert_alpha:
val = val[:3] + bytes((~val[4] & 0xFF,))
return val
def encode(self, val: bytes, ctx: Optional[se.ParseContext]) -> bytes:
return self._invert(val)
def decode(self, val: bytes, ctx: Optional[se.ParseContext], pod: bool = False) -> bytes:
return self._invert(val)
class TEFaceBitfield(se.SerializableBase):
"""
Arbitrary-length bitfield of faces
0x80 bit indicates bitfield has more bytes. Each byte can represent
on / off for 7 faces.
"""
@classmethod
def deserialize(cls, reader: se.BufferReader, ctx=None):
have_next = True
val = 0
while have_next:
char = reader.read(se.U8, ctx=ctx)
have_next = char & 0x80
val |= char & 0x7F
if have_next:
val <<= 7
# Bitfield of faces reconstructed, convert to tuple
i = 0
face_list = []
while val:
if val & 1:
face_list.append(i)
i += 1
val >>= 1
return tuple(face_list)
@classmethod
def serialize(cls, faces, writer: se.BufferWriter, ctx=None):
packed = 0
for face in faces:
packed |= 1 << face
char_arr = []
while packed:
# 7 faces per byte
val = packed & 0x7F
packed >>= 7
char_arr.append(val)
char_arr.reverse()
while char_arr:
val = char_arr.pop(0)
# need a continuation
if char_arr:
val |= 0x80
writer.write(se.U8, val, ctx=ctx)
class TEExceptionField(se.SerializableBase):
"""
TextureEntry field with a "default" value and trailing "exception" values
Each value that deviates from the default value is added as an "exception,"
prefixed with a bitfield of face numbers it applies to. A field is terminated
with a NUL, so long as it's not the last field. In that case, EOF implicitly
terminates.
"""
def __init__(self, spec, optional=False, first=False):
self._spec = spec
self._first = first
self._optional = optional
def serialize(self, vals, writer: se.BufferWriter, ctx=None):
if self._optional and not vals:
return
# NUL needed to mark the start of a field if this isn't the first one
if not self._first:
writer.write_bytes(b"\x00")
ctx = se.ParseContext(vals, parent=ctx)
default = vals[None]
writer.write(self._spec, default, ctx=ctx)
for faces, val in vals.items():
if faces is None:
continue
writer.write(TEFaceBitfield, faces, ctx=ctx)
writer.write(self._spec, val, ctx=ctx)
def deserialize(self, reader: se.BufferReader, ctx=None):
# No bytes left and this is an optional field
if self._optional and not reader:
return None
# Technically there's nothing preventing an implementation from
# repeating a face bitfield. You can use a MultiDict to preserve
# any duplicate keys if you care, but it's incredibly slow.
vals = {}
ctx = se.ParseContext(vals, parent=ctx)
vals[None] = reader.read(self._spec, ctx=ctx)
while reader:
faces = reader.read(TEFaceBitfield, ctx=ctx)
if not faces:
break
# Key will be a tuple of face numbers
vals[faces] = reader.read(self._spec, ctx=ctx)
return vals
def default_value(self):
return dict
_T = TypeVar("_T")
_TE_FIELD_KEY = Optional[Sequence[int]]
_TE_DICT = Dict[_TE_FIELD_KEY, _T]
def _te_field(spec: se.SERIALIZABLE_TYPE, first=False, optional=False,
default_factory: Union[se.MissingType, Callable[[], _T]] = se.MISSING,
default: Union[se.MissingType, _T] = se.MISSING):
if default_factory is not se.MISSING:
new_default_factory = lambda: {None: default_factory()}
elif default is not None:
new_default_factory = lambda: {None: default}
else:
new_default_factory = dataclasses.MISSING
return se.dataclass_field(
TEExceptionField(spec, first=first, optional=optional),
default_factory=new_default_factory,
)
# If this seems weird it's because it is. TE offsets are S16s with `0` as the actual 0
# point, and LL divides by `0x7FFF` to convert back to float. Negative S16s can
# actually go to -0x8000 due to two's complement, creating a larger range for negatives.
TE_S16_COORD = se.QuantizedFloat(se.S16, -1.000030518509476, 1.0, False)
class PackedTERotation(se.QuantizedFloat):
"""Another weird one, packed TE rotations have their own special quantization"""
def __init__(self):
super().__init__(se.S16, math.pi * -2, math.pi * 2, zero_median=False)
self.step_mag = 1.0 / (se.U16.max_val + 1)
def _float_to_quantized(self, val: float, lower: float, upper: float):
val = math.fmod(val, upper)
val = super()._float_to_quantized(val, lower, upper)
if val == se.S16.max_val + 1:
val = self.prim_min
return val
@dataclasses.dataclass
class TextureEntry:
"""Representation of a TE for a single face. Not sent over the wire."""
Textures: UUID = UUID('89556747-24cb-43ed-920b-47caed15465f')
Color: bytes = b"\xff\xff\xff\xff"
ScalesS: float = 1.0
ScalesT: float = 1.0
OffsetsS: float = 0.0
OffsetsT: float = 0.0
# In radians
Rotation: float = 0.0
MediaFlags: "MediaFlags" = dataclasses.field(default_factory=MediaFlags)
BasicMaterials: "BasicMaterials" = dataclasses.field(default_factory=BasicMaterials)
Glow: float = 0.0
Materials: UUID = UUID.ZERO
def st_to_uv(self, st_coord: Vector3) -> Vector3:
"""Convert OpenGL ST coordinates to UV coordinates, accounting for mapping"""
uv = Vector3(st_coord.X - 0.5, st_coord.Y - 0.5)
cos_rot = math.cos(self.Rotation)
sin_rot = math.sin(self.Rotation)
uv = Vector3(
X=uv.X * cos_rot + uv.Y * sin_rot,
Y=-uv.X * sin_rot + uv.Y * cos_rot
)
uv *= Vector3(self.ScalesS, self.ScalesT)
return uv + Vector3(self.OffsetsS + 0.5, self.OffsetsT + 0.5)
# Max number of TEs possible according to llprimitive (but not really true!)
# Useful if you don't know how many faces / TEs an object really has because it's mesh
# or something.
MAX_TES = 45
@dataclasses.dataclass
class TextureEntryCollection:
Textures: _TE_DICT[UUID] = _te_field(
# Plywood texture
se.UUID, first=True, default=UUID('89556747-24cb-43ed-920b-47caed15465f'))
# Bytes are inverted so fully opaque white is \x00\x00\x00\x00
Color: _TE_DICT[bytes] = _te_field(Color4(invert_bytes=True), default=b"\xff\xff\xff\xff")
ScalesS: _TE_DICT[float] = _te_field(se.F32, default=1.0)
ScalesT: _TE_DICT[float] = _te_field(se.F32, default=1.0)
OffsetsS: _TE_DICT[float] = _te_field(TE_S16_COORD, default=0.0)
OffsetsT: _TE_DICT[float] = _te_field(TE_S16_COORD, default=0.0)
Rotation: _TE_DICT[float] = _te_field(PackedTERotation(), default=0.0)
BasicMaterials: _TE_DICT["BasicMaterials"] = _te_field(
BUMP_SHINY_FULLBRIGHT, default_factory=BasicMaterials,
)
MediaFlags: _TE_DICT["MediaFlags"] = _te_field(MEDIA_FLAGS, default_factory=MediaFlags)
Glow: _TE_DICT[float] = _te_field(se.QuantizedFloat(se.U8, 0.0, 1.0), default=0.0)
Materials: _TE_DICT[UUID] = _te_field(se.UUID, optional=True, default=UUID.ZERO)
def unwrap(self):
"""Return `self` regardless of whether this is lazy wrapped object or not"""
return self
def realize(self, num_faces: int = MAX_TES) -> List[TextureEntry]:
"""
Turn the "default" vs "exception cases" wire format TE representation to per-face lookups
Makes it easier to get all TE details associated with a specific face
"""
as_dicts = [dict() for _ in range(num_faces)]
for field in dataclasses.fields(self):
key = field.name
vals = getattr(self, key)
# Fill give all faces the default value for this key
for te in as_dicts:
te[key] = copy.copy(vals[None])
# Walk over the exception cases and replace the default value
for face_nums, val in vals.items():
# Default case already handled
if face_nums is None:
continue
for face_num in face_nums:
if face_num >= num_faces:
raise ValueError(f"Bad value for num_faces? {face_num} >= {num_faces}")
as_dicts[face_num][key] = copy.copy(val)
return [TextureEntry(**x) for x in as_dicts]
@classmethod
def from_tes(cls, tes: List[TextureEntry]) -> "TextureEntryCollection":
instance = cls()
if not tes:
return instance
for field in dataclasses.fields(cls):
te_vals: Dict[Any, List[int]] = collections.defaultdict(list)
for i, te in enumerate(tes):
# Group values by what face they occur on
te_vals[getattr(te, field.name)].append(i)
# Make most common value the "default", everything else is an exception
sorted_vals = sorted(te_vals.items(), key=lambda x: len(x[1]), reverse=True)
default_val = sorted_vals.pop(0)[0]
te_vals = {None: default_val}
for val, face_nums in sorted_vals:
te_vals[tuple(face_nums)] = val
setattr(instance, field.name, te_vals)
return instance
TE_SERIALIZER = se.Dataclass(TextureEntryCollection)
@se.subfield_serializer("ObjectUpdate", "ObjectData", "TextureEntry")
@se.subfield_serializer("AvatarAppearance", "ObjectData", "TextureEntry")
@se.subfield_serializer("AgentSetAppearance", "ObjectData", "TextureEntry")
@se.subfield_serializer("ObjectImage", "ObjectData", "TextureEntry")
class TextureEntrySubfieldSerializer(se.SimpleSubfieldSerializer):
EMPTY_IS_NONE = True
TEMPLATE = se.TypedBytesGreedy(TE_SERIALIZER, empty_is_none=True, lazy=True)
DATA_PACKER_TE_TEMPLATE = se.TypedByteArray(
se.U32,
TE_SERIALIZER,
empty_is_none=True,
# TODO: Let Readers have lazy=False prop and let addons call
# out what subfield serializers should not be lazy. Lazy is way
# more expensive if you're going to deserialize every TE anyway
lazy=True,
)
@se.subfield_serializer("ImprovedTerseObjectUpdate", "ObjectData", "TextureEntry")
class DPTextureEntrySubfieldSerializer(se.SimpleSubfieldSerializer):
EMPTY_IS_NONE = True
TEMPLATE = DATA_PACKER_TE_TEMPLATE
class TextureAnimMode(IntFlag):
ON = 0x01
LOOP = 0x02
REVERSE = 0x04
PING_PONG = 0x08
SMOOTH = 0x10
ROTATE = 0x20
SCALE = 0x40
@dataclasses.dataclass
class TextureAnim:
Mode: TextureAnimMode = se.dataclass_field(se.IntFlag(TextureAnimMode, se.U8))
Face: int = se.dataclass_field(se.S8)
SizeX: int = se.dataclass_field(se.U8)
SizeY: int = se.dataclass_field(se.U8)
Start: float = se.dataclass_field(se.F32)
Length: float = se.dataclass_field(se.F32)
Rate: float = se.dataclass_field(se.F32)
TA_TEMPLATE = se.Dataclass(TextureAnim)
@se.subfield_serializer("ObjectUpdate", "ObjectData", "TextureAnim")
class TextureAnimSerializer(se.SimpleSubfieldSerializer):
EMPTY_IS_NONE = True
TEMPLATE = TA_TEMPLATE
@se.subfield_serializer("ObjectProperties", "ObjectData", "TextureID")
class TextureIDListSerializer(se.SimpleSubfieldSerializer):
EMPTY_IS_NONE = True
TEMPLATE = se.Collection(None, se.UUID)
class ParticleDataFlags(IntFlag):
INTERP_COLOR = 0x001
INTERP_SCALE = 0x002
BOUNCE = 0x004
WIND = 0x008
FOLLOW_SRC = 0x010
FOLLOW_VELOCITY = 0x020
TARGET_POS = 0x040
TARGET_LINEAR = 0x080
EMISSIVE = 0x100
BEAM = 0x200
RIBBON = 0x400
DATA_GLOW = 0x10000
DATA_BLEND = 0x20000
class ParticleFlags(IntFlag):
OBJECT_RELATIVE = 0x1
USE_NEW_ANGLE = 0x2
class ParticleBlendFunc(IntEnum):
ONE = 0
ZERO = 1
DEST_COLOR = 2
SOURCE_COLOR = 3
ONE_MINUS_DEST_COLOR = 4
ONE_MINUS_SOURCE_COLOR = 5
DEST_ALPHA = 6
SOURCE_ALPHA = 7
ONE_MINUS_DEST_ALPHA = 8
ONE_MINUS_SOURCE_ALPHA = 9
PARTDATA_FLAGS = se.IntFlag(ParticleDataFlags, se.U32)
class PartDataOption(se.OptionalFlagged):
def __init__(self, flag_val, spec):
super().__init__("PDataFlags", PARTDATA_FLAGS, flag_val, spec)
PDATA_BLOCK_TEMPLATE = se.Template({
"PDataFlags": PARTDATA_FLAGS,
"PDataMaxAge": se.FixedPoint(se.U16, 8, 8),
"StartColor": Color4(),
"EndColor": Color4(),
"StartScaleX": se.FixedPoint(se.U8, 3, 5),
"StartScaleY": se.FixedPoint(se.U8, 3, 5),
"EndScaleX": se.FixedPoint(se.U8, 3, 5),
"EndScaleY": se.FixedPoint(se.U8, 3, 5),
"StartGlow": PartDataOption(ParticleDataFlags.DATA_GLOW, se.QuantizedFloat(se.U8, 0.0, 1.0)),
"EndGlow": PartDataOption(ParticleDataFlags.DATA_GLOW, se.QuantizedFloat(se.U8, 0.0, 1.0)),
"BlendSource": PartDataOption(ParticleDataFlags.DATA_BLEND, se.IntEnum(ParticleBlendFunc, se.U8)),
"BlendDest": PartDataOption(ParticleDataFlags.DATA_BLEND, se.IntEnum(ParticleBlendFunc, se.U8)),
})
class PartPattern(IntFlag):
NONE = 0
DROP = 0x1
EXPLODE = 0x2
ANGLE = 0x4
ANGLE_CONE = 0x8
ANGLE_CONE_EMPTY = 0x10
PSYS_BLOCK_TEMPLATE = se.Template({
"CRC": se.U32,
"PSysFlags": se.IntFlag(ParticleFlags, se.U32),
"Pattern": se.IntFlag(PartPattern, se.U8),
"PSysMaxAge": se.FixedPoint(se.U16, 8, 8),
"StartAge": se.FixedPoint(se.U16, 8, 8),
"InnerAngle": se.FixedPoint(se.U8, 3, 5),
"OuterAngle": se.FixedPoint(se.U8, 3, 5),
"BurstRate": se.FixedPoint(se.U16, 8, 8),
"BurstRadius": se.FixedPoint(se.U16, 8, 8),
"BurstSpeedMin": se.FixedPoint(se.U16, 8, 8),
"BurstSpeedMax": se.FixedPoint(se.U16, 8, 8),
"BurstPartCount": se.U8,
"Vel": se.FixedPointVector3U16(8, 7, signed=True),
"Accel": se.FixedPointVector3U16(8, 7, signed=True),
"Texture": se.UUID,
"Target": se.UUID,
})
PSBLOCK_TEMPLATE = se.LengthSwitch({
0: se.Null,
86: se.Template({"PSys": PSYS_BLOCK_TEMPLATE, "PData": PDATA_BLOCK_TEMPLATE}),
# Catch-all, this is a variable-length psblock
None: se.Template({
"PSys": se.TypedByteArray(se.S32, PSYS_BLOCK_TEMPLATE),
"PData": se.TypedByteArray(se.S32, PDATA_BLOCK_TEMPLATE),
})
})
@se.subfield_serializer("ObjectUpdate", "ObjectData", "PSBlock")
class PSBlockSerializer(se.SimpleSubfieldSerializer):
TEMPLATE = PSBLOCK_TEMPLATE
@se.enum_field_serializer("ObjectExtraParams", "ObjectData", "ParamType")
class ExtraParamType(IntEnum):
FLEXIBLE = 0x10
LIGHT = 0x20
SCULPT = 0x30
LIGHT_IMAGE = 0x40
RESERVED = 0x50
MESH = 0x60
EXTENDED_MESH = 0x70
RENDER_MATERIAL = 0x80
REFLECTION_PROBE = 0x90
class ExtendedMeshFlags(IntFlag):
ANIMATED_MESH = 0x1
class SculptType(IntEnum):
NONE = 0
SPHERE = 1
TORUS = 2
PLANE = 3
CYLINDER = 4
MESH = 5
@dataclasses.dataclass
class SculptTypeData:
Type: SculptType = se.bitfield_field(bits=6, adapter=se.IntEnum(SculptType))
Invert: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter())
Mirror: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter())
class ReflectionProbeFlags(IntFlag):
# use a box influence volume
BOX_VOLUME = 0x1
# render dynamic objects (avatars) into this Reflection Probe
DYNAMIC = 0x2
EXTRA_PARAM_TEMPLATES = {
ExtraParamType.FLEXIBLE: se.Template({
"Tension": se.BitField(se.U8, {"Tension": 6, "Softness1": 2}),
"Drag": se.BitField(se.U8, {"Drag": 7, "Softness2": 1}),
"Gravity": se.U8,
"Wind": se.U8,
"UserForce": se.IfPresent(se.Vector3),
}),
ExtraParamType.LIGHT: se.Template({
"Color": Color4(),
"Radius": se.F32,
"Cutoff": se.F32,
"Falloff": se.F32,
}),
ExtraParamType.SCULPT: se.Template({
"Texture": se.UUID,
"TypeData": se.BitfieldDataclass(SculptTypeData, se.U8),
}),
ExtraParamType.LIGHT_IMAGE: se.Template({
"Texture": se.UUID,
"FOV": se.F32,
"Focus": se.F32,
"Ambiance": se.F32,
}),
ExtraParamType.MESH: se.Template({
"Asset": se.UUID,
"TypeData": se.BitfieldDataclass(SculptTypeData, se.U8),
}),
ExtraParamType.EXTENDED_MESH: se.Template({
"Flags": se.IntFlag(ExtendedMeshFlags, se.U32),
}),
ExtraParamType.RENDER_MATERIAL: se.Collection(se.U8, se.Template({
"TEIdx": se.U8,
"TEID": se.UUID,
})),
ExtraParamType.REFLECTION_PROBE: se.Template({
"Ambiance": se.F32,
"ClipDistance": se.F32,
"Flags": se.IntFlag(ReflectionProbeFlags, se.U8),
}),
}
@se.subfield_serializer("ObjectExtraParams", "ObjectData", "ParamData")
class ObjectExtraParamsDataSerializer(se.EnumSwitchedSubfieldSerializer):
ENUM_FIELD = "ParamType"
TEMPLATES = EXTRA_PARAM_TEMPLATES
EXTRA_PARAM_COLLECTION = se.DictAdapter(se.Collection(
length=se.U8,
entry_ser=se.EnumSwitch(se.IntEnum(ExtraParamType, se.U16), {
t: se.TypedByteArray(se.U32, v) for t, v in EXTRA_PARAM_TEMPLATES.items()
}),
))
@se.subfield_serializer("ObjectUpdate", "ObjectData", "ExtraParams")
class ObjectUpdateExtraParamsSerializer(se.SimpleSubfieldSerializer):
TEMPLATE = EXTRA_PARAM_COLLECTION
EMPTY_IS_NONE = True
@se.flag_field_serializer("ObjectUpdate", "ObjectData", "Flags")
class SoundFlags(IntFlag):
LOOP = 1 << 0
SYNC_MASTER = 1 << 1
SYNC_SLAVE = 1 << 2
SYNC_PENDING = 1 << 3
QUEUE = 1 << 4
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
TEXT = 1 << 2
PARTICLES = 1 << 3
SOUND = 1 << 4
PARENT_ID = 1 << 5
TEXTURE_ANIM = 1 << 6
ANGULAR_VELOCITY = 1 << 7
NAME_VALUES = 1 << 8
MEDIA_URL = 1 << 9
PARTICLES_NEW = 1 << 10
class CompressedOption(se.OptionalFlagged):
def __init__(self, flag_val, spec):
super().__init__("Flags", se.IntFlag(CompressedFlags, se.U32), flag_val, spec)
NAMEVALUES_TERMINATED_TEMPLATE = se.TypedBytesTerminated(
NameValuesSerializer, terminators=(b"\x00",), empty_is_none=True)
@se.subfield_serializer("ObjectUpdateCompressed", "ObjectData", "Data")
class ObjectUpdateCompressedDataSerializer(se.SimpleSubfieldSerializer):
TEMPLATE = se.Template({
"FullID": se.UUID,
"ID": se.U32,
"PCode": se.IntEnum(PCode, se.U8),
# Meaning of State is PCode dependent. Could be a bitfield of flags + shifted attachment
# point if an object with parents set to an avatar.
"State": ObjectStateAdapter(se.U8),
"CRC": se.U32,
"Material": se.IntEnum(MCode, se.U8),
"ClickAction": se.IntEnum(ClickAction, se.U8),
"Scale": se.Vector3,
"Position": se.Vector3,
"Rotation": se.PackedQuat(se.Vector3),
"Flags": se.IntFlag(CompressedFlags, se.U32),
# Only non-null if there's an attached sound
"OwnerID": se.UUID,
"AngularVelocity": CompressedOption(CompressedFlags.ANGULAR_VELOCITY, se.Vector3),
# Note: missing section specifically means ParentID = 0
"ParentID": CompressedOption(CompressedFlags.PARENT_ID, se.U32),
# This field is strange. State == TreeSpecies in other ObjectUpdate types
"TreeSpecies": CompressedOption(CompressedFlags.TREE, se.U8),
# Technically only allowed if TREE not set, but I'm not convinced this is ever
# used, or that any of the official unpackers would work correctly even if it was.
"ScratchPad": CompressedOption(CompressedFlags.SCRATCHPAD, se.ByteArray(se.U32)),
"Text": CompressedOption(CompressedFlags.TEXT, se.CStr()),
"TextColor": CompressedOption(CompressedFlags.TEXT, Color4()),
"MediaURL": CompressedOption(CompressedFlags.MEDIA_URL, se.CStr()),
"PSBlock": CompressedOption(CompressedFlags.PARTICLES, se.TypedBytesFixed(86, PSBLOCK_TEMPLATE)),
"ExtraParams": EXTRA_PARAM_COLLECTION,
"Sound": CompressedOption(CompressedFlags.SOUND, se.UUID),
"SoundGain": CompressedOption(CompressedFlags.SOUND, se.F32),
"SoundFlags": CompressedOption(CompressedFlags.SOUND, se.IntFlag(SoundFlags, se.U8)),
"SoundRadius": CompressedOption(CompressedFlags.SOUND, se.F32),
"NameValue": CompressedOption(CompressedFlags.NAME_VALUES, NAMEVALUES_TERMINATED_TEMPLATE),
# Intentionally not de-quantizing to preserve their real ranges.
"PathCurve": se.U8,
"ProfileCurve": se.U8,
"PathBegin": se.U16, # 0 to 1, quanta = 0.01
"PathEnd": se.U16, # 0 to 1, quanta = 0.01
"PathScaleX": se.U8, # 0 to 1, quanta = 0.01
"PathScaleY": se.U8, # 0 to 1, quanta = 0.01
"PathShearX": se.U8, # -.5 to .5, quanta = 0.01
"PathShearY": se.U8, # -.5 to .5, quanta = 0.01
"PathTwist": se.S8, # -1 to 1, quanta = 0.01
"PathTwistBegin": se.S8, # -1 to 1, quanta = 0.01
"PathRadiusOffset": se.S8, # -1 to 1, quanta = 0.01
"PathTaperX": se.S8, # -1 to 1, quanta = 0.01
"PathTaperY": se.S8, # -1 to 1, quanta = 0.01
"PathRevolutions": se.U8, # 0 to 3, quanta = 0.015
"PathSkew": se.S8, # -1 to 1, quanta = 0.01
"ProfileBegin": se.U16, # 0 to 1, quanta = 0.01
"ProfileEnd": se.U16, # 0 to 1, quanta = 0.01
"ProfileHollow": se.U16, # 0 to 1, quanta = 0.01
"TextureEntry": DATA_PACKER_TE_TEMPLATE,
"TextureAnim": CompressedOption(CompressedFlags.TEXTURE_ANIM, se.TypedByteArray(se.U32, TA_TEMPLATE)),
"PSBlockNew": CompressedOption(CompressedFlags.PARTICLES_NEW, PSBLOCK_TEMPLATE),
})
@se.flag_field_serializer("MultipleObjectUpdate", "ObjectData", "Type")
class MultipleObjectUpdateFlags(IntFlag):
POSITION = 0x01
ROTATION = 0x02
SCALE = 0x04
LINKED_SETS = 0x08
UNIFORM = 0x10
@se.subfield_serializer("MultipleObjectUpdate", "ObjectData", "Data")
class MultipleObjectUpdateDataSerializer(se.FlagSwitchedSubfieldSerializer):
FLAG_FIELD = "Type"
TEMPLATES = {
MultipleObjectUpdateFlags.POSITION: se.Vector3,
MultipleObjectUpdateFlags.ROTATION: se.PackedQuat(se.Vector3),
MultipleObjectUpdateFlags.SCALE: se.Vector3,
}
@se.flag_field_serializer("AgentUpdate", "AgentData", "ControlFlags")
@se.flag_field_serializer("ScriptControlChange", "Data", "Controls")
class AgentControlFlags(IntFlag):
AT_POS = 1
AT_NEG = 1 << 1
LEFT_POS = 1 << 2
LEFT_NEG = 1 << 3
UP_POS = 1 << 4
UP_NEG = 1 << 5
PITCH_POS = 1 << 6
PITCH_NEG = 1 << 7
YAW_POS = 1 << 8
YAW_NEG = 1 << 9
FAST_AT = 1 << 10
FAST_LEFT = 1 << 11
FAST_UP = 1 << 12
FLY = 1 << 13
STOP = 1 << 14
FINISH_ANIM = 1 << 15
STAND_UP = 1 << 16
SIT_ON_GROUND = 1 << 17
MOUSELOOK = 1 << 18
NUDGE_AT_POS = 1 << 19
NUDGE_AT_NEG = 1 << 20
NUDGE_LEFT_POS = 1 << 21
NUDGE_LEFT_NEG = 1 << 22
NUDGE_UP_POS = 1 << 23
NUDGE_UP_NEG = 1 << 24
TURN_LEFT = 1 << 25
TURN_RIGHT = 1 << 26
AWAY = 1 << 27
LBUTTON_DOWN = 1 << 28
LBUTTON_UP = 1 << 29
ML_LBUTTON_DOWN = 1 << 30
ML_LBUTTON_UP = 1 << 31
@se.flag_field_serializer("AgentUpdate", "AgentData", "Flags")
class AgentUpdateFlags(IntFlag):
HIDE_TITLE = 1
CLIENT_AUTOPILOT = 1 << 1
@se.enum_field_serializer("ChatFromViewer", "ChatData", "Type")
@se.enum_field_serializer("ChatFromSimulator", "ChatData", "ChatType")
class ChatType(IntEnum):
WHISPER = 0
NORMAL = 1
SHOUT = 2
OOC = 3
TYPING_START = 4
TYPING_STOP = 5
DEBUG_MSG = 6
REGION = 7
OWNER = 8
DIRECT = 9
IM = 10
IM_GROUP = 11
RADAR = 12
@se.enum_field_serializer("ChatFromSimulator", "ChatData", "SourceType")
class ChatSourceType(IntEnum):
SYSTEM = 0
AGENT = 1
OBJECT = 2
UNKNOWN = 3
@dataclasses.dataclass
class ThrottleData:
resend: float = se.dataclass_field(se.F32)
land: float = se.dataclass_field(se.F32)
wind: float = se.dataclass_field(se.F32)
cloud: float = se.dataclass_field(se.F32)
task: float = se.dataclass_field(se.F32)
texture: float = se.dataclass_field(se.F32)
asset: float = se.dataclass_field(se.F32)
@se.subfield_serializer("AgentThrottle", "Throttle", "Throttles")
class AgentThrottlesSerializer(se.SimpleSubfieldSerializer):
TEMPLATE = se.Dataclass(ThrottleData)
@se.subfield_serializer("ObjectUpdate", "ObjectData", "NameValue")
class NameValueSerializer(se.SimpleSubfieldSerializer):
TEMPLATE = NAMEVALUES_TERMINATED_TEMPLATE
@se.enum_field_serializer("SetFollowCamProperties", "CameraProperty", "Type")
class CameraPropertyType(IntEnum):
PITCH = 0
FOCUS_OFFSET = 1
FOCUS_OFFSET_X = 2
FOCUS_OFFSET_Y = 3
FOCUS_OFFSET_Z = 4
POSITION_LAG = 5
FOCUS_LAG = 6
DISTANCE = 7
BEHINDNESS_ANGLE = 8
BEHINDNESS_LAG = 9
POSITION_THRESHOLD = 10
FOCUS_THRESHOLD = 11
ACTIVE = 12
POSITION = 13
POSITION_X = 14
POSITION_Y = 15
POSITION_Z = 16
FOCUS = 17
FOCUS_X = 18
FOCUS_Y = 19
FOCUS_Z = 20
POSITION_LOCKED = 21
FOCUS_LOCKED = 22
@se.enum_field_serializer("DeRezObject", "AgentBlock", "Destination")
class DeRezObjectDestination(IntEnum):
SAVE_INTO_AGENT_INVENTORY = 0 # deprecated, disabled
ACQUIRE_TO_AGENT_INVENTORY = 1 # try to leave copy in world
SAVE_INTO_TASK_INVENTORY = 2
ATTACHMENT = 3 # deprecated
TAKE_INTO_AGENT_INVENTORY = 4 # delete from world
FORCE_TO_GOD_INVENTORY = 5 # force take copy
TRASH = 6
ATTACHMENT_TO_INV = 7 # deprecated
ATTACHMENT_EXISTS = 8 # deprecated
RETURN_TO_OWNER = 9 # back to owner's inventory
RETURN_TO_LAST_OWNER = 10 # deeded object back to last owner's inventory, deprecated
@se.flag_field_serializer("RegionHandshake", "RegionInfo", "RegionFlags")
@se.flag_field_serializer("RegionHandshake", "RegionInfo4", "RegionFlagsExtended")
@se.flag_field_serializer("SimStats", "Region", "RegionFlags")
@se.flag_field_serializer("SimStats", "RegionInfo", "RegionFlagsExtended")
@se.flag_field_serializer("RegionInfo", "RegionInfo", "RegionFlags")
@se.flag_field_serializer("RegionInfo", "RegionInfo3", "RegionFlagsExtended")
@se.flag_field_serializer("MapBlockReply", "Data", "RegionFlags")
class RegionFlags(IntFlag):
ALLOW_DAMAGE = 1 << 0
ALLOW_LANDMARK = 1 << 1
ALLOW_SET_HOME = 1 << 2
# Do we reset the home position when someone teleports away from here?
RESET_HOME_ON_TELEPORT = 1 << 3
SUN_FIXED = 1 << 4 # Does the sun move?
# Does the estate owner allow private parcels?
ALLOW_ACCESS_OVERRIDE = 1 << 5
BLOCK_TERRAFORM = 1 << 6
BLOCK_LAND_RESELL = 1 << 7
SANDBOX = 1 << 8 # All content wiped once per night
ALLOW_ENVIRONMENT_OVERRIDE = 1 << 9
SKIP_COLLISIONS = 1 << 12 # Pin all non agent rigid bodies
SKIP_SCRIPTS = 1 << 13
SKIP_PHYSICS = 1 << 14
EXTERNALLY_VISIBLE = 1 << 15
ALLOW_RETURN_ENCROACHING_OBJECT = 1 << 16
ALLOW_RETURN_ENCROACHING_ESTATE_OBJECT = 1 << 17
BLOCK_DWELL = 1 << 18
BLOCK_FLY = 1 << 19
ALLOW_DIRECT_TELEPORT = 1 << 20
# Is there an administrative override on scripts in the region at the
# moment. This is the similar skip scripts, except this flag is
# presisted in the database on an estate level.
ESTATE_SKIP_SCRIPTS = 1 << 21
RESTRICT_PUSHOBJECT = 1 << 22
DENY_ANONYMOUS = 1 << 23
ALLOW_PARCEL_CHANGES = 1 << 26
BLOCK_FLYOVER = 1 << 27
ALLOW_VOICE = 1 << 28
BLOCK_PARCEL_SEARCH = 1 << 29
DENY_AGEUNVERIFIED = 1 << 30
@se.flag_field_serializer("RegionHandshakeReply", "RegionInfo", "Flags")
class RegionHandshakeReplyFlags(IntFlag):
VOCACHE_CULLING_ENABLED = 0x1 # ask sim to send all cacheable objects.
VOCACHE_IS_EMPTY = 0x2 # the cache file is empty, no need to send cache probes.
SUPPORTS_SELF_APPEARANCE = 0x4 # inbound AvatarAppearance for self is ok
@se.flag_field_serializer("TeleportStart", "Info", "TeleportFlags")
@se.flag_field_serializer("TeleportProgress", "Info", "TeleportFlags")
@se.flag_field_serializer("TeleportFinish", "Info", "TeleportFlags")
@se.flag_field_serializer("TeleportLocal", "Info", "TeleportFlags")
@se.flag_field_serializer("TeleportLureRequest", "Info", "TeleportFlags")
class TeleportFlags(IntFlag):
SET_HOME_TO_TARGET = 1 << 0 # newbie leaving prelude (starter area)
SET_LAST_TO_TARGET = 1 << 1
VIA_LURE = 1 << 2
VIA_LANDMARK = 1 << 3
VIA_LOCATION = 1 << 4
VIA_HOME = 1 << 5
VIA_TELEHUB = 1 << 6
VIA_LOGIN = 1 << 7
VIA_GODLIKE_LURE = 1 << 8
GODLIKE = 1 << 9
NINE_ONE_ONE = 1 << 10 # What is this?
DISABLE_CANCEL = 1 << 11 # Used for llTeleportAgentHome()
VIA_REGION_ID = 1 << 12
IS_FLYING = 1 << 13
SHOW_RESET_HOME = 1 << 14
FORCE_REDIRECT = 1 << 15
VIA_GLOBAL_COORDS = 1 << 16
WITHIN_REGION = 1 << 17
@se.flag_field_serializer("AvatarPropertiesReply", "PropertiesData", "Flags")
class AvatarPropertiesFlags(IntFlag):
ALLOW_PUBLISH = 1 << 0 # whether profile is externally visible or not
MATURE_PUBLISH = 1 << 1 # profile is "mature"
IDENTIFIED = 1 << 2 # whether avatar has provided payment info
TRANSACTED = 1 << 3 # whether avatar has actively used payment info
ONLINE = 1 << 4 # the online status of this avatar, if known.
AGEVERIFIED = 1 << 5 # whether avatar has been age-verified
@se.flag_field_serializer("AvatarGroupsReply", "GroupData", "GroupPowers")
@se.flag_field_serializer("AgentGroupDataUpdate", "GroupData", "GroupPowers")
@se.flag_field_serializer("AgentDataUpdate", "AgentData", "GroupPowers")
@se.flag_field_serializer("GroupProfileReply", "GroupData", "PowersMask")
@se.flag_field_serializer("GroupRoleDataReply", "RoleData", "Powers")
class GroupPowerFlags(IntFlag):
MEMBER_INVITE = 1 << 1 # Invite member
MEMBER_EJECT = 1 << 2 # Eject member from group
MEMBER_OPTIONS = 1 << 3 # Toggle "Open enrollment" and change "Signup Fee"
MEMBER_VISIBLE_IN_DIR = 1 << 47
# Roles
ROLE_CREATE = 1 << 4 # Create new roles
ROLE_DELETE = 1 << 5 # Delete roles
ROLE_PROPERTIES = 1 << 6 # Change Role Names, Titles, and Descriptions
ROLE_ASSIGN_MEMBER_LIMITED = 1 << 7 # Assign Member to a Role that the assigner is in
ROLE_ASSIGN_MEMBER = 1 << 8 # Assign Member to Role
ROLE_REMOVE_MEMBER = 1 << 9 # Remove Member from Role
ROLE_CHANGE_ACTIONS = 1 << 10 # Change actions a role can perform
# Group Identity
GROUP_CHANGE_IDENTITY = 1 << 11 # Charter, insignia, 'Show In Group List', 'Publish on the web', 'Mature', etc.
# Parcel Management
LAND_DEED = 1 << 12 # Deed Land and Buy Land for Group
LAND_RELEASE = 1 << 13 # Release Land (to Gov. Linden)
# Set for sale info (Toggle "For Sale", Set Price, Set Target, Toggle "Sell objects with the land")
LAND_SET_SALE_INFO = 1 << 14
LAND_DIVIDE_JOIN = 1 << 15 # Divide and Join Parcels
# Parcel Identity
LAND_FIND_PLACES = 1 << 17 # Toggle "Show in Find Places" and Set Category.
# Change Parcel Identity: Parcel Name, Parcel Description, Snapshot, 'Publish on the web', and 'Mature' checkbox
LAND_CHANGE_IDENTITY = 1 << 18
LAND_SET_LANDING_POINT = 1 << 19 # Set Landing Point
# Parcel Settings
LAND_CHANGE_MEDIA = 1 << 20 # Change Media Settings
LAND_EDIT = 1 << 21 # Toggle Edit Land
# Toggle Set Home Point, Fly, Outside Scripts, Create/Edit Objects, Landmark, and Damage checkboxes
LAND_OPTIONS = 1 << 22
# Parcel Powers
LAND_ALLOW_EDIT_LAND = 1 << 23 # Bypass Edit Land Restriction
LAND_ALLOW_FLY = 1 << 24 # Bypass Fly Restriction
LAND_ALLOW_CREATE = 1 << 25 # Bypass Create/Edit Objects Restriction
LAND_ALLOW_LANDMARK = 1 << 26 # Bypass Landmark Restriction
LAND_ALLOW_SET_HOME = 1 << 28 # Bypass Set Home Point Restriction
LAND_ALLOW_HOLD_EVENT = 1 << 41 # Allowed to hold events on group-owned land
LAND_ALLOW_ENVIRONMENT = 1 << 46 # Allowed to change the environment
# Parcel Access
LAND_MANAGE_ALLOWED = 1 << 29 # Manage Allowed List
LAND_MANAGE_BANNED = 1 << 30 # Manage Banned List
LAND_MANAGE_PASSES = 1 << 31 # Change Sell Pass Settings
LAND_ADMIN = 1 << 32 # Eject and Freeze Users on the land
# Parcel Content
LAND_RETURN_GROUP_SET = 1 << 33 # Return objects on parcel that are set to group
LAND_RETURN_NON_GROUP = 1 << 34 # Return objects on parcel that are not set to group
LAND_RETURN_GROUP_OWNED = 1 << 48 # Return objects on parcel that are owned by the group
LAND_GARDENING = 1 << 35 # Parcel Gardening - plant and move linden trees
# Object Management
OBJECT_DEED = 1 << 36 # Deed Object
OBJECT_MANIPULATE = 1 << 38 # Manipulate Group Owned Objects (Move, Copy, Mod)
OBJECT_SET_SALE = 1 << 39 # Set Group Owned Object for Sale
# Accounting
ACCOUNTING_ACCOUNTABLE = 1 << 40 # Pay Group Liabilities and Receive Group Dividends
# Notices
NOTICES_SEND = 1 << 42 # Send Notices
NOTICES_RECEIVE = 1 << 43 # Receive Notices and View Notice History
# Proposals
# TODO: _DEPRECATED suffix as part of vote removal - DEV-24856:
PROPOSAL_START = 1 << 44 # Start Proposal
# TODO: _DEPRECATED suffix as part of vote removal - DEV-24856:
PROPOSAL_VOTE = 1 << 45 # Vote on Proposal
# Group chat moderation related
SESSION_JOIN = 1 << 16 # can join session
SESSION_VOICE = 1 << 27 # can hear/talk
SESSION_MODERATOR = 1 << 37 # can mute people's session
EXPERIENCE_ADMIN = 1 << 49 # has admin rights to any experiences owned by this group
EXPERIENCE_CREATOR = 1 << 50 # can sign scripts for experiences owned by this group
# Group Banning
GROUP_BAN_ACCESS = 1 << 51 # Allows access to ban / un-ban agents from a group.
@se.flag_field_serializer("GrantUserRights", "Rights", "RelatedRights")
@se.flag_field_serializer("ChangeUserRights", "Rights", "RelatedRights")
class UserRelatedRights(IntFlag):
"""See lluserrelations.h for definitions"""
ONLINE_STATUS = 1
MAP_LOCATION = 1 << 1
MODIFY_OBJECTS = 1 << 2
@se.flag_field_serializer("RequestObjectPropertiesFamily", "ObjectData", "RequestFlags")
@se.flag_field_serializer("ObjectPropertiesFamily", "ObjectData", "RequestFlags")
class ObjectPropertiesFamilyRequestFlags(IntFlag):
BUG_REPORT = 1 << 0
COMPLAINT_REPORT = 1 << 1
OBJECT_PAY = 1 << 2
@se.enum_field_serializer("RequestImage", "RequestImage", "Type")
class RequestImageType(IntEnum):
NORMAL = 0
AVATAR_BAKE = 1
@se.enum_field_serializer("ImageData", "ImageID", "Codec")
class ImageCodec(IntEnum):
INVALID = 0
RGB = 1
J2C = 2
BMP = 3
TGA = 4
JPEG = 5
DXT = 6
PNG = 7
@se.enum_field_serializer("LayerData", "LayerID", "Type")
class LayerDataType(IntEnum):
LAND_LAYER_CODE = ord('L')
WIND_LAYER_CODE = ord('7')
CLOUD_LAYER_CODE = ord('8')
WATER_LAYER_CODE = ord('W')
# <FS:CR> Aurora Sim
# Extended land layer for Aurora Sim
AURORA_LAND_LAYER_CODE = ord('M')
AURORA_WATER_LAYER_CODE = ord('X')
AURORA_WIND_LAYER_CODE = ord('9')
AURORA_CLOUD_LAYER_CODE = ord(':')
@se.enum_field_serializer("ModifyLand", "ModifyBlock", "Action")
class ModifyLandAction(IntEnum):
LEVEL = 0
RAISE = 1
LOWER = 2
SMOOTH = 3
NOISE = 4
REVERT = 5
@se.flag_field_serializer("RevokePermissions", "Data", "ObjectPermissions")
@se.flag_field_serializer("ScriptQuestion", "Data", "Questions")
@se.flag_field_serializer("ScriptAnswerYes", "Data", "Questions")
class ScriptPermissions(IntFlag):
# "1" itself seems to be unused?
TAKE_MONEY = 1 << 1
TAKE_CONTROLS = 1 << 2
# Doesn't seem to be used?
REMAP_CONTROLS = 1 << 3
TRIGGER_ANIMATIONS = 1 << 4
ATTACH = 1 << 5
# Doesn't seem to be used?
RELEASE_OWNERSHIP = 1 << 6
CHANGE_LINKS = 1 << 7
# Object joints don't exist anymore
CHANGE_JOINTS = 1 << 8
# Change its own permissions? Doesn't seem to be used.
CHANGE_PERMISSIONS = 1 << 9
TRACK_CAMERA = 1 << 10
CONTROL_CAMERA = 1 << 11
TELEPORT = 1 << 12
JOIN_EXPERIENCE = 1 << 13
MANAGE_ESTATE_ACCESS = 1 << 14
ANIMATION_OVERRIDE = 1 << 15
RETURN_OBJECTS = 1 << 16
FORCE_SIT = 1 << 17
CHANGE_ENVIRONMENT = 1 << 18
@se.flag_field_serializer("ParcelProperties", "ParcelData", "ParcelFlags")
class ParcelFlags(IntFlag):
ALLOW_FLY = 1 << 0 # Can start flying
ALLOW_OTHER_SCRIPTS = 1 << 1 # Scripts by others can run.
FOR_SALE = 1 << 2 # Can buy this land
FOR_SALE_OBJECTS = 1 << 7 # Can buy all objects on this land
ALLOW_LANDMARK = 1 << 3 # Always true/deprecated
ALLOW_TERRAFORM = 1 << 4
ALLOW_DAMAGE = 1 << 5
CREATE_OBJECTS = 1 << 6
# 7 is moved above
USE_ACCESS_GROUP = 1 << 8
USE_ACCESS_LIST = 1 << 9
USE_BAN_LIST = 1 << 10
USE_PASS_LIST = 1 << 11
SHOW_DIRECTORY = 1 << 12
ALLOW_DEED_TO_GROUP = 1 << 13
CONTRIBUTE_WITH_DEED = 1 << 14
SOUND_LOCAL = 1 << 15 # Hear sounds in this parcel only
SELL_PARCEL_OBJECTS = 1 << 16 # Objects on land are included as part of the land when the land is sold
ALLOW_PUBLISH = 1 << 17 # Allow publishing of parcel information on the web
MATURE_PUBLISH = 1 << 18 # The information on this parcel is mature
URL_WEB_PAGE = 1 << 19 # The "media URL" is an HTML page
URL_RAW_HTML = 1 << 20 # The "media URL" is a raw HTML string like <H1>Foo</H1>
RESTRICT_PUSHOBJECT = 1 << 21 # Restrict push object to either on agent or on scripts owned by parcel owner
DENY_ANONYMOUS = 1 << 22 # Deny all non identified/transacted accounts
# DENY_IDENTIFIED = 1 << 23 # Deny identified accounts
# DENY_TRANSACTED = 1 << 24 # Deny identified accounts
ALLOW_GROUP_SCRIPTS = 1 << 25 # Allow scripts owned by group
CREATE_GROUP_OBJECTS = 1 << 26 # Allow object creation by group members or objects
ALLOW_ALL_OBJECT_ENTRY = 1 << 27 # Allow all objects to enter a parcel
ALLOW_GROUP_OBJECT_ENTRY = 1 << 28 # Only allow group (and owner) objects to enter the parcel
ALLOW_VOICE_CHAT = 1 << 29 # Allow residents to use voice chat on this parcel
USE_ESTATE_VOICE_CHAN = 1 << 30
DENY_AGEUNVERIFIED = 1 << 31 # Prevent residents who aren't age-verified
@se.enum_field_serializer("UpdateMuteListEntry", "MuteData", "MuteType")
class MuteType(IntEnum):
BY_NAME = 0
AGENT = 1
OBJECT = 2
GROUP = 3
# Voice, presumably.
EXTERNAL = 4
@se.flag_field_serializer("UpdateMuteListEntry", "MuteData", "MuteFlags")
class MuteFlags(IntFlag):
# For backwards compatibility (since any mute list entries that were created before the flags existed
# will have a flags field of 0), some flags are "inverted".
# Note that it's possible, through flags, to completely disable an entry in the mute list.
# The code should detect this case and remove the mute list entry instead.
TEXT_CHAT = 1 << 0
VOICE_CHAT = 1 << 1
PARTICLES = 1 << 2
OBJECT_SOUNDS = 1 << 3
@property
def DEFAULT(self):
return 0x0
@property
def ALL(self):
return 0xF
class DateAdapter(se.Adapter):
def __init__(self, multiplier: int = 1):
super(DateAdapter, self).__init__(None)
self._multiplier = multiplier
def decode(self, val: Any, ctx: Optional[se.ParseContext], pod: bool = False) -> Any:
return datetime.datetime.fromtimestamp(val / self._multiplier).isoformat()
def encode(self, val: Any, ctx: Optional[se.ParseContext]) -> Any:
return int(datetime.datetime.fromisoformat(val).timestamp() * self._multiplier)
@se.enum_field_serializer("MeanCollisionAlert", "MeanCollision", "Type")
class MeanCollisionType(IntEnum):
INVALID = 0
BUMP = enum.auto()
LLPUSHOBJECT = enum.auto()
SELECTED_OBJECT_COLLIDE = enum.auto()
SCRIPTED_OBJECT_COLLIDE = enum.auto()
PHYSICAL_OBJECT_COLLIDE = enum.auto()
@se.subfield_serializer("ObjectProperties", "ObjectData", "CreationDate")
class ObjectCreationDateSerializer(se.AdapterSubfieldSerializer):
ADAPTER = DateAdapter(1_000_000)
ORIG_INLINE = True
@se.subfield_serializer("MeanCollisionAlert", "MeanCollision", "Time")
@se.subfield_serializer("ParcelProperties", "ParcelData", "ClaimDate")
@se.subfield_serializer("BulkUpdateInventory", "ItemData", "CreationDate")
class DateSerializer(se.AdapterSubfieldSerializer):
ADAPTER = DateAdapter()
ORIG_INLINE = True
class ParcelGridType(IntEnum):
PUBLIC = 0x00
OWNED = 0x01 # Presumably non-linden owned land
GROUP = 0x02
SELF = 0x03
FOR_SALE = 0x04
AUCTION = 0x05
class ParcelGridFlags(IntFlag):
UNUSED = 0x8
HIDDEN_AVS = 0x10
SOUND_LOCAL = 0x20
WEST_LINE = 0x40
SOUTH_LINE = 0x80
@dataclasses.dataclass
class ParcelGridInfo(se.BitfieldDataclass):
PRIM_SPEC: ClassVar[se.SerializablePrimitive] = se.U8
SHIFT: ClassVar[bool] = False
Type: Union[ParcelGridType, int] = se.bitfield_field(bits=3, adapter=se.IntEnum(ParcelGridType))
Flags: ParcelGridFlags = se.bitfield_field(bits=5, adapter=se.IntFlag(ParcelGridFlags))
@se.subfield_serializer("ParcelOverlay", "ParcelData", "Data")
class ParcelOverlaySerializer(se.SimpleSubfieldSerializer):
TEMPLATE = se.Collection(None, se.BitfieldDataclass(ParcelGridInfo))
class BitmapAdapter(se.Adapter):
def __init__(self, shape: Tuple[int, int]):
super().__init__(None)
self._shape = shape
def encode(self, val: Any, ctx: Optional[ParseContext]) -> Any:
if val and isinstance(val[0], bytes):
return b''.join(val)
return np.packbits(np.array(val, dtype=np.uint8).flatten(), bitorder="little").tobytes()
def decode(self, val: Any, ctx: Optional[ParseContext], pod: bool = False) -> Any:
if pod:
return [val[i:i + (self._shape[1] // 8)] for i in range(0, len(val), (self._shape[1] // 8))]
parcel_bitmap = np.frombuffer(val, dtype=np.uint8)
# This is a boolean array where each bit says whether the parcel occupies that grid.
return np.unpackbits(parcel_bitmap, bitorder="little").reshape(self._shape)
@se.subfield_serializer("ParcelProperties", "ParcelData", "Bitmap")
class ParcelPropertiesBitmapSerializer(se.AdapterSubfieldSerializer):
"""Bitmap that describes which grids a parcel occupies"""
REGION_METERS = 256
METERS_PER_CELL = 4
ADAPTER = BitmapAdapter((REGION_METERS // METERS_PER_CELL, REGION_METERS // METERS_PER_CELL))
@se.enum_field_serializer("ParcelProperties", "ParcelData", "LandingType")
class LandingType(IntEnum):
NONE = 1
LANDING_POINT = 1
DIRECT = 2
@se.enum_field_serializer("ParcelProperties", "ParcelData", "Status")
class LandOwnershipStatus(IntEnum):
LEASED = 0
LEASE_PENDING = 1
ABANDONED = 2
NONE = -1
@se.enum_field_serializer("ParcelProperties", "ParcelData", "Category")
class LandCategory(IntEnum):
NONE = 0
LINDEN = enum.auto()
ADULT = enum.auto()
ARTS = enum.auto()
BUSINESS = enum.auto()
EDUCATIONAL = enum.auto()
GAMING = enum.auto()
HANGOUT = enum.auto()
NEWCOMER = enum.auto()
PARK = enum.auto()
RESIDENTIAL = enum.auto()
SHOPPING = enum.auto()
STAGE = enum.auto()
OTHER = enum.auto()
ANY = -1
@se.http_serializer("RenderMaterials")
class RenderMaterialsSerializer(se.BaseHTTPSerializer):
@classmethod
def deserialize_resp_body(cls, method: str, body: bytes):
deser = llsd.unzip_llsd(llsd.parse_xml(body)["Zipped"])
return deser
@classmethod
def deserialize_req_body(cls, method: str, body: bytes):
if not body:
return se.UNSERIALIZABLE
deser = llsd.unzip_llsd(llsd.parse_xml(body)["Zipped"])
return deser
@classmethod
def serialize_req_body(cls, method: str, body):
if body == b"":
return body
return llsd.format_xml({"Zipped": llsd.zip_llsd(body)})
@se.http_serializer("RetrieveNavMeshSrc")
class RetrieveNavMeshSrcSerializer(se.BaseHTTPSerializer):
@classmethod
def deserialize_resp_body(cls, method: str, body: bytes):
deser = llsd.parse_xml(body)
# 15 bit window size, gzip wrapped
deser["navmesh_data"] = zlib.decompress(deser["navmesh_data"], wbits=15 | 32)
return deser