84 lines
2.6 KiB
Python
Executable file
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)
|