diff options
Diffstat (limited to 'libs/plugin_python/src/mobase')
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 |
