More changes and fixes to use the new native world (game.dll) fixes.
This commit is contained in:
@@ -4,4 +4,4 @@ from .paths import AppPaths
|
|||||||
|
|
||||||
__all__ = ["AppPaths", "__version__"]
|
__all__ = ["AppPaths", "__version__"]
|
||||||
|
|
||||||
__version__ = "0.5.8"
|
__version__ = "0.5.9"
|
||||||
|
|||||||
@@ -139,8 +139,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
play.add_argument("--no-box", action="store_true", help="launch without X11 window styling")
|
play.add_argument("--no-box", action="store_true", help="launch without X11 window styling")
|
||||||
play.add_argument(
|
play.add_argument(
|
||||||
"--publisher-dll",
|
"--publisher-dll",
|
||||||
|
"--no-world-fix",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="diagnostic fallback: disable the post-authentication world fix",
|
dest="publisher_dll",
|
||||||
|
help=(
|
||||||
|
"diagnostic fallback: disable the in-memory post-login world fix "
|
||||||
|
"(the publisher DLL is always used)"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
play.add_argument("--dry-run", action="store_true")
|
play.add_argument("--dry-run", action="store_true")
|
||||||
play.add_argument("--skip-runtime-setup", action="store_true")
|
play.add_argument("--skip-runtime-setup", action="store_true")
|
||||||
@@ -348,6 +353,10 @@ def _login_diagnostics(paths: AppPaths, timeout_seconds: float, as_json: bool) -
|
|||||||
print(f"game.dll: {report.game_dll}")
|
print(f"game.dll: {report.game_dll}")
|
||||||
if report.game_dll_sha256:
|
if report.game_dll_sha256:
|
||||||
print(f"DLL SHA-256: {report.game_dll_sha256}")
|
print(f"DLL SHA-256: {report.game_dll_sha256}")
|
||||||
|
world_fix_status = "ready" if report.world_fix_helper_ready else "not ready"
|
||||||
|
print(f"World fix: {world_fix_status} ({report.world_fix_helper})")
|
||||||
|
if report.world_fix_helper_sha256:
|
||||||
|
print(f"Helper SHA: {report.world_fix_helper_sha256}")
|
||||||
if report.runtime_kind:
|
if report.runtime_kind:
|
||||||
print(f"Runtime: {report.runtime_kind} ({report.runtime_path or 'unknown path'})")
|
print(f"Runtime: {report.runtime_kind} ({report.runtime_path or 'unknown path'})")
|
||||||
else:
|
else:
|
||||||
@@ -626,11 +635,7 @@ def _play_locked(
|
|||||||
overlay = None
|
overlay = None
|
||||||
_print_operation_status(as_json, 1, 4, "Preparing the managed game view…")
|
_print_operation_status(as_json, 1, 4, "Preparing the managed game view…")
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
overlay = prepare_runtime_view(
|
overlay = prepare_runtime_view(paths, source)
|
||||||
paths,
|
|
||||||
source,
|
|
||||||
use_world_fix=not publisher_dll,
|
|
||||||
)
|
|
||||||
_print_operation_status(as_json, 2, 4, "Selecting the configured game runtime…")
|
_print_operation_status(as_json, 2, 4, "Selecting the configured game runtime…")
|
||||||
selection = select_runtime(settings)
|
selection = select_runtime(settings)
|
||||||
runner = _runner(paths, "play", dry_run)
|
runner = _runner(paths, "play", dry_run)
|
||||||
@@ -652,6 +657,7 @@ def _play_locked(
|
|||||||
selection,
|
selection,
|
||||||
runner,
|
runner,
|
||||||
use_gamebox=not no_box,
|
use_gamebox=not no_box,
|
||||||
|
use_world_fix=not publisher_dll,
|
||||||
)
|
)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
current_game_log = _current_game_log(
|
current_game_log = _current_game_log(
|
||||||
|
|||||||
@@ -19,11 +19,7 @@ from ptdiag.assbin import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .paths import AppPaths
|
from .paths import AppPaths
|
||||||
from .v21_post_auth_reclaim import (
|
from .world_fix import V21_BINARY_HASHES
|
||||||
PATCHED_SHA256 as POST_AUTH_RECLAIM_GAME_DLL_SHA256,
|
|
||||||
PostAuthReclaimPatchError,
|
|
||||||
build_post_auth_reclaim_patch,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
WHISPERING_VALE_RELATIVE = Path("game/maps/WhisperingVale.rovug")
|
WHISPERING_VALE_RELATIVE = Path("game/maps/WhisperingVale.rovug")
|
||||||
@@ -39,10 +35,7 @@ COMPATIBILITY_BINARY_HASHES = {
|
|||||||
"Game.exe": "3cc0980aac0f65b379ff340ec61750498fb9f620fb6c88aa66281e8ab27023a5",
|
"Game.exe": "3cc0980aac0f65b379ff340ec61750498fb9f620fb6c88aa66281e8ab27023a5",
|
||||||
"game.dll": "7ea85b19f452962a39c91f967cad22495f066b2d52e8ac37f4e234463d5c4f09",
|
"game.dll": "7ea85b19f452962a39c91f967cad22495f066b2d52e8ac37f4e234463d5c4f09",
|
||||||
}
|
}
|
||||||
CURRENT_V21_BINARY_HASHES = {
|
CURRENT_V21_BINARY_HASHES = V21_BINARY_HASHES
|
||||||
"Game.exe": "2bc3edd26c5f6d7ae9bbdc23967c5aefd374bdf934fcdfa2ed42c6442e550837",
|
|
||||||
"game.dll": "4b4e5c28e2bfdaa8d36c8341fad27becfd03faf63aa8ea874f5d43c5b1961868",
|
|
||||||
}
|
|
||||||
PATCHED_V21_GAME_DLL_SHA256 = (
|
PATCHED_V21_GAME_DLL_SHA256 = (
|
||||||
"0be11b06ccf9be747e32b195c8d5980062f446d7d3a0a662ebfe0e602a01c538"
|
"0be11b06ccf9be747e32b195c8d5980062f446d7d3a0a662ebfe0e602a01c538"
|
||||||
)
|
)
|
||||||
@@ -75,6 +68,67 @@ class CompatibilityCacheResult:
|
|||||||
created_at: str
|
created_at: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RuntimeCompatibility:
|
||||||
|
"""Read-only compatibility result for the managed game currently on disk."""
|
||||||
|
|
||||||
|
compatible: bool
|
||||||
|
source_game: str
|
||||||
|
source_scene_sha256: str | None
|
||||||
|
scene_action: str | None
|
||||||
|
binary_action: str | None
|
||||||
|
reason: str | None
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_runtime_compatibility(
|
||||||
|
paths: AppPaths,
|
||||||
|
source_game: Path,
|
||||||
|
*,
|
||||||
|
use_world_fix: bool = True,
|
||||||
|
) -> RuntimeCompatibility:
|
||||||
|
"""Check whether ``prepare_runtime_view`` can recognize the installed release.
|
||||||
|
|
||||||
|
This preflight is intentionally read-only: it never normalizes a scene,
|
||||||
|
builds the verified runtime DLL, prepares save data, or replaces the runtime
|
||||||
|
view. The normal preparation path still performs every authoritative check
|
||||||
|
immediately before launch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
requested_source = source_game.expanduser()
|
||||||
|
source_scene_sha256 = None
|
||||||
|
scene_action = None
|
||||||
|
binary_action = None
|
||||||
|
try:
|
||||||
|
source = requested_source.resolve(strict=True)
|
||||||
|
_validate_game_tree(source)
|
||||||
|
source_scene = source / WHISPERING_VALE_RELATIVE
|
||||||
|
inspection = inspect_wrapped_assbin(source_scene)
|
||||||
|
source_scene_sha256 = inspection.file_sha256
|
||||||
|
scene_action = _scene_action(inspection)
|
||||||
|
# Retain the keyword for callers from the 0.5.8 candidate. The runtime
|
||||||
|
# view is now always publisher-pristine; activation happens in memory.
|
||||||
|
_ = use_world_fix
|
||||||
|
binary_action = _inspect_runtime_binary_action(paths, source)
|
||||||
|
except (OSError, ValueError, OverlayError) as exc:
|
||||||
|
reason = str(exc).strip() or type(exc).__name__
|
||||||
|
return RuntimeCompatibility(
|
||||||
|
compatible=False,
|
||||||
|
source_game=str(requested_source.resolve(strict=False)),
|
||||||
|
source_scene_sha256=source_scene_sha256,
|
||||||
|
scene_action=scene_action,
|
||||||
|
binary_action=binary_action,
|
||||||
|
reason=reason,
|
||||||
|
)
|
||||||
|
return RuntimeCompatibility(
|
||||||
|
compatible=True,
|
||||||
|
source_game=str(source),
|
||||||
|
source_scene_sha256=source_scene_sha256,
|
||||||
|
scene_action=scene_action,
|
||||||
|
binary_action=binary_action,
|
||||||
|
reason=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def prepare_runtime_view(
|
def prepare_runtime_view(
|
||||||
paths: AppPaths,
|
paths: AppPaths,
|
||||||
source_game: Path,
|
source_game: Path,
|
||||||
@@ -90,10 +144,12 @@ def prepare_runtime_view(
|
|||||||
inspection = inspect_wrapped_assbin(source_scene)
|
inspection = inspect_wrapped_assbin(source_scene)
|
||||||
action = _scene_action(inspection)
|
action = _scene_action(inspection)
|
||||||
game_save = _prepare_persistent_save(paths, source)
|
game_save = _prepare_persistent_save(paths, source)
|
||||||
|
# ``use_world_fix`` is retained as a compatibility keyword. No code path may
|
||||||
|
# replace the publisher DLL; the post-login fix is selected by ``run_game``.
|
||||||
|
_ = use_world_fix
|
||||||
binary_action, game_exe_source, game_dll_source = _runtime_game_binary_sources(
|
binary_action, game_exe_source, game_dll_source = _runtime_game_binary_sources(
|
||||||
paths,
|
paths,
|
||||||
source,
|
source,
|
||||||
use_world_fix=use_world_fix,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
build = paths.data / f".{paths.runtime_view.name}.build-{uuid.uuid4().hex}"
|
build = paths.data / f".{paths.runtime_view.name}.build-{uuid.uuid4().hex}"
|
||||||
@@ -207,22 +263,14 @@ def cache_compatibility_binaries(
|
|||||||
def select_binary_action(
|
def select_binary_action(
|
||||||
cache_hashes: dict[str, str] | None,
|
cache_hashes: dict[str, str] | None,
|
||||||
source_hashes: dict[str, str],
|
source_hashes: dict[str, str],
|
||||||
runtime_patch_hash: str | None = None,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
if runtime_patch_hash is not None:
|
|
||||||
if runtime_patch_hash != POST_AUTH_RECLAIM_GAME_DLL_SHA256:
|
|
||||||
raise OverlayError("post-authentication runtime patch has an unknown profile")
|
|
||||||
if cache_hashes is not None:
|
if cache_hashes is not None:
|
||||||
if cache_hashes != COMPATIBILITY_BINARY_HASHES:
|
if cache_hashes != COMPATIBILITY_BINARY_HASHES:
|
||||||
raise OverlayError("compatibility cache has an unknown binary profile")
|
raise OverlayError("compatibility cache has an unknown binary profile")
|
||||||
if source_hashes == COMPATIBILITY_BINARY_HASHES:
|
if source_hashes == COMPATIBILITY_BINARY_HASHES:
|
||||||
return "verified-source-pair"
|
return "verified-source-pair"
|
||||||
if runtime_patch_hash is not None:
|
if source_hashes == CURRENT_V21_BINARY_HASHES:
|
||||||
if source_hashes != CURRENT_V21_BINARY_HASHES:
|
return "verified-v21-publisher-pair"
|
||||||
raise OverlayError(
|
|
||||||
"post-authentication runtime patch does not match the installed client profile"
|
|
||||||
)
|
|
||||||
return "verified-v21-post-auth-world-fix"
|
|
||||||
return "upstream-pair"
|
return "upstream-pair"
|
||||||
|
|
||||||
|
|
||||||
@@ -378,38 +426,30 @@ def _reject_save_symlinks(root: Path) -> None:
|
|||||||
def _runtime_game_binary_sources(
|
def _runtime_game_binary_sources(
|
||||||
paths: AppPaths,
|
paths: AppPaths,
|
||||||
source: Path,
|
source: Path,
|
||||||
*,
|
|
||||||
use_world_fix: bool = True,
|
|
||||||
) -> tuple[str, Path, Path]:
|
) -> tuple[str, Path, Path]:
|
||||||
cache = paths.compatibility_binaries
|
cache = paths.compatibility_binaries
|
||||||
cache_hashes = _binary_hashes(cache) if cache.is_dir() else None
|
cache_hashes = _binary_hashes(cache) if cache.is_dir() else None
|
||||||
source_hashes = _binary_hashes(source)
|
source_hashes = _binary_hashes(source)
|
||||||
patch = paths.runtime_patch_dll
|
action = select_binary_action(cache_hashes, source_hashes)
|
||||||
patch_hash = None
|
|
||||||
if source_hashes == CURRENT_V21_BINARY_HASHES and use_world_fix:
|
|
||||||
if patch.exists() or patch.is_symlink():
|
|
||||||
if not patch.is_file() or patch.is_symlink():
|
|
||||||
raise OverlayError(f"runtime patch cache is not a regular file: {patch}")
|
|
||||||
patch_hash = _sha256(patch)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
result = build_post_auth_reclaim_patch(source / "game.dll", patch)
|
|
||||||
except PostAuthReclaimPatchError as exc:
|
|
||||||
raise OverlayError(
|
|
||||||
f"could not build the verified post-authentication world fix: {exc}"
|
|
||||||
) from exc
|
|
||||||
patch_hash = result.destination_sha256
|
|
||||||
payload = {"ok": True, **asdict(result)}
|
|
||||||
_write_json(patch.parent / "patch.json", payload)
|
|
||||||
_write_json(paths.runtime_patch_manifest, payload)
|
|
||||||
action = select_binary_action(cache_hashes, source_hashes, patch_hash)
|
|
||||||
return (
|
return (
|
||||||
action,
|
action,
|
||||||
source / "Game.exe",
|
source / "Game.exe",
|
||||||
patch if action == "verified-v21-post-auth-world-fix" else source / "game.dll",
|
source / "game.dll",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _inspect_runtime_binary_action(
|
||||||
|
paths: AppPaths,
|
||||||
|
source: Path,
|
||||||
|
) -> str:
|
||||||
|
"""Mirror publisher binary selection without writing any runtime cache."""
|
||||||
|
|
||||||
|
cache = paths.compatibility_binaries
|
||||||
|
cache_hashes = _binary_hashes(cache) if cache.is_dir() else None
|
||||||
|
source_hashes = _binary_hashes(source)
|
||||||
|
return select_binary_action(cache_hashes, source_hashes)
|
||||||
|
|
||||||
|
|
||||||
def _binary_hashes(directory: Path) -> dict[str, str]:
|
def _binary_hashes(directory: Path) -> dict[str, str]:
|
||||||
return {
|
return {
|
||||||
name: _sha256(directory / name)
|
name: _sha256(directory / name)
|
||||||
|
|||||||
@@ -182,10 +182,14 @@ class AppPaths:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def runtime_patch_directory(self) -> Path:
|
def runtime_patch_directory(self) -> Path:
|
||||||
|
"""Retired 0.5.8 disk-patch cache; never selected for play."""
|
||||||
|
|
||||||
return self.data / "runtime-patches" / "v21-post-auth-reclaim"
|
return self.data / "runtime-patches" / "v21-post-auth-reclaim"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def runtime_patch_dll(self) -> Path:
|
def runtime_patch_dll(self) -> Path:
|
||||||
|
"""Retired 0.5.8 DLL retained only so diagnostics can identify it."""
|
||||||
|
|
||||||
return self.runtime_patch_directory / "game.dll"
|
return self.runtime_patch_directory / "game.dll"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from .command import CommandOutcome, CommandRunner, OutputLineFilter
|
|||||||
from .gamebox import build_gamebox_command
|
from .gamebox import build_gamebox_command
|
||||||
from .paths import AppPaths
|
from .paths import AppPaths
|
||||||
from .settings import Settings
|
from .settings import Settings
|
||||||
|
from .world_fix import WorldFixError, resolve_world_fix_helper
|
||||||
|
|
||||||
|
|
||||||
WINDOWS_GAME_EXE = r"C:\Program Files (x86)\Pristontale EU\Game.exe"
|
WINDOWS_GAME_EXE = r"C:\Program Files (x86)\Pristontale EU\Game.exe"
|
||||||
@@ -250,6 +251,7 @@ def run_game(
|
|||||||
runner: CommandRunner,
|
runner: CommandRunner,
|
||||||
*,
|
*,
|
||||||
use_gamebox: bool = True,
|
use_gamebox: bool = True,
|
||||||
|
use_world_fix: bool = False,
|
||||||
timeout_seconds: float = 12 * 60 * 60,
|
timeout_seconds: float = 12 * 60 * 60,
|
||||||
check: bool = True,
|
check: bool = True,
|
||||||
extra_environment: Mapping[str, str] | None = None,
|
extra_environment: Mapping[str, str] | None = None,
|
||||||
@@ -257,21 +259,30 @@ def run_game(
|
|||||||
) -> CommandOutcome:
|
) -> CommandOutcome:
|
||||||
if not paths.runtime_view.is_dir() and not runner.dry_run:
|
if not paths.runtime_view.is_dir() and not runner.dry_run:
|
||||||
raise RuntimeErrorPT("runtime view is missing; prepare the game first")
|
raise RuntimeErrorPT("runtime view is missing; prepare the game first")
|
||||||
|
world_fix_helper = None
|
||||||
|
if use_world_fix and not runner.dry_run:
|
||||||
|
try:
|
||||||
|
world_fix_helper = resolve_world_fix_helper(paths.runtime_view)
|
||||||
|
except WorldFixError as exc:
|
||||||
|
raise RuntimeErrorPT(str(exc)) from exc
|
||||||
|
launch_executable = world_fix_helper or (paths.runtime_view / "Game.exe")
|
||||||
if selection.kind == "umu":
|
if selection.kind == "umu":
|
||||||
game_command = [
|
game_command = [
|
||||||
selection.executable,
|
selection.executable,
|
||||||
str(paths.runtime_view / "Game.exe"),
|
str(launch_executable),
|
||||||
"PTEU_REBORN",
|
|
||||||
]
|
]
|
||||||
|
if world_fix_helper is None:
|
||||||
|
game_command.append("PTEU_REBORN")
|
||||||
environment = _umu_environment(paths)
|
environment = _umu_environment(paths)
|
||||||
elif selection.kind == "proton7-container":
|
elif selection.kind == "proton7-container":
|
||||||
if not selection.proton_path:
|
if not selection.proton_path:
|
||||||
raise RuntimeErrorPT("Proton script path is missing")
|
raise RuntimeErrorPT("Proton script path is missing")
|
||||||
game_command = [
|
game_command = [
|
||||||
selection.executable,
|
selection.executable,
|
||||||
str(paths.runtime_view / "Game.exe"),
|
str(launch_executable),
|
||||||
"PTEU_REBORN",
|
|
||||||
]
|
]
|
||||||
|
if world_fix_helper is None:
|
||||||
|
game_command.append("PTEU_REBORN")
|
||||||
environment = _umu_proton7_environment(
|
environment = _umu_proton7_environment(
|
||||||
paths,
|
paths,
|
||||||
selection,
|
selection,
|
||||||
@@ -286,9 +297,10 @@ def run_game(
|
|||||||
"ptlinux.proton_compat",
|
"ptlinux.proton_compat",
|
||||||
selection.proton_path,
|
selection.proton_path,
|
||||||
"runinprefix",
|
"runinprefix",
|
||||||
WINDOWS_GAME_EXE,
|
str(world_fix_helper) if world_fix_helper is not None else WINDOWS_GAME_EXE,
|
||||||
"PTEU_REBORN",
|
|
||||||
]
|
]
|
||||||
|
if world_fix_helper is None:
|
||||||
|
game_command.append("PTEU_REBORN")
|
||||||
environment = _proton_environment(
|
environment = _proton_environment(
|
||||||
paths,
|
paths,
|
||||||
selection,
|
selection,
|
||||||
|
|||||||
@@ -637,6 +637,7 @@ def _validate_release_directory(release: Path) -> str:
|
|||||||
Path("ptlinux/cli.py"),
|
Path("ptlinux/cli.py"),
|
||||||
Path("ptlinux/self_update.py"),
|
Path("ptlinux/self_update.py"),
|
||||||
Path("ptlinux") / PUBLIC_KEY_FILENAME,
|
Path("ptlinux") / PUBLIC_KEY_FILENAME,
|
||||||
|
Path("ptlinux/native/pt-world-fix.exe"),
|
||||||
Path("ptdiag/__init__.py"),
|
Path("ptdiag/__init__.py"),
|
||||||
Path("ptdiag/assbin.py"),
|
Path("ptdiag/assbin.py"),
|
||||||
Path("ptdiag/logs.py"),
|
Path("ptdiag/logs.py"),
|
||||||
|
|||||||
122
ptlinux/ui.py
122
ptlinux/ui.py
@@ -29,6 +29,7 @@ from .news import (
|
|||||||
fetch_news_image,
|
fetch_news_image,
|
||||||
fetch_official_logo,
|
fetch_official_logo,
|
||||||
)
|
)
|
||||||
|
from .overlay import RuntimeCompatibility, inspect_runtime_compatibility
|
||||||
from .paths import AppPaths
|
from .paths import AppPaths
|
||||||
from .settings import Settings, ensure_settings, save_settings
|
from .settings import Settings, ensure_settings, save_settings
|
||||||
from .self_update import LauncherUpdateCheck, check_for_launcher_update
|
from .self_update import LauncherUpdateCheck, check_for_launcher_update
|
||||||
@@ -568,9 +569,12 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
operation_stage = 0
|
operation_stage = 0
|
||||||
operation_stage_total = 0
|
operation_stage_total = 0
|
||||||
operation_message = ""
|
operation_message = ""
|
||||||
|
operation_refreshes_compatibility = False
|
||||||
last_elapsed_second = -1
|
last_elapsed_second = -1
|
||||||
official_snapshot: DepotSnapshot | None = None
|
official_snapshot: DepotSnapshot | None = None
|
||||||
launcher_update_check: LauncherUpdateCheck | None = None
|
launcher_update_check: LauncherUpdateCheck | None = None
|
||||||
|
runtime_compatibility: RuntimeCompatibility | None = None
|
||||||
|
compatibility_check_generation = 0
|
||||||
buttons = (
|
buttons = (
|
||||||
play_button,
|
play_button,
|
||||||
update_button,
|
update_button,
|
||||||
@@ -753,18 +757,21 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
*,
|
*,
|
||||||
show_preloader: bool = False,
|
show_preloader: bool = False,
|
||||||
restart_on_success: bool = False,
|
restart_on_success: bool = False,
|
||||||
|
game_files_may_change: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
nonlocal operation_active
|
nonlocal operation_active
|
||||||
nonlocal operation_started_at
|
nonlocal operation_started_at
|
||||||
nonlocal operation_stage
|
nonlocal operation_stage
|
||||||
nonlocal operation_stage_total
|
nonlocal operation_stage_total
|
||||||
nonlocal operation_message
|
nonlocal operation_message
|
||||||
|
nonlocal operation_refreshes_compatibility
|
||||||
nonlocal last_elapsed_second
|
nonlocal last_elapsed_second
|
||||||
operation_active = True
|
operation_active = True
|
||||||
operation_started_at = time.monotonic()
|
operation_started_at = time.monotonic()
|
||||||
operation_stage = 0
|
operation_stage = 0
|
||||||
operation_stage_total = 0
|
operation_stage_total = 0
|
||||||
operation_message = ""
|
operation_message = ""
|
||||||
|
operation_refreshes_compatibility = game_files_may_change
|
||||||
last_elapsed_second = -1
|
last_elapsed_second = -1
|
||||||
progress_updates.clear()
|
progress_updates.clear()
|
||||||
transfer_rate.reset()
|
transfer_rate.reset()
|
||||||
@@ -1068,8 +1075,21 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
installation = inspect_game_installation(
|
installation = inspect_game_installation(
|
||||||
paths,
|
paths,
|
||||||
official_snapshot.build if official_snapshot is not None else None,
|
official_snapshot.build if official_snapshot is not None else None,
|
||||||
|
compatibility_ready=(
|
||||||
|
runtime_compatibility.compatible
|
||||||
|
if runtime_compatibility is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
play_button.configure(text=installation.action.value)
|
play_button.configure(text=installation.action.value)
|
||||||
|
if not operation_active:
|
||||||
|
play_button.configure(
|
||||||
|
state=(
|
||||||
|
"disabled"
|
||||||
|
if installation.action is PrimaryAction.CHECKING
|
||||||
|
else "normal"
|
||||||
|
),
|
||||||
|
)
|
||||||
if not update_activity or operation_active:
|
if not update_activity or operation_active:
|
||||||
return installation
|
return installation
|
||||||
if installation.action is PrimaryAction.INSTALL:
|
if installation.action is PrimaryAction.INSTALL:
|
||||||
@@ -1089,11 +1109,42 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
f"Installed depot {installation.local_build} · "
|
f"Installed depot {installation.local_build} · "
|
||||||
f"official depot {official_build}"
|
f"official depot {official_build}"
|
||||||
)
|
)
|
||||||
|
elif installation.action is PrimaryAction.CHECKING:
|
||||||
|
status.set("Checking game compatibility…")
|
||||||
|
progress_text.set("The installed release is being inspected without modifying it.")
|
||||||
|
elif installation.action is PrimaryAction.LAUNCHER_UPDATE:
|
||||||
|
status.set("Launcher compatibility update required.")
|
||||||
|
progress_text.set("This installed game release is not recognized by the launcher.")
|
||||||
else:
|
else:
|
||||||
status.set("Ready")
|
status.set("Ready")
|
||||||
progress_text.set("Click Play to start your journey")
|
progress_text.set("Click Play to start your journey")
|
||||||
return installation
|
return installation
|
||||||
|
|
||||||
|
def refresh_runtime_compatibility(*, update_activity: bool = True) -> None:
|
||||||
|
nonlocal compatibility_check_generation, runtime_compatibility
|
||||||
|
compatibility_check_generation += 1
|
||||||
|
check_generation = compatibility_check_generation
|
||||||
|
runtime_compatibility = None
|
||||||
|
local_status = inspect_game_installation(paths)
|
||||||
|
refresh_primary_action(update_activity=update_activity)
|
||||||
|
if not local_status.installed:
|
||||||
|
return
|
||||||
|
|
||||||
|
def worker() -> None:
|
||||||
|
result = inspect_runtime_compatibility(paths, paths.source_game)
|
||||||
|
events.put(
|
||||||
|
(
|
||||||
|
"compatibility",
|
||||||
|
(check_generation, result, update_activity),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
threading.Thread(
|
||||||
|
target=worker,
|
||||||
|
name="ptlinux-compatibility",
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
def refresh_remote_content() -> None:
|
def refresh_remote_content() -> None:
|
||||||
def broadcast_worker() -> None:
|
def broadcast_worker() -> None:
|
||||||
events.put(("broadcast", fetch_latest_broadcast(paths.broadcast_cache)))
|
events.put(("broadcast", fetch_latest_broadcast(paths.broadcast_cache)))
|
||||||
@@ -1127,8 +1178,10 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
).start()
|
).start()
|
||||||
|
|
||||||
def finish_operation(returncode: int) -> None:
|
def finish_operation(returncode: int) -> None:
|
||||||
nonlocal operation_active, operation_started_at
|
nonlocal operation_active, operation_refreshes_compatibility, operation_started_at
|
||||||
|
refresh_compatibility = operation_refreshes_compatibility
|
||||||
operation_active = False
|
operation_active = False
|
||||||
|
operation_refreshes_compatibility = False
|
||||||
operation_started_at = None
|
operation_started_at = None
|
||||||
progress_updates.clear()
|
progress_updates.clear()
|
||||||
transfer_rate.reset()
|
transfer_rate.reset()
|
||||||
@@ -1138,13 +1191,16 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
root.deiconify()
|
root.deiconify()
|
||||||
root.lift()
|
root.lift()
|
||||||
set_busy(False)
|
set_busy(False)
|
||||||
refresh_primary_action(update_activity=returncode == 0)
|
if refresh_compatibility:
|
||||||
|
refresh_runtime_compatibility(update_activity=returncode == 0)
|
||||||
|
else:
|
||||||
|
refresh_primary_action(update_activity=returncode == 0)
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
status.set(f"Operation failed (exit {returncode})")
|
status.set(f"Operation failed (exit {returncode})")
|
||||||
progress_text.set("See the activity log for details.")
|
progress_text.set("See the activity log for details.")
|
||||||
|
|
||||||
def poll_events() -> None:
|
def poll_events() -> None:
|
||||||
nonlocal launcher_update_check
|
nonlocal launcher_update_check, runtime_compatibility
|
||||||
processed = 0
|
processed = 0
|
||||||
try:
|
try:
|
||||||
while processed < EVENT_BATCH_SIZE:
|
while processed < EVENT_BATCH_SIZE:
|
||||||
@@ -1192,6 +1248,29 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
install_news_image(value)
|
install_news_image(value)
|
||||||
elif kind == "logo_image":
|
elif kind == "logo_image":
|
||||||
install_official_logo(value)
|
install_official_logo(value)
|
||||||
|
elif (
|
||||||
|
kind == "compatibility"
|
||||||
|
and isinstance(value, tuple)
|
||||||
|
and len(value) == 3
|
||||||
|
and isinstance(value[0], int)
|
||||||
|
and isinstance(value[1], RuntimeCompatibility)
|
||||||
|
and isinstance(value[2], bool)
|
||||||
|
):
|
||||||
|
check_generation, checked, update_activity = value
|
||||||
|
if check_generation == compatibility_check_generation:
|
||||||
|
runtime_compatibility = checked
|
||||||
|
if checked.compatible:
|
||||||
|
append_log(
|
||||||
|
"Compatibility check passed: "
|
||||||
|
f"scene={checked.scene_action}, "
|
||||||
|
f"binaries={checked.binary_action}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
append_log(
|
||||||
|
"Launcher compatibility update required: "
|
||||||
|
f"{checked.reason or 'unknown release profile'}"
|
||||||
|
)
|
||||||
|
refresh_primary_action(update_activity=update_activity)
|
||||||
elif kind == "version" and isinstance(value, DepotSnapshot):
|
elif kind == "version" and isinstance(value, DepotSnapshot):
|
||||||
version.set(
|
version.set(
|
||||||
f"Launcher {__version__} · {value.version} · depot {value.build}"
|
f"Launcher {__version__} · {value.version} · depot {value.build}"
|
||||||
@@ -1241,18 +1320,29 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
def command_for(*arguments: str) -> list[str]:
|
def command_for(*arguments: str) -> list[str]:
|
||||||
return [sys.executable, "-m", "ptlinux", *arguments]
|
return [sys.executable, "-m", "ptlinux", *arguments]
|
||||||
|
|
||||||
def run_launcher_update() -> None:
|
def run_launcher_update(required_reason: str | None = None) -> None:
|
||||||
check = launcher_update_check
|
check = launcher_update_check
|
||||||
if check is None:
|
if check is None:
|
||||||
launcher_update_button.configure(text="CHECKING…", state="disabled")
|
launcher_update_button.configure(text="CHECKING…", state="disabled")
|
||||||
|
if required_reason is not None:
|
||||||
|
status.set("Checking for a compatible launcher update…")
|
||||||
|
progress_text.set("Play remains disabled until this release is supported.")
|
||||||
refresh_remote_content()
|
refresh_remote_content()
|
||||||
return
|
return
|
||||||
if not check.available:
|
if not check.available:
|
||||||
messagebox.showinfo(
|
if required_reason is None:
|
||||||
"Launcher update",
|
messagebox.showinfo(
|
||||||
check.reason,
|
"Launcher update",
|
||||||
parent=root,
|
check.reason,
|
||||||
)
|
parent=root,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Compatibility update required",
|
||||||
|
"The installed game needs a launcher compatibility update:\n\n"
|
||||||
|
f"{required_reason}\n\n{check.reason}",
|
||||||
|
parent=root,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if not check.supported:
|
if not check.supported:
|
||||||
messagebox.showwarning(
|
messagebox.showwarning(
|
||||||
@@ -1287,11 +1377,19 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
run_operation(
|
run_operation(
|
||||||
"Installing official game files…",
|
"Installing official game files…",
|
||||||
command_for("update"),
|
command_for("update"),
|
||||||
|
game_files_may_change=True,
|
||||||
)
|
)
|
||||||
else:
|
elif installation.action is PrimaryAction.UPDATE:
|
||||||
run_operation(
|
run_operation(
|
||||||
"Synchronizing official game files…",
|
"Synchronizing official game files…",
|
||||||
command_for("update"),
|
command_for("update"),
|
||||||
|
game_files_may_change=True,
|
||||||
|
)
|
||||||
|
elif installation.action is PrimaryAction.LAUNCHER_UPDATE:
|
||||||
|
run_launcher_update(
|
||||||
|
runtime_compatibility.reason
|
||||||
|
if runtime_compatibility is not None
|
||||||
|
else "The installed release profile is not recognized.",
|
||||||
)
|
)
|
||||||
|
|
||||||
play_button.configure(command=run_primary_action)
|
play_button.configure(command=run_primary_action)
|
||||||
@@ -1299,12 +1397,14 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
command=lambda: run_operation(
|
command=lambda: run_operation(
|
||||||
"Synchronizing official game files…",
|
"Synchronizing official game files…",
|
||||||
command_for("update"),
|
command_for("update"),
|
||||||
|
game_files_may_change=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
repair_button.configure(
|
repair_button.configure(
|
||||||
command=lambda: run_operation(
|
command=lambda: run_operation(
|
||||||
"Checking every game file against the official depot…",
|
"Checking every game file against the official depot…",
|
||||||
command_for("update", "--no-import", "--repair"),
|
command_for("update", "--no-import", "--repair"),
|
||||||
|
game_files_may_change=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
doctor_button.configure(
|
doctor_button.configure(
|
||||||
@@ -1603,6 +1703,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
run_operation(
|
run_operation(
|
||||||
"Importing the existing game into the managed installation…",
|
"Importing the existing game into the managed installation…",
|
||||||
command_for("import-game", source),
|
command_for("import-game", source),
|
||||||
|
game_files_may_change=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
import_button.configure(command=choose_import_source)
|
import_button.configure(command=choose_import_source)
|
||||||
@@ -1619,6 +1720,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
news_content.bind("<Configure>", update_news_viewport)
|
news_content.bind("<Configure>", update_news_viewport)
|
||||||
news_canvas.bind("<Configure>", resize_news_content)
|
news_canvas.bind("<Configure>", resize_news_content)
|
||||||
refresh_primary_action()
|
refresh_primary_action()
|
||||||
|
refresh_runtime_compatibility()
|
||||||
poll_events()
|
poll_events()
|
||||||
load_official_logo()
|
load_official_logo()
|
||||||
refresh_remote_content()
|
refresh_remote_content()
|
||||||
|
|||||||
Reference in New Issue
Block a user