150 lines
4.2 KiB
Python
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)
|