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"""
"""
class BG3Utils:
_mod_settings_xml_start = """\
""" + get_node_string(
folder="GustavX",
name="GustavX",
uuid="cb555efe-2d9e-131f-8195-a89329d218ea",
version64="36028797018963968",
)
_mod_settings_xml_end = """
"""
def __init__(self, name: str):
self.main_window = None
self._name = name
from . import pak_parser
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:
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:
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