linux-mod-manager/lmm/games/bg3.py

174 lines
5.4 KiB
Python

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.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)