166 lines
4.5 KiB
Python
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
|