195 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			195 lines
		
	
	
		
			5.7 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_executables(self) -> list[Path]:
 | 
						|
        # Handle both the Dx11 version and the Vulkan version.
 | 
						|
        return [
 | 
						|
            self.installation_path / "bin" / "bg3_dx11.exe",
 | 
						|
            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)
 |