import os from typing import Optional import time from lmm.config import LMMConfig from lmm.const import LMM_GAMES_PATH from lmm.games.config import load_game_configs from lmm.games.game import Game from lmm.games.bg3 import GAME_NAME as BG3 from lmm.mods.bg3 import install_mod as bg3_install_mod import click def get_game_by_name(name: str) -> Optional[Game]: games = load_game_configs() for game in games: if game.name == name: return game return None @click.group() @click.pass_context def cli(ctx): if ctx.invoked_subcommand is None: click.echo("Invoked without a subcommand") @cli.command() @click.option("-g", "--game", required=True) @click.option("-p", "--profile", required=True) def launch(game: str, profile: str): game = get_game_by_name(game) if game is None: click.echo("Game not found") return profile = game.get_profile_by_name(profile) if profile is None: click.echo("Profile not found") return # Check if the game can start. if not game.can_start(): click.echo("Cannot start game:") for issue in game.cannot_start_reasons(): click.echo(issue) return # Check if we have a runner available. runner = None if profile.runner is not None: runner = profile.runner elif game.default_runner is not None: runner = game.default_runner if runner is None: click.echo("Cannot launch profile as no runner or default runner is configured") return # Prepare mounts mounts_ok = True mounts = game.prepare_overlays(profile) for mount in mounts: result = mount.mount() if result is None: click.echo("Mount failed") mounts_ok = False break print(f"Mounted overlayfs at {result}") # Launch the game if mounts_ok: runner.run(game) game.wait() # Unmount (with retries) failed = False 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() @click.option("-g", "--game", required=True) @click.option("-P", "--path", required=True) def install(game: str, path: str): install = { BG3: bg3_install_mod, }.get(game, None) if install is None: click.echo(f'Unknown game "{game}"') return install(Path(path)) @cli.command() @click.option("-g", "--game") def mods(game: Optional[str]): if game is not None: games = [game] else: games = [ item for item in os.listdir(LMM_GAMES_PATH) if (LMM_GAMES_PATH / item).is_dir() ] for idx, game_name in enumerate(games): click.echo(f"Installed mods for {game_name}:") for item in os.listdir(LMM_GAMES_PATH / game_name): path = LMM_GAMES_PATH / game_name / item if not path.is_dir(): continue click.echo(f"- {item}") if idx < len(games) - 1: click.echo() @cli.command def list(): games = load_game_configs() for idx, game in enumerate(games): if game.default_runner is not None: default_runner_name = game.default_runner.__class__.__name__ else: default_runner_name = "None" click.echo(f"- {game.name}") click.echo(f" {game.installation_path}") click.echo(f" Can start: {game.can_start()}") click.echo(f" Default runner: {default_runner_name}") click.echo(" Profiles:") for profile in game.profiles: mods = ", ".join(profile.get_mod_names()) if profile.runner is not None: runner_name = profile.runner.__class__.__name__ else: runner_name = "None" click.echo(f" - {profile.name}") click.echo(f" Runner: {runner_name}") click.echo(f" Mods: {mods}") if idx < len(games) - 1: click.echo() def main(): LMMConfig.init() cli() if __name__ == "__main__": cli()