Compare commits
7 Commits
76d9feb07a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 94582f1547 | |||
| b9fc712534 | |||
| 0998d0a655 | |||
| 2efc4fdf36 | |||
| 33e3eae4cb | |||
| a79006dc41 | |||
| f72e786efa |
@@ -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
|
||||||
|
|
||||||
|
|||||||
15
lmm/cli.py
15
lmm/cli.py
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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
95
lmm/games/lc.py
Normal 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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user