Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
import glob
import importlib
import os
import pathlib
import site
import sys
import typing

from PyQt6.QtCore import qWarning

from mobase import IPlugin

from .basic_game import BasicGame
from .basic_game_ini import BasicIniGame

Expand All @@ -18,7 +23,7 @@

def createPlugins():
# List of game class from python:
game_plugins: typing.List[BasicGame] = []
game_plugins: typing.List[IPlugin] = []

# We are going to list all game plugins:
curpath = os.path.abspath(os.path.dirname(__file__))
Expand Down Expand Up @@ -58,5 +63,25 @@ def createPlugins():
"Failed to instantiate {}: {}".format(name, e),
file=sys.stderr,
)
for path in pathlib.Path(escaped_games_path).rglob("plugins/__init__.py"):
module_path = "." + os.path.relpath(path.parent, curpath).replace(os.sep, ".")
try:
module = importlib.import_module(module_path, __package__)
if hasattr(module, "createPlugins") and callable(module.createPlugins):
try:
plugins: typing.Any = module.createPlugins()
for item in plugins:
if isinstance(item, IPlugin):
game_plugins.append(item)
except TypeError:
pass
if hasattr(module, "createPlugin") and callable(module.createPlugin):
plugin = module.createPlugin()
if isinstance(plugin, IPlugin):
game_plugins.append(plugin)
except ImportError as e:
qWarning(f"Error importing module {module_path}: {e}")
except Exception as e:
qWarning(f"Error calling function createPlugin(s) in {module_path}: {e}")

return game_plugins
2 changes: 1 addition & 1 deletion basic_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ def setGamePath(self, path: Path | str) -> None:
self._mappings.steamAPPId.set_value(steamid)
for gogid, gogpath in BasicGame.gog_games.items():
if gogpath == path:
self._mappings.steamAPPId.set_value(gogid)
self._mappings.gogAPPId.set_value(gogid)
for originid, originpath in BasicGame.origin_games.items():
if originpath == path:
self._mappings.originManifestIds.set_value(originid)
Expand Down
Empty file added games/baldursgate3/__init__.py
Empty file.
58 changes: 58 additions & 0 deletions games/baldursgate3/bg3_data_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from pathlib import Path

import mobase

from ...basic_features import BasicModDataChecker, GlobPatterns, utils
from . import bg3_utils


class BG3ModDataChecker(BasicModDataChecker):
def __init__(self):
super().__init__(
GlobPatterns(
valid=[
"*.pak",
str(Path("Mods") / "*.pak"), # standard mods
"bin", # native mods / Script Extender
"Script Extender", # mods which are configured via jsons in this folder
"Data", # loose file mods
]
+ [str(Path("*") / f) for f in bg3_utils.loose_file_folders],
move={
"Root/": "", # root builder not needed
"*.dll": "bin/",
"ScriptExtenderSettings.json": "bin/",
}
| {f: "Data/" for f in bg3_utils.loose_file_folders},
delete=["info.json", "*.txt"],
)
)

def dataLooksValid(
self, filetree: mobase.IFileTree
) -> mobase.ModDataChecker.CheckReturn:
status = mobase.ModDataChecker.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)
else:
status = mobase.ModDataChecker.INVALID
break
elif rp.valid.match(name):
if status is mobase.ModDataChecker.INVALID:
status = mobase.ModDataChecker.VALID
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
)
elif rp.delete.match(name) or rp.move_match(name) is not None:
status = mobase.ModDataChecker.FIXABLE
else:
status = mobase.ModDataChecker.INVALID
break
return status
54 changes: 54 additions & 0 deletions games/baldursgate3/bg3_data_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from enum import IntEnum, auto

import mobase

from . import bg3_utils


class Content(IntEnum):
PAK = auto()
WORKSPACE = auto()
NATIVE = auto()
LOOSE_FILES = auto()
SE_FILES = auto()


class BG3DataContent(mobase.ModDataContent):
BG3_CONTENTS: list[tuple[Content, str, str, bool] | tuple[Content, str, str]] = [
(Content.WORKSPACE, "Mod workspace", ":/MO/gui/content/script"),
(Content.PAK, "Pak", ":/MO/gui/content/bsa"),
(Content.LOOSE_FILES, "Loose file override mod", ":/MO/gui/content/texture"),
(Content.SE_FILES, "Script Extender Files", ":/MO/gui/content/inifile"),
(Content.NATIVE, "Native DLL mod", ":/MO/gui/content/plugin"),
]

def getAllContents(self) -> list[mobase.ModDataContent.Content]:
return [
mobase.ModDataContent.Content(id, name, icon, *filter_only)
for id, name, icon, *filter_only in self.BG3_CONTENTS
]

def getContentsFor(self, filetree: mobase.IFileTree) -> list[int]:
contents: set[int] = set()
for entry in filetree:
if isinstance(entry, mobase.IFileTree):
match entry.name():
case "Script Extender":
contents.add(Content.SE_FILES)
case "Data":
contents.add(Content.LOOSE_FILES)
case "Mods":
for e in entry:
if e.name().endswith(".pak"):
contents.add(Content.PAK)
break
case "bin":
contents.add(Content.NATIVE)
case _:
for e in entry:
if e.name() in bg3_utils.loose_file_folders:
contents.add(Content.WORKSPACE)
break
elif entry.name().endswith(".pak"):
contents.add(Content.PAK)
return list(contents)
104 changes: 104 additions & 0 deletions games/baldursgate3/bg3_file_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import functools
import json
import os
from pathlib import Path
from typing import Callable, Optional

import yaml
from PyQt6.QtCore import QDir, 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()
doc_dir = Path(self.doc_dir().path())
progress = self._utils.create_progress_window(
"Mapping files to documents folder", len(active_mods) + 1
)
docs_path_mods = doc_dir / "Mods"
docs_path_se = doc_dir / "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)
progress.setValue(progress.value() + 1)
QApplication.processEvents()
if progress.wasCanceled():
qWarning("mapping canceled by user")
return self.current_mappings
self.map_files(self._utils.overwrite_path)
self.create_mapping(
self._utils.modsettings_path,
doc_dir / "PlayerProfiles" / "Public" / self._utils.modsettings_path.name,
)
progress.setValue(len(active_mods) + 1)
QApplication.processEvents()
progress.close()
return self.current_mappings

def map_files(
self,
path: Path,
dest: Optional[Path] = None,
pattern: str = "*",
rel: bool = True,
):
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):
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)
else:
self.create_mapping(file, dest / dest_func(file))
for file in found_jsons:
self.create_mapping(file, dest / dest_func(file))

def create_mapping(self, file: Path, dest: Path):
self.current_mappings.append(
mobase.Mapping(
source=str(file),
destination=str(dest),
is_directory=file.is_dir(),
create_target=True,
)
)
Loading