Optimizations and performance boost to pre-loader for launcher and game client.
This commit is contained in:
@@ -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())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -44,15 +44,46 @@ PATCHED_V21_GAME_DLL_SHA256 = (
|
|||||||
RETIRED_POST_AUTH_GAME_DLL_SHA256 = (
|
RETIRED_POST_AUTH_GAME_DLL_SHA256 = (
|
||||||
"93cd27284eb294753da1111c99bfc9689f82b6d4a59302aee9fdbd8cb52452dd"
|
"93cd27284eb294753da1111c99bfc9689f82b6d4a59302aee9fdbd8cb52452dd"
|
||||||
)
|
)
|
||||||
RUNTIME_VIEW_CONFIGURATION_VERSION = 1
|
RUNTIME_VIEW_CONFIGURATION_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
class OverlayError(RuntimeError):
|
class OverlayError(RuntimeError):
|
||||||
"""Raised when a safe runtime view cannot be built."""
|
"""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)
|
@dataclass(frozen=True)
|
||||||
class OverlayResult:
|
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
|
source_game: str
|
||||||
runtime_view: str
|
runtime_view: str
|
||||||
source_scene_sha256: str
|
source_scene_sha256: str
|
||||||
@@ -94,8 +125,9 @@ def inspect_runtime_compatibility(
|
|||||||
|
|
||||||
This preflight is intentionally read-only: it never normalizes a scene,
|
This preflight is intentionally read-only: it never normalizes a scene,
|
||||||
builds the verified runtime DLL, prepares save data, or replaces the runtime
|
builds the verified runtime DLL, prepares save data, or replaces the runtime
|
||||||
view. The normal preparation path still performs every authoritative check
|
view. Preparation reuses a prior authoritative result only while every input
|
||||||
immediately before launch.
|
identity and runtime-view link remains unchanged; otherwise it repeats the
|
||||||
|
full checks before launch.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
requested_source = source_game.expanduser()
|
requested_source = source_game.expanduser()
|
||||||
@@ -105,6 +137,9 @@ def inspect_runtime_compatibility(
|
|||||||
try:
|
try:
|
||||||
source = requested_source.resolve(strict=True)
|
source = requested_source.resolve(strict=True)
|
||||||
_validate_game_tree(source)
|
_validate_game_tree(source)
|
||||||
|
cached = _cached_runtime_compatibility(paths, source)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
source_scene = source / WHISPERING_VALE_RELATIVE
|
source_scene = source / WHISPERING_VALE_RELATIVE
|
||||||
inspection = inspect_wrapped_assbin(source_scene)
|
inspection = inspect_wrapped_assbin(source_scene)
|
||||||
source_scene_sha256 = inspection.file_sha256
|
source_scene_sha256 = inspection.file_sha256
|
||||||
@@ -139,10 +174,14 @@ def prepare_runtime_view(
|
|||||||
source = source_game.expanduser().resolve(strict=True)
|
source = source_game.expanduser().resolve(strict=True)
|
||||||
_validate_game_tree(source)
|
_validate_game_tree(source)
|
||||||
_validate_runtime_target(paths)
|
_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
|
source_scene = source / WHISPERING_VALE_RELATIVE
|
||||||
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)
|
|
||||||
(
|
(
|
||||||
binary_action,
|
binary_action,
|
||||||
game_exe_source,
|
game_exe_source,
|
||||||
@@ -152,7 +191,7 @@ def prepare_runtime_view(
|
|||||||
paths,
|
paths,
|
||||||
source,
|
source,
|
||||||
)
|
)
|
||||||
reused = _reuse_runtime_view(
|
legacy_reuse = _reuse_legacy_runtime_view(
|
||||||
paths,
|
paths,
|
||||||
source,
|
source,
|
||||||
inspection,
|
inspection,
|
||||||
@@ -162,9 +201,10 @@ def prepare_runtime_view(
|
|||||||
game_exe_source,
|
game_exe_source,
|
||||||
game_dll_source,
|
game_dll_source,
|
||||||
source_binary_hashes,
|
source_binary_hashes,
|
||||||
|
input_identities,
|
||||||
)
|
)
|
||||||
if reused is not None:
|
if legacy_reuse is not None:
|
||||||
return reused
|
return legacy_reuse
|
||||||
|
|
||||||
build = paths.data / f".{paths.runtime_view.name}.build-{uuid.uuid4().hex}"
|
build = paths.data / f".{paths.runtime_view.name}.build-{uuid.uuid4().hex}"
|
||||||
backup = paths.data / f".{paths.runtime_view.name}.old-{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)
|
validate_scene_normalization(normalization)
|
||||||
else:
|
else:
|
||||||
shutil.copy2(source_scene, runtime_scene)
|
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():
|
if paths.runtime_view.exists() or paths.runtime_view.is_symlink():
|
||||||
paths.runtime_view.replace(backup)
|
paths.runtime_view.replace(backup)
|
||||||
@@ -234,6 +278,11 @@ def prepare_runtime_view(
|
|||||||
binary_action=binary_action,
|
binary_action=binary_action,
|
||||||
game_exe_sha256=_sha256(paths.runtime_view / "Game.exe"),
|
game_exe_sha256=_sha256(paths.runtime_view / "Game.exe"),
|
||||||
game_dll_sha256=_sha256(paths.runtime_view / "game.dll"),
|
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(),
|
created_at=datetime.now().astimezone().isoformat(),
|
||||||
links_created=links_created,
|
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}")
|
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,
|
paths: AppPaths,
|
||||||
source: Path,
|
source: Path,
|
||||||
inspection: SceneInspection,
|
inspection: SceneInspection,
|
||||||
@@ -480,22 +621,82 @@ def _reuse_runtime_view(
|
|||||||
game_exe_source: Path,
|
game_exe_source: Path,
|
||||||
game_dll_source: Path,
|
game_dll_source: Path,
|
||||||
source_binary_hashes: Mapping[str, str],
|
source_binary_hashes: Mapping[str, str],
|
||||||
|
input_identities: tuple[RuntimeFileIdentity, ...],
|
||||||
) -> OverlayResult | None:
|
) -> 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:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
if result.source_game != str(source):
|
game_exe_source = source / "Game.exe"
|
||||||
return None
|
game_dll_source = source / "game.dll"
|
||||||
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
|
|
||||||
if not _runtime_layout_ready(
|
if not _runtime_layout_ready(
|
||||||
paths,
|
paths,
|
||||||
source,
|
source,
|
||||||
@@ -507,7 +708,12 @@ def _reuse_runtime_view(
|
|||||||
return None
|
return None
|
||||||
runtime_scene = paths.runtime_view / WHISPERING_VALE_RELATIVE
|
runtime_scene = paths.runtime_view / WHISPERING_VALE_RELATIVE
|
||||||
try:
|
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
|
return None
|
||||||
except OSError:
|
except OSError:
|
||||||
return None
|
return None
|
||||||
@@ -651,7 +857,101 @@ def _read_overlay_result(path: Path) -> OverlayResult | None:
|
|||||||
return None
|
return None
|
||||||
if links_created < 0:
|
if links_created < 0:
|
||||||
return None
|
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(
|
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"],
|
source_game=value["source_game"],
|
||||||
runtime_view=value["runtime_view"],
|
runtime_view=value["runtime_view"],
|
||||||
source_scene_sha256=value["source_scene_sha256"],
|
source_scene_sha256=value["source_scene_sha256"],
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ ACTIVITY_LOG_TRIM_LINES = 500
|
|||||||
TRANSFER_RATE_WINDOW_SECONDS = 3.0
|
TRANSFER_RATE_WINDOW_SECONDS = 3.0
|
||||||
TRANSFER_RATE_MINIMUM_INTERVAL_SECONDS = 0.2
|
TRANSFER_RATE_MINIMUM_INTERVAL_SECONDS = 0.2
|
||||||
TRANSFER_RATE_IDLE_SECONDS = 2.0
|
TRANSFER_RATE_IDLE_SECONDS = 2.0
|
||||||
DASHBOARD_PRELOADER_MINIMUM_MILLISECONDS = 650
|
DASHBOARD_PRELOADER_MINIMUM_MILLISECONDS = 300
|
||||||
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS = 10_000
|
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS = 10_000
|
||||||
DASHBOARD_TRANSITION_STEP_MILLISECONDS = 16
|
DASHBOARD_TRANSITION_STEP_MILLISECONDS = 16
|
||||||
DASHBOARD_TRANSITION_STEPS = 9
|
DASHBOARD_TRANSITION_STEPS = 9
|
||||||
@@ -120,6 +120,7 @@ SERVER_STATUS_STALE_SECONDS = 5 * 60
|
|||||||
DASHBOARD_STARTUP_COMPONENTS = frozenset(
|
DASHBOARD_STARTUP_COMPONENTS = frozenset(
|
||||||
{
|
{
|
||||||
"broadcast",
|
"broadcast",
|
||||||
|
"cached_content",
|
||||||
"compatibility",
|
"compatibility",
|
||||||
"launcher_update",
|
"launcher_update",
|
||||||
"logo",
|
"logo",
|
||||||
@@ -130,6 +131,7 @@ DASHBOARD_STARTUP_COMPONENTS = frozenset(
|
|||||||
"version",
|
"version",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
DASHBOARD_STARTUP_REQUIRED_COMPONENTS = frozenset({"cached_content"})
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -559,9 +561,9 @@ def dashboard_preloader_delay(
|
|||||||
|
|
||||||
|
|
||||||
def dashboard_content_ready(ready_components: set[str] | frozenset[str]) -> bool:
|
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:
|
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_timeout_after: str | None = None
|
||||||
startup_progress_after: str | None = None
|
startup_progress_after: str | None = None
|
||||||
startup_progress_target = 0.0
|
startup_progress_target = 0.0
|
||||||
|
remote_refresh_started = False
|
||||||
root.deiconify()
|
root.deiconify()
|
||||||
root.lift()
|
root.lift()
|
||||||
startup_overlay.lift()
|
startup_overlay.lift()
|
||||||
@@ -1375,6 +1378,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
set_root_opacity(1.0)
|
set_root_opacity(1.0)
|
||||||
root.lift()
|
root.lift()
|
||||||
root.focus_set()
|
root.focus_set()
|
||||||
|
root.after_idle(start_remote_content_refresh)
|
||||||
|
|
||||||
def fade_dashboard_in(step: int = 0) -> None:
|
def fade_dashboard_in(step: int = 0) -> None:
|
||||||
if not set_root_opacity(
|
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()
|
threading.Thread(target=version_worker, name="ptlinux-version", daemon=True).start()
|
||||||
refresh_launcher_update_check()
|
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(
|
def release_game_launch_controls(
|
||||||
client_id: int,
|
client_id: int,
|
||||||
*,
|
*,
|
||||||
@@ -4282,15 +4293,19 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
if cached_launcher_update is not None:
|
if cached_launcher_update is not None:
|
||||||
show_launcher_update_check(cached_launcher_update, log_reason=False)
|
show_launcher_update_check(cached_launcher_update, log_reason=False)
|
||||||
refresh_runtime_compatibility()
|
refresh_runtime_compatibility()
|
||||||
|
mark_startup_component_ready(
|
||||||
|
"cached_content",
|
||||||
|
"Cached dashboard content ready",
|
||||||
|
)
|
||||||
poll_events()
|
poll_events()
|
||||||
load_official_logo()
|
load_official_logo()
|
||||||
update_startup_progress("Loading online dashboard content…")
|
update_startup_progress("Loading online dashboard content…")
|
||||||
startup_overlay.update_idletasks()
|
startup_overlay.update_idletasks()
|
||||||
startup_timeout_after = root.after(
|
if not startup_transition_started:
|
||||||
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS,
|
startup_timeout_after = root.after(
|
||||||
timeout_startup_preloader,
|
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS,
|
||||||
)
|
timeout_startup_preloader,
|
||||||
refresh_remote_content()
|
)
|
||||||
maybe_start_dashboard_transition()
|
maybe_start_dashboard_transition()
|
||||||
try:
|
try:
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|||||||
Reference in New Issue
Block a user