Files
m2pack-secure/scripts/validate_runtime_scenarios.py

224 lines
7.9 KiB
Python
Executable File

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from known_issues import classify_issue_ids, load_known_issue_ids
SETTING_TEXTURESET_RE = re.compile(r"^TextureSet\s+(.+)$", re.IGNORECASE)
SETTING_ENV_RE = re.compile(r"^Environment\s+(.+)$", re.IGNORECASE)
@dataclass
class MapCheck:
pack: str
map_dir: str
setting_ok: bool
mapproperty_ok: bool
textureset_ref: str | None
textureset_exists: bool
environment_ref: str | None
environment_exists: bool
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Validate cross-pack runtime references for Metin2 map/world content."
)
parser.add_argument(
"--runtime-root",
type=Path,
required=True,
help="Client runtime root containing assets/.",
)
parser.add_argument(
"--pack",
action="append",
default=[],
help="Specific outdoor/world asset directory to validate. Repeatable. Defaults to common world packs.",
)
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()
prefixes = ["d:/", "d:"]
for prefix in prefixes:
if value.startswith(prefix):
value = value[len(prefix):]
break
return value.lstrip("/")
def resolve_textureset_path(runtime_assets: Path, ref: str) -> Path:
rel = normalize_virtual_path(ref)
if not rel.startswith("textureset/"):
rel = f"textureset/{rel}"
return runtime_assets / "textureset" / rel
def resolve_environment_path(runtime_assets: Path, ref: str) -> Path:
rel = normalize_virtual_path(ref)
if "/" not in rel:
rel = f"ymir work/environment/{rel}"
return runtime_assets / "ETC" / rel
def validate_map_dir(pack_name: str, map_dir: Path, runtime_assets: Path) -> MapCheck:
setting_path = map_dir / "setting.txt"
mapproperty_path = map_dir / "mapproperty.txt"
textureset_ref = None
environment_ref = None
textureset_exists = False
environment_exists = False
if setting_path.is_file():
for raw_line in setting_path.read_text(encoding="utf-8", errors="ignore").splitlines():
line = raw_line.strip()
if not line:
continue
texture_match = SETTING_TEXTURESET_RE.match(line)
if texture_match:
textureset_ref = texture_match.group(1).strip()
textureset_exists = resolve_textureset_path(runtime_assets, textureset_ref).is_file()
continue
environment_match = SETTING_ENV_RE.match(line)
if environment_match:
environment_ref = environment_match.group(1).strip()
environment_exists = resolve_environment_path(runtime_assets, environment_ref).is_file()
return MapCheck(
pack=pack_name,
map_dir=map_dir.name,
setting_ok=setting_path.is_file(),
mapproperty_ok=mapproperty_path.is_file(),
textureset_ref=textureset_ref,
textureset_exists=textureset_exists if textureset_ref else False,
environment_ref=environment_ref,
environment_exists=environment_exists if environment_ref else False,
)
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}")
packs = args.pack or [
"OutdoorA1",
"OutdoorA2",
"OutdoorA3",
"OutdoorB1",
"OutdoorB3",
"OutdoorC1",
"OutdoorC3",
"OutdoorSnow1",
"outdoordesert1",
"outdoorflame1",
"outdoorfielddungeon1",
]
checks: list[MapCheck] = []
failures: list[str] = []
issue_map: dict[str, str] = {}
for pack_name in packs:
pack_dir = runtime_assets / pack_name
if not pack_dir.is_dir():
issue_id = f"world:pack_dir:{pack_name}"
message = f"missing pack dir: {pack_name}"
failures.append(message)
issue_map[issue_id] = message
continue
map_dirs = sorted([p for p in pack_dir.iterdir() if p.is_dir()])
if not map_dirs:
issue_id = f"world:map_dirs:{pack_name}"
message = f"no map dirs in pack: {pack_name}"
failures.append(message)
issue_map[issue_id] = message
continue
for map_dir in map_dirs:
check = validate_map_dir(pack_name, map_dir, runtime_assets)
checks.append(check)
if not check.setting_ok:
issue_id = f"world:setting:{pack_name}/{map_dir.name}"
message = f"{pack_name}/{map_dir.name}: missing setting.txt"
failures.append(message)
issue_map[issue_id] = message
if not check.mapproperty_ok:
issue_id = f"world:mapproperty:{pack_name}/{map_dir.name}"
message = f"{pack_name}/{map_dir.name}: missing mapproperty.txt"
failures.append(message)
issue_map[issue_id] = message
if not check.textureset_ref or not check.textureset_exists:
issue_id = f"world:textureset:{pack_name}/{map_dir.name}:{normalize_virtual_path(check.textureset_ref or '')}"
message = f"{pack_name}/{map_dir.name}: missing textureset target for {check.textureset_ref!r}"
failures.append(message)
issue_map[issue_id] = message
if not check.environment_ref or not check.environment_exists:
issue_id = f"world:environment:{pack_name}/{map_dir.name}:{normalize_virtual_path(check.environment_ref or '')}"
message = f"{pack_name}/{map_dir.name}: missing environment target for {check.environment_ref!r}"
failures.append(message)
issue_map[issue_id] = message
observed_issue_ids = set(issue_map.keys())
known_path, known_issue_ids = load_known_issue_ids(__file__, "world", 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_map_dirs": len(checks),
"packs": packs,
"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_map_dirs={result['checked_map_dirs']}")
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())