Optimizations and performance boost to pre-loader for launcher and game client.

This commit is contained in:
2026-09-01 22:21:54 +02:00
parent c2400db19e
commit 15dbd90dbb
3 changed files with 370 additions and 31 deletions

View File

@@ -1,4 +1,28 @@
from .cli import main
from __future__ import annotations
import sys
def main() -> int:
"""Dispatch the common GUI path without importing every CLI operation."""
arguments = sys.argv[1:]
if not arguments or arguments == ["gui"]:
from .paths import AppPaths
from .ui import run_dashboard
try:
run_dashboard(AppPaths.from_environment())
except KeyboardInterrupt:
return 130
except (ValueError, OSError, RuntimeError) as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
return 0
from .cli import main as cli_main
return cli_main(arguments)
raise SystemExit(main())

View File

@@ -44,15 +44,46 @@ PATCHED_V21_GAME_DLL_SHA256 = (
RETIRED_POST_AUTH_GAME_DLL_SHA256 = (
"93cd27284eb294753da1111c99bfc9689f82b6d4a59302aee9fdbd8cb52452dd"
)
RUNTIME_VIEW_CONFIGURATION_VERSION = 1
RUNTIME_VIEW_CONFIGURATION_VERSION = 2
class OverlayError(RuntimeError):
"""Raised when a safe runtime view cannot be built."""
@dataclass(frozen=True)
class RuntimeFileIdentity:
"""Cheap change detector for one previously hash-verified runtime input."""
name: str
device: int
inode: int
mode: int
size: int
mtime_ns: int
ctime_ns: int
@dataclass(frozen=True)
class OverlayResult:
source_game: str
runtime_view: str
source_scene_sha256: str
runtime_scene_sha256: str
scene_action: str
binary_action: str
game_exe_sha256: str
game_dll_sha256: str
input_identities: tuple[RuntimeFileIdentity, ...]
runtime_scene_identity: RuntimeFileIdentity
created_at: str
links_created: int
@dataclass(frozen=True)
class LegacyOverlayResult:
"""Version-one runtime manifest retained only for a one-time fast migration."""
source_game: str
runtime_view: str
source_scene_sha256: str
@@ -94,8 +125,9 @@ def inspect_runtime_compatibility(
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.
view. Preparation reuses a prior authoritative result only while every input
identity and runtime-view link remains unchanged; otherwise it repeats the
full checks before launch.
"""
requested_source = source_game.expanduser()
@@ -105,6 +137,9 @@ def inspect_runtime_compatibility(
try:
source = requested_source.resolve(strict=True)
_validate_game_tree(source)
cached = _cached_runtime_compatibility(paths, source)
if cached is not None:
return cached
source_scene = source / WHISPERING_VALE_RELATIVE
inspection = inspect_wrapped_assbin(source_scene)
source_scene_sha256 = inspection.file_sha256
@@ -139,10 +174,14 @@ def prepare_runtime_view(
source = source_game.expanduser().resolve(strict=True)
_validate_game_tree(source)
_validate_runtime_target(paths)
game_save = _prepare_persistent_save(paths, source)
reused = _reuse_runtime_view(paths, source, game_save)
if reused is not None:
return reused
input_identities = _runtime_input_identities(paths, source)
source_scene = source / WHISPERING_VALE_RELATIVE
inspection = inspect_wrapped_assbin(source_scene)
action = _scene_action(inspection)
game_save = _prepare_persistent_save(paths, source)
(
binary_action,
game_exe_source,
@@ -152,7 +191,7 @@ def prepare_runtime_view(
paths,
source,
)
reused = _reuse_runtime_view(
legacy_reuse = _reuse_legacy_runtime_view(
paths,
source,
inspection,
@@ -162,9 +201,10 @@ def prepare_runtime_view(
game_exe_source,
game_dll_source,
source_binary_hashes,
input_identities,
)
if reused is not None:
return reused
if legacy_reuse is not None:
return legacy_reuse
build = paths.data / f".{paths.runtime_view.name}.build-{uuid.uuid4().hex}"
backup = paths.data / f".{paths.runtime_view.name}.old-{uuid.uuid4().hex}"
@@ -213,6 +253,10 @@ def prepare_runtime_view(
validate_scene_normalization(normalization)
else:
shutil.copy2(source_scene, runtime_scene)
if _runtime_input_identities(paths, source) != input_identities:
raise OverlayError(
"game files changed while the managed runtime view was being prepared"
)
if paths.runtime_view.exists() or paths.runtime_view.is_symlink():
paths.runtime_view.replace(backup)
@@ -234,6 +278,11 @@ def prepare_runtime_view(
binary_action=binary_action,
game_exe_sha256=_sha256(paths.runtime_view / "Game.exe"),
game_dll_sha256=_sha256(paths.runtime_view / "game.dll"),
input_identities=input_identities,
runtime_scene_identity=_runtime_file_identity(
"runtime-scene",
paths.runtime_view / WHISPERING_VALE_RELATIVE,
),
created_at=datetime.now().astimezone().isoformat(),
links_created=links_created,
)
@@ -470,7 +519,99 @@ def _reject_save_symlinks(root: Path) -> None:
raise OverlayError(f"persistent game save contains a symbolic link: {path}")
def _reuse_runtime_view(
def _runtime_file_identity(
name: str,
path: Path,
*,
follow_symlinks: bool = True,
) -> RuntimeFileIdentity:
state = path.stat(follow_symlinks=follow_symlinks)
return RuntimeFileIdentity(
name=name,
device=state.st_dev,
inode=state.st_ino,
mode=state.st_mode,
size=state.st_size,
mtime_ns=state.st_mtime_ns,
ctime_ns=state.st_ctime_ns,
)
def _runtime_input_identities(
paths: AppPaths,
source: Path,
) -> tuple[RuntimeFileIdentity, ...]:
"""Snapshot inputs whose hashes established the cached runtime profile.
Nanosecond mtime plus ctime/inode/device prevents ordinary edits, replacements,
or timestamp restoration from bypassing the next authoritative hash check.
"""
identities = [
_runtime_file_identity("source-game-exe", source / "Game.exe"),
_runtime_file_identity("source-game-dll", source / "game.dll"),
_runtime_file_identity("source-scene", source / WHISPERING_VALE_RELATIVE),
]
cache = paths.compatibility_binaries
if cache.exists() or cache.is_symlink():
identities.append(
_runtime_file_identity(
"compatibility-cache",
cache,
follow_symlinks=False,
)
)
if cache.is_dir():
for name in COMPATIBILITY_BINARY_HASHES:
path = cache / name
if path.exists() or path.is_symlink():
identities.append(
_runtime_file_identity(
f"compatibility-cache-{name.casefold()}",
path,
)
)
return tuple(identities)
def _matching_cached_runtime_result(
paths: AppPaths,
source: Path,
) -> OverlayResult | None:
result = _read_overlay_result(paths.runtime_manifest)
if result is None:
return None
if result.source_game != str(source):
return None
if result.runtime_view != str(paths.runtime_view):
return None
try:
current_identities = _runtime_input_identities(paths, source)
except OSError:
return None
if current_identities != result.input_identities:
return None
return result
def _cached_runtime_compatibility(
paths: AppPaths,
source: Path,
) -> RuntimeCompatibility | None:
result = _matching_cached_runtime_result(paths, source)
if result is None:
return None
return RuntimeCompatibility(
compatible=True,
source_game=str(source),
source_scene_sha256=result.source_scene_sha256,
scene_action=result.scene_action,
binary_action=result.binary_action,
reason=None,
)
def _reuse_legacy_runtime_view(
paths: AppPaths,
source: Path,
inspection: SceneInspection,
@@ -480,22 +621,82 @@ def _reuse_runtime_view(
game_exe_source: Path,
game_dll_source: Path,
source_binary_hashes: Mapping[str, str],
input_identities: tuple[RuntimeFileIdentity, ...],
) -> OverlayResult | None:
result = _read_overlay_result(paths.runtime_manifest)
"""Upgrade an unchanged v1 view after its original hash checks pass once."""
legacy = _read_legacy_overlay_result(paths.runtime_manifest)
if legacy is None:
return None
if legacy.source_game != str(source):
return None
if legacy.runtime_view != str(paths.runtime_view):
return None
if legacy.source_scene_sha256 != inspection.file_sha256:
return None
if legacy.scene_action != scene_action or legacy.binary_action != binary_action:
return None
if legacy.game_exe_sha256 != source_binary_hashes.get("Game.exe"):
return None
if legacy.game_dll_sha256 != source_binary_hashes.get("game.dll"):
return None
if not _runtime_layout_ready(
paths,
source,
game_save,
game_exe_source,
game_dll_source,
legacy.links_created,
):
return None
runtime_scene = paths.runtime_view / WHISPERING_VALE_RELATIVE
try:
if _sha256(runtime_scene) != legacy.runtime_scene_sha256:
return None
if _runtime_input_identities(paths, source) != input_identities:
raise OverlayError(
"game files changed while the legacy runtime view was being verified"
)
runtime_scene_identity = _runtime_file_identity(
"runtime-scene",
runtime_scene,
)
except OSError:
return None
result = OverlayResult(
source_game=legacy.source_game,
runtime_view=legacy.runtime_view,
source_scene_sha256=legacy.source_scene_sha256,
runtime_scene_sha256=legacy.runtime_scene_sha256,
scene_action=legacy.scene_action,
binary_action=legacy.binary_action,
game_exe_sha256=legacy.game_exe_sha256,
game_dll_sha256=legacy.game_dll_sha256,
input_identities=input_identities,
runtime_scene_identity=runtime_scene_identity,
created_at=legacy.created_at,
links_created=legacy.links_created,
)
_write_json(
paths.runtime_manifest,
{
"configuration_version": RUNTIME_VIEW_CONFIGURATION_VERSION,
**asdict(result),
},
)
return result
def _reuse_runtime_view(
paths: AppPaths,
source: Path,
game_save: Path,
) -> OverlayResult | None:
result = _matching_cached_runtime_result(paths, source)
if result is None:
return None
if result.source_game != str(source):
return None
if result.runtime_view != str(paths.runtime_view):
return None
if result.source_scene_sha256 != inspection.file_sha256:
return None
if result.scene_action != scene_action or result.binary_action != binary_action:
return None
if result.game_exe_sha256 != source_binary_hashes.get("Game.exe"):
return None
if result.game_dll_sha256 != source_binary_hashes.get("game.dll"):
return None
game_exe_source = source / "Game.exe"
game_dll_source = source / "game.dll"
if not _runtime_layout_ready(
paths,
source,
@@ -507,7 +708,12 @@ def _reuse_runtime_view(
return None
runtime_scene = paths.runtime_view / WHISPERING_VALE_RELATIVE
try:
if _sha256(runtime_scene) != result.runtime_scene_sha256:
if (
_runtime_file_identity("runtime-scene", runtime_scene)
!= result.runtime_scene_identity
):
return None
if _runtime_input_identities(paths, source) != result.input_identities:
return None
except OSError:
return None
@@ -651,7 +857,101 @@ def _read_overlay_result(path: Path) -> OverlayResult | None:
return None
if links_created < 0:
return None
input_values = value.get("input_identities")
if not isinstance(input_values, list) or not input_values:
return None
input_identities: list[RuntimeFileIdentity] = []
for input_value in input_values:
identity = _parse_runtime_file_identity(input_value)
if identity is None:
return None
input_identities.append(identity)
input_names = {identity.name for identity in input_identities}
if len(input_names) != len(input_identities) or not {
"source-game-exe",
"source-game-dll",
"source-scene",
}.issubset(input_names):
return None
runtime_scene_identity = _parse_runtime_file_identity(
value.get("runtime_scene_identity")
)
if (
runtime_scene_identity is None
or runtime_scene_identity.name != "runtime-scene"
):
return None
return OverlayResult(
source_game=value["source_game"],
runtime_view=value["runtime_view"],
source_scene_sha256=value["source_scene_sha256"],
runtime_scene_sha256=value["runtime_scene_sha256"],
scene_action=value["scene_action"],
binary_action=value["binary_action"],
game_exe_sha256=value["game_exe_sha256"],
game_dll_sha256=value["game_dll_sha256"],
input_identities=tuple(input_identities),
runtime_scene_identity=runtime_scene_identity,
created_at=value["created_at"],
links_created=links_created,
)
def _parse_runtime_file_identity(value: object) -> RuntimeFileIdentity | None:
if not isinstance(value, dict):
return None
name = value.get("name")
if not isinstance(name, str) or not name:
return None
fields: dict[str, int] = {}
for field in ("device", "inode", "mode", "size", "mtime_ns", "ctime_ns"):
field_value = value.get(field)
if (
isinstance(field_value, bool)
or not isinstance(field_value, int)
or field_value < 0
):
return None
fields[field] = field_value
return RuntimeFileIdentity(
name=name,
device=fields["device"],
inode=fields["inode"],
mode=fields["mode"],
size=fields["size"],
mtime_ns=fields["mtime_ns"],
ctime_ns=fields["ctime_ns"],
)
def _read_legacy_overlay_result(path: Path) -> LegacyOverlayResult | None:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
return None
if not isinstance(value, dict) or value.get("configuration_version") != 1:
return None
string_fields = (
"source_game",
"runtime_view",
"source_scene_sha256",
"runtime_scene_sha256",
"scene_action",
"binary_action",
"game_exe_sha256",
"game_dll_sha256",
"created_at",
)
if not all(isinstance(value.get(name), str) for name in string_fields):
return None
links_created = value.get("links_created")
if (
isinstance(links_created, bool)
or not isinstance(links_created, int)
or links_created < 0
):
return None
return LegacyOverlayResult(
source_game=value["source_game"],
runtime_view=value["runtime_view"],
source_scene_sha256=value["source_scene_sha256"],

View File

@@ -105,7 +105,7 @@ ACTIVITY_LOG_TRIM_LINES = 500
TRANSFER_RATE_WINDOW_SECONDS = 3.0
TRANSFER_RATE_MINIMUM_INTERVAL_SECONDS = 0.2
TRANSFER_RATE_IDLE_SECONDS = 2.0
DASHBOARD_PRELOADER_MINIMUM_MILLISECONDS = 650
DASHBOARD_PRELOADER_MINIMUM_MILLISECONDS = 300
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS = 10_000
DASHBOARD_TRANSITION_STEP_MILLISECONDS = 16
DASHBOARD_TRANSITION_STEPS = 9
@@ -120,6 +120,7 @@ SERVER_STATUS_STALE_SECONDS = 5 * 60
DASHBOARD_STARTUP_COMPONENTS = frozenset(
{
"broadcast",
"cached_content",
"compatibility",
"launcher_update",
"logo",
@@ -130,6 +131,7 @@ DASHBOARD_STARTUP_COMPONENTS = frozenset(
"version",
}
)
DASHBOARD_STARTUP_REQUIRED_COMPONENTS = frozenset({"cached_content"})
@dataclass(frozen=True)
@@ -559,9 +561,9 @@ def dashboard_preloader_delay(
def dashboard_content_ready(ready_components: set[str] | frozenset[str]) -> bool:
"""Return whether every user-visible startup component has settled."""
"""Return whether cached content is ready for an interactive dashboard."""
return DASHBOARD_STARTUP_COMPONENTS.issubset(ready_components)
return DASHBOARD_STARTUP_REQUIRED_COMPONENTS.issubset(ready_components)
def dashboard_transition_opacity(step: int, *, fading_in: bool) -> float:
@@ -1284,6 +1286,7 @@ def run_dashboard(paths: AppPaths) -> None:
startup_timeout_after: str | None = None
startup_progress_after: str | None = None
startup_progress_target = 0.0
remote_refresh_started = False
root.deiconify()
root.lift()
startup_overlay.lift()
@@ -1375,6 +1378,7 @@ def run_dashboard(paths: AppPaths) -> None:
set_root_opacity(1.0)
root.lift()
root.focus_set()
root.after_idle(start_remote_content_refresh)
def fade_dashboard_in(step: int = 0) -> None:
if not set_root_opacity(
@@ -3389,6 +3393,13 @@ def run_dashboard(paths: AppPaths) -> None:
threading.Thread(target=version_worker, name="ptlinux-version", daemon=True).start()
refresh_launcher_update_check()
def start_remote_content_refresh() -> None:
nonlocal remote_refresh_started
if remote_refresh_started:
return
remote_refresh_started = True
refresh_remote_content()
def release_game_launch_controls(
client_id: int,
*,
@@ -4282,15 +4293,19 @@ def run_dashboard(paths: AppPaths) -> None:
if cached_launcher_update is not None:
show_launcher_update_check(cached_launcher_update, log_reason=False)
refresh_runtime_compatibility()
mark_startup_component_ready(
"cached_content",
"Cached dashboard content ready",
)
poll_events()
load_official_logo()
update_startup_progress("Loading online dashboard content…")
startup_overlay.update_idletasks()
startup_timeout_after = root.after(
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS,
timeout_startup_preloader,
)
refresh_remote_content()
if not startup_transition_started:
startup_timeout_after = root.after(
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS,
timeout_startup_preloader,
)
maybe_start_dashboard_transition()
try:
root.mainloop()