From 6d251be97cd3f83ef2e840bff58f6916e21b79e1 Mon Sep 17 00:00:00 2001 From: NotSoToothless <67082114+FURRO404@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:05:51 -0700 Subject: [PATCH] update for spectra changes (#1363) --- BOT/render_replay.py | 53 ++++++++++++++++++++++++++--------- BOT/transform.py | 10 +++++-- tests/test_transform_model.py | 26 +++++++++++++++++ tss_ws.py | 6 ++++ 4 files changed, 79 insertions(+), 16 deletions(-) diff --git a/BOT/render_replay.py b/BOT/render_replay.py index 40d0e66..86d3ab1 100644 --- a/BOT/render_replay.py +++ b/BOT/render_replay.py @@ -3575,6 +3575,10 @@ def _unit_to_model_name(unit_name: str) -> str: internal = (unit_name or "").strip() if not internal: return "tankModels/unknown" + if internal.startswith(("tankModels/", "airModels/")): + return internal + if internal.startswith("aircrafts/"): + return f"airModels/{internal.split('/', 1)[1]}" tags = _get_unit_tags(internal) or [] tag_set = set(tags) if "type_strike_ucav" in tag_set or "ucav" in internal.lower(): @@ -3645,6 +3649,15 @@ def _position_at_time(path: list[dict[str, float]], time_ms: float) -> dict[str, return prev +def _event_position_to_dict(pos: Any) -> dict[str, float] | None: + if not isinstance(pos, list) or len(pos) < 3: + return None + try: + return {"X": float(pos[0]), "Y": float(pos[1]), "Z": float(pos[2])} + except (TypeError, ValueError): + return None + + def _find_render_entity_for_event(entities: list[dict[str, Any]], player_id: int, event_model: str | None, @@ -3739,7 +3752,7 @@ def _convert_ws_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, Any]: if not isinstance(ent, dict): continue uid = _to_int(ent.get("uid"), 0) - unit = str(ent.get("unit") or "") + unit = str(ent.get("model_path") or ent.get("unit") or "") path_raw = ent.get("path") or [] if not isinstance(path_raw, list): continue @@ -3775,12 +3788,18 @@ def _convert_ws_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, Any]: victim_id = _to_int(kill.get("offended_uid"), 0) killer_id = _to_int(kill.get("offender_uid"), 0) kill_time = float(kill.get("time") or 0.0) - victim_model = _unit_to_model_name(str(kill.get("offended_unit") or "")) - killer_model = _unit_to_model_name(str(kill.get("offender_unit") or "")) + victim_model = _unit_to_model_name(str(kill.get("offended_unit_model_path") or kill.get("offended_unit") or "")) + killer_model = _unit_to_model_name(str(kill.get("offender_unit_model_path") or kill.get("offender_unit") or "")) victim_entity = _find_render_entity_for_event(entities_out, victim_id, victim_model, kill_time) killer_entity = _find_render_entity_for_event(entities_out, killer_id, killer_model, kill_time) - victim_pos = _position_at_time(victim_entity.get("Path", []), kill_time) if victim_entity else None - killer_pos = _position_at_time(killer_entity.get("Path", []), kill_time) if killer_entity else None + victim_pos = ( + _position_at_time(victim_entity.get("Path", []), kill_time) + if victim_entity else None + ) or _event_position_to_dict(kill.get("offended_pos")) + killer_pos = ( + _position_at_time(killer_entity.get("Path", []), kill_time) + if killer_entity else None + ) or _event_position_to_dict(kill.get("offender_pos")) payload: dict[str, Any] = { "Time": kill_time, "VictimID": victim_id, @@ -3816,8 +3835,8 @@ def _convert_ws_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, Any]: "Time": float(dmg.get("time") or 0.0), "OffenderID": _to_int(dmg.get("offender_uid"), 0), "OffendedID": _to_int(dmg.get("offended_uid"), 0), - "OffenderModel": _unit_to_model_name(str(dmg.get("offender_unit") or "")), - "OffendedModel": _unit_to_model_name(str(dmg.get("offended_unit") or "")), + "OffenderModel": _unit_to_model_name(str(dmg.get("offender_unit_model_path") or dmg.get("offender_unit") or "")), + "OffendedModel": _unit_to_model_name(str(dmg.get("offended_unit_model_path") or dmg.get("offended_unit") or "")), "Afire": bool(dmg.get("afire", False)), }) @@ -3905,7 +3924,7 @@ def _convert_local_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, An if not isinstance(ent, dict): continue uid = _to_int(ent.get("uid"), 0) - unit = str(ent.get("unit") or "") + unit = str(ent.get("model_path") or ent.get("unit") or "") path_raw = ent.get("path") or [] if not isinstance(path_raw, list): continue @@ -3941,12 +3960,18 @@ def _convert_local_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, An victim_id = _to_int(kill.get("offended_uid"), 0) killer_id = _to_int(kill.get("offender_uid"), 0) kill_time = float(kill.get("time") or 0.0) - victim_model = _unit_to_model_name(str(kill.get("offended_unit") or "")) - killer_model = _unit_to_model_name(str(kill.get("offender_unit") or "")) + victim_model = _unit_to_model_name(str(kill.get("offended_unit_model_path") or kill.get("offended_unit") or "")) + killer_model = _unit_to_model_name(str(kill.get("offender_unit_model_path") or kill.get("offender_unit") or "")) victim_entity = _find_render_entity_for_event(entities_out, victim_id, victim_model, kill_time) killer_entity = _find_render_entity_for_event(entities_out, killer_id, killer_model, kill_time) - victim_pos = _position_at_time(victim_entity.get("Path", []), kill_time) if victim_entity else None - killer_pos = _position_at_time(killer_entity.get("Path", []), kill_time) if killer_entity else None + victim_pos = ( + _position_at_time(victim_entity.get("Path", []), kill_time) + if victim_entity else None + ) or _event_position_to_dict(kill.get("offended_pos")) + killer_pos = ( + _position_at_time(killer_entity.get("Path", []), kill_time) + if killer_entity else None + ) or _event_position_to_dict(kill.get("offender_pos")) payload: dict[str, Any] = { "Time": kill_time, "VictimID": victim_id, @@ -3982,8 +4007,8 @@ def _convert_local_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, An "Time": float(dmg.get("time") or 0.0), "OffenderID": _to_int(dmg.get("offender_uid"), 0), "OffendedID": _to_int(dmg.get("offended_uid"), 0), - "OffenderModel": _unit_to_model_name(str(dmg.get("offender_unit") or "")), - "OffendedModel": _unit_to_model_name(str(dmg.get("offended_unit") or "")), + "OffenderModel": _unit_to_model_name(str(dmg.get("offender_unit_model_path") or dmg.get("offender_unit") or "")), + "OffendedModel": _unit_to_model_name(str(dmg.get("offended_unit_model_path") or dmg.get("offended_unit") or "")), "Afire": bool(dmg.get("afire", False)), }) diff --git a/BOT/transform.py b/BOT/transform.py index 7003fc1..385a32c 100644 --- a/BOT/transform.py +++ b/BOT/transform.py @@ -14,6 +14,8 @@ from __future__ import annotations import logging from typing import Any, Optional +from spectra_replay_normalize import strip_model_prefix + log = logging.getLogger("tssbot.transform") # Imported lazily/defensively so the module is importable (and unit-testable for the @@ -61,7 +63,7 @@ def _build_units(units: list[dict[str, Any]], translate, dead: set[str] | None = dead = dead or set() out: list[dict[str, Any]] = [] for u in units or []: - internal = str(u.get("unit") or "").strip() + internal = strip_model_prefix(u.get("unit")) if not internal: continue flag = u.get("dead", u.get("died")) @@ -71,6 +73,9 @@ def _build_units(units: list[dict[str, Any]], translate, dead: set[str] | None = "name": translate(internal) or u.get("unit_normalized") or internal, "used": bool(u.get("used")), "dead": is_dead, + "unit_type": u.get("unit_type"), + "unit_class": u.get("unit_class"), + "unit_country": u.get("unit_country"), }) return out @@ -81,7 +86,7 @@ def _dead_units_by_uid(game: dict[str, Any]) -> dict[str, set[str]]: kills = ((game.get("events") or {}).get("kills")) or [] for k in kills: uid = str(k.get("offended_uid") or "") - unit = str(k.get("offended_unit") or "").strip() + unit = strip_model_prefix(k.get("offended_unit")) if uid and unit: out.setdefault(uid, set()).add(unit) return out @@ -143,6 +148,7 @@ def build_scoreboard_model(game: dict[str, Any], lang_column: str = "") "fake_nick": None, "tag": str(p.get("tag") or ""), "country_id": p.get("country_id"), + "country": p.get("country"), "air_kills": int(p.get("air_kills") or 0), "ground_kills": int(p.get("ground_kills") or 0), "assists": int(p.get("assists") or 0), diff --git a/tests/test_transform_model.py b/tests/test_transform_model.py index b299bd8..87abdb7 100644 --- a/tests/test_transform_model.py +++ b/tests/test_transform_model.py @@ -29,3 +29,29 @@ def test_model_carries_tournament_id(): model = build_scoreboard_model(_game()) assert model is not None assert model["tournament_id"] == 24965 + + +def test_model_strips_unit_model_prefixes_for_dead_matching(): + game = _game() + game["players"]["2"]["units"][0]["unit"] = "tankModels/pz" + game["players"]["2"]["units"][0]["unit_type"] = "tank" + game["players"]["2"]["units"][0]["unit_class"] = "medium tank" + game["players"]["2"]["country"] = "germany" + game["events"] = { + "kills": [ + { + "offended_uid": "2", + "offended_unit": "tankModels/pz", + } + ] + } + + model = build_scoreboard_model(game) + + assert model is not None + bob = model["teams"][1]["players"][0] + assert bob["country"] == "germany" + assert bob["units"][0]["internal"] == "pz" + assert bob["units"][0]["dead"] is True + assert bob["units"][0]["unit_type"] == "tank" + assert bob["units"][0]["unit_class"] == "medium tank" diff --git a/tss_ws.py b/tss_ws.py index 8ed7f76..8b72be3 100644 --- a/tss_ws.py +++ b/tss_ws.py @@ -31,6 +31,7 @@ from websockets.asyncio.client import connect as wsconnect from BOT.storage import insert_match, insert_player_games, upsert_tss_teams from BOT.autologging import process_game as autolog_process_game from BOT.receiver_bridge import publish_replay_batch +from spectra_replay_normalize import normalize_spectra_replay from spectra_ws_payload import SpectraPayloadError, decode_spectra_ws_payload _HERE = Path(__file__).resolve().parent @@ -73,6 +74,7 @@ def _session_id(game: Dict[str, Any]) -> str: def _write_game(game: Dict[str, Any]) -> Path: """Normalize _id to hex, then write to REPLAYS/TSS//replay_data.json.gz.""" + game.update(normalize_spectra_replay(game)) sid = _session_id(game) game["_id"] = sid # hex from this point forward session_dir = REPLAYS_DIR / sid @@ -91,6 +93,10 @@ def normalize(data: Any) -> Optional[List[Dict[str, Any]]]: if isinstance(data, dict): if "_id" in data or "id" in data: return [data] + if isinstance(data.get("data"), dict): + return [data["data"]] + if isinstance(data.get("data"), list): + return data["data"] if "completed" in data: return data["completed"] log.warning("Unknown WS frame shape: %s", type(data))