diff --git a/hippolyzer/lib/base/inventory.py b/hippolyzer/lib/base/inventory.py index bdc689b..c1a984f 100644 --- a/hippolyzer/lib/base/inventory.py +++ b/hippolyzer/lib/base/inventory.py @@ -8,7 +8,6 @@ from __future__ import annotations import abc import dataclasses import datetime as dt -import enum import inspect import logging import struct @@ -32,6 +31,7 @@ from hippolyzer.lib.base.legacy_schema import ( SchemaUUID, schema_field, ) +from hippolyzer.lib.base.templates import SaleType, InventoryType, LegacyIntEnum, AssetType, FolderType MAGIC_ID = UUID("3c115e51-04f4-523c-9fa6-98aff1034730") LOG = logging.getLogger(__name__) @@ -41,29 +41,14 @@ _T = TypeVar("_T") class SchemaFlagField(SchemaHexInt): """Like a hex int, but must be serialized as bytes in LLSD due to being a U32""" @classmethod - def from_llsd(cls, val: Any) -> int: + def from_llsd(cls, val: Any, flavor: str) -> int: return struct.unpack("!I", val)[0] @classmethod - def to_llsd(cls, val: int) -> Any: + def to_llsd(cls, val: int, flavor: str) -> Any: return struct.pack("!I", val) -class LegacyIntEnum(enum.IntEnum): - _ignore_ = ['_LEGACY_NAMES'] - - @classmethod - def _get_legacy_names(cls) -> Tuple[str, ...]: - raise NotImplementedError() - - @classmethod - def from_legacy_name(cls, name: str): - return cls(cls._get_legacy_names().index(name)) - - def to_legacy_name(self) -> str: - return self._get_legacy_names()[self.value] - - class SchemaEnumField(SchemaStr, Generic[_T]): def __init__(self, enum_cls: Type[LegacyIntEnum]): super().__init__() @@ -73,12 +58,12 @@ class SchemaEnumField(SchemaStr, Generic[_T]): return self._enum_cls.from_legacy_name(val) def serialize(self, val: _T) -> str: - return self._enum_cls.to_legacy_name(val) + return self._enum_cls(val).to_legacy_name() - def from_llsd(self, val: str) -> _T: + def from_llsd(self, val: str, flavor: str) -> _T: return self.deserialize(val) - def to_llsd(self, val: _T) -> str: + def to_llsd(self, val: _T, flavor: str) -> str: return self.serialize(val) @@ -221,12 +206,12 @@ class InventoryModel(InventoryBase): return model @classmethod - def from_llsd(cls, llsd_val: List[Dict]) -> InventoryModel: + def from_llsd(cls, llsd_val: List[Dict], flavor: str = "legacy") -> InventoryModel: model = cls() for obj_dict in llsd_val: for inv_type in INVENTORY_TYPES: if inv_type.ID_ATTR in obj_dict: - if (obj := inv_type.from_llsd(obj_dict)) is not None: + if (obj := inv_type.from_llsd(obj_dict, flavor)) is not None: model.add(obj) break LOG.warning(f"Unknown object type {obj_dict!r}") @@ -258,8 +243,8 @@ class InventoryModel(InventoryBase): for node in self.ordered_nodes: node.to_writer(writer) - def to_llsd(self): - return list(node.to_llsd() for node in self.ordered_nodes) + def to_llsd(self, flavor: str = "legacy"): + return list(node.to_llsd(flavor) for node in self.ordered_nodes) def add(self, node: InventoryNodeBase): if node.node_id in self.nodes: @@ -338,17 +323,6 @@ class InventoryPermissions(InventoryBase): is_owner_group: Optional[int] = schema_field(SchemaInt, default=None, llsd_only=True) -class SaleType(LegacyIntEnum): - NOT = 0 - ORIGINAL = 1 - COPY = 2 - CONTENTS = 3 - - @classmethod - def _get_legacy_names(cls) -> Tuple[str, ...]: - return "not", "orig", "copy", "cntn" - - @dataclasses.dataclass class InventorySaleInfo(InventoryBase): SCHEMA_NAME: ClassVar[str] = "sale_info" @@ -413,7 +387,7 @@ class InventoryNodeBase(InventoryBase, _HasName): @dataclasses.dataclass class InventoryContainerBase(InventoryNodeBase): # TODO: Not a string in AIS - type: str = schema_field(SchemaStr) + type: AssetType = schema_field(SchemaEnumField(AssetType)) @property def children(self) -> Sequence[InventoryNodeBase]: @@ -442,8 +416,8 @@ class InventoryContainerBase(InventoryNodeBase): name=name, cat_id=UUID.random(), parent_id=self.node_id, - type="category", - pref_type="-1", + type=AssetType.CATEGORY, + pref_type=FolderType.NONE, owner_id=getattr(self, 'owner_id', UUID.ZERO), version=1, ) @@ -473,7 +447,8 @@ class InventoryCategory(InventoryContainerBase): VERSION_NONE: ClassVar[int] = -1 cat_id: UUID = schema_field(SchemaUUID) - pref_type: str = schema_field(SchemaStr, llsd_name="preferred_type") + # TODO: not a string in AIS + pref_type: FolderType = schema_field(SchemaEnumField(FolderType), llsd_name="preferred_type") name: str = schema_field(SchemaMultilineStr) owner_id: UUID = schema_field(SchemaUUID) version: int = schema_field(SchemaInt) @@ -492,9 +467,9 @@ class InventoryItem(InventoryNodeBase): asset_id: Optional[UUID] = schema_field(SchemaUUID, default=None) shadow_id: Optional[UUID] = schema_field(SchemaUUID, default=None) # TODO: Not a string in AIS - type: Optional[str] = schema_field(SchemaStr, default=None) + type: Optional[AssetType] = schema_field(SchemaEnumField(AssetType), default=None) # TODO: Not a string in AIS - inv_type: Optional[str] = schema_field(SchemaStr, default=None) + inv_type: Optional[InventoryType] = schema_field(SchemaEnumField(InventoryType), default=None) flags: Optional[int] = schema_field(SchemaFlagField, default=None) sale_info: Optional[InventorySaleInfo] = schema_field(InventorySaleInfo, default=None) name: Optional[str] = schema_field(SchemaMultilineStr, default=None) diff --git a/hippolyzer/lib/base/legacy_schema.py b/hippolyzer/lib/base/legacy_schema.py index ba48a39..1f2d39f 100644 --- a/hippolyzer/lib/base/legacy_schema.py +++ b/hippolyzer/lib/base/legacy_schema.py @@ -35,11 +35,11 @@ class SchemaFieldSerializer(abc.ABC, Generic[_T]): pass @classmethod - def from_llsd(cls, val: Any) -> _T: + def from_llsd(cls, val: Any, flavor: str) -> _T: return val @classmethod - def to_llsd(cls, val: _T) -> Any: + def to_llsd(cls, val: _T, flavor: str) -> Any: return val @@ -53,11 +53,11 @@ class SchemaDate(SchemaFieldSerializer[dt.datetime]): return str(calendar.timegm(val.utctimetuple())) @classmethod - def from_llsd(cls, val: Any) -> dt.datetime: + def from_llsd(cls, val: Any, flavor: str) -> dt.datetime: return dt.datetime.utcfromtimestamp(val) @classmethod - def to_llsd(cls, val: dt.datetime): + def to_llsd(cls, val: dt.datetime, flavor: str): return calendar.timegm(val.utctimetuple()) @@ -180,7 +180,7 @@ class SchemaBase(abc.ABC): return cls.from_str(data.decode("utf8")) @classmethod - def from_llsd(cls, inv_dict: Dict): + def from_llsd(cls, inv_dict: Dict, flavor: str = "legacy"): fields = cls._get_fields_dict(llsd=True) obj_dict = {} for key, val in inv_dict.items(): @@ -199,9 +199,9 @@ class SchemaBase(abc.ABC): # some kind of nested structure like sale_info if issubclass(spec_cls, SchemaBase): - obj_dict[key] = spec.from_llsd(val) + obj_dict[key] = spec.from_llsd(val, flavor) elif issubclass(spec_cls, SchemaFieldSerializer): - obj_dict[key] = spec.from_llsd(val) + obj_dict[key] = spec.from_llsd(val, flavor) else: raise ValueError(f"Unsupported spec for {key!r}, {spec!r}") else: @@ -217,7 +217,7 @@ class SchemaBase(abc.ABC): writer.seek(0) return writer.read() - def to_llsd(self): + def to_llsd(self, flavor: str = "legacy"): obj_dict = {} for field_name, field in self._get_fields_dict(llsd=True).items(): spec = field.metadata.get("spec") @@ -235,9 +235,9 @@ class SchemaBase(abc.ABC): # Some kind of nested structure like sale_info if isinstance(val, SchemaBase): - val = val.to_llsd() + val = val.to_llsd(flavor) elif issubclass(spec_cls, SchemaFieldSerializer): - val = spec.to_llsd(val) + val = spec.to_llsd(val, flavor) else: raise ValueError(f"Bad inventory spec {spec!r}") obj_dict[field_name] = val diff --git a/hippolyzer/lib/base/templates.py b/hippolyzer/lib/base/templates.py index eed6010..2d63f21 100644 --- a/hippolyzer/lib/base/templates.py +++ b/hippolyzer/lib/base/templates.py @@ -18,6 +18,17 @@ from hippolyzer.lib.base.datatypes import UUID, IntEnum, IntFlag, Vector3, Quate from hippolyzer.lib.base.namevalue import NameValuesSerializer +class LegacyIntEnum(IntEnum): + """Used for enums that have legacy string names, may be used in the legacy schema""" + @abc.abstractmethod + def to_legacy_name(self) -> str: + raise NotImplementedError() + + @classmethod + def from_legacy_name(cls, legacy_name: str): + raise NotImplementedError() + + @se.enum_field_serializer("RequestXfer", "XferID", "VFileType") @se.enum_field_serializer("AssetUploadRequest", "AssetBlock", "Type") @se.enum_field_serializer("AssetUploadComplete", "AssetBlock", "Type") @@ -26,7 +37,7 @@ from hippolyzer.lib.base.namevalue import NameValuesSerializer @se.enum_field_serializer("RezObject", "InventoryData", "Type") @se.enum_field_serializer("RezScript", "InventoryBlock", "Type") @se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "Type") -class AssetType(IntEnum): +class AssetType(LegacyIntEnum): TEXTURE = 0 SOUND = 1 CALLINGCARD = 2 @@ -47,7 +58,7 @@ class AssetType(IntEnum): GESTURE = 21 SIMSTATE = 22 LINK = 24 - LINK_FOLDER = 25 + FOLDER_LINK = 25 MARKETPLACE_FOLDER = 26 WIDGET = 40 PERSON = 45 @@ -62,8 +73,7 @@ class AssetType(IntEnum): UNKNOWN = 255 NONE = -1 - @property - def human_name(self): + def to_legacy_name(self) -> str: lower = self.name.lower() return { "animation": "animatn", @@ -71,8 +81,27 @@ class AssetType(IntEnum): "texture_tga": "txtr_tga", "image_tga": "img_tga", "sound_wav": "snd_wav", + "lsl_text": "lsltext", + "lsl_bytecode": "lslbyte", + "folder_link": "link_f", + "unknown": "invalid", }.get(lower, lower) + @classmethod + def from_legacy_name(cls, legacy_name: str): + reg_name = { + "animatn": "animation", + "callcard": "callingcard", + "txtr_tga": "texture_tga", + "img_tga": "image_tga", + "snd_wav": "sound_wav", + "lsltext": "lsl_text", + "lslbyte": "lsl_bytecode", + "invalid": "unknown", + "link_f": "folder_link", + }.get(legacy_name, legacy_name).upper() + return cls[reg_name] + @property def inventory_type(self): return { @@ -104,7 +133,7 @@ class AssetType(IntEnum): @se.enum_field_serializer("RezObject", "InventoryData", "InvType") @se.enum_field_serializer("RezScript", "InventoryBlock", "InvType") @se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "InvType") -class InventoryType(IntEnum): +class InventoryType(LegacyIntEnum): TEXTURE = 0 SOUND = 1 CALLINGCARD = 2 @@ -133,16 +162,23 @@ class InventoryType(IntEnum): UNKNOWN = 255 NONE = -1 - @property - def human_name(self): + def to_legacy_name(self) -> str: lower = self.name.lower() return { "callingcard": "callcard", "none": "-1", }.get(lower, lower) + @classmethod + def from_legacy_name(cls, legacy_name: str): + reg_name = { + "callcard": "callingcard", + "-1": "none", + }.get(legacy_name, legacy_name).upper() + return cls[reg_name] -class FolderType(IntEnum): + +class FolderType(LegacyIntEnum): TEXTURE = 0 SOUND = 1 CALLINGCARD = 2 @@ -161,6 +197,7 @@ class FolderType(IntEnum): 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. @@ -177,7 +214,7 @@ class FolderType(IntEnum): # Note: We actually *never* create folders with that type. This is used for icon override only. MARKETPLACE_VERSION = 55 SETTINGS = 56 - # Firestorm folders, may not actually exist + # Firestorm folders, may not actually exist in legacy schema FIRESTORM = 57 PHOENIX = 58 RLV = 59 @@ -185,6 +222,46 @@ class FolderType(IntEnum): MY_SUITCASE = 100 NONE = -1 + def to_legacy_name(self) -> str: + lower = self.name.lower() + return { + "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", + "none": "-1", + }.get(lower, lower) + + @classmethod + def from_legacy_name(cls, legacy_name: str): + reg_name = { + "callcard": "callingcard", + "lsltext": "lsl_text", + "animatn": "animation", + "snapshot": "snapshot_category", + "lstndfnd": "lost_and_found", + "ensemble": "ensemble_start", + "current": "current_outfit", + "my_otfts": "my_outfits", + "basic_rt": "basic_root", + "merchant": "marketplace_listings", + "stock": "marketplace_stock", + "version": "marketplace_version", + "suitcase": "my_suitcase", + "-1": "none", + }.get(legacy_name, legacy_name).upper() + return cls[reg_name] + @se.enum_field_serializer("AgentIsNowWearing", "WearableData", "WearableType") @se.enum_field_serializer("AgentWearablesUpdate", "WearableData", "WearableType") @@ -244,6 +321,9 @@ class Permissions(IntFlag): 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") @@ -252,12 +332,19 @@ class Permissions(IntFlag): @se.enum_field_serializer("RezObject", "InventoryData", "SaleType") @se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "SaleType") @se.enum_field_serializer("UpdateCreateInventoryItem", "InventoryData", "SaleType") -class SaleInfo(IntEnum): +class SaleType(LegacyIntEnum): NOT = 0 ORIGINAL = 1 COPY = 2 CONTENTS = 3 + @classmethod + def from_legacy_name(cls, legacy_name: str): + return cls(_SALE_TYPE_LEGACY_NAMES.index(legacy_name)) + + def to_legacy_name(self) -> str: + return _SALE_TYPE_LEGACY_NAMES[int(self.value)] + @se.flag_field_serializer("ParcelInfoReply", "Data", "Flags") class ParcelInfoFlags(IntFlag): diff --git a/hippolyzer/lib/client/asset_uploader.py b/hippolyzer/lib/client/asset_uploader.py index 10cc12e..45aa334 100644 --- a/hippolyzer/lib/client/asset_uploader.py +++ b/hippolyzer/lib/client/asset_uploader.py @@ -30,12 +30,12 @@ class AssetUploader: async def initiate_asset_upload(self, name: str, asset_type: AssetType, body: bytes, flags: Optional[int] = None) -> UploadToken: payload = { - "asset_type": asset_type.human_name, + "asset_type": asset_type.to_legacy_name(), "description": "(No Description)", "everyone_mask": 0, "group_mask": 0, "folder_id": UUID.ZERO, # Puts it in the default folder, I guess. Undocumented. - "inventory_type": asset_type.inventory_type.human_name, + "inventory_type": asset_type.inventory_type.to_legacy_name(), "name": name, "next_owner_mask": 581632, }