import abc import os from pathlib import Path import time from typing import Optional, Any from dataclasses import dataclass from lmm.overlayfs import OverlayFSMount from lmm.profile import Profile from lmm.steam import find_library_folder_for_game class CannotStartReason: pass @dataclass class PathNotExistingReason(CannotStartReason): # The path that is not existing path: Path def __str__(self) -> str: return f"Path {self.path} does not exist" class Game(abc.ABC): # The name of the game. name: str # List of profiles. profiles: list[Profile] # The default runner to use when launching the game. default_runner: Optional["Runner"] def __init__( self, name: str, profiles: list[Profile], default_runner: Optional["Runner"] ): self.name = name self.profiles = profiles self.default_runner = default_runner def get_profile_by_name(self, name: str) -> Optional[Profile]: for profile in self.profiles: if profile.name == name: return profile return None @classmethod def from_dict(cls, data: dict[str, Any]) -> "Game": raise NotImplementedError() def to_dict(self) -> dict[str, Any]: raise NotImplementedError() def installation_path(self) -> Path: raise NotImplementedError() def prepare_overlays(self, profile: Profile) -> list[OverlayFSMount]: raise NotImplementedError() def can_start(self) -> bool: return True def cannot_start_reasons(self) -> list[CannotStartReason]: return [] def wait(self): return None class ProtonGame(Game, abc.ABC): # The appid of the game in Steam. appid: str # The PID of the game. _pid: Optional[str] def __init__( self, name: str, appid: str, profiles: list[Profile], default_runner: Optional["Runner"], ): super().__init__(name, profiles, default_runner) self._pid = None self.appid = appid def compat_path(self) -> Path: raise NotImplementedError() @property def game_executables(self) -> list[Path]: raise NotImplementedError() @property def steam_library(self) -> Optional[Path]: return find_library_folder_for_game(self.appid) def wait(self): # Wait until we started the game. proc = Path("/proc") windows_paths = [str(executable).replace("/", "\\") for executable in self.game_executables] wine_exe_paths = [f"Z:{windows_path}" for windows_path in windows_paths] executables = [str(exe) for exe in self.game_executables] print(wine_exe_paths) print("Polling for game process...") while True: for dir in os.listdir(proc): if not dir.isnumeric(): continue proc_path = proc / dir / "cmdline" try: with open(proc_path, "r") as f: cmdline = f.read() # Workaround for Baldur's Gate 3, which launches "../bin/bg3_dx11.exe", when launched from # the Larion Launcher. if cmdline.startswith(".."): cmdline = os.path.join( os.readlink(proc / dir / "cwd"), cmdline ).replace("\\", "/") # Workaround for games like Baldur's Gate 3 that have a relative path in the cmdline. # Note that the cmdline contains null bytes that we have to remove. abs_cmdline = os.path.abspath(cmdline).replace("\x00", "") if ( any(cmdline.startswith(wine_exe_path) for wine_exe_path in wine_exe_paths) or abs_cmdline in executables or any(abs_cmdline.startswith(exe) for exe in executables) ): self._pid = dir break except: pass if self._pid is not None: print(f"Found {self.name} at PID {self._pid}") break print(".") time.sleep(2) print() # Poll until the game quits. print(f"Polling {self._pid}") pid_path = proc / self._pid while pid_path.exists(): time.sleep(2)