93 lines
2.3 KiB
Python
93 lines
2.3 KiB
Python
"""Read-only installation state used by the native launcher."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from .paths import AppPaths
|
|
|
|
|
|
REQUIRED_GAME_FILES = (
|
|
Path("Game.exe"),
|
|
Path("game.dll"),
|
|
Path("game.ini"),
|
|
Path("game/maps/WhisperingVale.rovug"),
|
|
)
|
|
|
|
|
|
class PrimaryAction(str, Enum):
|
|
"""Action presented by the launcher's large primary button."""
|
|
|
|
INSTALL = "INSTALL"
|
|
UPDATE = "UPDATE"
|
|
PLAY = "PLAY"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GameInstallationStatus:
|
|
"""Local game completeness and its relationship to the official depot."""
|
|
|
|
action: PrimaryAction
|
|
installed: bool
|
|
local_build: int | None
|
|
official_build: int | None
|
|
missing_files: tuple[Path, ...]
|
|
|
|
|
|
def inspect_game_installation(
|
|
paths: AppPaths,
|
|
official_build: int | None = None,
|
|
) -> GameInstallationStatus:
|
|
"""Select Install, Update, or Play without modifying the installation.
|
|
|
|
A completed native sync is the verified source for the local depot build.
|
|
When the official build cannot be checked, a complete local game remains
|
|
playable so temporary network failures do not lock users out.
|
|
"""
|
|
|
|
if official_build is not None and (
|
|
isinstance(official_build, bool)
|
|
or not isinstance(official_build, int)
|
|
or official_build < 0
|
|
):
|
|
raise ValueError("official build must be a non-negative integer")
|
|
|
|
missing_files = tuple(
|
|
relative
|
|
for relative in REQUIRED_GAME_FILES
|
|
if not (paths.source_game / relative).is_file()
|
|
)
|
|
installed = not missing_files
|
|
local_build = _read_verified_local_build(paths.native_depot_state)
|
|
if not installed:
|
|
action = PrimaryAction.INSTALL
|
|
elif official_build is not None and (
|
|
local_build is None or local_build < official_build
|
|
):
|
|
action = PrimaryAction.UPDATE
|
|
else:
|
|
action = PrimaryAction.PLAY
|
|
return GameInstallationStatus(
|
|
action=action,
|
|
installed=installed,
|
|
local_build=local_build,
|
|
official_build=official_build,
|
|
missing_files=missing_files,
|
|
)
|
|
|
|
|
|
def _read_verified_local_build(path: Path) -> int | 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("dry_run") is not False:
|
|
return None
|
|
build = value.get("build")
|
|
if isinstance(build, bool) or not isinstance(build, int) or build < 0:
|
|
return None
|
|
return build
|