Added support for multiple clients to be ran at the same time.

This commit is contained in:
2026-08-30 12:32:39 +02:00
parent 994fda5cc7
commit 94f2260f43
4 changed files with 453 additions and 64 deletions

View File

@@ -10,7 +10,7 @@ import json
from pathlib import Path
import sys
import time
from typing import Sequence
from typing import Callable, Sequence
from ptdiag.logs import (
LogFileState,
@@ -33,12 +33,13 @@ from .launcher import (
launch_official_updater,
prepare_launcher_binary,
)
from .locking import launcher_operation_lock
from .locking import LauncherPlayLease, launcher_operation_lock, launcher_play_lock
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 (
game_runtime_ready,
prepare_game_runtime,
run_game,
select_runtime,
@@ -687,14 +688,15 @@ def _maybe_import_game(
def _prepare(paths: AppPaths, game_dir: Path | None, as_json: bool) -> int:
if game_dir is None and game_import_requires_verification(paths):
raise LauncherError(
"the imported game has not completed its full publisher verification; "
"run pristontale-eu update"
)
settings = ensure_settings(paths)
source = _source_game(paths, settings, game_dir)
result = prepare_runtime_view(paths, source)
with _operation_guard(paths, "prepare", False):
if game_dir is None and game_import_requires_verification(paths):
raise LauncherError(
"the imported game has not completed its full publisher verification; "
"run pristontale-eu update"
)
settings = ensure_settings(paths)
source = _source_game(paths, settings, game_dir)
result = prepare_runtime_view(paths, source)
_emit({"ok": True, "runtime_view": asdict(result)}, as_json)
return 0
@@ -707,7 +709,7 @@ def _play(
skip_runtime_setup: bool,
as_json: bool,
) -> int:
with _operation_guard(paths, "play", dry_run):
with _play_guard(paths, dry_run) as play_lease:
return _play_locked(
paths,
game_dir,
@@ -715,6 +717,8 @@ def _play(
dry_run,
skip_runtime_setup,
as_json,
reuse_prepared=not play_lease.preparation_allowed,
before_game=play_lease.allow_shared_clients,
)
@@ -725,6 +729,9 @@ def _play_locked(
dry_run: bool,
skip_runtime_setup: bool,
as_json: bool,
*,
reuse_prepared: bool = False,
before_game: Callable[[], None] | None = None,
) -> int:
if game_dir is None and game_import_requires_verification(paths):
raise LauncherError(
@@ -734,16 +741,49 @@ def _play_locked(
settings = _load_or_default_settings(paths, persist=not dry_run)
source = _source_game(paths, settings, game_dir)
overlay = None
_print_operation_status(as_json, 1, 4, "Preparing the managed game view…")
if not dry_run:
_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:
overlay = prepare_runtime_view(paths, source)
_print_operation_status(as_json, 2, 4, "Selecting the configured game runtime…")
selection = select_runtime(settings)
runner = _runner(paths, "play", dry_run)
setup_results = []
if not skip_runtime_setup:
if reuse_prepared:
_print_operation_status(
as_json,
3,
4,
"Reusing the active Proton and DirectX runtime…",
)
if not 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:
_print_operation_status(as_json, 3, 4, "Preparing Proton and DirectX components…")
setup_results = prepare_game_runtime(paths, selection, runner)
else:
_print_operation_status(as_json, 3, 4, "Using the existing Proton runtime…")
_print_operation_status(as_json, 4, 4, "Starting the PristonTale game client…")
native_log_directory = paths.game_save / "logs"
native_log_baseline = snapshot_native_logs(
@@ -751,6 +791,8 @@ def _play_locked(
patterns=("log_*.txt",),
)
game_started_ns = time.time_ns()
if before_game is not None:
before_game()
try:
outcome = run_game(
paths,
@@ -1000,10 +1042,19 @@ def _load_or_default_settings(paths: AppPaths, *, persist: bool) -> Settings:
def _runner(paths: AppPaths, operation: str, dry_run: bool) -> CommandRunner:
timestamp = datetime.now().astimezone().strftime("%Y%m%d-%H%M%S")
timestamp = datetime.now().astimezone().strftime("%Y%m%d-%H%M%S-%f")
return CommandRunner(paths.logs / f"{timestamp}-{operation}.jsonl", dry_run=dry_run)
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)
def _operation_guard(
paths: AppPaths,
operation: str,