Compare commits

...

7 Commits

10 changed files with 147 additions and 21 deletions

View File

@@ -10,6 +10,7 @@ virtual file system and provides similar functionality using OverlayFS.
- Implemented - Implemented
- The mod structure has to be manually managed, i.e. PAKs must reside in the correct directory relative to their installation path - The mod structure has to be manually managed, i.e. PAKs must reside in the correct directory relative to their installation path
- [x] Project Diva Mega Mix+ - [x] Project Diva Mega Mix+
- [x] Lethal Company
## Mod Configuration ## Mod Configuration

View File

@@ -1,5 +1,6 @@
import os import os
from typing import Optional from typing import Optional
import time
from lmm.config import LMMConfig from lmm.config import LMMConfig
from lmm.const import LMM_GAMES_PATH from lmm.const import LMM_GAMES_PATH
@@ -75,9 +76,17 @@ def launch(game: str, profile: str):
runner.run(game) runner.run(game)
game.wait() game.wait()
# Unmount # Unmount (with retries)
for mount in mounts: failed = False
mount.unmount() for retry in range(5):
failed = any(not mount.unmount() for mount in mounts)
if not failed:
break
print(f"Unmounting failed ({retry +1 }/5). Waiting 3s...")
time.sleep(3)
if failed:
print("Unmounting failed!")
@cli.command() @cli.command()

View File

@@ -134,8 +134,12 @@ class BaldursGate3Game(ProtonGame):
return overlays return overlays
@property @property
def game_executable(self) -> Path: def game_executables(self) -> list[Path]:
return self.installation_path / "bin" / "bg3.exe" # Handle both the Dx11 version and the Vulkan version.
return [
self.installation_path / "bin" / "bg3_dx11.exe",
self.installation_path / "bin" / "bg3.exe",
]
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {

View File

@@ -15,6 +15,10 @@ from lmm.games.ron import (
GAME_NAME as RON, GAME_NAME as RON,
ReadyOrNotGame, ReadyOrNotGame,
) )
from lmm.games.lc import (
GAME_NAME as LC,
LethalCompanyGame,
)
import yaml import yaml
@@ -34,6 +38,8 @@ def load_game_configs() -> list[Game]:
BG3: BaldursGate3Game, BG3: BaldursGate3Game,
PDMM: ProjectDivaMegaMixGame, PDMM: ProjectDivaMegaMixGame,
RON: ReadyOrNotGame, RON: ReadyOrNotGame,
LC: LethalCompanyGame,
}.get(item, None) }.get(item, None)
if game_cls is None: if game_cls is None:
print(f"Unknown game {item}") print(f"Unknown game {item}")

View File

@@ -91,7 +91,7 @@ class ProtonGame(Game, abc.ABC):
raise NotImplementedError() raise NotImplementedError()
@property @property
def game_executable(self) -> Path: def game_executables(self) -> list[Path]:
raise NotImplementedError() raise NotImplementedError()
@property @property
@@ -101,9 +101,10 @@ class ProtonGame(Game, abc.ABC):
def wait(self): def wait(self):
# Wait until we started the game. # Wait until we started the game.
proc = Path("/proc") proc = Path("/proc")
windows_path = str(self.game_executable).replace("/", "\\") windows_paths = [str(executable).replace("/", "\\") for executable in self.game_executables]
wine_exe_path = f"Z:{windows_path}" wine_exe_paths = [f"Z:{windows_path}" for windows_path in windows_paths]
print(wine_exe_path) executables = [str(exe) for exe in self.game_executables]
print(wine_exe_paths)
print("Polling for game process...") print("Polling for game process...")
while True: while True:
for dir in os.listdir(proc): for dir in os.listdir(proc):
@@ -127,9 +128,9 @@ class ProtonGame(Game, abc.ABC):
abs_cmdline = os.path.abspath(cmdline).replace("\x00", "") abs_cmdline = os.path.abspath(cmdline).replace("\x00", "")
if ( if (
cmdline.startswith(wine_exe_path) any(cmdline.startswith(wine_exe_path) for wine_exe_path in wine_exe_paths)
or abs_cmdline == str(self.game_executable) or abs_cmdline in executables
or abs_cmdline.startswith(str(self.game_executable)) or any(abs_cmdline.startswith(exe) for exe in executables)
): ):
self._pid = dir self._pid = dir
break break

95
lmm/games/lc.py Normal file
View File

@@ -0,0 +1,95 @@
from pathlib import Path
from typing import Any
from lmm.games.game import ProtonGame
from lmm.profile import Profile
from lmm.overlayfs import OverlayFSMount
from lmm.const import LMM_GAMES_PATH
from lmm.runners.base import runner_from_config
GAME_NAME = "LethalCompany"
class LethalCompanyProfile(Profile):
# The names of the directories the the mods directory.
mods: list[str]
def __init__(self, name: str, mods: list[str], **kwargs):
super().__init__(GAME_NAME, name, **kwargs)
self.mods = mods
def get_mod_names(self) -> list[str]:
return self.mods
class LethalCompanyGame(ProtonGame):
def __init__(self, profiles: list[LethalCompanyProfile], **kwargs):
super().__init__(GAME_NAME, "1966720", profiles, **kwargs)
@property
def installation_path(self) -> Path:
return (
self.steam_library
/ "steamapps/common/Lethal Company"
)
@property
def game_executables(self) -> list[Path]:
return [
self.installation_path / "Lethal Company.exe",
]
def prepare_overlays(
self, profile: LethalCompanyProfile
) -> list[OverlayFSMount]:
return [
OverlayFSMount(
upper=self.installation_path,
lower=profile.mods,
mount=self.installation_path,
cd=LMM_GAMES_PATH / self.name,
),
]
def to_dict(self) -> dict[str, Any]:
return {
"profiles": [
{
"name": profile.name,
"mods": [
{
"name": mod,
}
for mod in profile.mods
],
}
for profile in self.profiles
]
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "LethalCompanyGame":
profiles = []
for profile in data["profiles"]:
mods = [mod["name"] for mod in profile["mods"]]
if "runner" in profile:
runner = runner_from_config(profile["runner"])
else:
runner = None
profiles.append(
LethalCompanyProfile(
profile["name"],
mods,
runner=runner,
),
)
if "default_runner" in data:
default_runner = runner_from_config(data["default_runner"])
else:
default_runner = None
return LethalCompanyGame(profiles, default_runner=default_runner)

View File

@@ -35,8 +35,10 @@ class ProjectDivaMegaMixGame(ProtonGame):
) )
@property @property
def game_executable(self) -> Path: def game_executables(self) -> list[Path]:
return self.installation_path / "DivaMegaMix.exe" return [
self.installation_path / "DivaMegaMix.exe",
]
def prepare_overlays( def prepare_overlays(
self, profile: ProjectDivaMegaMixProfile self, profile: ProjectDivaMegaMixProfile

View File

@@ -46,8 +46,10 @@ class ReadyOrNotGame(ProtonGame):
] ]
@property @property
def game_executable(self) -> Path: def game_executables(self) -> list[Path]:
return self.installation_path / "ReadyOrNot.exe" return [
self.installation_path / "ReadyOrNot.exe",
]
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {

View File

@@ -102,26 +102,30 @@ class OverlayFSMount:
self._mounted = True self._mounted = True
return self.mount_path return self.mount_path
def unmount(self): def unmount(self) -> bool:
if self._mounted: if self._mounted:
print("Unmounting...") print("Unmounting...")
# Remove the mount # Remove the mount
if LMMConfig.use_fuse: if LMMConfig.use_fuse:
run_cmd( returncode = run_cmd(
[ [
LMMConfig.fusermount_command, LMMConfig.fusermount_command,
"-u", "-u",
str(self.mount_path), str(self.mount_path),
] ]
) ) == 0
else: else:
run_sudo_cmd( returncode = run_sudo_cmd(
[ [
"umount", "umount",
str(self.mount_path), str(self.mount_path),
] ]
) )
if returncode != 0:
print("Unmounting failed")
return False
# Remove the temporary workdir # Remove the temporary workdir
if self.workdir is not None: if self.workdir is not None:
@@ -132,6 +136,7 @@ class OverlayFSMount:
print(f"Failed to clean temporary directory {self.workdir.name}") print(f"Failed to clean temporary directory {self.workdir.name}")
self._mounted = False self._mounted = False
return True
def __del__(self): def __del__(self):
self.unmount() self.unmount()

View File

@@ -4,7 +4,8 @@ version = "0.1.0"
requires-python = ">= 3.10" requires-python = ">= 3.10"
dependencies = [ dependencies = [
"vdf", "vdf",
"pyyaml" "pyyaml",
"click"
] ]
[project.optional-dependencies] [project.optional-dependencies]