164 lines
5.4 KiB
Python
Executable File
164 lines
5.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
|
|
from known_issues import classify_issue_ids, load_known_issue_ids
|
|
|
|
|
|
SOUND_DATA_RE = re.compile(r'^SoundData\d+\s+[0-9.]+\s+"([^"]+)"', re.IGNORECASE)
|
|
|
|
|
|
@dataclass
|
|
class AudioCheck:
|
|
file: str
|
|
references: list[str]
|
|
missing_references: list[str]
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate .mss audio script references against the runtime asset set."
|
|
)
|
|
parser.add_argument(
|
|
"--runtime-root",
|
|
type=Path,
|
|
required=True,
|
|
help="Client runtime root containing assets/.",
|
|
)
|
|
parser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Emit JSON output.",
|
|
)
|
|
parser.add_argument(
|
|
"--known-issues",
|
|
type=str,
|
|
default=None,
|
|
help="Optional known issues baseline JSON. Defaults to repo known_issues/runtime_known_issues.json if present.",
|
|
)
|
|
parser.add_argument(
|
|
"--strict-known-issues",
|
|
action="store_true",
|
|
help="Also fail when the known-issues baseline contains stale entries not observed anymore.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def normalize_virtual_path(value: str) -> str:
|
|
value = value.strip().strip('"').replace("\\", "/").lower()
|
|
for prefix in ("d:/", "d:"):
|
|
if value.startswith(prefix):
|
|
value = value[len(prefix):]
|
|
break
|
|
return value.lstrip("/")
|
|
|
|
|
|
def build_asset_index(runtime_assets: Path) -> set[str]:
|
|
index: set[str] = set()
|
|
for path in runtime_assets.rglob("*"):
|
|
if not path.is_file():
|
|
continue
|
|
try:
|
|
rel = path.relative_to(runtime_assets)
|
|
except ValueError:
|
|
continue
|
|
rel_posix = rel.as_posix().lower()
|
|
index.add(rel_posix)
|
|
if len(rel.parts) >= 2:
|
|
index.add(Path(*rel.parts[1:]).as_posix().lower())
|
|
return index
|
|
|
|
|
|
def parse_mss_references(path: Path) -> list[str]:
|
|
references: list[str] = []
|
|
for raw_line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
line = raw_line.strip()
|
|
match = SOUND_DATA_RE.match(line)
|
|
if match:
|
|
references.append(match.group(1))
|
|
return references
|
|
|
|
|
|
def validate_mss(path: Path, runtime_assets: Path, asset_index: set[str]) -> AudioCheck:
|
|
rel = path.relative_to(runtime_assets).as_posix()
|
|
missing: list[str] = []
|
|
references = parse_mss_references(path)
|
|
|
|
for reference in references:
|
|
normalized = normalize_virtual_path(reference)
|
|
if normalized and normalized not in asset_index:
|
|
missing.append(reference)
|
|
|
|
return AudioCheck(file=rel, references=references, missing_references=sorted(set(missing)))
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
runtime_root = args.runtime_root.resolve()
|
|
runtime_assets = runtime_root / "assets"
|
|
if not runtime_assets.is_dir():
|
|
raise SystemExit(f"assets dir not found: {runtime_assets}")
|
|
|
|
asset_index = build_asset_index(runtime_assets)
|
|
checks: list[AudioCheck] = []
|
|
failures: list[str] = []
|
|
issue_map: dict[str, str] = {}
|
|
referenced_audio_files: set[str] = set()
|
|
|
|
for path in sorted(runtime_assets.rglob("*.mss")):
|
|
check = validate_mss(path, runtime_assets, asset_index)
|
|
checks.append(check)
|
|
for missing in check.missing_references:
|
|
normalized = normalize_virtual_path(missing)
|
|
issue_id = f"audio:reference:{check.file}:{normalized}"
|
|
message = f"{check.file}: missing reference {missing}"
|
|
failures.append(message)
|
|
issue_map[issue_id] = message
|
|
for reference in check.references:
|
|
normalized = normalize_virtual_path(reference)
|
|
if normalized:
|
|
referenced_audio_files.add(normalized)
|
|
|
|
observed_issue_ids = set(issue_map.keys())
|
|
known_path, known_issue_ids = load_known_issue_ids(__file__, "audio", args.known_issues)
|
|
known_observed, unexpected_issue_ids, stale_known_issue_ids = classify_issue_ids(observed_issue_ids, known_issue_ids)
|
|
|
|
result = {
|
|
"ok": not unexpected_issue_ids and (not args.strict_known_issues or not stale_known_issue_ids),
|
|
"checked_files": len(checks),
|
|
"referenced_audio_files": len(referenced_audio_files),
|
|
"failures": failures,
|
|
"issue_ids": sorted(observed_issue_ids),
|
|
"known_issue_ids": sorted(known_observed),
|
|
"unexpected_issue_ids": sorted(unexpected_issue_ids),
|
|
"stale_known_issue_ids": sorted(stale_known_issue_ids),
|
|
"unexpected_failures": [issue_map[issue_id] for issue_id in sorted(unexpected_issue_ids)],
|
|
"stale_known_failures": sorted(stale_known_issue_ids),
|
|
"known_issues_path": str(known_path) if known_path else None,
|
|
"checks": [asdict(check) for check in checks],
|
|
}
|
|
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
print(
|
|
f"ok={result['ok']} checked_files={result['checked_files']} "
|
|
f"referenced_audio_files={result['referenced_audio_files']}"
|
|
)
|
|
for failure in result["unexpected_failures"]:
|
|
print(f"FAIL: {failure}")
|
|
for issue_id in result["stale_known_issue_ids"]:
|
|
print(f"STALE: {issue_id}")
|
|
|
|
return 0 if result["ok"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|