diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..c040316 --- /dev/null +++ b/.clang-format @@ -0,0 +1,21 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 4 +--- +Language: Cpp +# Force pointers to the type for C++. +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -4 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: All +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Stroustrup +ColumnLimit: 88 diff --git a/.gitignore b/.gitignore index cf71be7..5fc3c13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ +# build edit CMakeLists.txt.user /msbuild.log /*std*.log /*build + +# python +tests/**/__pycache__ + +# IDE +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt index d8520f9..1770be5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,30 @@ else() include(${CMAKE_CURRENT_LIST_DIR}/../cmake_common/mo2.cmake) endif() +set(PYTHON_BUILD_PATH ${PYTHON_ROOT}/PCBuild/amd64) +set(PYVERSION 310) + +set(PYBIND11_FINDPYTHON true) +set(PYTHON_EXECUTABLE ${PYTHON_BUILD_PATH}/python.exe) +set(PYTHON_INCLUDE_DIRS ${PYTHON_ROOT}/Include) +set(PYTHON_LIBRARIES ${MO2_INSTALL_LIBS_PATH}/python${PYVERSION}.lib) + +add_subdirectory(${MO2_BUILD_PATH}/pybind11 ${CMAKE_CURRENT_BINARY_DIR}/pybind11) + project(plugin_python) # order matters! +add_subdirectory(src/pybind11-qt) +add_subdirectory(src/mobase) add_subdirectory(src/runner) add_subdirectory(src/proxy) + +# force plugin_python to build mobase +add_dependencies(plugin_python mobase) +set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT plugin_python) + +set(PLUGIN_PYTHON_TESTS ${PLUGIN_PYTHON_TESTS} CACHE BOOL "build tests for plugin_python") +if (PLUGIN_PYTHON_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/src/mobase/CMakeLists.txt b/src/mobase/CMakeLists.txt new file mode 100644 index 0000000..fb596e2 --- /dev/null +++ b/src/mobase/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.16) + +pybind11_add_module(mobase MODULE) +mo2_configure_library(mobase + SOURCE_TREE + WARNINGS OFF + AUTOMOC ON + TRANSLATIONS OFF + PRIVATE_DEPENDS uibase Qt::Core +) +target_link_libraries(mobase PRIVATE pybind11::qt) +target_include_directories(mobase PRIVATE ${PYTHON_ROOT}/Include) + +install(TARGETS mobase DESTINATION bin/plugins/plugin_python/libs) diff --git a/src/mobase/deprecation.cpp b/src/mobase/deprecation.cpp new file mode 100644 index 0000000..860d071 --- /dev/null +++ b/src/mobase/deprecation.cpp @@ -0,0 +1,54 @@ +#include "deprecation.h" + +#include +#include + +#include + +#include + +#include "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> 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 filename = callable_frame[-1].attr("filename").cast(); + auto function = callable_frame[-1].attr("function").cast(); + auto lineno = callable_frame[-1].attr("lineno").cast(); + + // 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/src/mobase/deprecation.h b/src/mobase/deprecation.h new file mode 100644 index 0000000..d9a445c --- /dev/null +++ b/src/mobase/deprecation.h @@ -0,0 +1,27 @@ +#ifndef PYTHONRUNNER_UTILS_H +#define PYTHONRUNNER_UTILS_H + +#include + +#include + +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/src/mobase/mobase.cpp b/src/mobase/mobase.cpp new file mode 100644 index 0000000..8edcb39 --- /dev/null +++ b/src/mobase/mobase.cpp @@ -0,0 +1,82 @@ +#pragma warning(disable : 4100) +#pragma warning(disable : 4996) + +#include +#include + +#include "pybind11_all.h" + +#include "wrappers/pyfiletree.h" +#include "wrappers/wrappers.h" + +// TODO: remove these include (only for testing) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace MOBase; +namespace py = pybind11; + +PYBIND11_MODULE(mobase, m) +{ + using namespace mo2::python; + + py::module_::import("PyQt6.QtCore"); + 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_plugins_bindings(m); + + // widgets + // + py::module_ widgets( + py::reinterpret_borrow(PyImport_AddModule("mobase.widgets"))); + m.attr("widgets") = widgets; + mo2::python::add_widget_bindings(widgets); + + // functions + // + m.def("getFileVersion", &MOBase::getFileVersion, py::arg("filepath")); + m.def("getProductVersion", &MOBase::getProductVersion, py::arg("executable")); + m.def("getIconForExecutable", &MOBase::iconForExecutable, py::arg("executable")); + + // expose MoVariant - MoVariant is a fake object whose only purpose is to be + // used as a type-hint on the python side (e.g., def foo(x: + // mobase.MoVariant)) + // + // the real MoVariant is defined in the generated stubs, since it's only + // relevant when doing type-checking, but this needs to be defined, + // otherwise MoVariant is not found when actually running plugins through + // MO2, making them crash + m.attr("MoVariant") = py::none(); + + // private stuff for debugging/test + py::module_ moprivate( + py::reinterpret_borrow(PyImport_AddModule("mobase.private"))); + m.attr("private") = moprivate; + + // 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/src/mobase/pybind11_all.h b/src/mobase/pybind11_all.h new file mode 100644 index 0000000..93e8e08 --- /dev/null +++ b/src/mobase/pybind11_all.h @@ -0,0 +1,15 @@ +#ifndef PYTHON_PYBIND11_ALL_H +#define PYTHON_PYBIND11_ALL_H + +#include +#include +#include +#include + +#include "pybind11_utils/functional.h" + +#include "pybind11_qt/pybind11_qt.h" + +#include "pybind11_utils/shared_cpp_owner.h" + +#endif diff --git a/src/mobase/pybind11_utils/functional.h b/src/mobase/pybind11_utils/functional.h new file mode 100644 index 0000000..d23be5a --- /dev/null +++ b/src/mobase/pybind11_utils/functional.h @@ -0,0 +1,7 @@ +#ifndef PYTHON_PYBIND11_FUNCTIONAL_H +#define PYTHON_PYBIND11_FUNCTIONAL_H + +// TODO +#include + +#endif diff --git a/src/mobase/pybind11_utils/shared_cpp_owner.h b/src/mobase/pybind11_utils/shared_cpp_owner.h new file mode 100644 index 0000000..23d2e0d --- /dev/null +++ b/src/mobase/pybind11_utils/shared_cpp_owner.h @@ -0,0 +1,98 @@ +#ifndef PYTHON_PYBIND11_SHARED_CPP_OWNER_H +#define PYTHON_PYBIND11_SHARED_CPP_OWNER_H + +// pybind11 has some issues when a Python classes extend a C++ wrapper since the Python +// object is not kept alive alongside the returned object +// +// there is a pybind11 branch called "smart_holder" that tries to solve this in a very +// complicated way (with many other features) +// +// here, we simply use a custom type_caster<> for the classes we need - see the actual +// definition in mo2::python::detail below +// +// IMPORTANT: this only works for classes that are managed by shared_ptr on the C++ +// side, not Qt object +// + +// TODO: WIP for Qt object + +namespace mo2::python::detail { + + template + struct shared_cpp_owner_caster + : pybind11::detail::copyable_holder_caster { + + // note that the actual holder type might be different in term of constness + using type = Type; + using holder_type = SharedType; + + using base = pybind11::detail::copyable_holder_caster; + using base::holder; + using base::value; + + // in load, we use the default type_caster<> to extract the shared pointer, then + // we replace it by a custom one + // + // the custom shared_ptr<> holds the py::object BUT does not really manage the + // C++ object because it will ref-count but not delete it + // + // this should work because here it's how it works: + // - the Python object holds a standard shared_ptr<> for the C++ object -> the + // C++ object remains alive as long as the Python one remains alive + // - the C++ object holds a shared_ptr<> that manages the python object -> the + // Python object remains alive as-long as there is a shared_ptr<> on the C++ + // side + // + bool load(pybind11::handle src, bool convert) + { + namespace py = pybind11; + + if (!base::load(src, convert)) { + return false; + } + + holder.reset(holder.get(), [pyobj = py::reinterpret_borrow( + src)](auto*) mutable { + py::gil_scoped_acquire s; + pyobj = std::move(py::none()); + + // we do NOT delete the object here - if this was the last reference to + // the Python object, the Python object will delete it + }); + + return true; + } + + // cast simply forward to the original type_caster<> + // + static pybind11::handle cast(const holder_type& src, + pybind11::return_value_policy policy, + pybind11::handle parent) + { + return base::cast(src, policy, parent); + } + }; + +} // namespace mo2::python::detail + +#define MO2_PYBIND11_SHARED_CPP_HOLDER(Type) \ + namespace pybind11::detail { \ + template <> \ + struct type_caster> \ + : mo2::python::detail::shared_cpp_owner_caster> { \ + }; \ + template <> \ + struct type_caster> \ + : mo2::python::detail::shared_cpp_owner_caster< \ + Type, std::shared_ptr> { \ + }; \ + } + +#include +#include + +MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::IPluginRequirement) +MO2_PYBIND11_SHARED_CPP_HOLDER(MOBase::ISaveGame) + +#endif diff --git a/src/mobase/wrappers/basic_classes.cpp b/src/mobase/wrappers/basic_classes.cpp new file mode 100644 index 0000000..4db5848 --- /dev/null +++ b/src/mobase/wrappers/basic_classes.cpp @@ -0,0 +1,752 @@ +#include "wrappers.h" + +#include "../pybind11_all.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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) + { + py::enum_(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_(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_(m, "VersionInfo") + .def(py::init(), "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(), "major"_a, + "minor"_a, "subminor"_a, "subsubminor"_a, + "release_type"_a = VersionInfo::RELEASE_FINAL) + .def(py::init(), "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_(m, "ExecutableInfo") + .def(py::init(), "title"_a, "binary"_a) + .def("withArgument", &ExecutableInfo::withArgument, "argument"_a) + .def("withWorkingDirectory", &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_(m, "ExecutableForcedLoadSetting") + .def(py::init(), "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); + } + + void add_modinterface_classes(py::module_ m) + { + py::enum_(m, "EndorsedState") + .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_(m, "TrackedState") + .value("TRACKED_FALSE", TrackedState::TRACKED_FALSE) + .value("TRACKED_TRUE", TrackedState::TRACKED_TRUE) + .value("TRACKED_UNKNOWN", TrackedState::TRACKED_UNKNOWN); + + py::class_(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("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(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(iModRepositoryBridge, "_object"); + + py::class_(m, "ModRepositoryFileInfo") + .def(py::init(), "other"_a) + .def(py::init(), "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); + } + + void add_guessedstring_classes(py::module_ m) + { + py::enum_(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_>(m, "GuessedString") + .def(py::init<>()) + .def(py::init(), "value"_a, + "quality"_a = EGuessQuality::GUESS_USER) + .def("update", + py::overload_cast(&GuessedValue::update), + "value"_a) + .def("update", + py::overload_cast( + &GuessedValue::update), + "value"_a, "quality"_a) + + // Methods to simulate the assignment operator: + .def("reset", + [](GuessedValue* gv) { + *gv = GuessedValue(); + return gv; + }) + .def( + "reset", + [](GuessedValue* gv, const QString& value, EGuessQuality eq) { + *gv = GuessedValue(value, eq); + return gv; + }, + "value"_a, "quality"_a) + .def( + "reset", + [](GuessedValue* gv, const GuessedValue& 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* gv, + std::function(QString const&)> fn) { + gv->setFilter([fn](QString& s) { + auto ret = fn(s); + return std::visit( + [&s](auto v) { + if constexpr (std::is_same_v) { + s = v; + return true; + } + else if constexpr (std::is_same_v) { + return v; + } + }, + ret); + }); + }, + "filter"_a) + + // this makes a copy in python but it more practical than + // exposing an iterator + .def("variants", &GuessedValue::variants) + .def("__str__", &MOBase::GuessedValue::operator const QString&); + + // implicit conversion from QString - this allows passing Python string to + // function expecting GuessedValue + py::implicitly_convertible>(); + } + + void add_ipluginlist_classes(py::module_ m) + { + py::enum_(m, "PluginState") + .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_(m, "IPluginList") + .def("state", &MOBase::IPluginList::state, "name"_a) + .def("priority", &MOBase::IPluginList::priority, "name"_a) + .def("setPriority", &MOBase::IPluginList::setPriority, "name"_a, + "priority"_a) + .def("loadOrder", &MOBase::IPluginList::loadOrder, "name"_a) + .def("isMaster", &MOBase::IPluginList::isMaster, "name"_a) + .def("masters", &MOBase::IPluginList::masters, "name"_a) + .def("origin", &MOBase::IPluginList::origin, "name"_a) + .def("onRefreshed", &MOBase::IPluginList::onRefreshed, "callback"_a) + .def("onPluginMoved", &MOBase::IPluginList::onPluginMoved, "callback"_a) + + // Kept but deprecated for backward compatibility: + .def( + "onPluginStateChanged", + [](IPluginList* modList, + const std::function& + 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("onPluginStateChanged", &MOBase::IPluginList::onPluginStateChanged, + "callback"_a) + .def("pluginNames", &MOBase::IPluginList::pluginNames) + .def("setState", &MOBase::IPluginList::setState, ("name"_a, "state")) + .def("setLoadOrder", &MOBase::IPluginList::setLoadOrder, "loadorder"_a); + } + + void add_imodlist_classes(py::module_ m) + { + py::enum_(m, "ModState") + .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_(m, "IModList") + .def("displayName", &MOBase::IModList::displayName, "name"_a) + .def("allMods", &MOBase::IModList::allMods) + .def("allModsByProfilePriority", + &MOBase::IModList::allModsByProfilePriority, + "profile"_a = static_cast(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( + &MOBase::IModList::setActive), + "names"_a, "active"_a) + .def("setActive", + py::overload_cast(&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& 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) + { + py::class_(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_(m, "IOrganizer") + .def("createNexusBridge", &IOrganizer::createNexusBridge, + py::return_value_policy::reference) + .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::appVersion) + .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(&IOrganizer::isPluginEnabled, py::const_), + "plugin"_a) + .def("isPluginEnabled", + py::overload_cast(&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", &IOrganizer::installMod, + py::return_value_policy::reference, "filename"_a, + "name_suggestion"_a = "") + .def("resolvePath", &IOrganizer::resolvePath, "filename"_a) + .def("listDirectories", &IOrganizer::listDirectories, "directory"_a) + + // "provide multiple overloads of findFiles + .def( + "findFiles", + [](const IOrganizer* o, QString const& p, + std::function 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, QString const& p, const QStringList& gf) { + return o->findFiles(p, gf); + }, + "path"_a, "patterns"_a) + .def( + "findFiles", + [](const IOrganizer* o, QString const& p, const QString& f) { + return o->findFiles(p, QStringList{f}); + }, + "path"_a, "pattern"_a) + + .def("getFileOrigins", &IOrganizer::getFileOrigins, "filename"_a) + .def("findFileInfos", &IOrganizer::findFileInfos, "path"_a, "filter"_a) + + .def("virtualFileTree", &IOrganizer::virtualFileTree) + + .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("profile", &IOrganizer::profile, py::return_value_policy::reference) + + // 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 QString& executable, const QStringList& args, + const QString& 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>(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", &IOrganizer::onAboutToRun, "callback"_a) + .def("onFinishedRun", &IOrganizer::onFinishedRun, "callback"_a) + .def("onUserInterfaceInitialized", &IOrganizer::onUserInterfaceInitialized, + "callback"_a) + .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 const& func) { + o->onPluginEnabled(func); + }, + "callback"_a) + .def( + "onPluginEnabled", + [](IOrganizer* o, QString const& name, + std::function const& func) { + o->onPluginEnabled(name, func); + }, + "name"_a, "callback"_a) + .def( + "onPluginDisabled", + [](IOrganizer* o, std::function const& func) { + o->onPluginDisabled(func); + }, + "callback"_a) + .def( + "onPluginDisabled", + [](IOrganizer* o, QString const& name, + std::function 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& 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_idownload_manager_classes(py::module_ m) + { + py::class_(m, "IDownloadManager") + .def("startDownloadURLs", &IDownloadManager::startDownloadURLs, "urls"_a) + .def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile, + "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) + { + py::class_(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 modName, + QString archive, int modId) { + auto result = m->installArchive(modName, archive, modId); + return std::make_tuple(result, static_cast(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_(m, "PluginSetting") + .def(py::init(), "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_(m, "PluginRequirementFactory") + // pluginDependency + .def_static("pluginDependency", + py::overload_cast( + &PluginRequirementFactory::pluginDependency), + "plugins"_a) + .def_static("pluginDependency", + py::overload_cast( + &PluginRequirementFactory::pluginDependency), + "plugins"_a) + // gameDependency + .def_static("gameDependency", + py::overload_cast( + &PluginRequirementFactory::gameDependency), + "plugins"_a) + .def_static("gameDependency", + py::overload_cast( + &PluginRequirementFactory::gameDependency), + "plugins"_a) + // diagnose + .def_static("diagnose", &PluginRequirementFactory::diagnose, "diagnose"_a) + // basic + .def_static("basic", &PluginRequirementFactory::basic, "checker"_a, + "description"_a); + + py::class_(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 fmt::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_(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 py::make_tuple(active, supported); + }) + .def("absoluteIniFilePath", &IProfile::absoluteIniFilePath, "inifile"_a); + + add_ipluginlist_classes(m); + add_imodlist_classes(m); + add_idownload_manager_classes(m); + add_iinstallation_manager_classes(m); + add_iorganizer_classes(m); + } + +} // namespace mo2::python diff --git a/src/mobase/wrappers/game_features.cpp b/src/mobase/wrappers/game_features.cpp new file mode 100644 index 0000000..c08bdf8 --- /dev/null +++ b/src/mobase/wrappers/game_features.cpp @@ -0,0 +1,377 @@ +#include "wrappers.h" + +#include + +#include "../pybind11_all.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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_PURE(bool, GamePlugins, lightPluginsAreSupported, ); + } + }; + + 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 fileTree) const override + { + PYBIND11_OVERRIDE_PURE(CheckReturn, ModDataChecker, dataLooksValid, + fileTree); + } + + std::shared_ptr + fix(std::shared_ptr fileTree) const override + { + PYBIND11_OVERRIDE(std::shared_ptr, ModDataChecker, fix, + fileTree); + } + }; + + class PyModDataContent : public ModDataContent { + public: + std::vector getAllContents() const override + { + PYBIND11_OVERRIDE_PURE(std::vector, ModDataContent, + getAllContents, ); + ; + } + std::vector + getContentsFor(std::shared_ptr fileTree) const override + { + PYBIND11_OVERRIDE_PURE(std::vector, 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(QString, ScriptExtender, PluginPath, ); + } + + QString loaderName() const override + { + PYBIND11_OVERRIDE_PURE(QString, ScriptExtender, loaderName, ); + } + + QString loaderPath() const override + { + PYBIND11_OVERRIDE_PURE(QString, 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 PyPyUnmanagedMods : 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(QFileInfo, UnmanagedMods, referenceFile, modName); + } + QStringList secondaryFiles(const QString& modName) const override + { + PYBIND11_OVERRIDE_PURE(QStringList, UnmanagedMods, secondaryFiles, modName); + } + }; + + void add_game_feature_bindings(pybind11::module_ m) + { + // BSAInvalidation + + py::class_(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_(m, "DataArchives") + .def(py::init<>()) + .def("vanillaArchives", &DataArchives::vanillaArchives) + .def("archives", &DataArchives::archives, "profile"_a) + .def("addArchive", &DataArchives::addArchive, + ("profile"_a, "index", "name")) + .def("removeArchive", &DataArchives::removeArchive, "profile"_a, "name"_a); + + // GamePlugins + + py::class_(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); + + // LocalSavegames + + py::class_(m, "LocalSavegames") + .def(py::init<>()) + .def("mappings", &LocalSavegames::mappings, "profile_save_dir"_a) + .def("prepareProfile", &LocalSavegames::prepareProfile, "profile"_a); + + // ModDataChecker + + py::class_ pyModDataChecker(m, + "ModDataChecker"); + + py::enum_(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_ pyModDataContent(m, + "ModDataContent"); + + py::class_(pyModDataContent, "Content") + .def(py::init(), "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_(m, "SaveGameInfo") + .def(py::init<>()) + .def("getMissingAssets", &SaveGameInfo::getMissingAssets, "save"_a) + .def("getSaveGameWidget", &SaveGameInfo::getSaveGameWidget, + py::return_value_policy::reference, "parent"_a, "[optional]"); + + // ScriptExtender + + py::class_(m, "ScriptExtender") + .def(py::init<>()) + .def("BinaryName", &ScriptExtender::BinaryName) + .def("PluginPath", &ScriptExtender::PluginPath) + .def("loaderName", &ScriptExtender::loaderName) + .def("loaderPath", &ScriptExtender::loaderPath) + .def("savegameExtension", &ScriptExtender::savegameExtension) + .def("isInstalled", &ScriptExtender::isInstalled) + .def("getExtenderVersion", &ScriptExtender::getExtenderVersion) + .def("getArch", &ScriptExtender::getArch); + + // UnmanagedMods + + py::class_(m, "UnmanagedMods") + .def(py::init<>()) + .def("mods", &UnmanagedMods::mods, "official_only"_a) + .def("displayName", &UnmanagedMods::displayName, "mod_name"_a) + .def("referenceFile", &UnmanagedMods::referenceFile, "mod_name"_a) + .def("secondaryFiles", &UnmanagedMods::secondaryFiles, "mod_name"_a); + } + +} // namespace mo2::python + +namespace mo2::python { + + class GameFeaturesHelper { + using GameFeatures = std::tuple; + + template + static void helper(F&& f, std::index_sequence) + { + (f(static_cast*>(nullptr)), ...); + } + + public: + // apply the function f on a null-pointer of type Feature* for each game + // feature + template + static void apply(F&& f) + { + helper(f, std::make_index_sequence>{}); + } + }; + + pybind11::object extract_feature(IPluginGame const& game, pybind11::object type) + { + py::object py_feature = py::none(); + GameFeaturesHelper::apply([&](Feature* feature) { + if (py::type::of().is(type)) { + py_feature = py::cast(game.feature(), + py::return_value_policy::reference); + } + }); + return py_feature; + } + + pybind11::dict extract_feature_list(IPluginGame const& game) + { + // constructing a dict from class name to actual object + py::dict dict; + GameFeaturesHelper::apply([&](Feature* feature) { + dict[py::type::of()] = + py::cast(game.feature(), py::return_value_policy::reference); + }); + return dict; + } + + std::map + convert_feature_list(py::dict const& py_features) + { + std::map features; + GameFeaturesHelper::apply([&](Feature* feature) { + const auto py_type = py::type::of(); + if (py_features.contains(py_type)) { + features[std::type_index(typeid(Feature))] = + py_features[py_type].cast(); + } + }); + return features; + } + +} // namespace mo2::python diff --git a/src/mobase/wrappers/pyfiletree.cpp b/src/mobase/wrappers/pyfiletree.cpp new file mode 100644 index 0000000..d102381 --- /dev/null +++ b/src/mobase/wrappers/pyfiletree.cpp @@ -0,0 +1,333 @@ +#include "pyfiletree.h" + +#include +#include + +#include "../pybind11_all.h" + +#include +#include + +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::detail { + + // filetree implementation for testing purpose + // + class PyFileTree : public IFileTree { + public: + using callback_t = std::function; + + PyFileTree(std::shared_ptr parent, QString name, + callback_t callback) + : FileTreeEntry(parent, name), IFileTree(), m_Callback(callback) + { + } + + std::shared_ptr 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 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 + makeDirectory(std::shared_ptr parent, + QString name) const override + { + return std::make_shared(parent, name, m_Callback); + } + + bool + doPopulate(std::shared_ptr parent, + std::vector>& entries) const override + { + return true; + } + std::shared_ptr doClone() const override + { + return std::make_shared(nullptr, name(), m_Callback); + } + + private: + callback_t m_Callback; + }; + +} // namespace mo2::detail + +#pragma optimize("", off) + +namespace pybind11 { + const void* polymorphic_type_hook::get(const FileTreeEntry* src, + const std::type_info*& type) + { + if (auto p = dynamic_cast(src)) { + type = &typeid(IFileTree); + return p; + } + return src; + } +} // namespace pybind11 + +namespace mo2::python { + + void add_ifiletree_bindings(pybind11::module_& m) + { + + // FileTreeEntry Scope: + auto fileTreeEntryClass = + py::class_>(m, + "FileTreeEntry"); + + // we do not use the enum directly, we will mostly bind the FileTypes + // (with an S) + py::enum_(fileTreeEntryClass, "FileType", + py::arithmetic{}); + py::class_(fileTreeEntryClass, "FileTypes") + .def_property_readonly_static("FILE", + [](py::object) { + return FileTreeEntry::FILE; + }) + .def_property_readonly_static("DIRECTORY", + [](py::object) { + return FileTreeEntry::DIRECTORY; + }) + .def_property_readonly_static("FILE_OR_DIRECTORY", + [](py::object) { + return FileTreeEntry::FILE_OR_DIRECTORY; + }) + + .def(py::self == py::self) + .def(py::self != py::self) + .def(py::self | py::self); + py::implicitly_convertible(); + + fileTreeEntryClass + .def_property_readonly_static("FILE", + [](py::object) { + return FileTreeEntry::FILE; + }) + .def_property_readonly_static("DIRECTORY", + [](py::object) { + return FileTreeEntry::DIRECTORY; + }) + .def_property_readonly_static("FILE_OR_DIRECTORY", [](py::object) { + return FileTreeEntry::FILE_OR_DIRECTORY; + }); + + fileTreeEntryClass + + .def("isFile", &FileTreeEntry::isFile) + .def("isDir", &FileTreeEntry::isDir) + // Forcing the conversion to FileTypeS to avoid having to expose + // FileType in python: + .def("fileType", &FileTreeEntry::fileType) + // This should probably not be exposed in python since we provide + // automatic downcast: .def("getTree", + // static_cast(FileTreeEntry::*)()>(&FileTreeEntry::astree)) + .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), "[optional]") + .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 other) { + return entry == other.get(); + }) + + // Special methods for debug: + .def("__repr__", [](const FileTreeEntry* entry) { + return "FileTreeEntry(\"" + entry->name() + "\")"; + }); + + // IFileTree scope: + auto iFileTreeClass = + py::class_>( + m, "IFileTree", py::multiple_inheritance()); + + py::enum_(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_(iFileTreeClass, "WalkReturn") + .value("CONTINUE", IFileTree::WalkReturn::CONTINUE) + .value("STOP", IFileTree::WalkReturn::STOP) + .value("SKIP", IFileTree::WalkReturn::SKIP) + .export_values(); + + // Non-mutable operations: + iFileTreeClass.def("exists", + py::overload_cast( + &IFileTree::exists, py::const_), + py::arg("path"), + py::arg("type") = IFileTree::FILE_OR_DIRECTORY); + iFileTreeClass.def( + "find", py::overload_cast(&IFileTree::find), + py::arg("path"), py::arg("type") = IFileTree::FILE_OR_DIRECTORY, + "[optional]"); + iFileTreeClass.def("pathTo", &IFileTree::pathTo, py::arg("entry"), + py::arg("sep") = "\\"); + + // Note: walk() would probably be better as a generator in python, but + // it is likely impossible to construct from the C++ walk() method. + iFileTreeClass.def("walk", &IFileTree::walk, py::arg("callback"), + py::arg("sep") = "\\"); + + // 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 other, bool returnOverwrites) + -> std::variant { + 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 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 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 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(&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 { + return std::make_shared(nullptr, "", callback); + }, + py::arg("callback") = mo2::detail::PyFileTree::callback_t{}); + } + +} // namespace mo2::python + +#pragma optimize("", on) diff --git a/src/mobase/wrappers/pyfiletree.h b/src/mobase/wrappers/pyfiletree.h new file mode 100644 index 0000000..25655d2 --- /dev/null +++ b/src/mobase/wrappers/pyfiletree.h @@ -0,0 +1,34 @@ +#ifndef MO2_PYTHON_FILETREE_H +#define MO2_PYTHON_FILETREE_H + +#include "../pybind11_all.h" + +#include + +namespace pybind11 { + template <> + struct polymorphic_type_hook { + 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/src/mobase/wrappers/pyplugins.cpp b/src/mobase/wrappers/pyplugins.cpp new file mode 100644 index 0000000..36f11ba --- /dev/null +++ b/src/mobase/wrappers/pyplugins.cpp @@ -0,0 +1,272 @@ +#include "wrappers.h" + +#include + +#include "pyplugins.h" + +namespace py = pybind11; +using namespace pybind11::literals; +using namespace MOBase; + +namespace mo2::python { + + std::map PyPluginGame::featureList() const + { + py::dict pyFeatures = [this]() { + PYBIND11_OVERRIDE_PURE(py::dict, IPluginGame, _featureList, ); + }(); + + return convert_feature_list(pyFeatures); + } + + // this one is kind of big so it has its own function + void add_iplugingame_bindings(pybind11::module_ m) + { + py::enum_(m, "LoadOrderMechanism") + .value("FileTime", IPluginGame::LoadOrderMechanism::FileTime) + .value("PluginsTxt", IPluginGame::LoadOrderMechanism::PluginsTxt) + + .value("FILE_TIME", IPluginGame::LoadOrderMechanism::FileTime) + .value("PLUGINS_TXT", IPluginGame::LoadOrderMechanism::PluginsTxt); + + py::enum_(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_(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_>( + m, "IPluginGame", py::multiple_inheritance()) + .def(py::init<>()) + + .def("featureList", &extract_feature_list) + .def("feature", &extract_feature, "feature_type"_a, + py::return_value_policy::reference) + + .def("detectGame", &IPluginGame::detectGame) + .def("gameName", &IPluginGame::gameName) + .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("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("gameVariants", &IPluginGame::gameVariants) + .def("setGameVariant", &IPluginGame::setGameVariant, "variant"_a) + .def("binaryName", &IPluginGame::binaryName) + .def("gameShortName", &IPluginGame::gameShortName) + .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); + } + + // multiple installers + void add_iplugininstaller_bindings(pybind11::module_ m) + { + py::enum_(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); + + // 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_, IPlugin, + std::unique_ptr>( + 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::parentWidget) + .def("_manager", &PyPluginInstallerBase::manager, + py::return_value_policy::reference); + + py::class_>( + 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& modName, + std::shared_ptr& 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_>( + 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_>( + 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_>(m, "IPlugin", + py::multiple_inheritance()) + .def(py::init<>()); + + py::class_>( + m, "IPluginFileMapper", py::multiple_inheritance()) + .def(py::init<>()) + .def("mappings", &IPluginFileMapper::mappings); + + py::class_>( + 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_>( + 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_>( + m, "IPluginPreview", py::multiple_inheritance()) + .def(py::init<>()) + .def("supportedExtensions", &IPluginPreview::supportedExtensions) + .def("genFilePreview", &IPluginPreview::genFilePreview, "filename"_a, + "max_size"_a); + + py::class_>( + 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 objects; + + template + void append_if_instance(pybind11::object plugin_obj) + { + if (py::isinstance(plugin_obj)) { + objects.append(plugin_obj.cast()); + } + } + }; + + QList 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(plugin_obj); + helper.append_if_instance(plugin_obj); + + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + + helper.append_if_instance(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(plugin_obj); + helper.append_if_instance(plugin_obj); + + if (helper.objects.isEmpty()) { + helper.append_if_instance(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/src/mobase/wrappers/pyplugins.h b/src/mobase/wrappers/pyplugins.h new file mode 100644 index 0000000..0610f16 --- /dev/null +++ b/src/mobase/wrappers/pyplugins.h @@ -0,0 +1,493 @@ +#ifndef PYTHON_WRAPPERS_PYPLUGINS_H +#define PYTHON_WRAPPERS_PYPLUGINS_H + +#include "../pybind11_all.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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 PyPluginBaseNoFinal : public PluginBase { + public: + using PluginBase::PluginBase; + + ~PyPluginBaseNoFinal() + { + std::cout << "~PyPluginBaseNoFinal() " << typeid(this).name() << std::endl; + } + + 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 settings() const override + { + PYBIND11_OVERRIDE_PURE(QList, PluginBase, settings, ); + } + }; + + template + class PyPluginBase : public PyPluginBaseNoFinal { + public: + using PyPluginBaseNoFinal::PyPluginBaseNoFinal; + + std::vector> requirements() const + { + PYBIND11_OVERRIDE(std::vector>, + 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 { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin) + }; + + class PyPluginFileMapper : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginFileMapper) + public: + MappingType mappings() const override + { + PYBIND11_OVERRIDE_PURE(MappingType, IPluginFileMapper, mappings, ); + } + }; + + class PyPluginDiagnose : public PyPluginBase { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose) + public: + std::vector activeProblems() const + { + PYBIND11_OVERRIDE_PURE(std::vector, IPluginDiagnose, + activeProblems, ); + } + + QString shortDescription(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(QString, IPluginDiagnose, shortDescription, key); + } + + QString fullDescription(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(QString, IPluginDiagnose, fullDescription, key); + } + + bool hasGuidedFix(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(bool, IPluginDiagnose, hasGuidedFix, key); + } + + void startGuidedFix(unsigned int key) const + { + PYBIND11_OVERRIDE_PURE(void, IPluginDiagnose, startGuidedFix, key); + } + + // we need to bring this in public scope + using IPluginDiagnose::invalidate; + }; + + class PyPluginTool : public PyPluginBase { + 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 { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview) + public: + std::set supportedExtensions() const override + { + PYBIND11_OVERRIDE_PURE(std::set, IPluginPreview, + supportedExtensions, ); + } + + QWidget* genFilePreview(const QString& fileName, + const QSize& maxSize) const override + { + PYBIND11_OVERRIDE_PURE(QWidget*, IPluginPreview, genFilePreview, fileName, + maxSize); + } + }; + + class PyPluginModPage : public PyPluginBase { + 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 PyPluginInstallerBase : public PyPluginBase { + public: + using PyPluginBase::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 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 { + 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 supportedExtensions() const + { + PYBIND11_OVERRIDE_PURE(std::set, IPluginInstallerCustom, + supportedExtensions, ); + } + + EInstallResult install(GuessedValue& 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 { + Q_OBJECT + Q_INTERFACES( + MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) + public: + using py_install_return_type = + std::variant, + std::tuple, QString, int>>; + + EInstallResult install(GuessedValue& modName, + std::shared_ptr& 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; + if constexpr (std::is_same_v) { + return t; + } + else if constexpr (std::is_same_v>) { + tree = t; + return RESULT_SUCCESS; + } + else if constexpr (std::is_same_v< + type, std::tuple, + 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 { + 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, ); + } + void initializeProfile(const QDir& directory, + ProfileSettings settings) const override + { + PYBIND11_OVERRIDE_PURE(void, IPluginGame, initializeProfile, directory, + settings); + } + std::vector> + listSaves(QDir folder) const override + { + PYBIND11_OVERRIDE_PURE(std::vector>, + 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, ); + } + 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 executables() const override + { + PYBIND11_OVERRIDE_PURE(QList, IPluginGame, executables, ); + } + QList executableForcedLoads() const override + { + PYBIND11_OVERRIDE_PURE(QList, IPluginGame, + executableForcedLoads, ); + } + QString steamAPPId() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, steamAPPId, ); + } + QStringList primaryPlugins() const override + { + PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, primaryPlugins, ); + } + QStringList gameVariants() const override + { + PYBIND11_OVERRIDE_PURE(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, ); + } + QStringList primarySources() const override + { + PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, primarySources, ); + } + QStringList validShortNames() const override + { + PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, validShortNames, ); + } + QString gameNexusName() const override + { + PYBIND11_OVERRIDE_PURE(QString, IPluginGame, gameNexusName, ); + } + QStringList iniFiles() const override + { + PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, iniFiles, ); + } + QStringList DLCPlugins() const override + { + PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, DLCPlugins, ); + } + QStringList CCPlugins() const override + { + PYBIND11_OVERRIDE_PURE(QStringList, IPluginGame, CCPlugins, ); + } + LoadOrderMechanism loadOrderMechanism() const override + { + PYBIND11_OVERRIDE_PURE(LoadOrderMechanism, IPluginGame, + loadOrderMechanism, ); + } + SortMechanism sortMechanism() const override + { + PYBIND11_OVERRIDE_PURE(SortMechanism, IPluginGame, sortMechanism, ); + } + int nexusModOrganizerID() const override + { + PYBIND11_OVERRIDE_PURE(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, ); + } + + protected: + std::map featureList() const override; + }; + +} // namespace mo2::python + +#endif diff --git a/src/mobase/wrappers/widgets.cpp b/src/mobase/wrappers/widgets.cpp new file mode 100644 index 0000000..50e8406 --- /dev/null +++ b/src/mobase/wrappers/widgets.cpp @@ -0,0 +1,74 @@ +#include "wrappers.h" + +#include "../pybind11_all.h" + +#include + +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_(m, "TaskDialogButton") + .def(py::init(), + py::arg("text"), py::arg("description"), py::arg("button")) + .def(py::init(), py::arg("text"), + py::arg("button")); + + py::class_(m, "TaskDialog") + .def(py::init([](QWidget* parent, QString const& title, QString const& main, + QString const& content, QString const& details, + QMessageBox::Icon icon, + std::vector const& buttons, + std::variant> 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, + 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(nullptr), + py::arg("title") = "", py::arg("main") = "", py::arg("content") = "", + py::arg("details") = "", py::arg("icon") = QMessageBox::NoIcon, + py::arg("buttons") = std::vector{}, + 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("widget")) + .def("addContent", &TaskDialog::addContent, py::arg("widget")) + .def("exec", &TaskDialog::exec); + } + +} // namespace mo2::python diff --git a/src/mobase/wrappers/wrappers.cpp b/src/mobase/wrappers/wrappers.cpp new file mode 100644 index 0000000..42e04e2 --- /dev/null +++ b/src/mobase/wrappers/wrappers.cpp @@ -0,0 +1,116 @@ + +#include "wrappers.h" + +#include "../pybind11_all.h" + +#include +#include +#include +#include + +// IOrganizer must be bring here to properly compile the Python bindings of +// plugin requirements +#include +#include +#include +#include + +namespace py = pybind11; +using namespace MOBase; + +namespace mo2::python { + + class PyPluginRequirement : public IPluginRequirement { + public: + std::optional check(IOrganizer* organizer) const override + { + PYBIND11_OVERRIDE_PURE(std::optional, IPluginRequirement, check, + organizer); + }; + }; + + class PySaveGame : public ISaveGame { + public: + QString getFilepath() const override + { + PYBIND11_OVERRIDE_PURE(QString, 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 + { + PYBIND11_OVERRIDE_PURE(QStringList, 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_>(m, "ISaveGame") + .def(py::init<>()) + .def("getFilepath", &ISaveGame::getFilepath) + .def("getCreationTime", &ISaveGame::getCreationTime) + .def("getName", &ISaveGame::getName) + .def("getSaveGroupIdentifier", &ISaveGame::getSaveGroupIdentifier) + .def("allFiles", &ISaveGame::allFiles); + + // ISaveGameInfoWidget - custom holder to keep the Python object alive alongside + // the widget + + py::class_> + iSaveGameInfoWidget(m, "ISaveGameInfoWidget"); + iSaveGameInfoWidget.def(py::init<>()) + .def(py::init(), py::arg("parent")) + .def("setSave", &ISaveGameInfoWidget::setSave, py::arg("save")); + py::qt::add_qt_delegate(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_, + PyPluginRequirement> + iPluginRequirementClass(m, "IPluginRequirement"); + + py::class_(iPluginRequirementClass, "Problem") + .def(py::init(), py::arg("short_description"), + py::arg("long_description") = "") + .def("shortDescription", &IPluginRequirement::Problem::shortDescription) + .def("longDescription", &IPluginRequirement::Problem::longDescription); + + iPluginRequirementClass.def("check", &IPluginRequirement::check, + py::arg("organizer")); + } + +} // namespace mo2::python diff --git a/src/mobase/wrappers/wrappers.h b/src/mobase/wrappers/wrappers.h new file mode 100644 index 0000000..e097479 --- /dev/null +++ b/src/mobase/wrappers/wrappers.h @@ -0,0 +1,105 @@ +#ifndef PYTHON_WRAPPERS_WRAPPERS_H +#define PYTHON_WRAPPERS_WRAPPERS_H + +#include +#include +#include + +#include + +#include +#include + +#include + +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 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 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 Create the game feature corresponding to the given Python type from the + * given game. + * + * @param game Game plugin 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::IPluginGame const& game, + pybind11::object type); + + /** + * @brief Create Python dictionary mapping game feature classes to the game feature + * instances for the given game. + * + * @param game Game plugin to extract features from. + * + * @return a python dictionary mapping feature types (in Python) to feature objects. + */ + pybind11::dict extract_feature_list(MOBase::IPluginGame const& game); + + /** + * @brief Convert the given python map of features to a C++ one. + * + * @param py_features Python features to convert (type to feature). + * + * @return the map of features. + */ + std::map + convert_feature_list(pybind11::dict const& py_features); + +} // namespace mo2::python + +#endif // PYTHON_WRAPPERS_WRAPPERS_H diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index 8080d9c..54202a7 100644 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -5,4 +5,4 @@ mo2_configure_plugin(plugin_python WARNINGS OFF EXTRA_TRANSLATIONS ${CMAKE_CURRENT_SOURCE_DIR}/../runner) target_link_libraries(plugin_python PRIVATE pythonrunner) -mo2_install_target(plugin_python) +mo2_install_target(plugin_python FOLDER) diff --git a/src/proxy/plugin_python_en.ts b/src/proxy/plugin_python_en.ts index 639f459..b8eb0a7 100644 --- a/src/proxy/plugin_python_en.ts +++ b/src/proxy/plugin_python_en.ts @@ -4,70 +4,70 @@ ProxyPython - + Python Initialization failed - + On a previous start the Python Plugin failed to initialize. Do you want to try initializing python again (at the risk of another crash)? -Suggestion: Select "no", and click the warning sign for further help. Afterwards you have to re-enable the python plugin. + Suggestion: Select "no", and click the warning sign for further help.Afterwards you have to re-enable the python plugin. - + Python Proxy - + Proxy Plugin to allow plugins written in python to be loaded - + ModOrganizer path contains a semicolon - + Python DLL not found - + Invalid Python DLL - + Initializing Python failed - - + + invalid problem key %1 - + The path to Mod Organizer (%1) contains a semicolon. <br>While this is legal on NTFS drives, many softwares do not handle it correctly.<br>Unfortunately MO depends on libraries that seem to fall into that group.<br>As a result the python plugin cannot be loaded, and the only solution we canoffer is to remove the semicolon or move MO to a path without a semicolon. - + The Python plugin DLL was not found, maybe your antivirus deleted it. Re-installing MO2 might fix the problem. - + The Python plugin DLL is invalid, maybe your antivirus is blocking it. Re-installing MO2 and adding exclusions for it to your AV might fix the problem. - + The initialization of the Python plugin DLL failed, unfortunately without any details. @@ -75,12 +75,7 @@ Suggestion: Select "no", and click the warning sign for further help. QObject - - An unexpected C++ exception was thrown in python code. - - - - + An unknown exception was thrown in python code. diff --git a/src/proxy/proxypython.cpp b/src/proxy/proxypython.cpp index b5656a3..87f2455 100644 --- a/src/proxy/proxypython.cpp +++ b/src/proxy/proxypython.cpp @@ -16,227 +16,287 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with python proxy plugin. If not, see . */ - - #include "proxypython.h" + +#include + +#include +#include +#include +#include +#include + +#include "log.h" #include #include -#include -#include -#include -#include -#include -#include "log.h" +namespace fs = std::filesystem; using namespace MOBase; +// retrieve the path to the folder containing the proxy DLL +fs::path getPluginFolder() +{ + wchar_t path[MAX_PATH]; + HMODULE hm = NULL; + + if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCWSTR)&getPluginFolder, &hm) == 0) { + return {}; + } + if (GetModuleFileName(hm, path, sizeof(path)) == 0) { + return {}; + } + + return fs::path(path).parent_path(); +} + ProxyPython::ProxyPython() - : m_MOInfo{ nullptr }, - m_RunnerLib{ nullptr }, - m_Runner{ nullptr }, - m_LoadFailure(FailureType::NONE) + : m_MOInfo{nullptr}, m_RunnerLib{nullptr}, m_Runner{nullptr}, + m_LoadFailure(FailureType::NONE) { } - -bool ProxyPython::init(IOrganizer *moInfo) +bool ProxyPython::init(IOrganizer* moInfo) { - using CreatePythonRunner_func = IPythonRunner * (*)(); + using CreatePythonRunner_func = IPythonRunner* (*)(); - m_MOInfo = moInfo; + m_MOInfo = moInfo; - if (m_MOInfo && !m_MOInfo->isPluginEnabled(this)) { - return false; - } - - if (QCoreApplication::applicationDirPath().contains(';')) { - m_LoadFailure = FailureType::SEMICOLON; - return true; - } - - // load the pythonrunner library - m_RunnerLib = ::LoadLibraryW(QDir::toNativeSeparators( - IOrganizer::getPluginDataPath() + "/pythonRunner.dll").toStdWString().c_str()); - - if (!m_RunnerLib) { - DWORD error = ::GetLastError(); - log::error("failed to load python runner ({}): {}", qUtf8Printable(windowsErrorString(error))); - if (error == ERROR_MOD_NOT_FOUND) { - m_LoadFailure = FailureType::DLL_NOT_FOUND; + if (m_MOInfo && !m_MOInfo->isPluginEnabled(this)) { + return false; } - else { - m_LoadFailure = FailureType::INVALID_DLL; + + if (QCoreApplication::applicationDirPath().contains(';')) { + m_LoadFailure = FailureType::SEMICOLON; + return true; } - return true; - } - const CreatePythonRunner_func createPythonRunner = (CreatePythonRunner_func)::GetProcAddress(m_RunnerLib, "CreatePythonRunner"); - if (!createPythonRunner) { - m_LoadFailure = FailureType::INVALID_DLL; - return true; - } + const auto pluginFolder = getPluginFolder(); - if (m_MOInfo && m_MOInfo->persistent(name(), "tryInit", false).toBool()) { - m_LoadFailure = FailureType::INITIALIZATION; - if (QMessageBox::question(parentWidget(), tr("Python Initialization failed"), - tr("On a previous start the Python Plugin failed to initialize.\n" - "Do you want to try initializing python again (at the risk of another crash)?\n" - "Suggestion: Select \"no\", and click the warning sign for further help. Afterwards you have to re-enable the python plugin."), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) { - - // we force enabled here (note: this is a persistent settings since MO2 2.4 or something), plugin - // usually should not handle enabled/disabled themselves but this is a base plugin so... - m_MOInfo->setPersistent(name(), "enabled", false, true); - return true; + if (pluginFolder.empty()) { + DWORD error = ::GetLastError(); + m_LoadFailure = FailureType::DLL_NOT_FOUND; + log::error("failed to resolve Python proxy directory ({}): {}", error, + qUtf8Printable(windowsErrorString(::GetLastError()))); + return false; } - } - if (m_MOInfo) { - m_MOInfo->setPersistent(name(), "tryInit", true); - } + // load the pythonrunner library + const auto dllPaths = pluginFolder / "dlls"; + if (SetDllDirectoryW(dllPaths.c_str()) == 0) { + DWORD error = ::GetLastError(); + m_LoadFailure = FailureType::DLL_NOT_FOUND; + log::error("failed to add python DLL directory ({}): {}", error, + qUtf8Printable(windowsErrorString(::GetLastError()))); + return false; + } - m_Runner = std::unique_ptr{ createPythonRunner() }; + const auto runnerPath = pluginFolder / "pythonrunner.dll"; + m_RunnerLib = ::LoadLibraryW(runnerPath.c_str()); - if (m_MOInfo) { - m_MOInfo->setPersistent(name(), "tryInit", false); - } + if (!m_RunnerLib) { + DWORD error = ::GetLastError(); + log::error("failed to load python runner ({}): {}", error, + qUtf8Printable(windowsErrorString(error))); + if (error == ERROR_MOD_NOT_FOUND) { + m_LoadFailure = FailureType::DLL_NOT_FOUND; + } + else { + m_LoadFailure = FailureType::INVALID_DLL; + } + return true; + } - if (!m_Runner) { - m_LoadFailure = FailureType::INITIALIZATION; - } + const CreatePythonRunner_func createPythonRunner = + (CreatePythonRunner_func)::GetProcAddress(m_RunnerLib, "CreatePythonRunner"); + if (!createPythonRunner) { + m_LoadFailure = FailureType::INVALID_DLL; + return true; + } - return true; + if (m_MOInfo && m_MOInfo->persistent(name(), "tryInit", false).toBool()) { + m_LoadFailure = FailureType::INITIALIZATION; + if (QMessageBox::question( + parentWidget(), tr("Python Initialization failed"), + tr("On a previous start the Python Plugin failed to initialize.\n" + "Do you want to try initializing python again (at the risk of " + "another crash)?\n " + "Suggestion: Select \"no\", and click the warning sign for further " + "help.Afterwards you have to re-enable the python plugin."), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) == QMessageBox::No) { + // we force enabled here (note: this is a persistent settings since MO2 2.4 + // or something), plugin + // usually should not handle enabled/disabled themselves but this is a base + // plugin so... + m_MOInfo->setPersistent(name(), "enabled", false, true); + return true; + } + } + + if (m_MOInfo) { + m_MOInfo->setPersistent(name(), "tryInit", true); + } + + m_Runner = std::unique_ptr{createPythonRunner()}; + + if (m_Runner) { + const auto libpath = pluginFolder / "libs"; + const QStringList paths{ + QFileInfo(libpath / "pythoncore.zip").absoluteFilePath(), + QFileInfo(libpath).absoluteFilePath(), IOrganizer::getPluginDataPath()}; + m_Runner->initialize(paths); + } + + if (m_MOInfo) { + m_MOInfo->setPersistent(name(), "tryInit", false); + } + + if (!m_Runner || !m_Runner->isInitialized()) { + m_LoadFailure = FailureType::INITIALIZATION; + } + + // reset DLL directory + SetDllDirectoryW(NULL); + + return true; } QString ProxyPython::name() const { - return "Python Proxy"; + return "Python Proxy"; } QString ProxyPython::localizedName() const { - return tr("Python Proxy"); + return tr("Python Proxy"); } QString ProxyPython::author() const { - return "AnyOldName3, Holt59, Silarn, Tannin"; + return "AnyOldName3, Holt59, Silarn, Tannin"; } QString ProxyPython::description() const { - return tr("Proxy Plugin to allow plugins written in python to be loaded"); + return tr("Proxy Plugin to allow plugins written in python to be loaded"); } VersionInfo ProxyPython::version() const { - return VersionInfo(2, 3, 0, VersionInfo::RELEASE_FINAL); + return VersionInfo(2, 3, 0, VersionInfo::RELEASE_FINAL); } QList ProxyPython::settings() const { - return {}; + return {}; } QStringList ProxyPython::pluginList(const QDir& pluginPath) const { - QDir dir(pluginPath); - dir.setFilter(dir.filter() | QDir::NoDotAndDotDot); - QDirIterator iter(dir); + QDir dir(pluginPath); + dir.setFilter(dir.filter() | QDir::NoDotAndDotDot); + QDirIterator iter(dir); - // Note: We put python script (.py) and directory names, not the __init__.py - // files in those since it is easier for the runner to import them. - QStringList result; - while (iter.hasNext()) { - QString name = iter.next(); - QFileInfo info = iter.fileInfo(); + // Note: We put python script (.py) and directory names, not the __init__.py + // files in those since it is easier for the runner to import them. + QStringList result; + while (iter.hasNext()) { + QString name = iter.next(); + QFileInfo info = iter.fileInfo(); - - if (info.isFile() && name.endsWith(".py")) { - result.append(name); + if (info.isFile() && name.endsWith(".py")) { + result.append(name); + } + else if (info.isDir() && QDir(info.absoluteFilePath()).exists("__init__.py")) { + result.append(name); + } } - else if (info.isDir() && QDir(info.absoluteFilePath()).exists("__init__.py")) { - result.append(name); - } - } - return result; + return result; } QList ProxyPython::load(const QString& identifier) { - if (!m_Runner) { - return {}; - } - return m_Runner->load(identifier); + if (!m_Runner) { + return {}; + } + return m_Runner->load(identifier); } void ProxyPython::unload(const QString& identifier) { - if (m_Runner) { - return m_Runner->unload(identifier); - } + if (m_Runner) { + return m_Runner->unload(identifier); + } } std::vector ProxyPython::activeProblems() const { - auto failure = m_LoadFailure; + auto failure = m_LoadFailure; - // don't know how this could happen but wth - if (m_Runner && !m_Runner->isPythonInitialized()) { - failure = FailureType::INITIALIZATION; - } + // don't know how this could happen but wth + if (m_Runner && !m_Runner->isInitialized()) { + failure = FailureType::INITIALIZATION; + } - if (failure != FailureType::NONE) { - return { static_cast>(failure) }; - } + if (failure != FailureType::NONE) { + return {static_cast>(failure)}; + } - return {}; + return {}; } QString ProxyPython::shortDescription(unsigned int key) const { - switch (static_cast(key)) { + switch (static_cast(key)) { case FailureType::SEMICOLON: - return tr("ModOrganizer path contains a semicolon"); + return tr("ModOrganizer path contains a semicolon"); case FailureType::DLL_NOT_FOUND: - return tr("Python DLL not found"); + return tr("Python DLL not found"); case FailureType::INVALID_DLL: - return tr("Invalid Python DLL"); + return tr("Invalid Python DLL"); case FailureType::INITIALIZATION: - return tr("Initializing Python failed"); + return tr("Initializing Python failed"); default: - return tr("invalid problem key %1").arg(key); - } + return tr("invalid problem key %1").arg(key); + } } - QString ProxyPython::fullDescription(unsigned int key) const { - switch (static_cast(key)) { - case FailureType::SEMICOLON: - return tr("The path to Mod Organizer (%1) contains a semicolon.
" - "While this is legal on NTFS drives, many softwares do not handle it correctly.
" - "Unfortunately MO depends on libraries that seem to fall into that group.
" - "As a result the python plugin cannot be loaded, and the only solution we can" - "offer is to remove the semicolon or move MO to a path without a semicolon.").arg(QCoreApplication::applicationDirPath()); - case FailureType::DLL_NOT_FOUND: - return tr("The Python plugin DLL was not found, maybe your antivirus deleted it. Re-installing MO2 might fix the problem."); - case FailureType::INVALID_DLL: - return tr("The Python plugin DLL is invalid, maybe your antivirus is blocking it. " - "Re-installing MO2 and adding exclusions for it to your AV might fix the problem."); - case FailureType::INITIALIZATION: - return tr("The initialization of the Python plugin DLL failed, unfortunately without any details."); - default: - return tr("invalid problem key %1").arg(key); - } + switch (static_cast(key)) { + case FailureType::SEMICOLON: + return tr("The path to Mod Organizer (%1) contains a semicolon.
" + "While this is legal on NTFS drives, many softwares do not handle it " + "correctly.
" + "Unfortunately MO depends on libraries that seem to fall into that " + "group.
" + "As a result the python plugin cannot be loaded, and the only " + "solution we can" + "offer is to remove the semicolon or move MO to a path without a " + "semicolon.") + .arg(QCoreApplication::applicationDirPath()); + case FailureType::DLL_NOT_FOUND: + return tr("The Python plugin DLL was not found, maybe your antivirus deleted " + "it. Re-installing MO2 might fix the problem."); + case FailureType::INVALID_DLL: + return tr( + "The Python plugin DLL is invalid, maybe your antivirus is blocking it. " + "Re-installing MO2 and adding exclusions for it to your AV might fix the " + "problem."); + case FailureType::INITIALIZATION: + return tr("The initialization of the Python plugin DLL failed, unfortunately " + "without any details."); + default: + return tr("invalid problem key %1").arg(key); + } } bool ProxyPython::hasGuidedFix(unsigned int key) const { - return false; + return false; } -void ProxyPython::startGuidedFix(unsigned int key) const -{ -} +void ProxyPython::startGuidedFix(unsigned int key) const {} diff --git a/src/proxy/proxypython.h b/src/proxy/proxypython.h index 249d198..9da1b9d 100644 --- a/src/proxy/proxypython.h +++ b/src/proxy/proxypython.h @@ -25,59 +25,56 @@ along with python proxy plugin. If not, see . #include -#include #include +#include #include - -class ProxyPython : public QObject, public MOBase::IPluginProxy, public MOBase::IPluginDiagnose -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginProxy MOBase::IPluginDiagnose) -#if QT_VERSION >= QT_VERSION_CHECK(5,0,0) - Q_PLUGIN_METADATA(IID "org.tannin.ProxyPython" FILE "proxypython.json") +class ProxyPython : public QObject, + public MOBase::IPluginProxy, + public MOBase::IPluginDiagnose { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginProxy MOBase::IPluginDiagnose) +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + Q_PLUGIN_METADATA(IID "org.mo2.ProxyPython") #endif public: - ProxyPython(); + ProxyPython(); - virtual bool init(MOBase::IOrganizer *moInfo); - virtual QString name() const override; - virtual QString localizedName() const override; - virtual QString author() const override; - virtual QString description() const override; - virtual MOBase::VersionInfo version() const override; - virtual QList settings() const override; + virtual bool init(MOBase::IOrganizer* moInfo); + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList settings() const override; - QStringList pluginList(const QDir& pluginPath) const override; - QList load(const QString& identifier) override; - void unload(const QString& identifier) override; + QStringList pluginList(const QDir& pluginPath) const override; + QList load(const QString& identifier) override; + void unload(const QString& identifier) override; -public: // IPluginDiagnose - - virtual std::vector activeProblems() const override; - virtual QString shortDescription(unsigned int key) const override; - virtual QString fullDescription(unsigned int key) const override; - virtual bool hasGuidedFix(unsigned int key) const override; - virtual void startGuidedFix(unsigned int key) const override; +public: // IPluginDiagnose + virtual std::vector activeProblems() const override; + virtual QString shortDescription(unsigned int key) const override; + virtual QString fullDescription(unsigned int key) const override; + virtual bool hasGuidedFix(unsigned int key) const override; + virtual void startGuidedFix(unsigned int key) const override; private: + MOBase::IOrganizer* m_MOInfo; + HMODULE m_RunnerLib; + std::unique_ptr m_Runner; - MOBase::IOrganizer *m_MOInfo; - HMODULE m_RunnerLib; - std::unique_ptr m_Runner; - - enum class FailureType : unsigned int { - NONE = 0, - SEMICOLON = 1, - DLL_NOT_FOUND = 2, - INVALID_DLL = 3, - INITIALIZATION = 4 - }; - - FailureType m_LoadFailure; + enum class FailureType : unsigned int { + NONE = 0, + SEMICOLON = 1, + DLL_NOT_FOUND = 2, + INVALID_DLL = 3, + INITIALIZATION = 4 + }; + FailureType m_LoadFailure; }; -#endif // PROXYPYTHON_H +#endif // PROXYPYTHON_H diff --git a/src/proxy/proxypython.json b/src/proxy/proxypython.json deleted file mode 100644 index 69a88e3..0000000 --- a/src/proxy/proxypython.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/src/pybind11-qt/CMakeLists.txt b/src/pybind11-qt/CMakeLists.txt new file mode 100644 index 0000000..6b39049 --- /dev/null +++ b/src/pybind11-qt/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.16) + +add_library(pybind11-qt STATIC) +mo2_configure_library(pybind11-qt + SOURCE_TREE + WARNINGS OFF + AUTOMOC OFF + TRANSLATIONS OFF + PRIVATE_DEPENDS Qt::Core Qt::Widgets +) +target_link_libraries(pybind11-qt PUBLIC pybind11::pybind11) +target_include_directories(pybind11-qt + PUBLIC ${PYTHON_ROOT}/Include ${CMAKE_CURRENT_SOURCE_DIR}/include) + +# this is kind of broken but it only works with this... +target_compile_definitions(pybind11-qt PUBLIC QT_NO_KEYWORDS) + +add_library(pybind11::qt ALIAS pybind11-qt) diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_enum.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_enum.h new file mode 100644 index 0000000..45afd0c --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_enum.h @@ -0,0 +1,59 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_ENUM_HPP +#define PYTHON_PYBIND11_QT_DETAILS_ENUM_HPP + +#include + +#include + +#include "pybind11_qt_utils.h" + +namespace pybind11::detail::qt { + + // EnumData, with static members (const char[]) + // - package: name of the Python package containing the enum (e.g., + // PyQt6.QtCore) + // - name: full path to the enum, e.g. Qt.QGlobalColor + // + template + struct EnumData; + + // template class for most Qt types that have Python equivalent (QWidget, + // etc.) + // + template + struct qt_enum_caster { + + public: + PYBIND11_TYPE_CASTER(Enum, EnumData::package + const_name(".") + + EnumData::name); + + bool load(pybind11::handle src, bool) + { + if (PyLong_Check(src.ptr())) { + value = static_cast(PyLong_AsLong(src.ptr())); + return true; + } + + auto pyenum = + get_attr_rec(EnumData::package.text, EnumData::name.text); + + if (isinstance(src, pyenum)) { + value = static_cast(src.attr("value").cast()); + return true; + } + + return false; + } + + static pybind11::handle cast(Enum src, + pybind11::return_value_policy /* policy */, + pybind11::handle /* parent */) + { + auto pyenum = + get_attr_rec(EnumData::package.text, EnumData::name.text); + return pyenum(static_cast(src)); + } + }; +} // namespace pybind11::detail::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qlist.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qlist.h new file mode 100644 index 0000000..1322149 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qlist.h @@ -0,0 +1,50 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_QLIST_HPP +#define PYTHON_PYBIND11_QT_DETAILS_QLIST_HPP + +#include +#include + +namespace pybind11::detail::qt { + + // helper class for QList to construct from any proper iterable + // + template + struct qlist_caster { + using value_conv = make_caster; + + bool load(handle src, bool convert) + { + if (!isinstance(src) || isinstance(src) || + isinstance(src)) { + return false; + } + auto s = reinterpret_borrow(src); + value.clear(); + + if (isinstance(src)) { + value.reserve(s.cast().size()); + } + for (auto it : s) { + value_conv conv; + if (!conv.load(it, convert)) { + return false; + } + value.push_back(cast_op(std::move(conv))); + } + return true; + } + + template + static handle cast(T&& src, return_value_policy policy, handle parent) + { + return list_caster, Value>{}.cast(std::forward(src), policy, + parent); + } + + PYBIND11_TYPE_CASTER(Type, const_name("Iterable[") + value_conv::name + + const_name("]")); + }; + +} // namespace pybind11::detail::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qmap.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qmap.h new file mode 100644 index 0000000..ea743b7 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_qmap.h @@ -0,0 +1,70 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_QMAP_HPP +#define PYTHON_PYBIND11_QT_DETAILS_QMAP_HPP + +#include + +namespace pybind11::detail::qt { + + // helper class for QMap because QMap do not follow the standard std:: maps + // interface, for other containers, the pybind11 built-in xxx_caster works + // + // this code is basically a copy/paste from the pybind11 stl stuff with + // minor modifications + // + template + struct qmap_caster { + using key_conv = make_caster; + using value_conv = make_caster; + + bool load(handle src, bool convert) + { + if (!isinstance(src)) { + return false; + } + auto d = reinterpret_borrow(src); + value.clear(); + for (auto it : d) { + key_conv kconv; + value_conv vconv; + if (!kconv.load(it.first.ptr(), convert) || + !vconv.load(it.second.ptr(), convert)) { + return false; + } + value[cast_op(std::move(kconv))] = + cast_op(std::move(vconv)); + } + return true; + } + + template + static handle cast(T&& src, return_value_policy policy, handle parent) + { + dict d; + return_value_policy policy_key = policy; + return_value_policy policy_value = policy; + if (!std::is_lvalue_reference::value) { + policy_key = return_value_policy_override::policy(policy_key); + policy_value = + return_value_policy_override::policy(policy_value); + } + for (auto it = src.begin(); it != src.end(); ++it) { + auto key = reinterpret_steal( + key_conv::cast(forward_like(it.key()), policy_key, parent)); + auto value = reinterpret_steal(value_conv::cast( + forward_like(it.value()), policy_value, parent)); + if (!key || !value) { + return handle(); + } + d[key] = value; + } + return d.release(); + } + + PYBIND11_TYPE_CASTER(Type, const_name("Dict[") + key_conv::name + + const_name(", ") + value_conv::name + + const_name("]")); + }; + +} // namespace pybind11::detail::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h new file mode 100644 index 0000000..6e5d265 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_sip.h @@ -0,0 +1,188 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_SIP_HPP +#define PYTHON_PYBIND11_QT_DETAILS_SIP_HPP + +#include + +#include +#include + +#include +#include + +#include "../pybind11_qt_holder.h" + +namespace pybind11::detail::qt { + + /** + * @brief Retrieve the SIP api. + * + * @return const sipAPIDef* + */ + const sipAPIDef* sipAPI(); + + template + struct MetaData; + + template + struct MetaData>> + : MetaData> { + }; + + // template class for most Qt types that have Python equivalent (QWidget, + // etc.) + // + template + struct qt_type_caster { + + static constexpr bool is_pointer = std::is_pointer_v; + using pointer = std::conditional_t; + + QClass value; + + public: + static constexpr auto name = MetaData::python_name; + + operator pointer() + { + if constexpr (is_pointer) { + return value; + } + else { + return &value; + } + } + + template && !is_pointer, int> = 0> + operator T&() + { + return value; + } + + template && !is_pointer, int> = 0> + operator T&&() && + { + return std::move(value); + } + + template + using cast_op_type = + std::conditional_t>; + + bool load(pybind11::handle src, bool) + { + // special check for none for pointer classes + if constexpr (is_pointer) { + if (src.is_none()) { + value = nullptr; + return true; + } + } + + // this would transfer responsibility for deconstructing the + // object to C++, but pybind11 assumes l-value converters (such + // as this) don't do that instead, this should be called within + // the wrappers for functions which return deletable pointers. + // + // sipAPI()->api_transfer_to(objPtr, Py_None); + // + void* data = nullptr; + if (PyObject_TypeCheck(src.ptr(), qt::sipAPI()->api_simplewrapper_type)) { + data = reinterpret_cast(src.ptr())->data; + } + else if (PyObject_TypeCheck(src.ptr(), qt::sipAPI()->api_wrapper_type)) { + data = reinterpret_cast(src.ptr())->super.data; + } + + if (data) { + if constexpr (is_pointer) { + value = reinterpret_cast(data); + + // transfer ownership + sipAPI()->api_transfer_to(src.ptr(), Py_None); + + // tie the py::object to the C++ one + new pybind11::detail::qt::qobject_holder(value); + } + else { + value = *reinterpret_cast(data); + } + return true; + } + else { + return false; + } + } + + template < + typename T, + std::enable_if_t>::value, int> = 0> + static handle cast(T* src, return_value_policy policy, handle parent) + { + // note: when QClass is a pointer type, e.g. a QWidget*, T is a + // pointer to pointer, so we can defer to the standard cast() + + if (!src) { + return none().release(); + } + + if (!is_pointer && policy == return_value_policy::take_ownership) { + auto h = cast(std::move(*src), policy, parent); + delete src; + return h; + } + return cast(*src, policy, parent); + } + + static pybind11::handle cast(QClass src, pybind11::return_value_policy policy, + pybind11::handle /* parent */) + { + if constexpr (is_pointer) { + if (!src) { + return none().release(); + } + } + + const sipTypeDef* type = + qt::sipAPI()->api_find_type(MetaData::class_name); + if (type == nullptr) { + return Py_None; + } + + PyObject* sipObj; + void* sipData; + + if constexpr (is_pointer) { + sipData = src; + } + else if (std::is_copy_assignable_v) { + // we send to SIP a newly allocated object, and transfer the + // owernship to it + sipData = + new QClass(policy == ::pybind11::return_value_policy::take_ownership + ? std::move(src) + : src); + } + else { + sipData = &src; + } + + sipObj = qt::sipAPI()->api_convert_from_type(sipData, type, 0); + + if (sipObj == nullptr) { + return Py_None; + } + + if (policy == return_value_policy::take_ownership) { + // ensure Python deletes the C++ component + qt::sipAPI()->api_transfer_back(sipObj); + } + + return sipObj; + } + }; + +} // namespace pybind11::detail::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_utils.h b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_utils.h new file mode 100644 index 0000000..c877c15 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/details/pybind11_qt_utils.h @@ -0,0 +1,47 @@ +#ifndef PYTHON_PYBIND11_QT_DETAILS_UTILS_HPP +#define PYTHON_PYBIND11_QT_DETAILS_UTILS_HPP + +#include +#include + +#include + +namespace pybind11::detail::qt { + + /** + * @brief Convert a XXX::YYY compile time string to a XXX.YYY compile time + * string. Only one :: is allowed. + * + */ + template + constexpr descr qt_name_cpp2py(const char (&name)[N]) + { + descr res; + for (std::size_t i = 0, j = 0; i < N - 2; ++i) { + + res.text[i] = name[j]; + + if (res.text[i] == ':') { + res.text[i] = '.'; + j += 2; + } + else { + ++j; + } + } + return res; + } + + /** + * @brief Retrieve the class from the given package at the given path + * + * @param package Name of the module. + * @param path Path to the class/object in the module. + * + * @return the object at the given path in the given module + */ + pybind11::object get_attr_rec(std::string_view package, std::string_view path); + +} // namespace pybind11::detail::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h new file mode 100644 index 0000000..9091d3a --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt.h @@ -0,0 +1,88 @@ +#ifndef PYTHON_PYBIND11_QT_HPP +#define PYTHON_PYBIND11_QT_HPP + +// this header defines many type casters for Qt types, including: +// - basic Qt types such as QString and QVariant - those do not have PyQt6 equivalent +// - QFlags<> class templates +// - containers such as QList<>, QSet<>, etc., the QList<> casters is more flexible than +// the std::vector<> or std::list<> ones as it accepts any iterable +// - many Qt enumeration types (see pybind11_qt_enums) +// - many Qt classes with PyQt6 equivalent +// - copyable type are copied between Python and C++ +// - non-copyable type (QObject, QWidget, QMainWindow) are always owned by the C++ +// side, even when constructed on the Python side, and owned their corresponding +// Python object, e.g., an instance of a class inheriting QWidget created on the +// Python side can be safely used in C++ since the Python object will be owned by +// the C++ QWidget object +// + +#include "pybind11_qt_basic.h" +#include "pybind11_qt_containers.h" +#include "pybind11_qt_enums.h" +#include "pybind11_qt_holder.h" +#include "pybind11_qt_objects.h" +#include "pybind11_qt_qflags.h" + +namespace pybind11::qt { + + /** + * @brief Tie the lifetime of the Python object to the lifetime of the given + * QObject. + * + * @param owner QObject that will own the python object. + * @param child Python object that the QObject will own. + */ + inline void set_qt_owner(QObject* owner, object child) + { + new detail::qt::qobject_holder{owner, child}; + } + + /** + * @brief Tie the lifetime of the given object to the lifetime of the corresponding + * Python object. + * + * This object must have been created from Python and must inherit QObject. + * + * @param object Object to tie. + */ + template + void set_qt_owner(Class* object) + { + static_assert(std::is_base_of_v); + new detail::qt::qobject_holder{object}; + } + + /** + * @brief Add Qt "delegate" to the given class. + * + * This function defines two methods: __getattr__ and name, where name will + * simply return the PyQtX object as a QClass* object, while __getattr__ + * will delegate to the underlying QClass object when required. + * + * This allow access to Qt interface for object exposed using boost::python + * (e.g., signals, methods from QObject or QWidget, etc.). + * + * @param pyclass Python class to define the methods on. + * @param name Name of the method to retrieve the underlying object. + * + * @tparam QClass Name of the Qt class, cannot be deduced. + * @tparam Class Class being wrapped, deduced. + * @tparam Args... Arguments of the class template parameters, deduced. + */ + template + auto& add_qt_delegate(pybind11::class_& pyclass, const char* name) + { + return pyclass + .def(name, + [](Class* w) -> QClass* { + return w; + }) + .def( + "__getattr__", +[](Class* w, pybind11::str str) -> pybind11::object { + return pybind11::cast((QClass*)w).attr(str); + }); + } + +} // namespace pybind11::qt + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_basic.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_basic.h new file mode 100644 index 0000000..f4bf3ff --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_basic.h @@ -0,0 +1,36 @@ +#ifndef PYTHON_PYBIND11_QT_BASIC_HPP +#define PYTHON_PYBIND11_QT_BASIC_HPP + +#include +#include + +#include + +namespace pybind11::detail { + + // QString + // + template <> + struct type_caster { + PYBIND11_TYPE_CASTER(QString, const_name("str")); + + bool load(handle src, bool); + + static handle cast(QString src, return_value_policy policy, handle parent); + }; + + // QVariant - this needs to be defined BEFORE QVariantList + // + template <> + struct type_caster { + public: + PYBIND11_TYPE_CASTER(QVariant, const_name("MOVariant")); + + bool load(handle src, bool); + + static handle cast(QVariant var, return_value_policy policy, handle parent); + }; + +} // namespace pybind11::detail + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_containers.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_containers.h new file mode 100644 index 0000000..4c916e6 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_containers.h @@ -0,0 +1,56 @@ +#ifndef PYTHON_PYBIND11_QT_CONTAINERS_HPP +#define PYTHON_PYBIND11_QT_CONTAINERS_HPP + +#include +#include +#include + +#include +#include + +// this needs to be included here to get proper QVariantList and QVariantMap +#include "details/pybind11_qt_qmap.h" +#include "pybind11_qt_basic.h" +#include "details/pybind11_qt_qlist.h" + +namespace pybind11::detail { + + // QList + // + template + struct type_caster> : qt::qlist_caster, T> { + }; + + // QSet + // + template + struct type_caster> : set_caster, T> { + }; + + // QMap + // + template + struct type_caster> : qt::qmap_caster, K, V> { + }; + + // QStringList + // + template <> + struct type_caster : qt::qlist_caster { + }; + + // QVariantList + // + template <> + struct type_caster : qt::qlist_caster { + }; + + // QVariantMap + // + template <> + struct type_caster : qt::qmap_caster { + }; + +} // namespace pybind11::detail + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_enums.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_enums.h new file mode 100644 index 0000000..e6cdfe0 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_enums.h @@ -0,0 +1,118 @@ +#ifndef PYTHON_PYBIND11_QT_ENUMS_HPP +#define PYTHON_PYBIND11_QT_ENUMS_HPP + +#include +#include + +#include "details/pybind11_qt_enum.h" +#include "details/pybind11_qt_utils.h" + +#define PYQT_ENUM(QPackage, QEnum) \ + namespace qt { \ + template <> \ + struct EnumData { \ + constexpr static const auto package = \ + const_name("PyQt6.") + const_name(#QPackage); \ + constexpr static const auto name = qt_name_cpp2py(#QEnum); \ + }; \ + } \ + template <> \ + struct type_caster : qt::qt_enum_caster { \ + } + +namespace pybind11::detail { + + PYQT_ENUM(QtCore, Qt::AlignmentFlag); + PYQT_ENUM(QtCore, Qt::AnchorPoint); + PYQT_ENUM(QtCore, Qt::ApplicationAttribute); + PYQT_ENUM(QtCore, Qt::ApplicationState); + PYQT_ENUM(QtCore, Qt::ArrowType); + PYQT_ENUM(QtCore, Qt::AspectRatioMode); + PYQT_ENUM(QtCore, Qt::Axis); + PYQT_ENUM(QtCore, Qt::BGMode); + PYQT_ENUM(QtCore, Qt::BrushStyle); + PYQT_ENUM(QtCore, Qt::CaseSensitivity); + PYQT_ENUM(QtCore, Qt::CheckState); + PYQT_ENUM(QtCore, Qt::ChecksumType); + PYQT_ENUM(QtCore, Qt::ClipOperation); + PYQT_ENUM(QtCore, Qt::ConnectionType); + PYQT_ENUM(QtCore, Qt::ContextMenuPolicy); + PYQT_ENUM(QtCore, Qt::CoordinateSystem); + PYQT_ENUM(QtCore, Qt::Corner); + PYQT_ENUM(QtCore, Qt::CursorMoveStyle); + PYQT_ENUM(QtCore, Qt::CursorShape); + PYQT_ENUM(QtCore, Qt::DateFormat); + PYQT_ENUM(QtCore, Qt::DayOfWeek); + PYQT_ENUM(QtCore, Qt::DockWidgetArea); + PYQT_ENUM(QtCore, Qt::DropAction); + PYQT_ENUM(QtCore, Qt::Edge); + PYQT_ENUM(QtCore, Qt::EnterKeyType); + PYQT_ENUM(QtCore, Qt::EventPriority); + PYQT_ENUM(QtCore, Qt::FillRule); + PYQT_ENUM(QtCore, Qt::FindChildOption); + PYQT_ENUM(QtCore, Qt::FocusPolicy); + PYQT_ENUM(QtCore, Qt::FocusReason); + PYQT_ENUM(QtCore, Qt::GestureFlag); + PYQT_ENUM(QtCore, Qt::GestureState); + PYQT_ENUM(QtCore, Qt::GestureType); + PYQT_ENUM(QtCore, Qt::GlobalColor); + PYQT_ENUM(QtCore, Qt::HitTestAccuracy); + PYQT_ENUM(QtCore, Qt::ImageConversionFlag); + PYQT_ENUM(QtCore, Qt::InputMethodHint); + PYQT_ENUM(QtCore, Qt::InputMethodQuery); + PYQT_ENUM(QtCore, Qt::ItemDataRole); + PYQT_ENUM(QtCore, Qt::ItemFlag); + PYQT_ENUM(QtCore, Qt::ItemSelectionMode); + PYQT_ENUM(QtCore, Qt::ItemSelectionOperation); + PYQT_ENUM(QtCore, Qt::Key); + PYQT_ENUM(QtCore, Qt::KeyboardModifier); + PYQT_ENUM(QtCore, Qt::LayoutDirection); + PYQT_ENUM(QtCore, Qt::MaskMode); + PYQT_ENUM(QtCore, Qt::MatchFlag); + PYQT_ENUM(QtCore, Qt::Modifier); + PYQT_ENUM(QtCore, Qt::MouseButton); + PYQT_ENUM(QtCore, Qt::MouseEventFlag); + PYQT_ENUM(QtCore, Qt::MouseEventSource); + PYQT_ENUM(QtCore, Qt::NativeGestureType); + PYQT_ENUM(QtCore, Qt::NavigationMode); + PYQT_ENUM(QtCore, Qt::Orientation); + PYQT_ENUM(QtCore, Qt::PenCapStyle); + PYQT_ENUM(QtCore, Qt::PenJoinStyle); + PYQT_ENUM(QtCore, Qt::PenStyle); + PYQT_ENUM(QtCore, Qt::ScreenOrientation); + PYQT_ENUM(QtCore, Qt::ScrollBarPolicy); + PYQT_ENUM(QtCore, Qt::ScrollPhase); + PYQT_ENUM(QtCore, Qt::ShortcutContext); + PYQT_ENUM(QtCore, Qt::SizeHint); + PYQT_ENUM(QtCore, Qt::SizeMode); + PYQT_ENUM(QtCore, Qt::SortOrder); + PYQT_ENUM(QtCore, Qt::TabFocusBehavior); + PYQT_ENUM(QtCore, Qt::TextElideMode); + PYQT_ENUM(QtCore, Qt::TextFlag); + PYQT_ENUM(QtCore, Qt::TextFormat); + PYQT_ENUM(QtCore, Qt::TextInteractionFlag); + PYQT_ENUM(QtCore, Qt::TileRule); + PYQT_ENUM(QtCore, Qt::TimeSpec); + PYQT_ENUM(QtCore, Qt::TimerType); + PYQT_ENUM(QtCore, Qt::ToolBarArea); + PYQT_ENUM(QtCore, Qt::ToolButtonStyle); + PYQT_ENUM(QtCore, Qt::TransformationMode); + PYQT_ENUM(QtCore, Qt::WhiteSpaceMode); + PYQT_ENUM(QtCore, Qt::WidgetAttribute); + PYQT_ENUM(QtCore, Qt::WindowFrameSection); + PYQT_ENUM(QtCore, Qt::WindowModality); + PYQT_ENUM(QtCore, Qt::WindowState); + PYQT_ENUM(QtCore, Qt::WindowType); + + PYQT_ENUM(QtWidgets, QMessageBox::ButtonRole); + PYQT_ENUM(QtWidgets, QMessageBox::DialogCode); + PYQT_ENUM(QtWidgets, QMessageBox::Icon); + PYQT_ENUM(QtWidgets, QMessageBox::PaintDeviceMetric); + PYQT_ENUM(QtWidgets, QMessageBox::RenderFlag); + PYQT_ENUM(QtWidgets, QMessageBox::StandardButton); + +} // namespace pybind11::detail + +#undef PYQT_ENUM + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_holder.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_holder.h new file mode 100644 index 0000000..29d6d8f --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_holder.h @@ -0,0 +1,55 @@ +#ifndef PYTHON_PYBIND11_QT_HOLDER_HPP +#define PYTHON_PYBIND11_QT_HOLDER_HPP + +#include + +#include + +namespace pybind11::detail::qt { + + class qobject_holder : public QObject { + object p_; + + public: + /** + * @brief Construct a new qobject holder linked to the given QObject and + * maintaining the given python object alive. + * + * @param p Parent of this holder. + * @param o Python object to keep alive. + */ + qobject_holder(QObject* p, object o) : p_{o} { setParent(p); } + + template + qobject_holder(U* p) : qobject_holder{p, reinterpret_borrow(cast(p))} + { + } + + ~qobject_holder() + { + gil_scoped_acquire s; + p_ = std::move(none()); + } + }; + +} // namespace pybind11::detail::qt + +namespace pybind11::qt { + + template + class qholder { + using type = Type; + + type* qobj_; + + public: + qholder(type* qobj) : qobj_{qobj} { new detail::qt::qobject_holder(qobj_); } + + type* get() { return qobj_; } + }; + +} // namespace pybind11::qt + +PYBIND11_DECLARE_HOLDER_TYPE(T, ::pybind11::qt::qholder) + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_objects.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_objects.h new file mode 100644 index 0000000..85d7408 --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_objects.h @@ -0,0 +1,64 @@ +#ifndef PYTHON_PYBIND11_QT_OBJECTS_HPP +#define PYTHON_PYBIND11_QT_OBJECTS_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "details/pybind11_qt_sip.h" +#include "details/pybind11_qt_utils.h" + +#define PYQT_CLASS(QModule, QClass) \ + namespace qt { \ + template <> \ + struct MetaData { \ + constexpr static const auto class_name = #QClass; \ + constexpr static const auto python_name = \ + const_name("PyQt6.") + const_name(#QModule) + const_name(".") + \ + const_name(#QClass); \ + }; \ + } \ + template <> \ + struct type_caster \ + : std::conditional_t, \ + type_caster_generic, qt::qt_type_caster> { \ + }; \ + template <> \ + struct type_caster \ + : std::conditional_t, \ + qt::qt_type_caster, type_caster> { \ + } + +namespace pybind11::detail { + + // add declarations below to create bindings - the first argument is simply + // the name of the PyQt6 package containing the class, and is only used for + // the python signature + + PYQT_CLASS(QtCore, QDateTime); + PYQT_CLASS(QtCore, QDir); + PYQT_CLASS(QtCore, QFileInfo); + PYQT_CLASS(QtCore, QObject); + PYQT_CLASS(QtCore, QSize); + PYQT_CLASS(QtCore, QUrl); + + PYQT_CLASS(QtGui, QColor); + PYQT_CLASS(QtGui, QIcon); + PYQT_CLASS(QtGui, QPixmap); + + PYQT_CLASS(QtWidgets, QMainWindow); + PYQT_CLASS(QtWidgets, QWidget); + +} // namespace pybind11::detail + +#undef METADATA + +#endif diff --git a/src/pybind11-qt/include/pybind11_qt/pybind11_qt_qflags.h b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_qflags.h new file mode 100644 index 0000000..c9f4eca --- /dev/null +++ b/src/pybind11-qt/include/pybind11_qt/pybind11_qt_qflags.h @@ -0,0 +1,56 @@ +#ifndef PYTHON_PYBIND11_QT_QFLAGS_HPP +#define PYTHON_PYBIND11_QT_QFLAGS_HPP + +#include + +#include + +namespace pybind11::detail { + + // QFlags + // + template + struct type_caster> { + PYBIND11_TYPE_CASTER(QFlags, const_name("QFlags[") + make_caster::name + + const_name("]")); + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a QString + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool load(handle src, bool implicit) + { + PyObject* tmp = PyNumber_Long(src.ptr()); + + if (!tmp) { + return false; + } + + // we do an intermediate extraction to T but this actually + // can contains multiple values + T flag_value = static_cast(PyLong_AsLong(tmp)); + Py_DECREF(tmp); + + value = QFlags(flag_value); + + return !PyErr_Occurred(); + } + + /** + * Conversion part 2 (C++ -> Python): convert an QString instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + static handle cast(QFlags const& src, return_value_policy /* policy */, + handle /* parent */) + { + return PyLong_FromLong(static_cast(src)); + } + }; + +} // namespace pybind11::detail + +#endif diff --git a/src/pybind11-qt/pybind11_qt_basic.cpp b/src/pybind11-qt/pybind11_qt_basic.cpp new file mode 100644 index 0000000..b8f19f1 --- /dev/null +++ b/src/pybind11-qt/pybind11_qt_basic.cpp @@ -0,0 +1,159 @@ +#include "pybind11_qt/pybind11_qt_basic.h" + +#include + +#include + +#include "pybind11_qt/details/pybind11_qt_utils.h" + +// need to import containers to get QVariantList and QVariantMap +#include "pybind11_qt/pybind11_qt_containers.h" + +namespace pybind11::detail { + + template + QString qstring_from_stdstring(std::basic_string const& s) + { + if constexpr (std::is_same_v) { + return QString::fromStdString(s); + } + else if constexpr (std::is_same_v) { + return QString::fromStdWString(s); + } + else if constexpr (std::is_same_v) { + return QString::fromStdU16String(s); + } + else if constexpr (std::is_same_v) { + return QString::fromStdU32String(s); + } + } + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a QString + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool type_caster::load(handle src, bool implicit) + { + + PyObject* objPtr = src.ptr(); + + if (!PyBytes_Check(objPtr) && !PyUnicode_Check(objPtr)) { +#ifndef PYBIND11_QT_QSTRING_NOFS + // try converting from os.PathLike + type_caster path_caster; + if (!path_caster.load(src, implicit)) { + return false; + } + + value = qstring_from_stdstring((*path_caster).native()); + return true; +#else + // do not try to convert from os.PathLike + return false; +#endif + } + + // Ensure the string uses 8-bit characters + PyObject* strPtr = + PyUnicode_Check(objPtr) ? PyUnicode_AsUTF8String(objPtr) : objPtr; + + // Extract the character data from the python string + value = QString::fromUtf8(PyBytes_AsString(strPtr)); + + // Deallocate local copy if one was made + if (strPtr != objPtr) { + Py_DecRef(strPtr); + } + + return true; + } + + /** + * Conversion part 2 (C++ -> Python): convert an QString instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + handle type_caster::cast(QString src, return_value_policy /* policy */, + handle /* parent */) + { + static_assert(sizeof(QChar) == 2); + return PyUnicode_FromKindAndData(PyUnicode_2BYTE_KIND, src.constData(), + src.length()); + } + + bool type_caster::load(handle src, bool implicit) + { + // test for string first otherwise PyList_Check also works + if (PyBytes_Check(src.ptr()) || PyUnicode_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (PySequence_Check(src.ptr())) { + // we could check if all the elements can be converted to QString + // and store a QStringList in the QVariant but I am not sure that is + // really useful. + value = src.cast(); + return true; + } + else if (PyMapping_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (src == Py_None) { + value = QVariant(); + return true; + } + else if (PyDict_Check(src.ptr())) { + value = src.cast(); + return true; + } + // PyBool will also return true for PyLong_Check but not the other way + // around, so the order here is relevant. + else if (PyBool_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (PyLong_Check(src.ptr())) { + // QVariant doesn't have long. It has int or long long. Given that + // on m/s, long is 32 bits for 32- and 64- bit code... + value = src.cast(); + return true; + } + else { + return false; + } + } + + handle type_caster::cast(QVariant var, return_value_policy policy, + handle parent) + { + switch (var.type()) { + case QVariant::Invalid: + return Py_None; + case QVariant::Int: + return PyLong_FromLong(var.toInt()); + case QVariant::UInt: + return PyLong_FromUnsignedLong(var.toUInt()); + case QVariant::Bool: + return PyBool_FromLong(var.toBool()); + case QVariant::String: + return type_caster::cast(var.toString(), policy, parent); + // We need to check for StringList here because these are not considered + // List since List is QList will StringList is QList: + case QVariant::StringList: + return type_caster::cast(var.toStringList(), policy, parent); + case QVariant::List: + return type_caster::cast(var.toList(), policy, parent); + case QVariant::Map: + return type_caster::cast(var.toMap(), policy, parent); + default: { + PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.type()); + throw pybind11::error_already_set(); + } + } + } + +} // namespace pybind11::detail diff --git a/src/pybind11-qt/pybind11_qt_sip.cpp b/src/pybind11-qt/pybind11_qt_sip.cpp new file mode 100644 index 0000000..4ef8fb3 --- /dev/null +++ b/src/pybind11-qt/pybind11_qt_sip.cpp @@ -0,0 +1,62 @@ +#include "pybind11_qt/details/pybind11_qt_sip.h" + +#include + +#include + +namespace py = pybind11; + +namespace pybind11::detail::qt { + + const sipAPIDef* sipAPI() + { + std::string exception; + static const sipAPIDef* sipApi = nullptr; + if (sipApi == nullptr) { + PyImport_ImportModule("PyQt6.sip"); + + auto errorObj = PyErr_Occurred(); + if (errorObj != NULL) { + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + if (traceback != NULL) { + py::handle h_type(type); + py::handle h_val(value); + py::handle h_tb(traceback); + py::object tb(py::module_::import("traceback")); + py::object fmt_exp(tb.attr("format_exception")); + py::object exp_list(fmt_exp(h_type, h_val, h_tb)); + py::object exp_str(py::str("\n").attr("join")(exp_list)); + exception = exp_str.cast(); + } + PyErr_Restore(type, value, traceback); + throw std::runtime_error{"Failed to load SIP API: " + exception}; + } + + sipApi = (const sipAPIDef*)PyCapsule_Import("PyQt6.sip._C_API", 0); + if (sipApi == NULL) { + auto errorObj = PyErr_Occurred(); + if (errorObj != NULL) { + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + if (traceback != NULL) { + py::handle h_type(type); + py::handle h_val(value); + py::handle h_tb(traceback); + py::object tb(py::module_::import("traceback")); + py::object fmt_exp(tb.attr("format_exception")); + py::object exp_list(fmt_exp(h_type, h_val, h_tb)); + py::object exp_str(py::str("\n").attr("join")(exp_list)); + exception = exp_str.cast(); + } + PyErr_Restore(type, value, traceback); + } + throw std::runtime_error{"Failed to load SIP API: " + exception}; + } + } + return sipApi; + } + +} // namespace pybind11::detail::qt diff --git a/src/pybind11-qt/pybind11_qt_utils.cpp b/src/pybind11-qt/pybind11_qt_utils.cpp new file mode 100644 index 0000000..249bed5 --- /dev/null +++ b/src/pybind11-qt/pybind11_qt_utils.cpp @@ -0,0 +1,11 @@ +#include "pybind11_qt/details/pybind11_qt_utils.h" + +namespace pybind11::detail::qt { + + pybind11::object get_attr_rec(std::string_view package, std::string_view path) + { + + return module_::import("operator") + .attr("attrgetter")(path.data())(module_::import(package.data())); + } +} // namespace pybind11::detail::qt diff --git a/src/runner/CMakeLists.txt b/src/runner/CMakeLists.txt index ba0e971..0080232 100644 --- a/src/runner/CMakeLists.txt +++ b/src/runner/CMakeLists.txt @@ -1,50 +1,40 @@ cmake_minimum_required(VERSION 3.16) -# need to find Boost here with a dummy component to get Boost_LIBRARY_DIRS -find_package(Boost COMPONENTS thread REQUIRED) - add_library(pythonrunner SHARED) mo2_configure_library(pythonrunner + SOURCE_TREE WARNINGS OFF - BIGOBJ ON AUTOMOC ON TRANSLATIONS OFF - PRIVATE_DEPENDS uibase boost Qt::Core + PUBLIC_DEPENDS uibase Qt::Core ) -target_include_directories(pythonrunner PRIVATE ${PYTHON_ROOT}/Include) +target_link_libraries(pythonrunner PRIVATE pybind11::embed pybind11::qt) +target_include_directories(pythonrunner + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE ${PYTHON_ROOT}/Include) # this is kind of broken but it only works with this... -target_link_directories(pythonrunner - PRIVATE - ${MO2_INSTALL_LIBS_PATH} - ${Boost_LIBRARY_DIRS}) target_compile_definitions(pythonrunner PRIVATE QT_NO_KEYWORDS PYTHONRUNNER_LIBRARY) -mo2_install_target(pythonrunner INSTALLDIR bin/plugins/data) +mo2_install_target(pythonrunner INSTALLDIR bin/plugins/plugin_python) -mo2_add_filter(NAME src/converters GROUPS - converters - pythonutils - shared_ptr_converter - tuple_helper - variant_helper -) +# install DLLs files needed +set(DLL_DIRS ${MO2_INSTALL_PATH}/bin/plugins/plugin_python/dlls) +file(GLOB dlls_to_install + ${PYTHON_BUILD_PATH}/libffi*.dll + ${PYTHON_BUILD_PATH}/python${PYVERSION}.dll) +install(FILES ${dlls_to_install} DESTINATION ${DLL_DIRS}) -mo2_add_filter(NAME src/runner GROUPS - pythonrunner - pylogger - widgets -) +# install Python files +set(PYLIB_DIR ${MO2_INSTALL_PATH}/bin/plugins/plugin_python/libs) +file(GLOB libs_to_install ${PYTHON_BUILD_PATH}/pythoncore/*.pyd) +install(FILES ${libs_to_install} DESTINATION ${PYLIB_DIR}) +install(FILES ${PYTHON_BUILD_PATH}/pythoncore/python${PYVERSION}.zip + DESTINATION ${PYLIB_DIR} RENAME pythoncore.zip) -mo2_add_filter(NAME src/utils GROUPS - error - gilock - sipapiaccess -) - -mo2_add_filter(NAME src/wrappers GROUPS - gamefeatureswrappers - proxypluginwrappers - pythonwrapperutilities - uibasewrappers -) +# install PyQt6 +file(GLOB PYQT_DIR ${MO2_BUILD_PATH}/PyQt${QT_MAJOR_VERSION}*) +set(PYQT_LIB_DIR ${PYQT_DIR}/Lib/site-packages/PyQt${QT_MAJOR_VERSION}) +set(PYQT_TARGET_DIR ${PYLIB_DIR}/PyQt${QT_MAJOR_VERSION}) +file(GLOB pyqt_files ${PYQT_LIB_DIR}/*.py ${PYQT_LIB_DIR}/*.pyd ${PYQT_LIB_DIR}/*.pyi) +install(FILES ${pyqt_files} DESTINATION ${PYQT_TARGET_DIR}) diff --git a/src/runner/converters.h b/src/runner/converters.h deleted file mode 100644 index b7ee0dc..0000000 --- a/src/runner/converters.h +++ /dev/null @@ -1,581 +0,0 @@ -#ifndef PYTHON_CONVERTERS_HPP -#define PYTHON_CONVERTERS_HPP - -#include -#include -#include -#include -#include -#include - -// sip and qt slots seems to conflict -#include - -// Include the container converters from utils: -#include "pythonutils.h" - -namespace utils { - - namespace bpy = boost::python; - - namespace QString_converter { - - /** - * We need this since sip does not expose QString but uses standard python str. - */ - struct QString_to_python_str - { - static PyObject* convert(const QString& str) { - // It's safer to explicitly convert to unicode as if we don't, this can return - // either str or unicode without it being easy to know which to expect - bpy::object pyStr = bpy::object(qUtf8Printable(str)); - if (PyBytes_Check(pyStr.ptr())) - pyStr = pyStr.attr("decode")("utf-8"); - return bpy::incref(pyStr.ptr()); - } - }; - - struct QString_from_python_str - { - - static void* convertible(PyObject* objPtr) { - return PyBytes_Check(objPtr) || PyUnicode_Check(objPtr) ? objPtr : nullptr; - } - - static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { - // Ensure the string uses 8-bit characters - PyObject* strPtr = PyUnicode_Check(objPtr) ? PyUnicode_AsUTF8String(objPtr) : objPtr; - - // Extract the character data from the python string - const char* value = PyBytes_AsString(strPtr); - assert(value != nullptr); - - // allocate storage - void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - - // construct QString in the allocated memory - new (storage) QString(value); - - data->convertible = storage; - - // Deallocate local copy if one was made - if (strPtr != objPtr) - Py_DecRef(strPtr); - } - }; - - } - - namespace Enum_converter { - - /** - * - */ - template - struct Enum_to_int - { - static PyObject* convert(const Enum& flags) { - return bpy::incref(bpy::object(static_cast(flags)).ptr()); - } - }; - - template - struct Enum_from_python_obj - { - - static void* convertible(PyObject* objPtr) { - return PyLong_Check(objPtr) ? objPtr : nullptr; - } - - static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { - int intVersion = (int)PyLong_AsLong(objPtr); - void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - new (storage) Enum(static_cast(intVersion)); - data->convertible = storage; - } - }; - - } - - namespace QFlags_converter { - - /** - * - */ - template - struct QFlags_to_int - { - static PyObject* convert(const QFlags& flags) { - return bpy::incref(bpy::object(static_cast(flags)).ptr()); - } - }; - - template - struct QFlags_from_python_obj - { - - static void* convertible(PyObject* objPtr) { - return PyLong_Check(objPtr) ? objPtr : nullptr; - } - - static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { - int intVersion = (int)PyLong_AsLong(objPtr); - T tVersion = (T)intVersion; - void* storage = ((bpy::converter::rvalue_from_python_storage>*)data)->storage.bytes; - new (storage) QFlags(tVersion); - - data->convertible = storage; - } - }; - - } - - namespace QVariant_converter { - - struct QVariant_to_python_obj - { - static PyObject* convert(const QVariant& var) { - switch (var.typeId()) { - case QMetaType::Type::UnknownType: return bpy::incref(Py_None); - case QMetaType::Type::Int: return PyLong_FromLong(var.toInt()); - case QMetaType::Type::UInt: return PyLong_FromUnsignedLong(var.toUInt()); - case QMetaType::Type::Bool: return PyBool_FromLong(var.toBool()); - case QMetaType::Type::QString: return bpy::incref(bpy::object(var.toString()).ptr()); - // We need to check for StringList here because these are not considered List - // since List is QList will StringList is QList: - case QMetaType::Type::QStringList: return bpy::incref(bpy::object(var.toStringList()).ptr()); - case QMetaType::Type::QVariantList: { - return bpy::incref(bpy::object(var.toList()).ptr()); - } break; - case QMetaType::Type::QVariantMap: { - return bpy::incref(bpy::object(var.toMap()).ptr()); - } break; - default: { - PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.type()); - throw bpy::error_already_set(); - } break; - } - } - }; - - struct QVariant_from_python_obj - { - - static void* convertible(PyObject* objPtr) { - if (!PyBytes_Check(objPtr) && !PyUnicode_Check(objPtr) && !PyLong_Check(objPtr) && - !PyBool_Check(objPtr) && !PyList_Check(objPtr) && !PyDict_Check(objPtr) && - objPtr != Py_None) { - return nullptr; - } - return objPtr; - } - - template - static void constructVariant(const T& value, bpy::converter::rvalue_from_python_stage1_data* data) { - void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - - new (storage) QVariant(value); - - data->convertible = storage; - } - - static void constructVariant(bpy::converter::rvalue_from_python_stage1_data* data) { - void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - - new (storage) QVariant(); - - data->convertible = storage; - } - - static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { - if (PyList_Check(objPtr)) { - // We could check if all the elements can be converted to QString and store a QStringList - // in the QVariant but I am not sure that is really useful. - constructVariant(bpy::extract(objPtr)(), data); - } - else if (objPtr == Py_None) { - constructVariant(data); - } - else if (PyDict_Check(objPtr)) { - constructVariant(bpy::extract(objPtr)(), data); - } - else if (PyBytes_Check(objPtr) || PyUnicode_Check(objPtr)) { - constructVariant(bpy::extract(objPtr)(), data); - } - // PyBools will also return true for SIPLong_Check but not the other way around, so the order - // here is relevant. - else if (PyBool_Check(objPtr)) { - constructVariant(bpy::extract(objPtr)(), data); - } - else if (PyLong_Check(objPtr)) { - // QVariant doesn't have long. It has int or long long. Given that on m/s, - // long is 32 bits for 32- and 64- bit code... - constructVariant(bpy::extract(objPtr)(), data); - } - else { - PyErr_SetString(PyExc_TypeError, "type unsupported"); - throw bpy::error_already_set(); - } - } - }; - - } - - namespace QClass_converter { - - template struct MetaData; - - template <> struct MetaData { static const char* className() { return "QObject"; } }; - template <> struct MetaData { static const char* className() { return "QWidget"; } }; - template <> struct MetaData { static const char* className() { return "QMainWindow"; } }; - template <> struct MetaData { static const char* className() { return "QDateTime"; } }; - template <> struct MetaData { static const char* className() { return "QDir"; } }; - template <> struct MetaData { static const char* className() { return "QFileInfo"; } }; - template <> struct MetaData { static const char* className() { return "QIcon"; } }; - template <> struct MetaData { static const char* className() { return "QSize"; } }; - template <> struct MetaData { static const char* className() { return "QUrl"; } }; - template <> struct MetaData { static const char* className() { return "QVariant"; } }; - - template - struct QClass_converters - { - struct QClass_to_PyQt - { - template - static typename std::enable_if_t, T*> getSafeCopy(T* qClass) - { - return new T(*qClass); - } - - template - static typename std::enable_if_t, T*> getSafeCopy(T* qClass) - { - return qClass; - } - - static PyObject* convert(const T& object) { - const sipTypeDef* type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); - if (type == nullptr) { - return bpy::incref(Py_None); - } - - PyObject* sipObj = sipAPIAccess::sipAPI()->api_convert_from_type((void*)getSafeCopy((T*)&object), type, 0); - if (sipObj == nullptr) { - return bpy::incref(Py_None); - } - - if (std::is_copy_constructible_v) - // Ensure Python deletes the C++ component - sipAPIAccess::sipAPI()->api_transfer_back(sipObj); - - return bpy::incref(sipObj); - } - - static PyObject* convert(T* object) { - if (object == nullptr) { - return bpy::incref(Py_None); - } - - const sipTypeDef* type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); - if (type == nullptr) { - return bpy::incref(Py_None); - } - - PyObject* sipObj = sipAPIAccess::sipAPI()->api_convert_from_type(getSafeCopy(object), type, 0); - if (sipObj == nullptr) { - return bpy::incref(Py_None); - } - - if (std::is_copy_constructible_v) - // Ensure Python deletes the C++ component - sipAPIAccess::sipAPI()->api_transfer_back(sipObj); - - return bpy::incref(sipObj); - } - - static PyObject* convert(const T* object) { - return convert((T*)object); - } - - static PyTypeObject const* get_pytype() { - const sipTypeDef* type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); - if (type == nullptr) { - return bpy::incref(Py_None); - } - return bpy::incref(type->td_py_type); - } - }; - - static void* QClass_from_PyQt(PyObject* objPtr) - { - // This would transfer responsibility for deconstructing the object to C++, but Boost assumes l-value converters (such as this) don't do that - // Instead, this should be called within the wrappers for functions which return deletable pointers. - //sipAPI()->api_transfer_to(objPtr, Py_None); - if (PyObject_TypeCheck(objPtr, sipAPIAccess::sipAPI()->api_simplewrapper_type)) { - sipSimpleWrapper* wrapper; - wrapper = reinterpret_cast(objPtr); - return wrapper->data; - } - else if (PyObject_TypeCheck(objPtr, sipAPIAccess::sipAPI()->api_wrapper_type)) { - sipWrapper* wrapper; - wrapper = reinterpret_cast(objPtr); - return wrapper->super.data; - } - return nullptr; - } - }; - - } - - namespace details { - - inline bool has_arity(PyObject* object, std::size_t arity) { - // Mostly from https://stackoverflow.com/a/36143796/2666289 - bpy::object fn(bpy::handle<>(bpy::borrowed(object))); - - auto inspect = bpy::import("inspect"); - auto arg_spec = inspect.attr("getfullargspec")(fn); - bpy::object args = arg_spec.attr("args"), - varargs = arg_spec.attr("varargs"), - defaults = arg_spec.attr("defaults"); - - auto args_count = args ? bpy::len(args) : 0; - auto defaults_count = defaults ? bpy::len(defaults) : 0; - - if (static_cast(inspect.attr("ismethod")(fn)) && fn.attr("__self__")) { - --args_count; - } - - auto required_count = args_count - defaults_count; - - return required_count <= arity // Cannot require more parameters than given, - && (args_count >= arity || varargs); // Must accept enough parameters. - } - - template - struct wrap_impl; - - template <> - struct wrap_impl<> { - template - static decltype(auto) apply(T&& t) { return std::forward(t); } - }; - - template - struct wrap_impl, Ws... > : public wrap_impl<> { - using wrap_impl<>::apply; - - static auto apply(T t) { - return boost::ref(t); - } - }; - - template - struct wrap_impl, Ws... > : public wrap_impl<> { - using wrap_impl<>::apply; - - static auto apply(T t) { - return bpy::ptr(t); - } - }; - - template - struct wrap_impl { - template - static decltype(auto) apply(T&& t) { - return wrap::apply(std::forward(t)); - } - }; - - } - - /** - * @brief Convert a python callable to a valid C++ Callable object. Also works - * for None. - */ - template - struct Functor_converter; - - template - struct Functor_converter - { - - template - static decltype(auto) wrap(T&& t) { - return details::wrap_impl::apply(std::forward(t)); - } - - struct FunctorWrapper - { - FunctorWrapper(boost::python::object callable) : m_Callable(callable) { - } - - ~FunctorWrapper() { - GILock lock; - m_Callable = bpy::object(); - } - - R operator()(Args... params) { - GILock lock; - try { - if constexpr (std::is_same_v) { - m_Callable(wrap(params)... ); - } - else { - return bpy::extract(m_Callable(wrap(params)... )); - } - } - catch (const boost::python::error_already_set&) { - throw pyexcept::PythonError(); - } - catch (...) { - throw pyexcept::UnknownException(); - } - } - - boost::python::object m_Callable; - }; - - static void* convertible(PyObject* object) - { - // We allow None here, we will just default-construct a std::function: - if (object == Py_None) { - return object; - } - - // Otherwize we check that we have a callable object: - if (!PyCallable_Check(object) || !details::has_arity(object, sizeof...(Args))) { - return nullptr; - } - return object; - } - - static void construct(PyObject* object, bpy::converter::rvalue_from_python_stage1_data* data) - { - bpy::object callable(bpy::handle<>(bpy::borrowed(object))); - void* storage =((bpy::converter::rvalue_from_python_storage>*)data)->storage.bytes; - if (callable.is_none()) { - new (storage) std::function{}; - } - else { - new (storage) std::function(FunctorWrapper(callable)); - } - data->convertible = storage; - } - }; - - - - /** - * @brief Call policy that automatically downcast shared pointer of type FromType - * to shared pointer of type ToType. - */ - template - struct DowncastConverter { - - bool convertible() const { return true; } - - inline PyObject* operator()(std::shared_ptr p) const { - if (p == nullptr) { - return bpy::detail::none(); - } - else { - auto downcast_p = std::dynamic_pointer_cast(p); - bpy::object p_value = downcast_p == nullptr ? bpy::object{ p } : bpy::object{ downcast_p }; - return bpy::incref(p_value.ptr()); - } - } - - inline PyTypeObject const* get_pytype() const { - return bpy::converter::registered_pytype::get_pytype(); - } - - }; - - template - struct downcast_return { - - template - struct apply_; - - template - struct apply_> { - static_assert(std::is_convertible_v, std::shared_ptr>); - using type = DowncastConverter; - }; - - template - using apply = apply_>; - - }; - - // Functions: - inline void register_qstring_converter() { - using namespace QString_converter; - bpy::to_python_converter(); - bpy::converter::registry::push_back( - &QString_from_python_str::convertible, - &QString_from_python_str::construct, - bpy::type_id()); - } - - inline void register_qvariant_converter() { - using namespace QVariant_converter; - bpy::to_python_converter(); - bpy::converter::registry::push_back( - &QVariant_from_python_obj::convertible, - &QVariant_from_python_obj::construct, - bpy::type_id()); - } - - template - inline void register_qflags_converter() { - using T = typename Flags::enum_type; - using namespace QFlags_converter; - bpy::to_python_converter>(); - bpy::converter::registry::push_back( - &QFlags_from_python_obj::convertible, - &QFlags_from_python_obj::construct, - bpy::type_id()); - } - - template - inline void register_enum_converter() { - using namespace Enum_converter; - bpy::to_python_converter>(); - bpy::converter::registry::push_back( - &Enum_from_python_obj::convertible, - &Enum_from_python_obj::construct, - bpy::type_id()); - } - - template - inline void register_qclass_converter() { - using Converter = QClass_converter::QClass_converters; - bpy::converter::registry::insert(&Converter::QClass_from_PyQt, bpy::type_id()); - bpy::to_python_converter(); - bpy::to_python_converter(); - bpy::to_python_converter(); - } - - /** - * @brief Register a functor converter. - * - * @tparam Fn The function type to register. - * @tparam Wrappers... A list of wrapper (boost::python::pointer_wrapper or boost::reference_wrapper) - * indicating if parameters of the given (wrapped) type must be wrapped. - */ - template - inline void register_functor_converter() { - using Converter = Functor_converter; - bpy::converter::registry::push_back( - &Converter::convertible, - &Converter::construct, - bpy::type_id>()); - } - - - -} - -#endif \ No newline at end of file diff --git a/src/runner/error.cpp b/src/runner/error.cpp deleted file mode 100644 index 7ef6700..0000000 --- a/src/runner/error.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef Q_MOC_RUN -#include -#endif -#include -#include -#include "error.h" - -using namespace MOBase; -namespace bpy = boost::python; - -ErrWrapper & ErrWrapper::instance() -{ - static ErrWrapper err; - return err; -} - -void ErrWrapper::write(const char * message) -{ - buffer << message; - if (buffer.tellp() != 0 && buffer.str().back() == '\n') - { - // actually put the string in a variable so it doesn't get destroyed as soon as we get a pointer to its data - std::string string = buffer.str().substr(0, buffer.str().length() - 1); - qCritical().nospace().noquote() << string.c_str(); - buffer = std::stringstream(); - } - - if (recordingExceptionMessage) - { - lastException << message; - } -} - -void ErrWrapper::startRecordingExceptionMessage() -{ - recordingExceptionMessage = true; - lastException = std::stringstream(); -} - -void ErrWrapper::stopRecordingExceptionMessage() -{ - recordingExceptionMessage = false; -} - -QString ErrWrapper::getLastExceptionMessage() -{ - return QString::fromStdString(lastException.str()); -} diff --git a/src/runner/error.h b/src/runner/error.h index b44bd5b..ac8683f 100644 --- a/src/runner/error.h +++ b/src/runner/error.h @@ -1,123 +1,81 @@ -#ifndef ERROR_H -#define ERROR_H - -#include - -#include - -#include -#include - -struct ErrWrapper -{ - static ErrWrapper& instance(); - - void write(const char* message); - - void startRecordingExceptionMessage(); - - void stopRecordingExceptionMessage(); - - QString getLastExceptionMessage(); - - std::stringstream buffer; - bool recordingExceptionMessage; - std::stringstream lastException; -}; - -namespace pyexcept { - - /** - * @brief Exception to throw when a python implementation does not implement - * a pure virtual function. - */ - class MissingImplementation : public MOBase::Exception { - public: - MissingImplementation(std::string const& className, std::string const& methodName) : - Exception(QString::fromStdString( - fmt::format("Python class implementing \"{}\" has no implementation of method \"{}\".", - className, methodName))) { } - - }; - - /** - * @brief Exception to throw when a python error occurs. - */ - class PythonError : public MOBase::Exception { - public: - - /** - * @brief Create a new PythonError, fetching the error message from python. If the message - * cannot be retrieved, `defaultErrorMessage()` is used instead. - */ - PythonError() : Exception(getPythonErrorMessage()) { } - - /** - * @brief Create a new PythonError with the given message. - * - * @param message Message for the exception. - */ - PythonError(QString message) : Exception(message) { } - - protected: - - /** - * - */ - static QString defaultErrorMessage() { - return QObject::tr("An unexpected C++ exception was thrown in python code."); - } - - /** - * - */ - static QString getPythonErrorMessage() { - if (PyErr_Occurred()) { - ErrWrapper& errWrapper = ErrWrapper::instance(); - - errWrapper.startRecordingExceptionMessage(); - PyErr_Print(); - errWrapper.stopRecordingExceptionMessage(); - - return errWrapper.getLastExceptionMessage(); - } - else { - return defaultErrorMessage(); - } - } - }; - - /** - * @brief Exception to throw when an unknown error occured. This is typically thrown - * from a catch(...) block. - */ - class UnknownException : public MOBase::Exception { - public: - - /** - * @brief Create a new UnknownException with the default message. - * - * @see defaultErrorMessage - */ - UnknownException() : Exception(defaultErrorMessage()) { } - - /** - * @brief Create a new UnknownException with the given message. - * - * @param message Message for the exception. - */ - UnknownException(QString message) : Exception(message) { } - - protected: - - /** - * - */ - static QString defaultErrorMessage() { - return QObject::tr("An unknown exception was thrown in python code."); - } - }; - -} - -#endif // ERROR_H +#ifndef ERROR_H +#define ERROR_H + +#include + +#include +#include + +#include + +namespace pyexcept { + + /** + * @brief Exception to throw when a python implementation does not implement + * a pure virtual function. + */ + class MissingImplementation : public MOBase::Exception { + public: + MissingImplementation(std::string const& className, + std::string const& methodName) + : Exception(QString::fromStdString( + fmt::format("Python class implementing \"{}\" has no " + "implementation of method \"{}\".", + className, methodName))) + { + } + }; + + /** + * @brief Exception to throw when a python error occurs. + */ + class PythonError : public MOBase::Exception { + public: + /** + * @brief Create a new PythonError, fetching the error message from + * python. If the message cannot be retrieved, `defaultErrorMessage()` + * is used instead. + */ + PythonError(pybind11::error_already_set const& ex) : Exception(ex.what()) {} + + /** + * @brief Create a new PythonError with the given message. + * + * @param message Message for the exception. + */ + PythonError(QString message) : Exception(message) {} + }; + + /** + * @brief Exception to throw when an unknown error occured. This is + * typically thrown from a catch(...) block. + */ + class UnknownException : public MOBase::Exception { + public: + /** + * @brief Create a new UnknownException with the default message. + * + * @see defaultErrorMessage + */ + UnknownException() : Exception(defaultErrorMessage()) {} + + /** + * @brief Create a new UnknownException with the given message. + * + * @param message Message for the exception. + */ + UnknownException(QString message) : Exception(message) {} + + protected: + /** + * + */ + static QString defaultErrorMessage() + { + return QObject::tr("An unknown exception was thrown in python code."); + } + }; + +} // namespace pyexcept + +#endif // ERROR_H diff --git a/src/runner/gamefeatureswrappers.cpp b/src/runner/gamefeatureswrappers.cpp deleted file mode 100644 index cbfa5cc..0000000 --- a/src/runner/gamefeatureswrappers.cpp +++ /dev/null @@ -1,344 +0,0 @@ -#include "gamefeatureswrappers.h" - -#include -#include - -#include -#include -#include -#include - -#include "shared_ptr_converter.h" -#include "ifiletree.h" -#include "pythonwrapperutilities.h" - -///////////////////////////// -/// BSAInvalidation Wrapper - - -bool BSAInvalidationWrapper::isInvalidationBSA(const QString &bsaName) -{ - return basicWrapperFunctionImplementation(this, "isInvalidationBSA", bsaName); -} - -void BSAInvalidationWrapper::deactivate(MOBase::IProfile *profile) -{ - return basicWrapperFunctionImplementation(this, "deactivate", boost::python::ptr(profile)); -} - -void BSAInvalidationWrapper::activate(MOBase::IProfile *profile) -{ - return basicWrapperFunctionImplementation(this, "activate", boost::python::ptr(profile)); -} - -bool BSAInvalidationWrapper::prepareProfile(MOBase::IProfile *profile) -{ - return basicWrapperFunctionImplementation(this, "prepareProfile", boost::python::ptr(profile)); -} -/// end BSAInvalidation Wrapper -///////////////////////////// -/// DataArchives Wrapper - - -QStringList DataArchivesWrapper::vanillaArchives() const -{ - return basicWrapperFunctionImplementation(this, "vanillaArchives"); -} - -QStringList DataArchivesWrapper::archives(const MOBase::IProfile *profile) const -{ - return basicWrapperFunctionImplementation(this, "archives", boost::python::ptr(profile)); -} - -void DataArchivesWrapper::addArchive(MOBase::IProfile *profile, int index, const QString &archiveName) -{ - return basicWrapperFunctionImplementation(this, "addArchive", boost::python::ptr(profile), index, archiveName); -} - -void DataArchivesWrapper::removeArchive(MOBase::IProfile *profile, const QString &archiveName) -{ - return basicWrapperFunctionImplementation(this, "removeArchive", boost::python::ptr(profile), archiveName); -} -/// end DataArchives Wrapper -///////////////////////////// -/// GamePlugins Wrapper - - -void GamePluginsWrapper::writePluginLists(const MOBase::IPluginList * pluginList) -{ - return basicWrapperFunctionImplementation(this, "writePluginLists", boost::python::ptr(pluginList)); -} - -void GamePluginsWrapper::readPluginLists(MOBase::IPluginList * pluginList) -{ - return basicWrapperFunctionImplementation(this, "readPluginLists", boost::python::ptr(pluginList)); -} - -QStringList GamePluginsWrapper::getLoadOrder() -{ - return basicWrapperFunctionImplementation(this, "getLoadOrder"); -} - -bool GamePluginsWrapper::lightPluginsAreSupported() -{ - return basicWrapperFunctionImplementation(this, "lightPluginsAreSupported"); -} - -/// end GamePlugins Wrapper -///////////////////////////// -/// LocalSavegames Wrapper - - -MappingType LocalSavegamesWrapper::mappings(const QDir & profileSaveDir) const -{ - return basicWrapperFunctionImplementation(this, "mappings", profileSaveDir); -} - -bool LocalSavegamesWrapper::prepareProfile(MOBase::IProfile * profile) -{ - return basicWrapperFunctionImplementation(this, "prepareProfile", boost::python::ptr(profile)); -} - -/// end LocalSavegames Wrapper -///////////////////////////// -/// ModDataChecker Wrapper - -ModDataChecker::CheckReturn ModDataCheckerWrapper::dataLooksValid(std::shared_ptr fileTree) const { - return basicWrapperFunctionImplementation(this, "dataLooksValid", fileTree); -} - -std::shared_ptr ModDataCheckerWrapper::fix(std::shared_ptr fileTree) const { - return utils::clean_shared_ptr( - basicWrapperFunctionImplementationWithDefault>( - this, [](auto&&... args) { return nullptr; }, "fix", fileTree)); -} - -/// end ModDataChecker Wrapper -///////////////////////////// -/// ModDataContent Wrapper - -std::vector ModDataContentWrapper::getAllContents() const { - return basicWrapperFunctionImplementation>(this, "getAllContents"); -} -std::vector ModDataContentWrapper::getContentsFor(std::shared_ptr fileTree) const { - return basicWrapperFunctionImplementation>(this, "getContentsFor", fileTree); -} - -/// end ModDataContent Wrapper -///////////////////////////// -/// SaveGameInfo Wrapper - -SaveGameInfoWrapper::MissingAssets SaveGameInfoWrapper::getMissingAssets(MOBase::ISaveGame const& save) const -{ - return basicWrapperFunctionImplementation(this, "getMissingAssets", boost::ref(save)); -} - -MOBase::ISaveGameInfoWidget* SaveGameInfoWrapper::getSaveGameWidget(QWidget* parent) const -{ - return basicWrapperFunctionImplementation(this, m_SaveGameWidget, "getSaveGameWidget", parent); -} - -/// end SaveGameInfo Wrapper -///////////////////////////// -/// ScriptExtender Wrapper - -QString ScriptExtenderWrapper::BinaryName() const -{ - return basicWrapperFunctionImplementation(this, "BinaryName"); -} - -QString ScriptExtenderWrapper::PluginPath() const -{ - return basicWrapperFunctionImplementation(this, "PluginPath"); -} - -QString ScriptExtenderWrapper::loaderName() const -{ - return basicWrapperFunctionImplementation(this, "loaderName"); -} - -QString ScriptExtenderWrapper::loaderPath() const -{ - return basicWrapperFunctionImplementation(this, "loaderPath"); -} - -QString ScriptExtenderWrapper::savegameExtension() const -{ - return basicWrapperFunctionImplementation(this, "savegameExtension"); -} - -bool ScriptExtenderWrapper::isInstalled() const -{ - return basicWrapperFunctionImplementation(this, "isInstalled"); -} - -QString ScriptExtenderWrapper::getExtenderVersion() const -{ - return basicWrapperFunctionImplementation(this, "getExtenderVersion"); -} - -WORD ScriptExtenderWrapper::getArch() const -{ - return basicWrapperFunctionImplementation(this, "getArch"); -} - -/// end ScriptExtender Wrapper -///////////////////////////// -/// UnmanagedMods Wrapper - - -QStringList UnmanagedModsWrapper::mods(bool onlyOfficial) const -{ - return basicWrapperFunctionImplementation(this, "mods", onlyOfficial); -} - -QString UnmanagedModsWrapper::displayName(const QString & modName) const -{ - return basicWrapperFunctionImplementation(this, "displayName", modName); -} - -QFileInfo UnmanagedModsWrapper::referenceFile(const QString & modName) const -{ - return basicWrapperFunctionImplementation(this, "referenceFile", modName); -} - -QStringList UnmanagedModsWrapper::secondaryFiles(const QString & modName) const -{ - return basicWrapperFunctionImplementation(this, "secondaryFiles", modName); -} -/// end UnmanagedMods Wrapper -///////////////////////////// - - -game_features_map_from_python::game_features_map_from_python() -{ - boost::python::converter::registry::push_back(&convertible, &construct, boost::python::type_id>()); -} - -void * game_features_map_from_python::convertible(PyObject * objPtr) -{ - return PyDict_Check(objPtr) ? objPtr : nullptr; -} - -template -void insertGameFeature(std::map& map, const boost::python::object& pyObject) -{ - map[std::type_index(typeid(T))] = boost::python::extract(pyObject)(); -} - -void game_features_map_from_python::construct(PyObject * objPtr, boost::python::converter::rvalue_from_python_stage1_data * data) -{ - void *storage = ((boost::python::converter::rvalue_from_python_storage>*)data)->storage.bytes; - std::map *result = new (storage) std::map(); - boost::python::dict source(boost::python::handle<>(boost::python::borrowed(objPtr))); - boost::python::list keys = source.keys(); - int len = boost::python::len(keys); - for (int i = 0; i < len; ++i) - { - boost::python::object pyKey = keys[i]; - boost::python::object pyValue = source[pyKey]; - - boost::mp11::mp_for_each< - // Must user pointers because mp_for_each construct object: - boost::mp11::mp_transform - >([&](auto* pt) { - using T = std::remove_pointer_t; - boost::python::extract extract(pyValue); - if (extract.check()) { - (*result)[std::type_index(typeid(T))] = extract(); - } - }); - } - - data->convertible = storage; -} - -void registerGameFeaturesPythonConverters() -{ - namespace bpy = boost::python; - - game_features_map_from_python(); - - // Features require defs for all methods as Python can access C++ features - bpy::class_("BSAInvalidation") - .def("isInvalidationBSA", bpy::pure_virtual(&BSAInvalidation::isInvalidationBSA), bpy::arg("name")) - .def("deactivate", bpy::pure_virtual(&BSAInvalidation::deactivate), bpy::arg("profile")) - .def("activate", bpy::pure_virtual(&BSAInvalidation::activate), bpy::arg("profile")) - ; - - bpy::class_("DataArchives") - .def("vanillaArchives", bpy::pure_virtual(&DataArchives::vanillaArchives)) - .def("archives", bpy::pure_virtual(&DataArchives::archives), bpy::arg("profile")) - .def("addArchive", bpy::pure_virtual(&DataArchives::addArchive), (bpy::arg("profile"), "index", "name")) - .def("removeArchive", bpy::pure_virtual(&DataArchives::removeArchive), (bpy::arg("profile"), "name")) - ; - - bpy::class_("GamePlugins") - .def("writePluginLists", bpy::pure_virtual(&GamePlugins::writePluginLists), bpy::arg("plugin_list")) - .def("readPluginLists", bpy::pure_virtual(&GamePlugins::readPluginLists), bpy::arg("plugin_list")) - .def("getLoadOrder", bpy::pure_virtual(&GamePlugins::getLoadOrder)) - .def("lightPluginsAreSupported", bpy::pure_virtual(&GamePlugins::lightPluginsAreSupported)) - ; - - bpy::class_("LocalSavegames") - .def("mappings", bpy::pure_virtual(&LocalSavegames::mappings), bpy::arg("profile_save_dir")) - .def("prepareProfile", bpy::pure_virtual(&LocalSavegames::prepareProfile), bpy::arg("profile")) - ; - - auto modDataCheckerClass = bpy::class_("ModDataChecker"); - { - bpy::scope scope = modDataCheckerClass; - - bpy::enum_("CheckReturn") - .value("INVALID", ModDataChecker::CheckReturn::INVALID) - .value("FIXABLE", ModDataChecker::CheckReturn::FIXABLE) - .value("VALID", ModDataChecker::CheckReturn::VALID) - .export_values() - ; - - modDataCheckerClass - .def("dataLooksValid", bpy::pure_virtual(&ModDataChecker::dataLooksValid), bpy::arg("filetree")) - .def("fix", bpy::pure_virtual(&ModDataChecker::fix), bpy::arg("filetree")) - ; - } - - { - bpy::scope scope = bpy::class_("ModDataContent") - .def("getAllContents", bpy::pure_virtual(&ModDataContent::getAllContents)) - .def("getContentsFor", bpy::pure_virtual(&ModDataContent::getContentsFor), bpy::arg("filetree")) - ; - - bpy::class_("Content", - bpy::init>((bpy::arg("id"), "name", "icon", bpy::arg("filter_only") = false))) - .add_property("id", &ModDataContent::Content::id) - .add_property("name", &ModDataContent::Content::name) - .add_property("icon", &ModDataContent::Content::icon) - .def("isOnlyForFilter", &ModDataContent::Content::isOnlyForFilter) - ; - - } - - bpy::class_("SaveGameInfo") - .def("getMissingAssets", bpy::pure_virtual(&SaveGameInfo::getMissingAssets), bpy::arg("save")) - .def("getSaveGameWidget", bpy::pure_virtual(&SaveGameInfo::getSaveGameWidget), bpy::return_value_policy(), - bpy::arg("parent"), "[optional]") - ; - - bpy::class_("ScriptExtender") - .def("BinaryName", bpy::pure_virtual(&ScriptExtender::BinaryName)) - .def("PluginPath", bpy::pure_virtual(&ScriptExtender::PluginPath)) - .def("loaderName", bpy::pure_virtual(&ScriptExtender::loaderName)) - .def("loaderPath", bpy::pure_virtual(&ScriptExtender::loaderPath)) - .def("savegameExtension", bpy::pure_virtual(&ScriptExtender::savegameExtension)) - .def("isInstalled", bpy::pure_virtual(&ScriptExtender::isInstalled)) - .def("getExtenderVersion", bpy::pure_virtual(&ScriptExtender::getExtenderVersion)) - .def("getArch", bpy::pure_virtual(&ScriptExtender::getArch)) - ; - - bpy::class_("UnmanagedMods") - .def("mods", bpy::pure_virtual(&UnmanagedMods::mods), bpy::arg("official_only")) - .def("displayName", bpy::pure_virtual(&UnmanagedMods::displayName), bpy::arg("mod_name")) - .def("referenceFile", bpy::pure_virtual(&UnmanagedMods::referenceFile), bpy::arg("mod_name")) - .def("secondaryFiles", bpy::pure_virtual(&UnmanagedMods::secondaryFiles), bpy::arg("mod_name")) - ; -} diff --git a/src/runner/gamefeatureswrappers.h b/src/runner/gamefeatureswrappers.h deleted file mode 100644 index 7155d8c..0000000 --- a/src/runner/gamefeatureswrappers.h +++ /dev/null @@ -1,158 +0,0 @@ -#ifndef GAMEFEATURESWRAPPERS_H -#define GAMEFEATURESWRAPPERS_H - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// this might need turning off if Q_MOC_RUN is defined -#include -#include - -// This is a simple MPL list that contains all the game features in one place: -using MpGameFeaturesList = boost::mp11::mp_list< - BSAInvalidation, - DataArchives, - GamePlugins, - LocalSavegames, - ModDataChecker, - ModDataContent, - SaveGameInfo, - ScriptExtender, - UnmanagedMods ->; - -///////////////////////////// -/// Wrapper declarations - -class BSAInvalidationWrapper : public BSAInvalidation, public boost::python::wrapper -{ -public: - static constexpr const char* className = "BSAInvalidationWrapper"; - using boost::python::wrapper::get_override; - - virtual bool isInvalidationBSA(const QString &bsaName) override; - virtual void deactivate(MOBase::IProfile *profile) override; - virtual void activate(MOBase::IProfile *profile) override; - virtual bool prepareProfile(MOBase::IProfile *profile) override; -}; - -class DataArchivesWrapper : public DataArchives, public boost::python::wrapper -{ -public: - static constexpr const char* className = "DataArchivesWrapper"; - using boost::python::wrapper::get_override; - - virtual QStringList vanillaArchives() const override; - virtual QStringList archives(const MOBase::IProfile *profile) const override; - virtual void addArchive(MOBase::IProfile *profile, int index, const QString &archiveName) override; - virtual void removeArchive(MOBase::IProfile *profile, const QString &archiveName) override; -}; - -class GamePluginsWrapper : public GamePlugins, public boost::python::wrapper -{ -public: - static constexpr const char* className = "GamePluginsWrapper"; - using boost::python::wrapper::get_override; - - virtual void writePluginLists(const MOBase::IPluginList *pluginList) override; - virtual void readPluginLists(MOBase::IPluginList *pluginList) override; - virtual QStringList getLoadOrder() override; - virtual bool lightPluginsAreSupported() override; -}; - -class LocalSavegamesWrapper : public LocalSavegames, public boost::python::wrapper -{ -public: - static constexpr const char* className = "LocalSavegamesWrapper"; - using boost::python::wrapper::get_override; - - virtual MappingType mappings(const QDir &profileSaveDir) const override; - virtual bool prepareProfile(MOBase::IProfile *profile) override; -}; - -class ModDataCheckerWrapper : public ModDataChecker, public boost::python::wrapper -{ -public: - static constexpr const char* className = "ModDataCheckerWrapper"; - using boost::python::wrapper::get_override; - - virtual CheckReturn dataLooksValid(std::shared_ptr fileTree) const override; - virtual std::shared_ptr fix(std::shared_ptr fileTree) const override; -}; - -class ModDataContentWrapper : public ModDataContent, public boost::python::wrapper -{ -public: - static constexpr const char* className = "ModDataContentWrapper"; - using boost::python::wrapper::get_override; - - virtual std::vector getAllContents() const override; - virtual std::vector getContentsFor(std::shared_ptr fileTree) const override; - -}; - -class SaveGameInfoWrapper : public SaveGameInfo, public boost::python::wrapper -{ -public: - static constexpr const char* className = "SaveGameInfoWrapper"; - using boost::python::wrapper::get_override; - - virtual MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const override; - virtual MOBase::ISaveGameInfoWidget *getSaveGameWidget(QWidget *parent = 0) const override; - -private: - // We need to keep the python objects alive: - mutable std::map m_SaveGames; - mutable boost::python::object m_SaveGameWidget; -}; - -class ScriptExtenderWrapper : public ScriptExtender, public boost::python::wrapper -{ -public: - static constexpr const char* className = "ScriptExtenderWrapper"; - using boost::python::wrapper::get_override; - - virtual QString BinaryName() const override; - virtual QString PluginPath() const override; - virtual QString loaderName() const override; - virtual QString loaderPath() const override; - virtual QString savegameExtension() const override; - virtual bool isInstalled() const override; - virtual QString getExtenderVersion() const override; - virtual WORD getArch() const override; -}; - -class UnmanagedModsWrapper : public UnmanagedMods, public boost::python::wrapper -{ -public: - static constexpr const char* className = "UnmanagedModsWrapper"; - using boost::python::wrapper::get_override; - - virtual QStringList mods(bool onlyOfficial) const override; - virtual QString displayName(const QString &modName) const override; - virtual QFileInfo referenceFile(const QString &modName) const override; - virtual QStringList secondaryFiles(const QString &modName) const override; -}; - -/// end Wrapper declarations -///////////////////////////// - -struct game_features_map_from_python -{ - game_features_map_from_python(); - static void *convertible(PyObject *objPtr); - static void construct(PyObject *objPtr, boost::python::converter::rvalue_from_python_stage1_data *data); -}; - -void registerGameFeaturesPythonConverters(); - -#endif // GAMEFEATURESWRAPPERS_H diff --git a/src/runner/gilock.cpp b/src/runner/gilock.cpp deleted file mode 100644 index fb63387..0000000 --- a/src/runner/gilock.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "gilock.h" - -GILock::GILock() -{ - m_State = PyGILState_Ensure(); -} - -GILock::~GILock() -{ - PyErr_Clear(); - PyGILState_Release(m_State); -} diff --git a/src/runner/gilock.h b/src/runner/gilock.h deleted file mode 100644 index 560e29a..0000000 --- a/src/runner/gilock.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef GILOCK_H -#define GILOCK_H - - -#ifndef Q_MOC_RUN -#include -#endif // Q_MOC_RUN - -class GILock { -public: - GILock(); - ~GILock(); -private: - PyGILState_STATE m_State; -}; - - -#endif // GILOCK_H diff --git a/src/runner/proxypluginwrappers.cpp b/src/runner/proxypluginwrappers.cpp deleted file mode 100644 index 52f1c3b..0000000 --- a/src/runner/proxypluginwrappers.cpp +++ /dev/null @@ -1,473 +0,0 @@ -#include "proxypluginwrappers.h" - -#include "gilock.h" -#include -#include - -#include "shared_ptr_converter.h" -#include "pythonwrapperutilities.h" -#include "uibasewrappers.h" - -#include -#include - -namespace boost -{ - // See bug https://connect.microsoft.com/VisualStudio/Feedback/Details/2852624 -#if (_MSC_VER == 1900) - template<> const volatile MOBase::IOrganizer* get_pointer(const volatile MOBase::IOrganizer* p) { return p; } - template<> const volatile MOBase::IModInterface* get_pointer(const volatile MOBase::IModInterface* p) { return p; } - template<> const volatile MOBase::IPluginGame* get_pointer(const volatile MOBase::IPluginGame* p) { return p; } - template<> const volatile MOBase::IProfile* get_pointer(const volatile MOBase::IProfile* p) { return p; } - template<> const volatile MOBase::IModList* get_pointer(const volatile MOBase::IModList* p) { return p; } - template<> const volatile MOBase::IPluginList* get_pointer(const volatile MOBase::IPluginList* p) { return p; } - template<> const volatile MOBase::IDownloadManager* get_pointer(const volatile MOBase::IDownloadManager* p) { return p; } - template<> const volatile MOBase::IModRepositoryBridge* get_pointer(const volatile MOBase::IModRepositoryBridge* p) { return p; } -#endif -} - -using namespace MOBase; - -// See COMMON_I_PLUGIN_WRAPPER_DECLARATIONS__IMPL in proxypluginwrappers.h for explanation on -// the "include_requirements". -#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_IMPL(class_name, include_requirements) \ -bool class_name::init(MOBase::IOrganizer *moInfo) \ -{ \ - return basicWrapperFunctionImplementation(this, "init", boost::python::ptr(moInfo)); \ -} \ - \ -QString class_name::name() const \ -{ \ - return basicWrapperFunctionImplementation(this, "name"); \ -} \ - \ -QString class_name::localizedName() const \ -{ \ - return basicWrapperFunctionImplementationWithDefault(this, &class_name::localizedName_Default, "localizedName"); \ -} \ - \ -QString class_name::master() const \ -{ \ - return basicWrapperFunctionImplementationWithDefault(this, &class_name::master_Default, "master"); \ -} \ - \ -QString class_name::author() const \ -{ \ - return basicWrapperFunctionImplementation(this, "author"); \ -} \ - \ -QString class_name::description() const \ -{ \ - return basicWrapperFunctionImplementation(this, "description"); \ -} \ - \ -MOBase::VersionInfo class_name::version() const \ -{ \ - return basicWrapperFunctionImplementation(this, "version"); \ -} \ - \ -QList class_name::settings() const \ -{ \ - return basicWrapperFunctionImplementation>(this, "settings"); \ -} \ -QString class_name::localizedName_Default() const { return IPlugin::localizedName(); } \ -QString class_name::master_Default() const { return IPlugin::master(); } \ -BOOST_PP_EXPR_IF(include_requirements, \ - std::vector> class_name::requirements() const { \ - return basicWrapperFunctionImplementationWithDefault>>( \ - this, &class_name::requirements_Default, "requirements"); \ - } \ - std::vector> class_name::requirements_Default() const { return IPlugin::requirements(); } \ - bool class_name::enabledByDefault() const \ - { \ - return basicWrapperFunctionImplementationWithDefault(this, &class_name::enabledByDefault_Default, "enabledByDefault"); \ - } \ - bool class_name::enabledByDefault_Default() const { return IPlugin::enabledByDefault(); }) - -#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(class_name) COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_IMPL(class_name, 1) - -/// end COMMON_I_PLUGIN_WRAPPER_DEFINITIONS -///////////////////////////// -/// IPlugin Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginWrapper) -/// end IPlugin Wrapper -///////////////////////////////////// -/// IPluginDiagnose Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginDiagnoseWrapper) - -std::vector IPluginDiagnoseWrapper::activeProblems() const -{ - return basicWrapperFunctionImplementation>(this, "activeProblems"); -} - -QString IPluginDiagnoseWrapper::shortDescription(unsigned int key) const -{ - return basicWrapperFunctionImplementation(this, "shortDescription", key); -} - -QString IPluginDiagnoseWrapper::fullDescription(unsigned int key) const -{ - return basicWrapperFunctionImplementation(this, "fullDescription", key); -} - -bool IPluginDiagnoseWrapper::hasGuidedFix(unsigned int key) const -{ - return basicWrapperFunctionImplementation(this, "hasGuidedFix", key); -} - -void IPluginDiagnoseWrapper::startGuidedFix(unsigned int key) const -{ - basicWrapperFunctionImplementation(this, "startGuidedFix", key); -} - -/// end IPluginDiagnose Wrapper -///////////////////////////////////// -/// IPluginFileMapper Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginFileMapperWrapper) - -MappingType IPluginFileMapperWrapper::mappings() const -{ - return basicWrapperFunctionImplementation(this, "mappings"); -} -/// end IPluginFileMapper Wrapper -///////////////////////////////////// -/// IPluginGame Wrapper - -void IPluginGameWrapper::detectGame() -{ - return basicWrapperFunctionImplementation(this, "detectGame"); -} - -QString IPluginGameWrapper::gameName() const -{ - return basicWrapperFunctionImplementation(this, "gameName"); -} - -void IPluginGameWrapper::initializeProfile(const QDir & directory, ProfileSettings settings) const -{ - basicWrapperFunctionImplementation(this, "initializeProfile", directory, settings); -} - -std::vector> IPluginGameWrapper::listSaves(QDir folder) const -{ - // Why do I not need to hold python references here? Is it because those are wrapped - // in shared_ptr? - return basicWrapperFunctionImplementation>>(this, "listSaves", folder); -} - -bool IPluginGameWrapper::isInstalled() const -{ - return basicWrapperFunctionImplementation(this, "isInstalled"); -} - -QIcon IPluginGameWrapper::gameIcon() const -{ - return basicWrapperFunctionImplementation(this, "gameIcon"); -} - -QDir IPluginGameWrapper::gameDirectory() const -{ - return basicWrapperFunctionImplementation(this, "gameDirectory"); -} - -QDir IPluginGameWrapper::dataDirectory() const -{ - return basicWrapperFunctionImplementation(this, "dataDirectory"); -} - -void IPluginGameWrapper::setGamePath(const QString & path) -{ - basicWrapperFunctionImplementation(this, "setGamePath", path); -} - -QDir IPluginGameWrapper::documentsDirectory() const -{ - return basicWrapperFunctionImplementation(this, "documentsDirectory"); -} - -QDir IPluginGameWrapper::savesDirectory() const -{ - return basicWrapperFunctionImplementation(this, "savesDirectory"); -} - -QList IPluginGameWrapper::executables() const -{ - return basicWrapperFunctionImplementation>(this, "executables"); -} - -QList IPluginGameWrapper::executableForcedLoads() const -{ - return basicWrapperFunctionImplementation>(this, "executableForcedLoads"); -} - -QString IPluginGameWrapper::steamAPPId() const -{ - return basicWrapperFunctionImplementation(this, "steamAPPId"); -} - -QStringList IPluginGameWrapper::primaryPlugins() const -{ - return basicWrapperFunctionImplementation(this, "primaryPlugins"); -} - -QStringList IPluginGameWrapper::gameVariants() const -{ - return basicWrapperFunctionImplementation(this, "gameVariants"); -} - -void IPluginGameWrapper::setGameVariant(const QString & variant) -{ - basicWrapperFunctionImplementation(this, "setGameVariant", variant); -} - -QString IPluginGameWrapper::binaryName() const -{ - return basicWrapperFunctionImplementation(this, "binaryName"); -} - -QString IPluginGameWrapper::gameShortName() const -{ - return basicWrapperFunctionImplementation(this, "gameShortName"); -} - -QStringList IPluginGameWrapper::primarySources() const -{ - return basicWrapperFunctionImplementation(this, "primarySources"); -} - -QStringList IPluginGameWrapper::validShortNames() const -{ - return basicWrapperFunctionImplementation(this, "validShortNames"); -} - -QString IPluginGameWrapper::gameNexusName() const -{ - return basicWrapperFunctionImplementation(this, "gameNexusName"); -} - -QStringList IPluginGameWrapper::iniFiles() const -{ - return basicWrapperFunctionImplementation(this, "iniFiles"); -} - -QStringList IPluginGameWrapper::DLCPlugins() const -{ - return basicWrapperFunctionImplementation(this, "DLCPlugins"); -} - -QStringList IPluginGameWrapper::CCPlugins() const -{ - return basicWrapperFunctionImplementation(this, "CCPlugins"); -} - -IPluginGame::LoadOrderMechanism IPluginGameWrapper::loadOrderMechanism() const -{ - return basicWrapperFunctionImplementation(this, "loadOrderMechanism"); -} - -IPluginGame::SortMechanism IPluginGameWrapper::sortMechanism() const -{ - return basicWrapperFunctionImplementation(this, "sortMechanism"); -} - -int IPluginGameWrapper::nexusModOrganizerID() const -{ - return basicWrapperFunctionImplementation(this, "nexusModOrganizerID"); -} - -int IPluginGameWrapper::nexusGameID() const -{ - return basicWrapperFunctionImplementation(this, "nexusGameID"); -} - -bool IPluginGameWrapper::looksValid(QDir const & dir) const -{ - return basicWrapperFunctionImplementation(this, "looksValid", dir); -} - -QString IPluginGameWrapper::gameVersion() const -{ - return basicWrapperFunctionImplementation(this, "gameVersion"); -} - -QString IPluginGameWrapper::getLauncherName() const -{ - return basicWrapperFunctionImplementation(this, "getLauncherName"); -} - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_IMPL(IPluginGameWrapper, 0) - -std::map IPluginGameWrapper::featureList() const -{ - return basicWrapperFunctionImplementation>(this, "_featureList"); -} -/// end IPluginGame Wrapper -///////////////////////////////////// -/// IPluginInstaller macro - -#define COMMON_I_PLUGIN_INSTALLER_WRAPPER_DEFINITIONS(class_name) \ -unsigned int class_name::priority() const { return basicWrapperFunctionImplementation(this, "priority"); } \ -bool class_name::isManualInstaller() const { return basicWrapperFunctionImplementation(this, "isManualInstaller"); } \ -void class_name::onInstallationStart(QString const& archive, bool reinstallation, MOBase::IModInterface* currentMod) { \ - basicWrapperFunctionImplementationWithDefault(this, &class_name::onInstallationStart_Default, "onInstallationStart", archive, reinstallation, boost::python::ptr(currentMod)); } \ -void class_name::onInstallationEnd(EInstallResult result, MOBase::IModInterface* newMod) { \ - basicWrapperFunctionImplementationWithDefault(this, &class_name::onInstallationEnd_Default, "onInstallationEnd", result, boost::python::ptr(newMod)); } \ -bool class_name::isArchiveSupported(std::shared_ptr tree) const { return basicWrapperFunctionImplementation(this, "isArchiveSupported", tree); } - -/// end IPluginInstaller macro -///////////////////////////////////// -/// IPluginInstaller Wrapper - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginInstallerSimpleWrapper) -COMMON_I_PLUGIN_INSTALLER_WRAPPER_DEFINITIONS(IPluginInstallerSimpleWrapper) - -IPluginInstaller::EInstallResult IPluginInstallerSimpleWrapper::install( - GuessedValue& modName, std::shared_ptr& tree, - QString& version, int& nexusID) -{ - namespace bpy = boost::python; - - using return_type = std::variant< - IPluginInstaller::EInstallResult, - std::shared_ptr, - std::tuple, QString, int>>; - auto ret = basicWrapperFunctionImplementation(this, "install", boost::ref(modName), tree, version, nexusID); - - auto result = std::visit([&](auto const& t) { - using type = std::decay_t; - if constexpr (std::is_same_v) { - return t; - } - else if constexpr (std::is_same_v>) { - tree = t; - return IPluginInstaller::RESULT_SUCCESS; - } - else if constexpr (std::is_same_v, QString, int>>) { - tree = std::get<1>(t); - version = std::get<2>(t); - nexusID = std::get<3>(t); - return std::get<0>(t); - } - }, ret); - - tree = utils::clean_shared_ptr(tree); - return result; -} - -/// end IPluginInstallerSimple Wrapper -///////////////////////////////////// -/// IPluginInstallerCustom Wrapper -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginInstallerCustomWrapper) -COMMON_I_PLUGIN_INSTALLER_WRAPPER_DEFINITIONS(IPluginInstallerCustomWrapper) - -bool IPluginInstallerCustomWrapper::isArchiveSupported(const QString &archiveName) const -{ - return basicWrapperFunctionImplementation(this, "isArchiveSupported", archiveName); -} - -std::set IPluginInstallerCustomWrapper::supportedExtensions() const -{ - return basicWrapperFunctionImplementation>(this, "supportedExtensions"); -} - -IPluginInstaller::EInstallResult IPluginInstallerCustomWrapper::install( - GuessedValue &modName, QString gameName, const QString &archiveName, const QString &version, int modID) -{ - // Note: This requires far more less trouble than the "Simple" installer version since 1) there is no tree - // and 2) there version and modId cannot be modified: - return basicWrapperFunctionImplementation( - this, "install", boost::ref(modName), gameName, archiveName, version, modID); -} - -/// end IPluginInstallerCustom Wrapper -///////////////////////////// -/// IPluginModPage Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginModPageWrapper) - -QString IPluginModPageWrapper::displayName() const -{ - return basicWrapperFunctionImplementation(this, "displayName"); -} - -QIcon IPluginModPageWrapper::icon() const -{ - return basicWrapperFunctionImplementation(this, "icon"); -} - -QUrl IPluginModPageWrapper::pageURL() const -{ - return basicWrapperFunctionImplementation(this, "pageURL"); -} - -bool IPluginModPageWrapper::useIntegratedBrowser() const -{ - return basicWrapperFunctionImplementation(this, "useIntegratedBrowser"); -} - -bool IPluginModPageWrapper::handlesDownload(const QUrl & pageURL, const QUrl & downloadURL, MOBase::ModRepositoryFileInfo & fileInfo) const -{ - return basicWrapperFunctionImplementation(this, "handlesDownload", pageURL, downloadURL, fileInfo); -} - -void IPluginModPageWrapper::setParentWidget(QWidget * widget) -{ - basicWrapperFunctionImplementationWithDefault(this, &IPluginModPageWrapper::setParentWidget_Default, "setParentWidget", widget); -} -/// end IPluginModPage Wrapper -///////////////////////////// -/// IPluginPreview Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginPreviewWrapper) - -std::set IPluginPreviewWrapper::supportedExtensions() const -{ - return basicWrapperFunctionImplementation>(this, "supportedExtensions"); -} - -QWidget *IPluginPreviewWrapper::genFilePreview(const QString &fileName, const QSize &maxSize) const -{ - // We need responsibility for deleting the QWidget to be transferred to C++: - return wrapperFunctionImplementationWithApiTransfer(this, "genFilePreview", fileName, maxSize); -} -/// end IPluginPreview Wrapper -///////////////////////////// -/// IPluginTool Wrapper - - -COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginToolWrapper) - -QString IPluginToolWrapper::displayName() const -{ - return basicWrapperFunctionImplementation(this, "displayName"); -} - -QString IPluginToolWrapper::tooltip() const -{ - return basicWrapperFunctionImplementation(this, "tooltip"); -} - -QIcon IPluginToolWrapper::icon() const -{ - return basicWrapperFunctionImplementation(this, "icon"); -} - -void IPluginToolWrapper::setParentWidget(QWidget *parent) -{ - basicWrapperFunctionImplementationWithDefault(this, &IPluginToolWrapper::setParentWidget_Default, "setParentWidget", parent); -} - -void IPluginToolWrapper::display() const -{ - basicWrapperFunctionImplementation(this, "display"); -} - -/// end IPluginTool Wrapper diff --git a/src/runner/proxypluginwrappers.h b/src/runner/proxypluginwrappers.h deleted file mode 100644 index ccf18b7..0000000 --- a/src/runner/proxypluginwrappers.h +++ /dev/null @@ -1,268 +0,0 @@ -#ifndef PROXYPLUGINWRAPPERS_H -#define PROXYPLUGINWRAPPERS_H - - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef Q_MOC_RUN -#include -#include -#endif - -// The wrapper for IPluginGame cannot override requirements or enabledByDefault since they're final, -// so we need to be able to exclude the declarations. -#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_IMPL(include_requirements) \ -public: \ -virtual bool init(MOBase::IOrganizer *moInfo) override; \ -virtual QString name() const override; \ -virtual QString localizedName() const override; \ -virtual QString master() const override; \ -virtual QString author() const override; \ -virtual QString description() const override; \ -virtual MOBase::VersionInfo version() const override; \ -virtual QList settings() const override; \ -QString localizedName_Default() const; \ -QString master_Default() const; \ -BOOST_PP_EXPR_IF(include_requirements, \ - virtual std::vector> requirements() const override; \ - std::vector> requirements_Default() const; \ - virtual bool enabledByDefault() const override; \ - bool enabledByDefault_Default() const;) - -#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_IMPL(1) - -// Even though the base interface is not a QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. -// QObject must be the first base class because moc assumes the first base class is a QObject -class IPluginWrapper : public QObject, public MOBase::IPlugin, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -public: - static constexpr const char* className = "IPluginWrapper"; - using boost::python::wrapper::get_override; -}; - - -// Even though the base interface is not an IPlugin or QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. -// QObject must be the first base class because moc assumes the first base class is a QObject -class IPluginDiagnoseWrapper : public QObject, public MOBase::IPluginDiagnose, public MOBase::IPlugin, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose) - -public: - static constexpr const char* className = "IPluginDiagnoseWrapper"; - using boost::python::wrapper::get_override; - - // Bring in public scope: - using IPluginDiagnose::invalidate; - - virtual std::vector activeProblems() const override; - virtual QString shortDescription(unsigned int key) const override; - virtual QString fullDescription(unsigned int key) const override; - virtual bool hasGuidedFix(unsigned int key) const override; - virtual void startGuidedFix(unsigned int key) const override; - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -}; - - -// Even though the base interface is not an IPlugin or QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. -// QObject must be the first base class because moc assumes the first base class is a QObject -class IPluginFileMapperWrapper : public QObject, public MOBase::IPluginFileMapper, public MOBase::IPlugin, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginFileMapper) - -public: - static constexpr const char* className = "IPluginFileMapperWrapper"; - using boost::python::wrapper::get_override; - - virtual MappingType mappings() const override; - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -}; - - -class IPluginGameWrapper : public MOBase::IPluginGame, public boost::python::wrapper { - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame) - -public: - static constexpr const char* className = "IPluginGameWrapper"; - using boost::python::wrapper::get_override; - - virtual void detectGame() override; - virtual QString gameName() const override; - virtual void initializeProfile(const QDir &directory, ProfileSettings settings) const override; - virtual std::vector> listSaves(QDir folder) const override; - virtual bool isInstalled() const override; - virtual QIcon gameIcon() const override; - virtual QDir gameDirectory() const override; - virtual QDir dataDirectory() const override; - virtual void setGamePath(const QString &path) override; - virtual QDir documentsDirectory() const override; - virtual QDir savesDirectory() const override; - virtual QList executables() const override; - virtual QList executableForcedLoads() const override; - virtual QString steamAPPId() const override; - virtual QStringList primaryPlugins() const override; - virtual QStringList gameVariants() const override; - virtual void setGameVariant(const QString &variant) override; - virtual QString binaryName() const override; - virtual QString gameShortName() const override; - virtual QStringList primarySources() const override; - virtual QStringList validShortNames() const override; - virtual QString gameNexusName() const override; - virtual QStringList iniFiles() const override; - virtual QStringList DLCPlugins() const override; - virtual QStringList CCPlugins() const override; - virtual LoadOrderMechanism loadOrderMechanism() const override; - virtual SortMechanism sortMechanism() const override; - virtual int nexusModOrganizerID() const override; - virtual int nexusGameID() const override; - virtual bool looksValid(QDir const &dir) const override; - virtual QString gameVersion() const override; - virtual QString getLauncherName() const override; - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_IMPL(0) - -protected: - // Apparently, Python developers interpret an underscore in a function name as it being protected - virtual std::map featureList() const override; - - // Thankfully, the default implementation of the templated 'T *feature()' function should allow us to get away without overriding it. -}; - - -#define COMMON_I_PLUGIN_INSTALLER_WRAPPER_DECLARATIONS public: \ -using IPluginInstaller::parentWidget; \ -using IPluginInstaller::manager; \ -virtual unsigned int priority() const override; \ -virtual bool isManualInstaller() const override; \ -virtual void onInstallationStart(QString const& archive, bool reinstallation, MOBase::IModInterface* currentMod) override; \ -void onInstallationStart_Default(QString const& archive, bool reinstallation, MOBase::IModInterface* currentMod) { \ - return IPluginInstaller::onInstallationStart(archive, reinstallation, currentMod); } \ -virtual void onInstallationEnd(EInstallResult result, MOBase::IModInterface* newMod) override; \ -void onInstallationEnd_Default(EInstallResult result, MOBase::IModInterface* newMod) { \ - return IPluginInstaller::onInstallationEnd(result, newMod); } \ -virtual bool isArchiveSupported(std::shared_ptr tree) const override; - - -class IPluginInstallerSimpleWrapper : public MOBase::IPluginInstallerSimple, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS - COMMON_I_PLUGIN_INSTALLER_WRAPPER_DECLARATIONS - -public: - static constexpr const char* className = "IPluginInstallerSimpleWrapper"; - using boost::python::wrapper::get_override; - - virtual EInstallResult install(MOBase::GuessedValue& modName, std::shared_ptr& tree, - QString& version, int& nexusID) override; -}; - -class IPluginInstallerCustomWrapper : public MOBase::IPluginInstallerCustom, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerCustom) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS - - COMMON_I_PLUGIN_INSTALLER_WRAPPER_DECLARATIONS - -public: - static constexpr const char* className = "IPluginInstallerCustomWrapper"; - using boost::python::wrapper::get_override; - - virtual bool isArchiveSupported(const QString &archiveName) const override; - virtual std::set supportedExtensions() const override; - virtual EInstallResult install(MOBase::GuessedValue &modName, QString gameName, const QString &archiveName, - const QString &version, int modID) override; -}; - - -class IPluginModPageWrapper : public MOBase::IPluginModPage, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginModPage) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -public: - static constexpr const char* className = "IPluginModPageWrapper"; - using boost::python::wrapper::get_override; - - // Bring in public scope: - using IPluginModPage::parentWidget; - - virtual QString displayName() const override; - virtual QIcon icon() const override; - virtual QUrl pageURL() const override; - virtual bool useIntegratedBrowser() const override; - virtual bool handlesDownload(const QUrl &pageURL, const QUrl &downloadURL, MOBase::ModRepositoryFileInfo &fileInfo) const override; - virtual void setParentWidget(QWidget *widget) override; - - void setParentWidget_Default(QWidget* parent) { - IPluginModPage::setParentWidget(parent); - } -}; - - -class IPluginPreviewWrapper : public MOBase::IPluginPreview, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -public: - static constexpr const char* className = "IPluginPreviewWrapper"; - using boost::python::wrapper::get_override; - - virtual std::set supportedExtensions() const override; - virtual QWidget *genFilePreview(const QString &fileName, const QSize &maxSize) const override; -}; - - -class IPluginToolWrapper: public MOBase::IPluginTool, public boost::python::wrapper -{ - Q_OBJECT - Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool) - - COMMON_I_PLUGIN_WRAPPER_DECLARATIONS -public: - static constexpr const char* className = "IPluginToolWrapper"; - using boost::python::wrapper::get_override; - - // Bring in public scope: - using IPluginTool::parentWidget; - - virtual QString displayName() const override; - virtual QString tooltip() const override; - virtual QIcon icon() const override; - virtual void setParentWidget(QWidget *parent) override; - - void setParentWidget_Default(QWidget* parent) { - IPluginTool::setParentWidget(parent); - } - -public Q_SLOTS: - virtual void display() const override; -}; - - - - -#endif // PROXYPLUGINWRAPPERS_H diff --git a/src/runner/pylogger.cpp b/src/runner/pylogger.cpp deleted file mode 100644 index 29922c9..0000000 --- a/src/runner/pylogger.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "pylogger.h" - -#include "log.h" - -#include - -namespace bpy = boost::python; - -// Small structure to hold the levels - There are copy paste from -// my Python version and I assume these will not change soon: -struct PyLogLevel { - static constexpr int CRITICAL = 50; - static constexpr int ERROR = 40; - static constexpr int WARNING = 30; - static constexpr int INFO = 20; - static constexpr int DEBUG = 10; -}; - -// This is the function we are going to use as our Handler .emit -// method. -void emit_function(bpy::object self, bpy::object record) { - - // There are other parameters that could be used, but this is minimal for - // now (filename, line number, etc.). - const int level = bpy::extract(record.attr("levelno")); - const std::wstring msg = bpy::extract(bpy::str(record.attr("msg"))); - - switch (level) { - case PyLogLevel::CRITICAL: - case PyLogLevel::ERROR: - MOBase::log::error("{}", msg); - break; - case PyLogLevel::WARNING: - MOBase::log::warn("{}", msg); - break; - case PyLogLevel::INFO: - MOBase::log::info("{}", msg); - break; - case PyLogLevel::DEBUG: - default: // There is a "NOTSET" level in theory: - MOBase::log::debug("{}", msg); - break; - } -}; - -void configure_python_logging(bpy::object mobase) -{ - // Most of this is dealing with actual Python objects since it is not possible - // to derive from logging.Handler in C++ using Boost.Python, and since a lot of - // this would require extra register only for this. - - // Retrieve the logging module and the Handler class. - auto logging = bpy::import("logging"); - auto Handler = logging.attr("Handler"); - - // This is ugly but that's how it's done in C Python. - auto type = (PyObject*)&PyType_Type; - - // Create the "MO2Handler" python class: - auto methods = bpy::dict(); - methods["emit"] = bpy::make_function(emit_function); - auto MO2Handler = bpy::call(type, "LogHandler", bpy::make_tuple(Handler), methods); - - // Create the default logger: - auto handler = MO2Handler(); - handler.attr("setLevel")(PyLogLevel::DEBUG); - auto logger = logging.attr("getLogger")(bpy::object(mobase.attr("__name__"))); - logger.attr("setLevel")(PyLogLevel::DEBUG); - logger.attr("addHandler")(handler); - - // Set mobase attributes: - mobase.attr("LogHandler") = MO2Handler; - mobase.attr("logger") = logger; -} \ No newline at end of file diff --git a/src/runner/pylogger.h b/src/runner/pylogger.h deleted file mode 100644 index df6cd8f..0000000 --- a/src/runner/pylogger.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef MO2_PYTHON_LOGGER_H -#define MO2_PYTHON_LOGGER_H - -#include - -/** - * @brief Configure logging for MO2 python plugin. - * - * @param mobase The mobase module. - */ -void configure_python_logging(boost::python::object mobase); - -#endif \ No newline at end of file diff --git a/src/runner/pythonrunner.cpp b/src/runner/pythonrunner.cpp index 2d00f3d..3d7a2ff 100644 --- a/src/runner/pythonrunner.cpp +++ b/src/runner/pythonrunner.cpp @@ -1,1567 +1,300 @@ #include "pythonrunner.h" -#pragma warning( disable : 4100 ) -#pragma warning( disable : 4996 ) - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "uibasewrappers.h" -#include "proxypluginwrappers.h" -#include "gamefeatureswrappers.h" -#include "sipApiAccess.h" +#pragma warning(disable : 4100) +#pragma warning(disable : 4996) #include -#include + +#include #include #include -#include -#include -#include -#include +#include "pybind11_qt/pybind11_qt.h" +#include +#include -#ifndef Q_MOC_RUN -#include -#include -#endif +#include +#include -#include -#include - -#include "tuple_helper.h" -#include "variant_helper.h" -#include "converters.h" -#include "shared_ptr_converter.h" -#include "pylogger.h" -#include "widgets.h" +#include "error.h" +#include "pythonutils.h" using namespace MOBase; - -namespace bpy = boost::python; -namespace mp11 = boost::mp11; - -/** - * This macro should be used within a bpy::class_ declaration and will define two - * methods: __getattr__ and Name, where Name will simply return the object as a QClass* - * object, while __getattr__ will delegate to the underlying QClass object when required. - * - * This allow access to Qt interface for object exposed using boost::python (e.g., signals, - * methods from QObject or QWidget, etc.). - */ -#define Q_DELEGATE(Class, QClass, Name) \ - .def(Name, +[](Class* w) -> QClass* { return w; }, bpy::return_value_policy()) \ - .def("__getattr__", +[](Class* w, bpy::str str) -> bpy::object { \ - return bpy::object{ (QClass*)w }.attr(str); \ - }) - -BOOST_PYTHON_MODULE(mobase) -{ - PyEval_InitThreads(); - - bpy::import("PyQt6.QtCore"); - bpy::import("PyQt6.QtWidgets"); - - utils::register_qstring_converter(); - utils::register_qvariant_converter(); - - utils::register_qclass_converter(); - utils::register_qclass_converter(); - utils::register_qclass_converter(); - utils::register_qclass_converter(); - utils::register_qclass_converter(); - utils::register_qclass_converter(); - utils::register_qclass_converter(); - utils::register_qclass_converter(); - utils::register_qclass_converter(); - - // QFlags: - utils::register_qflags_converter(); - utils::register_qflags_converter(); - utils::register_qflags_converter(); - - // Enums: - utils::register_enum_converter(); - utils::register_enum_converter(); - - // Pointers: - bpy::register_ptr_to_python>(); - bpy::register_ptr_to_python>(); - bpy::implicitly_convertible, std::shared_ptr>(); - bpy::register_ptr_to_python>(); - bpy::register_ptr_to_python>(); - bpy::implicitly_convertible, std::shared_ptr>(); - - utils::shared_ptr_from_python>(); - bpy::register_ptr_to_python>(); - - utils::shared_ptr_from_python>(); - bpy::register_ptr_to_python>(); - - // Containers: - utils::register_sequence_container>(); - utils::register_sequence_container>(); - utils::register_sequence_container>(); - utils::register_sequence_container>(); - utils::register_sequence_container>(); - utils::register_sequence_container>(); - utils::register_sequence_container(); - utils::register_sequence_container>(); - utils::register_sequence_container>(); - utils::register_sequence_container>(); // Required for QVariant since this is QVariantList. - utils::register_sequence_container>>(); - utils::register_sequence_container>>(); - utils::register_sequence_container>>(); - utils::register_sequence_container>(); - utils::register_sequence_container>(); - utils::register_sequence_container>(); - - utils::register_set_container>(); - - utils::register_associative_container>(); // Required for QVariant since this is QVariantMap. - utils::register_associative_container>(); - utils::register_associative_container>(); - utils::register_associative_container>(); - - utils::register_associative_container(); - - utils::register_optional>(); - - // Tuple: - bpy::register_tuple>(); // IOrganizer::waitForApplication - bpy::register_tuple>(); // IProfile::invalidationActive - bpy::register_tuple>(); - bpy::register_tuple, QString, int>>(); - bpy::register_tuple>(); - - // Variants: - bpy::register_variant, - std::tuple, QString, int>>>(); - bpy::register_variant>(); - bpy::register_variant>(); - bpy::register_variant>>(); - bpy::register_variant>>(); - - // Functions: - utils::register_functor_converter(); // converter for the onRefreshed-callback - utils::register_functor_converter(); - utils::register_functor_converter(); - utils::register_functor_converter(); - utils::register_functor_converter(); // converter for the onModMoved-callback and onPluginMoved callbacks - utils::register_functor_converter&)>(); // converter for the onModStateChanged-callback (IModList) - utils::register_functor_converter&)>(); // converter for the onPluginStateChanged-callback (IPluginList) - utils::register_functor_converter(); - utils::register_functor_converter(); - utils::register_functor_converter>(); - utils::register_functor_converter>(); - utils::register_functor_converter>(); - utils::register_functor_converter(); - utils::register_functor_converter)>(); - utils::register_functor_converter>(); - utils::register_functor_converter const&)>(); - utils::register_functor_converter(QString const&)>(); - utils::register_functor_converter>(); - utils::register_functor_converter>(); - utils::register_functor_converter>(); - - // This one is kept for backward-compatibility while we deprecate onModStateChanged for singl mod. - utils::register_functor_converter(); // converter for the onModStateChanged-callback (IModList). - utils::register_functor_converter(); // converter for the onPluginStateChanged-callback (IPluginList). - - // - // Class declarations: - // - - bpy::enum_("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) - ; - - bpy::enum_("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) - ; - - bpy::class_("VersionInfo") - .def(bpy::init( - (bpy::arg("value"), bpy::arg("scheme") = VersionInfo::SCHEME_DISCOVER))) - // Note: Order of the two init<> below is important because ReleaseType is a simple enum with an - // implicit int conversion. - .def(bpy::init( - (bpy::arg("major"), "minor", "subminor", "subsubminor", bpy::arg("release_type") = VersionInfo::RELEASE_FINAL))) - .def(bpy::init( - (bpy::arg("major"), "minor", "subminor", bpy::arg("release_type") = VersionInfo::RELEASE_FINAL))) - .def("clear", &VersionInfo::clear) - .def("parse", &VersionInfo::parse, - (bpy::arg("value"), bpy::arg("scheme") = VersionInfo::SCHEME_DISCOVER, bpy::arg("is_manual") = false)) - .def("canonicalString", &VersionInfo::canonicalString) - .def("displayString", &VersionInfo::displayString, bpy::arg("forced_segments") = 2) - .def("isValid", &VersionInfo::isValid) - .def("scheme", &VersionInfo::scheme) - .def("__str__", &VersionInfo::canonicalString) - .def(bpy::self < bpy::self) - .def(bpy::self > bpy::self) - .def(bpy::self <= bpy::self) - .def(bpy::self >= bpy::self) - .def(bpy::self != bpy::self) - .def(bpy::self == bpy::self) - ; - - bpy::class_( - "PluginSetting", bpy::init( - (bpy::arg("key"), "description", "default_value"))) - .def_readwrite("key", &PluginSetting::key) - .def_readwrite("description", &PluginSetting::description) - .def_readwrite("default_value", &PluginSetting::defaultValue); - - bpy::class_("ExecutableInfo", - bpy::init((bpy::arg("title"), "binary"))) - .def("withArgument", &ExecutableInfo::withArgument, bpy::return_self<>(), bpy::arg("argument")) - .def("withWorkingDirectory", &ExecutableInfo::withWorkingDirectory, bpy::return_self<>(), bpy::arg("directory")) - .def("withSteamAppId", &ExecutableInfo::withSteamAppId, bpy::return_self<>(), bpy::arg("app_id")) - .def("asCustom", &ExecutableInfo::asCustom, bpy::return_self<>()) - .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) - ; - - bpy::class_("ExecutableForcedLoadSetting", - bpy::init((bpy::arg("process"), "library"))) - .def("withForced", &ExecutableForcedLoadSetting::withForced, bpy::return_self<>(), bpy::arg("forced")) - .def("withEnabled", &ExecutableForcedLoadSetting::withEnabled, bpy::return_self<>(), bpy::arg("enabled")) - .def("enabled", &ExecutableForcedLoadSetting::enabled) - .def("forced", &ExecutableForcedLoadSetting::forced) - .def("library", &ExecutableForcedLoadSetting::library) - .def("process", &ExecutableForcedLoadSetting::process) - ; - - bpy::class_, boost::noncopyable>("ISaveGame") - .def("getFilepath", bpy::pure_virtual(&ISaveGame::getFilepath)) - .def("getCreationTime", bpy::pure_virtual(&ISaveGame::getCreationTime)) - .def("getName", bpy::pure_virtual(&ISaveGame::getName)) - .def("getSaveGroupIdentifier", bpy::pure_virtual(&ISaveGame::getSaveGroupIdentifier)) - .def("allFiles", bpy::pure_virtual(&ISaveGame::allFiles)) - ; - - // See Q_DELEGATE for more details. - bpy::class_, ISaveGameInfoWidgetWrapper*, boost::noncopyable>( - "ISaveGameInfoWidget", bpy::init>(bpy::arg("parent"))) - .def("setSave", bpy::pure_virtual(&ISaveGameInfoWidget::setSave), bpy::arg("save")) - - Q_DELEGATE(ISaveGameInfoWidget, QWidget, "_widget") - ; - - // Plugin requirements: - auto iPluginRequirementClass = bpy::class_< - IPluginRequirementWrapper, bpy::bases<>, boost::noncopyable>("IPluginRequirement"); - { - bpy::scope scope = iPluginRequirementClass; - - bpy::class_("Problem", - bpy::init((bpy::arg("short_description"), bpy::arg("long_description") = ""))) - .def("shortDescription", &IPluginRequirement::Problem::shortDescription) - .def("longDescription", &IPluginRequirement::Problem::longDescription); - - iPluginRequirementClass - .def("check", bpy::pure_virtual(&IPluginRequirement::check), bpy::arg("organizer")) - ; - } - - bpy::class_("PluginRequirementFactory") - // pluginDependency - .def("pluginDependency", +[](QStringList const& pluginNames) { - return PluginRequirementFactory::pluginDependency(pluginNames); - }, bpy::arg("plugins")) - .def("pluginDependency", +[](QString const& pluginName) { - return PluginRequirementFactory::pluginDependency(pluginName); - }, bpy::arg("plugin")) - .staticmethod("pluginDependency") - // gameDependency - .def("gameDependency", +[](QStringList const& gameNames) { - return PluginRequirementFactory::gameDependency(gameNames); - }, bpy::arg("games")) - .def("gameDependency", +[](QString const& gameNames) { - return PluginRequirementFactory::gameDependency(gameNames); - }, bpy::arg("game")) - .staticmethod("gameDependency") - // diagnose - .def("diagnose", &PluginRequirementFactory::diagnose, bpy::arg("diagnose")) - .staticmethod("diagnose") - // basic - .def("basic", &PluginRequirementFactory::basic, (bpy::arg("checker"), "description")) - .staticmethod("basic"); - - bpy::class_("FileInfo", bpy::init<>()) - .add_property("filePath", - +[](const IOrganizer::FileInfo& info) { return info.filePath; }, - +[](IOrganizer::FileInfo& info, QString value) { info.filePath = value; }) - .add_property("archive", - +[](const IOrganizer::FileInfo& info) { return info.archive; }, - +[](IOrganizer::FileInfo& info, QString value) { info.archive = value; }) - .add_property("origins", - +[](const IOrganizer::FileInfo& info) { return info.origins; }, - +[](IOrganizer::FileInfo& info, QStringList value) { info.origins = value; }) - ; - - bpy::class_("IOrganizer", bpy::no_init) - .def("createNexusBridge", &IOrganizer::createNexusBridge, bpy::return_value_policy()) - .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::appVersion) - .def("createMod", &IOrganizer::createMod, bpy::return_value_policy(), bpy::arg("name")) - .def("getGame", &IOrganizer::getGame, bpy::return_value_policy(), bpy::arg("name")) - .def("modDataChanged", &IOrganizer::modDataChanged, bpy::arg("mod")) - .def("isPluginEnabled", +[](IOrganizer* o, IPlugin* plugin) { return o->isPluginEnabled(plugin); }, bpy::arg("plugin")) - .def("isPluginEnabled", +[](IOrganizer* o, QString const& plugin) { return o->isPluginEnabled(plugin); }, bpy::arg("plugin")) - .def("pluginSetting", &IOrganizer::pluginSetting, (bpy::arg("plugin_name"), "key")) - .def("setPluginSetting", &IOrganizer::setPluginSetting, (bpy::arg("plugin_name"), "key", "value")) - .def("persistent", &IOrganizer::persistent, (bpy::arg("plugin_name"), "key", bpy::arg("default") = QVariant())) - .def("setPersistent", &IOrganizer::setPersistent, (bpy::arg("plugin_name"), "key", "value", bpy::arg("sync") = true)) - .def("pluginDataPath", &IOrganizer::pluginDataPath) - .def("installMod", &IOrganizer::installMod, bpy::return_value_policy(), (bpy::arg("filename"), bpy::arg("name_suggestion") = "")) - .def("resolvePath", &IOrganizer::resolvePath, bpy::arg("filename")) - .def("listDirectories", &IOrganizer::listDirectories, bpy::arg("directory")) - - // Provide multiple overloads of findFiles: - .def("findFiles", +[](const IOrganizer* o, QString const& p, std::function f) { return o->findFiles(p, f); }, - (bpy::arg("path"), "filter")) - - // In C++, it is possible to create a QStringList implicitly from a single QString. This 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, QString const& p, const QStringList& gf) { return o->findFiles(p, gf); }, - (bpy::arg("path"), "patterns")) - .def("findFiles", +[](const IOrganizer* o, QString const& p, const QString& f) { return o->findFiles(p, QStringList{ f }); }, - (bpy::arg("path"), "pattern")) - - .def("getFileOrigins", &IOrganizer::getFileOrigins, bpy::arg("filename")) - .def("findFileInfos", &IOrganizer::findFileInfos, (bpy::arg("path"), "filter")) - - .def("virtualFileTree", &IOrganizer::virtualFileTree) - - .def("downloadManager", &IOrganizer::downloadManager, bpy::return_value_policy()) - .def("pluginList", &IOrganizer::pluginList, bpy::return_value_policy()) - .def("modList", &IOrganizer::modList, bpy::return_value_policy()) - .def("profile", &IOrganizer::profile, bpy::return_value_policy()) - - // 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 QString& executable, const QStringList& args, const QString& cwd, const QString& profile, - const QString& forcedCustomOverwrite, bool ignoreCustomOverwrite) { - return (std::uintptr_t) o->startApplication(executable, args, cwd, profile, forcedCustomOverwrite, ignoreCustomOverwrite); - }, (bpy::arg("executable"), (bpy::arg("args") = QStringList()), (bpy::arg("cwd") = ""), (bpy::arg("profile") = ""), - (bpy::arg("forcedCustomOverwrite") = ""), (bpy::arg("ignoreCustomOverwrite") = false)), bpy::return_value_policy()) - .def("waitForApplication", +[](IOrganizer *o, std::uintptr_t handle, bool refresh) { - DWORD returnCode; - bool result = o->waitForApplication((HANDLE)handle, refresh, &returnCode); - return std::make_tuple(result, returnCode); - }, (bpy::arg("handle"), bpy::arg("refresh") = true)) - .def("refresh", &IOrganizer::refresh, (bpy::arg("save_changes") = true)) - .def("managedGame", &IOrganizer::managedGame, bpy::return_value_policy()) - - .def("onAboutToRun", &IOrganizer::onAboutToRun, bpy::arg("callback")) - .def("onFinishedRun", &IOrganizer::onFinishedRun, bpy::arg("callback")) - .def("onUserInterfaceInitialized", &IOrganizer::onUserInterfaceInitialized, bpy::arg("callback")) - .def("onProfileCreated", &IOrganizer::onProfileCreated, bpy::arg("callback")) - .def("onProfileRenamed", &IOrganizer::onProfileRenamed, bpy::arg("callback")) - .def("onProfileRemoved", &IOrganizer::onProfileRemoved, bpy::arg("callback")) - .def("onProfileChanged", &IOrganizer::onProfileChanged, bpy::arg("callback")) - - .def("onPluginSettingChanged", &IOrganizer::onPluginSettingChanged, bpy::arg("callback")) - .def("onPluginEnabled", +[](IOrganizer* o, std::function const& func) { - o->onPluginEnabled(func); - }, bpy::arg("callback")) - .def("onPluginEnabled", +[](IOrganizer* o, QString const& name, std::function const& func) { - o->onPluginEnabled(name, func); - }, (bpy::arg("name"), bpy::arg("callback"))) - .def("onPluginDisabled", +[](IOrganizer* o, std::function const& func) { - o->onPluginDisabled(func); - }, bpy::arg("callback")) - .def("onPluginDisabled", +[](IOrganizer* o, QString const& name, std::function const& func) { - o->onPluginDisabled(name, func); - }, (bpy::arg("name"), bpy::arg("callback"))) - - // DEPRECATED: - .def("getMod", +[](IOrganizer* o, QString const& name) { - utils::show_deprecation_warning("getMod", - "IOrganizer::getMod(str) is deprecated, use IModList::getMod(str) instead."); - return o->modList()->getMod(name); - }, bpy::return_value_policy(), bpy::arg("name")) - .def("removeMod", +[](IOrganizer* o, IModInterface *mod) { - utils::show_deprecation_warning("removeMod", - "IOrganizer::removeMod(IModInterface) is deprecated, use IModList::removeMod(IModInterface) instead."); - return o->modList()->removeMod(mod); - }, bpy::arg("mod")) - .def("modsSortedByProfilePriority", +[](IOrganizer* o) { - utils::show_deprecation_warning("modsSortedByProfilePriority", - "IOrganizer::modsSortedByProfilePriority() is deprecated, use IModList::allModsByProfilePriority() instead."); - return o->modList()->allModsByProfilePriority(); - }) - .def("refreshModList", +[](IOrganizer* o, bool s) { - utils::show_deprecation_warning("refreshModList", - "IOrganizer::refreshModList(bool) is deprecated, use IOrganizer::refresh(bool) instead."); - o->refresh(s); - }, (bpy::arg("save_changes") = true)) - .def("onModInstalled", +[](IOrganizer* organizer, const std::function& func) { - utils::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()); });; - }, bpy::arg("callback")) - - .def("getPluginDataPath", &IOrganizer::getPluginDataPath) - .staticmethod("getPluginDataPath") - - ; - - // FileTreeEntry Scope: - auto fileTreeEntryClass = bpy::class_("FileTreeEntry", bpy::no_init); - { - - bpy::scope scope = fileTreeEntryClass; - - bpy::enum_("FileTypes") - .value("FILE_OR_DIRECTORY", FileTreeEntry::FILE_OR_DIRECTORY) - .value("FILE", FileTreeEntry::FILE) - .value("DIRECTORY", FileTreeEntry::DIRECTORY) - .export_values() - ; - - fileTreeEntryClass - - .def("isFile", &FileTreeEntry::isFile) - .def("isDir", &FileTreeEntry::isDir) - // Forcing the conversion to FileTypeS to avoid having to expose FileType in python: - .def("fileType", +[](FileTreeEntry* p) { return FileTreeEntry::FileTypes{ p->fileType() }; }) - // This should probably not be exposed in python since we provide automatic downcast: - // .def("getTree", static_cast(FileTreeEntry::*)()>(&FileTreeEntry::astree)) - .def("name", &FileTreeEntry::name) - .def("suffix", &FileTreeEntry::suffix) - .def("hasSuffix", +[](FileTreeEntry* entry, QStringList suffixes) { return entry->hasSuffix(suffixes); }, bpy::arg("suffixes")) - .def("hasSuffix", +[](FileTreeEntry* entry, QString suffix) { return entry->hasSuffix(suffix); }, bpy::arg("suffix")) - .def("parent", static_cast(FileTreeEntry::*)()>(&FileTreeEntry::parent), "[optional]") - .def("path", &FileTreeEntry::path, bpy::arg("sep") = "\\") - .def("pathFrom", &FileTreeEntry::pathFrom, (bpy::arg("tree"), bpy::arg("sep") = "\\")) - - // Mutable operation: - .def("detach", &FileTreeEntry::detach) - .def("moveTo", &FileTreeEntry::moveTo, bpy::arg("tree")) - - // Special methods: - .def("__eq__", +[](const FileTreeEntry* entry, QString other) { - return entry->compare(other) == 0; - }) - .def("__eq__", +[](const FileTreeEntry* entry, std::shared_ptr other) { - return entry == other.get(); - }) - - // Special methods for debug: - .def("__repr__", +[](const FileTreeEntry* entry) { return "FileTreeEntry(\"" + entry->name() + "\")"; }) - ; - } - - // IFileTree scope: - auto iFileTreeClass = bpy::class_, boost::noncopyable>("IFileTree", bpy::no_init); - { - - bpy::scope scope = iFileTreeClass; - - bpy::enum_("InsertPolicy") - .value("FAIL_IF_EXISTS", IFileTree::InsertPolicy::FAIL_IF_EXISTS) - .value("REPLACE", IFileTree::InsertPolicy::REPLACE) - .value("MERGE", IFileTree::InsertPolicy::MERGE) - .export_values() - ; - - bpy::enum_("WalkReturn") - .value("CONTINUE", IFileTree::WalkReturn::CONTINUE) - .value("STOP", IFileTree::WalkReturn::STOP) - .value("SKIP", IFileTree::WalkReturn::SKIP) - .export_values() - ; - - iFileTreeClass - - // Non-mutable operations: - .def("exists", static_cast(&IFileTree::exists), - (bpy::arg("path"), bpy::arg("type") = IFileTree::FILE_OR_DIRECTORY)) - .def("find", static_cast(IFileTree::*)(QString, IFileTree::FileTypes)>(&IFileTree::find), - bpy::return_value_policy>(), (bpy::arg("path"), bpy::arg("type") = IFileTree::FILE_OR_DIRECTORY), "[optional]") - .def("pathTo", &IFileTree::pathTo, (bpy::arg("entry"), bpy::arg("sep") = "\\")) - - // Note: walk() would probably be better as a generator in python, but it is likely impossible to construct - // from the C++ walk() method. - .def("walk", &IFileTree::walk, (bpy::arg("callback"), bpy::arg("sep") = "\\")) - - // Kind-of-static operations: - .def("createOrphanTree", &IFileTree::createOrphanTree, bpy::arg("name") = "") - - // addFile() and addDirectory throws exception instead of returning null pointer in order - // to have better traces. - .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; - }, (bpy::arg("path"), bpy::arg("replace_if_exists") = false)) - .def("addDirectory", +[](IFileTree* w, QString path) { - auto result = w->addDirectory(path); - if (result == nullptr) { - throw std::logic_error("addDirectory failed"); - } - return result; - }, bpy::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. - .def("merge", +[](IFileTree* p, std::shared_ptr other, bool returnOverwrites) -> std::variant { - 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 }; - }, (bpy::arg("other"), bpy::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++. - .def("insert", +[](IFileTree* p, std::shared_ptr entry, IFileTree::InsertPolicy insertPolicy) { - return p->insert(entry, insertPolicy) == p->end(); - }, (bpy::arg("entry"), bpy::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS)) - - .def("remove", +[](IFileTree* p, QString name) { return p->erase(name).first != p->end(); }, bpy::arg("name")) - .def("remove", +[](IFileTree* p, std::shared_ptr entry) { return p->erase(entry) != p->end(); }, bpy::arg("entry")) - - .def("move", &IFileTree::move, (bpy::arg("entry"), "path", bpy::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS)) - .def("copy", +[](IFileTree* w, std::shared_ptr entry, QString path, IFileTree::InsertPolicy insertPolicy) { - auto result = w->copy(entry, path, insertPolicy); - if (result == nullptr) { - throw std::logic_error("copy failed"); - } - return result; - }, (bpy::arg("entry"), bpy::arg("path") = "", bpy::arg("insert_policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS)) - - .def("clear", &IFileTree::clear) - .def("removeAll", &IFileTree::removeAll, bpy::arg("names")) - .def("removeIf", &IFileTree::removeIf, bpy::arg("filter")) - - // Special methods: - .def("__getitem__", static_cast(IFileTree::*)(std::size_t)>(&IFileTree::at), - bpy::return_value_policy>()) - .def("__iter__", bpy::range>>( - static_cast(&IFileTree::begin), - static_cast(&IFileTree::end))) - .def("__len__", &IFileTree::size) - .def("__bool__", +[](const IFileTree* tree) { return !tree->empty(); }) - .def("__repr__", +[](const IFileTree* entry) { return "IFileTree(\"" + entry->name() + "\")"; }) - ; - } - - - bpy::class_("IProfile", bpy::no_init) - .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, bpy::arg("inifile")) - ; - - bpy::class_("IModRepositoryBridge", bpy::no_init) - .def("requestDescription", &IModRepositoryBridge::requestDescription, (bpy::arg("game_name"), "mod_id", "user_data")) - .def("requestFiles", &IModRepositoryBridge::requestFiles, (bpy::arg("game_name"), "mod_id", "user_data")) - .def("requestFileInfo", &IModRepositoryBridge::requestFileInfo, (bpy::arg("game_name"), "mod_id", "file_id", "user_data")) - .def("requestDownloadURL", &IModRepositoryBridge::requestDownloadURL, (bpy::arg("game_name"), "mod_id", "file_id", "user_data")) - .def("requestToggleEndorsement", &IModRepositoryBridge::requestToggleEndorsement, (bpy::arg("game_name"), "mod_id", "mod_version", "endorse", "user_data")) - - Q_DELEGATE(IModRepositoryBridge, QObject, "_object") - ; - - bpy::class_("ModRepositoryFileInfo", bpy::no_init) - .def(bpy::init(bpy::arg("other"))) - .def(bpy::init>((bpy::arg("game_name"), "mod_id", "file_id"))) - .def("__str__", &ModRepositoryFileInfo::toString) - .def("createFromJson", &ModRepositoryFileInfo::createFromJson, bpy::arg("data")).staticmethod("createFromJson") - .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) - ; - - bpy::class_("IDownloadManager", bpy::no_init) - .def("startDownloadURLs", &IDownloadManager::startDownloadURLs, bpy::arg("urls")) - .def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile, (bpy::arg("mod_id"), "file_id")) - .def("downloadPath", &IDownloadManager::downloadPath, bpy::arg("id")) - .def("onDownloadComplete", &IDownloadManager::onDownloadComplete, bpy::arg("callback")) - .def("onDownloadPaused", &IDownloadManager::onDownloadPaused, bpy::arg("callback")) - .def("onDownloadFailed", &IDownloadManager::onDownloadFailed, bpy::arg("callback")) - .def("onDownloadRemoved", &IDownloadManager::onDownloadRemoved, bpy::arg("callback")) - ; - - bpy::class_("IInstallationManager", bpy::no_init) - .def("getSupportedExtensions", &IInstallationManager::getSupportedExtensions) - .def("extractFile", &IInstallationManager::extractFile, (bpy::arg("entry"), bpy::arg("silent") = false)) - .def("extractFiles", &IInstallationManager::extractFiles, (bpy::arg("entries"), bpy::arg("silent") = false)) - .def("createFile", +[](IInstallationManager* m, std::shared_ptr entry) { - return m->createFile(utils::clean_shared_ptr(entry)); - }, bpy::arg("entry")) - - // accept both QString and GuessedValue since the conversion is not automatic in Python, and - // return a tuple to get back the mod name and the mod ID - .def("installArchive", +[](IInstallationManager* m, std::variant> modName, QString archive, int modId) { - GuessedValue tmp; - if (auto* p = std::get_if(&modName)) { - tmp = *p; - } - else { - tmp = std::get>(modName); - } - auto result = m->installArchive(tmp, archive, modId); - return std::make_tuple(result, static_cast(tmp), modId); - }, (bpy::arg("mod_name"), "archive", bpy::arg("mod_id") = 0)) - ; - - bpy::enum_("EndorsedState") - .value("ENDORSED_FALSE", EndorsedState::ENDORSED_FALSE) - .value("ENDORSED_TRUE", EndorsedState::ENDORSED_TRUE) - .value("ENDORSED_UNKNOWN", EndorsedState::ENDORSED_UNKNOWN) - .value("ENDORSED_NEVER", EndorsedState::ENDORSED_NEVER) - ; - - bpy::enum_("TrackedState") - .value("TRACKED_FALSE", TrackedState::TRACKED_FALSE) - .value("TRACKED_TRUE", TrackedState::TRACKED_TRUE) - .value("TRACKED_UNKNOWN", TrackedState::TRACKED_UNKNOWN) - ; - - bpy::class_("IModInterface", bpy::no_init) - .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("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, bpy::arg("version")) - .def("setNewestVersion", &IModInterface::setNewestVersion, bpy::arg("version")) - .def("setIsEndorsed", &IModInterface::setIsEndorsed, bpy::arg("endorsed")) - .def("setNexusID", &IModInterface::setNexusID, bpy::arg("nexus_id")) - .def("addNexusCategory", &IModInterface::addNexusCategory, bpy::arg("category_id")) - .def("addCategory", &IModInterface::addCategory, bpy::arg("name")) - .def("removeCategory", &IModInterface::removeCategory, bpy::arg("name")) - .def("setGameName", &IModInterface::setGameName, bpy::arg("name")) - .def("setUrl", &IModInterface::setUrl, bpy::arg("url")) - .def("pluginSetting", &IModInterface::pluginSetting, (bpy::arg("plugin_name"), "key", bpy::arg("default") = QVariant())) - .def("pluginSettings", &IModInterface::pluginSettings, bpy::arg("plugin_name")) - .def("setPluginSetting", &IModInterface::setPluginSetting, (bpy::arg("plugin_name"), "key", bpy::arg("value"))) - .def("clearPluginSettings", &IModInterface::clearPluginSettings, bpy::arg("plugin_name")) - - ; - - bpy::enum_("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) - ; - - bpy::class_, boost::noncopyable>("GuessedString") - .def(bpy::init<>()) - .def(bpy::init((bpy::arg("value"), bpy::arg("quality") = EGuessQuality::GUESS_USER))) - .def("update", - static_cast& (GuessedValue::*)(const QString&)>(&GuessedValue::update), - bpy::return_self<>(), bpy::arg("value")) - .def("update", - static_cast& (GuessedValue::*)(const QString&, EGuessQuality)>(&GuessedValue::update), - bpy::return_self<>(), (bpy::arg("value"), "quality")) - - // Methods to simulate the assignment operator: - .def("reset", +[](GuessedValue* gv) { - *gv = GuessedValue(); }, bpy::return_self<>()) - .def("reset", +[](GuessedValue* gv, const QString& value, EGuessQuality eq) { - *gv = GuessedValue(value, eq); }, bpy::return_self<>(), (bpy::arg("value"), "quality")) - .def("reset", +[](GuessedValue* gv, const GuessedValue& other) { - *gv = other; }, bpy::return_self<>(), bpy::arg("other")) - - // Use an intermediate lambda to avoid having to register the std::function conversion: - .def("setFilter", +[](GuessedValue* gv, std::function(QString const&)> fn) { - gv->setFilter([fn](QString& s) { - auto ret = fn(s); - return std::visit([&s](auto v) { - if constexpr (std::is_same_v) { - s = v; - return true; - } - else if constexpr (std::is_same_v) { - return v; - } - }, ret); - }); - }, bpy::arg("filter")) - - // This makes a copy in python but it more practical than exposing an iterator: - .def("variants", &GuessedValue::variants, bpy::return_value_policy()) - .def("__str__", &MOBase::GuessedValue::operator const QString&, bpy::return_value_policy()) - ; - - bpy::enum_("PluginState") - .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) - ; - - bpy::class_("IPluginList", bpy::no_init) - .def("state", &MOBase::IPluginList::state, bpy::arg("name")) - .def("priority", &MOBase::IPluginList::priority, bpy::arg("name")) - .def("setPriority", &MOBase::IPluginList::setPriority, (bpy::arg("name"), "priority")) - .def("loadOrder", &MOBase::IPluginList::loadOrder, bpy::arg("name")) - .def("hasMasterExtension", &MOBase::IPluginList::hasMasterExtension, bpy::arg("name")) - .def("hasLightExtension", &MOBase::IPluginList::hasLightExtension, bpy::arg("name")) - .def("isMasterFlagged", &MOBase::IPluginList::isMasterFlagged, bpy::arg("name")) - .def("isLightFlagged", &MOBase::IPluginList::isLightFlagged, bpy::arg("name")) - .def("masters", &MOBase::IPluginList::masters, bpy::arg("name")) - .def("origin", &MOBase::IPluginList::origin, bpy::arg("name")) - .def("onRefreshed", &MOBase::IPluginList::onRefreshed, bpy::arg("callback")) - .def("onPluginMoved", &MOBase::IPluginList::onPluginMoved, bpy::arg("callback")) - .def("onPluginStateChanged", &MOBase::IPluginList::onPluginStateChanged, bpy::arg("callback")) - .def("pluginNames", &MOBase::IPluginList::pluginNames) - .def("setState", &MOBase::IPluginList::setState, (bpy::arg("name"), "state")) - .def("setLoadOrder", &MOBase::IPluginList::setLoadOrder, bpy::arg("loadorder")) - - // DEPRECATED - .def("isMaster", &MOBase::IPluginList::isMaster, bpy::arg("name")) - .def("onPluginStateChanged", +[](IPluginList* modList, const std::function& fn) { - utils::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); - } - }); - }, bpy::arg("callback")) - ; - - bpy::enum_("ModState") - .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) - ; - - bpy::class_("IModList", bpy::no_init) - .def("displayName", &MOBase::IModList::displayName, bpy::arg("name")) - .def("allMods", &MOBase::IModList::allMods) - .def("allModsByProfilePriority", &MOBase::IModList::allModsByProfilePriority, bpy::arg("profile") = bpy::ptr((IProfile*)nullptr)) - - .def("getMod", &MOBase::IModList::getMod, bpy::return_value_policy(), bpy::arg("name")) - .def("removeMod", &MOBase::IModList::removeMod, bpy::arg("mod")) - .def("renameMod", &MOBase::IModList::renameMod, bpy::return_value_policy(), (bpy::arg("mod"), bpy::arg("name"))) - - .def("state", &MOBase::IModList::state, bpy::arg("name")) - .def("setActive", - static_cast(&MOBase::IModList::setActive), (bpy::arg("names"), "active")) - .def("setActive", - static_cast(&MOBase::IModList::setActive), (bpy::arg("name"), "active")) - .def("priority", &MOBase::IModList::priority, bpy::arg("name")) - .def("setPriority", &MOBase::IModList::setPriority, (bpy::arg("name"), "priority")) - - // Kept but deprecated for backward compatibility: - .def("onModStateChanged", +[](IModList* modList, const std::function& fn) { - utils::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); - } - }); - }, bpy::arg("callback")) - - .def("onModInstalled", &MOBase::IModList::onModInstalled, bpy::arg("callback")) - .def("onModRemoved", &MOBase::IModList::onModRemoved, bpy::arg("callback")) - .def("onModStateChanged", &MOBase::IModList::onModStateChanged, bpy::arg("callback")) - .def("onModMoved", &MOBase::IModList::onModMoved, bpy::arg("callback")) - ; - - // Note: localizedName, master, requirements and enabledByDefault have to go in all the plugin wrappers declaration, - // since the default functions are specific to each wrapper, otherwise in turns into an - // infinite recursion mess. - bpy::class_("IPlugin") - .def("init", bpy::pure_virtual(&MOBase::IPlugin::init), bpy::arg("organizer")) - .def("name", bpy::pure_virtual(&MOBase::IPlugin::name)) - .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginWrapper::master_Default) - .def("author", bpy::pure_virtual(&MOBase::IPlugin::author)) - .def("description", bpy::pure_virtual(&MOBase::IPlugin::description)) - .def("version", bpy::pure_virtual(&MOBase::IPlugin::version)) - .def("requirements", &MOBase::IPlugin::requirements, &IPluginWrapper::requirements_Default) - .def("settings", bpy::pure_virtual(&MOBase::IPlugin::settings)) - .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginWrapper::enabledByDefault_Default) - ; - - bpy::class_, boost::noncopyable>("IPluginDiagnose") - .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginDiagnoseWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginDiagnoseWrapper::master_Default) - .def("requirements", &MOBase::IPlugin::requirements, &IPluginDiagnoseWrapper::requirements_Default) - .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginDiagnoseWrapper::enabledByDefault_Default) - - .def("activeProblems", bpy::pure_virtual(&MOBase::IPluginDiagnose::activeProblems)) - .def("shortDescription", bpy::pure_virtual(&MOBase::IPluginDiagnose::shortDescription), bpy::arg("key")) - .def("fullDescription", bpy::pure_virtual(&MOBase::IPluginDiagnose::fullDescription), bpy::arg("key")) - .def("hasGuidedFix", bpy::pure_virtual(&MOBase::IPluginDiagnose::hasGuidedFix), bpy::arg("key")) - .def("startGuidedFix", bpy::pure_virtual(&MOBase::IPluginDiagnose::startGuidedFix), bpy::arg("key")) - .def("_invalidate", &IPluginDiagnoseWrapper::invalidate) - ; - - bpy::class_("Mapping", bpy::init<>()) - .def("__init__", bpy::make_constructor(+[](QString src, QString dst, bool dir, bool crt) -> Mapping* { - return new Mapping{ src, dst, dir, crt }; - }, bpy::default_call_policies(), - (bpy::arg("source"), bpy::arg("destination"), bpy::arg("is_directory"), bpy::arg("create_target") = false))) - .def_readwrite("source", &Mapping::source) - .def_readwrite("destination", &Mapping::destination) - .def_readwrite("isDirectory", &Mapping::isDirectory) - .def_readwrite("createTarget", &Mapping::createTarget) - .def("__str__", +[](Mapping * m) { - return fmt::format(L"Mapping({}, {}, {}, {})", m->source.toStdWString(), m->destination.toStdWString(), m->isDirectory, m->createTarget); - }) - ; - - bpy::class_, boost::noncopyable>("IPluginFileMapper") - .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginFileMapperWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginFileMapperWrapper::master_Default) - .def("requirements", &MOBase::IPlugin::requirements, &IPluginFileMapperWrapper::requirements_Default) - .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginFileMapperWrapper::enabledByDefault_Default) - - .def("mappings", bpy::pure_virtual(&MOBase::IPluginFileMapper::mappings)) - ; - - bpy::enum_("LoadOrderMechanism") - .value("FileTime", MOBase::IPluginGame::LoadOrderMechanism::FileTime) - .value("PluginsTxt", MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) - - .value("FILE_TIME", MOBase::IPluginGame::LoadOrderMechanism::FileTime) - .value("PLUGINS_TXT", MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) - ; - - bpy::enum_("SortMechanism") - .value("NONE", MOBase::IPluginGame::SortMechanism::NONE) - .value("MLOX", MOBase::IPluginGame::SortMechanism::MLOX) - .value("BOSS", MOBase::IPluginGame::SortMechanism::BOSS) - .value("LOOT", MOBase::IPluginGame::SortMechanism::LOOT) - ; - - // This doesn't actually do the conversion, but might be convenient for accessing the names for enum bits - bpy::enum_("ProfileSetting") - .value("mods", MOBase::IPluginGame::MODS) - .value("configuration", MOBase::IPluginGame::CONFIGURATION) - .value("savegames", MOBase::IPluginGame::SAVEGAMES) - .value("preferDefaults", MOBase::IPluginGame::PREFER_DEFAULTS) - - .value("MODS", MOBase::IPluginGame::MODS) - .value("CONFIGURATION", MOBase::IPluginGame::CONFIGURATION) - .value("SAVEGAMES", MOBase::IPluginGame::SAVEGAMES) - .value("PREFER_DEFAULTS", MOBase::IPluginGame::PREFER_DEFAULTS) - ; - - bpy::class_, boost::noncopyable>("IPluginGame") - .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginGameWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginGameWrapper::master_Default) - - .def("detectGame", bpy::pure_virtual(&MOBase::IPluginGame::detectGame)) - .def("gameName", bpy::pure_virtual(&MOBase::IPluginGame::gameName)) - .def("initializeProfile", bpy::pure_virtual(&MOBase::IPluginGame::initializeProfile), (bpy::arg("directory"), "settings")) - .def("listSaves", bpy::pure_virtual(&MOBase::IPluginGame::listSaves), bpy::arg("folder")) - .def("isInstalled", bpy::pure_virtual(&MOBase::IPluginGame::isInstalled)) - .def("gameIcon", bpy::pure_virtual(&MOBase::IPluginGame::gameIcon)) - .def("gameDirectory", bpy::pure_virtual(&MOBase::IPluginGame::gameDirectory)) - .def("dataDirectory", bpy::pure_virtual(&MOBase::IPluginGame::dataDirectory)) - .def("setGamePath", bpy::pure_virtual(&MOBase::IPluginGame::setGamePath), bpy::arg("path")) - .def("documentsDirectory", bpy::pure_virtual(&MOBase::IPluginGame::documentsDirectory)) - .def("savesDirectory", bpy::pure_virtual(&MOBase::IPluginGame::savesDirectory)) - .def("executables", bpy::pure_virtual(&MOBase::IPluginGame::executables)) - .def("executableForcedLoads", bpy::pure_virtual(&MOBase::IPluginGame::executableForcedLoads)) - .def("steamAPPId", bpy::pure_virtual(&MOBase::IPluginGame::steamAPPId)) - .def("primaryPlugins", bpy::pure_virtual(&MOBase::IPluginGame::primaryPlugins)) - .def("gameVariants", bpy::pure_virtual(&MOBase::IPluginGame::gameVariants)) - .def("setGameVariant", bpy::pure_virtual(&MOBase::IPluginGame::setGameVariant), bpy::arg("variant")) - .def("binaryName", bpy::pure_virtual(&MOBase::IPluginGame::binaryName)) - .def("gameShortName", bpy::pure_virtual(&MOBase::IPluginGame::gameShortName)) - .def("primarySources", bpy::pure_virtual(&MOBase::IPluginGame::primarySources)) - .def("validShortNames", bpy::pure_virtual(&MOBase::IPluginGame::validShortNames)) - .def("gameNexusName", bpy::pure_virtual(&MOBase::IPluginGame::gameNexusName)) - .def("iniFiles", bpy::pure_virtual(&MOBase::IPluginGame::iniFiles)) - .def("DLCPlugins", bpy::pure_virtual(&MOBase::IPluginGame::DLCPlugins)) - .def("CCPlugins", bpy::pure_virtual(&MOBase::IPluginGame::CCPlugins)) - .def("loadOrderMechanism", bpy::pure_virtual(&MOBase::IPluginGame::loadOrderMechanism)) - .def("sortMechanism", bpy::pure_virtual(&MOBase::IPluginGame::sortMechanism)) - .def("nexusModOrganizerID", bpy::pure_virtual(&MOBase::IPluginGame::nexusModOrganizerID)) - .def("nexusGameID", bpy::pure_virtual(&MOBase::IPluginGame::nexusGameID)) - .def("looksValid", bpy::pure_virtual(&MOBase::IPluginGame::looksValid), bpy::arg("directory")) - .def("gameVersion", bpy::pure_virtual(&MOBase::IPluginGame::gameVersion)) - .def("getLauncherName", bpy::pure_virtual(&MOBase::IPluginGame::getLauncherName)) - - .def("featureList", +[](MOBase::IPluginGame* p) { - // Constructing a dict from class name to actual object: - bpy::dict dict; - mp11::mp_for_each< - // Must user pointers because mp_for_each construct object: - mp11::mp_transform - >([&](auto* pt) { - using T = std::remove_pointer_t; - typename bpy::reference_existing_object::apply::type converter; - - // Retrieve the python class object: - const bpy::converter::registration* registration = bpy::converter::registry::query(bpy::type_id()); - bpy::object key = bpy::object(bpy::handle<>(bpy::borrowed(registration->get_class_object()))); - - // Set the object: - dict[key] = bpy::handle<>(converter(p->feature())); - }); - return dict; - }) - - .def("feature", +[](MOBase::IPluginGame* p, bpy::object clsObj) { - bpy::object feature; - mp11::mp_for_each< - // Must user pointers because mp_for_each construct object: - mp11::mp_transform - >([&](auto* pt) { - using T = std::remove_pointer_t; - typename bpy::reference_existing_object::apply::type converter; - - // Retrieve the python class object: - const bpy::converter::registration* registration = bpy::converter::registry::query(bpy::type_id()); - - if (clsObj.ptr() == (PyObject*) registration->get_class_object()) { - feature = bpy::object(bpy::handle<>(converter(p->feature()))); - } - }); - return feature; - }, bpy::arg("feature_type")) - ; - - bpy::enum_("InstallResult") - .value("SUCCESS", MOBase::IPluginInstaller::RESULT_SUCCESS) - .value("FAILED", MOBase::IPluginInstaller::RESULT_FAILED) - .value("CANCELED", MOBase::IPluginInstaller::RESULT_CANCELED) - .value("MANUAL_REQUESTED", MOBase::IPluginInstaller::RESULT_MANUALREQUESTED) - .value("NOT_ATTEMPTED", MOBase::IPluginInstaller::RESULT_NOTATTEMPTED) - ; - - bpy::class_, boost::noncopyable>("IPluginInstaller", bpy::no_init) - .def("isArchiveSupported", &IPluginInstaller::isArchiveSupported, bpy::arg("tree")) - .def("priority", &IPluginInstaller::priority) - .def("onInstallationStart", &IPluginInstaller::onInstallationStart, (bpy::arg("archive"), bpy::arg("reinstallation"), bpy::arg("current_mod"))) - .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, (bpy::arg("result"), bpy::arg("new_mod"))) - .def("isManualInstaller", &IPluginInstaller::isManualInstaller) - .def("setParentWidget", &IPluginInstaller::setParentWidget, bpy::arg("parent")) - .def("setInstallationManager", &IPluginInstaller::setInstallationManager, bpy::arg("manager")) - ; - - bpy::class_, boost::noncopyable>("IPluginInstallerSimple") - .def("onInstallationStart", &IPluginInstaller::onInstallationStart, (bpy::arg("archive"), bpy::arg("reinstallation"), bpy::arg("current_mod"))) - .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, (bpy::arg("result"), bpy::arg("new_mod"))) - .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginInstallerSimpleWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginInstallerSimpleWrapper::master_Default) - .def("requirements", &MOBase::IPlugin::requirements, &IPluginInstallerSimpleWrapper::requirements_Default) - .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginInstallerSimpleWrapper::enabledByDefault_Default) - - // 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& modName, std::shared_ptr& tree, QString& version, int& nexusID) - -> std::variant, std::tuple, QString, int>> { - auto result = p->install(modName, tree, version, nexusID); - return std::make_tuple(result, tree, version, nexusID); - }, (bpy::arg("name"), "tree", "version", "nexus_id")) - .def("_parentWidget", &IPluginInstallerSimpleWrapper::parentWidget, bpy::return_value_policy()) - .def("_manager", &IPluginInstallerSimpleWrapper::manager, bpy::return_value_policy()) - ; - - bpy::class_, boost::noncopyable>("IPluginInstallerCustom") - .def("onInstallationStart", &IPluginInstaller::onInstallationStart, (bpy::arg("archive"), bpy::arg("reinstallation"), bpy::arg("current_mod"))) - .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, (bpy::arg("result"), bpy::arg("new_mod"))) - .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginInstallerCustomWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginInstallerCustomWrapper::master_Default) - .def("requirements", &MOBase::IPlugin::requirements, &IPluginInstallerCustomWrapper::requirements_Default) - .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginInstallerCustomWrapper::enabledByDefault_Default) - - // Needs to add both otherwize boost does not understand: - .def("isArchiveSupported", &IPluginInstaller::isArchiveSupported, bpy::arg("tree")) - .def("isArchiveSupported", &IPluginInstallerCustom::isArchiveSupported, bpy::arg("archive_name")) - .def("supportedExtensions", &IPluginInstallerCustom::supportedExtensions) - .def("install", &IPluginInstallerCustom::install, (bpy::arg("mod_name"), "game_name", "archive_name", "version", "nexus_id")) - .def("_parentWidget", &IPluginInstallerSimpleWrapper::parentWidget, bpy::return_value_policy()) - .def("_manager", &IPluginInstallerCustomWrapper::manager, bpy::return_value_policy()) - ; - - bpy::class_, boost::noncopyable>("IPluginModPage") - .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginModPageWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginModPageWrapper::master_Default) - .def("requirements", &MOBase::IPlugin::requirements, &IPluginModPageWrapper::requirements_Default) - .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginModPageWrapper::enabledByDefault_Default) - - .def("displayName", bpy::pure_virtual(&IPluginModPage::displayName)) - .def("icon", bpy::pure_virtual(&IPluginModPage::icon)) - .def("pageURL", bpy::pure_virtual(&IPluginModPage::pageURL)) - .def("useIntegratedBrowser", bpy::pure_virtual(&IPluginModPage::useIntegratedBrowser)) - .def("handlesDownload", bpy::pure_virtual(&IPluginModPage::handlesDownload), (bpy::arg("page_url"), "download_url", "fileinfo")) - .def("setParentWidget", &IPluginModPage::setParentWidget, &IPluginModPageWrapper::setParentWidget_Default, bpy::arg("parent")) - .def("_parentWidget", &IPluginModPageWrapper::parentWidget, bpy::return_value_policy()) - ; - - bpy::class_, boost::noncopyable>("IPluginPreview") - .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginPreviewWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginPreviewWrapper::master_Default) - .def("requirements", &MOBase::IPlugin::requirements, &IPluginPreviewWrapper::requirements_Default) - .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginPreviewWrapper::enabledByDefault_Default) - - .def("supportedExtensions", bpy::pure_virtual(&IPluginPreview::supportedExtensions)) - .def("genFilePreview", bpy::pure_virtual(&IPluginPreview::genFilePreview), bpy::return_value_policy(), - (bpy::arg("filename"), "max_size")) - ; - - bpy::class_, boost::noncopyable>("IPluginTool") - .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginToolWrapper::localizedName_Default) - .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) - .def("requirements", &MOBase::IPlugin::requirements, &IPluginToolWrapper::requirements_Default) - .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginToolWrapper::enabledByDefault_Default) - - .def("displayName", bpy::pure_virtual(&IPluginTool::displayName)) - .def("tooltip", bpy::pure_virtual(&IPluginTool::tooltip)) - .def("icon", bpy::pure_virtual(&IPluginTool::icon)) - .def("display", bpy::pure_virtual(&IPluginTool::display)) - .def("setParentWidget", &IPluginTool::setParentWidget, &IPluginToolWrapper::setParentWidget_Default, bpy::arg("parent")) - .def("_parentWidget", &IPluginToolWrapper::parentWidget, bpy::return_value_policy()) - ; - - registerGameFeaturesPythonConverters(); - - bpy::def("getFileVersion", &MOBase::getFileVersion, bpy::arg("filepath")); - bpy::def("getProductVersion", &MOBase::getProductVersion, bpy::arg("executable")); - bpy::def("getIconForExecutable", &MOBase::iconForExecutable, bpy::arg("executable")); - - bpy::object widgets(bpy::borrowed(PyImport_AddModule("mobase.widgets"))); - bpy::scope().attr("widgets") = widgets; - { - bpy::scope w_ = widgets; - register_widgets(); - } - - // Expose MoVariant: MoVariant is a fake object whose only purpose is to be used as a type-hint - // on the python side (e.g., def foo(x: mobase.MoVariant)). The real MoVariant is defined in the - // generated stubs, since it's only relevant when doing type-checking, but this needs to be defined, - // otherwise MoVariant is not found when actually running plugins through MO2, making them crash. - bpy::scope().attr("MoVariant") = bpy::object(); -} +namespace py = pybind11; /** * */ -class PythonRunner : public IPythonRunner -{ +class PythonRunner : public IPythonRunner { public: - PythonRunner(); - ~PythonRunner(); + PythonRunner() = default; + ~PythonRunner() = default; - bool initPython(); + QList load(const QString& identifier) override; + void unload(const QString& identifier) override; - QList load(const QString& identifier); - void unload(const QString& identifier); - - bool isPythonInitialized() const; - bool isPythonVersionSupported() const; + bool initialize(QStringList const& paths) override; + bool isInitialized() const override; private: - - void initPath(); - - /** - * @brief Ensure that the given folder is in sys.path. - */ - void ensureFolderInPath(QString folder); - - /** - * @brief Append the underlying object of the given python object to the - * interface list if it is an instance (pointer) of the given type. - * - * @param obj The object to check. - * @param interfaces The list to append the object to. - * - */ - template - void appendIfInstance(bpy::object const& obj, QList& interfaces); + /** + * @brief Ensure that the given folder is in sys.path. + */ + void ensureFolderInPath(QString folder); private: - - // For each "identifier" (python file or python module folder), contains the list - // of python objects to keep "alive" during the execution. - std::unordered_map> m_PythonObjects; - - wchar_t* m_PythonHome; + // for each "identifier" (python file or python module folder), contains the + // list of python objects - this does not keep the objects alive, it simply used to + // unload plugins + std::unordered_map> m_PythonObjects; }; IPythonRunner* CreatePythonRunner() { - std::unique_ptr result = std::make_unique(); - if (result->initPython()) { - return result.release(); - } - else { - return nullptr; - } + return new PythonRunner(); } -PythonRunner::PythonRunner() +bool PythonRunner::initialize(QStringList const& paths) { - m_PythonHome = new wchar_t[MAX_PATH + 1]; -} + // we only initialize Python once for the whole lifetime of the program, even if MO2 + // is restarted and the proxy or PythonRunner objects are deleted and recreated, + // Python is not re-initialized + // + // in an ideal world, we would initialize Python here (or in the constructor) and + // then finalize it in the destructor + // + // unfortunately, many library, including PyQt6, do not handle properly + // re-initializing the Python interpreter, so we cannot do that and we keep the + // interpreter alive + // -PythonRunner::~PythonRunner() { - // We need the GIL lock when destroying Python objects. - GILock lock; - - // Boost.Python does not handle cyclic garbace collection, so we need to release - // everything hold by the objects before deleting the objects themselves: - for (auto& [name, objects] : m_PythonObjects) { - for (auto& obj : objects) { - obj.attr("__dict__").attr("clear")(); + if (Py_IsInitialized()) { + return true; } - } - m_PythonObjects.clear(); -} + try { + static const char* argv0 = "ModOrganizer.exe"; -static const char *argv0 = "ModOrganizer.exe"; - -struct PrintWrapper -{ - void write(const char * message) - { - buffer << message; - if (buffer.tellp() != 0 && buffer.str().back() == '\n') - { - // actually put the string in a variable so it doesn't get destroyed as soon as we get a pointer to its data - std::string string = buffer.str().substr(0, buffer.str().length() - 1); - qDebug().nospace().noquote() << string.c_str(); - buffer = std::stringstream(); - } - } - - std::stringstream buffer; -}; - -// ErrWrapper is in error.h - -BOOST_PYTHON_MODULE(moprivate) -{ - bpy::class_("PrintWrapper", bpy::init<>()) - .def("write", &PrintWrapper::write); - bpy::class_("ErrWrapper", bpy::init<>()) - .def("instance", &ErrWrapper::instance, bpy::return_value_policy()).staticmethod("instance") - .def("write", &ErrWrapper::write) - .def("startRecordingExceptionMessage", &ErrWrapper::startRecordingExceptionMessage) - .def("stopRecordingExceptionMessage", &ErrWrapper::stopRecordingExceptionMessage) - .def("getLastExceptionMessage", &ErrWrapper::getLastExceptionMessage); - - utils::register_functor_converter(); - - // Expose a function to create a particular tree, only for debugging purpose, not in mobase. - bpy::def("makeTree", +[](std::function callback) -> std::shared_ptr { - struct FileTree : IFileTree { - - using callback_t = std::function; - - FileTree(std::shared_ptr parent, QString name, callback_t callback) : - FileTreeEntry(parent, name), IFileTree(), m_Callback(callback){ } - - std::shared_ptr addFile(QString name, bool) override { - if (m_Callback && !m_Callback(name, false)) { - throw UnsupportedOperationException("File rejected by callback."); + // initialize the core Path of Python, this must be done before initialization + // + if (!paths.isEmpty()) { + Py_SetPath(paths.join(';').toStdWString().c_str()); } - return IFileTree::addFile(name); - } - std::shared_ptr addDirectory(QString name) override { - if (m_Callback && !m_Callback(name, true)) { - throw UnsupportedOperationException("Directory rejected by callback."); + Py_OptimizeFlag = 2; + Py_NoSiteFlag = 1; + + py::initialize_interpreter(false, 1, &argv0); + + if (!Py_IsInitialized()) { + MOBase::log::error( + "failed to init python: failed to initialize interpreter."); + + if (PyGILState_Check()) { + PyEval_SaveThread(); + } + + return false; } - return IFileTree::addDirectory(name); - } - protected: + py::module_ mainModule = py::module_::import("__main__"); + py::object mainNamespace = mainModule.attr("__dict__"); + mainNamespace["sys"] = py::module_::import("sys"); + mainNamespace["mobase"] = py::module_::import("mobase"); - std::shared_ptr makeDirectory(std::shared_ptr parent, QString name) const override { - return std::make_shared(parent, name, m_Callback); - } + mo2::python::configure_python_stream(); + mo2::python::configure_python_logging(mainNamespace["mobase"]); - bool doPopulate(std::shared_ptr parent, std::vector>& entries) const override { return true; } - std::shared_ptr doClone() const override { return std::make_shared(nullptr, name(), m_Callback); } - - private: - callback_t m_Callback; - }; - return std::make_shared(nullptr, "", callback); - }, bpy::arg("callback") = bpy::object{}); -} - -bool PythonRunner::initPython() -{ - if (Py_IsInitialized()) - return true; - try { - wchar_t argBuffer[MAX_PATH]; - const size_t cSize = strlen(argv0) + 1; - mbstowcs(argBuffer, argv0, MAX_PATH); - - Py_SetProgramName(argBuffer); - PyImport_AppendInittab("mobase", &PyInit_mobase); - PyImport_AppendInittab("moprivate", &PyInit_moprivate); - Py_OptimizeFlag = 2; - Py_NoSiteFlag = 1; - initPath(); - Py_InitializeEx(0); - - if (!Py_IsInitialized()) { - if (PyGILState_Check()) + // we need to release the GIL here - which is what this does + // + // when Python is initialized, the GIl is acquired, and if it is not release, + // trying to acquire it on a different thread will deadlock PyEval_SaveThread(); - return false; + + return true; } - - PySys_SetArgv(0, (wchar_t**)&argBuffer); - - bpy::object mainModule = bpy::import("__main__"); - bpy::object mainNamespace = mainModule.attr("__dict__"); - mainNamespace["sys"] = bpy::import("sys"); - mainNamespace["moprivate"] = bpy::import("moprivate"); - bpy::import("site"); - bpy::exec("sys.stdout = moprivate.PrintWrapper()\n" - "sys.stderr = moprivate.ErrWrapper.instance()\n" - "sys.excepthook = lambda x, y, z: sys.__excepthook__(x, y, z)\n", - mainNamespace); - - - mainNamespace["mobase"] = bpy::import("mobase"); - configure_python_logging(mainNamespace["mobase"]); - - PyEval_SaveThread(); - return true; - } catch (const bpy::error_already_set&) { - qDebug("failed to init python"); - PyErr_Print(); - if (PyErr_Occurred()) { - PyErr_Print(); - } else { - qCritical("An unexpected C++ exception was thrown in python code"); + catch (const py::error_already_set& ex) { + MOBase::log::error("failed to init python: {}", ex.what()); + return false; } - if (PyGILState_Check()) - PyEval_SaveThread(); - return false; - } } - -bool handled_exec_file(bpy::str filename, bpy::object globals = bpy::object(), bpy::object locals = bpy::object()) +void PythonRunner::ensureFolderInPath(QString folder) { - return bpy::handle_exception(std::bind(bpy::exec_file, filename, globals, locals)); -} + py::module_ sys = py::module_::import("sys"); + py::list sysPath = sys.attr("path"); - -void PythonRunner::initPath() -{ - static QStringList paths = { - QCoreApplication::applicationDirPath() + "/pythoncore.zip", - QCoreApplication::applicationDirPath() + "/pythoncore", - IOrganizer::getPluginDataPath() - }; - - Py_SetPath(paths.join(';').toStdWString().c_str()); -} - -void PythonRunner::ensureFolderInPath(QString folder) { - bpy::object sys = bpy::import("sys"); - bpy::list sysPath = bpy::extract(sys.attr("path")); - - // Converting to QStringList for Qt::CaseInsensitive and because .index() - // raise an exception: - QStringList currentPath = bpy::extract(sysPath); - if (!currentPath.contains(folder, Qt::CaseInsensitive)) { - sysPath.insert(0, folder); - } -} - - - -template -void PythonRunner::appendIfInstance(bpy::object const& obj, QList &interfaces) { - bpy::extract extr{ obj }; - if (extr.check()) { - interfaces.append(extr); - } + // Converting to QStringList for Qt::CaseInsensitive and because .index() + // raise an exception: + const QStringList currentPath = sysPath.cast(); + if (!currentPath.contains(folder, Qt::CaseInsensitive)) { + sysPath.insert(0, folder); + } } QList PythonRunner::load(const QString& identifier) { - GILock lock; + py::gil_scoped_acquire lock; - // `pluginName` can either be a python file (single-file plugin or a folder (whole module). - // - // For whole module, we simply add the parent folder to path, then we load the module with a simple - // bpy::import, and we retrieve the associated __dict__ from which we extract either createPlugin or - // createPlugins. - // - // For single file, we need to use bpy::exec_file, and we will use the context (global variables) - // from __main__ (already contains mobase, and other required module). Since the context is shared - // between called of `instantiate`, we need to make sure to remove createPlugin(s) from previous call. - try { + // `pluginName` can either be a python file (single-file plugin or a folder + // (whole module). + // + // For whole module, we simply add the parent folder to path, then we load + // the module with a simple py::import, and we retrieve the associated + // __dict__ from which we extract either createPlugin or createPlugins. + // + // For single file, we need to use py::eval_file, and we will use the + // context (global variables) from __main__ (already contains mobase, and + // other required module). Since the context is shared between called of + // `instantiate`, we need to make sure to remove createPlugin(s) from + // previous call. + try { - // Dictionary that will contain createPlugin() or createPlugins(). - bpy::dict moduleDict; + // dictionary that will contain createPlugin() or createPlugins(). + py::dict moduleDict; - if (identifier.endsWith(".py")) { - bpy::object mainModule = bpy::import("__main__"); - bpy::dict moduleNamespace = bpy::extract(mainModule.attr("__dict__"))(); + if (identifier.endsWith(".py")) { + py::object mainModule = py::module_::import("__main__"); - std::string temp = ToString(identifier); - if (!handled_exec_file(temp.c_str(), moduleNamespace)) { - moduleDict = moduleNamespace; - } - } - else { - // Retrieve the module name: - QStringList parts = identifier.split("/"); - std::string moduleName = ToString(parts.takeLast()); - ensureFolderInPath(parts.join("/")); - moduleDict = bpy::dict(bpy::import(moduleName.c_str()).attr("__dict__")); - } + // make a copy, otherwise we might end up calling the createPlugin() or + // createPlugins() function multiple time + py::dict moduleNamespace = mainModule.attr("__dict__").attr("copy")(); - if (bpy::len(moduleDict) == 0) { - MOBase::log::error("Failed to import plugin from {}.", identifier); - throw pyexcept::PythonError(); - } - - // Create the plugins: - std::vector plugins; - - if (moduleDict.has_key("createPlugin")) { - plugins.push_back(moduleDict["createPlugin"]()); - - // Clear for future call - bpy::delitem(moduleDict, bpy::str("createPlugin")); - } - else if (moduleDict.has_key("createPlugins")) { - bpy::object pyPlugins = moduleDict["createPlugins"](); - if (!PySequence_Check(pyPlugins.ptr())) { - MOBase::log::error("Plugin {}: createPlugins must return a list.", identifier); - } - else { - bpy::list pyList(pyPlugins); - int nPlugins = bpy::len(pyList); - for (int i = 0; i < nPlugins; ++i) { - plugins.push_back(pyList[i]); + std::string temp = ToString(identifier); + py::eval_file(temp, moduleNamespace).is_none(); + moduleDict = moduleNamespace; } - } + else { + // Retrieve the module name: + QStringList parts = identifier.split("/"); + std::string moduleName = ToString(parts.takeLast()); + ensureFolderInPath(parts.join("/")); - // Clear for future call - bpy::delitem(moduleDict, bpy::str("createPlugins")); + // check if the module is already loaded + py::dict modules = py::module_::import("sys").attr("modules"); + if (modules.contains(moduleName)) { + py::module_ prev = modules[py::str(moduleName)]; + py::module_(prev).reload(); + moduleDict = prev.attr("__dict__"); + } + else { + moduleDict = py::module_::import(moduleName.c_str()).attr("__dict__"); + } + } + + if (py::len(moduleDict) == 0) { + MOBase::log::error("No plugins found in {}.", identifier); + return {}; + } + + // Create the plugins: + std::vector plugins; + + if (moduleDict.contains("createPlugin")) { + plugins.push_back(moduleDict["createPlugin"]()); + } + else if (moduleDict.contains("createPlugins")) { + py::object pyPlugins = moduleDict["createPlugins"](); + if (!py::isinstance(pyPlugins)) { + MOBase::log::error("Plugin {}: createPlugins must return a sequence.", + identifier); + } + else { + py::sequence pyList(pyPlugins); + size_t nPlugins = pyList.size(); + for (size_t i = 0; i < nPlugins; ++i) { + plugins.push_back(pyList[i]); + } + } + } + else { + MOBase::log::error("Plugin {}: missing a createPlugin(s) function.", + identifier); + } + + // If we have no plugins, there was an issue, and we already logged the + // problem: + if (plugins.empty()) { + return QList(); + } + + QList allInterfaceList; + + for (py::object pluginObj : plugins) { + + // save to be able to unload it + m_PythonObjects[identifier].push_back(pluginObj); + + QList interfaceList = py::module_::import("mobase.private") + .attr("extract_plugins")(pluginObj) + .cast>(); + + if (interfaceList.isEmpty()) { + MOBase::log::error("Plugin {}: no plugin interface implemented.", + identifier); + } + + // Append the plugins to the main list: + allInterfaceList.append(interfaceList); + } + + return allInterfaceList; } - else { - MOBase::log::error("Plugin {}: missing a createPlugin(s) function.", identifier); + catch (const py::error_already_set& ex) { + MOBase::log::error("Failed to import plugin from {}.", identifier); + throw pyexcept::PythonError(ex); } - - // If we have no plugins, there was an issue, and we already logged the problem: - if (plugins.empty()) { - return QList(); - } - - QList allInterfaceList; - - for (bpy::object pluginObj : plugins) { - - // Add the plugin to keep it alive: - m_PythonObjects[identifier].push_back(pluginObj); - - QList interfaceList; - - appendIfInstance(pluginObj, interfaceList); - // Must try the wrapper because it's only a plugin extension interface in C++, so doesn't extend QObject - appendIfInstance(pluginObj, interfaceList); - // Must try the wrapper because it's only a plugin extension interface in C++, so doesn't extend QObject - appendIfInstance(pluginObj, interfaceList); - appendIfInstance(pluginObj, interfaceList); - appendIfInstance(pluginObj, interfaceList); - appendIfInstance(pluginObj, interfaceList); - appendIfInstance(pluginObj, interfaceList); - appendIfInstance(pluginObj, interfaceList); - - if (interfaceList.isEmpty()) { - appendIfInstance(pluginObj, interfaceList); - } - - if (interfaceList.isEmpty()) { - MOBase::log::error("Plugin {}: no plugin interface implemented.", identifier); - } - - // Append the plugins to the main list: - allInterfaceList.append(interfaceList); - } - - return allInterfaceList; - } - catch (const bpy::error_already_set&) { - MOBase::log::error("Failed to import plugin from {}.", identifier); - throw pyexcept::PythonError(); - } } void PythonRunner::unload(const QString& identifier) { - auto it = m_PythonObjects.find(identifier); - if (it != m_PythonObjects.end()) { + auto it = m_PythonObjects.find(identifier); + if (it != m_PythonObjects.end()) { - GILock lock; + py::gil_scoped_acquire lock; - if (!identifier.endsWith(".py")) { + if (!identifier.endsWith(".py")) { - // At this point, the identifier is the full path to the module. - QDir folder(identifier); + // At this point, the identifier is the full path to the module. + QDir folder(identifier); - // We want to "unload" (remove from sys.modules) modules that come - // from this plugin (whose __path__ points under this module, including - // the module of the plugin itself). - bpy::object sys = bpy::import("sys"); - bpy::dict modules = bpy::extract(sys.attr("modules")); - bpy::list keys = modules.keys(); - for (std::size_t i = 0; i < bpy::len(keys); ++i) { - bpy::object mod = modules[keys[i]]; - if (PyObject_HasAttrString(mod.ptr(), "__path__")) { - QString mpath = bpy::extract(mod.attr("__path__")[0]); + // We want to "unload" (remove from sys.modules) modules that come + // from this plugin (whose __path__ points under this module, + // including the module of the plugin itself). + py::object sys = py::module_::import("sys"); + py::dict modules = sys.attr("modules"); + py::list keys = modules.attr("keys")(); + for (std::size_t i = 0; i < py::len(keys); ++i) { + py::object mod = modules[keys[i]]; + if (PyObject_HasAttrString(mod.ptr(), "__path__")) { + QString mpath = mod.attr("__path__")[0].cast(); - if (!folder.relativeFilePath(mpath).startsWith("..")) { - // If the path is under identifier, we need to unload it. - log::debug("Unloading module {} from {} for {}.", bpy::extract(keys[i])(), mpath, identifier); - bpy::delitem(modules, keys[i]); - } + if (!folder.relativeFilePath(mpath).startsWith("..")) { + // If the path is under identifier, we need to unload + // it. + log::debug("Unloading module {} from {} for {}.", + keys[i].cast(), mpath, identifier); + + PyDict_DelItem(modules.ptr(), keys[i].ptr()); + } + } + } } - } + + // Boost.Python does not handle cyclic garbace collection, so we need to + // release everything hold by the objects before deleting the objects + // themselves (done when erasing from m_PythonObjects). + for (auto& obj : it->second) { + obj.attr("__dict__").attr("clear")(); + } + + log::debug("Deleting {} python objects for {}.", it->second.size(), identifier); + m_PythonObjects.erase(it); } - - // Boost.Python does not handle cyclic garbace collection, so we need to release - // everything hold by the objects before deleting the objects themselves (done when - // erasing from m_PythonObjects). - for (auto& obj : it->second) { - obj.attr("__dict__").attr("clear")(); - } - - log::debug("Deleting {} python objects for {}.", it->second.size(), identifier); - m_PythonObjects.erase(it); - - } } -bool PythonRunner::isPythonInitialized() const +bool PythonRunner::isInitialized() const { - return Py_IsInitialized() != 0; + return Py_IsInitialized() != 0; } diff --git a/src/runner/pythonrunner.h b/src/runner/pythonrunner.h index ce8cad6..5e96fb8 100644 --- a/src/runner/pythonrunner.h +++ b/src/runner/pythonrunner.h @@ -1,33 +1,44 @@ -#ifndef PYTHONRUNNER_H -#define PYTHONRUNNER_H - -#include -#include -#include -#include -#include - - -class IPythonRunner { -public: - - virtual QList load(const QString& identifier) = 0; - virtual void unload(const QString& identifier) = 0; - - virtual bool isPythonInitialized() const = 0; - - virtual ~IPythonRunner() { } -}; - - -#ifdef PYTHONRUNNER_LIBRARY -#define PYDLLEXPORT Q_DECL_EXPORT -#else // PYTHONRUNNER_LIBRARY -#define PYDLLEXPORT Q_DECL_IMPORT -#endif // PYTHONRUNNER_LIBRARY - -extern "C" PYDLLEXPORT IPythonRunner *CreatePythonRunner(); - - - -#endif // PYTHONRUNNER_H +#ifndef PYTHONRUNNER_H +#define PYTHONRUNNER_H + +#include +#include + +#include +#include +#include + +#include +#include + +class IPythonRunner { +public: + virtual QList load(const QString& identifier) = 0; + virtual void unload(const QString& identifier) = 0; + + // initialize Python + // + // paths contains the list of built-in paths for the Python library (pythonxxx.zip, + // etc.), an empty list uses the default Python paths (e.g., the PYTHONPATH + // environment variable) + // + virtual bool initialize(QStringList const& paths = {}) = 0; + + // check if the runner has been initialized, i.e., initialize() has been called and + // succeeded + virtual bool isInitialized() const = 0; + + virtual ~IPythonRunner() {} +}; + +#ifdef PYTHONRUNNER_LIBRARY +#define PYDLLEXPORT Q_DECL_EXPORT +#else // PYTHONRUNNER_LIBRARY +#define PYDLLEXPORT Q_DECL_IMPORT +#endif // PYTHONRUNNER_LIBRARY + +// create the Python runner +// +extern "C" PYDLLEXPORT IPythonRunner* CreatePythonRunner(); + +#endif // PYTHONRUNNER_H diff --git a/src/runner/pythonutils.cpp b/src/runner/pythonutils.cpp index 37445c5..5361722 100644 --- a/src/runner/pythonutils.cpp +++ b/src/runner/pythonutils.cpp @@ -2,45 +2,152 @@ #include #include +#include -#include +#include +#include #include "log.h" -namespace utils { +namespace py = pybind11; - void show_deprecation_warning(std::string_view name, std::string_view message, bool show_once) { +namespace mo2::python { - // Contains the list of filename / line number for which a deprecation warning has already been shown. - static std::set> DeprecatedLines; - - // Find the caller: - auto inspect = bpy::import("inspect"); - auto current_frame = inspect.attr("currentframe")(); - auto callable_frame = inspect.attr("getouterframes")(current_frame, 2); - auto filename = bpy::extract(callable_frame[-1].attr("filename"))(); - auto function = bpy::extract(callable_frame[-1].attr("function"))(); - auto lineno = bpy::extract(callable_frame[-1].attr("lineno")); + class PrintWrapper { + MOBase::log::Levels level_; + std::stringstream buffer_; - // Only show once if requested: - if (show_once && DeprecatedLines.contains({ filename, lineno })) { - return; + public: + PrintWrapper(MOBase::log::Levels level) : level_{level} {} + + void write(std::string_view message) + { + buffer_ << message; + if (buffer_.tellp() != 0 && buffer_.str().back() == '\n') { + const auto full_message = buffer_.str(); + MOBase::log::log(level_, + full_message.substr(0, full_message.length() - 1)); + buffer_ = std::stringstream{}; + } + } + }; + + /** + * @brief Construct a dynamic Python type. + * + */ + template + pybind11::object make_python_type(std::string_view name, + pybind11::tuple base_classes, Args&&... args) + { + // this is ugly but that's how it's done in C Python + auto type = py::reinterpret_borrow((PyObject*)&PyType_Type); + + // create the python class + return type(name, base_classes, py::dict(std::forward(args)...)); } - // Register the deprecation: - DeprecatedLines.emplace(filename, lineno); + void configure_python_stream() + { + // create the "MO2Handler" python class + auto printWrapper = make_python_type( + "MO2PrintWrapper", py::make_tuple(), + py::arg("write") = py::cpp_function([](std::string_view message) { + static PrintWrapper wrapper(MOBase::log::Debug); + wrapper.write(message); + }), + py::arg("flush") = py::cpp_function([] {})); + auto errorWrapper = make_python_type( + "MO2ErrorWrapper", py::make_tuple(), + py::arg("write") = py::cpp_function([](std::string_view message) { + static PrintWrapper wrapper(MOBase::log::Error); + wrapper.write(message); + }), + py::arg("flush") = py::cpp_function([] {})); + py::module_ sys = py::module_::import("sys"); + sys.attr("stdout") = printWrapper(); + sys.attr("stderr") = errorWrapper(); - 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); + // this is required to handle exception in Python code OUTSIDE of pybind11 call, + // typically on Qt classes with methods overridden on the Python side + // + // without this, the application will crash instead of properly handling the + // exception as it would do with a py::error_already_set{} + // + // IMPORTANT: sys.attr("excepthook") = sys.attr("__excepthook__") DOES NOT WORK, + // and I have no clue why since the attribute does not seem to get updated (at + // least a print does not show it) + // + sys.attr("excepthook") = + py::eval("lambda x, y, z: sys.__excepthook__(x, y, z)"); } - else { - MOBase::log::warn( - "[deprecated] {} in {} [{}:{}]: {}", name, function, path.native(), lineno, message); - } - } -} \ No newline at end of file + // Small structure to hold the levels - There are copy paste from + // my Python version and I assume these will not change soon: + struct PyLogLevel { + static constexpr int CRITICAL = 50; + static constexpr int ERROR = 40; + static constexpr int WARNING = 30; + static constexpr int INFO = 20; + static constexpr int DEBUG = 10; + }; + + // This is the function we are going to use as our Handler .emit + // method. + void emit_function(py::object self, py::object record) + { + + // There are other parameters that could be used, but this is minimal + // for now (filename, line number, etc.). + const int level = record.attr("levelno").cast(); + const std::wstring msg = py::str(record.attr("msg")).cast(); + + switch (level) { + case PyLogLevel::CRITICAL: + case PyLogLevel::ERROR: + MOBase::log::error("{}", msg); + break; + case PyLogLevel::WARNING: + MOBase::log::warn("{}", msg); + break; + case PyLogLevel::INFO: + MOBase::log::info("{}", msg); + break; + case PyLogLevel::DEBUG: + default: // There is a "NOTSET" level in theory: + MOBase::log::debug("{}", msg); + break; + } + }; + + void configure_python_logging(py::module_ mobase) + { + // most of this is dealing with actual Python objects since it is not + // possible to derive from logging.Handler in C++ using Boost.Python, + // and since a lot of this would require extra register only for this. + + // see also + // https://github.com/pybind/pybind11/issues/1193#issuecomment-429451094 + + // retrieve the logging module and the Handler class. + auto logging = py::module_::import("logging"); + auto Handler = logging.attr("Handler"); + + // create the "MO2Handler" python class + auto MO2Handler = + make_python_type("LogHandler", py::make_tuple(Handler), + py::arg("emit") = py::cpp_function(emit_function)); + + // create the default logger + auto handler = MO2Handler(); + handler.attr("setLevel")(PyLogLevel::DEBUG); + auto logger = logging.attr("getLogger")(py::object(mobase.attr("__name__"))); + logger.attr("setLevel")(PyLogLevel::DEBUG); + logger.attr("addHandler")(handler); + + // set mobase attributes + mobase.attr("LogHandler") = MO2Handler; + mobase.attr("logger") = logger; + } + +} // namespace mo2::python diff --git a/src/runner/pythonutils.h b/src/runner/pythonutils.h index 6703557..019e782 100644 --- a/src/runner/pythonutils.h +++ b/src/runner/pythonutils.h @@ -1,271 +1,25 @@ #ifndef PYTHONRUNNER_UTILS_H #define PYTHONRUNNER_UTILS_H -#include +#include -#include "error.h" +#include -namespace utils { +namespace mo2::python { - namespace bpy = boost::python; + /** + * @brief Configure Python stdout and stderr to log to MO2. + * + */ + void configure_python_stream(); - namespace details { + /** + * @brief Configure logging for MO2 python plugin. + * + * @param mobase The mobase module. + */ + void configure_python_logging(pybind11::module_ mobase); - template - struct is_stdmap_iterator : std::false_type {}; +} // namespace mo2::python - template - struct is_stdmap_iterator()->first)>> : std::true_type {}; - - // Note: QMap and standard maps do not have the same type of iterators: - template {}, int> = 0> - inline auto set_dict_entry(bpy::dict& result, It const& it) { - result[bpy::object{ it->first }] = bpy::object{ it->second }; - } - - template {}, int > = 0> - inline auto set_dict_entry(bpy::dict& result, It const& it) { - result[bpy::object{ it.key() }] = bpy::object{ it.value() }; - } - - } - - template - struct map_to_python { - static PyObject* convert(const Map& map) { - bpy::dict result; - for (auto it = map.begin(); it != map.end(); ++it) { - details::set_dict_entry(result, it); - } - return bpy::incref(result.ptr()); - } - }; - - template - struct map_from_python { - - using key_type = typename Map::key_type; - using value_type = typename Map::mapped_type; - - static void* convertible(PyObject* objPtr) { - return PyDict_Check(objPtr) ? objPtr : nullptr; - } - - static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { - void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - Map* result = new (storage) Map(); - bpy::dict source(bpy::handle<>(bpy::borrowed(objPtr))); - bpy::list keys = source.keys(); - int len = bpy::len(keys); - for (int i = 0; i < len; ++i) { - bpy::object pyKey = keys[i]; - (*result)[bpy::extract(pyKey)] = bpy::extract(source[pyKey]); - } - - data->convertible = storage; - } - }; - - template - struct container_to_python_list { - static PyObject* convert(const Container& container) { - bpy::list pyList; - - try { - for (auto& item : container) { - pyList.append(item); - } - } - catch (const bpy::error_already_set&) { - throw pyexcept::PythonError(); - } - - return bpy::incref(pyList.ptr()); - } - }; - - - template - struct container_from_python_list { - - using value_type = typename Container::value_type; - - static void* convertible(PyObject* objPtr) { - // Check that the object can be iterated or is a sequence. There is no "clean" - // way checking that an object is iterable apparently (PyIter_Check checks that - // an object is an iterator, which is very different). - if (objPtr->ob_type->tp_iter != 0 || PySequence_Check(objPtr)) return objPtr; - return nullptr; - } - - static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { - void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - Container* result = new (storage) Container(); - bpy::object source(bpy::handle<>(bpy::borrowed(objPtr))); - bpy::stl_input_iterator begin(source), end; - std::copy(begin, end, std::back_inserter(*result)); - data->convertible = storage; - } - }; - - template - struct set_to_python { - static PyObject* convert(const Container& container) { - bpy::list pyList; - - try { - for (auto& item : container) - pyList.append(item); - } - catch (const bpy::error_already_set&) { - throw pyexcept::PythonError(); - } - - return bpy::incref(pyList.ptr()); - } - }; - - - template - struct set_from_python { - - using value_type = typename Container::value_type; - - static void* convertible(PyObject* objPtr) { - // See container_from_python. - if (objPtr->ob_type->tp_iter != 0 && PySequence_Check(objPtr)) return objPtr; - return nullptr; - } - - static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { - void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - Container* result = new (storage) Container(); - bpy::list source(bpy::handle<>(bpy::borrowed(objPtr))); - bpy::stl_input_iterator begin(source), end; - std::copy(begin, end, std::inserter(*result, result->begin())); - data->convertible = storage; - } - }; - - - template - struct optional_to_python { - static PyObject* convert(const Optional& optional) { - if (optional) { - return bpy::incref(bpy::object(*optional).ptr()); - } - else { - return bpy::incref(Py_None); - } - } - }; - - - template - struct optional_from_python { - - using value_type = typename Optional::value_type; - - static void* convertible(PyObject* objPtr) { - - if (objPtr == Py_None) { - return objPtr; - } - - bpy::object source(bpy::handle<>(bpy::borrowed(objPtr))); - return bpy::extract(source).check() ? objPtr : nullptr; - } - - static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { - void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; - Optional* result = new (storage) Optional(); - - bpy::object source(bpy::handle<>(bpy::borrowed(objPtr))); - if (!source.is_none()) { - *result = bpy::extract(source)(); - } - - data->convertible = storage; - } - }; - - - /** - * @brief Register from and to python converters for (at least) map, unordered_map and QMap. - * - * Any standard compliant associative container with key/value should work here. - * - * @tparam Map The container type to register. - */ - template - void register_associative_container() { - bpy::to_python_converter>(); - bpy::converter::registry::push_back( - &map_from_python::convertible - , &map_from_python::construct - , bpy::type_id()); - }; - - /** - * @brief Register from and to python converters for (at least) set, unordered_set and QSet. - * - * Any standard compliant associative container with key should work here. - * - * @tparam Map The container type to register. - */ - template - void register_set_container() { - bpy::to_python_converter>(); - bpy::converter::registry::push_back( - &set_from_python::convertible - , &set_from_python::construct - , bpy::type_id()); - }; - - /** - * @brief Register from and to python converters for sequence container. - * - * Any standard compliant container should work here. - * - * @tparam Map The container type to register. - */ - template - void register_sequence_container() { - bpy::to_python_converter>(); - bpy::converter::registry::push_back( - &container_from_python_list::convertible - , &container_from_python_list::construct - , bpy::type_id()); - }; - - /** - * @brief Register from and to python converters for optional. - * - * @tparam T The optional type (std::optional or boost::optional). - */ - template - void register_optional() { - bpy::to_python_converter>(); - bpy::converter::registry::push_back( - &optional_from_python::convertible - , &optional_from_python::construct - , bpy::type_id()); - }; - - - /** - * @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); - -} - -#endif \ No newline at end of file +#endif diff --git a/src/runner/pythonwrapperutilities.h b/src/runner/pythonwrapperutilities.h deleted file mode 100644 index 879b8d4..0000000 --- a/src/runner/pythonwrapperutilities.h +++ /dev/null @@ -1,168 +0,0 @@ -#ifndef PYTHONWRAPPERUTILITIES_H -#define PYTHONWRAPPERUTILITIES_H - -#include - -#include - -#include -#include - -#include "sipApiAccess.h" -#include "error.h" -#include "gilock.h" - -namespace details { - - /** - * @brief Common stuffs for all basicWrapperFunction methods. - */ - template - ReturnType wrapperFunctionImplementation(WrapperTypePtr wrapper, bool apiTransfer, Fn fn, boost::python::object* objPtr, const char *methodName, Args... args) { - GILock lock; - auto implementation = wrapper->get_override(methodName); - if (!implementation) { - if constexpr (std::is_same_v) { - throw pyexcept::MissingImplementation(wrapper->className, methodName); - } - else { - return std::invoke(fn, wrapper, args...); - } - } - try { - boost::python::object result = implementation(args...); - if (objPtr) { - *objPtr = result; - } - else if (apiTransfer) { - sipAPIAccess::sipAPI()->api_transfer_to(result.ptr(), Py_None); - } - if constexpr (!std::is_same_v) { - return boost::python::extract(result)(); - } - } - catch (const boost::python::error_already_set&) { - throw pyexcept::PythonError(); - } - catch (...) { - throw pyexcept::UnknownException(); - } - } - -} - -/** - * @brief Call the given method on the wrapper with the given arguments, with proper - * exception handling. - * - * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly - * available `className` attribute. - * @param methodName The name of the method. - * @param args... Arguments for the method. - * - * @return the result of calling the given Python method on the wrapper. - * - * @throw pyexcept::MissingImplementation if the method does not exist. - * @throw pyexcept::PythonError if an error occurs while executing the python method. - * @throw pyexecpt::UnknownException if an unknown error occurs. - */ -template -ReturnType basicWrapperFunctionImplementation(const WrapperType *wrapper, const char *methodName, Args... args) -{ - return details::wrapperFunctionImplementation(wrapper, false, nullptr, nullptr, methodName, args...); -} - -/** - * @brief Call the given method on the wrapper with the given arguments, with proper - * exception handling, and store the intermediate result in the given python object. - * - * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly - * available `className` attribute. - * @param ref Python object to which the result of `get_override()` should be stored. - * @param methodName The name of the method. - * @param args... Arguments for the method. - * - * @return the result of calling the given Python method on the wrapper. - * - * @throw pyexcept::MissingImplementation if the method does not exist. - * @throw pyexcept::PythonError if an error occurs while executing the python method. - * @throw pyexecpt::UnknownException if an unknown error occurs. - */ -template -ReturnType basicWrapperFunctionImplementation(const WrapperType* wrapper, boost::python::object &ref, const char* methodName, Args... args) -{ - return details::wrapperFunctionImplementation(wrapper, false, nullptr, &ref, methodName, args...); -} - -/** - * @brief Call the given method on the wrapper with the given arguments, with proper - * exception handling, and transfer the responsibility of the returned object to - * the C++ side. - * - * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly - * available `className` attribute. - * @param methodName The name of the method. - * @param args... Arguments for the method. - * - * @return the result of calling the given Python method on the wrapper. - * - * @throw pyexcept::MissingImplementation if the method does not exist. - * @throw pyexcept::PythonError if an error occurs while executing the python method. - * @throw pyexecpt::UnknownException if an unknown error occurs. - */ -template -ReturnType wrapperFunctionImplementationWithApiTransfer(const WrapperType* wrapper, const char* methodName, Args... args) -{ - return details::wrapperFunctionImplementation(wrapper, true, nullptr, nullptr, methodName, args...); -} - -/** - * @brief Call the given method on the wrapper with the given arguments, with proper - * exception handling, falling back to the given function if the method does not exist. - * - * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly - * available `className` attribute. - * @param fn The function to call if the method does not exists. - * @param methodName The name of the method. - * @param args... Arguments for the method. - * - * Note: `fn` does not have to be a member-function of `wrapper` but `std::invoke(fn, wrapper, args...)` must be valid. - * - * @return the result of calling the given Python method on the wrapper. - * - * @throw pyexcept::PythonError if an error occurs while executing the python method. - * @throw pyexecpt::UnknownException if an unknown error occurs. - */ -template -ReturnType basicWrapperFunctionImplementationWithDefault(WrapperTypePtr wrapper, Fn fn, const char* methodName, Args... args) -{ - return details::wrapperFunctionImplementation(wrapper, false, fn, nullptr, methodName, args...); -} - -/** - * @brief Call the given method on the wrapper with the given arguments, with proper - * exception handling, and store the intermediate result in the given python object, - * falling back to the given function if the method does not exist. - * - * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly - * available `className` attribute. - * @param fn The function to call if the method does not exists. - * @param ref Python object to which the result of `get_override()` should be stored. - * @param methodName The name of the method. - * @param args... Arguments for the method. - * - * Note: `fn` does not have to be a member-function of `wrapper` but `std::invoke(fn, wrapper, args...)` must be valid. - * - * @return the result of calling the given Python method on the wrapper. - * - * @throw pyexcept::PythonError if an error occurs while executing the python method. - * @throw pyexecpt::UnknownException if an unknown error occurs. - */ -template -ReturnType basicWrapperFunctionImplementationWithDefault(const WrapperType* wrapper, Fn fn, boost::python::object& ref, const char* methodName, Args... args) -{ - return details::wrapperFunctionImplementation(wrapper, false, fn, &ref, methodName, args...); -} - - -#endif // PYTHONWRAPPERUTILITIES_H diff --git a/src/runner/shared_ptr_converter.h b/src/runner/shared_ptr_converter.h deleted file mode 100644 index ab80ac3..0000000 --- a/src/runner/shared_ptr_converter.h +++ /dev/null @@ -1,125 +0,0 @@ -#ifndef PYTHONRUNNER_SHARED_PTR_CONVERTER_H -#define PYTHONRUNNER_SHARED_PTR_CONVERTER_H - -#include - -#include "error.h" -#include "gilock.h" - -namespace utils { - - // Shared pointers are handled in a special way by Boost.Python since they hold - // the wrapped Python object and only release it when the ref counter of the shared - // ptr drops to 0 using shared_ptr_deleter. - // - // Unfortunately for us, this will happen outside of the Python proxy for some objects - // and thus without the GIL lock, making everything crash, so we need a custom deleter - // that holds the GIL while releasing the lock. - // - // Note that this is only useful for Python -> C++ conversion, and without this, Boost - // will automatically wrapped the pointer. The C++ -> Python conversion is handled - // separately by boost::python::register_ptr_to_python. - // - // This is an open Boost.Python problem: https://github.com/boostorg/python/pull/11 - - template - struct shared_ptr_from_python; - - namespace details { - - struct shared_ptr_deleter_with_gil_lock : boost::python::converter::shared_ptr_deleter { - - using shared_ptr_deleter::shared_ptr_deleter; - - void operator()(void const* o) { - GILock lock; - shared_ptr_deleter::operator()(o); - } - - }; - - template - struct shared_ptr_void; - - template - struct shared_ptr_void> { using type = std::shared_ptr; }; - - template - struct shared_ptr_void> { using type = boost::shared_ptr; }; - - template - using shared_ptr_void_t = typename shared_ptr_void::type; - - } - - template - struct shared_ptr_from_python - { - using T = typename SharedPtr::element_type; - - shared_ptr_from_python() - { - using namespace boost::python; - converter::registry::insert(&convertible, &construct, type_id() -#ifndef BOOST_PYTHON_NO_PY_SIGNATURES - , &converter::expected_from_python_type_direct::get_pytype -#endif - ); - } - - private: - static void* convertible(PyObject* p) - { - if (p == Py_None) - return p; - - return boost::python::converter::get_lvalue_from_python(p, boost::python::converter::registered::converters); - } - - static void construct(PyObject* source, boost::python::converter::rvalue_from_python_stage1_data* data) - { - using namespace boost::python; - void* const storage = ((converter::rvalue_from_python_storage*)data)->storage.bytes; - // Deal with the "None" case. - if (data->convertible == source) - new (storage) SharedPtr(); - else - { - details::shared_ptr_void_t hold_convertible_ref_count( - (void*)0, details::shared_ptr_deleter_with_gil_lock(handle<>(borrowed(source)))); - // use aliasing constructor - new (storage) SharedPtr(hold_convertible_ref_count, - static_cast(data->convertible)); - } - - data->convertible = storage; - } - }; - - // release the bpy::object associated with the deleter of the given shared_ptr, - // if the given shared_ptr has a Boost.Python deleter - // - // this should only be used when returning from Python objects that have been created - // on the C++ side, e.g. if IFileTree.createOrphanTree() from Python and then return - // the tree - // - // for reason yet to be known, Boost.Python had a custom deleter in this case that tries - // to delete the bpy::object and fails, so we have to release the object manually - // - template - SharedPtr clean_shared_ptr(SharedPtr&& ptr) { - if (auto* d = get_deleter(ptr); d != nullptr) { - // we cannot do a proper reset() here, even with the GIL lock, for unknown reason, - // so we only release - // - // this might create lost references to Python object but this should not happen - // too often so hopefully it's not a big issue - // - d->owner.release(); - } - return ptr; - } - -} - -#endif \ No newline at end of file diff --git a/src/runner/sipapiaccess.cpp b/src/runner/sipapiaccess.cpp deleted file mode 100644 index 6d374c3..0000000 --- a/src/runner/sipapiaccess.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "sipapiaccess.h" -#include -#include -#include - -const sipAPIDef* sipAPIAccess::sipAPI() -{ - QString exception; - static const sipAPIDef* sipApi = nullptr; - if (sipApi == nullptr) { - PyImport_ImportModule("PyQt6.sip"); - - auto errorObj = PyErr_Occurred(); - if (errorObj != NULL) { - PyObject* type, * value, * traceback; - PyErr_Fetch(&type, &value, &traceback); - PyErr_NormalizeException(&type, &value, &traceback); - if (traceback != NULL) { - boost::python::handle<> h_type(type); - boost::python::handle<> h_val(value); - boost::python::handle<> h_tb(traceback); - boost::python::object tb(boost::python::import("traceback")); - boost::python::object fmt_exp(tb.attr("format_exception")); - boost::python::object exp_list(fmt_exp(h_type, h_val, h_tb)); - boost::python::object exp_str(boost::python::str("\n").join(exp_list)); - boost::python::extract returned(exp_str); - exception = QString::fromStdString(returned()); - } - PyErr_Restore(type, value, traceback); - throw MOBase::Exception(QString("Failed to load PyQt6: %1").arg(exception)); - } - - sipApi = (const sipAPIDef*)PyCapsule_Import("PyQt6.sip._C_API", 0); - if (sipApi == NULL) { - auto errorObj = PyErr_Occurred(); - if (errorObj != NULL) { - PyObject* type, * value, * traceback; - PyErr_Fetch(&type, &value, &traceback); - PyErr_NormalizeException(&type, &value, &traceback); - if (traceback != NULL) { - boost::python::handle<> h_type(type); - boost::python::handle<> h_val(value); - boost::python::handle<> h_tb(traceback); - boost::python::object tb(boost::python::import("traceback")); - boost::python::object fmt_exp(tb.attr("format_exception")); - boost::python::object exp_list(fmt_exp(h_type, h_val, h_tb)); - boost::python::object exp_str(boost::python::str("\n").join(exp_list)); - boost::python::extract returned(exp_str); - exception = QString::fromStdString(returned()); - } - PyErr_Restore(type, value, traceback); - } - throw MOBase::Exception(QString("Failed to load SIP API: %1").arg(exception)); - } - } - - return sipApi; -} \ No newline at end of file diff --git a/src/runner/sipapiaccess.h b/src/runner/sipapiaccess.h deleted file mode 100644 index 2706dda..0000000 --- a/src/runner/sipapiaccess.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef SIPAPIACCESS_H -#define SIPAPIACCESS_H - -#include - -class sipAPIAccess -{ -public: - static const sipAPIDef* sipAPI(); -}; - -#endif // SIPAPIACCESS_H diff --git a/src/runner/tuple_helper.h b/src/runner/tuple_helper.h deleted file mode 100644 index 1d7b8b5..0000000 --- a/src/runner/tuple_helper.h +++ /dev/null @@ -1,99 +0,0 @@ -#ifndef TUPLE_HELPER_H -#define TUPLE_HELPER_H - -#include -#include //len function - -namespace boost { - namespace python { - - template - struct to_py_tuple { - - static PyObject* convert(const TTuple& c_tuple) { - list values; - //add all c_tuple items to "values" list - convert_impl(c_tuple, values, std::make_index_sequence>{}); - //create Python tuple from the list - return incref(python::tuple(values).ptr()); - } - - private: - - template - static void convert_impl(const TTuple& c_tuple, list& values, std::index_sequence) { - (values.append(std::get(c_tuple)), ...); - } - - }; - - - template - struct from_py_sequence { - - using tuple_type = TTuple; - using index_sequence = std::make_index_sequence>; - - static void* convertible(PyObject* py_obj) { - - if (!PySequence_Check(py_obj)) { - return 0; - } - - if (!PyObject_HasAttrString(py_obj, "__len__")) { - return 0; - } - - python::object py_sequence(handle<>(borrowed(py_obj))); - - if (std::tuple_size_v != len(py_sequence)) { - return 0; - } - - if (convertible_impl(py_sequence, index_sequence{})) { - return py_obj; - } - else { - return 0; - } - } - - static void construct(PyObject* py_obj, converter::rvalue_from_python_stage1_data* data) { - typedef converter::rvalue_from_python_storage storage_t; - storage_t* the_storage = reinterpret_cast(data); - void* memory_chunk = the_storage->storage.bytes; - TTuple* c_tuple = new (memory_chunk) TTuple(); - data->convertible = memory_chunk; - - python::object py_sequence(handle<>(borrowed(py_obj))); - construct_impl(py_sequence, *c_tuple, index_sequence{}); - } - - private: - - template - static bool convertible_impl(const python::object& py_sequence, std::index_sequence) { - return (... && extract>(py_sequence[Is]).check()); - } - - template - static void construct_impl(const python::object& py_sequence, TTuple& c_tuple, std::index_sequence) { - c_tuple = tuple_type{ extract>(py_sequence[Is])... }; - } - - }; - - template< class TTuple> - void register_tuple() { - - to_python_converter< TTuple, to_py_tuple >(); - - converter::registry::push_back(&from_py_sequence::convertible - , &from_py_sequence::construct - , type_id()); - }; - - } -} //boost::python - -#endif \ No newline at end of file diff --git a/src/runner/uibasewrappers.h b/src/runner/uibasewrappers.h deleted file mode 100644 index 95e878a..0000000 --- a/src/runner/uibasewrappers.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef UIBASEWRAPPERS_H -#define UIBASEWRAPPERS_H - - -#ifndef Q_MOC_RUN -#pragma warning (push, 0) -#include -#pragma warning (pop) -#endif - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "error.h" -#include "gilock.h" -#include "pythonwrapperutilities.h" - -// This can be extended in C++, so why not in Python: -class IPluginRequirementWrapper : public MOBase::IPluginRequirement, public boost::python::wrapper -{ -public: - static constexpr const char* className = "IPluginRequirement"; - using boost::python::wrapper::get_override; - - virtual std::optional check(MOBase::IOrganizer *o) const override { - return basicWrapperFunctionImplementation>(this, "check", boost::python::ptr(o)); - }; -}; - -// This needs to be extendable in Python, so actually needs a wrapper: -class ISaveGameWrapper : public MOBase::ISaveGame, public boost::python::wrapper -{ -public: - static constexpr const char* className = "ISaveGameWrapper"; - using boost::python::wrapper::get_override; - - virtual QString getFilepath() const override { return basicWrapperFunctionImplementation(this, "getFilepath"); }; - virtual QDateTime getCreationTime() const override { return basicWrapperFunctionImplementation(this, "getCreationTime"); }; - virtual QString getName() const override { return basicWrapperFunctionImplementation(this, "getName"); }; - virtual QString getSaveGroupIdentifier() const override { return basicWrapperFunctionImplementation(this, "getSaveGroupIdentifier"); }; - virtual QStringList allFiles() const override { return basicWrapperFunctionImplementation(this, "allFiles"); }; - -protected: - - friend class IPluginGameWrapper; -}; - -// This needs a wrapper but currently I have no idea how to expose this properly to python: -class ISaveGameInfoWidgetWrapper : public MOBase::ISaveGameInfoWidget, public boost::python::wrapper -{ -public: - static constexpr const char* className = "ISaveGameInfoWidgetWrapper"; - using boost::python::wrapper::get_override; - - // Bring the constructor: - using ISaveGameInfoWidget::ISaveGameInfoWidget; - - virtual void setSave(MOBase::ISaveGame const& save) override { basicWrapperFunctionImplementation(this, "setSave", boost::ref(save)); }; -}; - -#endif // UIBASEWRAPPERS_H diff --git a/src/runner/variant_helper.h b/src/runner/variant_helper.h deleted file mode 100644 index 08fa27d..0000000 --- a/src/runner/variant_helper.h +++ /dev/null @@ -1,104 +0,0 @@ -#ifndef VARIANT_HELPER_H -#define VARIANT_HELPER_H - -#include - -/** - * Register variant(s) from and to python object. Greatly inspired by register_tuple<>. - */ - -namespace boost { - namespace python { - - template - struct to_py_variant { - - static PyObject* convert(const TVariant& c_variant) { - object value = std::visit([](auto const& value) { - return object{ value }; - }, c_variant); - //create Python object from the list - return incref(value.ptr()); - } - - }; - - template - struct variant_from_python; - - template