Skip to content

Commit 995d860

Browse files
author
d.schlatter
committed
move various utils into bg3utils, move pak parsing into parse_paks
1 parent 1c3a40a commit 995d860

4 files changed

Lines changed: 563 additions & 505 deletions

File tree

games/baldursgate3/bg3_utils.py

Lines changed: 248 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
1-
from functools import cached_property
1+
import json
2+
import os
3+
import shutil
4+
import functools
25
from pathlib import Path
6+
import typing
37

48
import mobase
5-
from PyQt6.QtCore import QCoreApplication, Qt
6-
from PyQt6.QtWidgets import QProgressDialog, QMainWindow
9+
import yaml
10+
from PyQt6.QtCore import (
11+
QCoreApplication,
12+
Qt,
13+
qInfo,
14+
qWarning,
15+
QEventLoop,
16+
QRunnable,
17+
QDir,
18+
QThreadPool,
19+
QThread,
20+
)
21+
from PyQt6.QtWidgets import QProgressDialog, QMainWindow, QApplication
722

8-
from games.baldursgate3.lslib_retriever import LSLibRetriever
23+
from games.baldursgate3 import pak_parser, lslib_retriever
924

1025

1126
class BG3Utils:
@@ -16,22 +31,105 @@ class BG3Utils:
1631
"Localization",
1732
"ScriptExtender",
1833
}
34+
_mod_settings_xml_start = """<?xml version="1.0" encoding="UTF-8"?>
35+
<save>
36+
<version major="4" minor="8" revision="0" build="200"/>
37+
<region id="ModuleSettings">
38+
<node id="root">
39+
<children>
40+
<node id="Mods">
41+
<children>
42+
<node id="ModuleShortDesc">
43+
<attribute id="Folder" type="LSString" value="GustavX"/>
44+
<attribute id="MD5" type="LSString" value=""/>
45+
<attribute id="Name" type="LSString" value="GustavX"/>
46+
<attribute id="PublishHandle" type="uint64" value="0"/>
47+
<attribute id="UUID" type="guid" value="cb555efe-2d9e-131f-8195-a89329d218ea"/>
48+
<attribute id="Version64" type="int64" value="36028797018963968"/>
49+
</node>"""
50+
_mod_settings_xml_end = """
51+
</children>
52+
</node>
53+
</children>
54+
</node>
55+
</region>
56+
</save>"""
57+
1958
def __init__(self, organizer: mobase.IOrganizer, name: str):
2059
self.main_window = None
2160
self._organizer = organizer
2261
self._name = name
23-
self.lslib_retriever = LSLibRetriever(self)
24-
@cached_property
62+
self._lslib_retriever = lslib_retriever.LSLibRetriever(self)
63+
self._pak_parser = pak_parser.BG3PakParser(self)
64+
65+
@functools.cached_property
66+
def autobuild_paks(self):
67+
return bool(self.get_setting("autobuild_paks"))
68+
69+
@functools.cached_property
70+
def extract_full_package(self):
71+
return bool(self.get_setting("extract_full_package"))
72+
73+
@functools.cached_property
74+
def remove_extracted_metadata(self):
75+
return bool(self.get_setting("remove_extracted_metadata"))
76+
77+
@functools.cached_property
78+
def force_load_dlls(self):
79+
return bool(self.get_setting("force_load_dlls"))
80+
81+
@functools.cached_property
82+
def log_diff(self):
83+
return bool(self.get_setting("log_diff"))
84+
85+
@functools.cached_property
86+
def convert_yamls_to_json(self):
87+
return bool(self.get_setting("convert_yamls_to_json"))
88+
89+
@functools.cached_property
90+
def log_dir(self):
91+
return Path(self._organizer.basePath()) / "logs/"
92+
93+
@functools.cached_property
94+
def modsettings_backup(self):
95+
return self.plugin_data_path / "temp/modsettings.lsx"
96+
97+
@functools.cached_property
98+
def modsettings_path(self):
99+
return self.overwrite_path / "PlayerProfiles/Public/modsettings.lsx"
100+
101+
@functools.cached_property
25102
def plugin_data_path(self) -> Path:
26103
"""Gets the path to the data folder for the current plugin."""
27104
return Path(self._organizer.pluginDataPath(), self._name).absolute()
28-
@cached_property
105+
106+
@functools.cached_property
29107
def tools_dir(self):
30108
return self.plugin_data_path / "tools"
109+
110+
@functools.cached_property
111+
def overwrite_path(self):
112+
return Path(self._organizer.overwritePath())
113+
114+
def active_mods(self) -> list[mobase.IModInterface]:
115+
modlist = self._organizer.modList()
116+
return [
117+
modlist.getMod(mod_name)
118+
for mod_name in filter(
119+
lambda mod: modlist.state(mod) & mobase.ModState.ACTIVE,
120+
modlist.allModsByProfilePriority(),
121+
)
122+
]
123+
124+
def _set_setting(self, key: str, value: mobase.MoVariant):
125+
self._organizer.setPluginSetting(self._name, key, value)
126+
31127
def get_setting(self, key: str) -> mobase.MoVariant:
32128
return self._organizer.pluginSetting(self._name, key)
129+
33130
def tr(self, trstr: str) -> str:
34131
return QCoreApplication.translate(self._name, trstr)
132+
35133
def create_progress_window(
36134
self, title: str, max_progress: int, msg: str = "", cancelable: bool = True
37135
) -> QProgressDialog:
@@ -46,6 +144,149 @@ def create_progress_window(
46144
progress.setWindowModality(Qt.WindowModality.ApplicationModal)
47145
progress.show()
48146
return progress
147+
49148
def on_user_interface_initialized(self, window: QMainWindow) -> None:
50149
self.main_window = window
51150
pass
151+
152+
def on_settings_changed(
153+
self,
154+
plugin_name: str,
155+
setting: str,
156+
old: mobase.MoVariant,
157+
new: mobase.MoVariant,
158+
) -> None:
159+
if self._name != plugin_name:
160+
return
161+
if new and setting == "check_for_lslib_updates":
162+
try:
163+
self._lslib_retriever.download_lslib_if_missing()
164+
finally:
165+
self._set_setting(setting, False)
166+
elif new and setting == "force_reparse_metadata":
167+
try:
168+
self.construct_modsettings_xml(
169+
exec_path="bin/bg3", force_reparse_metadata=True
170+
)
171+
finally:
172+
self._set_setting(setting, False)
173+
elif new and setting == "convert_jsons_to_yaml":
174+
try:
175+
self._convert_jsons_to_yaml()
176+
finally:
177+
self._set_setting(setting, False)
178+
elif setting in {
179+
"extract_full_package",
180+
"autobuild_paks",
181+
"remove_extracted_metadata",
182+
"force_load_dlls",
183+
"log_diff",
184+
"convert_yamls_to_json",
185+
} and hasattr(self, setting):
186+
delattr(self, setting)
187+
188+
def construct_modsettings_xml(
189+
self,
190+
exec_path: str = "",
191+
working_dir: typing.Optional[QDir] = None,
192+
args: str = "",
193+
force_reparse_metadata: bool = False,
194+
) -> bool:
195+
if (
196+
"bin/bg3" not in exec_path
197+
or not self._lslib_retriever.download_lslib_if_missing()
198+
):
199+
return True
200+
active_mods = self.active_mods()
201+
progress = self.create_progress_window(
202+
"Generating modsettings.xml", len(active_mods)
203+
)
204+
threadpool = QThreadPool.globalInstance()
205+
if threadpool is None:
206+
return False
207+
metadata: dict[str, str] = {}
208+
209+
def retrieve_mod_metadata_in_new_thread(mod: mobase.IModInterface):
210+
return lambda: metadata.update(
211+
self._pak_parser.get_metadata_for_files_in_mod(
212+
mod, force_reparse_metadata
213+
)
214+
)
215+
216+
for mod in active_mods:
217+
if progress.wasCanceled():
218+
qWarning("processing canceled by user")
219+
return False
220+
threadpool.start(QRunnable.create(retrieve_mod_metadata_in_new_thread(mod)))
221+
count = 0
222+
num_active_mods = len(active_mods)
223+
total_intervals_to_wait = (num_active_mods * 2) + 20
224+
while len(metadata.keys()) < num_active_mods:
225+
progress.setValue(len(metadata.keys()))
226+
QApplication.processEvents(QEventLoop.ProcessEventsFlag.AllEvents, 100)
227+
count += 1
228+
if count == total_intervals_to_wait or progress.wasCanceled():
229+
remaining_mods = {mod.name() for mod in active_mods} - metadata.keys()
230+
qWarning(f"processing did not finish in time for: {remaining_mods}")
231+
progress.close()
232+
break
233+
QThread.msleep(100)
234+
progress.setValue(num_active_mods)
235+
QApplication.processEvents(QEventLoop.ProcessEventsFlag.AllEvents, 100)
236+
progress.close()
237+
qInfo(f"writing mod load order to {self.modsettings_path}")
238+
self.modsettings_path.parent.mkdir(parents=True, exist_ok=True)
239+
self.modsettings_path.write_text(
240+
(
241+
self._mod_settings_xml_start
242+
+ "".join(
243+
metadata[mod.name()]
244+
for mod in active_mods
245+
if mod.name() in metadata
246+
)
247+
+ self._mod_settings_xml_end
248+
)
249+
)
250+
qInfo(
251+
f"backing up generated file {self.modsettings_path} to {self.modsettings_backup}, "
252+
f"check the backup after the executable runs for differences with the file used by the game if you encounter issues"
253+
)
254+
shutil.copy(self.modsettings_path, self.modsettings_backup)
255+
return True
256+
257+
def _convert_jsons_to_yaml(self):
258+
qInfo("converting all json files to yaml")
259+
active_mods = self.active_mods()
260+
progress = self.create_progress_window(
261+
"Converting all json files to yaml", len(active_mods) + 1
262+
)
263+
for mod in active_mods:
264+
_convert_jsons_in_dir_to_yaml(Path(mod.absolutePath()))
265+
progress.setValue(progress.value() + 1)
266+
QApplication.processEvents()
267+
if progress.wasCanceled():
268+
qWarning("conversion canceled by user")
269+
return
270+
_convert_jsons_in_dir_to_yaml(self.overwrite_path)
271+
progress.setValue(len(active_mods) + 1)
272+
QApplication.processEvents()
273+
progress.close()
274+
275+
def on_mod_installed(self, mod: mobase.IModInterface) -> None:
276+
if self._lslib_retriever.download_lslib_if_missing():
277+
self._pak_parser.get_metadata_for_files_in_mod(mod, True)
278+
279+
280+
def _convert_jsons_in_dir_to_yaml(path: Path):
281+
for file in list(path.rglob("*.json")):
282+
converted_path = file.parent / file.name.replace(".json", ".yaml")
283+
try:
284+
if not converted_path.exists() or os.path.getmtime(file) > os.path.getmtime(
285+
converted_path
286+
):
287+
with open(file, "r") as json_file:
288+
with open(converted_path, "w") as yaml_file:
289+
yaml.dump(json.load(json_file), yaml_file, indent=2)
290+
qInfo(f"Converted {file} to YAML")
291+
except OSError as e:
292+
qWarning(f"Error accessing file {converted_path}: {e}")

games/baldursgate3/lslib_retriever.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
class LSLibRetriever:
1515
def __init__(self, utils: bg3_utils.BG3Utils):
1616
self._utils = utils
17+
1718
@cached_property
1819
def _needed_lslib_files(self):
1920
return {
@@ -31,6 +32,7 @@ def _needed_lslib_files(self):
3132
"ZstdSharp.dll",
3233
}
3334
}
35+
3436
def download_lslib_if_missing(self):
3537
if not self._utils.get_setting("check_for_lslib_updates") and all(
3638
x.exists() for x in self._needed_lslib_files
@@ -67,7 +69,8 @@ def reporthook(block_num: int, block_size: int, total_size: int) -> None:
6769
)
6870
)
6971
msg_box.addButton(
70-
self._utils.tr("Download"), QMessageBox.ButtonRole.DestructiveRole
72+
self._utils.tr("Download"),
73+
QMessageBox.ButtonRole.DestructiveRole,
7174
)
7275
exit_btn = msg_box.addButton(
7376
self._utils.tr("Exit"), QMessageBox.ButtonRole.ActionRole

0 commit comments

Comments
 (0)