"""Per-user game data and XDG support paths used by the installer.""" from __future__ import annotations from dataclasses import dataclass import os from pathlib import Path from typing import Mapping APP_DIRECTORY_NAME = "pristontale-eu" DEFAULT_INSTALL_DIRECTORY_NAME = "PristontaleEU" INSTALL_DIRECTORY_VARIABLE = "PTLINUX_INSTALL_DIR" GAME_RELATIVE_PATH = Path("drive_c/Program Files (x86)/Pristontale EU") def _xdg_path( environment: Mapping[str, str], variable: str, fallback: Path, ) -> Path: value = environment.get(variable) if not value: return fallback path = Path(value).expanduser() # The XDG Base Directory specification requires absolute paths. Ignoring an # invalid relative value keeps settings anchored to HOME instead of making # their location depend on the launcher's current working directory. return path if path.is_absolute() else fallback @dataclass(frozen=True) class AppPaths: """All persistent paths for one user's installation.""" data: Path config: Path state: Path cache: Path @classmethod def from_environment( cls, environment: Mapping[str, str] | None = None, ) -> AppPaths: env = os.environ if environment is None else environment home_value = env.get("HOME") if not home_value: raise RuntimeError("HOME is not set") home = Path(home_value).expanduser() config_home = _xdg_path(env, "XDG_CONFIG_HOME", home / ".config") state_home = _xdg_path(env, "XDG_STATE_HOME", home / ".local/state") cache_home = _xdg_path(env, "XDG_CACHE_HOME", home / ".cache") install_value = env.get(INSTALL_DIRECTORY_VARIABLE) if install_value: data = Path(install_value).expanduser() if not data.is_absolute(): raise RuntimeError( f"{INSTALL_DIRECTORY_VARIABLE} must be an absolute path" ) else: data = home / "Games" / DEFAULT_INSTALL_DIRECTORY_NAME return cls( data=data.resolve(), config=(config_home / APP_DIRECTORY_NAME).resolve(), state=(state_home / APP_DIRECTORY_NAME).resolve(), cache=(cache_home / APP_DIRECTORY_NAME).resolve(), ) @property def downloads(self) -> Path: return self.cache / "downloads" @property def app_home(self) -> Path: """Stable home containing versioned native-launcher releases.""" return self.data / "app" @property def launcher_releases(self) -> Path: return self.app_home / "releases" @property def launcher_current(self) -> Path: return self.app_home / "current" @property def launcher_previous(self) -> Path: return self.app_home / "previous" @property def launcher_update_state(self) -> Path: return self.state / "launcher-update.json" @property def launcher_update_lock(self) -> Path: return self.state / "launcher-update.lock" @property def launcher_update_check_cache(self) -> Path: """Display-only cache of the last authenticated update check.""" return self.cache / "launcher-update-check.json" def launcher_update_archive(self, version: str) -> Path: return self.downloads / f"pristontale-eu-linux-update-v{version}.tar.gz" @property def launcher_pristine(self) -> Path: return self.downloads / "Pristontale EU Launcher.exe" @property def launcher_patched(self) -> Path: return self.data / "launcher" / "Pristontale EU Launcher.exe" @property def launcher_metadata(self) -> Path: return self.state / "launcher-download.json" @property def launcher_prefix(self) -> Path: return self.data / "launcher-prefix" @property def launcher_prefix_marker(self) -> Path: return self.state / "launcher-prefix.json" @property def launcher_operation_lock(self) -> Path: return self.state / "launcher-operation.lock" @property def game_runtime_marker(self) -> Path: """Verified Proton/D3D preparation state for the game prefix.""" return self.state / "game-runtime.json" @property def source_game(self) -> Path: return self.launcher_prefix / GAME_RELATIVE_PATH @property def game_import_manifest(self) -> Path: return self.state / "game-import.json" @property def native_depot_state(self) -> Path: """Last completed native depot synchronization.""" return self.state / "native-depot.json" @property def news_cache(self) -> Path: return self.cache / "news.json" @property def broadcast_cache(self) -> Path: """Cached latest announcement from the broadcast service.""" return self.cache / "broadcast.json" @property def news_image_cache(self) -> Path: """Cached official PNG thumbnails used by the native news cards.""" return self.cache / "news-images" @property def logo_cache(self) -> Path: """Cached PNG asset for the official PristonTale EU logo.""" return self.cache / "logo-small.png" @property def runtime_view(self) -> Path: return self.data / "runtime-view" @property def runtime_manifest(self) -> Path: return self.state / "runtime-view.json" @property def game_user_data(self) -> Path: """Writable game-created data kept outside the replaceable runtime view.""" return self.data / "user-data" @property def game_save(self) -> Path: return self.game_user_data / "save" @property def compatibility_binaries(self) -> Path: return self.data / "compatibility-binaries" @property def compatibility_manifest(self) -> Path: return self.state / "compatibility-binaries.json" @property 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" @property 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" @property def runtime_patch_manifest(self) -> Path: return self.state / "runtime-patch.json" @property def retired_runtime_patch_dll(self) -> Path: """The pre-0.5.7 all-session patch, retained only for diagnostics.""" return self.data / "runtime-patches" / "v21-texture-reclaim" / "game.dll" @property def compat_data(self) -> Path: return self.data / "game-compat" @property def proton_prefix(self) -> Path: return self.compat_data / "pfx" @property def umu_prefix(self) -> Path: return self.data / "umu-prefix" @property def dxvk_cache(self) -> Path: return self.cache / "dxvk-state" @property def logs(self) -> Path: return self.state / "logs" @property def settings_file(self) -> Path: return self.config / "settings.json" @property def settings_lock(self) -> Path: return self.config / "settings.lock" def create_base_directories(self) -> None: for path in ( self.data, self.config, self.state, self.cache, self.downloads, self.logs, self.dxvk_cache, ): path.mkdir(parents=True, exist_ok=True)