99 lines
3.5 KiB
Python
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())
|