aboutsummaryrefslogtreecommitdiff
path: root/libs/basic_games/games/baldursgate3/bg3_file_mapper.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/bg3_file_mapper.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/bg3_file_mapper.py')
-rw-r--r--libs/basic_games/games/baldursgate3/bg3_file_mapper.py131
1 files changed, 131 insertions, 0 deletions
diff --git a/libs/basic_games/games/baldursgate3/bg3_file_mapper.py b/libs/basic_games/games/baldursgate3/bg3_file_mapper.py
new file mode 100644
index 0000000..0ae6ef8
--- /dev/null
+++ b/libs/basic_games/games/baldursgate3/bg3_file_mapper.py
@@ -0,0 +1,131 @@
+import functools
+import os
+from pathlib import Path
+from typing import Callable, Optional
+
+from PyQt6.QtCore import QDir, QLoggingCategory, qDebug, qInfo, qWarning
+from PyQt6.QtWidgets import QApplication
+
+import mobase
+
+from . import bg3_utils
+
+
+class BG3FileMapper(mobase.IPluginFileMapper):
+ current_mappings: list[mobase.Mapping] = []
+
+ def __init__(self, utils: bg3_utils.BG3Utils, doc_dir: Callable[[], QDir]):
+ super().__init__()
+ self._utils = utils
+ self.doc_dir = doc_dir
+
+ @functools.cached_property
+ def doc_path(self):
+ return Path(self.doc_dir().path())
+
+ def mappings(self) -> list[mobase.Mapping]:
+ qInfo("creating custom bg3 mappings")
+ self.current_mappings.clear()
+ active_mods = self._utils.active_mods()
+ if not active_mods:
+ return []
+ progress = self._utils.create_progress_window(
+ "Mapping files to documents folder", len(active_mods) + 1
+ )
+ 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,
+ 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(
+ self,
+ path: Path,
+ 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] = (
+ (lambda f: os.path.relpath(f, path)) if rel else lambda f: f.name
+ )
+ found_jsons: set[Path] = set()
+ for file in list(path.rglob(pattern)):
+ if self._utils.convert_yamls_to_json and (
+ file.name.endswith(".yaml") or file.name.endswith(".yml")
+ ):
+ converted_path = file.parent / file.name.replace(
+ ".yaml", ".json"
+ ).replace(".yml", ".json")
+ try:
+ 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(
+ yaml.safe_load(yaml_file), json_file, indent=2
+ )
+ qDebug(f"Converted {file} to JSON")
+ found_jsons.add(converted_path)
+ except OSError as e:
+ qWarning(f"Error accessing file {converted_path}: {e}")
+ elif file.name.endswith(".json"):
+ found_jsons.add(file)
+ 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),
+ destination=str(dest),
+ is_directory=file.is_dir(),
+ create_target=True,
+ )
+ )