"""Command construction for the managed game window.""" from __future__ import annotations import os from pathlib import Path import shutil import sys from typing import Mapping, Sequence from .settings import Settings MIN_GAME_CANVAS_DIMENSION = 320 MAX_GAME_CANVAS_DIMENSION = 16384 MAX_GAME_CONFIG_BYTES = 1024 * 1024 class GameboxConfigurationError(ValueError): """Raised when stable outer scaling cannot use the game configuration.""" def find_gamescope( environment: Mapping[str, str] | None = None, ) -> str | None: """Find Gamescope using the environment inherited by the game launch.""" env = os.environ if environment is None else environment return shutil.which("gamescope", path=env.get("PATH")) def read_game_canvas_size(game_config: Path) -> tuple[int, int]: """Read the exact fixed render size advertised in ``game.ini``.""" try: content = game_config.read_bytes() except OSError as exc: raise GameboxConfigurationError( f"could not read game window configuration {game_config}: {exc}" ) from exc if len(content) > MAX_GAME_CONFIG_BYTES: raise GameboxConfigurationError( f"game window configuration is unexpectedly large: {game_config}" ) if b"\x00" in content: raise GameboxConfigurationError( f"game window configuration uses an unsupported encoding: {game_config}" ) try: text = content.decode("utf-8-sig") except UnicodeDecodeError as utf8_error: try: text = content.decode("cp1252") except UnicodeDecodeError: raise GameboxConfigurationError( f"game window configuration uses an unsupported encoding: {game_config}" ) from utf8_error values: dict[str, int] = {} section: str | None = None for line_number, line in enumerate(text.splitlines(), start=1): stripped = line.strip() if stripped.startswith("[") and stripped.endswith("]"): section = stripped[1:-1].strip().casefold() continue if section != "screen" or not stripped or stripped.startswith((";", "#")): continue left, separator, right = line.partition("=") if not separator: continue key = left.strip().casefold() if key not in {"width", "height"}: continue if key in values: raise GameboxConfigurationError( f"duplicate [Screen] {key.title()} in {game_config}" ) try: value = int(right.strip(), 10) except ValueError as exc: raise GameboxConfigurationError( f"invalid [Screen] {key.title()} on line {line_number} of {game_config}" ) from exc if value < MIN_GAME_CANVAS_DIMENSION or value > MAX_GAME_CANVAS_DIMENSION: raise GameboxConfigurationError( f"[Screen] {key.title()} must be between " f"{MIN_GAME_CANVAS_DIMENSION} and {MAX_GAME_CANVAS_DIMENSION}" ) values[key] = value if set(values) != {"width", "height"}: raise GameboxConfigurationError( f"game window configuration has no complete [Screen] Width/Height pair: " f"{game_config}" ) return values["width"], values["height"] def build_gamebox_command( game_command: Sequence[str], settings: Settings, *, game_size: tuple[int, int] | None = None, gamescope_path: str | None = None, ) -> list[str]: if not game_command: raise ValueError("game command cannot be empty") use_gamescope = ( settings.allow_window_resizing and gamescope_path is not None and game_size is not None ) command = [ sys.executable, "-m", "ptlinux.gamebox_runtime", "--window-title", "PristonTale EU", ] if settings.borderless: command.append("--borderless") if use_gamescope: command.append("--gamescope-scaling") if settings.suppress_unresponsive_warnings: command.append("--suppress-wm-ping") command.extend(("--", *[str(value) for value in game_command])) if not use_gamescope: return command assert gamescope_path is not None assert game_size is not None width, height = game_size if not ( MIN_GAME_CANVAS_DIMENSION <= width <= MAX_GAME_CANVAS_DIMENSION and MIN_GAME_CANVAS_DIMENSION <= height <= MAX_GAME_CANVAS_DIMENSION ): raise ValueError("game size is outside the supported Gamescope range") gamescope_command = [ sys.executable, "-m", "ptlinux.gamescope_runtime", "--width", str(width), "--height", str(height), "--", gamescope_path, "-w", str(width), "-h", str(height), "-W", str(width), "-H", str(height), # Gamescope recalculates ``fit`` from each committed game buffer, so an # in-game resolution change keeps its own aspect ratio without cropping. "-S", "fit", "-F", "linear", ] if settings.borderless: gamescope_command.append("--borderless") gamescope_command.extend(("--", *command)) return gamescope_command