-am (#1335)
This commit is contained in:
+140
-12
@@ -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,7 +3698,10 @@ 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:
|
||||
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(
|
||||
@@ -3593,23 +3719,25 @@ def export_replay_json(replay_path: Path) -> dict:
|
||||
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
|
||||
|
||||
|
||||
+13
-3
@@ -2034,9 +2034,19 @@ app.get('/api/match/minimap/:level', (req, res) => {
|
||||
if (!level || !/^[a-zA-Z0-9_]+$/.test(level)) {
|
||||
return res.status(400).json({ error: 'Invalid level name' });
|
||||
}
|
||||
const suffix = req.query.type === 'full' ? '.png' : '_tankmap.png';
|
||||
const minimapPath = path.join(__dirname, '..', '..', 'SHARED', 'MAPS', 'MINIMAPS', level + suffix);
|
||||
if (!fs.existsSync(minimapPath)) {
|
||||
const minimapsDir = path.join(__dirname, '..', '..', 'SHARED', 'MAPS', 'MINIMAPS');
|
||||
const names = req.query.type === 'full'
|
||||
? [level + '.png', level + '_map.png']
|
||||
: [level + '_tankmap.png', level + '.png', level + '_map.png'];
|
||||
let minimapPath = null;
|
||||
for (const name of [...new Set(names)]) {
|
||||
const candidate = path.join(minimapsDir, name);
|
||||
if (fs.existsSync(candidate)) {
|
||||
minimapPath = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!minimapPath) {
|
||||
return res.status(404).json({ error: 'Minimap not found' });
|
||||
}
|
||||
res.sendFile(minimapPath, {
|
||||
|
||||
Reference in New Issue
Block a user