"""Normalization helpers for provisional server collection flows.""" from __future__ import annotations from typing import TYPE_CHECKING from typing import Mapping if TYPE_CHECKING: from .a2s_client import A2SServerInfo MAP_NAME_ALIASES = { "stmarie": "St. Marie Du Mont", "stmariedumont": "St. Marie Du Mont", "saintemariedumont": "St. Marie Du Mont", "saintemariedumontwarfare": "St. Marie Du Mont", "saintemariedumontoffensiveus": "St. Marie Du Mont", "saintemariedumontoffensiveger": "St. Marie Du Mont", "saintemariedumontnight": "St. Marie Du Mont", "saintemariedumontovercast": "St. Marie Du Mont", "sainte-mariedumont": "St. Marie Du Mont", "sainte-marie-du-mont": "St. Marie Du Mont", "stmereeglise": "St. Mere Eglise", "stmereeglisewarfare": "St. Mere Eglise", "stmereegliseoffensiveus": "St. Mere Eglise", "stmereegliseoffensiveger": "St. Mere Eglise", "saintemereeglise": "St. Mere Eglise", "sainte-mere-eglise": "St. Mere Eglise", "purpleheartlane": "Purple Heart Lane", "utahbeach": "Utah Beach", "omahabeach": "Omaha Beach", "hurtgenforest": "Hurtgen Forest", "hill400": "Hill 400", "foy": "Foy", "kursk": "Kursk", "kharkov": "Kharkov", "kharkiv": "Kharkiv", "stalingrad": "Stalingrad", "remagen": "Remagen", "driel": "Driel", "elalamein": "El Alamein", "mortain": "Mortain", "carentan": "Carentan", "devn": "Elsenborn Ridge", "elsenbornridge": "Elsenborn Ridge", "elsenborn": "Elsenborn Ridge", "smolensk": "Smolensk", "smolenskwarfare": "Smolensk", "smolenskoffensiverus": "Smolensk", "smolenskoffensiveger": "Smolensk", "developertestmap": "Smolensk", "devq": "Smolensk", } def normalize_server_record( raw_record: Mapping[str, object], *, source_name: str, ) -> dict[str, object]: """Normalize a raw server record into the collector's internal shape.""" external_server_id = _string_or_none(raw_record.get("external_server_id")) return { "external_server_id": external_server_id, "server_name": _string_or_default(raw_record.get("server_name"), "Unknown server"), "status": _normalize_status(raw_record.get("status")), "players": _coerce_int(raw_record.get("players")), "max_players": _coerce_int(raw_record.get("max_players")), "current_map": normalize_map_name(raw_record.get("current_map")), "region": _string_or_none(raw_record.get("region")), "source_name": source_name, "snapshot_origin": "controlled-fallback", "source_ref": external_server_id or source_name, } def normalize_a2s_server_info( server_info: "A2SServerInfo", *, source_name: str, external_server_id: str | None = None, region: str | None = None, ) -> dict[str, object]: """Normalize a probed A2S payload into the collector's internal shape.""" resolved_external_id = external_server_id or ( f"a2s:{server_info.host}:{server_info.query_port}" ) return { "external_server_id": resolved_external_id, "server_name": server_info.server_name or "Unknown server", "status": "online", "players": server_info.players, "max_players": server_info.max_players, "current_map": normalize_map_name(server_info.map_name), "region": region, "source_name": source_name, "snapshot_origin": "real-a2s", "source_ref": f"a2s://{server_info.host}:{server_info.query_port}", } def normalize_map_name(value: object) -> str | None: """Normalize internal or abbreviated HLL map labels into a stable display name.""" normalized = _string_or_none(value) if normalized is None: return None alias_key = "".join(character.lower() for character in normalized if character.isalnum()) alias_match = MAP_NAME_ALIASES.get(alias_key) if alias_match: return alias_match for candidate_key, candidate_label in MAP_NAME_ALIASES.items(): if alias_key.startswith(candidate_key): return candidate_label prettified = _prettify_map_name(normalized) return prettified or normalized def _normalize_status(value: object) -> str: if not isinstance(value, str): return "unknown" normalized = value.strip().lower() if normalized in {"online", "offline", "unknown"}: return normalized return "unknown" def _coerce_int(value: object) -> int | None: if value is None: return None try: return int(value) except (TypeError, ValueError): return None def _string_or_none(value: object) -> str | None: if not isinstance(value, str): return None stripped = value.strip() return stripped or None def _string_or_default(value: object, default: str) -> str: normalized = _string_or_none(value) return normalized or default def _prettify_map_name(value: str) -> str: text = value.replace("_", " ").replace("-", " ").strip() compact_text = " ".join(text.split()) if not compact_text: return value return " ".join( word.upper() if word.isdigit() else word.capitalize() for word in compact_text.split(" ") )