2023-12-02 16:45:16 +00:00
|
|
|
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"
|
|
|
|
|
2023-12-02 16:46:48 +00:00
|
|
|
|
2023-12-02 16:45:16 +00:00
|
|
|
class BaldursGate3ModType(Enum):
|
|
|
|
# Override the game files
|
|
|
|
ROOT = 1
|
|
|
|
|
|
|
|
# Override the Paks in the compatdata prefix
|
|
|
|
PAK = 2
|
|
|
|
|
2023-12-02 16:46:48 +00:00
|
|
|
|
2023-12-02 16:45:16 +00:00
|
|
|
@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
|
|
|
|
|
2023-12-02 16:46:48 +00:00
|
|
|
def get_mods(
|
|
|
|
self, type: BaldursGate3ModType
|
|
|
|
) -> Generator[BaldursGate3Mod, None, None]:
|
2023-12-02 16:45:16 +00:00
|
|
|
"""Yields all mods of type @type"""
|
|
|
|
for mod in self.mods:
|
|
|
|
if not mod.type == type:
|
|
|
|
continue
|
2023-12-02 16:46:48 +00:00
|
|
|
|
2023-12-02 16:45:16 +00:00
|
|
|
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]
|
|
|
|
|
2023-12-02 16:46:48 +00:00
|
|
|
|
2023-12-02 16:45:16 +00:00
|
|
|
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:
|
2023-12-02 16:46:48 +00:00
|
|
|
return (
|
|
|
|
self.compat_path
|
|
|
|
/ "pfx"
|
|
|
|
/ "drive_c"
|
|
|
|
/ "users"
|
|
|
|
/ "steamuser"
|
|
|
|
/ "AppData"
|
|
|
|
/ "Local"
|
|
|
|
/ "Larian Studios"
|
|
|
|
/ "Baldur's Gate 3"
|
|
|
|
/ "Mods"
|
|
|
|
)
|
2023-12-02 16:45:16 +00:00
|
|
|
|
|
|
|
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))
|
2023-12-02 16:46:48 +00:00
|
|
|
mod_paths = [LMM_GAMES_PATH / GAME_NAME / mod.name for mod in mods]
|
2023-12-02 16:45:16 +00:00
|
|
|
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))
|
2023-12-02 16:46:48 +00:00
|
|
|
mod_paths = [os.path.join("./", mod.name) for mod in mods]
|
2023-12-02 16:45:16 +00:00
|
|
|
|
|
|
|
# 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:
|
2023-12-23 12:31:12 +00:00
|
|
|
return self.installation_path / "bin" / "bg3_dx11.exe"
|
2023-12-02 16:45:16 +00:00
|
|
|
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
|
|
return {
|
|
|
|
"profiles": [
|
|
|
|
{
|
|
|
|
"name": profile.name,
|
|
|
|
"mods": [
|
|
|
|
{
|
|
|
|
"name": mod.name,
|
|
|
|
"type": mod.type.value,
|
2023-12-02 16:46:48 +00:00
|
|
|
}
|
|
|
|
for mod in profile.mods
|
|
|
|
],
|
|
|
|
}
|
|
|
|
for profile in self.profiles
|
2023-12-02 16:45:16 +00:00
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
@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"],
|
2023-12-02 16:46:48 +00:00
|
|
|
BaldursGate3ModType.ROOT
|
|
|
|
if mod["type"] == 1
|
|
|
|
else BaldursGate3ModType.PAK,
|
2023-12-02 16:45:16 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
if "runner" in profile:
|
|
|
|
runner = runner_from_config(profile["runner"])
|
|
|
|
else:
|
|
|
|
runner = None
|
|
|
|
|
|
|
|
profiles.append(
|
|
|
|
BaldursGate3Profile(
|
|
|
|
profile["name"],
|
|
|
|
mods,
|
|
|
|
runner=runner,
|
|
|
|
),
|
|
|
|
)
|
2023-12-02 16:46:48 +00:00
|
|
|
|
2023-12-02 16:45:16 +00:00
|
|
|
if "default_runner" in data:
|
|
|
|
default_runner = runner_from_config(data["default_runner"])
|
|
|
|
else:
|
|
|
|
default_runner = None
|
|
|
|
|
2023-12-02 16:46:48 +00:00
|
|
|
return BaldursGate3Game(profiles, default_runner=default_runner)
|