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

166 lines
4.5 KiB
Python

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