Init
This commit is contained in:
203
ptdiag/cli.py
Normal file
203
ptdiag/cli.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user