"""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