Add black
This commit is contained in:
		
							parent
							
								
									d352e70956
								
							
						
					
					
						commit
						b0d801f8a1
					
				
							
								
								
									
										32
									
								
								lmm/cli.py
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								lmm/cli.py
									
									
									
									
									
								
							| @ -9,6 +9,7 @@ from lmm.mods.bg3 import install_mod as bg3_install_mod | |||||||
| 
 | 
 | ||||||
| import click | import click | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def get_game_by_name(name: str) -> Optional[Game]: | def get_game_by_name(name: str) -> Optional[Game]: | ||||||
|     games = load_game_configs() |     games = load_game_configs() | ||||||
|     for game in games: |     for game in games: | ||||||
| @ -16,12 +17,14 @@ def get_game_by_name(name: str) -> Optional[Game]: | |||||||
|             return game |             return game | ||||||
|     return None |     return None | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @click.group() | @click.group() | ||||||
| @click.pass_context | @click.pass_context | ||||||
| def cli(ctx): | def cli(ctx): | ||||||
|     if ctx.invoked_subcommand is None: |     if ctx.invoked_subcommand is None: | ||||||
|         click.echo("Invoked without a subcommand") |         click.echo("Invoked without a subcommand") | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @cli.command() | @cli.command() | ||||||
| @click.option("-g", "--game", required=True) | @click.option("-g", "--game", required=True) | ||||||
| @click.option("-p", "--profile", required=True) | @click.option("-p", "--profile", required=True) | ||||||
| @ -30,13 +33,13 @@ def launch(game: str, profile: str): | |||||||
|     if game is None: |     if game is None: | ||||||
|         click.echo("Game not found") |         click.echo("Game not found") | ||||||
|         return |         return | ||||||
|      | 
 | ||||||
|     profile = game.get_profile_by_name(profile) |     profile = game.get_profile_by_name(profile) | ||||||
|     if profile is None: |     if profile is None: | ||||||
|         click.echo("Profile not found") |         click.echo("Profile not found") | ||||||
|         return |         return | ||||||
| 
 | 
 | ||||||
|     runner = None  |     runner = None | ||||||
|     if profile.runner is not None: |     if profile.runner is not None: | ||||||
|         runner = profile.runner |         runner = profile.runner | ||||||
|     elif game.default_runner is not None: |     elif game.default_runner is not None: | ||||||
| @ -56,17 +59,18 @@ def launch(game: str, profile: str): | |||||||
|             break |             break | ||||||
| 
 | 
 | ||||||
|         print(f"Mounted overlayfs at {result}") |         print(f"Mounted overlayfs at {result}") | ||||||
|      | 
 | ||||||
|     # Launch the game |     # Launch the game | ||||||
|     if mounts_ok: |     if mounts_ok: | ||||||
|         #runner.run() |         # runner.run() | ||||||
|         #game.wait() |         # game.wait() | ||||||
|         print(f"Stub runner: Would use {runner.__class__.__name__}") |         print(f"Stub runner: Would use {runner.__class__.__name__}") | ||||||
| 
 | 
 | ||||||
|     # Unmount |     # Unmount | ||||||
|     for mount in mounts: |     for mount in mounts: | ||||||
|         mount.unmount() |         mount.unmount() | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @cli.command() | @cli.command() | ||||||
| @click.option("-g", "--game", required=True) | @click.option("-g", "--game", required=True) | ||||||
| @click.option("-P", "--path", required=True) | @click.option("-P", "--path", required=True) | ||||||
| @ -75,11 +79,12 @@ def install(game: str, path: str): | |||||||
|         BG3: bg3_install_mod, |         BG3: bg3_install_mod, | ||||||
|     }.get(game, None) |     }.get(game, None) | ||||||
|     if install is None: |     if install is None: | ||||||
|         click.echo(f"Unknown game \"{game}\"") |         click.echo(f'Unknown game "{game}"') | ||||||
|         return |         return | ||||||
|      | 
 | ||||||
|     install(Path(path)) |     install(Path(path)) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @cli.command() | @cli.command() | ||||||
| @click.option("-g", "--game") | @click.option("-g", "--game") | ||||||
| def mods(game: Optional[str]): | def mods(game: Optional[str]): | ||||||
| @ -87,7 +92,9 @@ def mods(game: Optional[str]): | |||||||
|         games = [game] |         games = [game] | ||||||
|     else: |     else: | ||||||
|         games = [ |         games = [ | ||||||
|             item for item in os.listdir(LMM_GAMES_PATH) if (LMM_GAMES_PATH / item).is_dir() |             item | ||||||
|  |             for item in os.listdir(LMM_GAMES_PATH) | ||||||
|  |             if (LMM_GAMES_PATH / item).is_dir() | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|     for idx, game_name in enumerate(games): |     for idx, game_name in enumerate(games): | ||||||
| @ -98,10 +105,11 @@ def mods(game: Optional[str]): | |||||||
|                 continue |                 continue | ||||||
| 
 | 
 | ||||||
|             click.echo(f"- {item}") |             click.echo(f"- {item}") | ||||||
|          | 
 | ||||||
|         if idx < len(games) - 1: |         if idx < len(games) - 1: | ||||||
|             click.echo() |             click.echo() | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @cli.command | @cli.command | ||||||
| def list(): | def list(): | ||||||
|     games = load_game_configs() |     games = load_game_configs() | ||||||
| @ -126,12 +134,14 @@ def list(): | |||||||
|             click.echo(f"  - {profile.name}") |             click.echo(f"  - {profile.name}") | ||||||
|             click.echo(f"    Runner: {runner_name}") |             click.echo(f"    Runner: {runner_name}") | ||||||
|             click.echo(f"    Mods: {mods}") |             click.echo(f"    Mods: {mods}") | ||||||
|          | 
 | ||||||
|         if idx < len(games) - 1: |         if idx < len(games) - 1: | ||||||
|             click.echo() |             click.echo() | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def main(): | def main(): | ||||||
|     cli() |     cli() | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     cli() |     cli() | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import subprocess | |||||||
| from typing import Optional | from typing import Optional | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def run_cmd(args: list[str], cd: Optional[Path] = None) -> int: | def run_cmd(args: list[str], cd: Optional[Path] = None) -> int: | ||||||
|     cmdline = " ".join(args) |     cmdline = " ".join(args) | ||||||
|     if cd is not None: |     if cd is not None: | ||||||
| @ -16,17 +17,20 @@ def run_cmd(args: list[str], cd: Optional[Path] = None) -> int: | |||||||
|     os.chdir(cwd) |     os.chdir(cwd) | ||||||
|     return ret |     return ret | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def run_sudo_cmd(args: list[str], cd: Optional[Path] = None) -> int: | def run_sudo_cmd(args: list[str], cd: Optional[Path] = None) -> int: | ||||||
|     return run_cmd( |     return run_cmd( | ||||||
|         [ |         [ | ||||||
|             "pkexec", |             "pkexec", | ||||||
|             "--user", "root", |             "--user", | ||||||
|  |             "root", | ||||||
|             "--keep-cwd", |             "--keep-cwd", | ||||||
|             *args, |             *args, | ||||||
|         ], |         ], | ||||||
|         cd=cd, |         cd=cd, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def run_cmd_shell(cmd: str) -> None: | def run_cmd_shell(cmd: str) -> None: | ||||||
|     print("Executing:", cmd) |     print("Executing:", cmd) | ||||||
|     subprocess.run( |     subprocess.run( | ||||||
| @ -34,6 +38,7 @@ def run_cmd_shell(cmd: str) -> None: | |||||||
|         shell=True, |         shell=True, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def run_cmd_nonblocking(args: list[str]) -> subprocess.Popen: | def run_cmd_nonblocking(args: list[str]) -> subprocess.Popen: | ||||||
|     cmdline = " ".join(args) |     cmdline = " ".join(args) | ||||||
|     print("Executing:", cmdline) |     print("Executing:", cmdline) | ||||||
|  | |||||||
| @ -2,4 +2,4 @@ from pathlib import Path | |||||||
| 
 | 
 | ||||||
| LMM_PATH: Path = Path.home() / ".local" / "share" / "lmm" | LMM_PATH: Path = Path.home() / ".local" / "share" / "lmm" | ||||||
| 
 | 
 | ||||||
| LMM_GAMES_PATH: Path = LMM_PATH / "games" | LMM_GAMES_PATH: Path = LMM_PATH / "games" | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ from lmm.runners.base import runner_from_config | |||||||
| 
 | 
 | ||||||
| GAME_NAME = "BaldursGate3" | GAME_NAME = "BaldursGate3" | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class BaldursGate3ModType(Enum): | class BaldursGate3ModType(Enum): | ||||||
|     # Override the game files |     # Override the game files | ||||||
|     ROOT = 1 |     ROOT = 1 | ||||||
| @ -19,6 +20,7 @@ class BaldursGate3ModType(Enum): | |||||||
|     # Override the Paks in the compatdata prefix |     # Override the Paks in the compatdata prefix | ||||||
|     PAK = 2 |     PAK = 2 | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @dataclass | @dataclass | ||||||
| class BaldursGate3Mod: | class BaldursGate3Mod: | ||||||
|     # The directory name |     # The directory name | ||||||
| @ -36,12 +38,14 @@ class BaldursGate3Profile(Profile): | |||||||
|         super().__init__(GAME_NAME, name, **kwargs) |         super().__init__(GAME_NAME, name, **kwargs) | ||||||
|         self.mods = mods |         self.mods = mods | ||||||
| 
 | 
 | ||||||
|     def get_mods(self, type: BaldursGate3ModType) -> Generator[BaldursGate3Mod, None, None]: |     def get_mods( | ||||||
|  |         self, type: BaldursGate3ModType | ||||||
|  |     ) -> Generator[BaldursGate3Mod, None, None]: | ||||||
|         """Yields all mods of type @type""" |         """Yields all mods of type @type""" | ||||||
|         for mod in self.mods: |         for mod in self.mods: | ||||||
|             if not mod.type == type: |             if not mod.type == type: | ||||||
|                 continue |                 continue | ||||||
|              | 
 | ||||||
|             yield mod |             yield mod | ||||||
| 
 | 
 | ||||||
|     def has_mods_of_type(self, type: BaldursGate3ModType) -> bool: |     def has_mods_of_type(self, type: BaldursGate3ModType) -> bool: | ||||||
| @ -51,6 +55,7 @@ class BaldursGate3Profile(Profile): | |||||||
|     def get_mod_names(self) -> list[str]: |     def get_mod_names(self) -> list[str]: | ||||||
|         return [mod.name for mod in self.mods] |         return [mod.name for mod in self.mods] | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class BaldursGate3Game(ProtonGame): | class BaldursGate3Game(ProtonGame): | ||||||
|     def __init__(self, profiles: list[BaldursGate3Profile], **kwargs): |     def __init__(self, profiles: list[BaldursGate3Profile], **kwargs): | ||||||
|         super().__init__(GAME_NAME, "1086940", profiles, **kwargs) |         super().__init__(GAME_NAME, "1086940", profiles, **kwargs) | ||||||
| @ -65,7 +70,18 @@ class BaldursGate3Game(ProtonGame): | |||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def user_mods_path(self) -> Path: |     def user_mods_path(self) -> Path: | ||||||
|         return self.compat_path / "pfx" / "drive_c" / "users" / "steamuser" / "AppData" / "Local" / "Larian Studios" / "Baldur's Gate 3" / "Mods" |         return ( | ||||||
|  |             self.compat_path | ||||||
|  |             / "pfx" | ||||||
|  |             / "drive_c" | ||||||
|  |             / "users" | ||||||
|  |             / "steamuser" | ||||||
|  |             / "AppData" | ||||||
|  |             / "Local" | ||||||
|  |             / "Larian Studios" | ||||||
|  |             / "Baldur's Gate 3" | ||||||
|  |             / "Mods" | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     def can_start(self) -> bool: |     def can_start(self) -> bool: | ||||||
|         return self.user_mods_path.exists() and self.installation_path.exists() |         return self.user_mods_path.exists() and self.installation_path.exists() | ||||||
| @ -87,9 +103,7 @@ class BaldursGate3Game(ProtonGame): | |||||||
| 
 | 
 | ||||||
|         if profile.has_mods_of_type(BaldursGate3ModType.ROOT): |         if profile.has_mods_of_type(BaldursGate3ModType.ROOT): | ||||||
|             mods = list(profile.get_mods(BaldursGate3ModType.ROOT)) |             mods = list(profile.get_mods(BaldursGate3ModType.ROOT)) | ||||||
|             mod_paths = [ |             mod_paths = [LMM_GAMES_PATH / GAME_NAME / mod.name for mod in mods] | ||||||
|                 LMM_GAMES_PATH / GAME_NAME / mod.name for mod in mods |  | ||||||
|             ] |  | ||||||
|             overlays.append( |             overlays.append( | ||||||
|                 OverlayFSMount( |                 OverlayFSMount( | ||||||
|                     self.installation_path, |                     self.installation_path, | ||||||
| @ -102,9 +116,7 @@ class BaldursGate3Game(ProtonGame): | |||||||
|             print(f"Loaded root mods: {mod_names}") |             print(f"Loaded root mods: {mod_names}") | ||||||
|         if profile.has_mods_of_type(BaldursGate3ModType.PAK): |         if profile.has_mods_of_type(BaldursGate3ModType.PAK): | ||||||
|             mods = list(profile.get_mods(BaldursGate3ModType.PAK)) |             mods = list(profile.get_mods(BaldursGate3ModType.PAK)) | ||||||
|             mod_paths = [ |             mod_paths = [os.path.join("./", mod.name) for mod in mods] | ||||||
|                 os.path.join("./", mod.name) for mod in mods |  | ||||||
|             ] |  | ||||||
| 
 | 
 | ||||||
|             # Merge all mods |             # Merge all mods | ||||||
|             overlays.append( |             overlays.append( | ||||||
| @ -134,9 +146,11 @@ class BaldursGate3Game(ProtonGame): | |||||||
|                         { |                         { | ||||||
|                             "name": mod.name, |                             "name": mod.name, | ||||||
|                             "type": mod.type.value, |                             "type": mod.type.value, | ||||||
|                         } for mod in profile.mods |                         } | ||||||
|                     ] |                         for mod in profile.mods | ||||||
|                 } for profile in self.profiles |                     ], | ||||||
|  |                 } | ||||||
|  |                 for profile in self.profiles | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -149,7 +163,9 @@ class BaldursGate3Game(ProtonGame): | |||||||
|                 mods.append( |                 mods.append( | ||||||
|                     BaldursGate3Mod( |                     BaldursGate3Mod( | ||||||
|                         mod["name"], |                         mod["name"], | ||||||
|                         BaldursGate3ModType.ROOT if mod["type"] == 1 else BaldursGate3ModType.PAK, |                         BaldursGate3ModType.ROOT | ||||||
|  |                         if mod["type"] == 1 | ||||||
|  |                         else BaldursGate3ModType.PAK, | ||||||
|                     ), |                     ), | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
| @ -165,10 +181,10 @@ class BaldursGate3Game(ProtonGame): | |||||||
|                     runner=runner, |                     runner=runner, | ||||||
|                 ), |                 ), | ||||||
|             ) |             ) | ||||||
|          | 
 | ||||||
|         if "default_runner" in data: |         if "default_runner" in data: | ||||||
|             default_runner = runner_from_config(data["default_runner"]) |             default_runner = runner_from_config(data["default_runner"]) | ||||||
|         else: |         else: | ||||||
|             default_runner = None |             default_runner = None | ||||||
| 
 | 
 | ||||||
|         return BaldursGate3Game(profiles, default_runner=default_runner) |         return BaldursGate3Game(profiles, default_runner=default_runner) | ||||||
|  | |||||||
| @ -47,4 +47,4 @@ def load_game_configs() -> list[Game]: | |||||||
|                 ) |                 ) | ||||||
|             except Exception as ex: |             except Exception as ex: | ||||||
|                 print(f"Failed to load game {item}: {ex}") |                 print(f"Failed to load game {item}: {ex}") | ||||||
|     return games |     return games | ||||||
|  | |||||||
| @ -9,9 +9,11 @@ from lmm.overlayfs import OverlayFSMount | |||||||
| from lmm.profile import Profile | from lmm.profile import Profile | ||||||
| from lmm.steam import find_library_folder_for_game | from lmm.steam import find_library_folder_for_game | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class CannotStartReason: | class CannotStartReason: | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @dataclass | @dataclass | ||||||
| class PathNotExistingReason(CannotStartReason): | class PathNotExistingReason(CannotStartReason): | ||||||
|     # The path that is not existing |     # The path that is not existing | ||||||
| @ -20,6 +22,7 @@ class PathNotExistingReason(CannotStartReason): | |||||||
|     def __str__(self) -> str: |     def __str__(self) -> str: | ||||||
|         return f"Path {self.path} does not exist" |         return f"Path {self.path} does not exist" | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class Game(abc.ABC): | class Game(abc.ABC): | ||||||
|     # The name of the game. |     # The name of the game. | ||||||
|     name: str |     name: str | ||||||
| @ -30,7 +33,9 @@ class Game(abc.ABC): | |||||||
|     # The default runner to use when launching the game. |     # The default runner to use when launching the game. | ||||||
|     default_runner: Optional["Runner"] |     default_runner: Optional["Runner"] | ||||||
| 
 | 
 | ||||||
|     def __init__(self, name: str, profiles: list[Profile], default_runner: Optional["Runner"]): |     def __init__( | ||||||
|  |         self, name: str, profiles: list[Profile], default_runner: Optional["Runner"] | ||||||
|  |     ): | ||||||
|         self.name = name |         self.name = name | ||||||
|         self.profiles = profiles |         self.profiles = profiles | ||||||
|         self.default_runner = default_runner |         self.default_runner = default_runner | ||||||
| @ -63,6 +68,7 @@ class Game(abc.ABC): | |||||||
|     def wait(self): |     def wait(self): | ||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class ProtonGame(Game, abc.ABC): | class ProtonGame(Game, abc.ABC): | ||||||
|     # The appid of the game in Steam. |     # The appid of the game in Steam. | ||||||
|     appid: str |     appid: str | ||||||
| @ -70,7 +76,13 @@ class ProtonGame(Game, abc.ABC): | |||||||
|     # The PID of the game. |     # The PID of the game. | ||||||
|     _pid: Optional[str] |     _pid: Optional[str] | ||||||
| 
 | 
 | ||||||
|     def __init__(self, name: str, appid: str, profiles: list[Profile], default_runner: Optional["Runner"]): |     def __init__( | ||||||
|  |         self, | ||||||
|  |         name: str, | ||||||
|  |         appid: str, | ||||||
|  |         profiles: list[Profile], | ||||||
|  |         default_runner: Optional["Runner"], | ||||||
|  |     ): | ||||||
|         super().__init__(name, profiles, default_runner) |         super().__init__(name, profiles, default_runner) | ||||||
|         self._pid = None |         self._pid = None | ||||||
|         self.appid = appid |         self.appid = appid | ||||||
| @ -106,7 +118,9 @@ class ProtonGame(Game, abc.ABC): | |||||||
|                     # Workaround for games like Baldur's Gate 3 that have a relative path in the cmdline. |                     # 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. |                     # Note that the cmdline contains null bytes that we have to remove. | ||||||
|                     abs_cmdline = os.path.abspath(cmdline).replace("\x00", "") |                     abs_cmdline = os.path.abspath(cmdline).replace("\x00", "") | ||||||
|                     if cmdline.startswith(wine_exe_path) or abs_cmdline == str(self.game_executable): |                     if cmdline.startswith(wine_exe_path) or abs_cmdline == str( | ||||||
|  |                         self.game_executable | ||||||
|  |                     ): | ||||||
|                         self._pid = dir |                         self._pid = dir | ||||||
|                         break |                         break | ||||||
|                 except: |                 except: | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ from lmm.runners.base import runner_from_config | |||||||
| 
 | 
 | ||||||
| GAME_NAME = "ProjectDivaMegaMix" | GAME_NAME = "ProjectDivaMegaMix" | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class ProjectDivaMegaMixProfile(Profile): | class ProjectDivaMegaMixProfile(Profile): | ||||||
|     # The names of the directories the the mods directory. |     # The names of the directories the the mods directory. | ||||||
|     mods: list[str] |     mods: list[str] | ||||||
| @ -21,19 +22,25 @@ class ProjectDivaMegaMixProfile(Profile): | |||||||
|     def get_mod_names(self) -> list[str]: |     def get_mod_names(self) -> list[str]: | ||||||
|         return self.mods |         return self.mods | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class ProjectDivaMegaMixGame(ProtonGame): | class ProjectDivaMegaMixGame(ProtonGame): | ||||||
|     def __init__(self, profiles: list[ProjectDivaMegaMixProfile], **kwargs): |     def __init__(self, profiles: list[ProjectDivaMegaMixProfile], **kwargs): | ||||||
|         super().__init__(GAME_NAME, "1761390", profiles, **kwargs) |         super().__init__(GAME_NAME, "1761390", profiles, **kwargs) | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def installation_path(self) -> Path: |     def installation_path(self) -> Path: | ||||||
|         return self.steam_library / "steamapps/common/Hatsune Miku Project DIVA Mega Mix Plus" |         return ( | ||||||
|  |             self.steam_library | ||||||
|  |             / "steamapps/common/Hatsune Miku Project DIVA Mega Mix Plus" | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def game_executable(self) -> Path: |     def game_executable(self) -> Path: | ||||||
|         return self.installation_path / "DivaMegaMix.exe" |         return self.installation_path / "DivaMegaMix.exe" | ||||||
| 
 | 
 | ||||||
|     def prepare_overlays(self, profile: ProjectDivaMegaMixProfile) -> list[OverlayFSMount]: |     def prepare_overlays( | ||||||
|  |         self, profile: ProjectDivaMegaMixProfile | ||||||
|  |     ) -> list[OverlayFSMount]: | ||||||
|         return [ |         return [ | ||||||
|             OverlayFSMount( |             OverlayFSMount( | ||||||
|                 upper=self.installation_path, |                 upper=self.installation_path, | ||||||
| @ -51,9 +58,11 @@ class ProjectDivaMegaMixGame(ProtonGame): | |||||||
|                     "mods": [ |                     "mods": [ | ||||||
|                         { |                         { | ||||||
|                             "name": mod, |                             "name": mod, | ||||||
|                         } for mod in profile.mods |                         } | ||||||
|                     ] |                         for mod in profile.mods | ||||||
|                 } for profile in self.profiles |                     ], | ||||||
|  |                 } | ||||||
|  |                 for profile in self.profiles | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -61,9 +70,7 @@ class ProjectDivaMegaMixGame(ProtonGame): | |||||||
|     def from_dict(cls, data: dict[str, Any]) -> "ProjectDivaMegaMixGame": |     def from_dict(cls, data: dict[str, Any]) -> "ProjectDivaMegaMixGame": | ||||||
|         profiles = [] |         profiles = [] | ||||||
|         for profile in data["profiles"]: |         for profile in data["profiles"]: | ||||||
|             mods = [ |             mods = [mod["name"] for mod in profile["mods"]] | ||||||
|                 mod["name"] for mod in profile["mods"] |  | ||||||
|             ] |  | ||||||
| 
 | 
 | ||||||
|             if "runner" in profile: |             if "runner" in profile: | ||||||
|                 runner = runner_from_config(profile["runner"]) |                 runner = runner_from_config(profile["runner"]) | ||||||
| @ -83,4 +90,4 @@ class ProjectDivaMegaMixGame(ProtonGame): | |||||||
|         else: |         else: | ||||||
|             default_runner = None |             default_runner = None | ||||||
| 
 | 
 | ||||||
|         return ProjectDivaMegaMixGame(profiles, default_runner=default_runner) |         return ProjectDivaMegaMixGame(profiles, default_runner=default_runner) | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ from lmm.games.game import ProtonGame | |||||||
| 
 | 
 | ||||||
| GAME_NAME = "ReadyOrNot" | GAME_NAME = "ReadyOrNot" | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class ReadyOrNotProfile(Profile): | class ReadyOrNotProfile(Profile): | ||||||
|     # Names of directories inside the game's mods directory. |     # Names of directories inside the game's mods directory. | ||||||
|     mods: list[str] |     mods: list[str] | ||||||
| @ -18,6 +19,7 @@ class ReadyOrNotProfile(Profile): | |||||||
|     def get_mod_names(self) -> list[str]: |     def get_mod_names(self) -> list[str]: | ||||||
|         return self.mods |         return self.mods | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class ReadyOrNotGame(ProtonGame): | class ReadyOrNotGame(ProtonGame): | ||||||
|     def __init__(self, profiles: list[ReadyOrNotProfile], **kwargs): |     def __init__(self, profiles: list[ReadyOrNotProfile], **kwargs): | ||||||
|         super().__init__(GAME_NAME, "1144200", profiles, **kwargs) |         super().__init__(GAME_NAME, "1144200", profiles, **kwargs) | ||||||
| @ -55,9 +57,11 @@ class ReadyOrNotGame(ProtonGame): | |||||||
|                     "mods": [ |                     "mods": [ | ||||||
|                         { |                         { | ||||||
|                             "name": mod, |                             "name": mod, | ||||||
|                         } for mod in profile.mods |                         } | ||||||
|                     ] |                         for mod in profile.mods | ||||||
|                 } for profile in self.profiles |                     ], | ||||||
|  |                 } | ||||||
|  |                 for profile in self.profiles | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -65,9 +69,7 @@ class ReadyOrNotGame(ProtonGame): | |||||||
|     def from_dict(cls, data: dict[str, Any]) -> "ReadyOrNotGame": |     def from_dict(cls, data: dict[str, Any]) -> "ReadyOrNotGame": | ||||||
|         profiles = [] |         profiles = [] | ||||||
|         for profile in data["profiles"]: |         for profile in data["profiles"]: | ||||||
|             mods = [ |             mods = [mod["name"] for mod in profile["mods"]] | ||||||
|                 mod["name"] for mod in profile["mods"] |  | ||||||
|             ] |  | ||||||
| 
 | 
 | ||||||
|             if "runner" in profile: |             if "runner" in profile: | ||||||
|                 runner = runner_from_config(profile["runner"]) |                 runner = runner_from_config(profile["runner"]) | ||||||
| @ -87,4 +89,4 @@ class ReadyOrNotGame(ProtonGame): | |||||||
|         else: |         else: | ||||||
|             default_runner = None |             default_runner = None | ||||||
| 
 | 
 | ||||||
|         return ReadyOrNotGame(profiles, default_runner=default_runner) |         return ReadyOrNotGame(profiles, default_runner=default_runner) | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import zipfile | |||||||
| from lmm.const import LMM_GAMES_PATH | from lmm.const import LMM_GAMES_PATH | ||||||
| from lmm.games.bg3 import GAME_NAME | from lmm.games.bg3 import GAME_NAME | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def install_mod(mod: Path): | def install_mod(mod: Path): | ||||||
|     match mod.suffix: |     match mod.suffix: | ||||||
|         case ".zip": |         case ".zip": | ||||||
| @ -12,6 +13,7 @@ def install_mod(mod: Path): | |||||||
|         case _: |         case _: | ||||||
|             print("Unknown mod") |             print("Unknown mod") | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def install_zip_mod(mod: Path): | def install_zip_mod(mod: Path): | ||||||
|     # Inspect the archive to see if it's a PAK mod |     # Inspect the archive to see if it's a PAK mod | ||||||
|     with zipfile.ZipFile(mod, "r") as f: |     with zipfile.ZipFile(mod, "r") as f: | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ from typing import Optional | |||||||
| 
 | 
 | ||||||
| from lmm.cmd import run_sudo_cmd | from lmm.cmd import run_sudo_cmd | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def compute_workdirs_location(mount: Path) -> Path: | def compute_workdirs_location(mount: Path) -> Path: | ||||||
|     with open("/proc/mounts", "r") as f: |     with open("/proc/mounts", "r") as f: | ||||||
|         mounts = f.read().split("\n") |         mounts = f.read().split("\n") | ||||||
| @ -18,7 +19,7 @@ def compute_workdirs_location(mount: Path) -> Path: | |||||||
|         if str(mount).startswith(m) and len(m) > len(closest_mount or ""): |         if str(mount).startswith(m) and len(m) > len(closest_mount or ""): | ||||||
|             closest_mount = m |             closest_mount = m | ||||||
|             continue |             continue | ||||||
|      | 
 | ||||||
|     return Path("/tmp") if closest_mount is None else Path(closest_mount) / ".temp" |     return Path("/tmp") if closest_mount is None else Path(closest_mount) / ".temp" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -35,7 +36,13 @@ class OverlayFSMount: | |||||||
| 
 | 
 | ||||||
|     cd: Optional[Path] |     cd: Optional[Path] | ||||||
| 
 | 
 | ||||||
|     def __init__(self, upper: Optional[Path], lower: list[Path | str], mount: Path, cd: Optional[Path] = None): |     def __init__( | ||||||
|  |         self, | ||||||
|  |         upper: Optional[Path], | ||||||
|  |         lower: list[Path | str], | ||||||
|  |         mount: Path, | ||||||
|  |         cd: Optional[Path] = None, | ||||||
|  |     ): | ||||||
|         self.upper = upper |         self.upper = upper | ||||||
|         self.lower = lower |         self.lower = lower | ||||||
|         self.mount_path = mount |         self.mount_path = mount | ||||||
| @ -58,9 +65,7 @@ class OverlayFSMount: | |||||||
|         return Path(self.workdir.name) |         return Path(self.workdir.name) | ||||||
| 
 | 
 | ||||||
|     def mount(self) -> Optional[Path]: |     def mount(self) -> Optional[Path]: | ||||||
|         lower = ":".join([ |         lower = ":".join([str(l) for l in self.lower]) | ||||||
|             str(l) for l in self.lower |  | ||||||
|         ]) |  | ||||||
|         options = f"lowerdir={lower}" |         options = f"lowerdir={lower}" | ||||||
|         if self.upper is not None: |         if self.upper is not None: | ||||||
|             workdir = self.create_workdir() |             workdir = self.create_workdir() | ||||||
| @ -69,8 +74,11 @@ class OverlayFSMount: | |||||||
|         result = run_sudo_cmd( |         result = run_sudo_cmd( | ||||||
|             [ |             [ | ||||||
|                 "mount", |                 "mount", | ||||||
|                 "-t", "overlay", "overlay", |                 "-t", | ||||||
|                 "-o", options, |                 "overlay", | ||||||
|  |                 "overlay", | ||||||
|  |                 "-o", | ||||||
|  |                 options, | ||||||
|                 str(self.mount_path), |                 str(self.mount_path), | ||||||
|             ], |             ], | ||||||
|             cd=self.cd, |             cd=self.cd, | ||||||
| @ -85,12 +93,14 @@ class OverlayFSMount: | |||||||
|     def unmount(self): |     def unmount(self): | ||||||
|         if self._mounted: |         if self._mounted: | ||||||
|             print("Unmounting...") |             print("Unmounting...") | ||||||
|              | 
 | ||||||
|             # Remove the mount |             # Remove the mount | ||||||
|             run_sudo_cmd([ |             run_sudo_cmd( | ||||||
|                 "umount", |                 [ | ||||||
|                 str(self.mount_path), |                     "umount", | ||||||
|             ]) |                     str(self.mount_path), | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
| 
 | 
 | ||||||
|             # Remove the temporary workdir |             # Remove the temporary workdir | ||||||
|             if self.workdir is not None: |             if self.workdir is not None: | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ from typing import Optional | |||||||
| 
 | 
 | ||||||
| from lmm.const import LMM_GAMES_PATH | from lmm.const import LMM_GAMES_PATH | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class Profile: | class Profile: | ||||||
|     # The name of the game. |     # The name of the game. | ||||||
|     game: str |     game: str | ||||||
| @ -19,4 +20,4 @@ class Profile: | |||||||
|         self.runner = runner |         self.runner = runner | ||||||
| 
 | 
 | ||||||
|     def get_mod_names(self) -> list[str]: |     def get_mod_names(self) -> list[str]: | ||||||
|         raise NotImplementedError() |         raise NotImplementedError() | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ from lmm.games.game import Game, ProtonGame | |||||||
| from lmm.runners.runner import Runner | from lmm.runners.runner import Runner | ||||||
| from lmm.runners.steam import SteamFlatpakAppIdRunner | from lmm.runners.steam import SteamFlatpakAppIdRunner | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def runner_from_config(data: dict[str, Any]) -> Optional[Runner]: | def runner_from_config(data: dict[str, Any]) -> Optional[Runner]: | ||||||
|     match data["type"]: |     match data["type"]: | ||||||
|         case "steam.flatpak": |         case "steam.flatpak": | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import abc | |||||||
| 
 | 
 | ||||||
| from lmm.games.game import Game | from lmm.games.game import Game | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class Runner(abc.ABC): | class Runner(abc.ABC): | ||||||
|     # Type identifier of the runner. |     # Type identifier of the runner. | ||||||
|     runner_type: str |     runner_type: str | ||||||
| @ -10,4 +11,4 @@ class Runner(abc.ABC): | |||||||
|         self.runner_type = runner_type |         self.runner_type = runner_type | ||||||
| 
 | 
 | ||||||
|     def run(self, game: Game): |     def run(self, game: Game): | ||||||
|         raise NotImplementedError() |         raise NotImplementedError() | ||||||
|  | |||||||
| @ -4,10 +4,12 @@ from pathlib import Path | |||||||
| from lmm.runners.runner import Runner | from lmm.runners.runner import Runner | ||||||
| from lmm.games.game import Game | from lmm.games.game import Game | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class SteamRuntime(abc.ABC): | class SteamRuntime(abc.ABC): | ||||||
|     def get_run_script(self) -> Path: |     def get_run_script(self) -> Path: | ||||||
|         raise NotImplementedError() |         raise NotImplementedError() | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class SteamSniperRuntime(SteamRuntime): | class SteamSniperRuntime(SteamRuntime): | ||||||
|     library: Path |     library: Path | ||||||
| 
 | 
 | ||||||
| @ -15,7 +17,13 @@ class SteamSniperRuntime(SteamRuntime): | |||||||
|         self.library = library |         self.library = library | ||||||
| 
 | 
 | ||||||
|     def get_run_script(self) -> Path: |     def get_run_script(self) -> Path: | ||||||
|         return self.library / "steamapps" / "common" / "SteamLinuxRuntime_sniper" / "run-in-sniper" |         return ( | ||||||
|  |             self.library | ||||||
|  |             / "steamapps" | ||||||
|  |             / "common" | ||||||
|  |             / "SteamLinuxRuntime_sniper" | ||||||
|  |             / "run-in-sniper" | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SteamSoldierRuntime(SteamRuntime): | class SteamSoldierRuntime(SteamRuntime): | ||||||
| @ -25,7 +33,14 @@ class SteamSoldierRuntime(SteamRuntime): | |||||||
|         self.library = library |         self.library = library | ||||||
| 
 | 
 | ||||||
|     def get_run_script(self) -> Path: |     def get_run_script(self) -> Path: | ||||||
|         return self.library / "steamapps" / "common" / "SteamLinuxRuntime_soldier" / "run-in-soldier" |         return ( | ||||||
|  |             self.library | ||||||
|  |             / "steamapps" | ||||||
|  |             / "common" | ||||||
|  |             / "SteamLinuxRuntime_soldier" | ||||||
|  |             / "run-in-soldier" | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class SteamFlatpakAppIdRunner(Runner): | class SteamFlatpakAppIdRunner(Runner): | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
| @ -34,12 +49,15 @@ class SteamFlatpakAppIdRunner(Runner): | |||||||
|     def run(self, game: Game): |     def run(self, game: Game): | ||||||
|         assert isinstance(game, ProtonGame) |         assert isinstance(game, ProtonGame) | ||||||
| 
 | 
 | ||||||
|         run_cmd_nonblocking([ |         run_cmd_nonblocking( | ||||||
|             "flatpak", |             [ | ||||||
|             "run", |                 "flatpak", | ||||||
|             "com.valvesoftware.Steam", |                 "run", | ||||||
|             f"steam://launch/{game.appid}/", |                 "com.valvesoftware.Steam", | ||||||
|         ]) |                 f"steam://launch/{game.appid}/", | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class SteamFlatpakProtonRunner(Runner): | class SteamFlatpakProtonRunner(Runner): | ||||||
|     # Path to the Proton prefix |     # Path to the Proton prefix | ||||||
| @ -55,7 +73,14 @@ class SteamFlatpakProtonRunner(Runner): | |||||||
|     # "waitforexitandrun" | "run" |     # "waitforexitandrun" | "run" | ||||||
|     launchmode: str |     launchmode: str | ||||||
| 
 | 
 | ||||||
|     def __init__(self, compat_data: Path, runtime: SteamRuntime, proton_version: str, game: str, launchmode: str = "waitforexitandrun"): |     def __init__( | ||||||
|  |         self, | ||||||
|  |         compat_data: Path, | ||||||
|  |         runtime: SteamRuntime, | ||||||
|  |         proton_version: str, | ||||||
|  |         game: str, | ||||||
|  |         launchmode: str = "waitforexitandrun", | ||||||
|  |     ): | ||||||
|         self.compat_data = compat_data |         self.compat_data = compat_data | ||||||
|         self.runtime = runtime |         self.runtime = runtime | ||||||
|         self.game = game |         self.game = game | ||||||
| @ -72,16 +97,22 @@ class SteamFlatpakProtonRunner(Runner): | |||||||
|         return [ |         return [ | ||||||
|             f"--env=STEAM_COMPAT_CLIENT_INSTALL_PATH={self._steam_install_path}", |             f"--env=STEAM_COMPAT_CLIENT_INSTALL_PATH={self._steam_install_path}", | ||||||
|             f"--env=STEAM_COMPAT_DATA_PATH={self.compat_data}", |             f"--env=STEAM_COMPAT_DATA_PATH={self.compat_data}", | ||||||
|             #f"--env=WINEDLLPATH={proton}/files/lib64/wine:{proton}/files/lib/wine", |             # f"--env=WINEDLLPATH={proton}/files/lib64/wine:{proton}/files/lib/wine", | ||||||
|             #f"--env=WINEPREFIX={self.compat_data}/pfx", |             # f"--env=WINEPREFIX={self.compat_data}/pfx", | ||||||
|             #f"--env=SteamGameId=1144200", |             # f"--env=SteamGameId=1144200", | ||||||
|             #"--env=WINEDLLOVERRIDES=steam.exe=b" |             # "--env=WINEDLLOVERRIDES=steam.exe=b" | ||||||
|             #f"--env=WINEDEBUG=-all", |             # f"--env=WINEDEBUG=-all", | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def _proton_path(self) -> Path: |     def _proton_path(self) -> Path: | ||||||
|         return Path.home() / "data" / "Steam" / "compatibilitytools.d" / self.proton_version |         return ( | ||||||
|  |             Path.home() | ||||||
|  |             / "data" | ||||||
|  |             / "Steam" | ||||||
|  |             / "compatibilitytools.d" | ||||||
|  |             / self.proton_version | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     def _build_launch_command(self) -> str: |     def _build_launch_command(self) -> str: | ||||||
|         command = [ |         command = [ | ||||||
| @ -94,12 +125,16 @@ class SteamFlatpakProtonRunner(Runner): | |||||||
|         return commandline |         return commandline | ||||||
| 
 | 
 | ||||||
|     def run(self): |     def run(self): | ||||||
|         run_cmd_shell(" ".join([ |         run_cmd_shell( | ||||||
|             "flatpak", |             " ".join( | ||||||
|             "run", |                 [ | ||||||
|             "--command=bash", |                     "flatpak", | ||||||
|             *self._build_environ(), |                     "run", | ||||||
|             "com.valvesoftware.Steam", |                     "--command=bash", | ||||||
|             "-c", |                     *self._build_environ(), | ||||||
|             f'"{self._build_launch_command()}"', |                     "com.valvesoftware.Steam", | ||||||
|         ])) |                     "-c", | ||||||
|  |                     f'"{self._build_launch_command()}"', | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ STEAM_PATHS = [ | |||||||
|     Path.home() / ".var/app/com.valvesoftware.Steam/data/Steam", |     Path.home() / ".var/app/com.valvesoftware.Steam/data/Steam", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def find_library_folder_for_game(appid: str) -> Optional[Path]: | def find_library_folder_for_game(appid: str) -> Optional[Path]: | ||||||
|     for steam_path in STEAM_PATHS: |     for steam_path in STEAM_PATHS: | ||||||
|         if not steam_path.exists(): |         if not steam_path.exists(): | ||||||
| @ -18,7 +19,7 @@ def find_library_folder_for_game(appid: str) -> Optional[Path]: | |||||||
| 
 | 
 | ||||||
|         with open(library_folders_vdf, "r") as f: |         with open(library_folders_vdf, "r") as f: | ||||||
|             content = vdf.load(f) |             content = vdf.load(f) | ||||||
|          | 
 | ||||||
|         for index in content["libraryfolders"]: |         for index in content["libraryfolders"]: | ||||||
|             library = content["libraryfolders"][index] |             library = content["libraryfolders"][index] | ||||||
| 
 | 
 | ||||||
| @ -28,4 +29,4 @@ def find_library_folder_for_game(appid: str) -> Optional[Path]: | |||||||
|             if appid in library["apps"]: |             if appid in library["apps"]: | ||||||
|                 return Path(library["path"]) |                 return Path(library["path"]) | ||||||
| 
 | 
 | ||||||
|     return None |     return None | ||||||
|  | |||||||
| @ -7,5 +7,11 @@ dependencies = [ | |||||||
|     "pyyaml" |     "pyyaml" | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [project.optional-dependencies] | ||||||
|  | dev = [ | ||||||
|  |     "black", | ||||||
|  |     "pylint" | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [project.scripts] | [project.scripts] | ||||||
| lmm = "lmm.cli:main" | lmm = "lmm.cli:main" | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user