Skip to content

Commit 7cd0e79

Browse files
committed
add extract_full_pak setting
1 parent 18f04d3 commit 7cd0e79

1 file changed

Lines changed: 101 additions & 58 deletions

File tree

games/game_baldursgate3.py

Lines changed: 101 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from configparser import SectionProxy
1515
from functools import cached_property
1616
from pathlib import Path
17-
from typing import Any, Callable, Optional
17+
from typing import Any, Callable
1818
from xml.etree import ElementTree
1919
from xml.etree.ElementTree import Element
2020

@@ -28,6 +28,7 @@
2828
qDebug,
2929
qInfo,
3030
qWarning,
31+
QDir,
3132
)
3233
from PyQt6.QtWidgets import (
3334
QApplication,
@@ -70,24 +71,6 @@ def __init__(self):
7071
)
7172

7273

73-
class Worker(QRunnable):
74-
def __init__(
75-
self,
76-
fn: Callable[[mobase.IModInterface, bool], str],
77-
mod: mobase.IModInterface,
78-
auto_build_paks: bool,
79-
metadata: dict[str, str],
80-
):
81-
super().__init__()
82-
self.fn = fn
83-
self.mod = mod
84-
self.autoBuildPaks = auto_build_paks
85-
self.metadata = metadata
86-
87-
def run(self):
88-
self.metadata.update({self.mod.name(): self.fn(self.mod, self.autoBuildPaks)})
89-
90-
9174
class BG3Game(BasicGame, mobase.IPluginFileMapper):
9275
Name = "Baldur's Gate 3 Plugin"
9376
Author = "daescha"
@@ -196,6 +179,11 @@ def settings(self):
196179
"Check to see if there has been a new release of LSLib and create download dialog if so.",
197180
False,
198181
),
182+
mobase.PluginSetting(
183+
"extract_full_package",
184+
"Extract the full pak when parsing metadata, instead of just meta.lsx.",
185+
False,
186+
),
199187
]
200188
for setting in custom_settings:
201189
setting.description = self.__tr(setting.description)
@@ -223,7 +211,7 @@ def executableForcedLoads(self) -> list[mobase.ExecutableForcedLoadSetting]:
223211
efls = super().executableForcedLoads()
224212
except AttributeError:
225213
efls = []
226-
if self._get_setting("force_load_dlls"):
214+
if self._force_load_dlls:
227215
qInfo("detecting dlls in enabled mods")
228216
libs: set[str] = set()
229217
tree: mobase.IFileTree | mobase.FileTreeEntry | None = (
@@ -260,6 +248,7 @@ def mappings(self) -> list[mobase.Mapping]:
260248
qInfo("creating custom bg3 mappings")
261249
mappings: list[mobase.Mapping] = []
262250
docs_path = Path(self.documentsDirectory().path())
251+
active_mods = self._active_mods()
263252

264253
def map_files(
265254
path: Path,
@@ -282,16 +271,16 @@ def map_files(
282271
)
283272

284273
progress = self._create_progress_window(
285-
"Mapping files to documents folder", len(self._active_mods()) + 1
274+
"Mapping files to documents folder", len(active_mods) + 1
286275
)
287-
for mod in self._active_mods():
276+
for mod in active_mods:
288277
modpath = Path(mod.absolutePath())
289278
map_files(modpath, pattern="*.pak", rel=False)
290279
map_files(modpath / "Script Extender")
291280
progress.setValue(progress.value() + 1)
292281
QApplication.processEvents()
293282
map_files(self._overwrite_path)
294-
progress.setValue(len(self._active_mods()) + 1)
283+
progress.setValue(len(active_mods) + 1)
295284
QApplication.processEvents()
296285
return mappings
297286

@@ -333,6 +322,26 @@ def _folder_pattern(self):
333322
def _tools_dir(self):
334323
return self._plugin_data_path / "tools"
335324

325+
@cached_property
326+
def _autobuild_paks(self):
327+
return bool(self._get_setting("autobuild_paks"))
328+
329+
@cached_property
330+
def _extract_full_package(self):
331+
return bool(self._get_setting("extract_full_package"))
332+
333+
@cached_property
334+
def _remove_extracted_metadata(self):
335+
return bool(self._get_setting("remove_extracted_metadata"))
336+
337+
@cached_property
338+
def _force_load_dlls(self):
339+
return bool(self._get_setting("force_load_dlls"))
340+
341+
@cached_property
342+
def _log_diff(self):
343+
return bool(self._get_setting("log_diff"))
344+
336345
@cached_property
337346
def _needed_lslib_files(self):
338347
return {
@@ -385,32 +394,48 @@ def _create_progress_window(
385394
progress.show()
386395
return progress
387396

397+
def _refresh_attr(self, setting: str):
398+
if hasattr(self, setting):
399+
delattr(self, setting)
400+
388401
def _on_settings_changed(
389402
self,
390403
plugin_name: str,
391404
setting: str,
392405
old: mobase.MoVariant,
393406
new: mobase.MoVariant,
394407
) -> None:
395-
if self.name() != plugin_name or not new:
408+
if self.name() != plugin_name:
396409
return
397-
if setting == "check_for_lslib_updates":
410+
if new and setting == "check_for_lslib_updates":
398411
try:
399412
self._download_lslib_if_missing()
400413
finally:
401414
self._set_setting(setting, False)
402-
elif setting == "force_reparse_metadata":
415+
elif new and setting == "force_reparse_metadata":
403416
try:
404-
self._construct_modsettings_xml()
417+
self._construct_modsettings_xml(
418+
exec_path="bin/bg3", force_reparse_metadata=True
419+
)
405420
finally:
406421
self._set_setting(setting, False)
422+
elif setting in {
423+
"extract_full_package",
424+
"autobuild_paks",
425+
"remove_extracted_metadata",
426+
"force_load_dlls",
427+
"log_diff",
428+
}:
429+
self._refresh_attr(f"_{setting}")
407430

408431
def _on_user_interface_initialized(self, window: QMainWindow) -> None:
409432
self._main_window = window
410433
pass
411434

412-
def _on_finished_run(self, x: str, y: int):
413-
if self._get_setting("log_diff"):
435+
def _on_finished_run(self, exec_path: str, exit_code: int):
436+
if "bin/bg3" not in exec_path:
437+
return
438+
if self._log_diff:
414439
for x in difflib.unified_diff(
415440
open(self._modsettings_backup).readlines(),
416441
open(self._modsettings_path).readlines(),
@@ -443,24 +468,31 @@ def _on_finished_run(self, x: str, y: int):
443468
except OSError:
444469
pass
445470

446-
def _construct_modsettings_xml(self, _: str = "") -> bool:
471+
def _construct_modsettings_xml(
472+
self,
473+
exec_path: str = "",
474+
working_dir: QDir = QDir.current(),
475+
args: str = "",
476+
force_reparse_metadata: bool = False,
477+
) -> bool:
478+
if "bin/bg3" not in exec_path:
479+
return True
447480
if not self._download_lslib_if_missing():
448481
return True
449482
active_mods = self._active_mods()
450-
autobuild_paks = self._get_setting("autobuild_paks")
451483
progress = self._create_progress_window(
452484
"Generating modsettings.xml", len(active_mods)
453485
)
454486
threadpool = QThreadPool.globalInstance()
455-
if threadpool is None or type(autobuild_paks) is not bool:
487+
if threadpool is None:
456488
return False
457489
metadata: dict[str, str] = {}
458490

459491
def get_runnable(mod: mobase.IModInterface):
460492
threadpool.start(
461493
QRunnable.create(
462494
lambda: metadata.update(
463-
self._get_metadata_for_files_in_mod(mod, autobuild_paks)
495+
self._get_metadata_for_files_in_mod(mod, force_reparse_metadata)
464496
)
465497
)
466498
)
@@ -477,9 +509,11 @@ def get_runnable(mod: mobase.IModInterface):
477509
if count == total_intervals_to_wait:
478510
remaining_mods = {mod.name() for mod in active_mods} - metadata.keys()
479511
qWarning(f"processing did not finish in time for: {remaining_mods}")
480-
progress.setValue(num_active_mods)
512+
progress.cancel()
481513
break
482514
QtCore.QThread.msleep(100)
515+
progress.setValue(num_active_mods)
516+
QApplication.processEvents(QEventLoop.ProcessEventsFlag.AllEvents, 100)
483517
qInfo(f"writing mod load order to {self._modsettings_path}")
484518
self._modsettings_path.write_text(
485519
(
@@ -501,17 +535,15 @@ def get_runnable(mod: mobase.IModInterface):
501535

502536
def _on_mod_installed(self, mod: mobase.IModInterface) -> None:
503537
if self._download_lslib_if_missing():
504-
self._get_metadata_for_files_in_mod(
505-
mod, bool(self._get_setting("autobuild_paks"))
506-
)
538+
self._get_metadata_for_files_in_mod(mod, True)
507539

508540
def _get_metadata_for_files_in_mod(
509-
self, mod: mobase.IModInterface, auto_build_paks: bool
541+
self, mod: mobase.IModInterface, force_reparse_metadata: bool
510542
):
511543
return {
512544
mod.name(): "".join(
513545
[
514-
self._get_metadata_for_file(mod, file)
546+
self._get_metadata_for_file(mod, file, force_reparse_metadata)
515547
for file in sorted(
516548
list(Path(mod.absolutePath()).rglob("*.pak"))
517549
+ (
@@ -520,7 +552,7 @@ def _get_metadata_for_files_in_mod(
520552
for f in Path(mod.absolutePath()).glob("*")
521553
if f.is_dir()
522554
]
523-
if auto_build_paks
555+
if self._autobuild_paks
524556
else []
525557
)
526558
)
@@ -532,13 +564,12 @@ def _get_metadata_for_file(
532564
self,
533565
mod: mobase.IModInterface,
534566
file: Path,
535-
force_recreate: Optional[bool] = None,
536-
rm_extracted: Optional[bool] = None,
567+
force_reparse_metadata: bool,
537568
) -> str:
538569
def run_divine(
539-
action: str, source: Path | str, extra_args: str = ""
570+
action: str, source: Path | str
540571
) -> subprocess.CompletedProcess[str]:
541-
command = f'{self._divine_command} -a "{action}" -s "{source}" {extra_args}'
572+
command = f'{self._divine_command} -a {action} -s "{source}"'
542573
result = subprocess.run(
543574
command,
544575
creationflags=subprocess.CREATE_NO_WINDOW,
@@ -577,13 +608,30 @@ def get_attr_value(root: Element, attr_id: str) -> str:
577608
return default_val if attr is None else attr.get("value", default_val)
578609

579610
def extract_data(output_file: Path) -> bool:
611+
out_dir = str(output_file)[:-4] if self._extract_full_package else ""
580612
if run_divine(
581-
"extract-single-file",
613+
f'{"extract-package" if self._extract_full_package else "extract-single-file -f meta.lsx"} -d "{output_file if not self._extract_full_package else out_dir}"',
582614
file,
583-
extra_args=f'-d "{output_file}" -f meta.lsx',
584615
).returncode:
585616
return False
586-
if not output_file.exists():
617+
if self._extract_full_package:
618+
qDebug(f"archive {file} extracted to {out_dir}")
619+
if run_divine(
620+
f'convert-resources -d "{out_dir}" -i lsf -o lsx -x "*.lsf"',
621+
out_dir,
622+
).returncode:
623+
qDebug(f"failed to convert lsf files in {out_dir} to readable lsx")
624+
extracted_meta_files = list(Path(out_dir).rglob("meta.lsx"))
625+
if len(extracted_meta_files) == 0:
626+
qInfo(
627+
f"No meta.lsx files found in {file.name}, {file.name} determined to be an override mod"
628+
)
629+
return False
630+
shutil.copyfile(
631+
extracted_meta_files[0],
632+
output_file,
633+
)
634+
elif not output_file.exists():
587635
qInfo(
588636
f"No meta.lsx files found in {file.name}, {file.name} determined to be an override mod"
589637
)
@@ -610,9 +658,8 @@ def parse_meta_lsx(meta_file: Path, section: SectionProxy):
610658
# 2. it has files in Mods/<folder_name>/ other than the meta.lsx file, or
611659
# 3. it has files in Public/<folder_name>
612660
result = run_divine(
613-
"list-package",
661+
f'list-package --use-regex -x "(/{folder_name}/(?!meta\\.lsx))|(Public/Engine/Timeline/MaterialGroups)"',
614662
file,
615-
extra_args=f'--use-regex -x "(/{folder_name}/(?!meta\\.lsx))|(Public/Engine/Timeline/MaterialGroups)"',
616663
)
617664
self._mod_cache[file] = (
618665
result.returncode == 0 and result.stdout.strip() != ""
@@ -635,10 +682,6 @@ def metadata_to_ini(condition: bool, to_parse: Callable[[], Path]):
635682
config.write(f)
636683
return get_module_short_desc()
637684

638-
if force_recreate is None:
639-
force_recreate = bool(self._get_setting("force_reparse_metadata"))
640-
if rm_extracted is None:
641-
rm_extracted = bool(self._get_setting("remove_extracted_metadata"))
642685
meta_ini = Path(mod.absolutePath()) / "meta.ini"
643686
config = configparser.ConfigParser()
644687
config.read(meta_ini, encoding="utf-8")
@@ -650,7 +693,7 @@ def metadata_to_ini(condition: bool, to_parse: Callable[[], Path]):
650693
)
651694
try:
652695
if (
653-
not force_recreate
696+
not force_reparse_metadata
654697
and config.has_section(file.name)
655698
and (
656699
"override" in config[file.name].keys()
@@ -662,8 +705,10 @@ def metadata_to_ini(condition: bool, to_parse: Callable[[], Path]):
662705
meta_file.unlink(missing_ok=True)
663706
return metadata_to_ini(extract_data(meta_file), lambda: meta_file)
664707
finally:
665-
if rm_extracted:
708+
if self._remove_extracted_metadata:
666709
meta_file.unlink(missing_ok=True)
710+
if self._extract_full_package:
711+
Path(str(meta_file)[:-4]).unlink(missing_ok=True)
667712
elif file.is_dir() and self._folder_pattern.search(file.name):
668713
# qDebug(f"directory is not packable: {file}")
669714
return ""
@@ -676,9 +721,7 @@ def metadata_to_ini(condition: bool, to_parse: Callable[[], Path]):
676721
qInfo(f"packable dir: {file}")
677722
pak_path = self._overwrite_path / f"Mods/{file.name}.pak"
678723
pak_path.unlink(missing_ok=True)
679-
if run_divine(
680-
"create-package", file, extra_args=f'-d "{pak_path}"'
681-
).returncode:
724+
if run_divine(f'create-package -d "{pak_path}"', file).returncode:
682725
return ""
683726
meta_files = list(file.glob("Mods/*/meta.lsx"))
684727
return metadata_to_ini(len(meta_files) > 0, lambda: meta_files[0])

0 commit comments

Comments
 (0)