1414from configparser import SectionProxy
1515from functools import cached_property
1616from pathlib import Path
17- from typing import Any , Callable , Optional
17+ from typing import Any , Callable
1818from xml .etree import ElementTree
1919from xml .etree .ElementTree import Element
2020
2828 qDebug ,
2929 qInfo ,
3030 qWarning ,
31+ QDir ,
3132)
3233from 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-
9174class 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