#!/usr/bin/env python3 """Validate generated character asset manifest and PNG files.""" from __future__ import annotations import json import struct import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DATA = ROOT / "data" PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" def load_json(path: Path) -> dict: with path.open("r", encoding="utf-8") as handle: return json.load(handle) def fail(message: str) -> None: print(f"ERROR: {message}", file=sys.stderr) raise SystemExit(1) def png_dimensions(path: Path) -> tuple[int, int]: with path.open("rb") as handle: signature = handle.read(8) if signature != PNG_SIGNATURE: fail(f"{path} is not a PNG file") length_bytes = handle.read(4) chunk_type = handle.read(4) if len(length_bytes) != 4 or chunk_type != b"IHDR": fail(f"{path} is missing a PNG IHDR chunk") length = struct.unpack(">I", length_bytes)[0] if length < 8: fail(f"{path} has an invalid PNG IHDR chunk") width, height = struct.unpack(">II", handle.read(8)) return width, height def main() -> None: characters_doc = load_json(DATA / "characters.json") assets_doc = load_json(DATA / "character_assets.json") character_ids = [character["id"] for character in characters_doc["characters"]] character_id_set = set(character_ids) asset_entries = assets_doc["assets"] asset_ids = [entry["character_id"] for entry in asset_entries] if set(asset_ids) != character_id_set: missing = sorted(character_id_set.difference(asset_ids)) extra = sorted(set(asset_ids).difference(character_id_set)) fail(f"asset manifest mismatch; missing={missing}, extra={extra}") if len(asset_ids) != len(set(asset_ids)): fail("asset manifest contains duplicate character ids") minimum = assets_doc["minimum_dimensions"] min_width = minimum["width"] min_height = minimum["height"] accepted = 0 reference = ROOT / assets_doc["style"]["reference"] if not reference.exists(): fail(f"style reference is missing: {reference}") png_dimensions(reference) for entry in asset_entries: status = entry["status"] if status not in {"accepted", "needs_revision", "pending", "rejected"}: fail(f"{entry['character_id']} has invalid status {status}") if status not in {"accepted", "needs_revision"}: continue if status == "accepted": accepted += 1 if "path" not in entry: fail(f"{entry['character_id']} has status {status} but no path") path = ROOT / entry["path"] if not path.exists(): fail(f"{entry['character_id']} asset is missing: {path}") width, height = png_dimensions(path) if width < min_width or height < min_height: fail( f"{entry['character_id']} asset is too small: " f"{width}x{height}, minimum {min_width}x{min_height}" ) revision_count = sum( 1 for entry in asset_entries if entry["status"] == "needs_revision" ) print( "Validated character asset manifest: " f"{accepted}/{len(asset_entries)} accepted, " f"{revision_count} needs revision." ) if __name__ == "__main__": main()