Files
Hippolyzer/hippolyzer/lib/base/templates.py
2021-06-12 10:26:52 +00:00

1653 lines
53 KiB
Python

"""
Serialization templates for structures used in LLUDP and HTTP bodies.
"""
import abc
import dataclasses
import enum
import importlib
import logging
import zlib
from typing import *
import hippolyzer.lib.base.serialization as se
from hippolyzer.lib.base import llsd
from hippolyzer.lib.base.datatypes import UUID, IntEnum, IntFlag
from hippolyzer.lib.base.namevalue import NameValuesSerializer
try:
importlib.reload(se) # type: ignore
except:
logging.exception("Failed to reload serialization lib")
@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")
class AssetType(IntEnum):
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
LINK_FOLDER = 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
UNKNOWN = 255
NONE = -1
@property
def human_name(self):
lower = self.name.lower()
return {
"animation": "animatn",
"callingcard": "callcard",
"texture_tga": "txtr_tga",
"image_tga": "img_tga",
"sound_wav": "snd_wav",
}.get(lower, lower)
@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,
}.get(self, AssetType.NONE)
@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")
class InventoryType(IntEnum):
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
UNKNOWN = 255
NONE = -1
@property
def human_name(self):
lower = self.name.lower()
return {
"callingcard": "callcard",
}.get(lower, lower)
@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
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("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")
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
@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("UpdateCreateInventoryItem", "InventoryData", "Flags")
@se.flag_field_serializer("UpdateTaskInventory", "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 attachment_point(self):
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 = enum.auto()
ASSET = enum.auto()
SIM_INV_ITEM = enum.auto()
SIM_ESTATE = enum.auto()
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 = enum.auto()
ASSET = enum.auto()
@se.enum_field_serializer("TransferInfo", "TransferInfo", "TargetType")
class TransferTargetType(IntEnum):
UNKNOWN = 0
FILE = enum.auto()
VFILE = enum.auto()
@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 = enum.auto()
CONNECTOR = enum.auto()
FLEXIBLE_OBJECT = enum.auto()
ANIMAL_CONTROLS = enum.auto()
LOCAL_ANIMATION_OBJECT = enum.auto()
CLOTH = enum.auto()
EFFECT_BEAM = enum.auto()
EFFECT_GLOW = enum.auto()
EFFECT_POINT = enum.auto()
EFFECT_TRAIL = enum.auto()
EFFECT_SPHERE = enum.auto()
EFFECT_SPIRAL = enum.auto()
EFFECT_EDIT = enum.auto()
EFFECT_LOOKAT = enum.auto()
EFFECT_POINTAT = enum.auto()
EFFECT_VOICE_VISUALIZER = enum.auto()
NAME_TAG = enum.auto()
EFFECT_BLOB = enum.auto()
class LookAtTarget(IntEnum):
NONE = 0
IDLE = enum.auto()
AUTO_LISTEN = enum.auto()
FREELOOK = enum.auto()
RESPOND = enum.auto()
HOVER = enum.auto()
CONVERSATION = enum.auto()
SELECT = enum.auto()
FOCUS = enum.auto()
MOUSELOOK = enum.auto()
CLEAR = enum.auto()
class PointAtTarget(IntEnum):
NONE = 0
SELECT = enum.auto()
GRAB = enum.auto()
CLEAR = enum.auto()
@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
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")
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
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.)
dataclasses.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("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
class BasicMaterials:
# Meaning is technically implementation-dependent, these are in LL data files
Bump: int = se.bitfield_field(bits=5)
FullBright: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter())
Shiny: int = se.bitfield_field(bits=2, adapter=se.IntEnum(ShineLevel))
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
class MediaFlags:
WebPage: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter())
TexGen: "TexGen" = se.bitfield_field(bits=2, adapter=se.IntEnum(TexGen))
# Probably unused but show it just in case
_Unused: int = se.bitfield_field(bits=5)
# 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
def _te_field(spec: se.SERIALIZABLE_TYPE, first=False, optional=False,
default_factory=dataclasses.MISSING, default=dataclasses.MISSING):
if default_factory is not dataclasses.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,
)
_T = TypeVar("_T")
TE_FIELD_TYPE = Dict[Optional[Sequence[int]], _T]
@dataclasses.dataclass
class TextureEntry:
Textures: TE_FIELD_TYPE[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_FIELD_TYPE[bytes] = _te_field(Color4(invert_bytes=True), default=b"\xff\xff\xff\xff")
ScalesS: TE_FIELD_TYPE[float] = _te_field(se.F32, default=1.0)
ScalesT: TE_FIELD_TYPE[float] = _te_field(se.F32, default=1.0)
OffsetsS: TE_FIELD_TYPE[int] = _te_field(se.S16, default=0)
OffsetsT: TE_FIELD_TYPE[int] = _te_field(se.S16, default=0)
Rotation: TE_FIELD_TYPE[int] = _te_field(se.S16, default=0)
BasicMaterials: TE_FIELD_TYPE["BasicMaterials"] = _te_field(
BUMP_SHINY_FULLBRIGHT, default_factory=lambda: BasicMaterials(Bump=0, FullBright=False, Shiny=0),
)
MediaFlags: TE_FIELD_TYPE["MediaFlags"] = _te_field(
MEDIA_FLAGS,
default_factory=lambda: MediaFlags(WebPage=False, TexGen=TexGen.DEFAULT, _Unused=0),
)
Glow: TE_FIELD_TYPE[int] = _te_field(se.U8, default=0)
Materials: TE_FIELD_TYPE[UUID] = _te_field(se.UUID, optional=True, default=UUID())
TE_SERIALIZER = se.Dataclass(TextureEntry)
@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 = TE_SERIALIZER
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
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())
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),
}),
}
@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
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.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
@se.subfield_serializer("AgentThrottle", "Throttle", "Throttles")
class AgentThrottlesSerializer(se.SimpleSubfieldSerializer):
TEMPLATE = se.Collection(None, se.F32)
@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 = enum.auto()
FOCUS_OFFSET_X = enum.auto()
FOCUS_OFFSET_Y = enum.auto()
FOCUS_OFFSET_Z = enum.auto()
POSITION_LAG = enum.auto()
FOCUS_LAG = enum.auto()
DISTANCE = enum.auto()
BEHINDNESS_ANGLE = enum.auto()
BEHINDNESS_LAG = enum.auto()
POSITION_THRESHOLD = enum.auto()
FOCUS_THRESHOLD = enum.auto()
ACTIVE = enum.auto()
POSITION = enum.auto()
POSITION_X = enum.auto()
POSITION_Y = enum.auto()
POSITION_Z = enum.auto()
FOCUS = enum.auto()
FOCUS_X = enum.auto()
FOCUS_Y = enum.auto()
FOCUS_Z = enum.auto()
POSITION_LOCKED = enum.auto()
FOCUS_LOCKED = enum.auto()
@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")
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("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
@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