204 lines
6.3 KiB
Python
204 lines
6.3 KiB
Python
"""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")
|