linux-mod-manager/lmm/games/game.py

151 lines
4.4 KiB
Python

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)