Files
aetherbound-guild/tools/verify_ui_asset_provenance.py
T

99 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Verify every Phase 7 HTML game asset is a byte-identical known candidate."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ASSET_ROOT = ROOT / "prototype/site/assets/game"
MANIFEST_PATH = ASSET_ROOT / "asset-provenance.json"
def digest(path: Path) -> str:
value = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
value.update(chunk)
return value.hexdigest()
def main() -> int:
manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
expected: set[Path] = set()
counts: dict[str, int] = {"actors": 0, "motion_recruit": 0, "motion_enemy": 0}
errors: list[str] = []
pairs: list[tuple[str, Path, Path]] = []
for entry in manifest["actors"]:
target = ASSET_ROOT / entry["target"]
source = ROOT / entry["source"]
pairs.append(("actors", target, source))
for motion in manifest.get("motion", []):
family = "motion_recruit" if motion["target_root"].endswith("recruit") else "motion_enemy"
for target_state, source_state in motion["states"].items():
target_root = ASSET_ROOT / motion["target_root"] / target_state
source_root = ROOT / motion["source_root"] / source_state / "frames"
source_files = sorted(source_root.glob("*.png"))
target_files = sorted(target_root.glob("*.png"))
source_names = [path.name for path in source_files]
target_names = [path.name for path in target_files]
if source_names != target_names:
errors.append(
f"motion frame set mismatch: {target_state} "
f"source={source_names} target={target_names}"
)
for source in source_files:
pairs.append((family, target_root / source.name, source))
for family in manifest["families"]:
name = family["name"]
counts[name] = 0
for filename in family["files"]:
target = ASSET_ROOT / family["target_root"] / filename
source = ROOT / family["source_root"] / filename
pairs.append((name, target, source))
for family, target, source in pairs:
expected.add(target)
if not source.is_file():
errors.append(f"missing source: {source.relative_to(ROOT)}")
continue
if not target.is_file():
errors.append(f"missing target: {target.relative_to(ROOT)}")
continue
if digest(source) != digest(target):
errors.append(
f"hash mismatch: {target.relative_to(ROOT)} <- {source.relative_to(ROOT)}"
)
continue
counts[family] += 1
actual = set(ASSET_ROOT.rglob("*.png"))
for path in sorted(actual - expected):
errors.append(f"unmapped target: {path.relative_to(ROOT)}")
for path in sorted(expected - actual):
errors.append(f"manifest target absent: {path.relative_to(ROOT)}")
if errors:
for error in errors:
print(f"ERROR: {error}")
return 1
total = sum(counts.values())
print(
"UI_ASSET_PROVENANCE_OK "
f"actors={counts['actors']} equipment={counts['equipment']} "
f"vfx={counts['vfx']} motion_recruit={counts['motion_recruit']} "
f"motion_enemy={counts['motion_enemy']} total={total}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())