Added more folders and restructured classes inside them.
This commit is contained in:
918
src/ptlinux/cli.py
Normal file
918
src/ptlinux/cli.py
Normal file
@@ -0,0 +1,918 @@
|
||||
"""Command-line interface for installation, updates, repair, and play."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from contextlib import AbstractContextManager, nullcontext
|
||||
from dataclasses import asdict, replace
|
||||
from datetime import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
from typing import Callable, Sequence
|
||||
from uuid import uuid4
|
||||
|
||||
from .diagnostics.native_logs import (
|
||||
LogFileState,
|
||||
classify_asset_references,
|
||||
newest_current_native_log,
|
||||
parse_current_run_log,
|
||||
snapshot_native_logs,
|
||||
)
|
||||
|
||||
from . import __version__
|
||||
from .application import common as _application_helpers
|
||||
from .application.commands import health as _health_commands
|
||||
from .application.commands import installation as _installation_commands
|
||||
from .application.commands import launcher_update as _launcher_update_commands
|
||||
from .application.commands import play as _play_commands
|
||||
from .application.commands import settings as _settings_commands
|
||||
from .command import CommandError, CommandOutcome, CommandRunner
|
||||
from .container_host import (
|
||||
ContainerHostReport,
|
||||
container_host_error,
|
||||
inspect_container_host,
|
||||
repair_container_host,
|
||||
repair_container_host_after_update,
|
||||
)
|
||||
from .depot import (
|
||||
DownloadOptions,
|
||||
SyncProgress,
|
||||
read_cached_depot_launch_catalog,
|
||||
read_current_depot_file_catalog,
|
||||
preflight_managed_game,
|
||||
select_depot_start_option,
|
||||
sync_managed_game,
|
||||
)
|
||||
from .doctor import inspect_host
|
||||
from .download import OFFICIAL_LAUNCHER_URL, fetch_official_launcher
|
||||
from .importer import GameImportResult, discover_existing_game, import_existing_game
|
||||
from .import_state import game_import_requires_verification, mark_game_import_verified
|
||||
from .launcher import (
|
||||
LauncherError,
|
||||
WineTools,
|
||||
configure_launcher_prefix,
|
||||
find_wine_tools,
|
||||
launch_official_updater,
|
||||
prepare_launcher_binary,
|
||||
)
|
||||
from .locking import (
|
||||
LauncherPlayLease,
|
||||
launcher_operation_lock,
|
||||
launcher_play_lock,
|
||||
managed_game_process_is_active,
|
||||
)
|
||||
from .login_diagnostics import inspect_login_environment
|
||||
from .login_trace import DEFAULT_TRACE_TIMEOUT_SECONDS, LOGIN_TRACE_DISABLED_REASON
|
||||
from .overlay import cache_compatibility_binaries, prepare_runtime_view
|
||||
from .paths import AppPaths
|
||||
from .runtime import (
|
||||
RuntimeErrorPT,
|
||||
game_runtime_ready,
|
||||
prepare_game_runtime,
|
||||
run_game,
|
||||
select_runtime,
|
||||
)
|
||||
from .settings import Settings, ensure_settings, load_settings, reset_settings
|
||||
from .self_update import (
|
||||
check_for_launcher_update,
|
||||
install_launcher_update,
|
||||
rollback_launcher_update,
|
||||
update_check_to_dict,
|
||||
verify_launcher_update_channel,
|
||||
)
|
||||
from .update_resolver import STABLE_CHANNEL
|
||||
from .telemetry import (
|
||||
classify_network_failure,
|
||||
current_telemetry,
|
||||
initialize_shared_telemetry,
|
||||
network_http_status,
|
||||
shutdown_shared_telemetry,
|
||||
)
|
||||
from .system_identity import is_running_inside_steam_runtime
|
||||
from .umu import (
|
||||
UmuBootstrapResult,
|
||||
find_umu_launcher,
|
||||
provision_umu,
|
||||
provision_umu_for_startup,
|
||||
)
|
||||
from .window_diagnostics import (
|
||||
WINDOW_DIAGNOSTICS_ENV,
|
||||
WINDOW_DIAGNOSTICS_SESSION_ENV,
|
||||
parse_window_diagnostic,
|
||||
)
|
||||
|
||||
|
||||
class _GameLaunchInterruptionReported(KeyboardInterrupt):
|
||||
"""Mark an in-game interrupt whose cancellation telemetry was already sent."""
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="pristontale-eu",
|
||||
description="PristonTale EU all-in-one Linux installer and clean launcher",
|
||||
)
|
||||
# Python 3.14 enables ANSI-colored argparse output by default. Keep the CLI's
|
||||
# help and error text stable for redirected output and downstream tooling.
|
||||
if hasattr(parser, "color"):
|
||||
parser.color = False
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"%(prog)s {__version__}",
|
||||
)
|
||||
parser.add_argument("--json", action="store_true", help="emit machine-readable output")
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
subparsers.add_parser("gui", help="open the native Linux dashboard")
|
||||
subparsers.add_parser("doctor", help="check cross-distribution dependencies")
|
||||
container_host = subparsers.add_parser(
|
||||
"container-host",
|
||||
help="check Bubblewrap and Ubuntu AppArmor for the Steam Runtime",
|
||||
)
|
||||
container_host.add_argument(
|
||||
"--repair",
|
||||
action="store_true",
|
||||
help="repair only recognized Ubuntu AppArmor profile states",
|
||||
)
|
||||
subparsers.add_parser(
|
||||
"runtime-bootstrap",
|
||||
help="download and verify the Steam Runtime launcher helper",
|
||||
)
|
||||
login_diagnostics = subparsers.add_parser(
|
||||
"login-diagnostics",
|
||||
help="check the client, runtime, and publisher login route without credentials",
|
||||
)
|
||||
login_diagnostics.add_argument(
|
||||
"--timeout",
|
||||
type=float,
|
||||
default=5.0,
|
||||
help="TCP connection timeout in seconds (default: 5)",
|
||||
)
|
||||
login_diagnostics.add_argument(
|
||||
"--runtime",
|
||||
choices=("configured", "auto", "proton7", "proton7-container", "umu"),
|
||||
default="configured",
|
||||
help=(
|
||||
"probe one runtime without changing settings or starting the game "
|
||||
"(default: configured)"
|
||||
),
|
||||
)
|
||||
|
||||
fetch = subparsers.add_parser("fetch", help="fetch and safely prepare the latest official launcher")
|
||||
fetch.add_argument("--dry-run", action="store_true")
|
||||
|
||||
setup = subparsers.add_parser("setup", help="configure Wine and .NET Desktop for the updater")
|
||||
setup.add_argument("--dry-run", action="store_true")
|
||||
setup.add_argument("--force-runtime", action="store_true")
|
||||
|
||||
update = subparsers.add_parser(
|
||||
"update",
|
||||
help="safely synchronize the official game depot without Wine",
|
||||
)
|
||||
update.add_argument("--dry-run", action="store_true")
|
||||
update.add_argument("--repair", action="store_true", help="hash-check every game file")
|
||||
import_options = update.add_mutually_exclusive_group()
|
||||
import_options.add_argument(
|
||||
"--seed-from",
|
||||
type=Path,
|
||||
help="import an existing game before updating",
|
||||
)
|
||||
import_options.add_argument(
|
||||
"--no-import",
|
||||
action="store_true",
|
||||
help="disable automatic existing-game discovery",
|
||||
)
|
||||
|
||||
sync = subparsers.add_parser(
|
||||
"sync",
|
||||
help="native official depot synchronization without bootstrap discovery",
|
||||
)
|
||||
sync.add_argument("--dry-run", action="store_true")
|
||||
sync.add_argument("--repair", action="store_true", help="hash-check every game file")
|
||||
|
||||
legacy_update = subparsers.add_parser(
|
||||
"wine-update",
|
||||
help="legacy official WPF updater (normally unnecessary)",
|
||||
)
|
||||
legacy_update.add_argument("--dry-run", action="store_true")
|
||||
legacy_update.add_argument("--force-runtime", action="store_true")
|
||||
legacy_import_options = legacy_update.add_mutually_exclusive_group()
|
||||
legacy_import_options.add_argument("--seed-from", type=Path)
|
||||
legacy_import_options.add_argument("--no-import", action="store_true")
|
||||
|
||||
import_game = subparsers.add_parser(
|
||||
"import-game",
|
||||
help="safely seed the managed installation from an existing game",
|
||||
)
|
||||
import_game.add_argument("source", type=Path)
|
||||
import_game.add_argument("--dry-run", action="store_true")
|
||||
|
||||
compatibility = subparsers.add_parser(
|
||||
"cache-compatibility",
|
||||
help="cache one exact verified Game.exe/game.dll compatibility pair",
|
||||
)
|
||||
compatibility.add_argument("source", type=Path)
|
||||
prepare = subparsers.add_parser("prepare", help="build the read-only runtime view and scene fix")
|
||||
prepare.add_argument("--game-dir", type=Path)
|
||||
|
||||
play = subparsers.add_parser("play", help="launch the game in the managed Proton window")
|
||||
play.add_argument("--game-dir", type=Path)
|
||||
play.add_argument(
|
||||
"--realm",
|
||||
help="launch an available realm from the last official depot manifest",
|
||||
)
|
||||
play.add_argument("--no-box", action="store_true", help="launch without X11 window styling")
|
||||
play.add_argument(
|
||||
"--publisher-dll",
|
||||
"--no-world-fix",
|
||||
action="store_true",
|
||||
dest="publisher_dll",
|
||||
help=(
|
||||
"compatibility option retained for older scripts; publisher code is "
|
||||
"always used without the retired world fix"
|
||||
),
|
||||
)
|
||||
play.add_argument("--dry-run", action="store_true")
|
||||
play.add_argument("--skip-runtime-setup", action="store_true")
|
||||
login_trace = subparsers.add_parser(
|
||||
"login-trace",
|
||||
help="retired unsafe authentication trace (always refuses to launch)",
|
||||
)
|
||||
login_trace.add_argument("--game-dir", type=Path)
|
||||
login_trace.add_argument("--no-box", action="store_true", help="launch without X11 window styling")
|
||||
login_trace.add_argument("--skip-runtime-setup", action="store_true")
|
||||
login_trace.add_argument(
|
||||
"--timeout",
|
||||
type=float,
|
||||
default=DEFAULT_TRACE_TIMEOUT_SECONDS,
|
||||
help="maximum game runtime in seconds (default: 600)",
|
||||
)
|
||||
|
||||
settings = subparsers.add_parser("settings", help="show, initialize, or reset settings")
|
||||
settings_action = settings.add_mutually_exclusive_group()
|
||||
settings_action.add_argument(
|
||||
"--init",
|
||||
action="store_true",
|
||||
help="create missing settings and migrate legacy files without resetting choices",
|
||||
)
|
||||
settings_action.add_argument(
|
||||
"--reset",
|
||||
action="store_true",
|
||||
help="explicitly replace all launcher settings with defaults",
|
||||
)
|
||||
|
||||
launcher_update = subparsers.add_parser(
|
||||
"launcher-update",
|
||||
help="check, verify, install, or roll back the native Linux launcher",
|
||||
)
|
||||
launcher_action = launcher_update.add_mutually_exclusive_group()
|
||||
launcher_action.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="authenticate the update manifest without installing it",
|
||||
)
|
||||
launcher_action.add_argument(
|
||||
"--verify",
|
||||
action="store_true",
|
||||
help="download and fully validate the published release without installing it",
|
||||
)
|
||||
launcher_action.add_argument(
|
||||
"--rollback",
|
||||
nargs="?",
|
||||
const="",
|
||||
metavar="VERSION",
|
||||
help=(
|
||||
"switch to VERSION from signed release history; without VERSION, "
|
||||
"switch to the locally retained previous release"
|
||||
),
|
||||
)
|
||||
launcher_update.add_argument(
|
||||
"--manifest-url",
|
||||
help="override the signed stable-manifest URL",
|
||||
)
|
||||
launcher_update.add_argument(
|
||||
"--public-key",
|
||||
type=Path,
|
||||
help="override the pinned launcher-update public key",
|
||||
)
|
||||
launcher_update.add_argument(
|
||||
"--history-url",
|
||||
help="override the signed launcher release-history URL",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(
|
||||
argv: Sequence[str] | None = None,
|
||||
*,
|
||||
_manage_telemetry: bool | None = None,
|
||||
) -> int:
|
||||
parser = build_parser()
|
||||
arguments = parser.parse_args(argv)
|
||||
command = arguments.command or "gui"
|
||||
paths = AppPaths.from_environment()
|
||||
manage_telemetry = argv is None if _manage_telemetry is None else _manage_telemetry
|
||||
telemetry_lifecycle = _start_command_telemetry(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
command,
|
||||
arguments,
|
||||
manage_telemetry,
|
||||
)
|
||||
telemetry = telemetry_lifecycle.telemetry
|
||||
telemetry_events = telemetry_lifecycle.events
|
||||
operation_started = telemetry_lifecycle.started_at
|
||||
try:
|
||||
returncode = _dispatch_command(parser, arguments, command, paths)
|
||||
except KeyboardInterrupt as exc:
|
||||
if command == "play":
|
||||
if (
|
||||
not isinstance(exc, _GameLaunchInterruptionReported)
|
||||
and not bool(getattr(arguments, "dry_run", False))
|
||||
):
|
||||
_report_game_launch_cancelled(
|
||||
telemetry,
|
||||
operation_started,
|
||||
stage="preflight",
|
||||
)
|
||||
else:
|
||||
_report_command_failure(
|
||||
telemetry,
|
||||
telemetry_events,
|
||||
command,
|
||||
operation_started,
|
||||
"Operation interrupted",
|
||||
stage="interrupted",
|
||||
)
|
||||
message = "Interrupted; PristonTale and its compatibility runtime were stopped."
|
||||
if arguments.json:
|
||||
_print_json({"ok": False, "error": message, "interrupted": True})
|
||||
else:
|
||||
print(f"\n{message}", file=sys.stderr)
|
||||
return 130
|
||||
except (ValueError, OSError, RuntimeError) as exc:
|
||||
_report_command_failure(
|
||||
telemetry,
|
||||
telemetry_events,
|
||||
command,
|
||||
operation_started,
|
||||
str(exc),
|
||||
stage=_failure_stage(command, exc),
|
||||
)
|
||||
if telemetry is not None and isinstance(exc, ValueError):
|
||||
telemetry.send_event(
|
||||
"configuration_error",
|
||||
severity="error",
|
||||
message=str(exc),
|
||||
metadata={"setting": "unknown", "reason": "invalid_value"},
|
||||
context={"operation": command, "stage": "configuration"},
|
||||
)
|
||||
if arguments.json:
|
||||
_print_json({"ok": False, "error": str(exc)})
|
||||
else:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
if isinstance(exc, CommandError):
|
||||
if exc.outcome.stdout:
|
||||
print(exc.outcome.stdout.rstrip(), file=sys.stderr)
|
||||
if exc.outcome.stderr:
|
||||
print(exc.outcome.stderr.rstrip(), file=sys.stderr)
|
||||
return 1
|
||||
except BaseException as exc:
|
||||
if telemetry is not None:
|
||||
telemetry.report_exception(
|
||||
"launcher_exception",
|
||||
exc,
|
||||
metadata={"command": command},
|
||||
context={"operation": command, "stage": "dispatch"},
|
||||
severity="critical",
|
||||
)
|
||||
_report_command_failure(
|
||||
telemetry,
|
||||
telemetry_events,
|
||||
command,
|
||||
operation_started,
|
||||
str(exc),
|
||||
stage="dispatch",
|
||||
)
|
||||
raise
|
||||
else:
|
||||
_report_command_result(
|
||||
telemetry,
|
||||
telemetry_events,
|
||||
command,
|
||||
operation_started,
|
||||
returncode,
|
||||
)
|
||||
return returncode
|
||||
finally:
|
||||
_shutdown_command_telemetry(sys.modules[__name__], telemetry_lifecycle)
|
||||
|
||||
|
||||
def _dispatch_command(
|
||||
parser: argparse.ArgumentParser,
|
||||
arguments: argparse.Namespace,
|
||||
command: str,
|
||||
paths: AppPaths,
|
||||
) -> int:
|
||||
if command == "gui":
|
||||
provision_umu_for_startup(paths)
|
||||
_container_host_startup_migration(paths)
|
||||
from .ui import run_dashboard
|
||||
|
||||
run_dashboard(paths)
|
||||
return 0
|
||||
if command == "doctor":
|
||||
return _doctor(arguments.json)
|
||||
if command == "container-host":
|
||||
return _container_host(arguments.repair, arguments.json)
|
||||
if command == "runtime-bootstrap":
|
||||
return _runtime_bootstrap(paths, arguments.json)
|
||||
if command == "login-diagnostics":
|
||||
return _login_diagnostics(
|
||||
paths,
|
||||
arguments.timeout,
|
||||
arguments.runtime,
|
||||
arguments.json,
|
||||
)
|
||||
if command == "fetch":
|
||||
return _fetch(paths, arguments.dry_run, arguments.json)
|
||||
if command == "setup":
|
||||
return _setup(
|
||||
paths,
|
||||
arguments.dry_run,
|
||||
arguments.force_runtime,
|
||||
arguments.json,
|
||||
)
|
||||
if command == "update":
|
||||
return _native_update(
|
||||
paths,
|
||||
arguments.dry_run,
|
||||
arguments.repair,
|
||||
arguments.seed_from,
|
||||
arguments.no_import,
|
||||
arguments.json,
|
||||
)
|
||||
if command == "sync":
|
||||
return _native_update(
|
||||
paths,
|
||||
arguments.dry_run,
|
||||
arguments.repair,
|
||||
None,
|
||||
True,
|
||||
arguments.json,
|
||||
)
|
||||
if command == "wine-update":
|
||||
return _wine_update(
|
||||
paths,
|
||||
arguments.dry_run,
|
||||
arguments.force_runtime,
|
||||
arguments.seed_from,
|
||||
arguments.no_import,
|
||||
arguments.json,
|
||||
)
|
||||
if command == "import-game":
|
||||
return _import_game(
|
||||
paths,
|
||||
arguments.source,
|
||||
arguments.dry_run,
|
||||
arguments.json,
|
||||
)
|
||||
if command == "cache-compatibility":
|
||||
result = cache_compatibility_binaries(paths, arguments.source)
|
||||
_emit({"ok": True, "compatibility": asdict(result)}, arguments.json)
|
||||
return 0
|
||||
if command == "prepare":
|
||||
return _prepare(paths, arguments.game_dir, arguments.json)
|
||||
if command == "play":
|
||||
return _play(
|
||||
paths,
|
||||
arguments.game_dir,
|
||||
arguments.no_box,
|
||||
arguments.dry_run,
|
||||
arguments.skip_runtime_setup,
|
||||
arguments.json,
|
||||
realm=arguments.realm,
|
||||
)
|
||||
if command == "login-trace":
|
||||
return _login_trace(
|
||||
paths,
|
||||
arguments.game_dir,
|
||||
arguments.no_box,
|
||||
arguments.skip_runtime_setup,
|
||||
arguments.timeout,
|
||||
arguments.json,
|
||||
)
|
||||
if command == "settings":
|
||||
return _settings(paths, arguments.init, arguments.reset, arguments.json)
|
||||
if command == "launcher-update":
|
||||
return _launcher_update(
|
||||
paths,
|
||||
arguments.check,
|
||||
arguments.verify,
|
||||
arguments.rollback,
|
||||
arguments.manifest_url,
|
||||
arguments.public_key,
|
||||
arguments.json,
|
||||
history_url=arguments.history_url,
|
||||
)
|
||||
parser.error(f"unknown command: {command}")
|
||||
|
||||
|
||||
_command_telemetry_events = _application_helpers.command_telemetry_events
|
||||
_start_command_telemetry = _application_helpers.start_command_telemetry
|
||||
_shutdown_command_telemetry = _application_helpers.shutdown_command_telemetry
|
||||
_report_command_result = _application_helpers.report_command_result
|
||||
_report_command_failure = _application_helpers.report_command_failure
|
||||
_report_game_launch_cancelled = _application_helpers.report_game_launch_cancelled
|
||||
_failure_stage = _application_helpers.failure_stage
|
||||
def _doctor(as_json: bool) -> int:
|
||||
return _health_commands.doctor(sys.modules[__name__], as_json)
|
||||
|
||||
|
||||
def _container_host(repair: bool, as_json: bool) -> int:
|
||||
return _health_commands.container_host(sys.modules[__name__], repair, as_json)
|
||||
|
||||
|
||||
_container_host_ok = _health_commands.container_host_ok
|
||||
|
||||
|
||||
def _container_host_startup_migration(paths: AppPaths) -> None:
|
||||
_health_commands.container_host_startup_migration(sys.modules[__name__], paths)
|
||||
|
||||
|
||||
def _runtime_bootstrap(paths: AppPaths, as_json: bool) -> int:
|
||||
return _health_commands.runtime_bootstrap(sys.modules[__name__], paths, as_json)
|
||||
|
||||
|
||||
def _login_diagnostics(
|
||||
paths: AppPaths,
|
||||
timeout_seconds: float,
|
||||
runtime_choice: str,
|
||||
as_json: bool,
|
||||
) -> int:
|
||||
return _health_commands.login_diagnostics(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
timeout_seconds,
|
||||
runtime_choice,
|
||||
as_json,
|
||||
)
|
||||
|
||||
def _fetch(paths: AppPaths, dry_run: bool, as_json: bool) -> int:
|
||||
return _installation_commands.fetch(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
dry_run,
|
||||
as_json,
|
||||
)
|
||||
|
||||
|
||||
def _setup(
|
||||
paths: AppPaths,
|
||||
dry_run: bool,
|
||||
force_runtime: bool,
|
||||
as_json: bool,
|
||||
) -> int:
|
||||
return _installation_commands.setup(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
dry_run,
|
||||
force_runtime,
|
||||
as_json,
|
||||
)
|
||||
|
||||
|
||||
def _native_update(
|
||||
paths: AppPaths,
|
||||
dry_run: bool,
|
||||
repair: bool,
|
||||
seed_from: Path | None,
|
||||
no_import: bool,
|
||||
as_json: bool,
|
||||
) -> int:
|
||||
return _installation_commands.native_update(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
dry_run,
|
||||
repair,
|
||||
seed_from,
|
||||
no_import,
|
||||
as_json,
|
||||
)
|
||||
|
||||
|
||||
def _wine_update(
|
||||
paths: AppPaths,
|
||||
dry_run: bool,
|
||||
force_runtime: bool,
|
||||
seed_from: Path | None,
|
||||
no_import: bool,
|
||||
as_json: bool,
|
||||
) -> int:
|
||||
return _installation_commands.wine_update(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
dry_run,
|
||||
force_runtime,
|
||||
seed_from,
|
||||
no_import,
|
||||
as_json,
|
||||
)
|
||||
|
||||
|
||||
def _configure_launcher_runtime(
|
||||
paths: AppPaths,
|
||||
tools: WineTools,
|
||||
runner: CommandRunner,
|
||||
*,
|
||||
force_runtime: bool,
|
||||
) -> list[CommandOutcome]:
|
||||
return _installation_commands.configure_launcher_runtime(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
tools,
|
||||
runner,
|
||||
force_runtime=force_runtime,
|
||||
)
|
||||
|
||||
|
||||
_print_sync_progress = _installation_commands.print_sync_progress
|
||||
_configured_download_options = _installation_commands.configured_download_options
|
||||
_print_operation_status = _installation_commands.print_operation_status
|
||||
|
||||
|
||||
def _import_game(
|
||||
paths: AppPaths,
|
||||
source: Path,
|
||||
dry_run: bool,
|
||||
as_json: bool,
|
||||
) -> int:
|
||||
return _installation_commands.import_game(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
source,
|
||||
dry_run,
|
||||
as_json,
|
||||
)
|
||||
|
||||
|
||||
def _maybe_import_game(
|
||||
paths: AppPaths,
|
||||
settings: Settings,
|
||||
seed_from: Path | None,
|
||||
runner: CommandRunner,
|
||||
*,
|
||||
announce: bool,
|
||||
) -> GameImportResult | None:
|
||||
return _installation_commands.maybe_import_game(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
settings,
|
||||
seed_from,
|
||||
runner,
|
||||
announce=announce,
|
||||
)
|
||||
|
||||
|
||||
def _import_with_telemetry(
|
||||
paths: AppPaths,
|
||||
source: Path,
|
||||
runner: CommandRunner,
|
||||
) -> GameImportResult:
|
||||
return _installation_commands.import_with_telemetry(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
source,
|
||||
runner,
|
||||
)
|
||||
|
||||
|
||||
def _prepare(paths: AppPaths, game_dir: Path | None, as_json: bool) -> int:
|
||||
return _installation_commands.prepare(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
game_dir,
|
||||
as_json,
|
||||
)
|
||||
def _play(
|
||||
paths: AppPaths,
|
||||
game_dir: Path | None,
|
||||
no_box: bool,
|
||||
dry_run: bool,
|
||||
skip_runtime_setup: bool,
|
||||
as_json: bool,
|
||||
*,
|
||||
realm: str | None = None,
|
||||
) -> int:
|
||||
return _play_commands.play(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
game_dir,
|
||||
no_box,
|
||||
dry_run,
|
||||
skip_runtime_setup,
|
||||
as_json,
|
||||
realm=realm,
|
||||
)
|
||||
|
||||
|
||||
def _play_locked(
|
||||
paths: AppPaths,
|
||||
game_dir: Path | None,
|
||||
no_box: bool,
|
||||
dry_run: bool,
|
||||
skip_runtime_setup: bool,
|
||||
as_json: bool,
|
||||
*,
|
||||
realm: str | None = None,
|
||||
reuse_prepared: bool = False,
|
||||
before_game: Callable[[], None] | None = None,
|
||||
game_lease_fds: Sequence[int] = (),
|
||||
) -> int:
|
||||
return _play_commands.play_locked(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
game_dir,
|
||||
no_box,
|
||||
dry_run,
|
||||
skip_runtime_setup,
|
||||
as_json,
|
||||
realm=realm,
|
||||
reuse_prepared=reuse_prepared,
|
||||
before_game=before_game,
|
||||
game_lease_fds=game_lease_fds,
|
||||
)
|
||||
|
||||
|
||||
_session_duration_class = _play_commands.session_duration_class
|
||||
_game_exit_metadata = _play_commands.game_exit_metadata
|
||||
_signal_name = _play_commands.signal_name
|
||||
|
||||
|
||||
def _ensure_portable_umu_for_play(
|
||||
paths: AppPaths,
|
||||
settings: Settings,
|
||||
*,
|
||||
dry_run: bool,
|
||||
) -> UmuBootstrapResult | None:
|
||||
return _play_commands.ensure_portable_umu_for_play(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
settings,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
|
||||
def _current_game_log(
|
||||
directory: Path,
|
||||
baseline: dict[str, LogFileState],
|
||||
started_ns: int,
|
||||
*,
|
||||
managed_asset_paths: Sequence[str] | None = None,
|
||||
source_game: Path | None = None,
|
||||
) -> dict[str, object] | None:
|
||||
return _play_commands.current_game_log(
|
||||
sys.modules[__name__],
|
||||
directory,
|
||||
baseline,
|
||||
started_ns,
|
||||
managed_asset_paths=managed_asset_paths,
|
||||
source_game=source_game,
|
||||
)
|
||||
|
||||
|
||||
def _current_game_crash(
|
||||
directory: Path,
|
||||
baseline: dict[str, LogFileState],
|
||||
started_ns: int,
|
||||
) -> dict[str, object] | None:
|
||||
return _play_commands.current_game_crash(
|
||||
sys.modules[__name__],
|
||||
directory,
|
||||
baseline,
|
||||
started_ns,
|
||||
)
|
||||
|
||||
|
||||
_runtime_issue_summary = _play_commands.runtime_issue_summary
|
||||
_gamescope_presentation_failure = _play_commands.gamescope_presentation_failure
|
||||
_game_window_was_ready = _play_commands.game_window_was_ready
|
||||
_game_readiness_signal = _play_commands.game_readiness_signal
|
||||
_window_diagnostic_severity = _play_commands.window_diagnostic_severity
|
||||
_print_game_outcome = _play_commands.print_game_outcome
|
||||
_print_interrupted_game_log = _play_commands.print_interrupted_game_log
|
||||
_login_trace = _play_commands.login_trace
|
||||
def _settings(
|
||||
paths: AppPaths,
|
||||
initialize: bool,
|
||||
reset: bool,
|
||||
as_json: bool,
|
||||
) -> int:
|
||||
return _settings_commands.settings(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
initialize,
|
||||
reset,
|
||||
as_json,
|
||||
)
|
||||
|
||||
|
||||
def _launcher_update(
|
||||
paths: AppPaths,
|
||||
check_only: bool,
|
||||
verify: bool,
|
||||
rollback: str | bool | None,
|
||||
manifest_url: str | None,
|
||||
public_key: Path | None,
|
||||
as_json: bool,
|
||||
*,
|
||||
history_url: str | None = None,
|
||||
) -> int:
|
||||
return _launcher_update_commands.launcher_update(
|
||||
sys.modules[__name__],
|
||||
paths,
|
||||
check_only,
|
||||
verify,
|
||||
rollback,
|
||||
manifest_url,
|
||||
public_key,
|
||||
as_json,
|
||||
history_url=history_url,
|
||||
)
|
||||
def _source_game(
|
||||
paths: AppPaths,
|
||||
_settings: Settings,
|
||||
argument: Path | None,
|
||||
) -> Path:
|
||||
"""Select the managed game unless an explicit diagnostic path was supplied.
|
||||
|
||||
The persisted ``game_source`` setting is an import seed only. It must not
|
||||
redirect normal Play, Prepare, or Login Trace operations away from the
|
||||
launcher-managed tree.
|
||||
"""
|
||||
|
||||
if argument is not None:
|
||||
return argument.expanduser().resolve()
|
||||
return paths.source_game
|
||||
|
||||
|
||||
def _load_or_default_settings(paths: AppPaths, *, persist: bool) -> Settings:
|
||||
if paths.settings_file.is_file():
|
||||
return load_settings(paths)
|
||||
if persist:
|
||||
return ensure_settings(paths)
|
||||
return Settings()
|
||||
|
||||
|
||||
def _runner(paths: AppPaths, operation: str, dry_run: bool) -> CommandRunner:
|
||||
timestamp = datetime.now().astimezone().strftime("%Y%m%d-%H%M%S-%f")
|
||||
return CommandRunner(
|
||||
paths.logs / f"{timestamp}-{operation}.jsonl",
|
||||
dry_run=dry_run,
|
||||
operation=operation,
|
||||
)
|
||||
|
||||
|
||||
def _play_guard(
|
||||
paths: AppPaths,
|
||||
dry_run: bool,
|
||||
) -> AbstractContextManager[LauncherPlayLease]:
|
||||
if dry_run:
|
||||
return nullcontext(LauncherPlayLease.dry_run())
|
||||
return launcher_play_lock(
|
||||
paths.launcher_operation_lock,
|
||||
active_game_probe=lambda: _managed_game_process_is_active(paths),
|
||||
)
|
||||
|
||||
|
||||
def _operation_guard(
|
||||
paths: AppPaths,
|
||||
operation: str,
|
||||
dry_run: bool,
|
||||
) -> AbstractContextManager[None]:
|
||||
if dry_run:
|
||||
return nullcontext()
|
||||
return launcher_operation_lock(
|
||||
paths.launcher_operation_lock,
|
||||
operation,
|
||||
active_game_probe=lambda: _managed_game_process_is_active(paths),
|
||||
)
|
||||
|
||||
|
||||
def _managed_game_process_is_active(paths: AppPaths) -> bool:
|
||||
return managed_game_process_is_active(
|
||||
paths.runtime_view,
|
||||
(paths.proton_prefix, paths.umu_prefix),
|
||||
)
|
||||
|
||||
|
||||
def _emit(value: object, as_json: bool) -> None:
|
||||
_application_helpers.emit(value, as_json, json_printer=_print_json)
|
||||
|
||||
|
||||
def _print_json(value: object) -> None:
|
||||
_application_helpers.print_json(value)
|
||||
Reference in New Issue
Block a user