Added more folders and restructured classes inside them.

This commit is contained in:
2026-09-12 11:10:47 +02:00
parent be7a4782e8
commit 2cbb9315bf
152 changed files with 21734 additions and 914 deletions

View File

@@ -0,0 +1,2 @@
"""Feature-oriented command workflows."""

View File

@@ -0,0 +1,190 @@
"""Host health, runtime bootstrap, and login diagnostic commands."""
from __future__ import annotations
from dataclasses import asdict
import sys
from typing import Any
from ...container_host import ContainerHostReport
from ...launcher import LauncherError
from ...paths import AppPaths
def doctor(api: Any, as_json: bool) -> int:
report = api.inspect_host()
if as_json:
api._print_json(report.to_dict())
else:
print(f"Distribution: {report.distribution} {report.version} ({report.architecture})")
print(f"Display: {report.display_backend}")
for tool in report.tools:
status = tool.path or "MISSING"
required = "required" if tool.required else "optional"
print(f"{tool.name:12} {status} [{required}] — {tool.purpose}")
optional_missing = [
tool.name for tool in report.tools if not tool.required and tool.path is None
]
if optional_missing:
print(
"Optional tools missing (these do not block launch): "
+ ", ".join(optional_missing)
)
print(f"Host readiness: {'READY' if report.ok else 'BLOCKED'}")
if report.blockers:
print("Blocking diagnostics:")
for blocker in report.blockers:
print(f" - {blocker}")
for warning in report.warnings:
print(f"Warning: {warning}")
if report.package_hint and any(
tool.required and tool.path is None for tool in report.tools
):
print(report.package_hint)
return 0 if report.ok else 1
def container_host(api: Any, repair: bool, as_json: bool) -> int:
report = api.repair_container_host() if repair else api.inspect_container_host()
ok = container_host_ok(report)
payload = {"ok": ok, "container_host": report.to_dict()}
if as_json:
api._print_json(payload)
else:
status = "ready" if report.ready else "blocked"
print(f"Bubblewrap: {status} ({report.bwrap_path or 'not found'})")
if report.active_profile_source:
print(f"AppArmor: {report.active_profile_source}")
elif report.apparmor_restrict_unprivileged_userns == "1":
print(
"AppArmor: unprivileged user namespaces are restricted; "
"no bwrap profile found"
)
if report.repair_actions:
for action in report.repair_actions:
print(f"Repair: {action}")
if report.repair_error:
print(f"Repair failed: {report.repair_error}", file=sys.stderr)
elif report.recommendation:
print(f"Action: {report.recommendation}", file=sys.stderr)
return 0 if ok else 1
def container_host_ok(report: ContainerHostReport) -> bool:
return (
report.ready
and not report.broken_umu_profile_link
and report.repair_error is None
)
def container_host_startup_migration(api: Any, paths: AppPaths) -> None:
report = api.repair_container_host_after_update(paths)
if report.repair_attempted and not container_host_ok(report):
print(f"Warning: {api.container_host_error(report)}", file=sys.stderr)
def runtime_bootstrap(api: Any, paths: AppPaths, as_json: bool) -> int:
api._print_operation_status(as_json, 1, 1, "Preparing the Steam Runtime helper…")
result = api.provision_umu(paths)
executable = api.find_umu_launcher(paths)
if executable is None:
raise LauncherError("the Steam Runtime helper is still unavailable")
api._emit(
{
"ok": True,
"runtime_bootstrap": {
"executable": executable,
"managed_download": asdict(result) if result else None,
},
},
as_json,
)
return 0
def login_diagnostics(
api: Any,
paths: AppPaths,
timeout_seconds: float,
runtime_choice: str,
as_json: bool,
) -> int:
settings = api._load_or_default_settings(paths, persist=False)
runner = api._runner(paths, "login-diagnostics", False)
with api._operation_guard(paths, "login-diagnostics", False):
report = api.inspect_login_environment(
paths,
settings,
timeout_seconds=timeout_seconds,
runner=runner,
runtime_override=(
None if runtime_choice == "configured" else runtime_choice
),
)
if as_json:
api._print_json(report.to_dict())
else:
probe = report.login_server
print(f"Launcher: {report.launcher_version} ({report.launcher_module})")
print(f"Python: {report.python_executable}")
print(f"Host: {report.distribution_id} ({report.kernel_release})")
print(
"Runtime test: "
f"{report.runtime_requested} "
f"(configured: {report.runtime_configured}; settings unchanged)"
)
status = "REACHABLE" if probe.reachable else "UNREACHABLE"
latency = f", {probe.latency_ms:.1f} ms" if probe.latency_ms is not None else ""
print(f"Login server: {status} ({probe.host}:{probe.port}{latency})")
if probe.error:
print(f"Network error: {probe.error}")
print("Authentication: not tested; no credentials or game-protocol data were sent")
print(f"Login responses: {report.login_response_ownership}")
identity_status = (
"ready"
if report.client_identity_ready is True
else "NOT READY"
if report.client_identity_ready is False
else "not probed"
)
print(f"Client identity: {identity_status}; raw identity values were not collected")
print(f"Privacy: {report.client_identity_privacy}")
if not report.login_trace_enabled:
print("Login trace: retired (will not launch or instrument the game)")
print(f"Publisher test: {report.publisher_comparison_command}")
print(f"Game profile: {report.game_profile}")
print(f"Game.exe: {report.game_exe}")
if report.game_exe_sha256:
print(f"Game SHA-256: {report.game_exe_sha256}")
print(f"game.dll: {report.game_dll}")
if report.game_dll_sha256:
print(f"DLL SHA-256: {report.game_dll_sha256}")
print(
"Managed source: "
+ ("matches runtime" if report.runtime_matches_managed_source else "DIFFERS")
)
if report.depot_build is not None:
print(
f"Depot: build {report.depot_build} "
f"({report.depot_version or 'unknown version'})"
)
if report.runtime_kind:
print(f"Runtime: {report.runtime_kind} ({report.runtime_path or 'unknown path'})")
else:
print(f"Runtime: unavailable ({report.runtime_error or 'selection failed'})")
if report.proton_sync_effective is not None:
print(
"Proton sync: "
f"{report.proton_sync_configured} -> {report.proton_sync_effective}"
)
prefix_status = "ready" if report.prefix_ready else "not ready"
directx_status = "ready" if report.directx_ready else "not ready"
print(f"Prefix: {prefix_status}; DirectX helpers {directx_status}")
print(f"Prefix path: {report.prefix or 'unavailable'}")
print(f"Latest log: {report.latest_game_log or 'not found'}")
if report.latest_game_log_modified_at:
print(f"Log modified: {report.latest_game_log_modified_at}")
for warning in report.warnings:
print(f"Warning: {warning}")
return 0 if report.preflight_ok else 1

View File

@@ -0,0 +1,479 @@
"""Download, setup, depot synchronization, import, and prepare commands."""
from __future__ import annotations
from dataclasses import asdict
import json
from pathlib import Path
import time
from typing import Any
from ...command import CommandOutcome, CommandRunner
from ...depot import DownloadOptions, SyncProgress
from ...importer import GameImportResult
from ...launcher import LauncherError, WineTools
from ...paths import AppPaths
from ...settings import Settings
def fetch(api: Any, paths: AppPaths, dry_run: bool, as_json: bool) -> int:
if not dry_run:
paths.create_base_directories()
if dry_run:
value = {
"ok": True,
"dry_run": True,
"url": api.OFFICIAL_LAUNCHER_URL,
"download": str(paths.launcher_pristine),
"prepared": str(paths.launcher_patched),
}
else:
telemetry = api.current_telemetry()
download_started_at = time.monotonic()
if telemetry is not None:
telemetry.send_event(
"download_started",
metadata={
"resource_type": "launcher",
"mirror_id": "pristontale.eu",
},
context={"operation": "download", "stage": "launcher_download"},
)
try:
download = api.fetch_official_launcher(
paths.launcher_pristine,
paths.launcher_metadata,
)
except BaseException as exc:
if telemetry is not None:
telemetry.send_event(
"download_failed",
severity="error",
message=str(exc),
metadata={
"resource_type": "launcher",
"duration_ms": round(
(time.monotonic() - download_started_at) * 1000
),
"failure_type": api.classify_network_failure(exc),
"http_status": api.network_http_status(exc),
"mirror_id": "pristontale.eu",
},
context={"operation": "download", "stage": "launcher_download"},
)
raise
download_duration = max(0.0, time.monotonic() - download_started_at)
download_bytes = download.size if download.changed else 0
if telemetry is not None:
telemetry.send_event(
"download_completed",
metadata={
"resource_type": "launcher",
"download_bytes": download_bytes,
"content_bytes": download.size,
"duration_ms": round(download_duration * 1000),
"average_mbps": (
round((download_bytes * 8) / download_duration / 1_000_000, 3)
if download_bytes and download_duration > 0
else None
),
"retry_count": 0,
"resume_used": False,
"http_status": 200 if download.changed else 304,
"changed": download.changed,
"mirror_id": "pristontale.eu",
},
context={"operation": "download", "stage": "launcher_download"},
)
validation_started_at = time.monotonic()
try:
patch = api.prepare_launcher_binary(
paths.launcher_pristine,
paths.launcher_patched,
)
except BaseException as exc:
if telemetry is not None:
telemetry.send_event(
"file_validation_failed",
severity="error",
message=str(exc),
metadata={
"file_category": "launcher",
"duration_ms": round(
(time.monotonic() - validation_started_at) * 1000
),
},
context={"operation": "download", "stage": "validation"},
)
raise
value = {
"ok": True,
"download": asdict(download),
"patch": asdict(patch),
"warning": None if patch.supported_profile else (
"The launcher is newer than the verified font-patch profile; "
"it was retained byte-for-byte."
),
}
api._emit(value, as_json)
return 0
def setup(
api: Any,
paths: AppPaths,
dry_run: bool,
force_runtime: bool,
as_json: bool,
) -> int:
tools = api.find_wine_tools()
if tools is None:
raise LauncherError("Wine, Wineboot, and Winetricks are required; run doctor")
runner = api._runner(paths, "setup", dry_run)
with api._operation_guard(paths, "setup", dry_run):
results = api._configure_launcher_runtime(
paths,
tools,
runner,
force_runtime=force_runtime,
)
api._emit(
{
"ok": True,
"dry_run": dry_run,
"prefix": str(paths.launcher_prefix),
"commands": [asdict(result) for result in results],
},
as_json,
)
return 0
def native_update(
api: Any,
paths: AppPaths,
dry_run: bool,
repair: bool,
seed_from: Path | None,
no_import: bool,
as_json: bool,
) -> int:
"""Use the native verified depot client for normal installs and updates."""
settings = api._load_or_default_settings(paths, persist=not dry_run)
download_options = api._configured_download_options(settings)
runner = api._runner(paths, "native-update", dry_run)
with api._operation_guard(paths, "native-update", dry_run):
api._print_operation_status(as_json, 1, 3, "Checking local game files…")
game_import = None
if not no_import:
game_import = api._maybe_import_game(
paths,
settings,
seed_from,
runner,
announce=not as_json,
)
verify_import = game_import is not None or api.game_import_requires_verification(paths)
api._print_operation_status(as_json, 2, 3, "Synchronizing official game files…")
result = api.sync_managed_game(
paths,
repair=repair or verify_import,
quarantine_unmanaged=verify_import,
dry_run=dry_run,
progress=None if as_json else api._print_sync_progress,
**(
{"download_options": download_options}
if download_options is not None
else {}
),
)
if verify_import and not dry_run:
api.mark_game_import_verified(
paths,
build=result.build,
files_quarantined=result.files_quarantined,
quarantine_directory=result.quarantine_directory,
)
overlay = None
if not dry_run and paths.source_game.is_dir():
api._print_operation_status(as_json, 3, 3, "Preparing the clean game runtime view…")
overlay = asdict(api.prepare_runtime_view(paths, paths.source_game))
api._emit(
{
"ok": True,
"dry_run": dry_run,
"game_import": asdict(game_import) if game_import else None,
"native_update": asdict(result),
"runtime_view": overlay,
},
as_json,
)
return 0
def wine_update(
api: Any,
paths: AppPaths,
dry_run: bool,
force_runtime: bool,
seed_from: Path | None,
no_import: bool,
as_json: bool,
) -> int:
api._fetch(paths, dry_run, True)
tools = api.find_wine_tools()
if tools is None:
raise LauncherError("Wine, Wineboot, and Winetricks are required; run doctor")
runner = api._runner(paths, "update", dry_run)
settings = api._load_or_default_settings(paths, persist=not dry_run)
download_options = api._configured_download_options(settings)
with api._operation_guard(paths, "update", dry_run):
api._configure_launcher_runtime(
paths,
tools,
runner,
force_runtime=force_runtime,
)
game_import = None
if not no_import:
game_import = api._maybe_import_game(
paths,
settings,
seed_from,
runner,
announce=not as_json,
)
updater = api.launch_official_updater(paths, tools, runner)
native_verification = None
verify_import = not dry_run and (
game_import is not None or api.game_import_requires_verification(paths)
)
if verify_import:
native_verification = api.sync_managed_game(
paths,
repair=True,
quarantine_unmanaged=True,
progress=None if as_json else api._print_sync_progress,
**(
{"download_options": download_options}
if download_options is not None
else {}
),
)
api.mark_game_import_verified(
paths,
build=native_verification.build,
files_quarantined=native_verification.files_quarantined,
quarantine_directory=native_verification.quarantine_directory,
)
overlay = None
if not dry_run and paths.source_game.is_dir():
overlay = asdict(api.prepare_runtime_view(paths, paths.source_game))
value = {
"ok": True,
"dry_run": dry_run,
"game_import": asdict(game_import) if game_import else None,
"updater": asdict(updater),
"native_verification": (
asdict(native_verification) if native_verification is not None else None
),
"runtime_view": overlay,
}
api._emit(value, as_json)
return 0
def configure_launcher_runtime(
api: Any,
paths: AppPaths,
tools: WineTools,
runner: CommandRunner,
*,
force_runtime: bool,
) -> list[CommandOutcome]:
if not force_runtime or runner.dry_run:
return api.configure_launcher_prefix(
paths,
tools,
runner,
force_runtime=force_runtime,
)
telemetry = api.current_telemetry()
started_at = time.monotonic()
metadata = {
"runtime_type": "dotnet_desktop",
"runtime_version": "6",
}
if telemetry is not None:
telemetry.send_event(
"runtime_reinstall_requested",
metadata={**metadata, "reason": "force_runtime"},
context={"operation": "runtime_repair", "stage": "requested"},
)
telemetry.send_event(
"runtime_repair_started",
metadata=metadata,
context={"operation": "runtime_repair", "stage": "started"},
)
try:
results = api.configure_launcher_prefix(
paths,
tools,
runner,
force_runtime=True,
)
except BaseException as exc:
if telemetry is not None:
telemetry.send_event(
"runtime_repair_failed",
severity="error",
message=str(exc),
metadata={
**metadata,
"duration_ms": round((time.monotonic() - started_at) * 1000),
},
context={"operation": "runtime_repair", "stage": "runtime_install"},
)
raise
if telemetry is not None:
telemetry.send_event(
"runtime_repair_success",
metadata={
**metadata,
"duration_ms": round((time.monotonic() - started_at) * 1000),
},
context={"operation": "runtime_repair", "stage": "completed"},
)
return results
def print_sync_progress(progress: SyncProgress) -> None:
"""Emit line-delimited progress that the native Tk dashboard can consume."""
print(f"PROGRESS {json.dumps(asdict(progress), sort_keys=True)}", flush=True)
def configured_download_options(settings: Settings) -> DownloadOptions | None:
if (
settings.download_speed_limit_bytes_per_second is None
and not settings.low_bandwidth_mode
):
return None
return DownloadOptions(
bandwidth_limit_bytes_per_second=(
settings.download_speed_limit_bytes_per_second
),
low_bandwidth=settings.low_bandwidth_mode,
)
def print_operation_status(
as_json: bool,
step: int,
total: int,
message: str,
) -> None:
"""Emit a structured operation stage for the native launcher dashboard."""
if not as_json:
print(
f"STATUS {json.dumps({'step': step, 'total': total, 'message': message})}",
flush=True,
)
def import_game(
api: Any,
paths: AppPaths,
source: Path,
dry_run: bool,
as_json: bool,
) -> int:
runner = api._runner(paths, "import-game", dry_run)
if not as_json:
print(f"Importing existing game from {source.expanduser()}…", flush=True)
with api._operation_guard(paths, "import-game", dry_run):
result = api._import_with_telemetry(paths, source, runner)
api._emit({"ok": True, "dry_run": dry_run, "game_import": asdict(result)}, as_json)
return 0
def maybe_import_game(
api: Any,
paths: AppPaths,
settings: Settings,
seed_from: Path | None,
runner: CommandRunner,
*,
announce: bool,
) -> GameImportResult | None:
if paths.source_game.exists() or paths.source_game.is_symlink():
if seed_from is not None:
raise RuntimeError(
f"cannot seed an existing managed game directory: {paths.source_game}"
)
return None
candidate = seed_from
if candidate is None:
candidate = api.discover_existing_game(paths, settings)
if candidate is None:
return None
if announce:
print(f"Bootstrapping from existing game: {candidate}", flush=True)
return api._import_with_telemetry(paths, candidate, runner)
def import_with_telemetry(
api: Any,
paths: AppPaths,
source: Path,
runner: CommandRunner,
) -> GameImportResult:
telemetry = api.current_telemetry()
started_at = time.monotonic()
if telemetry is not None and not runner.dry_run:
telemetry.send_event(
"import_started",
context={"operation": "import", "stage": "copy"},
)
try:
result = api.import_existing_game(paths, source, runner)
except BaseException as exc:
if telemetry is not None and not runner.dry_run:
telemetry.send_event(
"import_failed",
severity="error",
message=str(exc),
metadata={
"duration_ms": round((time.monotonic() - started_at) * 1000),
},
context={"operation": "import", "stage": "copy"},
)
raise
if telemetry is not None and not runner.dry_run:
telemetry.send_event(
"import_success",
metadata={
"files_imported": result.files,
"bytes_imported": result.bytes,
"duration_ms": round((time.monotonic() - started_at) * 1000),
},
context={"operation": "import", "stage": "completed"},
)
return result
def prepare(api: Any, paths: AppPaths, game_dir: Path | None, as_json: bool) -> int:
with api._operation_guard(paths, "prepare", False):
if game_dir is None and api.game_import_requires_verification(paths):
raise LauncherError(
"the imported game has not completed its full publisher verification; "
"run pristontale-eu update"
)
settings = api.ensure_settings(paths)
source = api._source_game(paths, settings, game_dir)
result = api.prepare_runtime_view(paths, source)
api._emit({"ok": True, "runtime_view": asdict(result)}, as_json)
return 0

View File

@@ -0,0 +1,138 @@
"""Launcher self-update command workflow."""
from __future__ import annotations
from dataclasses import asdict
from pathlib import Path
import sys
from typing import Any
from ...paths import AppPaths
from ...umu import UmuBootstrapResult
from ...update_resolver import STABLE_CHANNEL
def launcher_update(
api: Any,
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:
try:
selected_channel = api.load_settings(paths).update_channel
except (OSError, TypeError, ValueError):
# Keep update recovery usable when settings are absent or damaged.
selected_channel = STABLE_CHANNEL
rollback_requested = rollback is not None and rollback is not False
if rollback_requested:
target_version = rollback if isinstance(rollback, str) and rollback else None
result = api.rollback_launcher_update(
paths,
target_version=target_version,
selected_channel=selected_channel,
history_url=history_url,
public_key_path=public_key,
)
api._emit(
{
"ok": True,
"launcher_rollback": asdict(result),
},
as_json,
)
return 0
if check_only:
check = api.check_for_launcher_update(
manifest_url=manifest_url,
public_key_path=public_key,
channel=selected_channel,
)
api._emit(
{
"ok": True,
"launcher_update": api.update_check_to_dict(check),
},
as_json,
)
return 0
if verify:
api._print_operation_status(as_json, 1, 2, "Authenticating the launcher release…")
result = api.verify_launcher_update_channel(
manifest_url=manifest_url,
public_key_path=public_key,
channel=selected_channel,
)
api._print_operation_status(as_json, 2, 2, "Published launcher release verified.")
api._emit(
{
"ok": True,
"launcher_update_verification": asdict(result),
},
as_json,
)
return 0
api._print_operation_status(as_json, 1, 4, "Verifying the signed launcher release…")
result = api.install_launcher_update(
paths,
manifest_url=manifest_url,
public_key_path=public_key,
channel=selected_channel,
)
api._print_operation_status(
as_json,
2,
4,
"Launcher update installed; preparing the Steam Runtime helper…",
)
umu_result: UmuBootstrapResult | None = None
umu_executable: str | None = None
umu_warning: str | None = None
try:
umu_result = api.provision_umu(paths)
umu_executable = api.find_umu_launcher(paths)
except (OSError, RuntimeError) as exc:
# Activation is already durable. Keep the update successful so the GUI can
# restart into the new release, whose startup hook and Play path both retry.
warning_message = (
"the launcher update was installed, but the Steam Runtime helper could "
f"not be prepared yet: {exc}"
)
umu_warning = warning_message
if not as_json:
print(f"Warning: {warning_message}", file=sys.stderr)
api._print_operation_status(
as_json,
3,
4,
"Checking Steam Runtime sandbox permissions (authentication may be requested)…",
)
container_report = api.repair_container_host()
container_warning = container_report.repair_error
if not container_report.ready and container_warning is None:
container_warning = container_report.recommendation
if container_warning and not as_json:
print(f"Warning: {container_warning}", file=sys.stderr)
api._print_operation_status(as_json, 4, 4, "Launcher update and runtime check complete.")
api._emit(
{
"ok": True,
"launcher_update": asdict(result),
"runtime_bootstrap": {
"executable": umu_executable,
"managed_download": asdict(umu_result) if umu_result else None,
"warning": umu_warning,
},
"container_host": {
**container_report.to_dict(),
"warning": container_warning,
},
},
as_json,
)
return 0

View File

@@ -0,0 +1,938 @@
"""Game launch, runtime outcome, log, and crash reporting commands."""
from __future__ import annotations
from collections.abc import Callable, Sequence
from dataclasses import asdict, replace
from datetime import datetime
from pathlib import Path
import re
import signal
import sys
import threading
import time
from typing import Any
from uuid import uuid4
from ...command import CommandOutcome
from ...diagnostics.native_logs import LogFileState
from ...game.runtime import RuntimeErrorPT, runtime_settings_for_realm
from ...launcher import LauncherError
from ...login_trace import LOGIN_TRACE_DISABLED_REASON
from ...paths import AppPaths
from ...settings import Settings
from ...umu import UmuBootstrapResult
from ...window_diagnostics import (
WINDOW_DIAGNOSTICS_ENV,
WINDOW_DIAGNOSTICS_SESSION_ENV,
)
def play(
api: Any,
paths: AppPaths,
game_dir: Path | None,
no_box: bool,
dry_run: bool,
skip_runtime_setup: bool,
as_json: bool,
*,
realm: str | None = None,
) -> int:
with api._play_guard(paths, dry_run) as play_lease:
return api._play_locked(
paths,
game_dir,
no_box,
dry_run,
skip_runtime_setup,
as_json,
realm=realm,
reuse_prepared=not play_lease.preparation_allowed,
before_game=play_lease.allow_shared_clients,
game_lease_fds=play_lease.inherited_fds,
)
def play_locked(
api: Any,
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:
telemetry = api.current_telemetry()
launch_requested_at = time.monotonic()
window_diagnostics_active = (
telemetry is not None
and not dry_run
and bool(getattr(telemetry, "enabled", True))
)
launch_session_id = uuid4().hex if window_diagnostics_active else None
launch_session_metadata = (
{"launch_session_id": launch_session_id}
if launch_session_id is not None
else {}
)
if telemetry is not None and not dry_run:
telemetry.send_event(
"game_launch_requested",
metadata=launch_session_metadata,
context={"operation": "launch_game", "stage": "preflight"},
)
if game_dir is None and api.game_import_requires_verification(paths):
raise LauncherError(
"the imported game has not completed its full publisher verification; "
"run pristontale-eu update before playing"
)
settings = api._load_or_default_settings(paths, persist=not dry_run)
launcher_nested_in_steam_runtime = api.is_running_inside_steam_runtime()
if launcher_nested_in_steam_runtime and not dry_run:
print(
"Warning: the launcher itself is running inside Steam Runtime. Start "
"it from the desktop menu or a host terminal; nesting its UMU runtime "
"can cause black game output.",
file=sys.stderr,
flush=True,
)
window_launch_metadata: dict[str, object] = {
"gamebox_enabled": not no_box,
"resizable_window_requested": settings.allow_window_resizing,
"launcher_inside_steam_runtime": launcher_nested_in_steam_runtime,
}
if (
launcher_nested_in_steam_runtime
and settings.allow_window_resizing
and not no_box
):
window_launch_metadata["gamescope_disabled_reason"] = (
"launcher_inside_steam_runtime"
)
start_option = api.select_depot_start_option(
api.read_cached_depot_launch_catalog(paths.depot_launch_catalog_cache),
realm or settings.realm,
)
settings = runtime_settings_for_realm(settings, start_option.name)
source = api._source_game(paths, settings, game_dir)
overlay = None
depot_file_catalog = None
depot_preflight = None
if game_dir is None and not dry_run:
depot_file_catalog = api.read_current_depot_file_catalog(paths)
api._print_operation_status(
as_json,
1,
4,
(
"Reusing the active managed game view…"
if reuse_prepared
else "Preparing the managed game view…"
),
)
if reuse_prepared:
if game_dir is not None:
raise LauncherError(
"an additional client cannot replace the active managed game view; "
"close every client before using --game-dir"
)
if not paths.runtime_view.is_dir():
raise LauncherError(
"another game client is active, but the prepared runtime view is missing"
)
elif not dry_run:
if game_dir is None:
if depot_file_catalog is not None:
depot_preflight = api.preflight_managed_game(
paths,
catalog=depot_file_catalog,
)
if depot_preflight is not None and not depot_preflight.ready:
details = "; ".join(
f"{issue.path}: {issue.reason}"
for issue in depot_preflight.issues[:3]
)
raise LauncherError(
"the managed game integrity check found "
f"{depot_preflight.issue_count} missing or changed file(s)"
+ (f" ({details})" if details else "")
+ "; run `pristontale-eu update --repair` before playing"
)
overlay = api.prepare_runtime_view(paths, source)
api._print_operation_status(as_json, 2, 4, "Ensuring the compatibility runtime is available…")
umu_bootstrap = (
None
if reuse_prepared
else api._ensure_portable_umu_for_play(paths, settings, dry_run=dry_run)
)
selection = api.select_runtime(settings, paths=paths)
runner = api._runner(paths, "play", dry_run)
setup_results = []
if reuse_prepared:
api._print_operation_status(
as_json,
3,
4,
"Reusing the active Proton and DirectX runtime…",
)
if not api.game_runtime_ready(paths, selection):
raise LauncherError(
"another game client is active, but its prepared runtime no longer "
"matches the current settings; close every client and launch again"
)
elif not skip_runtime_setup:
api._print_operation_status(as_json, 3, 4, "Preparing Proton and DirectX components…")
setup_results = api.prepare_game_runtime(paths, selection, runner)
else:
api._print_operation_status(as_json, 3, 4, "Using the existing Proton runtime…")
api._print_operation_status(as_json, 4, 4, "Starting the PristonTale game client…")
native_log_directory = paths.game_save / "logs"
native_log_baseline = api.snapshot_native_logs(
native_log_directory,
patterns=("log_*.txt",),
)
crash_directory = paths.game_save / "crash"
crash_baseline = api.snapshot_native_logs(
crash_directory,
patterns=("*.dat", "*.dmp"),
)
game_started_ns = time.time_ns()
game_started_at = time.monotonic()
launch_success_lock = threading.Lock()
# CommandRunner keeps a bounded output tail. Preserve readiness here so a
# long, noisy session cannot evict the evidence and become a false failure.
observed_readiness_signal: str | None = None
launch_success_reported = False
reported_runtime_warnings: set[str] = set()
if telemetry is not None and not dry_run:
telemetry.send_event(
"game_launch_started",
metadata={
**launch_session_metadata,
**window_launch_metadata,
"launch_duration_ms": round(
(game_started_at - launch_requested_at) * 1000
),
"launch_stage": "game_start",
"runtime": selection.kind,
"realm": start_option.name,
},
context={"operation": "launch_game", "stage": "game_start"},
)
if before_game is not None:
before_game()
managed_asset_paths = (
[entry.path for entry in depot_file_catalog.files]
if depot_file_catalog is not None
else None
)
def observe_game_output(line: str) -> str | None:
"""Timestamp launch success when the supervisor first confirms readiness."""
nonlocal observed_readiness_signal, launch_success_reported
presentation_failure = api._gamescope_presentation_failure(line)
if telemetry is not None and not dry_run and presentation_failure is not None:
with launch_success_lock:
if presentation_failure not in reported_runtime_warnings:
reported_runtime_warnings.add(presentation_failure)
telemetry.send_event(
"game_window_diagnostic",
severity="warning",
metadata={
**launch_session_metadata,
**window_launch_metadata,
"component": "gamescope_host",
"diagnostic": presentation_failure,
"runtime": selection.kind,
"realm": start_option.name,
},
context={
"operation": "launch_game",
"stage": "presentation",
},
)
window_diagnostic = api.parse_window_diagnostic(line)
if window_diagnostic is not None:
diagnostic_session_id = window_diagnostic.get("session_id")
if launch_session_id is None or diagnostic_session_id != launch_session_id:
return line
if telemetry is not None and window_diagnostics_active:
with launch_success_lock:
telemetry.send_event(
"game_window_diagnostic",
severity=api._window_diagnostic_severity(window_diagnostic),
metadata={
"runtime": selection.kind,
"realm": start_option.name,
**window_diagnostic,
},
context={
"operation": "launch_game",
"stage": "window_diagnostics",
},
)
return None
live_readiness_signal = api._game_readiness_signal(line)
if live_readiness_signal is None:
return line
with launch_success_lock:
if observed_readiness_signal is None:
observed_readiness_signal = live_readiness_signal
if telemetry is None or dry_run or launch_success_reported:
return line
ready_at = time.monotonic()
launch_success_reported = telemetry.send_event(
"game_launch_success",
metadata={
**launch_session_metadata,
**window_launch_metadata,
"launch_duration_ms": round(
(ready_at - launch_requested_at) * 1000
),
"game_start_duration_ms": round(
(ready_at - game_started_at) * 1000
),
"game_window_ready": True,
"readiness_signal": observed_readiness_signal,
"runtime": selection.kind,
"realm": start_option.name,
},
context={"operation": "launch_game", "stage": "ready"},
)
return line
try:
outcome = api.run_game(
paths,
settings,
selection,
runner,
start_option=start_option,
use_gamebox=not no_box,
concurrent_prefix_launch=reuse_prepared,
check=False,
output_line_filter=observe_game_output,
extra_environment=(
{
WINDOW_DIAGNOSTICS_ENV: "1",
WINDOW_DIAGNOSTICS_SESSION_ENV: launch_session_id,
}
if launch_session_id is not None
else None
),
pass_fds=game_lease_fds,
)
except KeyboardInterrupt:
game_runtime_seconds = max(0.0, time.monotonic() - game_started_at)
current_game_log = api._current_game_log(
native_log_directory,
native_log_baseline,
game_started_ns,
managed_asset_paths=managed_asset_paths,
source_game=source if managed_asset_paths is not None else None,
)
launch_evidence = observed_readiness_signal
if launch_evidence is None and current_game_log is not None:
launch_evidence = "game_log"
if telemetry is not None and not dry_run:
api._report_game_launch_cancelled(
telemetry,
launch_requested_at,
stage="running" if launch_evidence is not None else "game_start",
metadata={
**launch_session_metadata,
**window_launch_metadata,
"runtime_seconds": round(game_runtime_seconds),
"clean_exit": False,
"launch_succeeded": launch_evidence is not None,
"game_window_ready": observed_readiness_signal is not None,
"readiness_signal": launch_evidence,
"session_duration_class": api._session_duration_class(
game_runtime_seconds
),
"runtime": selection.kind,
"realm": start_option.name,
},
)
if current_game_log is not None and not as_json:
api._print_interrupted_game_log(current_game_log)
raise api._GameLaunchInterruptionReported from None
current_game_log = api._current_game_log(
native_log_directory,
native_log_baseline,
game_started_ns,
managed_asset_paths=managed_asset_paths,
source_game=source if managed_asset_paths is not None else None,
)
current_game_crash = api._current_game_crash(
crash_directory,
crash_baseline,
game_started_ns,
)
runtime_issues = api._runtime_issue_summary(outcome.stderr)
retained_readiness_signal = api._game_readiness_signal(outcome.stderr)
game_readiness_signal = observed_readiness_signal or retained_readiness_signal
game_window_ready = game_readiness_signal is not None
runtime_issues["game_window_ready"] = game_window_ready
if game_readiness_signal is not None:
runtime_issues["game_readiness_signal"] = game_readiness_signal
if (
selection.kind == "proton7-container"
and runtime_issues["host_futex_fatal_count"]
):
runtime_issues["recommended_one_variable_comparison"] = "proton7"
elif selection.kind == "proton7" and runtime_issues["gstreamer_missing_libraries"]:
runtime_issues["recommended_one_variable_comparison"] = "proton7-container"
elif runtime_issues["dbghelp_symdeferred_assertion_count"]:
runtime_issues["recommended_one_variable_comparison"] = "umu"
clean_exit = outcome.returncode == 0 and current_game_crash is None
launch_evidence = game_readiness_signal
if launch_evidence is None and current_game_log is not None:
launch_evidence = "game_log"
elif launch_evidence is None and clean_exit:
launch_evidence = "clean_process_exit"
launch_succeeded = launch_evidence is not None
report = {
"ok": clean_exit,
"launch_succeeded": launch_succeeded,
"dry_run": dry_run,
"runtime": asdict(selection),
"umu_bootstrap": asdict(umu_bootstrap) if umu_bootstrap else None,
"realm": asdict(start_option),
"runtime_view": asdict(overlay) if overlay else None,
"depot_preflight": (
{
**asdict(depot_preflight),
"ready": depot_preflight.ready,
"fully_verified": depot_preflight.fully_verified,
}
if depot_preflight
else None
),
"setup_commands": [asdict(result) for result in setup_results],
"game": asdict(outcome),
"current_game_log": current_game_log,
"current_game_crash": current_game_crash,
"runtime_issues": runtime_issues,
}
game_runtime_seconds = max(0.0, time.monotonic() - game_started_at)
if telemetry is not None and not dry_run:
common_game_metadata: dict[str, object] = {
**launch_session_metadata,
**window_launch_metadata,
"runtime_seconds": round(game_runtime_seconds),
"clean_exit": clean_exit,
"timed_out": outcome.timed_out,
"launch_succeeded": launch_succeeded,
"game_window_ready": game_window_ready,
"readiness_signal": launch_evidence,
"crash_dump": current_game_crash is not None,
"session_duration_class": api._session_duration_class(game_runtime_seconds),
"runtime": selection.kind,
"realm": start_option.name,
**api._game_exit_metadata(outcome.returncode, selection.kind),
}
if launch_succeeded:
if not launch_success_reported:
assert launch_evidence is not None
telemetry.send_event(
"game_launch_success",
metadata={
**launch_session_metadata,
**window_launch_metadata,
"detection_duration_ms": round(game_runtime_seconds * 1000),
"detected_after_exit": True,
"game_window_ready": game_window_ready,
"readiness_signal": launch_evidence,
"runtime": selection.kind,
"realm": start_option.name,
},
context={"operation": "launch_game", "stage": "post_exit"},
)
else:
telemetry.send_event(
"game_launch_failed",
severity="error",
message=(
"Game client wrote a crash dump"
if current_game_crash is not None
else "Game session timed out"
if outcome.timed_out
else f"Game client exited with status {outcome.returncode}"
),
metadata={
**common_game_metadata,
"runtime_issues": runtime_issues,
"stdout": outcome.stdout,
"stderr": outcome.stderr,
},
context={"operation": "launch_game", "stage": "game_start"},
)
if launch_succeeded:
exit_message = None
if current_game_crash is not None:
exit_message = "Game client wrote a crash dump"
elif outcome.timed_out:
exit_message = "Game session timed out"
elif common_game_metadata.get("signal_inferred") is True:
exit_message = (
"Game session ended with status "
f"{common_game_metadata['raw_exit_code']}, consistent with "
f"{common_game_metadata['signal_name']} wrapped by UMU"
)
elif "signal" in common_game_metadata:
exit_message = (
"Game session was terminated by "
f"{common_game_metadata['signal_name']}"
)
elif outcome.returncode not in {0, None}:
exit_message = f"Game session ended with status {outcome.returncode}"
telemetry.send_event(
"game_exited",
severity=(
"error"
if current_game_crash is not None
else "info" if clean_exit else "warning"
),
message=exit_message,
metadata=(
common_game_metadata
if clean_exit
else {
**common_game_metadata,
"runtime_issues": runtime_issues,
"stdout": outcome.stdout,
"stderr": outcome.stderr,
}
),
context={"operation": "launch_game", "stage": "exited"},
)
if as_json or dry_run:
api._emit(report, as_json)
else:
api._print_game_outcome(
outcome,
current_game_log,
runtime_issues,
current_game_crash,
)
return 0 if outcome.returncode == 0 and current_game_crash is None else 1
def session_duration_class(duration_seconds: float) -> str:
if duration_seconds < 30:
return "under_30_seconds"
if duration_seconds < 5 * 60:
return "under_5_minutes"
return "normal"
def game_exit_metadata(
returncode: int | None,
runtime_kind: str,
) -> dict[str, object]:
metadata: dict[str, object] = {"exit_code": returncode}
if returncode is None:
return metadata
if returncode < 0:
signal_number = -returncode
metadata.update(
{
"exit_code": 128 + min(signal_number, 127),
"raw_exit_code": returncode,
"signal": signal_number,
"signal_name": signal_name(signal_number),
}
)
return metadata
# UMU returns Popen's negative child status from a Python main function.
# SystemExit encodes -SIGKILL as 247 before our Python gamebox sees it.
wrapped_sigkill = 256 - int(signal.SIGKILL)
if runtime_kind in {"umu", "proton7-container"} and returncode == wrapped_sigkill:
metadata.update(
{
"exit_code": 128 + int(signal.SIGKILL),
"raw_exit_code": returncode,
"signal": int(signal.SIGKILL),
"signal_name": signal.SIGKILL.name,
"signal_inferred": True,
"signal_inference": "umu_python_system_exit",
}
)
return metadata
def signal_name(signal_number: int) -> str:
try:
return signal.Signals(signal_number).name
except ValueError:
return f"signal {signal_number}"
def ensure_portable_umu_for_play(
api: Any,
paths: AppPaths,
settings: Settings,
*,
dry_run: bool,
) -> UmuBootstrapResult | None:
"""Provide a pinned UMU when the selected policy may use a container."""
if settings.runtime == "proton7":
return None
if dry_run:
return None
try:
return api.provision_umu(paths)
except (OSError, RuntimeError) as exc:
if settings.runtime != "auto":
raise LauncherError(
f"the verified portable UMU runtime helper could not be installed: {exc}"
) from exc
try:
api.select_runtime(replace(settings, runtime="proton7"), paths=paths)
except RuntimeErrorPT:
raise LauncherError(
"no usable game runtime is available and the verified portable UMU "
f"runtime helper could not be installed: {exc}"
) from exc
print(
"Warning: the Steam Runtime helper could not be installed; falling back "
f"to direct Proton 7 for this launch ({exc}).",
file=sys.stderr,
)
return None
def current_game_log(
api: Any,
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:
state = api.newest_current_native_log(
directory,
baseline,
started_ns,
patterns=("log_*.txt",),
)
if state is None:
return None
analysis = api.parse_current_run_log(state, baseline)
asset_references = api.classify_asset_references(
analysis,
managed_asset_paths,
source_game=source_game,
)
return {
"path": str(state.path),
"modified_at": datetime.fromtimestamp(
state.mtime_ns / 1_000_000_000
).astimezone().isoformat(),
"size": state.size,
"load_texture_to_material_failed": analysis.load_texture_to_material_failed,
"import_scene_threw": analysis.import_scene_threw,
"asset_pack_health_failed": analysis.asset_pack_health_failed,
"managed_asset_integrity_failed": asset_references.managed_integrity_failed,
"asset_references": asset_references.to_dict(),
"runtime_out_of_memory": analysis.runtime_out_of_memory,
"out_of_memory_count": len(analysis.out_of_memory_events),
"out_of_memory_effect_paths": analysis.out_of_memory_effect_paths,
"effect_creation_failures": [
event.to_dict() for event in analysis.effect_creation_failures
],
"failed_import_scenes": analysis.failed_import_scenes,
"stage_without_new_stage": analysis.stage_without_new_stage,
"module_code_sync_error": analysis.module_code_sync_error,
"module_code_sync_left": analysis.module_code_sync_left,
"module_code_sync_right": analysis.module_code_sync_right,
"module_code_sync_delta": analysis.module_code_sync_delta,
"banned_code_1": analysis.banned_code_1,
"banned_code_1_in_native_log": analysis.banned_code_1,
"banned_code_1_observation_scope": (
"native-game-log-only; server-provided login-screen text is not "
"written to this log"
),
"failed_file_find_count": analysis.failed_file_find_count,
"failed_file_load_count": analysis.failed_file_load_count,
"failed_file_read_count": analysis.failed_file_read_count,
"failed_asset_file_count": analysis.failed_asset_file_count,
"failed_texture_file_count": analysis.failed_texture_file_count,
"failed_effect_file_count": analysis.failed_effect_file_count,
"missing_texture_reference_count": analysis.missing_texture_reference_count,
"missing_effect_reference_count": analysis.missing_effect_reference_count,
"missing_image_reference_count": analysis.missing_image_reference_count,
"missing_pack_image_reference_count": analysis.missing_pack_image_reference_count,
"stage_without_new_stage_events": [
event.to_dict()
for event in analysis.events
if event.kind == "stage_without_new_stage"
],
"scene_imports": {
name: [scene.to_dict() for scene in imports]
for name, imports in sorted(analysis.scenes.items())
},
"failed_file_count": len(analysis.failed_files),
"failed_files": [event.to_dict() for event in analysis.failed_files[:64]],
}
def current_game_crash(
api: Any,
directory: Path,
baseline: dict[str, LogFileState],
started_ns: int,
) -> dict[str, object] | None:
state = api.newest_current_native_log(
directory,
baseline,
started_ns,
patterns=("*.dat", "*.dmp"),
)
if state is None:
return None
return {
"path": str(state.path),
"modified_at": datetime.fromtimestamp(
state.mtime_ns / 1_000_000_000
).astimezone().isoformat(),
"size": state.size,
}
def runtime_issue_summary(stderr: str) -> dict[str, object]:
folded = stderr.casefold()
futex_message = "the futex facility returned an unexpected error code"
wrong_elf_message = "wrong elf class: elfclass64"
dbghelp_assertion = "assertion failed: module->module.symtype != symdeferred"
bwrap_uid_map_denied = "bwrap: setting up uid map: permission denied"
missing_libraries = sorted(
{
library
for line in folded.splitlines()
if "gstreamer-warning" in line and "failed to load plugin" in line
for library in re.findall(
r"([a-z0-9_.+-]+\.so(?:\.\d+)*): cannot open shared object file",
line,
)
}
)
return {
"game_window_ready": game_window_was_ready(folded),
"host_futex_fatal_count": folded.count(futex_message),
"gstreamer_wrong_elf_class_count": folded.count(wrong_elf_message),
"gstreamer_missing_library_count": len(missing_libraries),
"gstreamer_missing_libraries": missing_libraries,
"gstreamer_stream_error": "winegstreamer error" in folded,
"mesa_egl_initialization_failed": "egl: failed to create dri2 screen" in folded,
"gamescope_present_queue_unsupported": (
"physical device queue doesn't support presenting on our surface" in folded
),
"gamescope_vulkan_output_failed_count": folded.count(
"vulkan_make_output failed"
),
"steam_api_app_id_missing": "steamapi_init() failed; no appid found" in folded,
"dbghelp_symdeferred_assertion_count": folded.count(dbghelp_assertion),
"bubblewrap_uid_map_permission_denied": bwrap_uid_map_denied in folded,
}
def gamescope_presentation_failure(output: str) -> str | None:
folded = output.casefold()
for diagnostic, marker in (
(
"gamescope_present_queue_unsupported",
"physical device queue doesn't support presenting on our surface",
),
("gamescope_vulkan_output_failed", "vulkan_make_output failed"),
):
if marker in folded:
return diagnostic
return None
def game_window_was_ready(stderr: str) -> bool:
return game_readiness_signal(stderr) is not None
def game_readiness_signal(output: str) -> str | None:
folded = output.casefold()
for signal_name, marker in (
("window_ready", "game window ready:"),
("rendered_frame", "rendered game frame:"),
(
"window_supervision",
"game window resizing is disabled to prevent repeated direct3d device resets",
),
):
if marker in folded:
return signal_name
return None
def window_diagnostic_severity(diagnostic: dict[str, object]) -> str:
name = diagnostic.get("diagnostic")
details = diagnostic.get("details")
if isinstance(name, str) and (
name.endswith(("_failed", "_unavailable"))
or name in {
"host_hidden_state_cleared",
"host_inactive_remap",
"host_minimize_latched",
}
):
return "warning"
if isinstance(details, dict):
if name == "host_scaling_configured" and not all(
details.get(key) is True
for key in (
"resize_increments_applied",
"scaling_enabled",
"compositing_preferred",
)
):
return "warning"
if name == "scaling_configured" and details.get("enabled") is not True:
return "warning"
return "debug"
def print_game_outcome(
outcome: CommandOutcome,
current_game_log: dict[str, object] | None,
runtime_issues: dict[str, object],
current_game_crash: dict[str, object] | None = None,
) -> None:
# Keep the human-facing stream concise. CommandRunner already relayed every
# process line live; replaying its capture here made each Proton failure appear
# twice in the dashboard.
if current_game_crash is not None:
crash_path = current_game_crash.get("path")
crash_path_text = crash_path if isinstance(crash_path, str) else "unknown"
print(
f"Game client crashed and wrote a dump: {crash_path_text}",
file=sys.stderr,
)
elif outcome.timed_out:
print("Game client timed out and was stopped.", file=sys.stderr)
elif outcome.returncode == 0:
print("Game client exited normally.")
elif outcome.returncode is None:
print("Game client exited without a status.", file=sys.stderr)
else:
print(f"Game client exited with status {outcome.returncode}.", file=sys.stderr)
if current_game_log is not None:
print_interrupted_game_log(current_game_log)
wrong_elf_count = runtime_issues["gstreamer_wrong_elf_class_count"]
if isinstance(wrong_elf_count, int) and wrong_elf_count:
print(
"The repeated GStreamer wrong-ELF-class messages are plugin-scan noise; "
"they do not mean that Proton itself failed to start.",
file=sys.stderr,
)
missing_libraries = runtime_issues["gstreamer_missing_libraries"]
if isinstance(missing_libraries, list) and missing_libraries:
print(
"Media plugins could not load these shared libraries: "
+ ", ".join(str(value) for value in missing_libraries)
+ ".",
file=sys.stderr,
)
assertion_count = runtime_issues["dbghelp_symdeferred_assertion_count"]
if isinstance(assertion_count, int) and assertion_count:
print(
"Proton 7's older Wine build hit its dbghelp SymDeferred assertion; this "
"can mask the client failure that occurred immediately before it.",
file=sys.stderr,
)
if runtime_issues["bubblewrap_uid_map_permission_denied"]:
print(
"Ubuntu denied Bubblewrap's UID map before Proton started. Run "
"`pristontale-eu container-host --repair` and retry.",
file=sys.stderr,
)
recommendation = runtime_issues.get("recommended_one_variable_comparison")
if isinstance(recommendation, str):
print(
f"Recommended single-variable runtime comparison: {recommendation}.",
file=sys.stderr,
)
def print_interrupted_game_log(report: dict[str, object]) -> None:
log_path = report.get("path")
log_path_text = log_path if isinstance(log_path, str) else "unknown"
print(f"\nCurrent game log: {log_path_text}", file=sys.stderr)
asset_references = report.get("asset_references")
catalog_available = (
asset_references.get("catalog_available")
if isinstance(asset_references, dict)
else False
)
if report["module_code_sync_error"]:
print(
"Detected a publisher module-integrity mismatch; the game was launched "
"with publisher-pristine code in this launcher version.",
file=sys.stderr,
)
elif report["runtime_out_of_memory"]:
effects = report.get("out_of_memory_effect_paths")
effect_text = ""
if isinstance(effects, list) and effects:
effect_text = " while creating " + ", ".join(str(path) for path in effects[:3])
print(
"Detected Direct3D resource exhaustion in the 32-bit game process"
+ effect_text
+ ". Restarting the client can recover temporarily; depot repair will not "
"fix this failure.",
file=sys.stderr,
)
elif report["managed_asset_integrity_failed"]:
print(
"The game reported an asset that belongs to the publisher manifest but is "
"missing from the managed installation.",
file=sys.stderr,
)
print(
"Run `pristontale-eu update --repair` and restart.",
file=sys.stderr,
)
elif report["asset_pack_health_failed"] and not catalog_available:
print(
"The game reported many unresolved asset paths, but no matching cached "
"publisher manifest was available to classify them. Run "
"`pristontale-eu update` to refresh the manifest cache.",
file=sys.stderr,
)
elif report["load_texture_to_material_failed"]:
print(
"Detected a texture/material failure while loading the game world.",
file=sys.stderr,
)
elif report["import_scene_threw"] or report["failed_import_scenes"]:
print("Detected a game-world scene import failure.", file=sys.stderr)
def login_trace(
paths: AppPaths,
game_dir: Path | None,
no_box: bool,
skip_runtime_setup: bool,
timeout_seconds: float,
as_json: bool,
) -> int:
# Preserve the command name so old instructions fail safely before touching
# the prefix, preparing the runtime view, or starting Game.exe.
_ = paths, game_dir, no_box, skip_runtime_setup, timeout_seconds, as_json
raise RuntimeError(LOGIN_TRACE_DISABLED_REASON)

View File

@@ -0,0 +1,32 @@
"""Launcher settings command workflow."""
from __future__ import annotations
from dataclasses import asdict
from typing import Any
from ...paths import AppPaths
def settings(
api: Any,
paths: AppPaths,
initialize: bool,
reset: bool,
as_json: bool,
) -> int:
if reset:
settings = api.reset_settings(paths)
elif initialize:
settings = api.ensure_settings(paths)
else:
settings = api.load_settings(paths)
api._emit(
{
"ok": True,
"path": str(paths.settings_file),
"settings": asdict(settings),
},
as_json,
)
return 0