squad-proto/tools/asset_coverage.py

73 lines
2.6 KiB
Python
Raw Permalink Normal View History

"""Text-only coverage gate for every visual kind currently present in the game."""
from __future__ import annotations
import argparse
from collections import defaultdict
from pathlib import Path
import sys
import yaml
ROOT = Path(__file__).resolve().parents[1]
COVERAGE = ROOT / "assets/content_coverage.yaml"
MANIFESTS = ROOT / "assets/manifests"
def declared_assets() -> set[str]:
result: set[str] = set()
for path in sorted(MANIFESTS.glob("*.yaml")):
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
result.update(key for key, value in raw.items()
if key not in {"version", "defaults"} and isinstance(value, dict))
return result
def requirements(spec: dict) -> dict[str, list[str]]:
result: dict[str, list[str]] = defaultdict(list)
agent = spec["agents"]
for slot, components in agent["components"].items():
for component in components:
for animation in agent["animations"]:
result[f"agent/{slot}"].append(f"agent_{component}_{animation}")
for kind in spec["enemies"]["kinds"]:
for animation in spec["enemies"]["animations"]:
result["enemy"].append(f"{kind}_{animation}")
result["environment"].extend(spec["environment"]["required"])
result["item_icon"].extend(spec["item_icons"]["required"])
return result
def report(release: bool) -> int:
spec = yaml.safe_load(COVERAGE.read_text(encoding="utf-8"))
if spec.get("version") != 1:
raise ValueError("assets/content_coverage.yaml: unsupported version")
have = declared_assets()
groups = requirements(spec)
missing_total = 0
print(f"{'GROUP':<20} {'HAVE':>5} {'NEED':>5} {'MISSING':>7}")
print("-" * 40)
for group, wanted in groups.items():
missing = sorted(set(wanted) - have)
missing_total += len(missing)
print(f"{group:<20} {len(wanted)-len(missing):>5} {len(wanted):>5} {len(missing):>7}")
for asset in missing:
print(f" - {asset}")
covered = sum(len(v) for v in groups.values()) - missing_total
total = sum(len(v) for v in groups.values())
print(f"coverage: {covered}/{total}; missing={missing_total}")
return 1 if release and missing_total else 0
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--release", action="store_true", help="fail if any required asset is absent")
return report(parser.parse_args().release)
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, ValueError, KeyError) as error:
print(f"ERROR: {error}", file=sys.stderr)
raise SystemExit(2)