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