More inventory / wearables updates
This commit is contained in:
@@ -389,6 +389,21 @@ class InventoryPermissions(InventoryBase):
|
||||
# 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)
|
||||
|
||||
@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
|
||||
class InventorySaleInfo(InventoryBase):
|
||||
@@ -397,6 +412,10 @@ class InventorySaleInfo(InventoryBase):
|
||||
sale_type: SaleType = schema_field(SchemaEnumField(SaleType))
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -35,6 +35,12 @@ class HippoLLSDXMLFormatter(base_llsd.serde_xml.LLSDXMLFormatter, HippoLLSDBaseF
|
||||
def __init__(self):
|
||||
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):
|
||||
def __init__(self):
|
||||
|
||||
@@ -16,7 +16,7 @@ from hippolyzer.lib.base.datatypes import UUID
|
||||
from hippolyzer.lib.base.helpers import get_resource_filename
|
||||
from hippolyzer.lib.base.inventory import InventorySaleInfo, InventoryPermissions
|
||||
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__)
|
||||
_T = TypeVar("_T")
|
||||
@@ -77,6 +77,12 @@ class AvatarTEIndex(enum.IntEnum):
|
||||
def is_baked(self) -> bool:
|
||||
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
|
||||
class VisualParam:
|
||||
@@ -84,6 +90,7 @@ class VisualParam:
|
||||
name: str
|
||||
value_min: float
|
||||
value_max: float
|
||||
value_default: float
|
||||
# These might be `None` if the param isn't meant to be directly edited
|
||||
edit_group: Optional[str]
|
||||
wearable: Optional[str]
|
||||
@@ -102,6 +109,7 @@ class VisualParams(List[VisualParam]):
|
||||
wearable=param.get("wearable"),
|
||||
value_min=float(param.attrib["value_min"]),
|
||||
value_max=float(param.attrib["value_max"]),
|
||||
value_default=float(param.attrib.get("value_default", 0.0))
|
||||
))
|
||||
|
||||
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"))
|
||||
|
||||
|
||||
# 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
|
||||
class Wearable(SchemaBase):
|
||||
name: str
|
||||
@@ -128,7 +164,7 @@ class Wearable(SchemaBase):
|
||||
sale_info: InventorySaleInfo
|
||||
# VisualParam ID -> val
|
||||
parameters: Dict[int, float]
|
||||
# TextureEntry ID -> texture ID
|
||||
# TextureEntry ID -> texture UUID
|
||||
textures: Dict[int, UUID]
|
||||
|
||||
@classmethod
|
||||
@@ -203,3 +239,22 @@ class Wearable(SchemaBase):
|
||||
writer.write(f"textures {len(self.textures)}\n")
|
||||
for te_id, texture_id in self.textures.items():
|
||||
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
|
||||
|
||||
@@ -219,7 +219,8 @@ class InventoryManager:
|
||||
self.model.upsert(InventoryItem.from_llsd(link_llsd, flavor="ais"))
|
||||
|
||||
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
|
||||
|
||||
# Get rid of anything we were asked to
|
||||
|
||||
@@ -152,6 +152,15 @@ class TestDatatypes(unittest.TestCase):
|
||||
def test_str_llsd_serialization(self):
|
||||
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):
|
||||
val = JankStringyBytes(b"foo\x00")
|
||||
self.assertTrue("o" in val)
|
||||
|
||||
Reference in New Issue
Block a user