diff --git a/games/baldursgate3/bg3_file_mapper.py b/games/baldursgate3/bg3_file_mapper.py index ba0b987..0ae6ef8 100644 --- a/games/baldursgate3/bg3_file_mapper.py +++ b/games/baldursgate3/bg3_file_mapper.py @@ -1,11 +1,9 @@ import functools -import json import os from pathlib import Path from typing import Callable, Optional -import yaml -from PyQt6.QtCore import QDir, qDebug, qInfo, qWarning +from PyQt6.QtCore import QDir, QLoggingCategory, qDebug, qInfo, qWarning from PyQt6.QtWidgets import QApplication import mobase @@ -29,29 +27,49 @@ def mappings(self) -> list[mobase.Mapping]: qInfo("creating custom bg3 mappings") self.current_mappings.clear() active_mods = self._utils.active_mods() - doc_dir = Path(self.doc_dir().path()) + if not active_mods: + return [] progress = self._utils.create_progress_window( "Mapping files to documents folder", len(active_mods) + 1 ) - docs_path_mods = doc_dir / "Mods" - docs_path_se = doc_dir / "Script Extender" + docs_path_mods = self.doc_path / "Mods" + docs_path_se = self.doc_path / "Script Extender" for mod in active_mods: modpath = Path(mod.absolutePath()) self.map_files(modpath, dest=docs_path_mods, pattern="*.pak", rel=False) self.map_files(modpath / "Script Extender", dest=docs_path_se) + if self._utils.convert_yamls_to_json: + self.map_files(modpath / "bin", only_convert=True) progress.setValue(progress.value() + 1) QApplication.processEvents() if progress.wasCanceled(): qWarning("mapping canceled by user") return self.current_mappings + (self._utils.overwrite_path / "Script Extender").mkdir( + parents=True, exist_ok=True + ) + (self._utils.overwrite_path / "Stats").mkdir(parents=True, exist_ok=True) + (self._utils.overwrite_path / "Temp").mkdir(parents=True, exist_ok=True) + (self._utils.overwrite_path / "LevelCache").mkdir(parents=True, exist_ok=True) + (self._utils.overwrite_path / "Stats").mkdir(parents=True, exist_ok=True) + (self._utils.overwrite_path / "Mods").mkdir(parents=True, exist_ok=True) + (self._utils.overwrite_path / "GMCampaigns").mkdir(parents=True, exist_ok=True) self.map_files(self._utils.overwrite_path) self.create_mapping( self._utils.modsettings_path, - doc_dir / "PlayerProfiles" / "Public" / self._utils.modsettings_path.name, + self.doc_path + / "PlayerProfiles" + / "Public" + / self._utils.modsettings_path.name, ) progress.setValue(len(active_mods) + 1) QApplication.processEvents() progress.close() + cat = QLoggingCategory.defaultCategory() + if cat is not None and cat.isDebugEnabled(): + qDebug( + f"resolved mappings: { {m.source: m.destination for m in self.current_mappings} }" + ) return self.current_mappings def map_files( @@ -60,6 +78,7 @@ def map_files( dest: Optional[Path] = None, pattern: str = "*", rel: bool = True, + only_convert: bool = False, ): dest = dest if dest else self.doc_path dest_func: Callable[[Path], str] = ( @@ -77,6 +96,10 @@ def map_files( if not converted_path.exists() or os.path.getmtime( file ) > os.path.getmtime(converted_path): + import json + + import yaml + with open(file, "r") as yaml_file: with open(converted_path, "w") as json_file: json.dump( @@ -88,12 +111,16 @@ def map_files( qWarning(f"Error accessing file {converted_path}: {e}") elif file.name.endswith(".json"): found_jsons.add(file) - else: + elif not only_convert: self.create_mapping(file, dest / dest_func(file)) + if only_convert: + return for file in found_jsons: self.create_mapping(file, dest / dest_func(file)) def create_mapping(self, file: Path, dest: Path): + bg3_utils.create_dir_if_needed(dest) + self.current_mappings.append( mobase.Mapping( source=str(file), diff --git a/games/baldursgate3/bg3_utils.py b/games/baldursgate3/bg3_utils.py index a61890c..958d173 100644 --- a/games/baldursgate3/bg3_utils.py +++ b/games/baldursgate3/bg3_utils.py @@ -2,6 +2,7 @@ import shutil import typing from pathlib import Path +from time import sleep from PyQt6.QtCore import ( QCoreApplication, @@ -27,30 +28,47 @@ } +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 = """ - - - - - - - - - - - - - - - """ + _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 @@ -89,28 +107,32 @@ def convert_yamls_to_json(self): @functools.cached_property def log_dir(self): - return Path(self._organizer.basePath()) / "logs" + return create_dir_if_needed(Path(self._organizer.basePath()) / "logs") @functools.cached_property def modsettings_backup(self): - return self.plugin_data_path / "temp" / "modsettings.lsx" + return create_dir_if_needed(self.plugin_data_path / "temp" / "modsettings.lsx") @functools.cached_property def modsettings_path(self): - return Path(self._organizer.profilePath()) / "modsettings.lsx" + 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 Path(self._organizer.pluginDataPath(), self._name).absolute() + return create_dir_if_needed( + Path(self._organizer.pluginDataPath(), self._name).absolute() + ) @functools.cached_property def tools_dir(self): - return self.plugin_data_path / "tools" + return create_dir_if_needed(self.plugin_data_path / "tools") @functools.cached_property def overwrite_path(self): - return Path(self._organizer.overwritePath()) + return create_dir_if_needed(Path(self._organizer.overwritePath())) def active_mods(self) -> list[mobase.IModInterface]: modlist = self._organizer.modList() @@ -234,9 +256,19 @@ def retrieve_mod_metadata_in_new_thread(mod: mobase.IModInterface): 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 diff --git a/games/baldursgate3/lslib_retriever.py b/games/baldursgate3/lslib_retriever.py index 5a031f0..ee1122c 100644 --- a/games/baldursgate3/lslib_retriever.py +++ b/games/baldursgate3/lslib_retriever.py @@ -86,6 +86,7 @@ def reporthook(block_num: int, block_size: int, total_size: int) -> None: err.setText( "LSLib tools are required for the proper generation of the modsettings.xml file, file will not be generated" ) + err.exec() return False else: progress = self._utils.create_progress_window( @@ -106,6 +107,7 @@ def reporthook(block_num: int, block_size: int, total_size: int) -> None: new_msg.setText( self._utils.tr("Latest version of LSLib already downloaded!") ) + new_msg.exec() except Exception as e: qDebug(f"Download failed: {e}") diff --git a/games/baldursgate3/pak_parser.py b/games/baldursgate3/pak_parser.py index 826e124..18598a9 100644 --- a/games/baldursgate3/pak_parser.py +++ b/games/baldursgate3/pak_parser.py @@ -161,17 +161,21 @@ def _get_metadata_for_file( ) build_pak = True if pak_path.exists(): - pak_creation_time = os.path.getmtime(pak_path) - for root, _, files in os.walk(file): - for f in files: - file_path = os.path.join(root, f) - try: - if os.path.getmtime(file_path) > pak_creation_time: + try: + pak_creation_time = os.path.getmtime(pak_path) + for root, _, files in file.walk(): + for f in files: + file_path = root.joinpath(f) + try: + if os.path.getmtime(file_path) > pak_creation_time: + break + except OSError as e: + qDebug(f"Error accessing file {file_path}: {e}") break - except OSError as e: - qDebug(f"Error accessing file {file_path}: {e}") - break - else: + else: + build_pak = False + except OSError as e: + qDebug(f"Error accessing file {pak_path}: {e}") build_pak = False if build_pak: pak_path.unlink(missing_ok=True) @@ -274,18 +278,18 @@ def metadata_to_ini( def get_module_short_desc(config: configparser.ConfigParser, file: Path) -> str: + if not config.has_section(file.name): + return "" + section: configparser.SectionProxy = config[file.name] return ( "" - if not config.has_section(file.name) - or "override" in config[file.name].keys() - or "Name" not in config[file.name].keys() - else f""" - - - - - - - - """ + if "override" in section.keys() or "Name" not in section.keys() + else bg3_utils.get_node_string( + folder=section["Folder"], + md5=section["MD5"], + name=section["Name"], + publish_handle=section["PublishHandle"], + uuid=section["UUID"], + version64=section["Version64"], + ) ) diff --git a/games/baldursgate3/plugins/bg3_tool_plugin.py b/games/baldursgate3/plugins/bg3_tool_plugin.py index d77c5fc..fc258a6 100644 --- a/games/baldursgate3/plugins/bg3_tool_plugin.py +++ b/games/baldursgate3/plugins/bg3_tool_plugin.py @@ -1,19 +1,21 @@ -from pathlib import Path - from PyQt6.QtCore import QCoreApplication -from PyQt6.QtGui import QIcon +from PyQt6.QtGui import QIcon, QPixmap import mobase class BG3ToolPlugin(mobase.IPluginTool, mobase.IPlugin): - icon_file = desc = sub_name = "" + desc = sub_name = "" + icon_bytes: bytes def __init__(self): mobase.IPluginTool.__init__(self) mobase.IPlugin.__init__(self) self._pluginName = self._displayName = "BG3 Tools" self._pluginVersion = mobase.VersionInfo(1, 0, 0) + pixmap = QPixmap() + pixmap.loadFromData(self.icon_bytes, "SVG") + self.qicon = QIcon(pixmap) def init(self, organizer: mobase.IOrganizer) -> bool: self._organizer = organizer @@ -41,7 +43,7 @@ def settings(self) -> list[mobase.PluginSetting]: return [] def icon(self) -> QIcon: - return QIcon(str(Path(__file__).parent / "icons" / self.icon_file)) + return self.qicon def description(self) -> str: return QCoreApplication.translate(self._pluginName, self.desc) diff --git a/games/baldursgate3/plugins/check_for_lslib_updates_plugin.py b/games/baldursgate3/plugins/check_for_lslib_updates_plugin.py index bc4df8c..b534e8a 100644 --- a/games/baldursgate3/plugins/check_for_lslib_updates_plugin.py +++ b/games/baldursgate3/plugins/check_for_lslib_updates_plugin.py @@ -1,8 +1,9 @@ from .bg3_tool_plugin import BG3ToolPlugin +from .icons import download class BG3ToolCheckForLsLibUpdates(BG3ToolPlugin): - icon_file = "ui-update.ico" + icon_bytes = download sub_name = "Check For LsLib Updates" desc = "Check to see if there has been a new release of LSLib and create download dialog if so." diff --git a/games/baldursgate3/plugins/convert_jsons_to_yaml_plugin.py b/games/baldursgate3/plugins/convert_jsons_to_yaml_plugin.py index 58a51bb..fde3431 100644 --- a/games/baldursgate3/plugins/convert_jsons_to_yaml_plugin.py +++ b/games/baldursgate3/plugins/convert_jsons_to_yaml_plugin.py @@ -6,10 +6,11 @@ from PyQt6.QtWidgets import QApplication from .bg3_tool_plugin import BG3ToolPlugin +from .icons import exchange class BG3ToolConvertJsonsToYaml(BG3ToolPlugin): - icon_file = "ui-next.ico" + icon_bytes = exchange sub_name = "Convert JSONS to YAML" desc = "Convert all jsons in active mods to yaml immediately." @@ -39,14 +40,14 @@ def display(self): def _convert_jsons_in_dir_to_yaml(path: Path): - import yaml - for file in list(path.rglob("*.json")): converted_path = file.parent / file.name.replace(".json", ".yaml") try: if not converted_path.exists() or os.path.getmtime(file) > os.path.getmtime( converted_path ): + import yaml + with open(file, "r") as json_file: with open(converted_path, "w") as yaml_file: yaml.dump( diff --git a/games/baldursgate3/plugins/icons.py b/games/baldursgate3/plugins/icons.py new file mode 100644 index 0000000..adc1023 --- /dev/null +++ b/games/baldursgate3/plugins/icons.py @@ -0,0 +1,42 @@ +refresh = b""" + + + + + + + +""" + +exchange = b""" + + + + + + + + + + +""" + +download = b""" + + + + + + + + + + +""" diff --git a/games/baldursgate3/plugins/icons/ui-next.ico b/games/baldursgate3/plugins/icons/ui-next.ico deleted file mode 100644 index 1ef6e3b..0000000 Binary files a/games/baldursgate3/plugins/icons/ui-next.ico and /dev/null differ diff --git a/games/baldursgate3/plugins/icons/ui-refresh.ico b/games/baldursgate3/plugins/icons/ui-refresh.ico deleted file mode 100644 index c018f6c..0000000 Binary files a/games/baldursgate3/plugins/icons/ui-refresh.ico and /dev/null differ diff --git a/games/baldursgate3/plugins/icons/ui-update.ico b/games/baldursgate3/plugins/icons/ui-update.ico deleted file mode 100644 index 5faaf18..0000000 Binary files a/games/baldursgate3/plugins/icons/ui-update.ico and /dev/null differ diff --git a/games/baldursgate3/plugins/reparse_pak_metadata_plugin.py b/games/baldursgate3/plugins/reparse_pak_metadata_plugin.py index 4f75371..f32a8a0 100644 --- a/games/baldursgate3/plugins/reparse_pak_metadata_plugin.py +++ b/games/baldursgate3/plugins/reparse_pak_metadata_plugin.py @@ -1,8 +1,9 @@ from .bg3_tool_plugin import BG3ToolPlugin +from .icons import refresh class BG3ToolReparsePakMetadata(BG3ToolPlugin): - icon_file = "ui-refresh.ico" + icon_bytes = refresh sub_name = "Reparse Pak Metadata" desc = "Force reparsing mod metadata immediately." diff --git a/games/game_baldursgate3.py b/games/game_baldursgate3.py index fcc4497..560eec1 100644 --- a/games/game_baldursgate3.py +++ b/games/game_baldursgate3.py @@ -7,6 +7,7 @@ from typing import Any from PyQt6.QtCore import ( + QLoggingCategory, qDebug, qInfo, ) @@ -179,37 +180,70 @@ def _base_dlls(self) -> set[str]: 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 self.utils.log_diff: + 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( - open(self.utils.modsettings_backup).readlines(), - open(self.utils.modsettings_path).readlines(), + 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: - qDebug(f"moving {path} to {self.utils.log_dir}") - shutil.move(path, self.utils.log_dir / path.name) + 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) - qDebug("cleaning empty dirs from overwrite directory") - for folder in sorted( - list(os.walk(self.utils.overwrite_path))[1:], reverse=True - ): - try: - os.rmdir(folder[0]) - except OSError: - pass + 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}" + )