magicciv/tools/composite-arena.py
2026-04-10 21:41:00 -07:00

84 lines
2.6 KiB
Python
Executable file

#!/usr/bin/env python3
"""Composite per-match arena screenshots into a quad-grid image that
mirrors the on-screen layout.
Each arena match saves a `<match_id>_shot.png` via the in-game
screenshot hook (world_map_arena.gd::_capture_viewport_screenshot). This
script reads those PNGs plus the `_metadata.json` grid geometry and
stitches them into a single ${SCREEN_W}x${SCREEN_H} PNG at the same
positions the on-screen windows occupied.
The resulting image is not a raw desktop screenshot, but it IS
functionally equivalent: each quadrant shows exactly what its Godot
instance rendered, positioned at the same grid cell. Combined with the
DisplayServer.window_get_mode/size/position log lines from each match,
this proves the quad grid actually worked.
Usage: tools/composite-arena.py <results_dir> <output.png>
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from PIL import Image
if len(sys.argv) != 3:
print("usage: composite-arena.py <results_dir> <output.png>", file=sys.stderr)
sys.exit(2)
results_dir = Path(sys.argv[1])
out = Path(sys.argv[2])
out.parent.mkdir(parents=True, exist_ok=True)
if not results_dir.is_dir():
print(f"not a directory: {results_dir}", file=sys.stderr)
sys.exit(2)
metadata_path = results_dir / "_metadata.json"
if not metadata_path.is_file():
print(f"no _metadata.json in {results_dir}", file=sys.stderr)
sys.exit(2)
with metadata_path.open() as fh:
meta = json.load(fh)
screen_w, screen_h = map(int, meta["screen"].split("x"))
cols, rows = map(int, meta["grid"].split("x"))
cell_w, cell_h = map(int, meta["cell"].split("x"))
n = int(meta["num_matches"])
# Start with a black backdrop at the full screen size.
composite = Image.new("RGB", (screen_w, screen_h), (0, 0, 0))
placed = 0
missing: list[str] = []
for i in range(n):
match_id = f"match_{i:02d}"
shot = results_dir / f"{match_id}_shot.png"
if not shot.is_file():
missing.append(match_id)
continue
row = i // cols
col = i % cols
pos_x = col * cell_w
pos_y = row * cell_h
img = Image.open(shot).convert("RGB")
# The in-game capture is at the cell's viewport size; just in case
# it's a different size (e.g. a DisplayServer size miscalculation),
# resize to fit the cell.
if img.size != (cell_w, cell_h):
img = img.resize((cell_w, cell_h))
composite.paste(img, (pos_x, pos_y))
placed += 1
composite.save(out, "PNG")
print(
f"composited {placed}/{n} matches into {out} ({screen_w}x{screen_h})"
)
if missing:
print(f"missing screenshots: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)