More enumification in inventory code

This commit is contained in:
Salad Dais
2023-12-21 19:18:58 +00:00
parent 74e4e0c4ec
commit 16958e516d
4 changed files with 126 additions and 64 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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):

View File

@@ -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,
}