Add optional apparel sync (equippedItems)
Introduce an optional equippedItems transform extension to snapshot tracked visible apparel slots. Plugin: capture local equipped ARMO form IDs on the game-thread, include equippedItems in transform packets, parse equippedItems on the networking thread, and apply/clear synced apparel on runtime proxies (with defensive logging for missing forms). Server: fake_client parses and prints equippedItems. Docs and protocol updated; changelog and dev-log entry added. Backwards-compatible: equippedItems is optional and empty formId denotes intentional unequip.
This commit is contained in:
@@ -66,6 +66,24 @@ def get_optional_game_time(packet: dict[str, Any], field_name: str, default: flo
|
||||
return parsed_value
|
||||
|
||||
|
||||
def get_optional_equipped_items(packet: dict[str, Any]) -> list[dict[str, str]]:
|
||||
value = packet.get("equippedItems", [])
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
|
||||
equipped_items: list[dict[str, str]] = []
|
||||
for item in value:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
slot = item.get("slot")
|
||||
form_id = item.get("formId", "")
|
||||
if isinstance(slot, str) and isinstance(form_id, str):
|
||||
equipped_items.append({"slot": slot, "formId": form_id})
|
||||
|
||||
return equipped_items
|
||||
|
||||
|
||||
def update_host_world_state(packet: dict[str, Any]) -> None:
|
||||
global host_world_state
|
||||
|
||||
@@ -165,6 +183,7 @@ def update_remote_player_state(packet: dict[str, Any]) -> None:
|
||||
"movementSpeed": get_optional_float(packet, "movementSpeed"),
|
||||
"actorStateFlags1": get_optional_uint32(packet, "actorStateFlags1"),
|
||||
"actorStateFlags2": get_optional_uint32(packet, "actorStateFlags2"),
|
||||
"equippedItems": get_optional_equipped_items(packet),
|
||||
"lastReceivedLocalTime": time.time(),
|
||||
}
|
||||
|
||||
@@ -175,6 +194,12 @@ def update_remote_player_state(packet: dict[str, Any]) -> None:
|
||||
def print_remote_player(player: dict[str, Any]) -> None:
|
||||
client_time = player["clientTime"] if player["clientTime"] is not None else "missing from packet"
|
||||
server_time = player["serverTime"] if player["serverTime"] is not None else "missing from packet"
|
||||
equipped_items = player.get("equippedItems", [])
|
||||
equipment_text = (
|
||||
", ".join(f"{item['slot']}={item['formId'] or '<empty>'}" for item in equipped_items)
|
||||
if equipped_items
|
||||
else "<not sent>"
|
||||
)
|
||||
|
||||
log(
|
||||
"\n".join(
|
||||
@@ -189,6 +214,7 @@ def print_remote_player(player: dict[str, Any]) -> None:
|
||||
f"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}"
|
||||
),
|
||||
f"Actor State: flags1={player['actorStateFlags1']:08X}, flags2={player['actorStateFlags2']:08X}",
|
||||
f"Equipment: {equipment_text}",
|
||||
f"Cell: {player['cellId']}",
|
||||
f"Worldspace: {player['worldspaceId']}",
|
||||
f"Client Time: {client_time}",
|
||||
@@ -207,6 +233,11 @@ def print_remote_players() -> None:
|
||||
log("Known remote players:")
|
||||
for player_id in sorted(remote_players):
|
||||
player = remote_players[player_id]
|
||||
equipment_text = (
|
||||
", ".join(f"{item['slot']}={item['formId'] or '<empty>'}" for item in player.get("equippedItems", []))
|
||||
if player.get("equippedItems")
|
||||
else "<not sent>"
|
||||
)
|
||||
log(
|
||||
f"- Player {player_id}: "
|
||||
f"pos=({player['x']:.2f}, {player['y']:.2f}, {player['z']:.2f}), "
|
||||
@@ -215,6 +246,7 @@ def print_remote_players() -> None:
|
||||
f"sneaking={player['isSneaking']}, jumping={player['isJumping']}, "
|
||||
f"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}, "
|
||||
f"flags1={player['actorStateFlags1']:08X}, flags2={player['actorStateFlags2']:08X}, "
|
||||
f"equipment={equipment_text}, "
|
||||
f"cell={player['cellId']}, worldspace={player['worldspaceId']}, "
|
||||
f"serverTime={player['serverTime']}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user