diff --git a/umu/umu_run.py b/umu/umu_run.py index 38a3b0c4..4c2a461c 100755 --- a/umu/umu_run.py +++ b/umu/umu_run.py @@ -29,6 +29,7 @@ from umu_proton import get_umu_proton from umu_runtime import setup_umu from umu_util import ( + find_subdir, get_libc, is_installed_verb, is_winetricks_verb, @@ -462,9 +463,17 @@ def run_command(command: list[AnyPath]) -> int: # For winetricks, change directory to $PROTONPATH/protonfixes if os.environ.get("EXE", "").endswith("winetricks"): cwd = f"{os.environ['PROTONPATH']}/protonfixes" + elif os.environ.get("STORE") == "gog" and ( + subdir := find_subdir(os.environ) + ): + cwd = f"{os.environ['STEAM_COMPAT_INSTALL_PATH']}/{subdir}" else: + # TODO: Create an environment variable to allow clients to not allow + # UMU to change directories so that the user's setting is respected. cwd = Path.cwd() + log.debug("CWD: '%s'", cwd) + # Create a subprocess but do not set it as subreaper # Unnecessary in a Flatpak and prctl() will fail if libc could not be found if FLATPAK_PATH or not libc: diff --git a/umu/umu_util.py b/umu/umu_util.py index 6349acca..e86be478 100644 --- a/umu/umu_util.py +++ b/umu/umu_util.py @@ -1,9 +1,12 @@ +import os from ctypes.util import find_library from functools import lru_cache +from json import load from pathlib import Path from re import Pattern, compile from shutil import which from subprocess import PIPE, STDOUT, Popen, TimeoutExpired +from typing import Any from umu_log import log @@ -145,3 +148,75 @@ def is_steamdeck() -> bool: break return is_sd + + +def _parse_gogcfg(env: os._Environ[str]) -> Path | None: + json: dict[str, Any] + gog_info: Path + subdir: Path | None = None + install_dir: Path = Path(env["STEAM_COMPAT_INSTALL_PATH"]) + + if not install_dir.is_dir(): + return None + + try: + # Assume that there's only 1 *.info file in the game dir + gog_info = max( + file + for file in install_dir.glob("*.info") + if file.name.startswith("goggame") + ) + except ValueError: + log.debug("No *.info files were found in '%s'", install_dir) + return None + + with gog_info.open(mode="r", encoding="utf-8") as file: + json = load(file) + + if not json: + log.debug("File '%s' is empty", gog_info) + log.debug("Will not change to a subdirectory") + return None + + if "playTasks" not in json: + log.debug("File '%s' does not have a 'playTasks' property", gog_info) + log.debug("Will not change to a subdirectory") + return None + + # Get the first result and assume it's the correct one + for item in json["playTasks"]: + if (path := item.get("path")) and len( + subpaths := Path(path).parts + ) > 1: + subdir = Path(*subpaths[:-1]) + log.debug( + "Found subdirectory '%s'", + subdir, + ) + break + + return subdir + + +def find_subdir(env: os._Environ[str]) -> Path | None: + """Find the correct directory to run the executable by parsing a file. + + Some games require to be executed in a specific way, by either requiring + the path to be relative or to be in a subdirectory within the game + directory. Otherwise, the game may fail to run. The correct subdirectory + is usually defined within a configuration file by the developer (e.g., + installscript.vdf or *.info files). + """ + subdir: Path | None + store: str = env["STORE"] + + # TODO: Parse Steam's .vdf files. + match store: + # For GOG, metadata is in *.info files + case "gog": + log.debug("GOG store detected") + subdir = _parse_gogcfg(env) + case _: + subdir = None + + return subdir