aboutsummaryrefslogtreecommitdiff
path: root/libs/plugin_python/src/mobase
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/plugin_python/src/mobase
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/plugin_python/src/mobase')
-rw-r--r--libs/plugin_python/src/mobase/CMakeLists.txt40
-rw-r--r--libs/plugin_python/src/mobase/README.md110
-rw-r--r--libs/plugin_python/src/mobase/deprecation.cpp55
-rw-r--r--libs/plugin_python/src/mobase/deprecation.h27
-rw-r--r--libs/plugin_python/src/mobase/mobase.cpp86
-rw-r--r--libs/plugin_python/src/mobase/pybind11_all.h146
-rw-r--r--libs/plugin_python/src/mobase/wrappers/basic_classes.cpp952
-rw-r--r--libs/plugin_python/src/mobase/wrappers/game_features.cpp422
-rw-r--r--libs/plugin_python/src/mobase/wrappers/known_folders.h155
-rw-r--r--libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp329
-rw-r--r--libs/plugin_python/src/mobase/wrappers/pyfiletree.h34
-rw-r--r--libs/plugin_python/src/mobase/wrappers/pyplugins.cpp262
-rw-r--r--libs/plugin_python/src/mobase/wrappers/pyplugins.h527
-rw-r--r--libs/plugin_python/src/mobase/wrappers/utils.cpp50
-rw-r--r--libs/plugin_python/src/mobase/wrappers/widgets.cpp77
-rw-r--r--libs/plugin_python/src/mobase/wrappers/wrappers.cpp120
-rw-r--r--libs/plugin_python/src/mobase/wrappers/wrappers.h109
17 files changed, 3501 insertions, 0 deletions
diff --git a/libs/plugin_python/src/mobase/CMakeLists.txt b/libs/plugin_python/src/mobase/CMakeLists.txt
new file mode 100644
index 0000000..2fac52f
--- /dev/null
+++ b/libs/plugin_python/src/mobase/CMakeLists.txt
@@ -0,0 +1,40 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(Qt6 COMPONENTS Core)
+if(NOT TARGET mo2::uibase)
+ find_package(mo2-uibase CONFIG REQUIRED)
+endif()
+
+pybind11_add_module(mobase MODULE)
+mo2_default_source_group()
+mo2_configure_target(mobase
+ NO_SOURCES
+ WARNINGS 4
+ EXTERNAL_WARNINGS 4
+ AUTOMOC ON
+ TRANSLATIONS OFF
+)
+mo2_target_sources(mobase
+ FOLDER src
+ PRIVATE
+ deprecation.cpp
+ deprecation.h
+ mobase.cpp
+ pybind11_all.h
+)
+mo2_target_sources(mobase
+ FOLDER src/wrappers
+ PRIVATE
+ ./wrappers/basic_classes.cpp
+ ./wrappers/game_features.cpp
+ ./wrappers/known_folders.h
+ ./wrappers/pyfiletree.cpp
+ ./wrappers/pyfiletree.h
+ ./wrappers/pyplugins.cpp
+ ./wrappers/pyplugins.h
+ ./wrappers/utils.cpp
+ ./wrappers/widgets.cpp
+ ./wrappers/wrappers.cpp
+ ./wrappers/wrappers.h
+)
+target_link_libraries(mobase PRIVATE pybind11::qt pybind11::utils mo2::uibase Qt6::Core)
diff --git a/libs/plugin_python/src/mobase/README.md b/libs/plugin_python/src/mobase/README.md
new file mode 100644
index 0000000..b4d184e
--- /dev/null
+++ b/libs/plugin_python/src/mobase/README.md
@@ -0,0 +1,110 @@
+# mobase
+
+`mobase` is a ModOrganizer2 Python API.
+It provides access to the part of
+[`uibase`](https://github.com/ModOrganizer2/modorganizer-uibase) C++ API from
+Python through [`pybind11`](https://github.com/pybind/pybind11).
+
+## Organization
+
+**Important:** All (most) files should include `pybind11_all.h` (either directly
+or through another header) to get proper `type_caster` available.
+
+- `mobase.cpp` contains the `PYBIND11_MODULE` definition of `mobase` but is otherwise
+ the entrypoint for other functions.
+- `wrappers.h` contains the declaration of most functions implemented under
+ `wrappers/`.
+- The other files under `wrappers/` contains bindings and trampoline classes (see
+ below) for `uibase` classes.
+ - `basic_classes.cpp` contains the bindings for most classes that cannot be extended
+ in Python (`IOrganizer`, `IModInterface`, etc.)
+ - `game_features.cpp` contains the bindings and trampoline classes for game features.
+ - `pyfiletree.h` and `pyfiletree.cpp` contains bindings for the `IFileTree`-related
+ classes.
+ - `pyplugins.h` contains the trampoline classes for the `IPluginXXX` classes and
+ `pyplugins.cpp` the bindings.
+ - `pyplugins.h` is required since the trampoline classes are tagged with `Q_OBJECT`,
+ and MOC does not work if the classes are declared in a C++ file.
+ - `pyplugins.cpp` also contains the `extract_plugins` function in `mobase.private`
+ that is used to extract plugins from Python object in the runner.
+ - `widgets.cpp` contains the bindings for the widget classes.
+ - `wrappers.cpp` contains the trampoline and bindings for non-plugin classes that can
+ be extended through Python.
+
+## Updating mobase
+
+### Classes that cannot be extended through Python
+
+Updating or adding classes that cannot be extended through Python is quite easily.
+One simply needs to declare the appropriate `py::class_` or add new `.def()`.
+
+See below for things to remember when creating pybind11 bindings.
+
+### Free functions
+
+Similar to classes that cannot be extended through Python, see above.
+
+### Classes that can be extended through Python: Plugins
+
+To extend plugins, simply update the trampoline classes in `pyplugins.h` and the
+bindings in `pyplugins.cpp`.
+
+**Note:** For new plugins, simply look at the existing one.
+
+### Classes that can be extended through Python: Game Features
+
+To extend or expose game features:
+
+- Create (if there is not already one) a trampoline class for the feature in
+ `game_features.cpp`.
+ - Add implementation of missing functions if required.
+- Add the bindings in `add_game_feature_bindings` in `game_features.cpp`.
+- For new feature, add the feature type to `GameFeaturesHelper::GameFeatures` in
+ `game_features.cpp`.
+
+### Classes that can be extended through Python: Others
+
+Non-plugin classes should be added to the `wrappers.cpp` file and should be exposed
+with `std::shared_ptr<>` or `qobject_holder<>` holders.
+
+- If the classes extends `QObject`, use a `qobject_holder`.
+- Otherwise use a `std::shared_ptr<>` holder and add a `MO2_PYBIND11_SHARED_CPP_HOLDER`
+ declaration in `pybind11_all.h`.
+
+Trampoline can be defined directly in `wrappers.cpp`, and bindings in the appropriate
+function.
+See the existing classes for example.
+
+**Important:**
+You need to make sure that `uibase` manipulates such classes through
+`std::shared_ptr<>` (unless those inherit `QObject`).
+Using `std::unique_ptr<>` is not possible since `std::unique_ptr<>` cannot have custom
+runtime-specified deleters.
+
+## Things to remember
+
+Here are a few things to remember when creating bindings:
+
+- If a function has multiple overloads that can conflict in Python, the more complex
+ one must be defined first as pybind11 will try calling them in order.
+- If a C++ function expect a `QString`, `QFileInfo` or `QDir` that represents a file or
+ a directory, wrapping the function with `wrap_for_filepath` or `wrap_for_directory` is
+ a good idea. This allows Python to call the function with `pathlib.Path`.
+- Most of the C++ function taking a reference to modify in C++ cannot be directly
+ exposed in Python since Python cannot modify reference to simple type (e.g.
+ `QString&` or `int&`).
+ The best way to expose such function is to bind a lambda that returns a variant from
+ Python, e.g.
+
+```cpp
+// assume the C++ function is QString fn(QString& foo, QString const& bar, int& maz);
+
+m.def("function", [](QString& foo, QString const& bar, int& maz) {
+ // call the function
+ auto ret = function(foo, bar, maz);
+
+ // make a tuple containing the return value (if there is one), and the modified
+ // values passed by reference
+ return std::make_tuple(ret, foo, maz);
+});
+```
diff --git a/libs/plugin_python/src/mobase/deprecation.cpp b/libs/plugin_python/src/mobase/deprecation.cpp
new file mode 100644
index 0000000..958712d
--- /dev/null
+++ b/libs/plugin_python/src/mobase/deprecation.cpp
@@ -0,0 +1,55 @@
+#include "deprecation.h"
+
+#include <filesystem>
+#include <set>
+
+#include <pybind11/pybind11.h>
+
+#include <QCoreApplication>
+
+#include <uibase/log.h>
+
+namespace py = pybind11;
+
+namespace mo2::python {
+
+ void show_deprecation_warning(std::string_view name, std::string_view message,
+ bool show_once)
+ {
+
+ // Contains the list of filename / line number for which a deprecation
+ // warning has already been shown.
+ static std::set<std::pair<std::string, int>> DeprecatedLines;
+
+ // Find the caller:
+ auto inspect = py::module_::import("inspect");
+ auto current_frame = inspect.attr("currentframe")();
+ py::sequence callable_frame = inspect.attr("getouterframes")(current_frame, 2);
+ auto last_frame = callable_frame[py::int_(-1)];
+ auto filename = last_frame.attr("filename").cast<std::string>();
+ auto function = last_frame.attr("function").cast<std::string>();
+ auto lineno = last_frame.attr("lineno").cast<int>();
+
+ // Only show once if requested:
+ if (show_once && DeprecatedLines.contains({filename, lineno})) {
+ return;
+ }
+
+ // Register the deprecation:
+ DeprecatedLines.emplace(filename, lineno);
+
+ auto path = relative(std::filesystem::path(filename),
+ QCoreApplication::applicationDirPath().toStdWString());
+
+ // Show the message:
+ if (message.empty()) {
+ MOBase::log::warn("[deprecated] {} in {} [{}:{}].", name, function,
+ path.native(), lineno);
+ }
+ else {
+ MOBase::log::warn("[deprecated] {} in {} [{}:{}]: {}", name, function,
+ path.native(), lineno, message);
+ }
+ }
+
+} // namespace mo2::python
diff --git a/libs/plugin_python/src/mobase/deprecation.h b/libs/plugin_python/src/mobase/deprecation.h
new file mode 100644
index 0000000..d9a445c
--- /dev/null
+++ b/libs/plugin_python/src/mobase/deprecation.h
@@ -0,0 +1,27 @@
+#ifndef PYTHONRUNNER_UTILS_H
+#define PYTHONRUNNER_UTILS_H
+
+#include <string_view>
+
+#include <pybind11/pybind11.h>
+
+namespace mo2::python {
+
+ /**
+ * @brief Show a deprecation warning.
+ *
+ * This methods will print a warning in MO2 log containing the location of
+ * the call to the deprecated function. If show_once is true, the
+ * deprecation warning will only be logged the first time the function is
+ * called at this location.
+ *
+ * @param name Name of the deprecated function.
+ * @param message Deprecation message.
+ * @param show_once Only show the message once per call location.
+ */
+ void show_deprecation_warning(std::string_view name, std::string_view message = "",
+ bool show_once = true);
+
+} // namespace mo2::python
+
+#endif
diff --git a/libs/plugin_python/src/mobase/mobase.cpp b/libs/plugin_python/src/mobase/mobase.cpp
new file mode 100644
index 0000000..b1daa2b
--- /dev/null
+++ b/libs/plugin_python/src/mobase/mobase.cpp
@@ -0,0 +1,86 @@
+#pragma warning(disable : 4100)
+#pragma warning(disable : 4996)
+
+#include <format>
+#include <tuple>
+#include <variant>
+
+#include <pybind11/embed.h>
+
+#include "pybind11_all.h"
+
+#include "wrappers/pyfiletree.h"
+#include "wrappers/wrappers.h"
+
+using namespace MOBase;
+namespace py = pybind11;
+
+PYBIND11_MODULE(mobase, m)
+{
+ using namespace mo2::python;
+
+ m.add_object("PyQt6", py::module_::import("PyQt6"));
+ m.add_object("PyQt6.QtCore", py::module_::import("PyQt6.QtCore"));
+ m.add_object("PyQt6.QtGui", py::module_::import("PyQt6.QtGui"));
+ m.add_object("PyQt6.QtWidgets", py::module_::import("PyQt6.QtWidgets"));
+
+ // bindings
+ //
+ mo2::python::add_basic_bindings(m);
+ mo2::python::add_wrapper_bindings(m);
+
+ // game features must be added before plugins
+ mo2::python::add_game_feature_bindings(m);
+ mo2::python::add_igamefeatures_classes(m);
+
+ mo2::python::add_plugins_bindings(m);
+
+ // widgets
+ //
+ py::module_ widgets = m.def_submodule("widgets");
+ mo2::python::add_widget_bindings(widgets);
+
+ // utils
+ //
+ py::module_ utils = m.def_submodule("utils");
+ mo2::python::add_utils_bindings(utils);
+
+ // functions
+ //
+ m.def("getFileVersion", wrap_for_filepath(&MOBase::getFileVersion),
+ py::arg("filepath"));
+ m.def("getProductVersion", wrap_for_filepath(&MOBase::getProductVersion),
+ py::arg("executable"));
+ m.def("getIconForExecutable", wrap_for_filepath(&MOBase::iconForExecutable),
+ py::arg("executable"));
+
+ // typing stuff to be consistent with stubs and allow plugin developers to properly
+ // type their code if they want
+ {
+ m.add_object("TypeVar", py::module_::import("typing").attr("TypeVar"));
+
+ auto s = m.attr("__dict__");
+
+ // expose MoVariant
+ //
+ // this needs to be defined, otherwise MoVariant is not found when actually
+ // running plugins through MO2, making them crash (if plugins use MoVariant in
+ // their own code)
+ //
+ m.add_object(
+ "MoVariant",
+ py::eval("None | bool | int | str | list[object] | dict[str, object]"));
+
+ // same thing for GameFeatureType
+ //
+ m.add_object("GameFeatureType", py::eval("TypeVar('GameFeatureType')", s));
+ }
+
+ // private stuff for debugging/test
+ py::module_ moprivate = m.def_submodule("private");
+
+ // expose a function to create a particular tree, only for debugging
+ // purpose, not in mobase.
+ mo2::python::add_make_tree_function(moprivate);
+ moprivate.def("extract_plugins", &mo2::python::extract_plugins);
+}
diff --git a/libs/plugin_python/src/mobase/pybind11_all.h b/libs/plugin_python/src/mobase/pybind11_all.h
new file mode 100644
index 0000000..c92d7ff
--- /dev/null
+++ b/libs/plugin_python/src/mobase/pybind11_all.h
@@ -0,0 +1,146 @@
+#ifndef PYTHON_PYBIND11_ALL_H
+#define PYTHON_PYBIND11_ALL_H
+
+#include <filesystem>
+
+#include <QDir>
+
+#include <pybind11/operators.h>
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include <pybind11/stl/filesystem.h>
+
+#include "pybind11_qt/pybind11_qt.h"
+
+#include "pybind11_utils/functional.h"
+#include "pybind11_utils/generator.h"
+#include "pybind11_utils/shared_cpp_owner.h"
+#include "pybind11_utils/smart_variant_wrapper.h"
+
+#include <uibase/game_features/game_feature.h>
+#include <uibase/isavegame.h>
+#include <uibase/pluginrequirements.h>
+
+namespace mo2::python {
+
+ namespace detail {
+
+ template <>
+ struct smart_variant_converter<QString> {
+
+ static QString from(std::filesystem::path const& path)
+ {
+ return QString::fromStdString(path.string());
+ }
+
+ static QString from(QFileInfo const& fileInfo)
+ {
+ return fileInfo.filePath();
+ }
+
+ static QString from(QDir const& dir) { return dir.path(); }
+ };
+
+ template <>
+ struct smart_variant_converter<std::filesystem::path> {
+
+ static std::filesystem::path from(QString const& value)
+ {
+ return {value.toStdWString()};
+ }
+
+ static std::filesystem::path from(QFileInfo const& fileInfo)
+ {
+ return fileInfo.filesystemFilePath();
+ }
+
+ static std::filesystem::path from(QDir const& dir)
+ {
+ return dir.filesystemPath();
+ }
+ };
+
+ // we do not need specialization for QFileInfo and QDir because both of them can
+ // be constructed from std::filesystem::path and QString already
+
+ } // namespace detail
+
+ using FileWrapper = smart_variant<QString, std::filesystem::path, QFileInfo>;
+ using DirectoryWrapper = smart_variant<QString, std::filesystem::path, QDir>;
+
+ // wrap the given function to accept FileWrapper (str | PathLike | QFileInfo) at the
+ // given argument positions (or any valid positions if Is... is empty)
+ //
+ template <std::size_t... Is, class Fn>
+ auto wrap_for_filepath(Fn&& fn)
+ {
+ return mo2::python::wrap_arguments<FileWrapper, Is...>(std::forward<Fn>(fn));
+ }
+
+ // wrap the given function to accept DirectoryWrapper (str | PathLike | QDir)
+ // at the given argument positions (or any valid positions if Is... is empty)
+ //
+ template <std::size_t... Is, class Fn>
+ auto wrap_for_directory(Fn&& fn)
+ {
+ return mo2::python::wrap_arguments<DirectoryWrapper, Is...>(
+ std::forward<Fn>(fn));
+ }
+
+ // wrap a function-like object to return a FileWrapper instead of its return type,
+ // useful to generate proper typing in Python
+ //
+ // note that QFileInfo has a __fspath__ in Python, so it is quite easy to convert
+ // from "FileWrapper", a.k.a., str | os.PathLike | QFileInfo to Path by simply
+ // calling Path() on the return type if necessary
+ //
+ // this should be combined with custom return-value in PYBIND11_OVERRIDE(_PURE), see
+ // ISaveGame binding for an example
+ //
+ template <class Fn>
+ auto wrap_return_for_filepath(Fn&& fn)
+ {
+ return mo2::python::wrap_return<FileWrapper>(std::forward<Fn>(fn));
+ }
+
+ // similar to wrap_return_for_filepath, except it returns a DirectoryWrapper instead
+ // of its return type
+ //
+ // this is much less practical than wrap_return_for_filepath since QDir does not
+ // expose __fspath__, so more complex things need to be done in Python, which is why
+ // this should be used carefully (e.g., should not be used if the return type is
+ // already QDir)
+ //
+ template <class Fn>
+ auto wrap_return_for_directory(Fn&& fn)
+ {
+ return mo2::python::wrap_return<DirectoryWrapper>(std::forward<Fn>(fn));
+ }
+
+ // convert a QList to QStringList - QString must be constructible from QString
+ //
+ template <class T>
+ QStringList toQStringList(QList<T> const& list)
+ {
+ static_assert(std::is_constructible_v<QString, T>,
+ "QString must be constructible from T.");
+ return {list.begin(), list.end()};
+ }
+
+ // convert a QStringList to a QList - T must be constructible from QString
+ //
+ template <class T>
+ QList<T> toQList(QStringList const& list)
+ {
+ static_assert(std::is_constructible_v<T, QString>,
+ "T must be constructible from QString.");
+ return {list.begin(), list.end()};
+ }
+
+} // namespace mo2::python
+
+MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::IPluginRequirement)
+MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::ISaveGame)
+MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::GameFeature)
+
+#endif
diff --git a/libs/plugin_python/src/mobase/wrappers/basic_classes.cpp b/libs/plugin_python/src/mobase/wrappers/basic_classes.cpp
new file mode 100644
index 0000000..ea69a46
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/basic_classes.cpp
@@ -0,0 +1,952 @@
+#include "wrappers.h"
+
+#include "../pybind11_all.h"
+
+#include <format>
+#include <memory>
+
+#include <pybind11_utils/generator.h>
+#include <uibase/executableinfo.h>
+#include <uibase/filemapping.h>
+#include <uibase/game_features/igamefeatures.h>
+#include <uibase/guessedvalue.h>
+#include <uibase/idownloadmanager.h>
+#include <uibase/iexecutable.h>
+#include <uibase/iexecutableslist.h>
+#include <uibase/iinstallationmanager.h>
+#include <uibase/iinstance.h>
+#include <uibase/iinstancemanager.h>
+#include <uibase/imodinterface.h>
+#include <uibase/imodrepositorybridge.h>
+#include <uibase/imoinfo.h>
+#include <uibase/iplugin.h>
+#include <uibase/iplugindiagnose.h>
+#include <uibase/iplugingame.h>
+#include <uibase/ipluginlist.h>
+#include <uibase/pluginsetting.h>
+#include <uibase/versioninfo.h>
+
+#include "../deprecation.h"
+#include "pyfiletree.h"
+
+using namespace MOBase;
+
+namespace mo2::python {
+
+ namespace py = pybind11;
+
+ using namespace pybind11::literals;
+
+ void add_versioninfo_classes(py::module_ m)
+ {
+ // Version
+ py::class_<Version> pyVersion(m, "Version");
+
+ py::enum_<Version::ReleaseType>(pyVersion, "ReleaseType")
+ .value("DEVELOPMENT", Version::Development)
+ .value("ALPHA", Version::Alpha)
+ .value("BETA", Version::Beta)
+ .value("RELEASE_CANDIDATE", Version::ReleaseCandidate)
+ .export_values();
+
+ py::enum_<Version::ParseMode>(pyVersion, "ParseMode")
+ .value("SEMVER", Version::ParseMode::SemVer)
+ .value("MO2", Version::ParseMode::MO2);
+
+ py::enum_<Version::FormatMode>(pyVersion, "FormatMode", py::arithmetic{})
+ .value("FORCE_SUBPATCH", Version::FormatMode::ForceSubPatch)
+ .value("NO_SEPARATOR", Version::FormatMode::NoSeparator)
+ .value("SHORT_ALPHA_BETA", Version::FormatMode::ShortAlphaBeta)
+ .value("NO_METADATA", Version::FormatMode::NoMetadata)
+ .value("CONDENSED",
+ static_cast<Version::FormatMode>(Version::FormatCondensed.toInt()))
+ .export_values()
+ .def("__xor__",
+ py::overload_cast<Version::FormatMode, Version::FormatModes>(
+ &operator^))
+ .def("__and__",
+ py::overload_cast<Version::FormatMode, Version::FormatModes>(
+ &operator&))
+ .def("__or__", py::overload_cast<Version::FormatMode, Version::FormatModes>(
+ &operator|))
+ .def("__rxor__",
+ py::overload_cast<Version::FormatMode, Version::FormatModes>(
+ &operator^))
+ .def("__rand__",
+ py::overload_cast<Version::FormatMode, Version::FormatModes>(
+ &operator&))
+ .def("__ror__",
+ py::overload_cast<Version::FormatMode, Version::FormatModes>(
+ &operator|));
+
+ pyVersion
+ .def_static("parse", &Version::parse, "value"_a,
+ "mode"_a = Version::ParseMode::SemVer)
+ .def(py::init<int, int, int, QString>(), "major"_a, "minor"_a, "patch"_a,
+ "metadata"_a = "")
+ .def(py::init<int, int, int, int, QString>(), "major"_a, "minor"_a,
+ "patch"_a, "subpatch"_a, "metadata"_a = "")
+ .def(py::init<int, int, int, Version::ReleaseType, QString>(), "major"_a,
+ "minor"_a, "patch"_a, "type"_a, "metadata"_a = "")
+ .def(py::init<int, int, int, int, Version::ReleaseType, QString>(),
+ "major"_a, "minor"_a, "patch"_a, "subpatch"_a, "type"_a,
+ "metadata"_a = "")
+ .def(py::init<int, int, int, Version::ReleaseType, int, QString>(),
+ "major"_a, "minor"_a, "patch"_a, "type"_a, "prerelease"_a,
+ "metadata"_a = "")
+ .def(py::init<int, int, int, int, Version::ReleaseType, int, QString>(),
+ "major"_a, "minor"_a, "patch"_a, "subpatch"_a, "type"_a,
+ "prerelease"_a, "metadata"_a = "")
+ .def(py::init<int, int, int, int,
+ std::vector<std::variant<int, Version::ReleaseType>>,
+ QString>(),
+ "major"_a, "minor"_a, "patch"_a, "subpatch"_a, "prereleases"_a,
+ "metadata"_a = "")
+ .def("isPreRelease", &Version::isPreRelease)
+ .def_property_readonly("major", &Version::major)
+ .def_property_readonly("minor", &Version::minor)
+ .def_property_readonly("patch", &Version::patch)
+ .def_property_readonly("subpatch", &Version::subpatch)
+ .def_property_readonly("prereleases", &Version::preReleases)
+ .def_property_readonly("build_metadata", &Version::buildMetadata)
+ .def("string", &Version::string, "mode"_a = Version::FormatModes{})
+ .def("__str__",
+ [](Version const& version) {
+ return version.string(Version::FormatCondensed);
+ })
+ .def(py::self < py::self)
+ .def(py::self > py::self)
+ .def(py::self <= py::self)
+ .def(py::self >= py::self)
+ .def(py::self != py::self)
+ .def(py::self == py::self);
+
+ // VersionInfo
+ py::enum_<MOBase::VersionInfo::ReleaseType>(m, "ReleaseType")
+ .value("final", MOBase::VersionInfo::RELEASE_FINAL)
+ .value("candidate", MOBase::VersionInfo::RELEASE_CANDIDATE)
+ .value("beta", MOBase::VersionInfo::RELEASE_BETA)
+ .value("alpha", MOBase::VersionInfo::RELEASE_ALPHA)
+ .value("prealpha", MOBase::VersionInfo::RELEASE_PREALPHA)
+
+ .value("FINAL", MOBase::VersionInfo::RELEASE_FINAL)
+ .value("CANDIDATE", MOBase::VersionInfo::RELEASE_CANDIDATE)
+ .value("BETA", MOBase::VersionInfo::RELEASE_BETA)
+ .value("ALPHA", MOBase::VersionInfo::RELEASE_ALPHA)
+ .value("PRE_ALPHA", MOBase::VersionInfo::RELEASE_PREALPHA);
+
+ py::enum_<MOBase::VersionInfo::VersionScheme>(m, "VersionScheme")
+ .value("discover", MOBase::VersionInfo::SCHEME_DISCOVER)
+ .value("regular", MOBase::VersionInfo::SCHEME_REGULAR)
+ .value("decimalmark", MOBase::VersionInfo::SCHEME_DECIMALMARK)
+ .value("numbersandletters", MOBase::VersionInfo::SCHEME_NUMBERSANDLETTERS)
+ .value("date", MOBase::VersionInfo::SCHEME_DATE)
+ .value("literal", MOBase::VersionInfo::SCHEME_LITERAL)
+
+ .value("DISCOVER", MOBase::VersionInfo::SCHEME_DISCOVER)
+ .value("REGULAR", MOBase::VersionInfo::SCHEME_REGULAR)
+ .value("DECIMAL_MARK", MOBase::VersionInfo::SCHEME_DECIMALMARK)
+ .value("NUMBERS_AND_LETTERS", MOBase::VersionInfo::SCHEME_NUMBERSANDLETTERS)
+ .value("DATE", MOBase::VersionInfo::SCHEME_DATE)
+ .value("LITERAL", MOBase::VersionInfo::SCHEME_LITERAL);
+
+ py::class_<VersionInfo>(m, "VersionInfo")
+ .def(py::init<>())
+ .def(py::init<QString, VersionInfo::VersionScheme>(), "value"_a,
+ "scheme"_a = VersionInfo::SCHEME_DISCOVER)
+ // note: order of the two init<> below is important because
+ // ReleaseType is a simple enum with an implicit int conversion.
+ .def(py::init<int, int, int, int, VersionInfo::ReleaseType>(), "major"_a,
+ "minor"_a, "subminor"_a, "subsubminor"_a,
+ "release_type"_a = VersionInfo::RELEASE_FINAL)
+ .def(py::init<int, int, int, VersionInfo::ReleaseType>(), "major"_a,
+ "minor"_a, "subminor"_a, "release_type"_a = VersionInfo::RELEASE_FINAL)
+ .def("clear", &VersionInfo::clear)
+ .def("parse", &VersionInfo::parse, "value"_a,
+ "scheme"_a = VersionInfo::SCHEME_DISCOVER, "is_manual"_a = false)
+ .def("canonicalString", &VersionInfo::canonicalString)
+ .def("displayString", &VersionInfo::displayString, "forced_segments"_a = 2)
+ .def("isValid", &VersionInfo::isValid)
+ .def("scheme", &VersionInfo::scheme)
+ .def("__str__", &VersionInfo::canonicalString)
+ .def(py::self < py::self)
+ .def(py::self > py::self)
+ .def(py::self <= py::self)
+ .def(py::self >= py::self)
+ .def(py::self != py::self)
+ .def(py::self == py::self);
+ }
+
+ void add_executable_classes(py::module_ m)
+ {
+ py::class_<ExecutableInfo>(m, "ExecutableInfo")
+ .def(py::init<const QString&, const FileWrapper&>(), "title"_a, "binary"_a)
+ .def("withArgument", &ExecutableInfo::withArgument, "argument"_a)
+ .def("withWorkingDirectory",
+ wrap_for_directory(&ExecutableInfo::withWorkingDirectory),
+ "directory"_a)
+ .def("withSteamAppId", &ExecutableInfo::withSteamAppId, "app_id"_a)
+ .def("asCustom", &ExecutableInfo::asCustom)
+ .def("isValid", &ExecutableInfo::isValid)
+ .def("title", &ExecutableInfo::title)
+ .def("binary", &ExecutableInfo::binary)
+ .def("arguments", &ExecutableInfo::arguments)
+ .def("workingDirectory", &ExecutableInfo::workingDirectory)
+ .def("steamAppID", &ExecutableInfo::steamAppID)
+ .def("isCustom", &ExecutableInfo::isCustom);
+
+ py::class_<ExecutableForcedLoadSetting>(m, "ExecutableForcedLoadSetting")
+ .def(py::init<const QString&, const QString&>(), "process"_a, "library"_a)
+ .def("withForced", &ExecutableForcedLoadSetting::withForced, "forced"_a)
+ .def("withEnabled", &ExecutableForcedLoadSetting::withEnabled, "enabled"_a)
+ .def("enabled", &ExecutableForcedLoadSetting::enabled)
+ .def("forced", &ExecutableForcedLoadSetting::forced)
+ .def("library", &ExecutableForcedLoadSetting::library)
+ .def("process", &ExecutableForcedLoadSetting::process);
+
+ py::class_<IExecutable>(m, "IExecutable")
+ .def("title", &IExecutable::title)
+ .def("binaryInfo", &IExecutable::binaryInfo)
+ .def("arguments", &IExecutable::arguments)
+ .def("steamAppID", &IExecutable::steamAppID)
+ .def("workingDirectory", &IExecutable::workingDirectory)
+ .def("isShownOnToolbar", &IExecutable::isShownOnToolbar)
+ .def("usesOwnIcon", &IExecutable::usesOwnIcon)
+ .def("minimizeToSystemTray", &IExecutable::minimizeToSystemTray)
+ .def("hide", &IExecutable::hide);
+
+ py::class_<IExecutablesList>(m, "IExecutablesList")
+ .def("executables",
+ [](IExecutablesList* executablesList) {
+ return make_generator(executablesList->executables(),
+ py::return_value_policy::reference);
+ })
+ .def("getByTitle", &IExecutablesList::getByTitle, "title"_a)
+ .def("getByBinary", &IExecutablesList::getByBinary, "info"_a)
+ .def("titleExists", &IExecutablesList::contains, "title"_a);
+ }
+
+ void add_modinterface_classes(py::module_ m)
+ {
+ py::enum_<EndorsedState>(m, "EndorsedState", py::arithmetic())
+ .value("ENDORSED_FALSE", EndorsedState::ENDORSED_FALSE)
+ .value("ENDORSED_TRUE", EndorsedState::ENDORSED_TRUE)
+ .value("ENDORSED_UNKNOWN", EndorsedState::ENDORSED_UNKNOWN)
+ .value("ENDORSED_NEVER", EndorsedState::ENDORSED_NEVER);
+
+ py::enum_<TrackedState>(m, "TrackedState", py::arithmetic())
+ .value("TRACKED_FALSE", TrackedState::TRACKED_FALSE)
+ .value("TRACKED_TRUE", TrackedState::TRACKED_TRUE)
+ .value("TRACKED_UNKNOWN", TrackedState::TRACKED_UNKNOWN);
+
+ py::class_<IModInterface>(m, "IModInterface")
+ .def("name", &IModInterface::name)
+ .def("absolutePath", &IModInterface::absolutePath)
+
+ .def("comments", &IModInterface::comments)
+ .def("notes", &IModInterface::notes)
+ .def("gameName", &IModInterface::gameName)
+ .def("repository", &IModInterface::repository)
+ .def("nexusId", &IModInterface::nexusId)
+ .def("version", &IModInterface::version)
+ .def("newestVersion", &IModInterface::newestVersion)
+ .def("ignoredVersion", &IModInterface::ignoredVersion)
+ .def("installationFile", &IModInterface::installationFile)
+ .def("converted", &IModInterface::converted)
+ .def("validated", &IModInterface::validated)
+ .def("color", &IModInterface::color)
+ .def("url", &IModInterface::url)
+ .def("primaryCategory", &IModInterface::primaryCategory)
+ .def("categories", &IModInterface::categories)
+ .def("author", &IModInterface::author)
+ .def("uploader", &IModInterface::uploader)
+ .def("uploaderUrl", &IModInterface::uploaderUrl)
+ .def("trackedState", &IModInterface::trackedState)
+ .def("endorsedState", &IModInterface::endorsedState)
+ .def("fileTree", &IModInterface::fileTree)
+ .def("isOverwrite", &IModInterface::isOverwrite)
+ .def("isBackup", &IModInterface::isBackup)
+ .def("isSeparator", &IModInterface::isSeparator)
+ .def("isForeign", &IModInterface::isForeign)
+
+ .def("setVersion", &IModInterface::setVersion, "version"_a)
+ .def("setNewestVersion", &IModInterface::setNewestVersion, "version"_a)
+ .def("setIsEndorsed", &IModInterface::setIsEndorsed, "endorsed"_a)
+ .def("setNexusID", &IModInterface::setNexusID, "nexus_id"_a)
+ .def("addNexusCategory", &IModInterface::addNexusCategory, "category_id"_a)
+ .def("addCategory", &IModInterface::addCategory, "name"_a)
+ .def("removeCategory", &IModInterface::removeCategory, "name"_a)
+ .def("setGameName", &IModInterface::setGameName, "name"_a)
+ .def("setUrl", &IModInterface::setUrl, "url"_a)
+ .def("pluginSetting", &IModInterface::pluginSetting, "plugin_name"_a,
+ "key"_a, "default"_a = QVariant())
+ .def("pluginSettings", &IModInterface::pluginSettings, "plugin_name"_a)
+ .def("setPluginSetting", &IModInterface::setPluginSetting, "plugin_name"_a,
+ "key"_a, "value"_a)
+ .def("clearPluginSettings", &IModInterface::clearPluginSettings,
+ "plugin_name"_a);
+ }
+
+ void add_modrepository_classes(py::module_ m)
+ {
+ py::class_<IModRepositoryBridge> iModRepositoryBridge(m,
+ "IModRepositoryBridge");
+ iModRepositoryBridge
+ .def("requestDescription", &IModRepositoryBridge::requestDescription,
+ "game_name"_a, "mod_id"_a, "user_data"_a)
+ .def("requestFiles", &IModRepositoryBridge::requestFiles, "game_name"_a,
+ "mod_id"_a, "user_data"_a)
+ .def("requestFileInfo", &IModRepositoryBridge::requestFileInfo,
+ "game_name"_a, "mod_id"_a, "file_id"_a, "user_data"_a)
+ .def("requestDownloadURL", &IModRepositoryBridge::requestDownloadURL,
+ "game_name"_a, "mod_id"_a, "file_id"_a, "user_data"_a)
+ .def("requestToggleEndorsement",
+ &IModRepositoryBridge::requestToggleEndorsement, "game_name"_a,
+ "mod_id"_a, "mod_version"_a, "endorse"_a, "user_data"_a);
+
+ py::qt::add_qt_delegate<QObject>(iModRepositoryBridge, "_object");
+
+ py::class_<ModRepositoryFileInfo>(m, "ModRepositoryFileInfo")
+ .def(py::init<const ModRepositoryFileInfo&>(), "other"_a)
+ .def(py::init<QString, int, int>(), "game_name"_a = "", "mod_id"_a = 0,
+ "file_id"_a = 0)
+ .def("__str__", &ModRepositoryFileInfo::toString)
+ .def_static("createFromJson", &ModRepositoryFileInfo::createFromJson,
+ "data"_a)
+ .def_readwrite("name", &ModRepositoryFileInfo::name)
+ .def_readwrite("uri", &ModRepositoryFileInfo::uri)
+ .def_readwrite("description", &ModRepositoryFileInfo::description)
+ .def_readwrite("version", &ModRepositoryFileInfo::version)
+ .def_readwrite("newestVersion", &ModRepositoryFileInfo::newestVersion)
+ .def_readwrite("categoryID", &ModRepositoryFileInfo::categoryID)
+ .def_readwrite("modName", &ModRepositoryFileInfo::modName)
+ .def_readwrite("gameName", &ModRepositoryFileInfo::gameName)
+ .def_readwrite("modID", &ModRepositoryFileInfo::modID)
+ .def_readwrite("fileID", &ModRepositoryFileInfo::fileID)
+ .def_readwrite("fileSize", &ModRepositoryFileInfo::fileSize)
+ .def_readwrite("fileName", &ModRepositoryFileInfo::fileName)
+ .def_readwrite("fileCategory", &ModRepositoryFileInfo::fileCategory)
+ .def_readwrite("fileTime", &ModRepositoryFileInfo::fileTime)
+ .def_readwrite("repository", &ModRepositoryFileInfo::repository)
+ .def_readwrite("userData", &ModRepositoryFileInfo::userData)
+ .def_readwrite("author", &ModRepositoryFileInfo::author)
+ .def_readwrite("uploader", &ModRepositoryFileInfo::uploader)
+ .def_readwrite("uploaderUrl", &ModRepositoryFileInfo::uploaderUrl);
+ }
+
+ void add_guessedstring_classes(py::module_ m)
+ {
+ py::enum_<MOBase::EGuessQuality>(m, "GuessQuality")
+ .value("INVALID", MOBase::GUESS_INVALID)
+ .value("FALLBACK", MOBase::GUESS_FALLBACK)
+ .value("GOOD", MOBase::GUESS_GOOD)
+ .value("META", MOBase::GUESS_META)
+ .value("PRESET", MOBase::GUESS_PRESET)
+ .value("USER", MOBase::GUESS_USER);
+
+ py::class_<MOBase::GuessedValue<QString>>(m, "GuessedString")
+ .def(py::init<>())
+ .def(py::init<QString const&, EGuessQuality>(), "value"_a,
+ "quality"_a = EGuessQuality::GUESS_USER)
+ .def("update",
+ py::overload_cast<const QString&>(&GuessedValue<QString>::update),
+ "value"_a)
+ .def("update",
+ py::overload_cast<const QString&, EGuessQuality>(
+ &GuessedValue<QString>::update),
+ "value"_a, "quality"_a)
+
+ // Methods to simulate the assignment operator:
+ .def("reset",
+ [](GuessedValue<QString>* gv) {
+ *gv = GuessedValue<QString>();
+ return gv;
+ })
+ .def(
+ "reset",
+ [](GuessedValue<QString>* gv, const QString& value, EGuessQuality eq) {
+ *gv = GuessedValue<QString>(value, eq);
+ return gv;
+ },
+ "value"_a, "quality"_a)
+ .def(
+ "reset",
+ [](GuessedValue<QString>* gv, const GuessedValue<QString>& other) {
+ *gv = other;
+ return gv;
+ },
+ "other"_a)
+
+ // use an intermediate lambda because we cannot have a function with a
+ // non-const reference in Python - in Python, the function should returned a
+ // bool or the modified value
+ .def(
+ "setFilter",
+ [](GuessedValue<QString>* gv,
+ std::function<std::variant<QString, bool>(QString const&)> fn) {
+ gv->setFilter([fn](QString& s) {
+ auto ret = fn(s);
+ return std::visit(
+ [&s](auto v) {
+ if constexpr (std::is_same_v<decltype(v), QString>) {
+ s = v;
+ return true;
+ }
+ else if constexpr (std::is_same_v<decltype(v), bool>) {
+ return v;
+ }
+ },
+ ret);
+ });
+ },
+ "filter"_a)
+
+ // this makes a copy in python but it more practical than
+ // exposing an iterator
+ .def("variants", &GuessedValue<QString>::variants)
+ .def("__str__", &MOBase::GuessedValue<QString>::operator const QString&);
+
+ // implicit conversion from QString - this allows passing Python string to
+ // function expecting GuessedValue<QString>
+ py::implicitly_convertible<QString, GuessedValue<QString>>();
+ }
+
+ void add_ipluginlist_classes(py::module_ m)
+ {
+ py::enum_<IPluginList::PluginState>(m, "PluginState", py::arithmetic())
+ .value("missing", IPluginList::STATE_MISSING)
+ .value("inactive", IPluginList::STATE_INACTIVE)
+ .value("active", IPluginList::STATE_ACTIVE)
+
+ .value("MISSING", IPluginList::STATE_MISSING)
+ .value("INACTIVE", IPluginList::STATE_INACTIVE)
+ .value("ACTIVE", IPluginList::STATE_ACTIVE);
+
+ py::class_<IPluginList>(m, "IPluginList")
+ .def("state", &IPluginList::state, "name"_a)
+ .def("priority", &IPluginList::priority, "name"_a)
+ .def("setPriority", &IPluginList::setPriority, "name"_a, "priority"_a)
+ .def("loadOrder", &IPluginList::loadOrder, "name"_a)
+ .def("masters", &IPluginList::masters, "name"_a)
+ .def("origin", &IPluginList::origin, "name"_a)
+ .def("onRefreshed", &IPluginList::onRefreshed, "callback"_a)
+ .def("onPluginMoved", &IPluginList::onPluginMoved, "callback"_a)
+
+ .def("isMasterFlagged", &IPluginList::isMasterFlagged, "name"_a)
+ .def("hasMasterExtension", &IPluginList::hasMasterExtension, "name"_a)
+ .def("isMediumFlagged", &IPluginList::isMediumFlagged, "name"_a)
+ .def("isLightFlagged", &IPluginList::isLightFlagged, "name"_a)
+ .def("isBlueprintFlagged", &IPluginList::isBlueprintFlagged, "name"_a)
+ .def("hasLightExtension", &IPluginList::hasLightExtension, "name"_a)
+ .def("hasNoRecords", &IPluginList::hasNoRecords, "name"_a)
+
+ .def("formVersion", &IPluginList::formVersion, "name"_a)
+ .def("headerVersion", &IPluginList::headerVersion, "name"_a)
+ .def("author", &IPluginList::author, "name"_a)
+ .def("description", &IPluginList::description, "name"_a)
+
+ // Kept but deprecated for backward compatibility:
+ .def(
+ "onPluginStateChanged",
+ [](IPluginList* modList,
+ const std::function<void(const QString&, IPluginList::PluginStates)>&
+ fn) {
+ mo2::python::show_deprecation_warning(
+ "onPluginStateChanged",
+ "onPluginStateChanged(Callable[[str, "
+ "IPluginList.PluginStates], None]) is deprecated, "
+ "use onPluginStateChanged(Callable[[Dict[str, "
+ "IPluginList.PluginStates], None]) instead.");
+ return modList->onPluginStateChanged([fn](auto const& map) {
+ for (const auto& entry : map) {
+ fn(entry.first, entry.second);
+ }
+ });
+ },
+ "callback"_a)
+ .def(
+ "isMaster",
+ [](IPluginList* modList, QString const& name) {
+ mo2::python::show_deprecation_warning(
+ "isMaster",
+ "isMaster(str) is deprecated, use isMasterFlagged() or "
+ "hasMasterExtension() instead.");
+ return modList->isMasterFlagged(name);
+ },
+ "name"_a)
+ .def("onPluginStateChanged", &MOBase::IPluginList::onPluginStateChanged,
+ "callback"_a)
+ .def("pluginNames", &MOBase::IPluginList::pluginNames)
+ .def("setState", &MOBase::IPluginList::setState, "name"_a, "state"_a)
+ .def("setLoadOrder", &MOBase::IPluginList::setLoadOrder, "loadorder"_a);
+ }
+
+ void add_imodlist_classes(py::module_ m)
+ {
+ py::enum_<IModList::ModState>(m, "ModState", py::arithmetic())
+ .value("exists", IModList::STATE_EXISTS)
+ .value("active", IModList::STATE_ACTIVE)
+ .value("essential", IModList::STATE_ESSENTIAL)
+ .value("empty", IModList::STATE_EMPTY)
+ .value("endorsed", IModList::STATE_ENDORSED)
+ .value("valid", IModList::STATE_VALID)
+ .value("alternate", IModList::STATE_ALTERNATE)
+
+ .value("EXISTS", IModList::STATE_EXISTS)
+ .value("ACTIVE", IModList::STATE_ACTIVE)
+ .value("ESSENTIAL", IModList::STATE_ESSENTIAL)
+ .value("EMPTY", IModList::STATE_EMPTY)
+ .value("ENDORSED", IModList::STATE_ENDORSED)
+ .value("VALID", IModList::STATE_VALID)
+ .value("ALTERNATE", IModList::STATE_ALTERNATE);
+
+ py::class_<IModList>(m, "IModList")
+ .def("displayName", &MOBase::IModList::displayName, "name"_a)
+ .def("allMods", &MOBase::IModList::allMods)
+ .def("allModsByProfilePriority",
+ &MOBase::IModList::allModsByProfilePriority,
+ "profile"_a = static_cast<IProfile*>(nullptr))
+ .def("getMod", &MOBase::IModList::getMod,
+ py::return_value_policy::reference, "name"_a)
+ .def("removeMod", &MOBase::IModList::removeMod, "mod"_a)
+ .def("renameMod", &MOBase::IModList::renameMod,
+ py::return_value_policy::reference, "mod"_a, "name"_a)
+
+ .def("state", &MOBase::IModList::state, "name"_a)
+ .def("setActive",
+ py::overload_cast<QStringList const&, bool>(
+ &MOBase::IModList::setActive),
+ "names"_a, "active"_a)
+ .def("setActive",
+ py::overload_cast<QString const&, bool>(&MOBase::IModList::setActive),
+ "name"_a, "active"_a)
+ .def("priority", &MOBase::IModList::priority, "name"_a)
+ .def("setPriority", &MOBase::IModList::setPriority, "name"_a, "priority"_a)
+
+ // kept but deprecated for backward compatibility
+ .def(
+ "onModStateChanged",
+ [](IModList* modList,
+ const std::function<void(const QString&, IModList::ModStates)>& fn) {
+ mo2::python::show_deprecation_warning(
+ "onModStateChanged",
+ "onModStateChanged(Callable[[str, IModList.ModStates], None]) "
+ "is deprecated, "
+ "use onModStateChanged(Callable[[Dict[str, "
+ "IModList.ModStates], None]) instead.");
+ return modList->onModStateChanged([fn](auto const& map) {
+ for (const auto& entry : map) {
+ fn(entry.first, entry.second);
+ }
+ });
+ },
+ "callback"_a)
+
+ .def("onModInstalled", &MOBase::IModList::onModInstalled, "callback"_a)
+ .def("onModRemoved", &MOBase::IModList::onModRemoved, "callback"_a)
+ .def("onModStateChanged", &MOBase::IModList::onModStateChanged,
+ "callback"_a)
+ .def("onModMoved", &MOBase::IModList::onModMoved, "callback"_a);
+ }
+
+ void add_iorganizer_classes(py::module_ m)
+ {
+ // define INVALID_HANDLE_VALUE for startApplication, etc.
+ m.attr("INVALID_HANDLE_VALUE") = py::int_((std::uintptr_t)INVALID_HANDLE_VALUE);
+
+ py::class_<IOrganizer::FileInfo>(m, "FileInfo")
+ .def(py::init<>())
+ .def_readwrite("filePath", &IOrganizer::FileInfo::filePath)
+ .def_readwrite("archive", &IOrganizer::FileInfo::archive)
+ .def_readwrite("origins", &IOrganizer::FileInfo::origins);
+
+ py::class_<IOrganizer>(m, "IOrganizer")
+ .def("createNexusBridge", &IOrganizer::createNexusBridge,
+ py::return_value_policy::reference)
+ .def("instanceName", &IOrganizer::instanceName)
+ .def("profileName", &IOrganizer::profileName)
+ .def("profilePath", &IOrganizer::profilePath)
+ .def("downloadsPath", &IOrganizer::downloadsPath)
+ .def("overwritePath", &IOrganizer::overwritePath)
+ .def("basePath", &IOrganizer::basePath)
+ .def("modsPath", &IOrganizer::modsPath)
+ .def("appVersion",
+ [](IOrganizer& o) {
+ mo2::python::show_deprecation_warning(
+ "appVersion", "IOrganizer::appVersion() is deprecated, use "
+ "IOrganizer::version() instead.");
+#pragma warning(push)
+#pragma warning(disable : 4996)
+ return o.appVersion();
+#pragma warning(pop)
+ })
+ .def("version", &IOrganizer::version)
+ .def("createMod", &IOrganizer::createMod,
+ py::return_value_policy::reference, "name"_a)
+ .def("getGame", &IOrganizer::getGame, py::return_value_policy::reference,
+ "name"_a)
+ .def("modDataChanged", &IOrganizer::modDataChanged, "mod"_a)
+ .def("isPluginEnabled",
+ py::overload_cast<IPlugin*>(&IOrganizer::isPluginEnabled, py::const_),
+ "plugin"_a)
+ .def("isPluginEnabled",
+ py::overload_cast<QString const&>(&IOrganizer::isPluginEnabled,
+ py::const_),
+ "plugin"_a)
+ .def("pluginSetting", &IOrganizer::pluginSetting, "plugin_name"_a, "key"_a)
+ .def("setPluginSetting", &IOrganizer::setPluginSetting, "plugin_name"_a,
+ "key"_a, "value"_a)
+ .def("persistent", &IOrganizer::persistent, "plugin_name"_a, "key"_a,
+ "default"_a = QVariant())
+ .def("setPersistent", &IOrganizer::setPersistent, "plugin_name"_a, "key"_a,
+ "value"_a, "sync"_a = true)
+ .def("pluginDataPath", &IOrganizer::pluginDataPath)
+ .def("installMod", wrap_for_filepath<1>(&IOrganizer::installMod),
+ py::return_value_policy::reference, "filename"_a,
+ "name_suggestion"_a = "")
+ .def("resolvePath", wrap_for_filepath(&IOrganizer::resolvePath),
+ "filename"_a)
+ .def("listDirectories", &IOrganizer::listDirectories, "directory"_a)
+
+ // "provide multiple overloads of findFiles
+ .def(
+ "findFiles",
+ [](const IOrganizer* o, DirectoryWrapper const& p,
+ std::function<bool(QString const&)> const& f) {
+ return o->findFiles(p, f);
+ },
+ "path"_a, "filter"_a)
+
+ // in C++, it is possible to create a QStringList implicitly from
+ // a single QString, in Python is not possible with the current
+ // converters in python (and I do not think it is a good idea to
+ // have it everywhere), but here it is nice to be able to
+ // pass a single string, so we add an extra overload
+ //
+ // important: the order matters, because a Python string can be
+ // converted to a QStringList since it is a sequence of
+ // single-character strings:
+ .def(
+ "findFiles",
+ [](const IOrganizer* o, DirectoryWrapper const& p,
+ const QStringList& gf) {
+ return o->findFiles(p, gf);
+ },
+ "path"_a, "patterns"_a)
+ .def(
+ "findFiles",
+ [](const IOrganizer* o, DirectoryWrapper const& p, const QString& f) {
+ return o->findFiles(p, QStringList{f});
+ },
+ "path"_a, "pattern"_a)
+
+ .def("getFileOrigins", &IOrganizer::getFileOrigins, "filename"_a)
+ .def("findFileInfos", wrap_for_directory(&IOrganizer::findFileInfos),
+ "path"_a, "filter"_a)
+
+ .def("virtualFileTree", &IOrganizer::virtualFileTree)
+
+ .def("instanceManager", &IOrganizer::instanceManager,
+ py::return_value_policy::reference)
+ .def("downloadManager", &IOrganizer::downloadManager,
+ py::return_value_policy::reference)
+ .def("pluginList", &IOrganizer::pluginList,
+ py::return_value_policy::reference)
+ .def("modList", &IOrganizer::modList, py::return_value_policy::reference)
+ .def("executablesList", &IOrganizer::executablesList,
+ py::return_value_policy::reference)
+ .def("gameFeatures", &IOrganizer::gameFeatures,
+ py::return_value_policy::reference)
+ .def("profile", &IOrganizer::profile)
+ .def("profileNames", &IOrganizer::profileNames)
+ .def("getProfile", &IOrganizer::getProfile, "name"_a)
+
+ // custom implementation for startApplication and
+ // waitForApplication because 1) HANDLE (= void*) is not properly
+ // converted from/to python, and 2) we need to convert the by-ptr
+ // argument to a return-tuple for waitForApplication
+ .def(
+ "startApplication",
+ [](IOrganizer* o, const FileWrapper& executable,
+ const QStringList& args, const DirectoryWrapper& cwd,
+ const QString& profile, const QString& forcedCustomOverwrite,
+ bool ignoreCustomOverwrite) -> std::uintptr_t {
+ return (std::uintptr_t)o->startApplication(
+ executable, args, cwd, profile, forcedCustomOverwrite,
+ ignoreCustomOverwrite);
+ },
+ "executable"_a, "args"_a = QStringList(), "cwd"_a = "",
+ "profile"_a = "", "forcedCustomOverwrite"_a = "",
+ "ignoreCustomOverwrite"_a = false)
+ .def(
+ "waitForApplication",
+ [](IOrganizer* o, std::uintptr_t handle, bool refresh) {
+ DWORD returnCode;
+ bool result =
+ o->waitForApplication((HANDLE)handle, refresh, &returnCode);
+
+ // we force signed return code because it's probably what's expected
+ // in Python
+ return std::make_tuple(
+ result, static_cast<std::make_signed_t<DWORD>>(returnCode));
+ },
+ "handle"_a, "refresh"_a = true)
+
+ .def("refresh", &IOrganizer::refresh, "save_changes"_a = true)
+ .def("managedGame", &IOrganizer::managedGame,
+ py::return_value_policy::reference)
+
+ .def("onAboutToRun",
+ py::overload_cast<std::function<bool(QString const&, const QDir&,
+ const QString&)> const&>(
+ &IOrganizer::onAboutToRun),
+ "callback"_a)
+ .def("onAboutToRun",
+ py::overload_cast<std::function<bool(QString const&)> const&>(
+ &IOrganizer::onAboutToRun),
+ "callback"_a)
+ .def("onFinishedRun", &IOrganizer::onFinishedRun, "callback"_a)
+ .def("onUserInterfaceInitialized", &IOrganizer::onUserInterfaceInitialized,
+ "callback"_a)
+ .def("onNextRefresh", &IOrganizer::onNextRefresh, "callback"_a,
+ "immediate_if_possible"_a = true)
+ .def("onProfileCreated", &IOrganizer::onProfileCreated, "callback"_a)
+ .def("onProfileRenamed", &IOrganizer::onProfileRenamed, "callback"_a)
+ .def("onProfileRemoved", &IOrganizer::onProfileRemoved, "callback"_a)
+ .def("onProfileChanged", &IOrganizer::onProfileChanged, "callback"_a)
+
+ .def("onPluginSettingChanged", &IOrganizer::onPluginSettingChanged,
+ "callback"_a)
+ .def(
+ "onPluginEnabled",
+ [](IOrganizer* o, std::function<void(const IPlugin*)> const& func) {
+ o->onPluginEnabled(func);
+ },
+ "callback"_a)
+ .def(
+ "onPluginEnabled",
+ [](IOrganizer* o, QString const& name,
+ std::function<void()> const& func) {
+ o->onPluginEnabled(name, func);
+ },
+ "name"_a, "callback"_a)
+ .def(
+ "onPluginDisabled",
+ [](IOrganizer* o, std::function<void(const IPlugin*)> const& func) {
+ o->onPluginDisabled(func);
+ },
+ "callback"_a)
+ .def(
+ "onPluginDisabled",
+ [](IOrganizer* o, QString const& name,
+ std::function<void()> const& func) {
+ o->onPluginDisabled(name, func);
+ },
+ "name"_a, "callback"_a)
+
+ // DEPRECATED:
+ .def(
+ "getMod",
+ [](IOrganizer* o, QString const& name) {
+ mo2::python::show_deprecation_warning(
+ "getMod", "IOrganizer::getMod(str) is deprecated, use "
+ "IModList::getMod(str) instead.");
+ return o->modList()->getMod(name);
+ },
+ py::return_value_policy::reference, "name"_a)
+ .def(
+ "removeMod",
+ [](IOrganizer* o, IModInterface* mod) {
+ mo2::python::show_deprecation_warning(
+ "removeMod",
+ "IOrganizer::removeMod(IModInterface) is deprecated, use "
+ "IModList::removeMod(IModInterface) instead.");
+ return o->modList()->removeMod(mod);
+ },
+ "mod"_a)
+ .def("modsSortedByProfilePriority",
+ [](IOrganizer* o) {
+ mo2::python::show_deprecation_warning(
+ "modsSortedByProfilePriority",
+ "IOrganizer::modsSortedByProfilePriority() is deprecated, use "
+ "IModList::allModsByProfilePriority() instead.");
+ return o->modList()->allModsByProfilePriority();
+ })
+ .def(
+ "refreshModList",
+ [](IOrganizer* o, bool s) {
+ mo2::python::show_deprecation_warning(
+ "refreshModList",
+ "IOrganizer::refreshModList(bool) is deprecated, use "
+ "IOrganizer::refresh(bool) instead.");
+ o->refresh(s);
+ },
+ "save_changes"_a = true)
+ .def(
+ "onModInstalled",
+ [](IOrganizer* organizer,
+ const std::function<void(QString const&)>& func) {
+ mo2::python::show_deprecation_warning(
+ "onModInstalled",
+ "IOrganizer::onModInstalled(Callable[[str], None]) is "
+ "deprecated, "
+ "use IModList::onModInstalled(Callable[[IModInterface], None]) "
+ "instead.");
+ return organizer->modList()->onModInstalled(
+ [func](MOBase::IModInterface* m) {
+ func(m->name());
+ });
+ ;
+ },
+ "callback"_a)
+
+ .def_static("getPluginDataPath", &IOrganizer::getPluginDataPath);
+ }
+
+ void add_iinstance_manager_classes(py::module_ m)
+ {
+ py::class_<IInstance, std::shared_ptr<IInstance>>(m, "IInstance")
+ .def("displayName", &IInstance::displayName)
+ .def("gameName", &IInstance::gameName)
+ .def("gameDirectory", &IInstance::gameDirectory)
+ .def("isPortable", &IInstance::isPortable);
+
+ py::class_<IInstanceManager>(m, "IInstanceManager")
+ .def("currentInstance", &IInstanceManager::currentInstance)
+ .def("globalInstancePaths", &IInstanceManager::globalInstancePaths)
+ .def("getGlobalInstance", &IInstanceManager::getGlobalInstance);
+ }
+
+ void add_idownload_manager_classes(py::module_ m)
+ {
+ py::class_<IDownloadManager>(m, "IDownloadManager")
+ .def("startDownloadURLs", &IDownloadManager::startDownloadURLs, "urls"_a)
+ .def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile,
+ "mod_id"_a, "file_id"_a)
+ .def("startDownloadNexusFileForGame",
+ &IDownloadManager::startDownloadNexusFileForGame, "game_name"_a,
+ "mod_id"_a, "file_id"_a)
+ .def("downloadPath", &IDownloadManager::downloadPath, "id"_a)
+ .def("onDownloadComplete", &IDownloadManager::onDownloadComplete,
+ "callback"_a)
+ .def("onDownloadPaused", &IDownloadManager::onDownloadPaused, "callback"_a)
+ .def("onDownloadFailed", &IDownloadManager::onDownloadFailed, "callback"_a)
+ .def("onDownloadRemoved", &IDownloadManager::onDownloadRemoved,
+ "callback"_a);
+ }
+
+ void add_iinstallation_manager_classes(py::module_ m)
+ {
+ // add this here to get proper typing
+ py::enum_<IPluginInstaller::EInstallResult>(m, "InstallResult")
+ .value("SUCCESS", IPluginInstaller::RESULT_SUCCESS)
+ .value("FAILED", IPluginInstaller::RESULT_FAILED)
+ .value("CANCELED", IPluginInstaller::RESULT_CANCELED)
+ .value("MANUAL_REQUESTED", IPluginInstaller::RESULT_MANUALREQUESTED)
+ .value("NOT_ATTEMPTED", IPluginInstaller::RESULT_NOTATTEMPTED);
+
+ py::class_<IInstallationManager>(m, "IInstallationManager")
+ .def("getSupportedExtensions",
+ &IInstallationManager::getSupportedExtensions)
+ .def("extractFile", &IInstallationManager::extractFile, "entry"_a,
+ "silent"_a = false)
+ .def("extractFiles", &IInstallationManager::extractFiles, "entries"_a,
+ "silent"_a = false)
+ .def("createFile", &IInstallationManager::createFile, "entry"_a)
+
+ // return a tuple to get back the mod name and the mod ID
+ .def(
+ "installArchive",
+ [](IInstallationManager* m, GuessedValue<QString> modName,
+ FileWrapper archive, int modId) {
+ auto result = m->installArchive(modName, archive, modId);
+ return std::make_tuple(result, static_cast<QString>(modName),
+ modId);
+ },
+ "mod_name"_a, "archive"_a, "mod_id"_a = 0);
+ }
+
+ void add_basic_bindings(py::module_ m)
+ {
+ add_versioninfo_classes(m);
+ add_executable_classes(m);
+ add_guessedstring_classes(m);
+
+ add_ifiletree_bindings(m);
+
+ add_modinterface_classes(m);
+ add_modrepository_classes(m);
+
+ py::class_<PluginSetting>(m, "PluginSetting")
+ .def(py::init<const QString&, const QString&, const QVariant&>(), "key"_a,
+ "description"_a, "default_value"_a)
+ .def_readwrite("key", &PluginSetting::key)
+ .def_readwrite("description", &PluginSetting::description)
+ .def_readwrite("default_value", &PluginSetting::defaultValue);
+
+ py::class_<PluginRequirementFactory>(m, "PluginRequirementFactory")
+ // pluginDependency
+ .def_static("pluginDependency",
+ py::overload_cast<QStringList const&>(
+ &PluginRequirementFactory::pluginDependency),
+ "plugins"_a)
+ .def_static("pluginDependency",
+ py::overload_cast<QString const&>(
+ &PluginRequirementFactory::pluginDependency),
+ "plugin"_a)
+ // gameDependency
+ .def_static("gameDependency",
+ py::overload_cast<QStringList const&>(
+ &PluginRequirementFactory::gameDependency),
+ "games"_a)
+ .def_static("gameDependency",
+ py::overload_cast<QString const&>(
+ &PluginRequirementFactory::gameDependency),
+ "game"_a)
+ // diagnose
+ .def_static("diagnose", &PluginRequirementFactory::diagnose, "diagnose"_a)
+ // basic
+ .def_static("basic", &PluginRequirementFactory::basic, "checker"_a,
+ "description"_a);
+
+ py::class_<Mapping>(m, "Mapping")
+ .def(py::init<>())
+ .def(py::init([](QString src, QString dst, bool dir, bool crt) -> Mapping {
+ return {src, dst, dir, crt};
+ }),
+ "source"_a, "destination"_a, "is_directory"_a,
+ "create_target"_a = false)
+ .def_readwrite("source", &Mapping::source)
+ .def_readwrite("destination", &Mapping::destination)
+ .def_readwrite("isDirectory", &Mapping::isDirectory)
+ .def_readwrite("createTarget", &Mapping::createTarget)
+ .def("__str__", [](Mapping const& m) {
+ return std::format(L"Mapping({}, {}, {}, {})", m.source.toStdWString(),
+ m.destination.toStdWString(), m.isDirectory,
+ m.createTarget);
+ });
+
+ // must be done BEFORE imodlist because there is a default argument to a
+ // IProfile* in the modlist class
+ py::class_<IProfile, std::shared_ptr<IProfile>>(m, "IProfile")
+ .def("name", &IProfile::name)
+ .def("absolutePath", &IProfile::absolutePath)
+ .def("localSavesEnabled", &IProfile::localSavesEnabled)
+ .def("localSettingsEnabled", &IProfile::localSettingsEnabled)
+ .def("invalidationActive",
+ [](const IProfile* p) {
+ bool supported;
+ bool active = p->invalidationActive(&supported);
+ return std::make_tuple(active, supported);
+ })
+ .def("absoluteIniFilePath", &IProfile::absoluteIniFilePath, "inifile"_a);
+
+ add_ipluginlist_classes(m);
+ add_imodlist_classes(m);
+ add_iinstance_manager_classes(m);
+ add_idownload_manager_classes(m);
+ add_iinstallation_manager_classes(m);
+ add_iorganizer_classes(m);
+ }
+
+} // namespace mo2::python
diff --git a/libs/plugin_python/src/mobase/wrappers/game_features.cpp b/libs/plugin_python/src/mobase/wrappers/game_features.cpp
new file mode 100644
index 0000000..655334b
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/game_features.cpp
@@ -0,0 +1,422 @@
+#include "wrappers.h"
+
+#include <tuple>
+
+#include "../pybind11_all.h"
+
+#include <uibase/ipluginlist.h>
+#include <uibase/isavegameinfowidget.h>
+
+#include <uibase/game_features/bsainvalidation.h>
+#include <uibase/game_features/dataarchives.h>
+#include <uibase/game_features/gameplugins.h>
+#include <uibase/game_features/igamefeatures.h>
+#include <uibase/game_features/localsavegames.h>
+#include <uibase/game_features/moddatachecker.h>
+#include <uibase/game_features/moddatacontent.h>
+#include <uibase/game_features/savegameinfo.h>
+#include <uibase/game_features/scriptextender.h>
+#include <uibase/game_features/unmanagedmods.h>
+
+#include "pyfiletree.h"
+
+namespace py = pybind11;
+
+using namespace MOBase;
+using namespace pybind11::literals;
+
+namespace mo2::python {
+
+ class PyBSAInvalidation : public BSAInvalidation {
+ public:
+ bool isInvalidationBSA(const QString& bsaName) override
+ {
+ PYBIND11_OVERRIDE_PURE(bool, BSAInvalidation, isInvalidationBSA, bsaName);
+ }
+ void deactivate(MOBase::IProfile* profile) override
+ {
+ PYBIND11_OVERRIDE_PURE(void, BSAInvalidation, deactivate, profile);
+ }
+ void activate(MOBase::IProfile* profile) override
+ {
+ PYBIND11_OVERRIDE_PURE(void, BSAInvalidation, activate, profile);
+ }
+ bool prepareProfile(MOBase::IProfile* profile) override
+ {
+ PYBIND11_OVERRIDE_PURE(bool, BSAInvalidation, prepareProfile, profile);
+ }
+ };
+
+ class PyDataArchives : public DataArchives {
+ public:
+ QStringList vanillaArchives() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QStringList, DataArchives, vanillaArchives, );
+ }
+ QStringList archives(const MOBase::IProfile* profile) const override
+ {
+ PYBIND11_OVERRIDE_PURE(QStringList, DataArchives, archives, profile);
+ }
+ void addArchive(MOBase::IProfile* profile, int index,
+ const QString& archiveName) override
+ {
+ PYBIND11_OVERRIDE_PURE(void, DataArchives, addArchive, profile, index,
+ archiveName);
+ }
+ void removeArchive(MOBase::IProfile* profile,
+ const QString& archiveName) override
+ {
+ PYBIND11_OVERRIDE_PURE(void, DataArchives, removeArchive, profile,
+ archiveName);
+ }
+ };
+
+ class PyGamePlugins : public GamePlugins {
+ public:
+ void writePluginLists(const MOBase::IPluginList* pluginList) override
+ {
+ PYBIND11_OVERRIDE_PURE(void, GamePlugins, writePluginLists, pluginList);
+ }
+ void readPluginLists(MOBase::IPluginList* pluginList) override
+ {
+ // TODO: cannot update plugin list or create one from Python so this is
+ // useless
+ PYBIND11_OVERRIDE_PURE(void, GamePlugins, readPluginLists, pluginList);
+ }
+ QStringList getLoadOrder() override
+ {
+ PYBIND11_OVERRIDE_PURE(QStringList, GamePlugins, getLoadOrder, );
+ }
+ bool lightPluginsAreSupported() override
+ {
+ PYBIND11_OVERRIDE(bool, GamePlugins, lightPluginsAreSupported, );
+ }
+ bool mediumPluginsAreSupported() override
+ {
+ PYBIND11_OVERRIDE(bool, GamePlugins, mediumPluginsAreSupported, );
+ }
+ bool blueprintPluginsAreSupported() override
+ {
+ PYBIND11_OVERRIDE(bool, GamePlugins, blueprintPluginsAreSupported, );
+ }
+ };
+
+ class PyLocalSavegames : public LocalSavegames {
+ public:
+ MappingType mappings(const QDir& profileSaveDir) const override
+ {
+ PYBIND11_OVERRIDE_PURE(MappingType, LocalSavegames, mappings,
+ profileSaveDir);
+ }
+ bool prepareProfile(MOBase::IProfile* profile) override
+ {
+ PYBIND11_OVERRIDE_PURE(bool, LocalSavegames, prepareProfile, profile);
+ }
+ };
+
+ class PyModDataChecker : public ModDataChecker {
+ public:
+ CheckReturn
+ dataLooksValid(std::shared_ptr<const MOBase::IFileTree> fileTree) const override
+ {
+ PYBIND11_OVERRIDE_PURE(CheckReturn, ModDataChecker, dataLooksValid,
+ fileTree);
+ }
+
+ std::shared_ptr<MOBase::IFileTree>
+ fix(std::shared_ptr<MOBase::IFileTree> fileTree) const override
+ {
+ PYBIND11_OVERRIDE(std::shared_ptr<MOBase::IFileTree>, ModDataChecker, fix,
+ fileTree);
+ }
+ };
+
+ class PyModDataContent : public ModDataContent {
+ public:
+ std::vector<Content> getAllContents() const override
+ {
+ PYBIND11_OVERRIDE_PURE(std::vector<Content>, ModDataContent,
+ getAllContents, );
+ ;
+ }
+ std::vector<int>
+ getContentsFor(std::shared_ptr<const MOBase::IFileTree> fileTree) const override
+ {
+ PYBIND11_OVERRIDE_PURE(std::vector<int>, ModDataContent, getContentsFor,
+ fileTree);
+ }
+ };
+
+ class PySaveGameInfo : public SaveGameInfo {
+ public:
+ MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const override
+ {
+ PYBIND11_OVERRIDE_PURE(MissingAssets, SaveGameInfo, getMissingAssets,
+ &save);
+ }
+ ISaveGameInfoWidget* getSaveGameWidget(QWidget* parent = 0) const override
+ {
+ PYBIND11_OVERRIDE_PURE(ISaveGameInfoWidget*, SaveGameInfo,
+ getSaveGameWidget, parent);
+ }
+ };
+
+ class PyScriptExtender : public ScriptExtender {
+ public:
+ QString BinaryName() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, binaryName, );
+ }
+
+ QString PluginPath() const override
+ {
+ PYBIND11_OVERRIDE_PURE(DirectoryWrapper, ScriptExtender, pluginPath, );
+ }
+
+ QString loaderName() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, loaderName, );
+ }
+
+ QString loaderPath() const override
+ {
+ PYBIND11_OVERRIDE_PURE(FileWrapper, ScriptExtender, loaderPath, );
+ }
+
+ QString savegameExtension() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, savegameExtension, );
+ }
+
+ bool isInstalled() const override
+ {
+ PYBIND11_OVERRIDE_PURE(bool, ScriptExtender, isInstalled, );
+ }
+
+ QString getExtenderVersion() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, getExtenderVersion, );
+ }
+
+ WORD getArch() const override
+ {
+ PYBIND11_OVERRIDE_PURE(WORD, ScriptExtender, getArch, );
+ }
+ };
+
+ class PyUnmanagedMods : public UnmanagedMods {
+ public:
+ QStringList mods(bool onlyOfficial) const override
+ {
+ PYBIND11_OVERRIDE_PURE(QStringList, UnmanagedMods, mods, onlyOfficial);
+ }
+ QString displayName(const QString& modName) const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, UnmanagedMods, displayName, modName);
+ }
+ QFileInfo referenceFile(const QString& modName) const override
+ {
+ PYBIND11_OVERRIDE_PURE(FileWrapper, UnmanagedMods, referenceFile, modName);
+ }
+ QStringList secondaryFiles(const QString& modName) const override
+ {
+ return toQStringList([&] {
+ PYBIND11_OVERRIDE_PURE(QList<FileWrapper>, UnmanagedMods,
+ secondaryFiles, modName);
+ }());
+ }
+ };
+
+ void add_game_feature_bindings(pybind11::module_ m)
+ {
+ // this is just to allow accepting GameFeature in function, we do not expose
+ // anything from game feature to Python since typeInfo() is useless in Python
+ //
+ py::class_<GameFeature, std::shared_ptr<GameFeature>>(m, "GameFeature");
+
+ // BSAInvalidation
+
+ py::class_<BSAInvalidation, GameFeature, PyBSAInvalidation,
+ std::shared_ptr<BSAInvalidation>>(m, "BSAInvalidation")
+ .def(py::init<>())
+ .def("isInvalidationBSA", &BSAInvalidation::isInvalidationBSA, "name"_a)
+ .def("deactivate", &BSAInvalidation::deactivate, "profile"_a)
+ .def("activate", &BSAInvalidation::activate, "profile"_a);
+
+ // DataArchives
+
+ py::class_<DataArchives, GameFeature, PyDataArchives,
+ std::shared_ptr<DataArchives>>(m, "DataArchives")
+ .def(py::init<>())
+ .def("vanillaArchives", &DataArchives::vanillaArchives)
+ .def("archives", &DataArchives::archives, "profile"_a)
+ .def("addArchive", &DataArchives::addArchive, "profile"_a, "index"_a,
+ "name"_a)
+ .def("removeArchive", &DataArchives::removeArchive, "profile"_a, "name"_a);
+
+ // GamePlugins
+
+ py::class_<GamePlugins, GameFeature, PyGamePlugins,
+ std::shared_ptr<GamePlugins>>(m, "GamePlugins")
+ .def(py::init<>())
+ .def("writePluginLists", &GamePlugins::writePluginLists, "plugin_list"_a)
+ .def("readPluginLists", &GamePlugins::readPluginLists, "plugin_list"_a)
+ .def("getLoadOrder", &GamePlugins::getLoadOrder)
+ .def("lightPluginsAreSupported", &GamePlugins::lightPluginsAreSupported)
+ .def("mediumPluginsAreSupported", &GamePlugins::mediumPluginsAreSupported)
+ .def("blueprintPluginsAreSupported",
+ &GamePlugins::blueprintPluginsAreSupported);
+
+ // LocalSavegames
+
+ py::class_<LocalSavegames, GameFeature, PyLocalSavegames,
+ std::shared_ptr<LocalSavegames>>(m, "LocalSavegames")
+ .def(py::init<>())
+ .def("mappings", &LocalSavegames::mappings, "profile_save_dir"_a)
+ .def("prepareProfile", &LocalSavegames::prepareProfile, "profile"_a);
+
+ // ModDataChecker
+
+ py::class_<ModDataChecker, GameFeature, PyModDataChecker,
+ std::shared_ptr<ModDataChecker>>
+ pyModDataChecker(m, "ModDataChecker");
+
+ py::enum_<ModDataChecker::CheckReturn>(pyModDataChecker, "CheckReturn")
+ .value("INVALID", ModDataChecker::CheckReturn::INVALID)
+ .value("FIXABLE", ModDataChecker::CheckReturn::FIXABLE)
+ .value("VALID", ModDataChecker::CheckReturn::VALID)
+ .export_values();
+
+ pyModDataChecker.def(py::init<>())
+ .def("dataLooksValid", &ModDataChecker::dataLooksValid, "filetree"_a)
+ .def("fix", &ModDataChecker::fix, "filetree"_a);
+
+ // ModDataContent
+ py::class_<ModDataContent, GameFeature, PyModDataContent,
+ std::shared_ptr<ModDataContent>>
+ pyModDataContent(m, "ModDataContent");
+
+ py::class_<ModDataContent::Content>(pyModDataContent, "Content")
+ .def(py::init<int, QString, QString, bool>(), "id"_a, "name"_a, "icon"_a,
+ "filter_only"_a = false)
+ .def_property_readonly("id", &ModDataContent::Content::id)
+ .def_property_readonly("name", &ModDataContent::Content::name)
+ .def_property_readonly("icon", &ModDataContent::Content::icon)
+ .def("isOnlyForFilter", &ModDataContent::Content::isOnlyForFilter);
+
+ pyModDataContent.def(py::init<>())
+ .def("getAllContents", &ModDataContent::getAllContents)
+ .def("getContentsFor", &ModDataContent::getContentsFor, "filetree"_a);
+
+ // SaveGameInfo
+
+ py::class_<SaveGameInfo, GameFeature, PySaveGameInfo,
+ std::shared_ptr<SaveGameInfo>>(m, "SaveGameInfo")
+ .def(py::init<>())
+ .def("getMissingAssets", &SaveGameInfo::getMissingAssets, "save"_a)
+ .def("getSaveGameWidget", &SaveGameInfo::getSaveGameWidget,
+ py::return_value_policy::reference, "parent"_a);
+
+ // ScriptExtender
+
+ py::class_<ScriptExtender, GameFeature, PyScriptExtender,
+ std::shared_ptr<ScriptExtender>>(m, "ScriptExtender")
+ .def(py::init<>())
+ .def("binaryName", &ScriptExtender::BinaryName)
+ .def("pluginPath", wrap_return_for_directory(&ScriptExtender::PluginPath))
+ .def("loaderName", &ScriptExtender::loaderName)
+ .def("loaderPath", wrap_return_for_filepath(&ScriptExtender::loaderPath))
+ .def("savegameExtension", &ScriptExtender::savegameExtension)
+ .def("isInstalled", &ScriptExtender::isInstalled)
+ .def("getExtenderVersion", &ScriptExtender::getExtenderVersion)
+ .def("getArch", &ScriptExtender::getArch);
+
+ // UnmanagedMods
+
+ py::class_<UnmanagedMods, GameFeature, PyUnmanagedMods,
+ std::shared_ptr<UnmanagedMods>>(m, "UnmanagedMods")
+ .def(py::init<>())
+ .def("mods", &UnmanagedMods::mods, "official_only"_a)
+ .def("displayName", &UnmanagedMods::displayName, "mod_name"_a)
+ .def("referenceFile",
+ wrap_return_for_filepath(&UnmanagedMods::referenceFile), "mod_name"_a)
+ .def(
+ "secondaryFiles",
+ [](UnmanagedMods* m, const QString& modName) -> QList<FileWrapper> {
+ auto result = m->secondaryFiles(modName);
+ return {result.begin(), result.end()};
+ },
+ "mod_name"_a);
+ }
+
+ void add_igamefeatures_classes(py::module_ m)
+ {
+ py::class_<IGameFeatures>(m, "IGameFeatures")
+ .def("registerFeature",
+ py::overload_cast<QStringList const&, std::shared_ptr<GameFeature>,
+ int, bool>(&IGameFeatures::registerFeature),
+ "games"_a, "feature"_a, "priority"_a, "replace"_a = false)
+ .def("registerFeature",
+ py::overload_cast<MOBase::IPluginGame*, std::shared_ptr<GameFeature>,
+ int, bool>(&IGameFeatures::registerFeature),
+ "game"_a, "feature"_a, "priority"_a, "replace"_a = false)
+ .def("registerFeature",
+ py::overload_cast<std::shared_ptr<GameFeature>, int, bool>(
+ &IGameFeatures::registerFeature),
+ "feature"_a, "priority"_a, "replace"_a = false)
+ .def("unregisterFeature", &IGameFeatures::unregisterFeature, "feature"_a)
+ .def("unregisterFeatures", &unregister_feature, "feature_type"_a)
+ .def("gameFeature", &extract_feature, "feature_type"_a,
+ py ::return_value_policy::reference);
+ }
+
+} // namespace mo2::python
+
+namespace mo2::python {
+
+ class GameFeaturesHelper {
+ template <class F, std::size_t... Is>
+ static void helper(F&& f, std::index_sequence<Is...>)
+ {
+ (f(static_cast<
+ std::tuple_element_t<Is, MOBase::details::BaseGameFeaturesP>>(
+ nullptr)),
+ ...);
+ }
+
+ public:
+ // apply the function f on a null-pointer of type Feature* for each game
+ // feature
+ template <class F>
+ static void apply(F&& f)
+ {
+ helper(f, std::make_index_sequence<
+ std::tuple_size_v<MOBase::details::BaseGameFeaturesP>>{});
+ }
+ };
+
+ pybind11::object extract_feature(IGameFeatures const& gameFeatures,
+ pybind11::object type)
+ {
+ py::object py_feature = py::none();
+ GameFeaturesHelper::apply([&]<class Feature>(Feature*) {
+ if (py::type::of<Feature>().is(type)) {
+ py_feature = py::cast(gameFeatures.gameFeature<Feature>(),
+ py::return_value_policy::reference);
+ }
+ });
+ return py_feature;
+ }
+
+ int unregister_feature(MOBase::IGameFeatures& gameFeatures, pybind11::object type)
+ {
+ int count = 0;
+ GameFeaturesHelper::apply([&]<class Feature>(Feature*) {
+ if (py::type::of<Feature>().is(type)) {
+ count = gameFeatures.unregisterFeatures<Feature>();
+ }
+ });
+ return count;
+ }
+
+} // namespace mo2::python
diff --git a/libs/plugin_python/src/mobase/wrappers/known_folders.h b/libs/plugin_python/src/mobase/wrappers/known_folders.h
new file mode 100644
index 0000000..f74238b
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/known_folders.h
@@ -0,0 +1,155 @@
+#include <KnownFolders.h>
+
+namespace mo2::python {
+
+ struct KnownFolder {
+ const char* name;
+ KNOWNFOLDERID guid;
+ };
+
+ const std::array<KnownFolder, 142> KNOWN_FOLDERS{{
+ {"AccountPictures", FOLDERID_AccountPictures},
+ {"AddNewPrograms", FOLDERID_AddNewPrograms},
+ {"AdminTools", FOLDERID_AdminTools},
+ {"AllAppMods", FOLDERID_AllAppMods},
+ {"AppCaptures", FOLDERID_AppCaptures},
+ {"AppDataDesktop", FOLDERID_AppDataDesktop},
+ {"AppDataDocuments", FOLDERID_AppDataDocuments},
+ {"AppDataFavorites", FOLDERID_AppDataFavorites},
+ {"AppDataProgramData", FOLDERID_AppDataProgramData},
+ {"ApplicationShortcuts", FOLDERID_ApplicationShortcuts},
+ {"AppsFolder", FOLDERID_AppsFolder},
+ {"AppUpdates", FOLDERID_AppUpdates},
+ {"CameraRoll", FOLDERID_CameraRoll},
+ {"CameraRollLibrary", FOLDERID_CameraRollLibrary},
+ {"CDBurning", FOLDERID_CDBurning},
+ {"ChangeRemovePrograms", FOLDERID_ChangeRemovePrograms},
+ {"CommonAdminTools", FOLDERID_CommonAdminTools},
+ {"CommonOEMLinks", FOLDERID_CommonOEMLinks},
+ {"CommonPrograms", FOLDERID_CommonPrograms},
+ {"CommonStartMenu", FOLDERID_CommonStartMenu},
+ {"CommonStartMenuPlaces", FOLDERID_CommonStartMenuPlaces},
+ {"CommonStartup", FOLDERID_CommonStartup},
+ {"CommonTemplates", FOLDERID_CommonTemplates},
+ {"ComputerFolder", FOLDERID_ComputerFolder},
+ {"ConflictFolder", FOLDERID_ConflictFolder},
+ {"ConnectionsFolder", FOLDERID_ConnectionsFolder},
+ {"Contacts", FOLDERID_Contacts},
+ {"ControlPanelFolder", FOLDERID_ControlPanelFolder},
+ {"Cookies", FOLDERID_Cookies},
+ {"CurrentAppMods", FOLDERID_CurrentAppMods},
+ {"Desktop", FOLDERID_Desktop},
+ {"DevelopmentFiles", FOLDERID_DevelopmentFiles},
+ {"Device", FOLDERID_Device},
+ {"DeviceMetadataStore", FOLDERID_DeviceMetadataStore},
+ {"Documents", FOLDERID_Documents},
+ {"DocumentsLibrary", FOLDERID_DocumentsLibrary},
+ {"Downloads", FOLDERID_Downloads},
+ {"Favorites", FOLDERID_Favorites},
+ {"Fonts", FOLDERID_Fonts},
+ {"Games", FOLDERID_Games},
+ {"GameTasks", FOLDERID_GameTasks},
+ {"History", FOLDERID_History},
+ {"HomeGroup", FOLDERID_HomeGroup},
+ {"HomeGroupCurrentUser", FOLDERID_HomeGroupCurrentUser},
+ {"ImplicitAppShortcuts", FOLDERID_ImplicitAppShortcuts},
+ {"InternetCache", FOLDERID_InternetCache},
+ {"InternetFolder", FOLDERID_InternetFolder},
+ {"Libraries", FOLDERID_Libraries},
+ {"Links", FOLDERID_Links},
+ {"LocalAppData", FOLDERID_LocalAppData},
+ {"LocalAppDataLow", FOLDERID_LocalAppDataLow},
+ {"LocalDocuments", FOLDERID_LocalDocuments},
+ {"LocalDownloads", FOLDERID_LocalDownloads},
+ {"LocalizedResourcesDir", FOLDERID_LocalizedResourcesDir},
+ {"LocalMusic", FOLDERID_LocalMusic},
+ {"LocalPictures", FOLDERID_LocalPictures},
+ {"LocalStorage", FOLDERID_LocalStorage},
+ {"LocalVideos", FOLDERID_LocalVideos},
+ {"Music", FOLDERID_Music},
+ {"MusicLibrary", FOLDERID_MusicLibrary},
+ {"NetHood", FOLDERID_NetHood},
+ {"NetworkFolder", FOLDERID_NetworkFolder},
+ {"Objects3D", FOLDERID_Objects3D},
+ {"OneDrive", FOLDERID_OneDrive},
+ {"OriginalImages", FOLDERID_OriginalImages},
+ {"PhotoAlbums", FOLDERID_PhotoAlbums},
+ {"Pictures", FOLDERID_Pictures},
+ {"PicturesLibrary", FOLDERID_PicturesLibrary},
+ {"Playlists", FOLDERID_Playlists},
+ {"PrintersFolder", FOLDERID_PrintersFolder},
+ {"PrintHood", FOLDERID_PrintHood},
+ {"Profile", FOLDERID_Profile},
+ {"ProgramData", FOLDERID_ProgramData},
+ {"ProgramFiles", FOLDERID_ProgramFiles},
+ {"ProgramFilesCommon", FOLDERID_ProgramFilesCommon},
+ {"ProgramFilesCommonX64", FOLDERID_ProgramFilesCommonX64},
+ {"ProgramFilesCommonX86", FOLDERID_ProgramFilesCommonX86},
+ {"ProgramFilesX64", FOLDERID_ProgramFilesX64},
+ {"ProgramFilesX86", FOLDERID_ProgramFilesX86},
+ {"Programs", FOLDERID_Programs},
+ {"Public", FOLDERID_Public},
+ {"PublicDesktop", FOLDERID_PublicDesktop},
+ {"PublicDocuments", FOLDERID_PublicDocuments},
+ {"PublicDownloads", FOLDERID_PublicDownloads},
+ {"PublicGameTasks", FOLDERID_PublicGameTasks},
+ {"PublicLibraries", FOLDERID_PublicLibraries},
+ {"PublicMusic", FOLDERID_PublicMusic},
+ {"PublicPictures", FOLDERID_PublicPictures},
+ {"PublicRingtones", FOLDERID_PublicRingtones},
+ {"PublicUserTiles", FOLDERID_PublicUserTiles},
+ {"PublicVideos", FOLDERID_PublicVideos},
+ {"QuickLaunch", FOLDERID_QuickLaunch},
+ {"Recent", FOLDERID_Recent},
+ {"RecordedCalls", FOLDERID_RecordedCalls},
+ {"RecordedTVLibrary", FOLDERID_RecordedTVLibrary},
+ {"RecycleBinFolder", FOLDERID_RecycleBinFolder},
+ {"ResourceDir", FOLDERID_ResourceDir},
+ {"RetailDemo", FOLDERID_RetailDemo},
+ {"Ringtones", FOLDERID_Ringtones},
+ {"RoamedTileImages", FOLDERID_RoamedTileImages},
+ {"RoamingAppData", FOLDERID_RoamingAppData},
+ {"RoamingTiles", FOLDERID_RoamingTiles},
+ {"SampleMusic", FOLDERID_SampleMusic},
+ {"SamplePictures", FOLDERID_SamplePictures},
+ {"SamplePlaylists", FOLDERID_SamplePlaylists},
+ {"SampleVideos", FOLDERID_SampleVideos},
+ {"SavedGames", FOLDERID_SavedGames},
+ {"SavedPictures", FOLDERID_SavedPictures},
+ {"SavedPicturesLibrary", FOLDERID_SavedPicturesLibrary},
+ {"SavedSearches", FOLDERID_SavedSearches},
+ {"Screenshots", FOLDERID_Screenshots},
+ {"SEARCH_CSC", FOLDERID_SEARCH_CSC},
+ {"SEARCH_MAPI", FOLDERID_SEARCH_MAPI},
+ {"SearchHistory", FOLDERID_SearchHistory},
+ {"SearchHome", FOLDERID_SearchHome},
+ {"SearchTemplates", FOLDERID_SearchTemplates},
+ {"SendTo", FOLDERID_SendTo},
+ {"SidebarDefaultParts", FOLDERID_SidebarDefaultParts},
+ {"SidebarParts", FOLDERID_SidebarParts},
+ {"SkyDrive", FOLDERID_SkyDrive},
+ {"SkyDriveCameraRoll", FOLDERID_SkyDriveCameraRoll},
+ {"SkyDriveDocuments", FOLDERID_SkyDriveDocuments},
+ {"SkyDriveMusic", FOLDERID_SkyDriveMusic},
+ {"SkyDrivePictures", FOLDERID_SkyDrivePictures},
+ {"StartMenu", FOLDERID_StartMenu},
+ {"StartMenuAllPrograms", FOLDERID_StartMenuAllPrograms},
+ {"Startup", FOLDERID_Startup},
+ {"SyncManagerFolder", FOLDERID_SyncManagerFolder},
+ {"SyncResultsFolder", FOLDERID_SyncResultsFolder},
+ {"SyncSetupFolder", FOLDERID_SyncSetupFolder},
+ {"System", FOLDERID_System},
+ {"SystemX86", FOLDERID_SystemX86},
+ {"Templates", FOLDERID_Templates},
+ {"UserPinned", FOLDERID_UserPinned},
+ {"UserProfiles", FOLDERID_UserProfiles},
+ {"UserProgramFiles", FOLDERID_UserProgramFiles},
+ {"UserProgramFilesCommon", FOLDERID_UserProgramFilesCommon},
+ {"UsersFiles", FOLDERID_UsersFiles},
+ {"UsersLibraries", FOLDERID_UsersLibraries},
+ {"Videos", FOLDERID_Videos},
+ {"VideosLibrary", FOLDERID_VideosLibrary},
+ {"Windows", FOLDERID_Windows},
+ }};
+
+} // namespace mo2::python
diff --git a/libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp b/libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp
new file mode 100644
index 0000000..6dacf26
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/pyfiletree.cpp
@@ -0,0 +1,329 @@
+#include "pyfiletree.h"
+
+#include <tuple>
+#include <variant>
+
+#include "../pybind11_all.h"
+
+#include <uibase/ifiletree.h>
+#include <uibase/log.h>
+
+namespace py = pybind11;
+using namespace MOBase;
+
+namespace mo2::detail {
+
+ // filetree implementation for testing purpose
+ //
+ class PyFileTree : public IFileTree {
+ public:
+ using callback_t = std::function<bool(QString, bool)>;
+
+ PyFileTree(std::shared_ptr<const IFileTree> parent, QString name,
+ callback_t callback)
+ : FileTreeEntry(parent, name), IFileTree(), m_Callback(callback)
+ {
+ }
+
+ std::shared_ptr<FileTreeEntry> addFile(QString name, bool) override
+ {
+ if (m_Callback && !m_Callback(name, false)) {
+ throw UnsupportedOperationException("File rejected by callback.");
+ }
+ return IFileTree::addFile(name);
+ }
+
+ std::shared_ptr<IFileTree> addDirectory(QString name) override
+ {
+ if (m_Callback && !m_Callback(name, true)) {
+ throw UnsupportedOperationException("Directory rejected by callback.");
+ }
+ return IFileTree::addDirectory(name);
+ }
+
+ protected:
+ std::shared_ptr<IFileTree>
+ makeDirectory(std::shared_ptr<const IFileTree> parent,
+ QString name) const override
+ {
+ return std::make_shared<PyFileTree>(parent, name, m_Callback);
+ }
+
+ bool doPopulate([[maybe_unused]] std::shared_ptr<const IFileTree> parent,
+ std::vector<std::shared_ptr<FileTreeEntry>>&) const override
+ {
+ return true;
+ }
+ std::shared_ptr<IFileTree> doClone() const override
+ {
+ return std::make_shared<PyFileTree>(nullptr, name(), m_Callback);
+ }
+
+ private:
+ callback_t m_Callback;
+ };
+
+} // namespace mo2::detail
+
+#pragma optimize("", off)
+
+namespace pybind11 {
+ const void* polymorphic_type_hook<FileTreeEntry>::get(const FileTreeEntry* src,
+ const std::type_info*& type)
+ {
+ if (auto p = dynamic_cast<const IFileTree*>(src)) {
+ type = &typeid(IFileTree);
+ return p;
+ }
+ return src;
+ }
+} // namespace pybind11
+
+namespace mo2::python {
+
+ void add_ifiletree_bindings(pybind11::module_& m)
+ {
+ // FileTreeEntry class:
+ auto fileTreeEntryClass =
+ py::class_<FileTreeEntry, std::shared_ptr<FileTreeEntry>>(m,
+ "FileTreeEntry");
+
+ // IFileTree class:
+ auto iFileTreeClass =
+ py::class_<IFileTree, FileTreeEntry, std::shared_ptr<IFileTree>>(
+ m, "IFileTree", py::multiple_inheritance());
+
+ // this is FILE_OR_DIRECTORY but as a FileType since we kind of cheat for the
+ // exposure in Python and this help pybind11 creates proper typing
+
+ const auto FILE_OR_DIRECTORY = static_cast<FileTreeEntry::FileType>(
+ FileTreeEntry::FILE_OR_DIRECTORY.toInt());
+
+ // we do not use the enum directly, we will mostly bind the FileTypes
+ // (with an S)
+ py::enum_<FileTreeEntry::FileType>(fileTreeEntryClass, "FileTypes",
+ py::arithmetic{})
+ .value("FILE", FileTreeEntry::FileType::FILE)
+ .value("DIRECTORY", FileTreeEntry::FileType::DIRECTORY)
+ .value("FILE_OR_DIRECTORY", FILE_OR_DIRECTORY)
+ .export_values();
+
+ fileTreeEntryClass
+
+ .def("isFile", &FileTreeEntry::isFile)
+ .def("isDir", &FileTreeEntry::isDir)
+ .def("fileType", &FileTreeEntry::fileType)
+ .def("name", &FileTreeEntry::name)
+ .def("suffix", &FileTreeEntry::suffix)
+ .def(
+ "hasSuffix",
+ [](FileTreeEntry* entry, QStringList suffixes) {
+ return entry->hasSuffix(suffixes);
+ },
+ py::arg("suffixes"))
+ .def(
+ "hasSuffix",
+ [](FileTreeEntry* entry, QString suffix) {
+ return entry->hasSuffix(suffix);
+ },
+ py::arg("suffix"))
+ .def("parent", py::overload_cast<>(&FileTreeEntry::parent))
+ .def("path", &FileTreeEntry::path, py::arg("sep") = "\\")
+ .def("pathFrom", &FileTreeEntry::pathFrom, py::arg("tree"),
+ py::arg("sep") = "\\")
+
+ // Mutable operation:
+ .def("detach", &FileTreeEntry::detach)
+ .def("moveTo", &FileTreeEntry::moveTo, py::arg("tree"))
+
+ // Special methods:
+ .def("__eq__",
+ [](const FileTreeEntry* entry, QString other) {
+ return entry->compare(other) == 0;
+ })
+ .def("__eq__",
+ [](const FileTreeEntry* entry, std::shared_ptr<FileTreeEntry> other) {
+ return entry == other.get();
+ })
+
+ // Special methods for debug:
+ .def("__repr__", [](const FileTreeEntry* entry) {
+ return "FileTreeEntry(\"" + entry->name() + "\")";
+ });
+
+ py::enum_<IFileTree::InsertPolicy>(iFileTreeClass, "InsertPolicy")
+ .value("FAIL_IF_EXISTS", IFileTree::InsertPolicy::FAIL_IF_EXISTS)
+ .value("REPLACE", IFileTree::InsertPolicy::REPLACE)
+ .value("MERGE", IFileTree::InsertPolicy::MERGE)
+ .export_values();
+
+ py::enum_<IFileTree::WalkReturn>(iFileTreeClass, "WalkReturn")
+ .value("CONTINUE", IFileTree::WalkReturn::CONTINUE)
+ .value("STOP", IFileTree::WalkReturn::STOP)
+ .value("SKIP", IFileTree::WalkReturn::SKIP)
+ .export_values();
+
+ // in C++ this is not an inner enum due to the conditional feature of glob(),
+ // but in Python this makes more sense as a inner enum
+ py::enum_<GlobPatternType>(iFileTreeClass, "GlobPatternType")
+ .value("GLOB", GlobPatternType::GLOB)
+ .value("REGEX", GlobPatternType::REGEX)
+ .export_values();
+
+ // Non-mutable operations:
+ iFileTreeClass.def("exists",
+ py::overload_cast<QString, IFileTree::FileTypes>(
+ &IFileTree::exists, py::const_),
+ py::arg("path"), py::arg("type") = FILE_OR_DIRECTORY);
+ iFileTreeClass.def(
+ "find", py::overload_cast<QString, IFileTree::FileTypes>(&IFileTree::find),
+ py::arg("path"), py::arg("type") = FILE_OR_DIRECTORY);
+ iFileTreeClass.def("pathTo", &IFileTree::pathTo, py::arg("entry"),
+ py::arg("sep") = "\\");
+
+ iFileTreeClass.def(
+ "walk",
+ py::overload_cast<
+ std::function<IFileTree::WalkReturn(
+ QString const&, std::shared_ptr<const FileTreeEntry>)>,
+ QString>(&IFileTree::walk, py::const_),
+ py::arg("callback"), py::arg("sep") = "\\");
+
+ // the walk() and glob() generator version are free functions in C++ due to the
+ // conditional nature, but in Python, it makes more sense to have them as method
+ // of IFileTree directly
+
+ iFileTreeClass.def("walk", [](std::shared_ptr<const IFileTree> tree) {
+ return make_generator(walk(tree));
+ });
+
+ iFileTreeClass.def(
+ "glob",
+ [](std::shared_ptr<const IFileTree> tree, QString pattern,
+ GlobPatternType patternType) {
+ return make_generator(glob(tree, pattern, patternType));
+ },
+ py::arg("pattern"), py::arg("type") = GlobPatternType::GLOB);
+
+ // Kind-of-static operations:
+ iFileTreeClass.def("createOrphanTree", &IFileTree::createOrphanTree,
+ py::arg("name") = "");
+
+ // addFile() and addDirectory throws exception instead of returning null
+ // pointer in order to have better traces.
+ iFileTreeClass.def(
+ "addFile",
+ [](IFileTree* w, QString path, bool replaceIfExists) {
+ auto result = w->addFile(path, replaceIfExists);
+ if (result == nullptr) {
+ throw std::logic_error("addFile failed");
+ }
+ return result;
+ },
+ py::arg("path"), py::arg("replace_if_exists") = false);
+ iFileTreeClass.def(
+ "addDirectory",
+ [](IFileTree* w, QString path) {
+ auto result = w->addDirectory(path);
+ if (result == nullptr) {
+ throw std::logic_error("addDirectory failed");
+ }
+ return result;
+ },
+ py::arg("path"));
+
+ // Merge needs custom return types depending if the user wants overrides
+ // or not. A failure is translated into an exception for easier tracing
+ // and handling.
+ iFileTreeClass.def(
+ "merge",
+ [](IFileTree* p, std::shared_ptr<IFileTree> other, bool returnOverwrites)
+ -> std::variant<IFileTree::OverwritesType, std::size_t> {
+ IFileTree::OverwritesType overwrites;
+ auto result = p->merge(other, returnOverwrites ? &overwrites : nullptr);
+ if (result == IFileTree::MERGE_FAILED) {
+ throw std::logic_error("merge failed");
+ }
+ if (returnOverwrites) {
+ return {overwrites};
+ }
+ return {result};
+ },
+ py::arg("other"), py::arg("overwrites") = false);
+
+ // Insert and erase returns an iterator, which makes no sense in python,
+ // so we convert it to bool. Erase is also renamed "remove" since
+ // "erase" is very C++.
+ iFileTreeClass.def(
+ "insert",
+ [](IFileTree* p, std::shared_ptr<FileTreeEntry> entry,
+ IFileTree::InsertPolicy insertPolicy) {
+ return p->insert(entry, insertPolicy) == p->end();
+ },
+ py::arg("entry"),
+ py::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS);
+
+ iFileTreeClass.def(
+ "remove",
+ [](IFileTree* p, QString name) {
+ return p->erase(name).first != p->end();
+ },
+ py::arg("name"));
+ iFileTreeClass.def(
+ "remove",
+ [](IFileTree* p, std::shared_ptr<FileTreeEntry> entry) {
+ return p->erase(entry) != p->end();
+ },
+ py::arg("entry"));
+
+ iFileTreeClass.def("move", &IFileTree::move, py::arg("entry"), py::arg("path"),
+ py::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS);
+ iFileTreeClass.def(
+ "copy",
+ [](IFileTree* w, std::shared_ptr<FileTreeEntry> entry, QString path,
+ IFileTree::InsertPolicy insertPolicy) {
+ auto result = w->copy(entry, path, insertPolicy);
+ if (result == nullptr) {
+ throw std::logic_error("copy failed");
+ }
+ return result;
+ },
+ py::arg("entry"), py::arg("path") = "",
+ py::arg("insert_policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS);
+
+ iFileTreeClass.def("clear", &IFileTree::clear);
+ iFileTreeClass.def("removeAll", &IFileTree::removeAll, py::arg("names"));
+ iFileTreeClass.def("removeIf", &IFileTree::removeIf, py::arg("filter"));
+
+ // Special methods:
+ iFileTreeClass.def("__getitem__",
+ py::overload_cast<std::size_t>(&IFileTree::at));
+
+ iFileTreeClass.def("__iter__", [](IFileTree* tree) {
+ return py::make_iterator(*tree);
+ });
+ iFileTreeClass.def("__len__", &IFileTree::size);
+ iFileTreeClass.def(
+ "__bool__", +[](const IFileTree* tree) {
+ return !tree->empty();
+ });
+ iFileTreeClass.def(
+ "__repr__", +[](const IFileTree* entry) {
+ return "IFileTree(\"" + entry->name() + "\")";
+ });
+ }
+
+ void add_make_tree_function(pybind11::module_& m)
+ {
+ m.def(
+ "makeTree",
+ [](mo2::detail::PyFileTree::callback_t callback)
+ -> std::shared_ptr<IFileTree> {
+ return std::make_shared<mo2::detail::PyFileTree>(nullptr, "", callback);
+ },
+ py::arg("callback") = mo2::detail::PyFileTree::callback_t{});
+ }
+
+} // namespace mo2::python
+
+#pragma optimize("", on)
diff --git a/libs/plugin_python/src/mobase/wrappers/pyfiletree.h b/libs/plugin_python/src/mobase/wrappers/pyfiletree.h
new file mode 100644
index 0000000..0e94665
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/pyfiletree.h
@@ -0,0 +1,34 @@
+#ifndef MO2_PYTHON_FILETREE_H
+#define MO2_PYTHON_FILETREE_H
+
+#include "../pybind11_all.h"
+
+#include <uibase/ifiletree.h>
+
+namespace pybind11 {
+ template <>
+ struct polymorphic_type_hook<MOBase::FileTreeEntry> {
+ static const void* get(const MOBase::FileTreeEntry* src,
+ const std::type_info*& type);
+ };
+} // namespace pybind11
+
+namespace mo2::python {
+
+ /**
+ * @brief Add bindings for FileTreeEntry andIFileTree to the given module.
+ *
+ * @param mobase Module to add the bindings to.
+ */
+ void add_ifiletree_bindings(pybind11::module_& m);
+
+ /**
+ * @brief Add makeTree() function to the given module, useful for debugging.
+ *
+ * @param mobase Module to add the function to.
+ */
+ void add_make_tree_function(pybind11::module_& m);
+
+} // namespace mo2::python
+
+#endif
diff --git a/libs/plugin_python/src/mobase/wrappers/pyplugins.cpp b/libs/plugin_python/src/mobase/wrappers/pyplugins.cpp
new file mode 100644
index 0000000..bcce0df
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/pyplugins.cpp
@@ -0,0 +1,262 @@
+#include "wrappers.h"
+
+#include <tuple>
+
+#include "pyplugins.h"
+
+namespace py = pybind11;
+using namespace pybind11::literals;
+using namespace MOBase;
+
+namespace mo2::python {
+
+ // this one is kind of big so it has its own function
+ void add_iplugingame_bindings(pybind11::module_ m)
+ {
+ py::enum_<IPluginGame::LoadOrderMechanism>(m, "LoadOrderMechanism")
+ .value("None", IPluginGame::LoadOrderMechanism::None)
+ .value("FileTime", IPluginGame::LoadOrderMechanism::FileTime)
+ .value("PluginsTxt", IPluginGame::LoadOrderMechanism::PluginsTxt)
+
+ .value("NONE", IPluginGame::LoadOrderMechanism::None)
+ .value("FILE_TIME", IPluginGame::LoadOrderMechanism::FileTime)
+ .value("PLUGINS_TXT", IPluginGame::LoadOrderMechanism::PluginsTxt);
+
+ py::enum_<IPluginGame::SortMechanism>(m, "SortMechanism")
+ .value("NONE", IPluginGame::SortMechanism::NONE)
+ .value("MLOX", IPluginGame::SortMechanism::MLOX)
+ .value("BOSS", IPluginGame::SortMechanism::BOSS)
+ .value("LOOT", IPluginGame::SortMechanism::LOOT);
+
+ // this does not actually do the conversion, but might be convenient
+ // for accessing the names for enum bits
+ py::enum_<IPluginGame::ProfileSetting>(m, "ProfileSetting", py::arithmetic())
+ .value("mods", IPluginGame::MODS)
+ .value("configuration", IPluginGame::CONFIGURATION)
+ .value("savegames", IPluginGame::SAVEGAMES)
+ .value("preferDefaults", IPluginGame::PREFER_DEFAULTS)
+
+ .value("MODS", IPluginGame::MODS)
+ .value("CONFIGURATION", IPluginGame::CONFIGURATION)
+ .value("SAVEGAMES", IPluginGame::SAVEGAMES)
+ .value("PREFER_DEFAULTS", IPluginGame::PREFER_DEFAULTS);
+
+ py::class_<IPluginGame, PyPluginGame, IPlugin,
+ std::unique_ptr<IPluginGame, py::nodelete>>(
+ m, "IPluginGame", py::multiple_inheritance())
+ .def(py::init<>())
+ .def("detectGame", &IPluginGame::detectGame)
+ .def("gameName", &IPluginGame::gameName)
+ .def("displayGameName", &IPluginGame::displayGameName)
+ .def("initializeProfile", &IPluginGame::initializeProfile, "directory"_a,
+ "settings"_a)
+ .def("listSaves", &IPluginGame::listSaves, "folder"_a)
+ .def("isInstalled", &IPluginGame::isInstalled)
+ .def("gameIcon", &IPluginGame::gameIcon)
+ .def("gameDirectory", &IPluginGame::gameDirectory)
+ .def("dataDirectory", &IPluginGame::dataDirectory)
+ .def("modDataDirectory", &IPluginGame::modDataDirectory)
+ .def("secondaryDataDirectories", &IPluginGame::secondaryDataDirectories)
+ .def("setGamePath", &IPluginGame::setGamePath, "path"_a)
+ .def("documentsDirectory", &IPluginGame::documentsDirectory)
+ .def("savesDirectory", &IPluginGame::savesDirectory)
+ .def("executables", &IPluginGame::executables)
+ .def("executableForcedLoads", &IPluginGame::executableForcedLoads)
+ .def("steamAPPId", &IPluginGame::steamAPPId)
+ .def("primaryPlugins", &IPluginGame::primaryPlugins)
+ .def("enabledPlugins", &IPluginGame::enabledPlugins)
+ .def("gameVariants", &IPluginGame::gameVariants)
+ .def("setGameVariant", &IPluginGame::setGameVariant, "variant"_a)
+ .def("binaryName", &IPluginGame::binaryName)
+ .def("gameShortName", &IPluginGame::gameShortName)
+ .def("lootGameName", &IPluginGame::lootGameName)
+ .def("primarySources", &IPluginGame::primarySources)
+ .def("validShortNames", &IPluginGame::validShortNames)
+ .def("gameNexusName", &IPluginGame::gameNexusName)
+ .def("iniFiles", &IPluginGame::iniFiles)
+ .def("DLCPlugins", &IPluginGame::DLCPlugins)
+ .def("CCPlugins", &IPluginGame::CCPlugins)
+ .def("loadOrderMechanism", &IPluginGame::loadOrderMechanism)
+ .def("sortMechanism", &IPluginGame::sortMechanism)
+ .def("nexusModOrganizerID", &IPluginGame::nexusModOrganizerID)
+ .def("nexusGameID", &IPluginGame::nexusGameID)
+ .def("looksValid", &IPluginGame::looksValid, "directory"_a)
+ .def("gameVersion", &IPluginGame::gameVersion)
+ .def("getLauncherName", &IPluginGame::getLauncherName)
+ .def("getSupportURL", &IPluginGame::getSupportURL)
+ .def("getModMappings", &IPluginGame::getModMappings);
+ }
+
+ // multiple installers
+ void add_iplugininstaller_bindings(pybind11::module_ m)
+ {
+ // this is bind but should not be inherited in Python - does not make sense,
+ // having it makes it simpler to bind the Simple and Custom installers
+ py::class_<IPluginInstaller, PyPluginInstallerBase<IPluginInstaller>, IPlugin,
+ std::unique_ptr<IPluginInstaller, py::nodelete>>(
+ m, "IPluginInstaller", py::multiple_inheritance())
+ .def("isArchiveSupported", &IPluginInstaller::isArchiveSupported, "tree"_a)
+ .def("priority", &IPluginInstaller::priority)
+ .def("onInstallationStart", &IPluginInstaller::onInstallationStart,
+ "archive"_a, "reinstallation"_a, "current_mod"_a)
+ .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, "result"_a,
+ "new_mod"_a)
+ .def("isManualInstaller", &IPluginInstaller::isManualInstaller)
+ .def("setParentWidget", &IPluginInstaller::setParentWidget, "parent"_a)
+ .def("setInstallationManager", &IPluginInstaller::setInstallationManager,
+ "manager"_a)
+ .def("_parentWidget",
+ &PyPluginInstallerBase<IPluginInstaller>::parentWidget)
+ .def("_manager", &PyPluginInstallerBase<IPluginInstaller>::manager,
+ py::return_value_policy::reference);
+
+ py::class_<IPluginInstallerSimple, PyPluginInstallerSimple, IPluginInstaller,
+ IPlugin, std::unique_ptr<IPluginInstallerSimple, py::nodelete>>(
+ m, "IPluginInstallerSimple", py::multiple_inheritance())
+ .def(py::init<>())
+
+ // note: keeping the variant here even if we always return a tuple
+ // to be consistent with the wrapper and have proper stubs generation.
+ .def(
+ "install",
+ [](IPluginInstallerSimple* p, GuessedValue<QString>& modName,
+ std::shared_ptr<IFileTree>& tree, QString& version,
+ int& nexusID) -> PyPluginInstallerSimple::py_install_return_type {
+ auto result = p->install(modName, tree, version, nexusID);
+ return std::make_tuple(result, tree, version, nexusID);
+ },
+ "name"_a, "tree"_a, "version"_a, "nexus_id"_a);
+
+ py::class_<IPluginInstallerCustom, PyPluginInstallerCustom, IPluginInstaller,
+ IPlugin, std::unique_ptr<IPluginInstallerCustom, py::nodelete>>(
+ m, "IPluginInstallerCustom", py::multiple_inheritance())
+ .def(py::init<>())
+ .def("isArchiveSupported", &IPluginInstallerCustom::isArchiveSupported,
+ "archive_name"_a)
+ .def("supportedExtensions", &IPluginInstallerCustom::supportedExtensions)
+ .def("install", &IPluginInstallerCustom::install, "mod_name"_a,
+ "game_name"_a, "archive_name"_a, "version"_a, "nexus_id"_a);
+ }
+
+ void add_plugins_bindings(pybind11::module_ m)
+ {
+ py::class_<IPlugin, PyPlugin, std::unique_ptr<IPlugin, py::nodelete>>(
+ m, "IPluginBase", py::multiple_inheritance())
+ .def(py::init<>())
+ .def("init", &IPlugin::init, "organizer"_a)
+ .def("name", &IPlugin::name)
+ .def("localizedName", &IPlugin::localizedName)
+ .def("master", &IPlugin::master)
+ .def("author", &IPlugin::author)
+ .def("description", &IPlugin::description)
+ .def("version", &IPlugin::version)
+ .def("requirements", &IPlugin::requirements)
+ .def("settings", &IPlugin::settings)
+ .def("enabledByDefault", &IPlugin::enabledByDefault);
+
+ py::class_<IPyPlugin, PyPlugin, IPlugin,
+ std::unique_ptr<IPyPlugin, py::nodelete>>(m, "IPlugin",
+ py::multiple_inheritance())
+ .def(py::init<>());
+
+ py::class_<IPyPluginFileMapper, PyPluginFileMapper, IPlugin,
+ std::unique_ptr<IPyPluginFileMapper, py::nodelete>>(
+ m, "IPluginFileMapper", py::multiple_inheritance())
+ .def(py::init<>())
+ .def("mappings", &IPluginFileMapper::mappings);
+
+ py::class_<IPyPluginDiagnose, PyPluginDiagnose, IPlugin,
+ std::unique_ptr<IPyPluginDiagnose, py::nodelete>>(
+ m, "IPluginDiagnose", py::multiple_inheritance())
+ .def(py::init<>())
+ .def("activeProblems", &IPluginDiagnose::activeProblems)
+ .def("shortDescription", &IPluginDiagnose::shortDescription, "key"_a)
+ .def("fullDescription", &IPluginDiagnose::fullDescription, "key"_a)
+ .def("hasGuidedFix", &IPluginDiagnose::hasGuidedFix, "key"_a)
+ .def("startGuidedFix", &IPluginDiagnose::startGuidedFix, "key"_a)
+ .def("_invalidate", &PyPluginDiagnose::invalidate);
+
+ py::class_<IPluginTool, PyPluginTool, IPlugin,
+ std::unique_ptr<IPluginTool, py::nodelete>>(
+ m, "IPluginTool", py::multiple_inheritance())
+ .def(py::init<>())
+ .def("displayName", &IPluginTool::displayName)
+ .def("tooltip", &IPluginTool::tooltip)
+ .def("icon", &IPluginTool::icon)
+ .def("display", &IPluginTool::display)
+ .def("setParentWidget", &IPluginTool::setParentWidget)
+ .def("_parentWidget", &PyPluginTool::parentWidget);
+
+ py::class_<IPluginPreview, PyPluginPreview, IPlugin,
+ std::unique_ptr<IPluginPreview, py::nodelete>>(
+ m, "IPluginPreview", py::multiple_inheritance())
+ .def(py::init<>())
+ .def("supportedExtensions", &IPluginPreview::supportedExtensions)
+ .def("supportsArchives", &IPluginPreview::supportsArchives)
+ .def("genFilePreview", &IPluginPreview::genFilePreview, "filename"_a,
+ "max_size"_a)
+ .def("genDataPreview", &IPluginPreview::genDataPreview, "file_data"_a,
+ "filename"_a, "max_size"_a);
+
+ py::class_<IPluginModPage, PyPluginModPage, IPlugin,
+ std::unique_ptr<IPluginModPage, py::nodelete>>(
+ m, "IPluginModPage", py::multiple_inheritance())
+ .def(py::init<>())
+ .def("displayName", &IPluginModPage::displayName)
+ .def("icon", &IPluginModPage::icon)
+ .def("pageURL", &IPluginModPage::pageURL)
+ .def("useIntegratedBrowser", &IPluginModPage::useIntegratedBrowser)
+ .def("handlesDownload", &IPluginModPage::handlesDownload, "page_url"_a,
+ "download_url"_a, "fileinfo"_a)
+ .def("setParentWidget", &IPluginModPage::setParentWidget, "parent"_a)
+ .def("_parentWidget", &PyPluginModPage::parentWidget);
+
+ add_iplugingame_bindings(m);
+ add_iplugininstaller_bindings(m);
+ }
+
+ struct extract_plugins_helper {
+ QList<QObject*> objects;
+
+ template <class IPluginClass>
+ void append_if_instance(pybind11::object plugin_obj)
+ {
+ if (py::isinstance<IPluginClass>(plugin_obj)) {
+ objects.append(plugin_obj.cast<IPluginClass*>());
+ }
+ }
+ };
+
+ QList<QObject*> extract_plugins(pybind11::object plugin_obj)
+ {
+ extract_plugins_helper helper;
+
+ // we need to check the trampoline class for these since the interfaces do not
+ // extend IPlugin
+ helper.append_if_instance<IPyPluginFileMapper>(plugin_obj);
+ helper.append_if_instance<IPyPluginDiagnose>(plugin_obj);
+
+ helper.append_if_instance<IPluginModPage>(plugin_obj);
+ helper.append_if_instance<IPluginPreview>(plugin_obj);
+ helper.append_if_instance<IPluginTool>(plugin_obj);
+
+ helper.append_if_instance<IPluginGame>(plugin_obj);
+
+ // we need to check the two installer types because IPluginInstaller does not
+ // inherit QObject, and the trampoline do not have a common ancestor
+ helper.append_if_instance<IPluginInstallerSimple>(plugin_obj);
+ helper.append_if_instance<IPluginInstallerCustom>(plugin_obj);
+
+ if (helper.objects.isEmpty()) {
+ helper.append_if_instance<IPyPlugin>(plugin_obj);
+ }
+
+ // tie the lifetime of the Python object to the lifetime of the QObject
+ for (auto* object : helper.objects) {
+ py::qt::set_qt_owner(object, plugin_obj);
+ }
+
+ return helper.objects;
+ }
+
+} // namespace mo2::python
diff --git a/libs/plugin_python/src/mobase/wrappers/pyplugins.h b/libs/plugin_python/src/mobase/wrappers/pyplugins.h
new file mode 100644
index 0000000..17f7ab5
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/pyplugins.h
@@ -0,0 +1,527 @@
+#ifndef PYTHON_WRAPPERS_PYPLUGINS_H
+#define PYTHON_WRAPPERS_PYPLUGINS_H
+
+#include "../pybind11_all.h"
+
+#include <uibase/iinstallationmanager.h>
+
+#include <uibase/iplugin.h>
+#include <uibase/iplugindiagnose.h>
+#include <uibase/ipluginfilemapper.h>
+#include <uibase/iplugingame.h>
+#include <uibase/iplugininstaller.h>
+#include <uibase/iplugininstallercustom.h>
+#include <uibase/iplugininstallersimple.h>
+#include <uibase/ipluginmodpage.h>
+#include <uibase/ipluginpreview.h>
+#include <uibase/iplugintool.h>
+
+// these needs to be defined in a header file for automoc - this file is included only
+// in pyplugins.cpp
+namespace mo2::python {
+
+ using namespace MOBase;
+
+ // we need two base trampoline because IPluginGame has some final methods.
+ template <class PluginBase>
+ class PyPluginBaseNoFinal : public PluginBase {
+ public:
+ using PluginBase::PluginBase;
+
+ PyPluginBaseNoFinal(PyPluginBaseNoFinal const&) = delete;
+ PyPluginBaseNoFinal(PyPluginBaseNoFinal&&) = delete;
+ PyPluginBaseNoFinal& operator=(PyPluginBaseNoFinal const&) = delete;
+ PyPluginBaseNoFinal& operator=(PyPluginBaseNoFinal&&) = delete;
+
+ bool init(IOrganizer* organizer) override
+ {
+ PYBIND11_OVERRIDE_PURE(bool, PluginBase, init, organizer);
+ }
+ QString name() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, PluginBase, name, );
+ }
+ QString localizedName() const override
+ {
+ PYBIND11_OVERRIDE(QString, PluginBase, localizedName, );
+ }
+ QString master() const override
+ {
+ PYBIND11_OVERRIDE(QString, PluginBase, master, );
+ }
+ QString author() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, PluginBase, author, );
+ }
+ QString description() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, PluginBase, description, );
+ }
+ VersionInfo version() const override
+ {
+ PYBIND11_OVERRIDE_PURE(VersionInfo, PluginBase, version, );
+ }
+ QList<PluginSetting> settings() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QList<PluginSetting>, PluginBase, settings, );
+ }
+ };
+
+ template <class PluginBase>
+ class PyPluginBase : public PyPluginBaseNoFinal<PluginBase> {
+ public:
+ using PyPluginBaseNoFinal<PluginBase>::PyPluginBaseNoFinal;
+
+ std::vector<std::shared_ptr<const IPluginRequirement>> requirements() const
+ {
+ PYBIND11_OVERRIDE(std::vector<std::shared_ptr<const IPluginRequirement>>,
+ PluginBase, requirements, );
+ }
+ bool enabledByDefault() const override
+ {
+ PYBIND11_OVERRIDE(bool, PluginBase, enabledByDefault, );
+ }
+ };
+
+ // these classes do not inherit IPlugin or QObject so we need intermediate class to
+ // get proper bindings
+ class IPyPlugin : public QObject, public IPlugin {};
+ class IPyPluginFileMapper : public IPyPlugin, public IPluginFileMapper {};
+ class IPyPluginDiagnose : public IPyPlugin, public IPluginDiagnose {};
+
+ // PyXXX classes - trampoline classes for the plugins
+
+ class PyPlugin : public PyPluginBase<IPyPlugin> {
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin)
+ };
+
+ class PyPluginFileMapper : public PyPluginBase<IPyPluginFileMapper> {
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginFileMapper)
+ public:
+ MappingType mappings() const override
+ {
+ PYBIND11_OVERRIDE_PURE(MappingType, IPyPluginFileMapper, mappings, );
+ }
+ };
+
+ class PyPluginDiagnose : public PyPluginBase<IPyPluginDiagnose> {
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose)
+ public:
+ std::vector<unsigned int> activeProblems() const
+ {
+ PYBIND11_OVERRIDE_PURE(std::vector<unsigned int>, IPyPluginDiagnose,
+ activeProblems, );
+ }
+
+ QString shortDescription(unsigned int key) const
+ {
+ PYBIND11_OVERRIDE_PURE(QString, IPyPluginDiagnose, shortDescription, key);
+ }
+
+ QString fullDescription(unsigned int key) const
+ {
+ PYBIND11_OVERRIDE_PURE(QString, IPyPluginDiagnose, fullDescription, key);
+ }
+
+ bool hasGuidedFix(unsigned int key) const
+ {
+ PYBIND11_OVERRIDE_PURE(bool, IPyPluginDiagnose, hasGuidedFix, key);
+ }
+
+ void startGuidedFix(unsigned int key) const
+ {
+ PYBIND11_OVERRIDE_PURE(void, IPyPluginDiagnose, startGuidedFix, key);
+ }
+
+ // we need to bring this in public scope
+ using IPluginDiagnose::invalidate;
+ };
+
+ class PyPluginTool : public PyPluginBase<IPluginTool> {
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool)
+ public:
+ QString displayName() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, IPluginTool, displayName, );
+ }
+ QString tooltip() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, IPluginTool, tooltip, );
+ }
+ QIcon icon() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QIcon, IPluginTool, icon, );
+ }
+ void setParentWidget(QWidget* widget) override
+ {
+ PYBIND11_OVERRIDE(void, IPluginTool, setParentWidget, widget);
+ }
+ void display() const override
+ {
+ PYBIND11_OVERRIDE_PURE(void, IPluginTool, display, );
+ }
+
+ // we need to bring this in public scope
+ using IPluginTool::parentWidget;
+ };
+
+ class PyPluginPreview : public PyPluginBase<IPluginPreview> {
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview)
+ public:
+ std::set<QString> supportedExtensions() const override
+ {
+ PYBIND11_OVERRIDE_PURE(std::set<QString>, IPluginPreview,
+ supportedExtensions, );
+ }
+
+ bool supportsArchives() const override
+ {
+ PYBIND11_OVERRIDE(bool, IPluginPreview, supportsArchives, );
+ }
+
+ QWidget* genFilePreview(const QString& fileName,
+ const QSize& maxSize) const override
+ {
+ PYBIND11_OVERRIDE_PURE(QWidget*, IPluginPreview, genFilePreview, fileName,
+ maxSize);
+ }
+
+ QWidget* genDataPreview(const QByteArray& fileData, const QString& fileName,
+ const QSize& maxSize) const override
+ {
+ PYBIND11_OVERRIDE(QWidget*, IPluginPreview, genDataPreview, fileData,
+ fileName, maxSize);
+ }
+ };
+
+ class PyPluginModPage : public PyPluginBase<IPluginModPage> {
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginModPage)
+ public:
+ QString displayName() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, IPluginModPage, displayName, );
+ }
+
+ QIcon icon() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QIcon, IPluginModPage, icon, );
+ }
+
+ QUrl pageURL() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QUrl, IPluginModPage, pageURL, );
+ }
+
+ bool useIntegratedBrowser() const override
+ {
+ PYBIND11_OVERRIDE_PURE(bool, IPluginModPage, useIntegratedBrowser, );
+ }
+
+ bool handlesDownload(const QUrl& pageURL, const QUrl& downloadURL,
+ ModRepositoryFileInfo& fileInfo) const override
+ {
+ // TODO: cannot modify fileInfo from Python
+ PYBIND11_OVERRIDE_PURE(bool, IPluginModPage, handlesDownload, pageURL,
+ downloadURL, &fileInfo);
+ }
+
+ void setParentWidget(QWidget* widget) override
+ {
+ PYBIND11_OVERRIDE(void, IPluginModPage, setParentWidget, widget);
+ }
+
+ // we need to bring this in public scope
+ using IPluginModPage::parentWidget;
+ };
+
+ // installers
+ template <class PluginInstallerBase>
+ class PyPluginInstallerBase : public PyPluginBase<PluginInstallerBase> {
+ public:
+ using PyPluginBase<PluginInstallerBase>::PyPluginBase;
+
+ unsigned int priority() const override
+ {
+ PYBIND11_OVERRIDE_PURE(unsigned int, PluginInstallerBase, priority);
+ }
+
+ bool isManualInstaller() const override
+ {
+ PYBIND11_OVERRIDE_PURE(bool, PluginInstallerBase, isManualInstaller, );
+ }
+
+ void onInstallationStart(QString const& archive, bool reinstallation,
+ IModInterface* currentMod)
+ {
+ PYBIND11_OVERRIDE(void, PluginInstallerBase, onInstallationStart, archive,
+ reinstallation, currentMod);
+ }
+
+ void onInstallationEnd(IPluginInstaller::EInstallResult result,
+ IModInterface* newMod)
+ {
+ PYBIND11_OVERRIDE(void, PluginInstallerBase, onInstallationEnd, result,
+ newMod);
+ }
+
+ bool isArchiveSupported(std::shared_ptr<const IFileTree> tree) const override
+ {
+ PYBIND11_OVERRIDE_PURE(bool, PluginInstallerBase, isArchiveSupported, tree);
+ }
+
+ // we need to bring these in public scope
+ using PluginInstallerBase::manager;
+ using PluginInstallerBase::parentWidget;
+ };
+
+ class PyPluginInstallerCustom
+ : public PyPluginInstallerBase<IPluginInstallerCustom> {
+ Q_OBJECT
+ Q_INTERFACES(
+ MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerCustom)
+ public:
+ bool isArchiveSupported(const QString& archiveName) const
+ {
+ PYBIND11_OVERRIDE_PURE(bool, IPluginInstallerCustom, isArchiveSupported,
+ archiveName);
+ }
+
+ std::set<QString> supportedExtensions() const
+ {
+ PYBIND11_OVERRIDE_PURE(std::set<QString>, IPluginInstallerCustom,
+ supportedExtensions, );
+ }
+
+ EInstallResult install(GuessedValue<QString>& modName, QString gameName,
+ const QString& archiveName, const QString& version,
+ int nexusID) override
+ {
+ PYBIND11_OVERRIDE_PURE(EInstallResult, IPluginInstallerCustom, install,
+ &modName, gameName, archiveName, version, nexusID);
+ }
+ };
+
+ class PyPluginInstallerSimple
+ : public PyPluginInstallerBase<IPluginInstallerSimple> {
+ Q_OBJECT
+ Q_INTERFACES(
+ MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple)
+ public:
+ using py_install_return_type =
+ std::variant<IPluginInstaller::EInstallResult, std::shared_ptr<IFileTree>,
+ std::tuple<IPluginInstaller::EInstallResult,
+ std::shared_ptr<IFileTree>, QString, int>>;
+
+ EInstallResult install(GuessedValue<QString>& modName,
+ std::shared_ptr<IFileTree>& tree, QString& version,
+ int& nexusID) override
+ {
+ const auto result = [&, this]() {
+ PYBIND11_OVERRIDE_PURE(py_install_return_type, IPluginInstallerSimple,
+ install, &modName, tree, version, nexusID);
+ }();
+
+ return std::visit(
+ [&tree, &version, &nexusID](auto const& t) {
+ using type = std::decay_t<decltype(t)>;
+ if constexpr (std::is_same_v<type, EInstallResult>) {
+ return t;
+ }
+ else if constexpr (std::is_same_v<type,
+ std::shared_ptr<IFileTree>>) {
+ tree = t;
+ return RESULT_SUCCESS;
+ }
+ else if constexpr (std::is_same_v<
+ type, std::tuple<EInstallResult,
+ std::shared_ptr<IFileTree>,
+ QString, int>>) {
+ tree = std::get<1>(t);
+ version = std::get<2>(t);
+ nexusID = std::get<3>(t);
+ return std::get<0>(t);
+ }
+ },
+ result);
+ }
+ };
+
+ // game
+ class PyPluginGame : public PyPluginBaseNoFinal<IPluginGame> {
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame)
+ public:
+ void detectGame() override
+ {
+ PYBIND11_OVERRIDE_PURE(void, IPluginGame, detectGame, );
+ }
+ QString gameName() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameName, );
+ }
+ QString displayGameName() const override
+ {
+ PYBIND11_OVERRIDE(QString, IPluginGame, displayGameName, );
+ }
+ void initializeProfile(const QDir& directory,
+ ProfileSettings settings) const override
+ {
+ PYBIND11_OVERRIDE_PURE(void, IPluginGame, initializeProfile, directory,
+ settings);
+ }
+ std::vector<std::shared_ptr<const ISaveGame>>
+ listSaves(QDir folder) const override
+ {
+ PYBIND11_OVERRIDE_PURE(std::vector<std::shared_ptr<const ISaveGame>>,
+ IPluginGame, listSaves, folder);
+ }
+ bool isInstalled() const override
+ {
+ PYBIND11_OVERRIDE_PURE(bool, IPluginGame, isInstalled, );
+ }
+ QIcon gameIcon() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QIcon, IPluginGame, gameIcon, );
+ }
+ QDir gameDirectory() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, gameDirectory, );
+ }
+ QDir dataDirectory() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, dataDirectory, );
+ }
+ QString modDataDirectory() const override
+ {
+ PYBIND11_OVERRIDE(QString, IPluginGame, modDataDirectory, );
+ }
+ QMap<QString, QDir> secondaryDataDirectories() const override
+ {
+ using string_dir_map = QMap<QString, QDir>;
+ PYBIND11_OVERRIDE(string_dir_map, IPluginGame, secondaryDataDirectories, );
+ }
+ void setGamePath(const QString& path) override
+ {
+ PYBIND11_OVERRIDE_PURE(void, IPluginGame, setGamePath, path);
+ }
+ QDir documentsDirectory() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, documentsDirectory, );
+ }
+ QDir savesDirectory() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QDir, IPluginGame, savesDirectory, );
+ }
+ QList<ExecutableInfo> executables() const override
+ {
+ PYBIND11_OVERRIDE(QList<ExecutableInfo>, IPluginGame, executables, );
+ }
+ QList<ExecutableForcedLoadSetting> executableForcedLoads() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QList<ExecutableForcedLoadSetting>, IPluginGame,
+ executableForcedLoads, );
+ }
+ QString steamAPPId() const override
+ {
+ PYBIND11_OVERRIDE(QString, IPluginGame, steamAPPId, );
+ }
+ QStringList primaryPlugins() const override
+ {
+ PYBIND11_OVERRIDE(QStringList, IPluginGame, primaryPlugins, );
+ }
+ QStringList enabledPlugins() const override
+ {
+ PYBIND11_OVERRIDE(QStringList, IPluginGame, enabledPlugins, );
+ }
+ QStringList gameVariants() const override
+ {
+ PYBIND11_OVERRIDE(QStringList, IPluginGame, gameVariants, );
+ }
+ void setGameVariant(const QString& variant) override
+ {
+ PYBIND11_OVERRIDE_PURE(void, IPluginGame, setGameVariant, variant);
+ }
+ QString binaryName() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, IPluginGame, binaryName, );
+ }
+ QString gameShortName() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameShortName, );
+ }
+ QString lootGameName() const override
+ {
+ PYBIND11_OVERRIDE(QString, IPluginGame, lootGameName, );
+ }
+ QStringList primarySources() const override
+ {
+ PYBIND11_OVERRIDE(QStringList, IPluginGame, primarySources, );
+ }
+ QStringList validShortNames() const override
+ {
+ PYBIND11_OVERRIDE(QStringList, IPluginGame, validShortNames, );
+ }
+ QString gameNexusName() const override
+ {
+ PYBIND11_OVERRIDE(QString, IPluginGame, gameNexusName, );
+ }
+ QStringList iniFiles() const override
+ {
+ PYBIND11_OVERRIDE(QStringList, IPluginGame, iniFiles, );
+ }
+ QStringList DLCPlugins() const override
+ {
+ PYBIND11_OVERRIDE(QStringList, IPluginGame, DLCPlugins, );
+ }
+ QStringList CCPlugins() const override
+ {
+ PYBIND11_OVERRIDE(QStringList, IPluginGame, CCPlugins, );
+ }
+ LoadOrderMechanism loadOrderMechanism() const override
+ {
+ PYBIND11_OVERRIDE(LoadOrderMechanism, IPluginGame, loadOrderMechanism, );
+ }
+ SortMechanism sortMechanism() const override
+ {
+ PYBIND11_OVERRIDE(SortMechanism, IPluginGame, sortMechanism, );
+ }
+ int nexusModOrganizerID() const override
+ {
+ PYBIND11_OVERRIDE(int, IPluginGame, nexusModOrganizerID, );
+ }
+ int nexusGameID() const override
+ {
+ PYBIND11_OVERRIDE_PURE(int, IPluginGame, nexusGameID, );
+ }
+ bool looksValid(QDir const& dir) const override
+ {
+ PYBIND11_OVERRIDE_PURE(bool, IPluginGame, looksValid, dir);
+ }
+ QString gameVersion() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameVersion, );
+ }
+ QString getLauncherName() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, IPluginGame, getLauncherName, );
+ }
+ QString getSupportURL() const override
+ {
+ PYBIND11_OVERRIDE(QString, IPluginGame, getSupportURL, );
+ }
+ QMap<QString, QStringList> getModMappings() const override
+ {
+ using vfs_map = QMap<QString, QStringList>;
+ PYBIND11_OVERRIDE(vfs_map, IPluginGame, getModMappings, );
+ }
+ };
+
+} // namespace mo2::python
+
+#endif
diff --git a/libs/plugin_python/src/mobase/wrappers/utils.cpp b/libs/plugin_python/src/mobase/wrappers/utils.cpp
new file mode 100644
index 0000000..95111fa
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/utils.cpp
@@ -0,0 +1,50 @@
+#include "wrappers.h"
+
+#include "../pybind11_all.h"
+
+#include <uibase/report.h>
+#include <uibase/utility.h>
+
+#ifdef _WIN32
+#include "known_folders.h"
+#endif
+
+namespace py = pybind11;
+using namespace MOBase;
+
+namespace mo2::python {
+
+ void add_utils_bindings(pybind11::module_ m)
+ {
+#ifdef _WIN32
+ py::class_<KnownFolder> pyKnownFolder(m, "KnownFolder");
+ for (std::size_t i = 0; i < KNOWN_FOLDERS.size(); ++i) {
+ pyKnownFolder.attr(KNOWN_FOLDERS[i].name) = py::int_(i);
+ }
+
+ m.def(
+ "getKnownFolder",
+ [](std::size_t knownFolderId, QString what) {
+ return getKnownFolder(KNOWN_FOLDERS.at(knownFolderId).guid, what);
+ },
+ py::arg("known_folder"), py::arg("what") = "");
+
+ m.def(
+ "getOptionalKnownFolder",
+ [](std::size_t knownFolderId) {
+ const auto r =
+ getOptionalKnownFolder(KNOWN_FOLDERS.at(knownFolderId).guid);
+ return r.isEmpty() ? py::none{} : py::cast(r);
+ },
+ py::arg("known_folder"));
+#endif
+
+ m.def("getFileVersion", wrap_for_filepath(&MOBase::getFileVersion),
+ py::arg("filepath"));
+ m.def("getProductVersion", wrap_for_filepath(&MOBase::getProductVersion),
+ py::arg("executable"));
+ m.def("getIconForExecutable", wrap_for_filepath(&MOBase::iconForExecutable),
+ py::arg("executable"));
+ }
+
+} // namespace mo2::python
diff --git a/libs/plugin_python/src/mobase/wrappers/widgets.cpp b/libs/plugin_python/src/mobase/wrappers/widgets.cpp
new file mode 100644
index 0000000..e626540
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/widgets.cpp
@@ -0,0 +1,77 @@
+#include "wrappers.h"
+
+#include "../pybind11_all.h"
+
+#include <uibase/report.h>
+
+namespace py = pybind11;
+using namespace MOBase;
+
+namespace mo2::python {
+
+ void add_widget_bindings(pybind11::module_ m)
+ {
+ // TaskDialog is also in Windows System.
+ using TaskDialog = MOBase::TaskDialog;
+
+ // TaskDialog
+ py::class_<TaskDialogButton>(m, "TaskDialogButton")
+ .def(py::init<QString, QString, QMessageBox::StandardButton>(),
+ py::arg("text"), py::arg("description"), py::arg("button"))
+ .def(py::init<QString, QMessageBox::StandardButton>(), py::arg("text"),
+ py::arg("button"))
+ .def_readwrite("text", &TaskDialogButton::text)
+ .def_readwrite("description", &TaskDialogButton::description)
+ .def_readwrite("button", &TaskDialogButton::button);
+
+ py::class_<TaskDialog>(m, "TaskDialog")
+ .def(py::init([](QWidget* parent, QString const& title, QString const& main,
+ QString const& content, QString const& details,
+ QMessageBox::Icon icon,
+ std::vector<TaskDialogButton> const& buttons,
+ std::variant<QString, std::tuple<QString, QString>> const&
+ remember) {
+ auto* dialog = new TaskDialog(parent, title);
+ dialog->main(main).content(content).details(details).icon(icon);
+
+ for (auto& button : buttons) {
+ dialog->button(button);
+ }
+
+ std::visit(
+ [dialog](auto const& item) {
+ QString action, file;
+ if constexpr (std::is_same_v<std::decay_t<decltype(item)>,
+ QString>) {
+ action = item;
+ }
+ else {
+ action = std::get<0>(item);
+ file = std::get<1>(item);
+ }
+ dialog->remember(action, file);
+ },
+ remember);
+
+ return dialog;
+ }),
+ py::return_value_policy::take_ownership,
+ py::arg("parent") = static_cast<QWidget*>(nullptr),
+ py::arg("title") = "", py::arg("main") = "", py::arg("content") = "",
+ py::arg("details") = "", py::arg("icon") = QMessageBox::NoIcon,
+ py::arg("buttons") = std::vector<TaskDialogButton>{},
+ py::arg("remember") = "")
+ .def("setTitle", &TaskDialog::title, py::arg("title"))
+ .def("setMain", &TaskDialog::main, py::arg("main"))
+ .def("setContent", &TaskDialog::content, py::arg("content"))
+ .def("setDetails", &TaskDialog::details, py::arg("details"))
+ .def("setIcon", &TaskDialog::icon, py::arg("icon"))
+ .def("addButton", &TaskDialog::button, py::arg("button"))
+ .def("setRemember", &TaskDialog::remember, py::arg("action"),
+ py::arg("file") = "")
+ .def("setWidth", &TaskDialog::setWidth, py::arg("width"))
+ .def("addContent", &TaskDialog::addContent, py::arg("widget"))
+ .def("exec", &TaskDialog::exec);
+ }
+
+} // namespace mo2::python
diff --git a/libs/plugin_python/src/mobase/wrappers/wrappers.cpp b/libs/plugin_python/src/mobase/wrappers/wrappers.cpp
new file mode 100644
index 0000000..10f043d
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/wrappers.cpp
@@ -0,0 +1,120 @@
+
+#include "wrappers.h"
+
+#include "../pybind11_all.h"
+
+#include <QDir>
+#include <QIcon>
+#include <QString>
+#include <QUrl>
+
+// IOrganizer must be bring here to properly compile the Python bindings of
+// plugin requirements
+#include <uibase/imoinfo.h>
+#include <uibase/isavegame.h>
+#include <uibase/isavegameinfowidget.h>
+#include <uibase/pluginrequirements.h>
+
+using namespace pybind11::literals;
+namespace py = pybind11;
+using namespace MOBase;
+
+namespace mo2::python {
+
+ class PyPluginRequirement : public IPluginRequirement {
+ public:
+ std::optional<Problem> check(IOrganizer* organizer) const override
+ {
+ PYBIND11_OVERRIDE_PURE(std::optional<Problem>, IPluginRequirement, check,
+ organizer);
+ };
+ };
+
+ class PySaveGame : public ISaveGame {
+ public:
+ QString getFilepath() const override
+ {
+ PYBIND11_OVERRIDE_PURE(FileWrapper, ISaveGame, getFilepath, );
+ }
+
+ QDateTime getCreationTime() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QDateTime, ISaveGame, getCreationTime, );
+ }
+
+ QString getName() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, ISaveGame, getName, );
+ }
+
+ QString getSaveGroupIdentifier() const override
+ {
+ PYBIND11_OVERRIDE_PURE(QString, ISaveGame, getSaveGroupIdentifier, );
+ }
+
+ QStringList allFiles() const override
+ {
+ return toQStringList([&] {
+ PYBIND11_OVERRIDE_PURE(QList<FileWrapper>, ISaveGame, allFiles, );
+ }());
+ }
+
+ ~PySaveGame() { std::cout << "~PySaveGame()" << std::endl; }
+ };
+
+ class PySaveGameInfoWidget : public ISaveGameInfoWidget {
+ public:
+ // Bring the constructor:
+ using ISaveGameInfoWidget::ISaveGameInfoWidget;
+
+ void setSave(ISaveGame const& save) override
+ {
+ PYBIND11_OVERRIDE_PURE(void, ISaveGameInfoWidget, setSave, &save);
+ }
+
+ ~PySaveGameInfoWidget() { std::cout << "~PySaveGameInfoWidget()" << std::endl; }
+ };
+
+ void add_wrapper_bindings(pybind11::module_ m)
+ {
+ // ISaveGame - custom type_caster<> for shared_ptr<> to keep the Python object
+ // alive when returned from Python (see shared_cpp_owner.h)
+
+ py::class_<ISaveGame, PySaveGame, std::shared_ptr<ISaveGame>>(m, "ISaveGame")
+ .def(py::init<>())
+ .def("getFilepath", wrap_return_for_filepath(&ISaveGame::getFilepath))
+ .def("getCreationTime", &ISaveGame::getCreationTime)
+ .def("getName", &ISaveGame::getName)
+ .def("getSaveGroupIdentifier", &ISaveGame::getSaveGroupIdentifier)
+ .def("allFiles", [](ISaveGame* s) -> QList<FileWrapper> {
+ const auto result = s->allFiles();
+ return {result.begin(), result.end()};
+ });
+
+ // ISaveGameInfoWidget - custom holder to keep the Python object alive alongside
+ // the widget
+
+ py::class_<ISaveGameInfoWidget, PySaveGameInfoWidget,
+ py::qt::qobject_holder<ISaveGameInfoWidget>>
+ iSaveGameInfoWidget(m, "ISaveGameInfoWidget");
+ iSaveGameInfoWidget.def(py::init<QWidget*>(), "parent"_a = (QWidget*)nullptr)
+ .def("setSave", &ISaveGameInfoWidget::setSave, "save"_a);
+ py::qt::add_qt_delegate<QWidget>(iSaveGameInfoWidget, "_widget");
+
+ // IPluginRequirement - custom type_caster<> for shared_ptr<> to keep the Python
+ // object alive when returned from Python (see shared_cpp_owner.h)
+
+ py::class_<IPluginRequirement, std::shared_ptr<IPluginRequirement>,
+ PyPluginRequirement>
+ iPluginRequirementClass(m, "IPluginRequirement");
+
+ py::class_<IPluginRequirement::Problem>(iPluginRequirementClass, "Problem")
+ .def(py::init<QString, QString>(), "short_description"_a,
+ "long_description"_a = "")
+ .def("shortDescription", &IPluginRequirement::Problem::shortDescription)
+ .def("longDescription", &IPluginRequirement::Problem::longDescription);
+
+ iPluginRequirementClass.def("check", &IPluginRequirement::check, "organizer"_a);
+ }
+
+} // namespace mo2::python
diff --git a/libs/plugin_python/src/mobase/wrappers/wrappers.h b/libs/plugin_python/src/mobase/wrappers/wrappers.h
new file mode 100644
index 0000000..43dd574
--- /dev/null
+++ b/libs/plugin_python/src/mobase/wrappers/wrappers.h
@@ -0,0 +1,109 @@
+#ifndef PYTHON_WRAPPERS_WRAPPERS_H
+#define PYTHON_WRAPPERS_WRAPPERS_H
+
+#include <any>
+#include <map>
+#include <typeindex>
+
+#include <pybind11/pybind11.h>
+
+#include <QList>
+#include <QObject>
+
+#include <uibase/iplugingame.h>
+
+namespace mo2::python {
+
+ /**
+ * @brief Add bindings for the various classes in uibase that are not
+ * wrappers (i.e., cannot be extended from Python).
+ *
+ * @param m Python module to add bindings to.
+ */
+ void add_basic_bindings(pybind11::module_ m);
+
+ /**
+ * @brief Add bindings for the various custom widget classes in uibase that
+ * cannot be extended from Python.
+ *
+ * @param m Python module to add bindings to.
+ */
+ void add_widget_bindings(pybind11::module_ m);
+
+ /**
+ * @brief Add bindings for the various utilities classes and functions in uibase
+ * that cannot be extended from Python.
+ *
+ * @param m Python module to add bindings to.
+ */
+ void add_utils_bindings(pybind11::module_ m);
+
+ /**
+ * @brief Add bindings for the uibase wrappers to the given module. uibase
+ * wrappers include classes from uibase that can be extended from Python but
+ * are neither plugins nor game features (e.g., ISaveGame).
+ *
+ * @param m Python module to add bindings to.
+ */
+ void add_wrapper_bindings(pybind11::module_ m);
+
+ /**
+ * @brief Add bindings for the various plugin classes in uibase that can be
+ * extended from Python.
+ *
+ * @param m Python module to add bindings to.
+ */
+ void add_plugins_bindings(pybind11::module_ m);
+
+ /**
+ * @brief Extract plugins from the given object. For each plugin implemented, an
+ * object is returned.
+ *
+ * The returned QObject* are set as owner of the given object so that the Python
+ * object lifetime does not end immediately after returning to C++.
+ *
+ * @param object Python object to extract plugins from.
+ *
+ * @return a QObject* for each plugin implemented by the given object.
+ */
+ QList<QObject*> extract_plugins(pybind11::object object);
+
+ /**
+ * @brief Add bindings for the various game features classes in uibase that
+ * can be extended from Python.
+ *
+ * @param m Python module to add bindings to.
+ */
+ void add_game_feature_bindings(pybind11::module_ m);
+
+ /**
+ * @brief Add bindings for IGameFeatures.
+ *
+ * @param m Python module to add bindings to.
+ */
+ void add_igamefeatures_classes(pybind11::module_ m);
+
+ /**
+ * @brief Extract the game feature corresponding to the given Python type.
+ *
+ * @param gameFeatures Game features to extract the feature from.
+ * @param type Type of the feature to extract.
+ *
+ * @return the feature from the game, or None is the game as no such feature.
+ */
+ pybind11::object extract_feature(MOBase::IGameFeatures const& gameFeatures,
+ pybind11::object type);
+
+ /**
+ * @brief Unregister the game feature corresponding to the given Python type.
+ *
+ * @param gameFeatures Game features to unregister the feature from.
+ * @param type Type of the feature to unregister.
+ *
+ * @return the feature from the game, or None is the game as no such feature.
+ */
+ int unregister_feature(MOBase::IGameFeatures& gameFeatures, pybind11::object type);
+
+} // namespace mo2::python
+
+#endif // PYTHON_WRAPPERS_WRAPPERS_H