2432 lines
81 KiB
Python
2432 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, Quaternion
|
|
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")
|
|
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")
|
|
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
|
|
|
|
|
|
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")
|
|
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("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)]
|
|
|
|
|
|
@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")
|
|
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
|
|
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
|
|
|
|
|
|
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 = 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 CreationDateSerializer(se.AdapterSubfieldSerializer):
|
|
ADAPTER = DateAdapter(1_000_000)
|
|
ORIG_INLINE = True
|
|
|
|
|
|
@se.subfield_serializer("MeanCollisionAlert", "MeanCollision", "Time")
|
|
@se.subfield_serializer("ParcelProperties", "ParcelData", "ClaimDate")
|
|
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"""
|
|
ADAPTER = BitmapAdapter((256 // 4, 256 // 4))
|
|
|
|
|
|
@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
|
|
|
|
|
|
# Beta puppetry stuff, subject to change!
|
|
|
|
|
|
class PuppetryEventMask(IntFlag):
|
|
POSITION = 1 << 0
|
|
POSITION_IN_PARENT_FRAME = 1 << 1
|
|
ROTATION = 1 << 2
|
|
ROTATION_IN_PARENT_FRAME = 1 << 3
|
|
SCALE = 1 << 4
|
|
DISABLE_CONSTRAINT = 1 << 7
|
|
|
|
|
|
class PuppetryOption(se.OptionalFlagged):
|
|
def __init__(self, flag_val, spec):
|
|
super().__init__("mask", se.IntFlag(PuppetryEventMask, se.U8), flag_val, spec)
|
|
|
|
|
|
# Range to use for puppetry's quantized floats when converting to<->from U16
|
|
LL_PELVIS_OFFSET_RANGE = (-5.0, 5.0)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class PuppetryJointData:
|
|
# Where does this number come from? `avatar_skeleton.xml`?
|
|
joint_id: int = se.dataclass_field(se.S16)
|
|
# Determines which fields will follow
|
|
mask: PuppetryEventMask = se.dataclass_field(se.IntFlag(PuppetryEventMask, se.U8))
|
|
rotation: Optional[Quaternion] = se.dataclass_field(
|
|
# These are very odd scales for a quantized quaternion, but that's what they are.
|
|
PuppetryOption(PuppetryEventMask.ROTATION, se.PackedQuat(se.Vector3U16(*LL_PELVIS_OFFSET_RANGE))),
|
|
)
|
|
position: Optional[Vector3] = se.dataclass_field(
|
|
PuppetryOption(PuppetryEventMask.POSITION, se.Vector3U16(*LL_PELVIS_OFFSET_RANGE)),
|
|
)
|
|
scale: Optional[Vector3] = se.dataclass_field(
|
|
PuppetryOption(PuppetryEventMask.SCALE, se.Vector3U16(*LL_PELVIS_OFFSET_RANGE)),
|
|
)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class PuppetryEventData:
|
|
time: int = se.dataclass_field(se.S32)
|
|
# Must be set manually due to below issue
|
|
num_joints: int = se.dataclass_field(se.U16)
|
|
# This field is packed in the least helpful way possible. The length field
|
|
# is in between the collection count and the collection data, but the length
|
|
# field essentially only tells you how many bytes until the end of the buffer
|
|
# proper, which you already know from msgsystem. Why is this here?
|
|
joints: List[PuppetryJointData] = se.dataclass_field(se.TypedByteArray(
|
|
se.U32,
|
|
# Just treat contents as a greedy collection, tries to keep reading until EOF
|
|
se.Collection(None, se.Dataclass(PuppetryJointData)),
|
|
))
|
|
|
|
|
|
@se.subfield_serializer("AgentAnimation", "PhysicalAvatarEventList", "TypeData")
|
|
@se.subfield_serializer("AvatarAnimation", "PhysicalAvatarEventList", "TypeData")
|
|
class PuppetryEventDataSerializer(se.SimpleSubfieldSerializer):
|
|
# You can have multiple joint events packed in right after the other, implicitly.
|
|
# They may _or may not_ be split into separate PhysicalAvatarEventList blocks?
|
|
# This doesn't seem to be handled specifically in the decoder, is this a
|
|
# serialization bug in the viewer?
|
|
TEMPLATE = se.Collection(None, se.Dataclass(PuppetryEventData))
|
|
EMPTY_IS_NONE = True
|