Sync from GitHub main #1
@@ -0,0 +1,237 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
MAX_CHARACTER_NAME_CHARS = 128
|
||||
MAX_EQUIPPED_ITEMS = 32
|
||||
MAX_EQUIPMENT_SLOT_CHARS = 64
|
||||
MAX_HEAD_PARTS = 64
|
||||
MAX_MORPHS = 128
|
||||
MAX_MORPH_REGIONS = 128
|
||||
MAX_FACIAL_BONE_MORPHS = 128
|
||||
MAX_TINTS = 128
|
||||
|
||||
|
||||
def _is_int(value: Any, minimum: int = 0, maximum: int = 0xFFFFFFFF) -> bool:
|
||||
return isinstance(value, int) and not isinstance(value, bool) and minimum <= value <= maximum
|
||||
|
||||
|
||||
def _is_finite_number(value: Any, minimum: float, maximum: float) -> bool:
|
||||
return (
|
||||
isinstance(value, (int, float))
|
||||
and not isinstance(value, bool)
|
||||
and math.isfinite(float(value))
|
||||
and minimum <= float(value) <= maximum
|
||||
)
|
||||
|
||||
|
||||
def _normalize_form_id(value: Any, *, allow_empty: bool = True) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
if allow_empty and value == "":
|
||||
return ""
|
||||
if not 1 <= len(value) <= 8:
|
||||
return None
|
||||
if any(character not in "0123456789abcdefABCDEF" for character in value):
|
||||
return None
|
||||
return value.upper().zfill(8)
|
||||
|
||||
|
||||
def _normalize_vec3(value: Any, minimum: float, maximum: float) -> list[float] | None:
|
||||
if not isinstance(value, list) or len(value) != 3:
|
||||
return None
|
||||
if not all(_is_finite_number(component, minimum, maximum) for component in value):
|
||||
return None
|
||||
return [float(component) for component in value]
|
||||
|
||||
|
||||
def _normalize_appearance(value: Any) -> dict[str, Any] | None:
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
|
||||
clean: dict[str, Any] = {}
|
||||
version = value.get("version", 4)
|
||||
if not _is_int(version, 1, 1000):
|
||||
return None
|
||||
clean["version"] = int(version)
|
||||
|
||||
for name in ("raceFormId", "hairColorFormId", "facialHairColorFormId", "complexionFormId"):
|
||||
if name in value:
|
||||
form_id = _normalize_form_id(value[name])
|
||||
if form_id is None:
|
||||
return None
|
||||
clean[name] = form_id
|
||||
|
||||
if "height" in value:
|
||||
if not _is_finite_number(value["height"], 0.25, 4.0):
|
||||
return None
|
||||
clean["height"] = float(value["height"])
|
||||
|
||||
if "isFemale" in value:
|
||||
if not isinstance(value["isFemale"], bool):
|
||||
return None
|
||||
clean["isFemale"] = value["isFemale"]
|
||||
|
||||
if "morphWeight" in value:
|
||||
weight = value["morphWeight"]
|
||||
if not isinstance(weight, dict):
|
||||
return None
|
||||
clean_weight: dict[str, float] = {}
|
||||
for name in ("thin", "muscular", "large"):
|
||||
if not _is_finite_number(weight.get(name), -100.0, 100.0):
|
||||
return None
|
||||
clean_weight[name] = float(weight[name])
|
||||
clean["morphWeight"] = clean_weight
|
||||
|
||||
if "bodyTintColor" in value:
|
||||
color = value["bodyTintColor"]
|
||||
if not isinstance(color, dict):
|
||||
return None
|
||||
clean_color: dict[str, int] = {}
|
||||
for name in ("r", "g", "b", "a"):
|
||||
if not _is_int(color.get(name), 0, 255):
|
||||
return None
|
||||
clean_color[name] = int(color[name])
|
||||
clean["bodyTintColor"] = clean_color
|
||||
|
||||
if "headParts" in value:
|
||||
head_parts = value["headParts"]
|
||||
if not isinstance(head_parts, list) or len(head_parts) > MAX_HEAD_PARTS:
|
||||
return None
|
||||
clean_head_parts: list[str] = []
|
||||
for form_id in head_parts:
|
||||
normalized = _normalize_form_id(form_id, allow_empty=False)
|
||||
if normalized is None:
|
||||
return None
|
||||
clean_head_parts.append(normalized)
|
||||
clean["headParts"] = clean_head_parts
|
||||
|
||||
if "morphs" in value:
|
||||
morphs = value["morphs"]
|
||||
if not isinstance(morphs, list) or len(morphs) > MAX_MORPHS:
|
||||
return None
|
||||
clean_morphs: list[dict[str, Any]] = []
|
||||
for morph in morphs:
|
||||
if not isinstance(morph, dict):
|
||||
return None
|
||||
morph_id = _normalize_form_id(morph.get("id"), allow_empty=False)
|
||||
if morph_id is None or not _is_finite_number(morph.get("value"), -1000.0, 1000.0):
|
||||
return None
|
||||
clean_morphs.append({"id": morph_id, "value": float(morph["value"])})
|
||||
clean["morphs"] = clean_morphs
|
||||
|
||||
if "morphRegions" in value:
|
||||
regions = value["morphRegions"]
|
||||
if not isinstance(regions, list) or len(regions) > MAX_MORPH_REGIONS:
|
||||
return None
|
||||
if not all(_is_finite_number(region, -1000.0, 1000.0) for region in regions):
|
||||
return None
|
||||
clean["morphRegions"] = [float(region) for region in regions]
|
||||
|
||||
if "facialBoneMorphs" in value:
|
||||
morphs = value["facialBoneMorphs"]
|
||||
if not isinstance(morphs, list) or len(morphs) > MAX_FACIAL_BONE_MORPHS:
|
||||
return None
|
||||
clean_bones: list[dict[str, Any]] = []
|
||||
for morph in morphs:
|
||||
if not isinstance(morph, dict):
|
||||
return None
|
||||
morph_id = _normalize_form_id(morph.get("id"), allow_empty=False)
|
||||
position = _normalize_vec3(morph.get("position"), -10000.0, 10000.0)
|
||||
rotation = _normalize_vec3(morph.get("rotation"), -10000.0, 10000.0)
|
||||
scale = _normalize_vec3(morph.get("scale"), -100.0, 100.0)
|
||||
if morph_id is None or position is None or rotation is None or scale is None:
|
||||
return None
|
||||
clean_bones.append(
|
||||
{
|
||||
"id": morph_id,
|
||||
"position": position,
|
||||
"rotation": rotation,
|
||||
"scale": scale,
|
||||
}
|
||||
)
|
||||
clean["facialBoneMorphs"] = clean_bones
|
||||
|
||||
if "tints" in value:
|
||||
tints = value["tints"]
|
||||
if not isinstance(tints, list) or len(tints) > MAX_TINTS:
|
||||
return None
|
||||
clean_tints: list[dict[str, Any]] = []
|
||||
for tint in tints:
|
||||
if not isinstance(tint, dict):
|
||||
return None
|
||||
if not _is_int(tint.get("id"), 0, 0xFFFF):
|
||||
return None
|
||||
if not _is_int(tint.get("type"), 0, 0xFFFFFFFF):
|
||||
return None
|
||||
if not _is_int(tint.get("value"), 0, 255):
|
||||
return None
|
||||
clean_tint: dict[str, Any] = {
|
||||
"id": int(tint["id"]),
|
||||
"type": int(tint["type"]),
|
||||
"value": int(tint["value"]),
|
||||
}
|
||||
if "color" in tint:
|
||||
color = _normalize_form_id(tint["color"], allow_empty=False)
|
||||
if color is None or not _is_int(tint.get("swatch", 0), 0, 0xFFFF):
|
||||
return None
|
||||
clean_tint["color"] = color
|
||||
clean_tint["swatch"] = int(tint.get("swatch", 0))
|
||||
clean_tints.append(clean_tint)
|
||||
clean["tints"] = clean_tints
|
||||
|
||||
return clean
|
||||
|
||||
|
||||
def normalize_player_state_packet(
|
||||
packet: dict[str, Any],
|
||||
normalize_action_events: Callable[[Any], list[dict[str, Any]]],
|
||||
) -> dict[str, Any] | None:
|
||||
clean: dict[str, Any] = {"type": "playerState"}
|
||||
has_state = False
|
||||
|
||||
if "equippedItems" in packet:
|
||||
items = packet["equippedItems"]
|
||||
if not isinstance(items, list) or len(items) > MAX_EQUIPPED_ITEMS:
|
||||
return None
|
||||
clean_items: list[dict[str, str]] = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
slot = item.get("slot")
|
||||
if not isinstance(slot, str) or not 1 <= len(slot) <= MAX_EQUIPMENT_SLOT_CHARS:
|
||||
return None
|
||||
form_id = _normalize_form_id(item.get("formId", ""))
|
||||
if form_id is None:
|
||||
return None
|
||||
clean_items.append({"slot": slot, "formId": form_id})
|
||||
clean["equippedItems"] = clean_items
|
||||
has_state = True
|
||||
|
||||
if "appearance" in packet:
|
||||
appearance = _normalize_appearance(packet["appearance"])
|
||||
if appearance is None:
|
||||
return None
|
||||
clean["appearance"] = appearance
|
||||
has_state = True
|
||||
|
||||
if "actionEvents" in packet:
|
||||
action_events = packet["actionEvents"]
|
||||
if not isinstance(action_events, list):
|
||||
return None
|
||||
normalized_actions = normalize_action_events(action_events)
|
||||
if len(normalized_actions) != len(action_events):
|
||||
return None
|
||||
clean["actionEvents"] = normalized_actions
|
||||
has_state = True
|
||||
|
||||
if "characterName" in packet:
|
||||
name = packet["characterName"]
|
||||
if not isinstance(name, str) or len(name) > MAX_CHARACTER_NAME_CHARS:
|
||||
return None
|
||||
clean["characterName"] = name
|
||||
has_state = True
|
||||
|
||||
return clean if has_state else None
|
||||
Reference in New Issue
Block a user