This commit is contained in:
NotSoToothless
2026-06-18 20:11:22 -07:00
committed by GitHub
parent 48f96ca8ff
commit 76844c1c6f
2 changed files with 170 additions and 32 deletions
+157 -29
View File
@@ -907,6 +907,46 @@ def _capture_sort_key(name: str) -> tuple[int, str]:
return (int(digits) if digits else 999, name)
def _capture_area_number(name: str) -> int | None:
digits = "".join(ch for ch in name if ch.isdigit())
return int(digits) if digits else None
def _mission_briefing_zone_numbers(mission_def: dict | None) -> set[int]:
"""Extract cap numbers referenced by the mission briefing map icons."""
if not isinstance(mission_def, dict):
return set()
briefing = mission_def.get("mission_settings", {}).get("briefing")
if not isinstance(briefing, dict):
return set()
out: set[int] = set()
def walk(node: Any) -> None:
if isinstance(node, dict):
target = node.get("target")
icon_type = str(node.get("icontype", "")).lower()
if isinstance(target, str):
target_l = target.lower()
if "zone" in target_l and (
"basezone" in icon_type
or "capture" in icon_type
or "zone" in icon_type
or "briefing_zone" in target_l
):
digits = "".join(ch for ch in target if ch.isdigit())
if digits:
out.add(int(digits))
for value in node.values():
walk(value)
elif isinstance(node, list):
for item in node:
walk(item)
walk(briefing)
return out
def _capture_radius_from_tm(tm: list) -> float:
try:
a = np.array(tm[0], dtype=np.float64)
@@ -953,6 +993,15 @@ def resolve_capture_areas(mission_def: dict | None, mission_def_path: Path | Non
if not capture_names:
return []
briefing_zone_numbers = _mission_briefing_zone_numbers(mission_def)
if briefing_zone_numbers:
filtered = [
name for name in capture_names
if _capture_area_number(name) in briefing_zone_numbers
]
if filtered:
capture_names = filtered
# Keep one variant per area base (prefer arcade, then realistic, hardcore).
suffix_prio = ["_arcade", "_realistic", "_hardcore", "_simulator", ""]
groups: dict[str, dict[str, str]] = {}
@@ -1032,6 +1081,35 @@ def resolve_capture_areas(mission_def: dict | None, mission_def_path: Path | Non
return out
def _capture_areas_from_zone_geometry(zone_geometry: dict | None) -> list[dict]:
"""Build capture area geometry from replay zone centers when mission .blk lacks areas."""
if not isinstance(zone_geometry, dict):
return []
out: list[dict] = []
for letter in sorted(zone_geometry.keys()):
zdata = zone_geometry.get(letter)
if not isinstance(zdata, dict):
continue
center = zdata.get("center")
if not isinstance(center, list) or len(center) < 3:
continue
try:
cx = float(center[0])
cz = float(center[2])
radius = float(zdata.get("radius", 0.0) or 0.0)
except Exception:
continue
out.append({
"name": f"replay_zone_{letter}",
"type": "Sphere",
"x": cx,
"z": cz,
"radius": radius,
"tm": None,
})
return out
def _world_to_map_px(x: float, z: float, x0: float, z0: float, xr: float, zr: float, canvas: int) -> tuple[int, int]:
px = int(round((x - x0) / xr * canvas))
py = int(round((z0 + zr - z) / zr * canvas))
@@ -1471,7 +1549,7 @@ def load_map_image(level_path: str, level_def: dict | None,
def load_level_coords(level_path: str, session_id: int = 0) -> dict | None:
"""Load tankMapCoord0/1 from local .blkx files (including datamine clone)."""
"""Load local level .blkx data, including full-map-only air levels."""
del session_id # kept in signature for call-site compatibility
stem = Path(level_path).stem
candidates = [
@@ -1485,10 +1563,13 @@ def load_level_coords(level_path: str, session_id: int = 0) -> dict | None:
data = _load_json_file(blkx)
if not isinstance(data, dict):
continue
if "tankMapCoord0" in data and "tankMapCoord1" in data:
if (
("tankMapCoord0" in data and "tankMapCoord1" in data)
or ("mapCoord0" in data and "mapCoord1" in data)
):
print(f" LevelDef : {blkx}")
return data
print(f" LevelDef missing tank coords: {blkx.name}")
print(f" LevelDef missing map coords: {blkx.name}")
return None
@@ -1514,6 +1595,22 @@ def select_tank_coords(level_def: dict, use_alt_map_coord: bool
raise ValueError("LevelDef missing usable tank map coordinates")
def select_map_coords(level_def: dict) -> tuple[list[float], list[float], str]:
"""Pick full tactical-map coordinate bounds from a level definition."""
mc0 = level_def.get("mapCoord0")
mc1 = level_def.get("mapCoord1")
if isinstance(mc0, list) and mc0 and isinstance(mc0[0], list):
mc0 = mc0[0]
if isinstance(mc1, list) and mc1 and isinstance(mc1[0], list):
mc1 = mc1[0]
if (
isinstance(mc0, list) and len(mc0) >= 2
and isinstance(mc1, list) and len(mc1) >= 2
):
return mc0, mc1, "mapCoord"
raise ValueError("LevelDef missing usable full map coordinates")
# ── Coordinate transform ───────────────────────────────────────────────────────
@@ -2752,6 +2849,8 @@ def render_gob(
tc0, tc1 = _clamp_bounds_to_base(tc0, tc1, base_tc0, base_tc1)
print(f"ok ({coord_src}) X=[{tc0[0]}, {tc1[0]}] Z=[{tc0[1]}, {tc1[1]}]")
capture_areas = resolve_capture_areas(mission_def, mission_def_path, battle_type)
if not capture_areas:
capture_areas = _capture_areas_from_zone_geometry(d.get("ZoneGeometry"))
if capture_areas:
print("Capture : " + ", ".join(
f"{c['name']}({c['type']})" for c in capture_areas
@@ -3129,6 +3228,26 @@ def _position_at_time(path: list[dict[str, float]], time_ms: float) -> dict[str,
return prev
def _zone_geometry_from_raw_zones(zones_src: Any) -> dict[str, dict]:
if not isinstance(zones_src, dict):
return {}
out: dict[str, dict] = {}
for letter, zdata in zones_src.items():
if not isinstance(letter, str) or not isinstance(zdata, dict):
continue
center = zdata.get("center")
if not isinstance(center, list) or len(center) < 3:
continue
try:
out[letter] = {
"center": [float(center[0]), float(center[1]), float(center[2])],
"radius": float(zdata.get("radius", 0.0) or 0.0),
}
except Exception:
continue
return out
def _convert_ws_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, Any]:
players_src = replay.get("players") or {}
if not isinstance(players_src, dict):
@@ -3266,6 +3385,7 @@ def _convert_ws_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, Any]:
for letter, zdata in zones_src.items()
if isinstance(zdata, dict)
} if isinstance(zones_src, dict) else {}
zone_geometry_out = _zone_geometry_from_raw_zones(zones_src)
tickets_out = tickets_src if isinstance(tickets_src, dict) else {}
team_slot_names = {}
if winner_team:
@@ -3286,6 +3406,7 @@ def _convert_ws_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, Any]:
"Kills": kills_out,
"DamageReports": damages_out,
"Zones": zones_out,
"ZoneGeometry": zone_geometry_out,
"Tickets": tickets_out,
"WinnerSlot": winner_team,
"TeamSlotNames": team_slot_names,
@@ -3414,6 +3535,7 @@ def _convert_local_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, An
# Zones (letter -> cap timeline, positive == slot 1), tickets (slot -> timeline),
# the winning slot, and slot -> name all stay in slot space for the readers.
zones_out = replay.get("zones") if isinstance(replay.get("zones"), dict) else {}
zone_geometry_out = _zone_geometry_from_raw_zones(replay.get("zones"))
tickets_out = replay.get("tickets") if isinstance(replay.get("tickets"), dict) else {}
team_slot_names = replay.get("team_slot_names") if isinstance(replay.get("team_slot_names"), dict) else {}
@@ -3430,6 +3552,7 @@ def _convert_local_replay_to_render_dict(replay: dict[str, Any]) -> dict[str, An
"Kills": kills_out,
"DamageReports": damages_out,
"Zones": zones_out,
"ZoneGeometry": zone_geometry_out,
"Tickets": tickets_out,
"WinnerSlot": _to_int(replay.get("winner_team_slot"), 0),
"TeamSlotNames": team_slot_names,
@@ -3496,7 +3619,7 @@ def _vehicle_icon_key(model_name: str) -> str:
def _vehicle_mini_icon(model_name: str) -> str | None:
"""Return the mini icon filename for aircraft (e.g. 'spitfire_ix_ico'), or None."""
internal = model_name.split("/")[-1]
mini_path = Path(__file__).resolve().parent / "ICONS" / "MINIS" / f"{internal}_ico.png"
mini_path = MINIS_DIR / f"{internal}_ico.png"
if mini_path.exists():
return f"mini:{internal}_ico"
return None
@@ -3575,41 +3698,46 @@ def export_replay_json(replay_path: Path) -> dict:
and e.get("ModelName", "").startswith("tankModels/")
and e.get("Path")]
capture_areas = resolve_capture_areas(mission_def, mission_def_path, battle_type)
if not capture_areas:
capture_areas = _capture_areas_from_zone_geometry(d.get("ZoneGeometry"))
if level_data:
base_c0, base_c1, _ = select_tank_coords(level_data, use_alt_map_coord)
tank_map_coords = {"x0": base_c0[0], "z0": base_c0[1], "x1": base_c1[0], "z1": base_c1[1]}
c0, c1, _ = resolve_world_bounds(
level_data,
use_alt_map_coord,
mission_def,
mission_def_path,
battle_type,
)
c0, c1, _ = _fit_world_bounds_to_ground_activity(
c0, c1, "json",
mission_def, mission_def_path, battle_type,
ground_entities_for_bounds,
)
c0, c1 = _expand_bounds_by_pixels(c0, c1, canvas=CANVAS_MIN, pad_px=MAP_PAD_PX)
c0, c1 = _clamp_bounds_to_base(c0, c1, base_c0, base_c1)
level_coords = {"x0": c0[0], "z0": c0[1], "x1": c1[0], "z1": c1[1]}
try:
base_c0, base_c1, _ = select_tank_coords(level_data, use_alt_map_coord)
tank_map_coords = {"x0": base_c0[0], "z0": base_c0[1], "x1": base_c1[0], "z1": base_c1[1]}
c0, c1, _ = resolve_world_bounds(
level_data,
use_alt_map_coord,
mission_def,
mission_def_path,
battle_type,
)
c0, c1, _ = _fit_world_bounds_to_ground_activity(
c0, c1, "json",
mission_def, mission_def_path, battle_type,
ground_entities_for_bounds,
)
c0, c1 = _expand_bounds_by_pixels(c0, c1, canvas=CANVAS_MIN, pad_px=MAP_PAD_PX)
c0, c1 = _clamp_bounds_to_base(c0, c1, base_c0, base_c1)
level_coords = {"x0": c0[0], "z0": c0[1], "x1": c1[0], "z1": c1[1]}
except ValueError:
mc0, mc1, _ = select_map_coords(level_data)
level_coords = {"x0": mc0[0], "z0": mc0[1], "x1": mc1[0], "z1": mc1[1]}
else:
level_coords = {"x0": 0, "z0": 0, "x1": 4096, "z1": 4096}
map_coords = None
full_map_level = None
if level_data and "mapCoord0" in level_data and "mapCoord1" in level_data:
mc0 = level_data["mapCoord0"]
mc1 = level_data["mapCoord1"]
if isinstance(mc0[0], list):
mc0 = mc0[0]
if isinstance(mc1[0], list):
mc1 = mc1[0]
if level_data:
try:
mc0, mc1, _ = select_map_coords(level_data)
except ValueError:
mc0 = mc1 = None
if level_data and mc0 is not None and mc1 is not None:
map_coords = {"x0": mc0[0], "z0": mc0[1], "x1": mc1[0], "z1": mc1[1]}
stem = Path(level_path).stem if level_path else ""
custom = level_data.get("customLevelMap", "")
if custom:
full_map_level = custom.rstrip("*")
full_map_level = _clean_map_key(custom)
else:
full_map_level = stem + "_map" if stem else None