Add appearance tint syncing (appearance v3)
Introduce tint syncing for skin/complection/makeup/beard by adding RemoteAppearanceTint and a tints vector, and bump appearance payload version to 3. Parse/emit tints in networking and update fake server/client test payloads. Capture live Player/NPC tintingData during appearance capture. Apply tints to proxies by updating existing entries or reconstructing missing ones via template resolution and MakeTintEntry (game allocator + vtable fixup); unresolved templates are skipped with throttled warnings. Update docs/changelog and add logging for applied tints. Backwards compatible with v1/v2 packets.
This commit is contained in:
+24
-2
@@ -101,6 +101,7 @@ def get_optional_appearance(packet: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"morphs": [],
|
||||
"morphRegions": [],
|
||||
"facialBoneMorphs": [],
|
||||
"tints": [],
|
||||
}
|
||||
|
||||
head_parts = value.get("headParts", [])
|
||||
@@ -137,6 +138,26 @@ def get_optional_appearance(packet: dict[str, Any]) -> dict[str, Any] | None:
|
||||
if isinstance(morph, dict) and isinstance(morph.get("id", ""), str)
|
||||
]
|
||||
|
||||
# Added in appearance version 3; palette-only color/swatch are preserved when present.
|
||||
tints = value.get("tints", [])
|
||||
if isinstance(tints, list):
|
||||
parsed_tints: list[dict[str, Any]] = []
|
||||
for tint in tints:
|
||||
if not isinstance(tint, dict) or not isinstance(tint.get("id"), (int, float)):
|
||||
continue
|
||||
entry: dict[str, Any] = {
|
||||
"id": int(tint.get("id", 0)),
|
||||
"type": int(tint["type"]) if isinstance(tint.get("type"), (int, float)) else 0,
|
||||
"value": int(tint["value"]) if isinstance(tint.get("value"), (int, float)) else 0,
|
||||
}
|
||||
color = tint.get("color")
|
||||
if isinstance(color, str) and color:
|
||||
entry["color"] = color
|
||||
if isinstance(tint.get("swatch"), (int, float)):
|
||||
entry["swatch"] = int(tint["swatch"])
|
||||
parsed_tints.append(entry)
|
||||
appearance["tints"] = parsed_tints
|
||||
|
||||
return appearance
|
||||
|
||||
|
||||
@@ -274,7 +295,8 @@ def print_remote_player(player: dict[str, Any]) -> None:
|
||||
f"height={appearance.get('height')}, headParts={len(appearance.get('headParts', []))}, "
|
||||
f"morphs={len(appearance.get('morphs', []))}, "
|
||||
f"morphRegions={len(appearance.get('morphRegions', []))}, "
|
||||
f"facialBoneMorphs={len(appearance.get('facialBoneMorphs', []))}"
|
||||
f"facialBoneMorphs={len(appearance.get('facialBoneMorphs', []))}, "
|
||||
f"tints={len(appearance.get('tints', []))}"
|
||||
)
|
||||
if isinstance(appearance, dict)
|
||||
else "<not sent>"
|
||||
@@ -320,7 +342,7 @@ def print_remote_players() -> None:
|
||||
)
|
||||
appearance = player.get("appearance")
|
||||
appearance_text = (
|
||||
f"race={appearance.get('raceFormId') or '<empty>'}, headParts={len(appearance.get('headParts', []))}, morphs={len(appearance.get('morphs', []))}, morphRegions={len(appearance.get('morphRegions', []))}, facialBoneMorphs={len(appearance.get('facialBoneMorphs', []))}"
|
||||
f"race={appearance.get('raceFormId') or '<empty>'}, headParts={len(appearance.get('headParts', []))}, morphs={len(appearance.get('morphs', []))}, morphRegions={len(appearance.get('morphRegions', []))}, facialBoneMorphs={len(appearance.get('facialBoneMorphs', []))}, tints={len(appearance.get('tints', []))}"
|
||||
if isinstance(appearance, dict)
|
||||
else "<not sent>"
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ IDLE_ANGLE_Z = 3.93
|
||||
TELEPORT_TEST_OFFSET_X = 900.0
|
||||
TELEPORT_TEST_OFFSET_Y = 450.0
|
||||
DEFAULT_APPEARANCE = {
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"raceFormId": "00013746",
|
||||
"height": 1.0,
|
||||
"morphWeight": {"thin": 0.0, "muscular": 0.0, "large": 0.0},
|
||||
@@ -55,6 +55,7 @@ DEFAULT_APPEARANCE = {
|
||||
"morphs": [],
|
||||
"morphRegions": [],
|
||||
"facialBoneMorphs": [],
|
||||
"tints": [],
|
||||
}
|
||||
|
||||
ClientSnapshotProvider = Callable[[], list[dict[str, Any]]]
|
||||
|
||||
Reference in New Issue
Block a user