diff options
Diffstat (limited to 'libs/basic_games/games/baldursgate3/bg3_utils.py')
| -rw-r--r-- | libs/basic_games/games/baldursgate3/bg3_utils.py | 274 |
1 files changed, 274 insertions, 0 deletions
diff --git a/libs/basic_games/games/baldursgate3/bg3_utils.py b/libs/basic_games/games/baldursgate3/bg3_utils.py new file mode 100644 index 0000000..958d173 --- /dev/null +++ b/libs/basic_games/games/baldursgate3/bg3_utils.py @@ -0,0 +1,274 @@ +import functools +import shutil +import typing +from pathlib import Path +from time import sleep + +from PyQt6.QtCore import ( + QCoreApplication, + QDir, + QEventLoop, + QRunnable, + Qt, + QThread, + QThreadPool, + qInfo, + qWarning, +) +from PyQt6.QtWidgets import QApplication, QMainWindow, QProgressDialog + +import mobase + +loose_file_folders = { + "Public", + "Mods", + "Generated", + "Localization", + "ScriptExtender", +} + + +def get_node_string( + folder: str = "", + md5: str = "", + name: str = "", + publish_handle: str = "0", + uuid: str = "", + version64: str = "0", +) -> str: + return f""" + <node id="ModuleShortDesc"> + <attribute id="Folder" type="LSString" value="{folder}"/> + <attribute id="MD5" type="LSString" value="{md5}"/> + <attribute id="Name" type="LSString" value="{name}"/> + <attribute id="PublishHandle" type="uint64" value="{publish_handle}"/> + <attribute id="UUID" type="guid" value="{uuid}"/> + <attribute id="Version64" type="int64" value="{version64}"/> + </node>""" + + +class BG3Utils: + _mod_settings_xml_start = """\ +<?xml version="1.0" encoding="UTF-8"?> +<save> + <version major="4" minor="8" revision="0" build="500"/> + <region id="ModuleSettings"> + <node id="root"> + <children> + <node id="Mods"> + <children>""" + get_node_string( + folder="GustavX", + name="GustavX", + uuid="cb555efe-2d9e-131f-8195-a89329d218ea", + version64="36028797018963968", + ) + _mod_settings_xml_end = """ + </children> + </node> + </children> + </node> + </region> +</save>""" + + def __init__(self, name: str): + self.main_window = None + self._name = name + from . import lslib_retriever, pak_parser + + self.lslib_retriever = lslib_retriever.LSLibRetriever(self) + self._pak_parser = pak_parser.BG3PakParser(self) + + def init(self, organizer: mobase.IOrganizer): + self._organizer = organizer + + @functools.cached_property + def autobuild_paks(self): + return bool(self.get_setting("autobuild_paks")) + + @functools.cached_property + def extract_full_package(self): + return bool(self.get_setting("extract_full_package")) + + @functools.cached_property + def remove_extracted_metadata(self): + return bool(self.get_setting("remove_extracted_metadata")) + + @functools.cached_property + def force_load_dlls(self): + return bool(self.get_setting("force_load_dlls")) + + @functools.cached_property + def log_diff(self): + return bool(self.get_setting("log_diff")) + + @functools.cached_property + def convert_yamls_to_json(self): + return bool(self.get_setting("convert_yamls_to_json")) + + @functools.cached_property + def log_dir(self): + return create_dir_if_needed(Path(self._organizer.basePath()) / "logs") + + @functools.cached_property + def modsettings_backup(self): + return create_dir_if_needed(self.plugin_data_path / "temp" / "modsettings.lsx") + + @functools.cached_property + def modsettings_path(self): + return create_dir_if_needed( + Path(self._organizer.profilePath()) / "modsettings.lsx" + ) + + @functools.cached_property + def plugin_data_path(self) -> Path: + """Gets the path to the data folder for the current plugin.""" + return create_dir_if_needed( + Path(self._organizer.pluginDataPath(), self._name).absolute() + ) + + @functools.cached_property + def tools_dir(self): + return create_dir_if_needed(self.plugin_data_path / "tools") + + @functools.cached_property + def overwrite_path(self): + return create_dir_if_needed(Path(self._organizer.overwritePath())) + + def active_mods(self) -> list[mobase.IModInterface]: + modlist = self._organizer.modList() + return [ + modlist.getMod(mod_name) + for mod_name in filter( + lambda mod: modlist.state(mod) & mobase.ModState.ACTIVE, + modlist.allModsByProfilePriority(), + ) + ] + + def _set_setting(self, key: str, value: mobase.MoVariant): + self._organizer.setPluginSetting(self._name, key, value) + + def get_setting(self, key: str) -> mobase.MoVariant: + return self._organizer.pluginSetting(self._name, key) + + def tr(self, trstr: str) -> str: + return QCoreApplication.translate(self._name, trstr) + + def create_progress_window( + self, title: str, max_progress: int, msg: str = "", cancelable: bool = True + ) -> QProgressDialog: + progress = QProgressDialog( + self.tr(msg if msg else title), + self.tr("Cancel") if cancelable else None, + 0, + max_progress, + self.main_window, + ) + progress.setWindowTitle(self.tr(f"BG3 Plugin: {title}")) + progress.setWindowModality(Qt.WindowModality.ApplicationModal) + progress.show() + return progress + + def on_user_interface_initialized(self, window: QMainWindow) -> None: + self.main_window = window + + def on_settings_changed( + self, + plugin_name: str, + setting: str, + old: mobase.MoVariant, + new: mobase.MoVariant, + ) -> None: + if self._name != plugin_name: + return + if setting in { + "extract_full_package", + "autobuild_paks", + "remove_extracted_metadata", + "force_load_dlls", + "log_diff", + "convert_yamls_to_json", + } and hasattr(self, setting): + delattr(self, setting) + + def construct_modsettings_xml( + self, + exec_path: str = "", + working_dir: typing.Optional[QDir] = None, + args: str = "", + force_reparse_metadata: bool = False, + ) -> bool: + if ( + "bin/bg3" not in exec_path + or not self.lslib_retriever.download_lslib_if_missing() + ): + return True + active_mods = self.active_mods() + progress = self.create_progress_window( + "Generating modsettings.xml", len(active_mods) + ) + threadpool = QThreadPool.globalInstance() + if threadpool is None: + return False + metadata: dict[str, str] = {} + + def retrieve_mod_metadata_in_new_thread(mod: mobase.IModInterface): + return lambda: metadata.update( + self._pak_parser.get_metadata_for_files_in_mod( + mod, force_reparse_metadata + ) + ) + + for mod in active_mods: + if progress.wasCanceled(): + qWarning("processing canceled by user") + return False + threadpool.start(QRunnable.create(retrieve_mod_metadata_in_new_thread(mod))) + count = 0 + num_active_mods = len(active_mods) + total_intervals_to_wait = (num_active_mods * 2) + 20 + while len(metadata.keys()) < num_active_mods: + progress.setValue(len(metadata.keys())) + QApplication.processEvents(QEventLoop.ProcessEventsFlag.AllEvents, 100) + count += 1 + if count == total_intervals_to_wait or progress.wasCanceled(): + remaining_mods = {mod.name() for mod in active_mods} - metadata.keys() + qWarning(f"processing did not finish in time for: {remaining_mods}") + progress.close() + break + QThread.msleep(100) + progress.setValue(num_active_mods) + QApplication.processEvents(QEventLoop.ProcessEventsFlag.AllEvents, 100) + progress.close() + qInfo(f"writing mod load order to {self.modsettings_path}") + self.modsettings_path.parent.mkdir(parents=True, exist_ok=True) + self.modsettings_path.write_text( + ( + self._mod_settings_xml_start + + "".join( + metadata[mod.name()] + for mod in active_mods + if mod.name() in metadata + ) + + self._mod_settings_xml_end + ) + ) + qInfo( + f"backing up generated file {self.modsettings_path} to {self.modsettings_backup}, " + f"check the backup after the executable runs for differences with the file used by the game if you encounter issues" + ) + self.modsettings_backup.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(self.modsettings_path, self.modsettings_backup) + sleep(0.5) + return True + + def on_mod_installed(self, mod: mobase.IModInterface) -> None: + if self.lslib_retriever.download_lslib_if_missing(): + self._pak_parser.get_metadata_for_files_in_mod(mod, True) + + +def create_dir_if_needed(path: Path) -> Path: + if "." not in path.name[1:]: + path.mkdir(parents=True, exist_ok=True) + else: + path.parent.mkdir(parents=True, exist_ok=True) + return path |
