diff options
Diffstat (limited to 'libs/basic_games/games/game_baldursgate3.py')
| -rw-r--r-- | libs/basic_games/games/game_baldursgate3.py | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/libs/basic_games/games/game_baldursgate3.py b/libs/basic_games/games/game_baldursgate3.py new file mode 100644 index 0000000..4898696 --- /dev/null +++ b/libs/basic_games/games/game_baldursgate3.py @@ -0,0 +1,239 @@ +import datetime +import difflib +import os +import shutil +from functools import cached_property +from pathlib import Path +from typing import Any + +from PyQt6.QtCore import QLoggingCategory, qDebug, qInfo + +import mobase + +from ..basic_features import BasicGameSaveGameInfo, BasicLocalSavegames +from ..basic_game import BasicGame +from .baldursgate3 import bg3_file_mapper + + +class BG3Game(BasicGame, bg3_file_mapper.BG3FileMapper): + Name = "Baldur's Gate 3 Plugin" + Author = "daescha" + Version = "0.1.0" + GameName = "Baldur's Gate 3" + GameShortName = "baldursgate3" + GameNexusName = "baldursgate3" + GameValidShortNames = ["bg3"] + GameLauncher = "Launcher/LariLauncher.exe" + GameBinary = "bin/bg3.exe" + GameDataPath = "" + GameDocumentsDirectory = ( + "%USERPROFILE%/AppData/Local/Larian Studios/Baldur's Gate 3" + ) + GameSavesDirectory = "%GAME_DOCUMENTS%/PlayerProfiles/Public/Savegames/Story" + GameSaveExtension = "lsv" + GameNexusId = 3474 + GameSteamId = 1086940 + GameGogId = 1456460669 + + def __init__(self): + BasicGame.__init__(self) + from .baldursgate3 import bg3_utils + + self.utils = bg3_utils.BG3Utils(self.name()) + bg3_file_mapper.BG3FileMapper.__init__( + self, self.utils, self.documentsDirectory + ) + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self.utils.init(organizer) + from .baldursgate3 import bg3_data_checker, bg3_data_content + + self._register_feature(bg3_data_checker.BG3ModDataChecker()) + self._register_feature(bg3_data_content.BG3DataContent()) + self._register_feature(BasicGameSaveGameInfo(lambda s: s.with_suffix(".webp"))) + self._register_feature(BasicLocalSavegames(self)) + organizer.onAboutToRun(self.utils.construct_modsettings_xml) + organizer.onFinishedRun(self._on_finished_run) + organizer.onUserInterfaceInitialized(self.utils.on_user_interface_initialized) + organizer.modList().onModInstalled(self.utils.on_mod_installed) + organizer.onPluginSettingChanged(self.utils.on_settings_changed) + return True + + def settings(self): + base_settings = super().settings() + custom_settings = [ + mobase.PluginSetting( + "force_load_dlls", + "Force load all dlls detected in active mods. Removes the need for 'Native Mod Loader' and similar mods.", + True, + ), + mobase.PluginSetting( + "log_diff", + "Log a diff of the modsettings.xml file before and after the game runs to check for differences.", + False, + ), + mobase.PluginSetting( + "delete_levelcache_folders_older_than_x_days", + "Maximum number of days a file in overwrite/LevelCache is allowed to exist before being deleted " + "after the executable finishes. Set to negative to disable.", + 3, + ), + mobase.PluginSetting( + "autobuild_paks", + "Autobuild folders likely to be PAK folders with every run of an executable.", + True, + ), + mobase.PluginSetting( + "remove_extracted_metadata", + "Remove extracted meta.lsx files when they are no longer needed.", + True, + ), + mobase.PluginSetting( + "extract_full_package", + "Extract the full pak when parsing metadata, instead of just meta.lsx.", + False, + ), + mobase.PluginSetting( + "convert_yamls_to_json", + "Convert YAMLs to JSONs when executable runs. Allows one to configure ScriptExtender and related mods with YAML files.", + False, + ), + ] + for setting in custom_settings: + setting.description = self.utils.tr(setting.description) + base_settings.append(setting) + return base_settings + + def executables(self) -> list[mobase.ExecutableInfo]: + return [ + mobase.ExecutableInfo( + f"{self.gameName()}: DX11", + self.gameDirectory().absoluteFilePath("bin/bg3_dx11.exe"), + ), + mobase.ExecutableInfo( + f"{self.gameName()}: Vulkan", + self.gameDirectory().absoluteFilePath(self.binaryName()), + ), + mobase.ExecutableInfo( + "Larian Launcher", + self.gameDirectory().absoluteFilePath(self.getLauncherName()), + ), + ] + + def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]: + try: + efls = super().executableForcedLoads() + except AttributeError: + efls = [] + if self.utils.force_load_dlls: + qInfo("detecting dlls in enabled mods") + libs: set[str] = set() + tree: mobase.IFileTree | mobase.FileTreeEntry | None = ( + self._organizer.virtualFileTree().find("bin") + ) + if type(tree) is not mobase.IFileTree: + return efls + + def find_dlls( + _: Any, entry: mobase.FileTreeEntry + ) -> mobase.IFileTree.WalkReturn: + relpath = entry.pathFrom(tree) + if ( + relpath + and entry.hasSuffix("dll") + and relpath not in self._base_dlls + ): + libs.add(relpath) + return mobase.IFileTree.WalkReturn.CONTINUE + + tree.walk(find_dlls) + exes = self.executables() + qDebug(f"dlls to force load: {libs}") + efls = efls + [ + mobase.ExecutableForcedLoadSetting( + exe.binary().fileName(), lib + ).withEnabled(True) + for lib in libs + for exe in exes + ] + return efls + + def loadOrderMechanism(self) -> mobase.LoadOrderMechanism: + return mobase.LoadOrderMechanism.PLUGINS_TXT + + @cached_property + def _base_dlls(self) -> set[str]: + base_bin = Path(self.gameDirectory().absoluteFilePath("bin")) + return {str(f.relative_to(base_bin)) for f in base_bin.glob("*.dll")} + + def _on_finished_run(self, exec_path: str, exit_code: int): + if "bin/bg3" not in exec_path: + return + cat = QLoggingCategory.defaultCategory() + self.utils.log_dir.mkdir(parents=True, exist_ok=True) + if ( + cat is not None + and cat.isDebugEnabled() + and self.utils.log_diff + and self.utils.modsettings_backup.exists() + and self.utils.modsettings_path.exists() + ): + for x in difflib.unified_diff( + self.utils.modsettings_backup.open().readlines(), + self.utils.modsettings_path.open().readlines(), + fromfile=str(self.utils.modsettings_backup), + tofile=str(self.utils.modsettings_path), + lineterm="", + ): + qDebug(x) + moved: dict[str, str] = {} + for path in self.utils.overwrite_path.rglob("*.log"): + try: + moved[str(path.relative_to(Path.home()))] = str( + (self.utils.log_dir / path.name).relative_to(Path.home()) + ) + path.replace(self.utils.log_dir / path.name) + except PermissionError as e: + qDebug(str(e)) + for path in self.utils.overwrite_path.rglob("*log.txt"): + dest = self.utils.log_dir / path.name + if path.name == "log.txt": + dest = self.utils.log_dir / f"{path.parent.name}-{path.name}" + try: + moved[str(path.relative_to(Path.home()))] = str( + dest.relative_to(Path.home()) + ) + path.replace(dest) + except PermissionError as e: + qDebug(str(e)) + if cat is not None and cat.isDebugEnabled() and len(moved) > 0: + qDebug(f"moved log files to logs dir: {moved}") + days = self.utils.get_setting("delete_levelcache_folders_older_than_x_days") + if type(days) is int and days >= 0: + cutoff_time = datetime.datetime.now() - datetime.timedelta(days=days) + qDebug(f"cleaning folders in overwrite/LevelCache older than {cutoff_time}") + removed: set[Path] = set() + for path in self.utils.overwrite_path.glob("LevelCache/*"): + if ( + datetime.datetime.fromtimestamp(os.path.getmtime(path)) + < cutoff_time + ): + shutil.rmtree(path, ignore_errors=True) + removed.add(path) + if cat is not None and cat.isDebugEnabled() and len(removed) > 0: + qDebug( + f"cleaned the following folders due to them being older than {cutoff_time}: {removed}" + ) + for fdir in {self.utils.overwrite_path, self.doc_path}: + removed: set[Path] = set() + for folder in sorted(list(fdir.walk(top_down=False)))[:-1]: + try: + folder[0].rmdir() + removed.add(folder[0].relative_to(Path.home())) + except OSError: + pass + if cat is not None and cat.isDebugEnabled() and len(removed) > 0: + qDebug( + f"cleaned empty dirs from {fdir.relative_to(Path.home())} {removed}" + ) |
