206 lines
6.8 KiB
Python
206 lines
6.8 KiB
Python
"""
|
|
Body parts and linden clothing layers
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import enum
|
|
import logging
|
|
from io import StringIO
|
|
from typing import *
|
|
|
|
from xml.etree.ElementTree import parse as parse_etree
|
|
|
|
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
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
_T = TypeVar("_T")
|
|
|
|
WEARABLE_VERSION = "LLWearable version 22"
|
|
DEFAULT_WEARABLE_TEX = UUID("c228d1cf-4b5d-4ba8-84f4-899a0796aa97")
|
|
|
|
|
|
class AvatarTEIndex(enum.IntEnum):
|
|
"""From llavatarappearancedefines.h"""
|
|
HEAD_BODYPAINT = 0
|
|
UPPER_SHIRT = enum.auto()
|
|
LOWER_PANTS = enum.auto()
|
|
EYES_IRIS = enum.auto()
|
|
HAIR = enum.auto()
|
|
UPPER_BODYPAINT = enum.auto()
|
|
LOWER_BODYPAINT = enum.auto()
|
|
LOWER_SHOES = enum.auto()
|
|
HEAD_BAKED = enum.auto()
|
|
UPPER_BAKED = enum.auto()
|
|
LOWER_BAKED = enum.auto()
|
|
EYES_BAKED = enum.auto()
|
|
LOWER_SOCKS = enum.auto()
|
|
UPPER_JACKET = enum.auto()
|
|
LOWER_JACKET = enum.auto()
|
|
UPPER_GLOVES = enum.auto()
|
|
UPPER_UNDERSHIRT = enum.auto()
|
|
LOWER_UNDERPANTS = enum.auto()
|
|
SKIRT = enum.auto()
|
|
SKIRT_BAKED = enum.auto()
|
|
HAIR_BAKED = enum.auto()
|
|
LOWER_ALPHA = enum.auto()
|
|
UPPER_ALPHA = enum.auto()
|
|
HEAD_ALPHA = enum.auto()
|
|
EYES_ALPHA = enum.auto()
|
|
HAIR_ALPHA = enum.auto()
|
|
HEAD_TATTOO = enum.auto()
|
|
UPPER_TATTOO = enum.auto()
|
|
LOWER_TATTOO = enum.auto()
|
|
HEAD_UNIVERSAL_TATTOO = enum.auto()
|
|
UPPER_UNIVERSAL_TATTOO = enum.auto()
|
|
LOWER_UNIVERSAL_TATTOO = enum.auto()
|
|
SKIRT_TATTOO = enum.auto()
|
|
HAIR_TATTOO = enum.auto()
|
|
EYES_TATTOO = enum.auto()
|
|
LEFT_ARM_TATTOO = enum.auto()
|
|
LEFT_LEG_TATTOO = enum.auto()
|
|
AUX1_TATTOO = enum.auto()
|
|
AUX2_TATTOO = enum.auto()
|
|
AUX3_TATTOO = enum.auto()
|
|
LEFTARM_BAKED = enum.auto()
|
|
LEFTLEG_BAKED = enum.auto()
|
|
AUX1_BAKED = enum.auto()
|
|
AUX2_BAKED = enum.auto()
|
|
AUX3_BAKED = enum.auto()
|
|
|
|
@property
|
|
def is_baked(self) -> bool:
|
|
return self.name.endswith("_BAKED")
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class VisualParam:
|
|
id: int
|
|
name: str
|
|
value_min: float
|
|
value_max: float
|
|
# These might be `None` if the param isn't meant to be directly edited
|
|
edit_group: Optional[str]
|
|
wearable: Optional[str]
|
|
|
|
|
|
class VisualParams(List[VisualParam]):
|
|
def __init__(self, lad_path):
|
|
super().__init__()
|
|
with open(lad_path, "rb") as f:
|
|
doc = parse_etree(f)
|
|
for param in doc.findall(".//param"):
|
|
self.append(VisualParam(
|
|
id=int(param.attrib["id"]),
|
|
name=param.attrib["name"],
|
|
edit_group=param.get("edit_group"),
|
|
wearable=param.get("wearable"),
|
|
value_min=float(param.attrib["value_min"]),
|
|
value_max=float(param.attrib["value_max"]),
|
|
))
|
|
|
|
def by_name(self, name: str) -> VisualParam:
|
|
return [x for x in self if x.name == name][0]
|
|
|
|
def by_edit_group(self, edit_group: str) -> List[VisualParam]:
|
|
return [x for x in self if x.edit_group == edit_group]
|
|
|
|
def by_wearable(self, wearable: str) -> List[VisualParam]:
|
|
return [x for x in self if x.wearable == wearable]
|
|
|
|
def by_id(self, vparam_id: int) -> VisualParam:
|
|
return [x for x in self if x.id == vparam_id][0]
|
|
|
|
|
|
VISUAL_PARAMS = VisualParams(get_resource_filename("lib/base/data/avatar_lad.xml"))
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class Wearable(SchemaBase):
|
|
name: str
|
|
wearable_type: WearableType
|
|
permissions: InventoryPermissions
|
|
sale_info: InventorySaleInfo
|
|
# VisualParam ID -> val
|
|
parameters: Dict[int, float]
|
|
# TextureEntry ID -> texture ID
|
|
textures: Dict[int, UUID]
|
|
|
|
@classmethod
|
|
def _skip_to_next_populated_line(cls, reader: StringIO):
|
|
old_pos = reader.tell()
|
|
while peeked_data := reader.readline():
|
|
# Read until we find a non-blank line
|
|
if peeked_data.lstrip("\n"):
|
|
break
|
|
old_pos = reader.tell()
|
|
# Reading an empty string means EOF
|
|
if not peeked_data:
|
|
raise SchemaParsingError("Premature EOF")
|
|
reader.seek(old_pos)
|
|
|
|
@classmethod
|
|
def _read_and_parse_line(cls, reader: StringIO):
|
|
cls._skip_to_next_populated_line(reader)
|
|
return parse_schema_line(reader.readline())
|
|
|
|
@classmethod
|
|
def _read_expected_key(cls, reader: StringIO, expected_key: str) -> str:
|
|
key, val = cls._read_and_parse_line(reader)
|
|
if key != expected_key:
|
|
raise ValueError(f"Expected {expected_key} not found, {(key, val)!r}")
|
|
return val
|
|
|
|
@classmethod
|
|
def from_reader(cls, reader: StringIO) -> Wearable:
|
|
cls._skip_to_next_populated_line(reader)
|
|
version_str = reader.readline().rstrip()
|
|
if version_str != WEARABLE_VERSION:
|
|
raise ValueError(f"Bad wearable version {version_str!r}")
|
|
cls._skip_to_next_populated_line(reader)
|
|
name = reader.readline().rstrip()
|
|
|
|
permissions = InventoryPermissions.from_reader(reader, read_header=True)
|
|
sale_info = InventorySaleInfo.from_reader(reader, read_header=True)
|
|
|
|
wearable_type = WearableType(int(cls._read_expected_key(reader, "type")))
|
|
num_params = int(cls._read_expected_key(reader, "parameters"))
|
|
params = {}
|
|
for _ in range(num_params):
|
|
param_id, param_val = cls._read_and_parse_line(reader)
|
|
if param_val == ".":
|
|
param_val = "0.0"
|
|
params[int(param_id)] = float(param_val)
|
|
|
|
num_textures = int(cls._read_expected_key(reader, "textures"))
|
|
textures = {}
|
|
for _ in range(num_textures):
|
|
te_id, texture_id = cls._read_and_parse_line(reader)
|
|
textures[int(te_id)] = UUID(texture_id)
|
|
return Wearable(
|
|
name=name,
|
|
wearable_type=wearable_type,
|
|
permissions=permissions,
|
|
sale_info=sale_info,
|
|
parameters=params,
|
|
textures=textures
|
|
)
|
|
|
|
def to_writer(self, writer: StringIO):
|
|
writer.write(f"{WEARABLE_VERSION}\n")
|
|
writer.write(f"{self.name}\n\n")
|
|
self.permissions.to_writer(writer)
|
|
self.sale_info.to_writer(writer)
|
|
writer.write(f"type {int(self.wearable_type)}\n")
|
|
writer.write(f"parameters {len(self.parameters)}\n")
|
|
for param_id, param_val in self.parameters.items():
|
|
writer.write(f"{param_id} {param_val}\n")
|
|
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")
|