diff --git a/src/proxy/plugin_python_en.ts b/src/proxy/plugin_python_en.ts index b2049aa..e4e2f66 100644 --- a/src/proxy/plugin_python_en.ts +++ b/src/proxy/plugin_python_en.ts @@ -4,58 +4,58 @@ ProxyPython - + 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. diff --git a/src/proxy/proxypython.cpp b/src/proxy/proxypython.cpp index 65308fd..a05a8ea 100644 --- a/src/proxy/proxypython.cpp +++ b/src/proxy/proxypython.cpp @@ -17,229 +17,237 @@ You should have received a copy of the GNU General Public License along with python proxy plugin. If not, see . */ - #include "proxypython.h" +#include "log.h" +#include +#include +#include +#include +#include #include #include -#include -#include -#include -#include -#include -#include "log.h" using namespace MOBase; 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; } + + // 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; + } + else { + m_LoadFailure = FailureType::INVALID_DLL; + } + return true; + } + + const CreatePythonRunner_func createPythonRunner = + (CreatePythonRunner_func)::GetProcAddress(m_RunnerLib, "CreatePythonRunner"); + if (!createPythonRunner) { + m_LoadFailure = FailureType::INVALID_DLL; + 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_MOInfo) { + m_MOInfo->setPersistent(name(), "tryInit", false); + } + + if (!m_Runner) { + m_LoadFailure = FailureType::INITIALIZATION; + } + return true; - } - - const CreatePythonRunner_func createPythonRunner = (CreatePythonRunner_func)::GetProcAddress(m_RunnerLib, "CreatePythonRunner"); - if (!createPythonRunner) { - m_LoadFailure = FailureType::INVALID_DLL; - 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_MOInfo) { - m_MOInfo->setPersistent(name(), "tryInit", false); - } - - if (!m_Runner) { - m_LoadFailure = FailureType::INITIALIZATION; - } - - 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.fileName() == "test.py") { - result.append(name); + if (info.fileName() == "pyCfg.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); + } } - if (info.isFile() && name.endsWith(".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->isPythonInitialized()) { + 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/runner-pybind11/pythonrunner.cpp b/src/runner-pybind11/pythonrunner.cpp index 107c5e1..433be3a 100644 --- a/src/runner-pybind11/pythonrunner.cpp +++ b/src/runner-pybind11/pythonrunner.cpp @@ -300,19 +300,19 @@ PythonRunner::PythonRunner() {} PythonRunner::~PythonRunner() { - // We need the GIL lock when destroying Python objects. - py::gil_scoped_acquire lock; - // m_Interpreter.reset(); - - // 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")(); - // } + // 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")(); + // } // } - // m_PythonObjects.clear(); + // we need to clear this here otherwise there is a crash in + // finalize_interpreter() + m_PythonObjects.clear(); + + py::finalize_interpreter(); } // ErrWrapper is in error.h @@ -331,13 +331,6 @@ bool PythonRunner::initPython() try { static const char* argv0 = "ModOrganizer.exe"; - // we initialize the interpreter "the old way" because - // scoped_interpreter does not seem to work well with PyQt - 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); @@ -345,7 +338,8 @@ bool PythonRunner::initPython() Py_NoSiteFlag = 1; initPath(); - Py_InitializeEx(0); + + py::initialize_interpreter(false, 1, &argv0); if (!Py_IsInitialized()) { return false; diff --git a/src/runner-pybind11/pythonutils.cpp b/src/runner-pybind11/pythonutils.cpp index 99f3450..9998c10 100644 --- a/src/runner-pybind11/pythonutils.cpp +++ b/src/runner-pybind11/pythonutils.cpp @@ -56,13 +56,15 @@ namespace mo2::python { 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(); diff --git a/src/runner-pybind11/wrappers/pyplugins.cpp b/src/runner-pybind11/wrappers/pyplugins.cpp new file mode 100644 index 0000000..915a9e2 --- /dev/null +++ b/src/runner-pybind11/wrappers/pyplugins.cpp @@ -0,0 +1,335 @@ +#include "wrappers.h" + +#include "pyplugins.h" + +namespace py = pybind11; +using namespace pybind11::literals; +using namespace MOBase; + +namespace mo2::python { + + // this one is kind of big so it has its own function + void add_iplugingame_bindings(pybind11::module_ m) + { + + py::enum_(m, "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); + + py::enum_(m, "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 does not actually do the conversion, but might be convenient + // for accessing the names for enum bits + py::enum_(m, "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); + + // py::class_, + // boost::noncopyable>("IPluginGame") + // .def("localizedName", &MOBase::IPlugin::localizedName, + // &IPluginGameWrapper::localizedName_Default) .def("master", + // &MOBase::IPlugin::master, &IPluginGameWrapper::master_Default) + + // .def("detectGame", + // py::pure_virtual(&MOBase::IPluginGame::detectGame)) + // .def("gameName", + // py::pure_virtual(&MOBase::IPluginGame::gameName)) + // .def("initializeProfile", + // py::pure_virtual(&MOBase::IPluginGame::initializeProfile), + // (py::arg("directory"), "settings")) .def("listSaves", + // py::pure_virtual(&MOBase::IPluginGame::listSaves), + // py::arg("folder")) .def("isInstalled", + // py::pure_virtual(&MOBase::IPluginGame::isInstalled)) + // .def("gameIcon", + // py::pure_virtual(&MOBase::IPluginGame::gameIcon)) + // .def("gameDirectory", + // py::pure_virtual(&MOBase::IPluginGame::gameDirectory)) + // .def("dataDirectory", + // py::pure_virtual(&MOBase::IPluginGame::dataDirectory)) + // .def("setGamePath", + // py::pure_virtual(&MOBase::IPluginGame::setGamePath), + // py::arg("path")) .def("documentsDirectory", + // py::pure_virtual(&MOBase::IPluginGame::documentsDirectory)) + // .def("savesDirectory", + // py::pure_virtual(&MOBase::IPluginGame::savesDirectory)) + // .def("executables", + // py::pure_virtual(&MOBase::IPluginGame::executables)) + // .def("executableForcedLoads", + // py::pure_virtual(&MOBase::IPluginGame::executableForcedLoads)) + // .def("steamAPPId", + // py::pure_virtual(&MOBase::IPluginGame::steamAPPId)) + // .def("primaryPlugins", + // py::pure_virtual(&MOBase::IPluginGame::primaryPlugins)) + // .def("gameVariants", + // py::pure_virtual(&MOBase::IPluginGame::gameVariants)) + // .def("setGameVariant", + // py::pure_virtual(&MOBase::IPluginGame::setGameVariant), + // py::arg("variant")) .def("binaryName", + // py::pure_virtual(&MOBase::IPluginGame::binaryName)) + // .def("gameShortName", + // py::pure_virtual(&MOBase::IPluginGame::gameShortName)) + // .def("primarySources", + // py::pure_virtual(&MOBase::IPluginGame::primarySources)) + // .def("validShortNames", + // py::pure_virtual(&MOBase::IPluginGame::validShortNames)) + // .def("gameNexusName", + // py::pure_virtual(&MOBase::IPluginGame::gameNexusName)) + // .def("iniFiles", + // py::pure_virtual(&MOBase::IPluginGame::iniFiles)) + // .def("DLCPlugins", + // py::pure_virtual(&MOBase::IPluginGame::DLCPlugins)) + // .def("CCPlugins", + // py::pure_virtual(&MOBase::IPluginGame::CCPlugins)) + // .def("loadOrderMechanism", + // py::pure_virtual(&MOBase::IPluginGame::loadOrderMechanism)) + // .def("sortMechanism", + // py::pure_virtual(&MOBase::IPluginGame::sortMechanism)) + // .def("nexusModOrganizerID", + // py::pure_virtual(&MOBase::IPluginGame::nexusModOrganizerID)) + // .def("nexusGameID", + // py::pure_virtual(&MOBase::IPluginGame::nexusGameID)) + // .def("looksValid", + // py::pure_virtual(&MOBase::IPluginGame::looksValid), + // py::arg("directory")) .def("gameVersion", + // py::pure_virtual(&MOBase::IPluginGame::gameVersion)) + // .def("getLauncherName", + // py::pure_virtual(&MOBase::IPluginGame::getLauncherName)) + + // .def("featureList", +[](MOBase::IPluginGame* p) { + // // Constructing a dict from class name to actual object: + // py::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 py::reference_existing_object::apply::type + // converter; + + // // Retrieve the python class object: + // const py::converter::registration* registration = + // py::converter::registry::query(py::type_id()); py::object + // key + // = + // py::object(py::handle<>(py::borrowed(registration->get_class_object()))); + + // // Set the object: + // dict[key] = py::handle<>(converter(p->feature())); + // }); + // return dict; + // }) + + // .def("feature", +[](MOBase::IPluginGame* p, py::object clsObj) { + // py::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 py::reference_existing_object::apply::type + // converter; + + // // Retrieve the python class object: + // const py::converter::registration* registration = + // py::converter::registry::query(py::type_id()); + + // if (clsObj.ptr() == (PyObject*) + // registration->get_class_object()) + // { + // feature = + // py::object(py::handle<>(converter(p->feature()))); + // } + // }); + // return feature; + // }, py::arg("feature_type")) + // ; + } + + // multiple installers + void add_iplugininstaller_bindings(pybind11::module_ m) + { + py::enum_(m, "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); + + // 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", &MOBase::IPlugin::init, "organizer"_a) + .def("name", &MOBase::IPlugin::name) + .def("localizedName", &MOBase::IPlugin::localizedName) + .def("master", &MOBase::IPlugin::master) + .def("author", &MOBase::IPlugin::author) + .def("description", &MOBase::IPlugin::description) + .def("version", &MOBase::IPlugin::version) + .def("requirements", &MOBase::IPlugin::requirements) + .def("settings", &MOBase::IPlugin::settings) + .def("enabledByDefault", &MOBase::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", &MOBase::IPluginDiagnose::activeProblems) + .def("shortDescription", &MOBase::IPluginDiagnose::shortDescription, + py::arg("key")) + .def("fullDescription", &MOBase::IPluginDiagnose::fullDescription, + py::arg("key")) + .def("hasGuidedFix", &MOBase::IPluginDiagnose::hasGuidedFix, py::arg("key")) + .def("startGuidedFix", &MOBase::IPluginDiagnose::startGuidedFix, + py::arg("key")) + .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); + helper.append_if_instance(plugin_obj); + helper.append_if_instance(plugin_obj); + + if (helper.objects.isEmpty()) { + if (py::isinstance(plugin_obj)) { + helper.objects.append(plugin_obj.cast()); + } + } + + return helper.objects; + } + +} // namespace mo2::python diff --git a/src/runner-pybind11/wrappers/pyplugins.h b/src/runner-pybind11/wrappers/pyplugins.h new file mode 100644 index 0000000..ba6b8c7 --- /dev/null +++ b/src/runner-pybind11/wrappers/pyplugins.h @@ -0,0 +1,334 @@ +#ifndef PYTHON_WRAPPERS_PYPLUGINS_H +#define PYTHON_WRAPPERS_PYPLUGINS_H + +#include +#include +#include +#include + +#include "../pybind11_qt/pybind11_qt.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; + + class IPyPlugin : public QObject, public IPlugin {}; + + class IPyPluginFileMapper : public IPyPlugin, public IPluginFileMapper {}; + + class IPyPluginDiagnose : public IPyPlugin, public IPluginDiagnose {}; + + template + class PyPluginBase : public PluginBase { + public: + using PluginBase::PluginBase; + + 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, ); + } + std::vector> requirements() const + { + PYBIND11_OVERRIDE(std::vector>, + PluginBase, requirements, ); + } + 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, ); + } + bool enabledByDefault() const override + { + PYBIND11_OVERRIDE(bool, PluginBase, enabledByDefault, ); + } + }; + + 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 + { + // TODO: transfer ownership to C++ + 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::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::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( + [&modName, &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); + } + }; + +} // namespace mo2::python + +#endif