More inventory / wearables updates

This commit is contained in:
Salad Dais
2025-06-13 09:26:42 +00:00
parent 0dbba40fe1
commit c8dc67ea37
5 changed files with 93 additions and 3 deletions

View File

@@ -389,6 +389,21 @@ class InventoryPermissions(InventoryBase):
# It's kind of redundant since it just means owner_id == NULL_KEY && group_id != NULL_KEY. # It's kind of redundant since it just means owner_id == NULL_KEY && group_id != NULL_KEY.
is_owner_group: Optional[int] = schema_field(SchemaInt, default=None, llsd_only=True) is_owner_group: Optional[int] = schema_field(SchemaInt, default=None, llsd_only=True)
@classmethod
def make_default(cls) -> Self:
return cls(
base_mask=0xFFffFFff,
owner_mask=0xFFffFFff,
group_mask=0,
everyone_mask=0,
next_owner_mask=0x82000,
creator_id=UUID.ZERO,
owner_id=UUID.ZERO,
last_owner_id=UUID.ZERO,
group_id=UUID.ZERO,
is_owner_group=None
)
@dataclasses.dataclass @dataclasses.dataclass
class InventorySaleInfo(InventoryBase): class InventorySaleInfo(InventoryBase):
@@ -397,6 +412,10 @@ class InventorySaleInfo(InventoryBase):
sale_type: SaleType = schema_field(SchemaEnumField(SaleType)) sale_type: SaleType = schema_field(SchemaEnumField(SaleType))
sale_price: int = schema_field(SchemaInt) sale_price: int = schema_field(SchemaInt)
@classmethod
def make_default(cls) -> Self:
return cls(sale_type=SaleType.NOT, sale_price=10)
class _HasBaseNodeAttrs(abc.ABC): class _HasBaseNodeAttrs(abc.ABC):
""" """

View File

@@ -35,6 +35,12 @@ class HippoLLSDXMLFormatter(base_llsd.serde_xml.LLSDXMLFormatter, HippoLLSDBaseF
def __init__(self): def __init__(self):
super().__init__() super().__init__()
def _generate(self, something):
if isinstance(something, int) and type(something) is not int:
# The lookup in the underlying library will fail if we don't convert IntEnums to actual ints.
something = int(something)
return super()._generate(something)
class HippoLLSDXMLPrettyFormatter(base_llsd.serde_xml.LLSDXMLPrettyFormatter, HippoLLSDBaseFormatter): class HippoLLSDXMLPrettyFormatter(base_llsd.serde_xml.LLSDXMLPrettyFormatter, HippoLLSDBaseFormatter):
def __init__(self): def __init__(self):

View File

@@ -16,7 +16,7 @@ from hippolyzer.lib.base.datatypes import UUID
from hippolyzer.lib.base.helpers import get_resource_filename from hippolyzer.lib.base.helpers import get_resource_filename
from hippolyzer.lib.base.inventory import InventorySaleInfo, InventoryPermissions from hippolyzer.lib.base.inventory import InventorySaleInfo, InventoryPermissions
from hippolyzer.lib.base.legacy_schema import SchemaBase, parse_schema_line, SchemaParsingError from hippolyzer.lib.base.legacy_schema import SchemaBase, parse_schema_line, SchemaParsingError
from hippolyzer.lib.base.templates import WearableType from hippolyzer.lib.base.templates import WearableType, AssetType
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
_T = TypeVar("_T") _T = TypeVar("_T")
@@ -77,6 +77,12 @@ class AvatarTEIndex(enum.IntEnum):
def is_baked(self) -> bool: def is_baked(self) -> bool:
return self.name.endswith("_BAKED") return self.name.endswith("_BAKED")
@property
def asset_type(self) -> AssetType:
if self in (WearableType.HAIR, WearableType.SKIN, WearableType.EYES, WearableType.SHAPE):
return AssetType.BODYPART
return AssetType.CLOTHING
@dataclasses.dataclass @dataclasses.dataclass
class VisualParam: class VisualParam:
@@ -84,6 +90,7 @@ class VisualParam:
name: str name: str
value_min: float value_min: float
value_max: float value_max: float
value_default: float
# These might be `None` if the param isn't meant to be directly edited # These might be `None` if the param isn't meant to be directly edited
edit_group: Optional[str] edit_group: Optional[str]
wearable: Optional[str] wearable: Optional[str]
@@ -102,6 +109,7 @@ class VisualParams(List[VisualParam]):
wearable=param.get("wearable"), wearable=param.get("wearable"),
value_min=float(param.attrib["value_min"]), value_min=float(param.attrib["value_min"]),
value_max=float(param.attrib["value_max"]), value_max=float(param.attrib["value_max"]),
value_default=float(param.attrib.get("value_default", 0.0))
)) ))
def by_name(self, name: str) -> VisualParam: def by_name(self, name: str) -> VisualParam:
@@ -120,6 +128,34 @@ class VisualParams(List[VisualParam]):
VISUAL_PARAMS = VisualParams(get_resource_filename("lib/base/data/avatar_lad.xml")) VISUAL_PARAMS = VisualParams(get_resource_filename("lib/base/data/avatar_lad.xml"))
# See `llpaneleditwearable.cpp`, which TE slots should be set for each wearable type is hardcoded
# in the viewer.
WEARABLE_TEXTURE_SLOTS: Dict[WearableType, Sequence[AvatarTEIndex]] = {
WearableType.SHAPE: (),
WearableType.SKIN: (AvatarTEIndex.HEAD_BODYPAINT, AvatarTEIndex.UPPER_BODYPAINT, AvatarTEIndex.LOWER_BODYPAINT),
WearableType.HAIR: (AvatarTEIndex.HAIR,),
WearableType.EYES: (AvatarTEIndex.EYES_IRIS,),
WearableType.SHIRT: (AvatarTEIndex.UPPER_SHIRT,),
WearableType.PANTS: (AvatarTEIndex.LOWER_PANTS,),
WearableType.SHOES: (AvatarTEIndex.LOWER_SHOES,),
WearableType.SOCKS: (AvatarTEIndex.LOWER_SOCKS,),
WearableType.JACKET: (AvatarTEIndex.UPPER_JACKET, AvatarTEIndex.LOWER_JACKET),
WearableType.GLOVES: (AvatarTEIndex.UPPER_GLOVES,),
WearableType.UNDERSHIRT: (AvatarTEIndex.UPPER_UNDERSHIRT,),
WearableType.UNDERPANTS: (AvatarTEIndex.LOWER_UNDERPANTS,),
WearableType.SKIRT: (AvatarTEIndex.SKIRT,),
WearableType.ALPHA: (AvatarTEIndex.LOWER_ALPHA, AvatarTEIndex.UPPER_ALPHA,
AvatarTEIndex.HEAD_ALPHA, AvatarTEIndex.EYES_ALPHA, AvatarTEIndex.HAIR_ALPHA),
WearableType.TATTOO: (AvatarTEIndex.LOWER_TATTOO, AvatarTEIndex.UPPER_TATTOO, AvatarTEIndex.HEAD_TATTOO),
WearableType.UNIVERSAL: (AvatarTEIndex.HEAD_UNIVERSAL_TATTOO, AvatarTEIndex.UPPER_UNIVERSAL_TATTOO,
AvatarTEIndex.LOWER_UNIVERSAL_TATTOO, AvatarTEIndex.SKIRT_TATTOO,
AvatarTEIndex.HAIR_TATTOO, AvatarTEIndex.EYES_TATTOO, AvatarTEIndex.LEFT_ARM_TATTOO,
AvatarTEIndex.LEFT_LEG_TATTOO, AvatarTEIndex.AUX1_TATTOO, AvatarTEIndex.AUX2_TATTOO,
AvatarTEIndex.AUX3_TATTOO),
WearableType.PHYSICS: (),
}
@dataclasses.dataclass @dataclasses.dataclass
class Wearable(SchemaBase): class Wearable(SchemaBase):
name: str name: str
@@ -128,7 +164,7 @@ class Wearable(SchemaBase):
sale_info: InventorySaleInfo sale_info: InventorySaleInfo
# VisualParam ID -> val # VisualParam ID -> val
parameters: Dict[int, float] parameters: Dict[int, float]
# TextureEntry ID -> texture ID # TextureEntry ID -> texture UUID
textures: Dict[int, UUID] textures: Dict[int, UUID]
@classmethod @classmethod
@@ -203,3 +239,22 @@ class Wearable(SchemaBase):
writer.write(f"textures {len(self.textures)}\n") writer.write(f"textures {len(self.textures)}\n")
for te_id, texture_id in self.textures.items(): for te_id, texture_id in self.textures.items():
writer.write(f"{te_id} {texture_id}\n") writer.write(f"{te_id} {texture_id}\n")
@classmethod
def make_default(cls, w_type: WearableType) -> Self:
instance = cls(
name="New " + w_type.name.replace("_", " ").title(),
permissions=InventoryPermissions.make_default(),
sale_info=InventorySaleInfo.make_default(),
parameters={},
textures={},
wearable_type=w_type,
)
for te_idx in WEARABLE_TEXTURE_SLOTS[w_type]:
instance.textures[te_idx] = DEFAULT_WEARABLE_TEX
for param in VISUAL_PARAMS.by_wearable(w_type.name.lower()):
instance.parameters[param.id] = param.value_default
return instance

View File

@@ -219,7 +219,8 @@ class InventoryManager:
self.model.upsert(InventoryItem.from_llsd(link_llsd, flavor="ais")) self.model.upsert(InventoryItem.from_llsd(link_llsd, flavor="ais"))
for cat_id, version in payload.get("_updated_category_versions", {}).items(): for cat_id, version in payload.get("_updated_category_versions", {}).items():
cat_node = self.model.get_category(cat_id) # The key will be a string, so convert to UUID first
cat_node = self.model.get_category(UUID(cat_id))
cat_node.version = version cat_node.version = version
# Get rid of anything we were asked to # Get rid of anything we were asked to

View File

@@ -152,6 +152,15 @@ class TestDatatypes(unittest.TestCase):
def test_str_llsd_serialization(self): def test_str_llsd_serialization(self):
self.assertEqual(b"'foo\\nbar'", llsd.format_notation("foo\nbar")) self.assertEqual(b"'foo\\nbar'", llsd.format_notation("foo\nbar"))
def test_int_enum_llsd_serialization(self):
class SomeIntEnum(IntEnum):
FOO = 4
orig = SomeIntEnum.FOO
val = llsd.parse_xml(llsd.format_xml(orig))
self.assertIsInstance(val, int)
self.assertEqual(orig, val)
def test_jank_stringy_bytes(self): def test_jank_stringy_bytes(self):
val = JankStringyBytes(b"foo\x00") val = JankStringyBytes(b"foo\x00")
self.assertTrue("o" in val) self.assertTrue("o" in val)