Skip to content
26 changes: 14 additions & 12 deletions games/baldursgate3/bg3_data_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,30 @@ def __init__(self):
def dataLooksValid(
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
status = mobase.ModDataChecker.INVALID
invalid, valid, fixable = (
mobase.ModDataChecker.INVALID,
mobase.ModDataChecker.VALID,
mobase.ModDataChecker.FIXABLE,
)
rank = (invalid, valid, fixable).index
status = invalid
rp = self._regex_patterns
for entry in filetree:
name = entry.name().casefold()
if rp.unfold.match(name):
if utils.is_directory(entry):
status = self.dataLooksValid(entry)
status = max(status, self.dataLooksValid(entry), key=rank)
else:
status = mobase.ModDataChecker.INVALID
status = invalid
break
elif rp.valid.match(name):
if status is mobase.ModDataChecker.INVALID:
status = mobase.ModDataChecker.VALID
status = max(status, valid, key=rank)
elif isinstance(entry, mobase.IFileTree):
status = (
mobase.ModDataChecker.VALID
if all(rp.valid.match(e.pathFrom(filetree)) for e in entry)
else mobase.ModDataChecker.INVALID
)
if all(rp.valid.match(e.pathFrom(filetree)) for e in entry):
status = max(status, valid, key=rank)
elif rp.delete.match(name) or rp.move_match(name) is not None:
status = mobase.ModDataChecker.FIXABLE
status = fixable
else:
status = mobase.ModDataChecker.INVALID
status = invalid
break
return status
71 changes: 34 additions & 37 deletions games/baldursgate3/bg3_file_mapper.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import functools
import json
import os
from pathlib import Path
from typing import Callable, Optional
Expand All @@ -12,8 +13,6 @@


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
Expand All @@ -25,55 +24,58 @@ def doc_path(self):

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
)
mappings: list[mobase.Mapping] = []
self.create_mapping(
mappings,
self._utils.modsettings_path,
self.doc_path
/ "PlayerProfiles"
/ "Public"
/ self._utils.modsettings_path.name,
)
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)
self.map_files(
mappings, modpath, dest=docs_path_mods, pattern="*.pak", rel=False
)
self.map_files(mappings, modpath / "Script Extender", dest=docs_path_se)
if self._utils.convert_yamls_to_json:
self.map_files(modpath / "bin", only_convert=True)
self.map_files(mappings, modpath / "bin", only_convert=True)
progress.setValue(progress.value() + 1)
QApplication.processEvents()
if progress.wasCanceled():
qWarning("mapping canceled by user")
return self.current_mappings
return 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,
)
self.map_files(mappings, self._utils.overwrite_path)
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} }"
f"resolved mappings: { {m.source: m.destination for m in mappings} }"
)
return self.current_mappings
return mappings

def map_files(
self,
mappings: list[mobase.Mapping],
path: Path,
dest: Optional[Path] = None,
pattern: str = "*",
Expand All @@ -89,39 +91,34 @@ def map_files(
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")
import yaml

converted_path = file.with_suffix(".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
)
data = yaml.safe_load(file.read_text(encoding="utf-8"))
converted_path.write_text(
json.dumps(data, indent=2), encoding="utf-8"
)
qDebug(f"Converted {file} to JSON")
found_jsons.add(converted_path)
except OSError as e:
qWarning(f"Error accessing file {converted_path}: {e}")
except (OSError, ValueError, yaml.YAMLError) as e:
qWarning(f"Skipping {file}, conversion to JSON failed: {e}")
elif file.name.endswith(".json"):
found_jsons.add(file)
elif not only_convert:
self.create_mapping(file, dest / dest_func(file))
self.create_mapping(mappings, file, dest / dest_func(file))
if only_convert:
return
for file in found_jsons:
self.create_mapping(file, dest / dest_func(file))
self.create_mapping(mappings, file, dest / dest_func(file))

def create_mapping(self, file: Path, dest: Path):
bg3_utils.create_dir_if_needed(dest)
def create_mapping(self, mappings: list[mobase.Mapping], file: Path, dest: Path):
bg3_utils.create_dir_if_needed(dest, is_file=not file.is_dir())

self.current_mappings.append(
mappings.append(
mobase.Mapping(
source=str(file),
destination=str(dest),
Expand Down
33 changes: 25 additions & 8 deletions games/baldursgate3/bg3_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import typing
from pathlib import Path
from time import sleep
from xml.sax.saxutils import escape

from PyQt6.QtCore import (
QCoreApplication,
Expand Down Expand Up @@ -36,6 +37,10 @@ def get_node_string(
uuid: str = "",
version64: str = "0",
) -> str:
folder, md5, name, publish_handle, uuid, version64 = (
escape(v, {'"': """})
for v in (folder, md5, name, publish_handle, uuid, version64)
)
return f"""
<node id="ModuleShortDesc">
<attribute id="Folder" type="LSString" value="{folder}"/>
Expand Down Expand Up @@ -111,12 +116,14 @@ def log_dir(self):

@functools.cached_property
def modsettings_backup(self):
return create_dir_if_needed(self.plugin_data_path / "temp" / "modsettings.lsx")
return create_dir_if_needed(
self.plugin_data_path / "temp" / "modsettings.lsx", is_file=True
)

@functools.cached_property
@property
def modsettings_path(self):
return create_dir_if_needed(
Path(self._organizer.profilePath()) / "modsettings.lsx"
Path(self._organizer.profilePath()) / "modsettings.lsx", is_file=True
)

@functools.cached_property
Expand Down Expand Up @@ -267,9 +274,19 @@ def on_mod_installed(self, mod: mobase.IModInterface) -> None:
self._pak_parser.get_metadata_for_files_in_mod(mod, True)


def create_dir_if_needed(path: Path) -> Path:
if "." not in path.name[1:]:
path.mkdir(parents=True, exist_ok=True)
else:
path.parent.mkdir(parents=True, exist_ok=True)
def create_dir_if_needed(path: Path, is_file: bool = False) -> Path:
(path.parent if is_file else path).mkdir(parents=True, exist_ok=True)
return path


def remove_empty_dirs(root: Path) -> set[Path]:
removed: set[Path] = set()
for folder, _, _ in root.walk(top_down=False):
if folder == root:
continue
try:
folder.rmdir()
removed.add(folder)
except OSError:
pass
return removed
Loading
Loading