286 lines
7.2 KiB
Python
286 lines
7.2 KiB
Python
"""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 game_import_quarantine(self) -> Path:
|
|
"""Recoverable files excluded while authenticating an imported game."""
|
|
|
|
return self.data / "import-quarantine"
|
|
|
|
@property
|
|
def native_depot_state(self) -> Path:
|
|
"""Last completed native depot synchronization."""
|
|
|
|
return self.state / "native-depot.json"
|
|
|
|
@property
|
|
def depot_launch_catalog_cache(self) -> Path:
|
|
"""Last official realm choices used for network-free game launches."""
|
|
|
|
return self.cache / "depot-launch-catalog.json"
|
|
|
|
@property
|
|
def news_cache(self) -> Path:
|
|
return self.cache / "news.json"
|
|
|
|
@property
|
|
def patches_cache(self) -> Path:
|
|
"""Cached official patch changelog shown beside the news feed."""
|
|
|
|
return self.cache / "patches.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 game_config(self) -> Path:
|
|
"""Writable game settings kept outside the launcher-managed source tree."""
|
|
|
|
return self.game_user_data / "game.ini"
|
|
|
|
@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)
|