Files
pristontale-eu-linux/ptlinux/paths.py

303 lines
7.7 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 runtime_tools(self) -> Path:
"""Launcher-managed helpers that are independent of app releases."""
return self.data / "runtime-tools"
@property
def managed_umu_executable(self) -> Path:
return self.runtime_tools / "bin/umu-run"
@property
def managed_umu_metadata(self) -> Path:
return self.state / "umu-launcher.json"
def managed_umu_archive(self, version: str) -> Path:
return self.downloads / f"umu-launcher-{version}-zipapp.tar"
@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)