"""Command-line interface for diagnostics and controlled runtime experiments.""" from __future__ import annotations import argparse import json from pathlib import Path import sys from typing import Sequence from .assbin import ( AssbinError, extract_embedded_texture, inspect_wrapped_assbin, normalize_wrapped_assbin_textures, ) from .config import ConfigError, load_config from .experiment import run_experiment from .filesystem import verify_known_game_hashes from .ini import IniError, override_ini_value from .logs import parse_native_log from .process import collect_environment from .testcase import TestcaseError, load_testcase def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="ptdiag") parser.add_argument("--config", type=Path, help="path to local.toml") subcommands = parser.add_subparsers(dest="command", required=True) subcommands.add_parser("env", help="inspect host and configured paths") subcommands.add_parser("verify", help="verify environment and known core hashes") log_parser = subcommands.add_parser("log", help="native game-log operations") log_commands = log_parser.add_subparsers(dest="log_command", required=True) analyze = log_commands.add_parser("analyze", help="parse a native game log") analyze.add_argument("path", type=Path) scene_parser = subcommands.add_parser("scene", help="wrapped scene-cache operations") scene_commands = scene_parser.add_subparsers(dest="scene_command", required=True) inspect = scene_commands.add_parser( "inspect", help="inspect an XOR-wrapped Assimp .rovug cache", ) inspect.add_argument("path", type=Path) inspect.add_argument("--output", type=Path, help="also write the complete JSON result") extract = scene_commands.add_parser( "extract-texture", help="extract and verify one embedded texture from a .rovug cache", ) extract.add_argument("path", type=Path) extract.add_argument("index", type=int) extract.add_argument("output", type=Path) normalize = scene_commands.add_parser( "normalize-textures", help="write a cache with selected 16-bit RGB PNGs normalized to 8-bit", ) normalize.add_argument("path", type=Path) normalize.add_argument("output", type=Path) normalize.add_argument("indices", type=int, nargs="+") ini_parser = subcommands.add_parser("ini", help="verified INI operations") ini_commands = ini_parser.add_subparsers(dest="ini_command", required=True) ini_override = ini_commands.add_parser( "override", help="replace one exact INI value after checking its expected value", ) ini_override.add_argument("path", type=Path) ini_override.add_argument("section") ini_override.add_argument("key") ini_override.add_argument("expected") ini_override.add_argument("value") run_parser = subcommands.add_parser("run", help="run a controlled testcase") run_parser.add_argument("testcase", help="testcase name or TOML path") run_parser.add_argument( "--dry-run", action="store_true", help="write and validate the complete plan without launching Game.exe", ) run_parser.add_argument( "--results-root", type=Path, help="override the result-directory root", ) return parser def main(argv: Sequence[str] | None = None) -> None: raise SystemExit(run(argv)) def run(argv: Sequence[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) if args.command == "log" and args.log_command == "analyze": try: analysis = parse_native_log(args.path) except OSError as exc: _print_json({"ok": False, "error": str(exc)}) return 2 _print_json({"ok": True, **analysis.to_summary()}) return 0 if args.command == "scene" and args.scene_command == "inspect": try: inspection = inspect_wrapped_assbin(args.path) except (AssbinError, OSError) as exc: _print_json({"ok": False, "error": str(exc)}) return 2 payload = {"ok": True, **inspection.to_dict()} if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) with args.output.open("w", encoding="utf-8") as stream: json.dump(payload, stream, indent=2, sort_keys=True) stream.write("\n") _print_json(payload) return 0 if args.command == "scene" and args.scene_command == "extract-texture": try: inspection = inspect_wrapped_assbin(args.path) extraction = extract_embedded_texture( args.path, inspection, args.index, args.output, ) except (AssbinError, OSError) as exc: _print_json({"ok": False, "error": str(exc)}) return 2 _print_json({"ok": True, **extraction}) return 0 if args.command == "scene" and args.scene_command == "normalize-textures": try: normalization = normalize_wrapped_assbin_textures( args.path, args.output, tuple(args.indices), ) except (AssbinError, OSError) as exc: _print_json({"ok": False, "error": str(exc)}) return 2 _print_json({"ok": True, **normalization}) return 0 if args.command == "ini" and args.ini_command == "override": try: result = override_ini_value( args.path, args.section, args.key, args.expected, args.value, ) except (IniError, OSError) as exc: _print_json({"ok": False, "error": str(exc)}) return 2 _print_json(result) return 0 try: config = load_config(args.config) except ConfigError as exc: _print_json({"ok": False, "error": str(exc)}) return 2 if args.command == "run": try: testcase = load_testcase(args.testcase) outcome = run_experiment( config, testcase, dry_run=args.dry_run, results_root=args.results_root, ) except (TestcaseError, OSError, ValueError) as exc: _print_json({"ok": False, "error": str(exc)}) return 2 _print_json( { "ok": outcome.summary.get("ok", False), "status": outcome.summary.get("status"), "result_directory": str(outcome.result_directory), } ) return outcome.exit_code environment = collect_environment(config) if args.command == "env": _print_json(environment) return 0 if environment["ok"] else 1 hashes = verify_known_game_hashes(config.paths.live_game) hashes_ok = all(item.ok for item in hashes) payload = { "ok": bool(environment["ok"]) and hashes_ok, "environment": environment, "hashes": [item.to_dict() for item in hashes], } _print_json(payload) return 0 if payload["ok"] else 1 def _print_json(value: object) -> None: json.dump(value, sys.stdout, indent=2, sort_keys=True) sys.stdout.write("\n")