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

150 lines
4.2 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_executable(self) -> 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_path = str(self.game_executable).replace("/", "\\")
wine_exe_path = f"Z:{windows_path}"
print(wine_exe_path)
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 (
cmdline.startswith(wine_exe_path)
or abs_cmdline == str(self.game_executable)
or abs_cmdline.startswith(str(self.game_executable))
):
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)