aboutsummaryrefslogtreecommitdiff
path: root/libs/basic_games/games/baldursgate3/lslib_retriever.py
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/basic_games/games/baldursgate3/lslib_retriever.py
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/basic_games/games/baldursgate3/lslib_retriever.py')
-rw-r--r--libs/basic_games/games/baldursgate3/lslib_retriever.py162
1 files changed, 162 insertions, 0 deletions
diff --git a/libs/basic_games/games/baldursgate3/lslib_retriever.py b/libs/basic_games/games/baldursgate3/lslib_retriever.py
new file mode 100644
index 0000000..ee1122c
--- /dev/null
+++ b/libs/basic_games/games/baldursgate3/lslib_retriever.py
@@ -0,0 +1,162 @@
+import json
+import shutil
+import traceback
+import urllib.request
+import zipfile
+from functools import cached_property
+
+from PyQt6.QtCore import qDebug, qWarning
+from PyQt6.QtWidgets import QApplication, QMessageBox
+
+from . import bg3_utils
+
+
+class LSLibRetriever:
+ def __init__(self, utils: bg3_utils.BG3Utils):
+ self._utils = utils
+
+ @cached_property
+ def _needed_lslib_files(self):
+ return {
+ self._utils.tools_dir / x
+ for x in {
+ "CommandLineArgumentsParser.dll",
+ "Divine.dll",
+ "Divine.dll.config",
+ "Divine.exe",
+ "Divine.runtimeconfig.json",
+ "K4os.Compression.LZ4.dll",
+ "K4os.Compression.LZ4.Streams.dll",
+ "LSLib.dll",
+ "LSLibNative.dll",
+ "LZ4.dll",
+ "Newtonsoft.Json.dll",
+ "System.IO.Hashing.dll",
+ "ZstdSharp.dll",
+ }
+ }
+
+ def download_lslib_if_missing(self, force: bool = False) -> bool:
+ if not force and all(x.exists() for x in self._needed_lslib_files):
+ return True
+ try:
+ self._utils.tools_dir.mkdir(exist_ok=True, parents=True)
+ downloaded = False
+
+ def reporthook(block_num: int, block_size: int, total_size: int) -> None:
+ if total_size > 0:
+ progress.setValue(
+ min(int(block_num * block_size * 100 / total_size), 100)
+ )
+ QApplication.processEvents()
+
+ with urllib.request.urlopen(
+ "https://api.github.com/repos/Norbyte/lslib/releases/latest"
+ ) as response:
+ assets = json.loads(response.read().decode("utf-8"))["assets"][0]
+ zip_path = self._utils.tools_dir / assets["name"]
+ if not zip_path.exists():
+ old_archives = list(self._utils.tools_dir.glob("*.zip"))
+ msg_box = QMessageBox(self._utils.main_window)
+ msg_box.setWindowTitle(
+ self._utils.tr("Baldur's Gate 3 Plugin - Missing dependencies")
+ )
+ if old_archives:
+ msg_box.setText(self._utils.tr("LSLib update available."))
+ else:
+ msg_box.setText(
+ self._utils.tr(
+ "LSLib tools are missing.\nThese are necessary for the plugin to create the load order file for BG3."
+ )
+ )
+ msg_box.addButton(
+ self._utils.tr("Download"),
+ QMessageBox.ButtonRole.DestructiveRole,
+ )
+ exit_btn = msg_box.addButton(
+ self._utils.tr("Exit"), QMessageBox.ButtonRole.ActionRole
+ )
+ msg_box.setIcon(QMessageBox.Icon.Warning)
+ msg_box.exec()
+
+ if msg_box.clickedButton() == exit_btn:
+ if not old_archives:
+ err = QMessageBox(self._utils.main_window)
+ err.setIcon(QMessageBox.Icon.Critical)
+ 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(
+ "Downloading LSLib", 100, cancelable=False
+ )
+ urllib.request.urlretrieve(
+ assets["browser_download_url"], str(zip_path), reporthook
+ )
+ progress.close()
+ downloaded = True
+ for archive in old_archives:
+ archive.unlink()
+ old_archives = []
+ else:
+ old_archives = []
+ new_msg = QMessageBox(self._utils.main_window)
+ new_msg.setIcon(QMessageBox.Icon.Information)
+ new_msg.setText(
+ self._utils.tr("Latest version of LSLib already downloaded!")
+ )
+ new_msg.exec()
+
+ except Exception as e:
+ qDebug(f"Download failed: {e}")
+ err = QMessageBox(self._utils.main_window)
+ err.setIcon(QMessageBox.Icon.Critical)
+ err.setText(
+ self._utils.tr(
+ f"Failed to download LSLib tools:\n{traceback.format_exc()}"
+ )
+ )
+ err.exec()
+ return False
+ try:
+ if old_archives:
+ zip_path = sorted(old_archives)[-1]
+ if old_archives or not downloaded:
+ dialog_message = "Ensuring all necessary LSLib files have been extracted from archive..."
+ win_title = "Verifying LSLib files"
+ else:
+ dialog_message = "Extracting/Updating LSLib files..."
+ win_title = "Extracting LSLib"
+ x_progress = self._utils.create_progress_window(
+ win_title, len(self._needed_lslib_files), msg=dialog_message
+ )
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
+ for file in self._needed_lslib_files:
+ if downloaded or not file.exists():
+ shutil.move(
+ zip_ref.extract(
+ f"Packed/Tools/{file.name}", self._utils.tools_dir
+ ),
+ file,
+ )
+ x_progress.setValue(x_progress.value() + 1)
+ QApplication.processEvents()
+ if x_progress.wasCanceled():
+ qWarning("processing canceled by user")
+ return False
+ x_progress.close()
+ shutil.rmtree(self._utils.tools_dir / "Packed", ignore_errors=True)
+ except Exception as e:
+ qDebug(f"Extraction failed: {e}")
+ err = QMessageBox(self._utils.main_window)
+ err.setIcon(QMessageBox.Icon.Critical)
+ err.setText(
+ self._utils.tr(
+ f"Failed to extract LSLib tools:\n{traceback.format_exc()}"
+ )
+ )
+ err.exec()
+ return False
+ return True