import os from pathlib import Path from enum import Enum, auto from dataclasses import dataclass from typing import Generator, Any from lmm.const import LMM_GAMES_PATH from lmm.profile import Profile from lmm.overlayfs import OverlayFSMount from lmm.games.game import ProtonGame, CannotStartReason, PathNotExistingReason from lmm.runners.base import runner_from_config GAME_NAME = "BaldursGate3" class BaldursGate3ModType(Enum): # Override the game files ROOT = 1 # Override the Paks in the compatdata prefix PAK = 2 @dataclass class BaldursGate3Mod: # The directory name name: str # The type of mod type: BaldursGate3ModType @dataclass class BaldursGate3Profile(Profile): mods: list[BaldursGate3Mod] def __init__(self, name: str, mods: list[BaldursGate3Mod], **kwargs): super().__init__(GAME_NAME, name, **kwargs) self.mods = mods def get_mods( self, type: BaldursGate3ModType ) -> Generator[BaldursGate3Mod, None, None]: """Yields all mods of type @type""" for mod in self.mods: if not mod.type == type: continue yield mod def has_mods_of_type(self, type: BaldursGate3ModType) -> bool: """Returns whether the profile has any mods of type @type""" return any(self.get_mods(type)) def get_mod_names(self) -> list[str]: return [mod.name for mod in self.mods] class BaldursGate3Game(ProtonGame): def __init__(self, profiles: list[BaldursGate3Profile], **kwargs): super().__init__(GAME_NAME, "1086940", profiles, **kwargs) @property def installation_path(self) -> Path: return self.steam_library / "steamapps" / "common" / "Baldurs Gate 3" @property def compat_path(self) -> Path: return self.steam_library / "steamapps" / "compatdata" / self.appid @property def user_mods_path(self) -> Path: return ( self.compat_path / "pfx" / "drive_c" / "users" / "steamuser" / "AppData" / "Local" / "Larian Studios" / "Baldur's Gate 3" / "Mods" ) def can_start(self) -> bool: return self.user_mods_path.exists() and self.installation_path.exists() def cannot_start_reasons(self) -> list[CannotStartReason]: issues = [] if not self.user_mods_path.exists(): issues.append( PathNotExistingReason(self.user_mods_path), ) if not self.installation_path.exists(): issues.append( PathNotExistingReason(self.installation_path), ) return issues def prepare_overlays(self, profile: BaldursGate3Profile) -> list[OverlayFSMount]: overlays = [] if profile.has_mods_of_type(BaldursGate3ModType.ROOT): mods = list(profile.get_mods(BaldursGate3ModType.ROOT)) mod_paths = [LMM_GAMES_PATH / GAME_NAME / mod.name for mod in mods] overlays.append( OverlayFSMount( self.installation_path, mod_paths, mount=self.installation_path, ), ) mod_names = ", ".join([mod.name for mod in mods]) print(f"Loaded root mods: {mod_names}") if profile.has_mods_of_type(BaldursGate3ModType.PAK): mods = list(profile.get_mods(BaldursGate3ModType.PAK)) mod_paths = [os.path.join("./", mod.name) for mod in mods] # Merge all mods overlays.append( OverlayFSMount( self.user_mods_path, mod_paths, self.user_mods_path, cd=LMM_GAMES_PATH / self.name, ), ) mod_names = ", ".join([mod.name for mod in mods]) print(f"Loaded PAK mods: {mod_names}") return overlays @property def game_executable(self) -> Path: return self.installation_path / "bin" / "bg3_dx11.exe" def to_dict(self) -> dict[str, Any]: return { "profiles": [ { "name": profile.name, "mods": [ { "name": mod.name, "type": mod.type.value, } for mod in profile.mods ], } for profile in self.profiles ] } @classmethod def from_dict(cls, data: dict[str, Any]) -> "BaldursGate3Game": profiles = [] for profile in data["profiles"]: mods = [] for mod in profile["mods"]: mods.append( BaldursGate3Mod( mod["name"], BaldursGate3ModType.ROOT if mod["type"] == 1 else BaldursGate3ModType.PAK, ), ) if "runner" in profile: runner = runner_from_config(profile["runner"]) else: runner = None profiles.append( BaldursGate3Profile( profile["name"], mods, runner=runner, ), ) if "default_runner" in data: default_runner = runner_from_config(data["default_runner"]) else: default_runner = None return BaldursGate3Game(profiles, default_runner=default_runner)