Character Appearance Phase 1 Done

Bump appearance payloads to version 2 and add `morphRegions` and `facialBoneMorphs` capture, serialization, and proxy application. Also fix head-part syncing when sender and proxy counts differ by reallocating the proxy array instead of deferring the whole list, and update the Python harness and protocol docs to match.
This commit is contained in:
2026-06-30 17:37:02 +12:00
parent 8ff1a90fa5
commit 5b54db64d4
10 changed files with 346 additions and 32 deletions
+38 -2
View File
@@ -99,6 +99,8 @@ def get_optional_appearance(packet: dict[str, Any]) -> dict[str, Any] | None:
"facialHairColorFormId": value.get("facialHairColorFormId", ""),
"headParts": [],
"morphs": [],
"morphRegions": [],
"facialBoneMorphs": [],
}
head_parts = value.get("headParts", [])
@@ -113,9 +115,41 @@ 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 2; older senders omit these and the lists stay empty.
morph_regions = value.get("morphRegions", [])
if isinstance(morph_regions, list):
appearance["morphRegions"] = [
get_optional_float({"v": entry}, "v")
for entry in morph_regions
if isinstance(entry, (int, float))
]
facial_bone_morphs = value.get("facialBoneMorphs", [])
if isinstance(facial_bone_morphs, list):
appearance["facialBoneMorphs"] = [
{
"id": morph.get("id", ""),
"position": _read_float3(morph.get("position")),
"rotation": _read_float3(morph.get("rotation")),
"scale": _read_float3(morph.get("scale"), default=1.0),
}
for morph in facial_bone_morphs
if isinstance(morph, dict) and isinstance(morph.get("id", ""), str)
]
return appearance
def _read_float3(value: Any, default: float = 0.0) -> list[float]:
result = [default, default, default]
if isinstance(value, list):
for index in range(min(len(value), 3)):
element = value[index]
if isinstance(element, (int, float)):
result[index] = float(element)
return result
def update_host_world_state(packet: dict[str, Any]) -> None:
global host_world_state
@@ -238,7 +272,9 @@ def print_remote_player(player: dict[str, Any]) -> None:
(
f"version={appearance.get('version')}, race={appearance.get('raceFormId') or '<empty>'}, "
f"height={appearance.get('height')}, headParts={len(appearance.get('headParts', []))}, "
f"morphs={len(appearance.get('morphs', []))}"
f"morphs={len(appearance.get('morphs', []))}, "
f"morphRegions={len(appearance.get('morphRegions', []))}, "
f"facialBoneMorphs={len(appearance.get('facialBoneMorphs', []))}"
)
if isinstance(appearance, dict)
else "<not sent>"
@@ -284,7 +320,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', []))}"
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', []))}"
if isinstance(appearance, dict)
else "<not sent>"
)
+3 -1
View File
@@ -44,7 +44,7 @@ IDLE_ANGLE_Z = 3.93
TELEPORT_TEST_OFFSET_X = 900.0
TELEPORT_TEST_OFFSET_Y = 450.0
DEFAULT_APPEARANCE = {
"version": 1,
"version": 2,
"raceFormId": "00013746",
"height": 1.0,
"morphWeight": {"thin": 0.0, "muscular": 0.0, "large": 0.0},
@@ -53,6 +53,8 @@ DEFAULT_APPEARANCE = {
"facialHairColorFormId": "",
"headParts": [],
"morphs": [],
"morphRegions": [],
"facialBoneMorphs": [],
}
ClientSnapshotProvider = Callable[[], list[dict[str, Any]]]