Files
modorganizer-plugin_python/src/runner/pythonrunner.cpp
T

1565 lines
78 KiB
C++

#include "pythonrunner.h"
#pragma warning( disable : 4100 )
#pragma warning( disable : 4996 )
#include <idownloadmanager.h>
#include <ifiletree.h>
#include <iinstallationmanager.h>
#include <imodinterface.h>
#include <imodlist.h>
#include <imodrepositorybridge.h>
#include <iplugin.h>
#include <iplugingame.h>
#include <iplugininstaller.h>
#include <iplugintool.h>
#include <iprofile.h>
#include <log.h>
#include <report.h>
#include "uibasewrappers.h"
#include "proxypluginwrappers.h"
#include "gamefeatureswrappers.h"
#include "sipApiAccess.h"
#include <Windows.h>
#include <utility.h>
#include <QDir>
#include <QFile>
#include <QCoreApplication>
#include <QWidget>
#include <variant>
#include <tuple>
#ifndef Q_MOC_RUN
#include <boost/python.hpp>
#include <boost/mp11.hpp>
#endif
#include "tuple_helper.h"
#include "variant_helper.h"
#include "converters.h"
#include "shared_ptr_converter.h"
#include "pylogger.h"
#include "widgets.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<bpy::reference_existing_object>()) \
.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<QObject>();
utils::register_qclass_converter<QDateTime>();
utils::register_qclass_converter<QDir>();
utils::register_qclass_converter<QFileInfo>();
utils::register_qclass_converter<QWidget>();
utils::register_qclass_converter<QMainWindow>();
utils::register_qclass_converter<QIcon>();
utils::register_qclass_converter<QSize>();
utils::register_qclass_converter<QUrl>();
// QFlags:
utils::register_qflags_converter<IPluginList::PluginStates>();
utils::register_qflags_converter<IPluginGame::ProfileSettings>();
utils::register_qflags_converter<IModList::ModStates>();
// Enums:
utils::register_enum_converter<QMessageBox::StandardButton>();
utils::register_enum_converter<QMessageBox::Icon>();
// Pointers:
bpy::register_ptr_to_python<std::shared_ptr<FileTreeEntry>>();
bpy::register_ptr_to_python<std::shared_ptr<const FileTreeEntry>>();
bpy::implicitly_convertible<std::shared_ptr<FileTreeEntry>, std::shared_ptr<const FileTreeEntry>>();
bpy::register_ptr_to_python<std::shared_ptr<IFileTree>>();
bpy::register_ptr_to_python<std::shared_ptr<const IFileTree>>();
bpy::implicitly_convertible<std::shared_ptr<IFileTree>, std::shared_ptr<const IFileTree>>();
utils::shared_ptr_from_python<std::shared_ptr<const ISaveGame>>();
bpy::register_ptr_to_python<std::shared_ptr<const ISaveGame>>();
utils::shared_ptr_from_python<std::shared_ptr<const IPluginRequirement>>();
bpy::register_ptr_to_python<std::shared_ptr<const IPluginRequirement>>();
// Containers:
utils::register_sequence_container<std::vector<int>>();
utils::register_sequence_container<std::vector<unsigned int>>();
utils::register_sequence_container<QList<ExecutableInfo>>();
utils::register_sequence_container<QList<ExecutableForcedLoadSetting>>();
utils::register_sequence_container<QList<PluginSetting>>();
utils::register_sequence_container<QList<ModRepositoryFileInfo*>>();
utils::register_sequence_container<QStringList>();
utils::register_sequence_container<QList<QFileInfo>>();
utils::register_sequence_container<QList<IOrganizer::FileInfo>>();
utils::register_sequence_container<QList<QVariant>>(); // Required for QVariant since this is QVariantList.
utils::register_sequence_container<std::vector<std::shared_ptr<const FileTreeEntry>>>();
utils::register_sequence_container<std::vector<std::shared_ptr<const ISaveGame>>>();
utils::register_sequence_container<std::vector<std::shared_ptr<const IPluginRequirement>>>();
utils::register_sequence_container<std::vector<ModDataContent::Content>>();
utils::register_sequence_container<std::vector<Mapping>>();
utils::register_sequence_container<std::vector<TaskDialogButton>>();
utils::register_set_container<std::set<QString>>();
utils::register_associative_container<QMap<QString, QVariant>>(); // Required for QVariant since this is QVariantMap.
utils::register_associative_container<QMap<QString, QStringList>>();
utils::register_associative_container<std::map<QString, IModList::ModStates>>();
utils::register_associative_container<std::map<QString, QVariant>>();
utils::register_associative_container<IFileTree::OverwritesType>();
utils::register_optional<std::optional<IPluginRequirement::Problem>>();
// Tuple:
bpy::register_tuple<std::tuple<bool, DWORD>>(); // IOrganizer::waitForApplication
bpy::register_tuple<std::tuple<bool, bool>>(); // IProfile::invalidationActive
bpy::register_tuple<std::tuple<IPluginInstaller::EInstallResult, QString, int>>();
bpy::register_tuple<std::tuple<IPluginInstaller::EInstallResult, std::shared_ptr<IFileTree>, QString, int>>();
bpy::register_tuple<std::tuple<QString, QString>>();
// Variants:
bpy::register_variant<std::variant<
IPluginInstaller::EInstallResult,
std::shared_ptr<IFileTree>,
std::tuple<IPluginInstaller::EInstallResult, std::shared_ptr<IFileTree>, QString, int>>>();
bpy::register_variant<std::variant<IFileTree::OverwritesType, std::size_t>>();
bpy::register_variant<std::variant<QString, bool>>();
bpy::register_variant<std::variant<QString, std::tuple<QString, QString>>>();
bpy::register_variant<std::variant<QString, GuessedValue<QString>>>();
// Functions:
utils::register_functor_converter<void()>(); // converter for the onRefreshed-callback
utils::register_functor_converter<void(const QString&)>();
utils::register_functor_converter<void(int)>();
utils::register_functor_converter<void(const QString&, unsigned int)>();
utils::register_functor_converter<void(const QString&, int, int)>(); // converter for the onModMoved-callback and onPluginMoved callbacks
utils::register_functor_converter<void(const std::map<QString, IModList::ModStates>&)>(); // converter for the onModStateChanged-callback (IModList)
utils::register_functor_converter<void(const std::map<QString, IPluginList::PluginStates>&)>(); // converter for the onPluginStateChanged-callback (IPluginList)
utils::register_functor_converter<void(const QString&, const QString&, const QVariant&, const QVariant&)>();
utils::register_functor_converter<void(QMainWindow*)>();
utils::register_functor_converter<void(IProfile*), bpy::pointer_wrapper<IProfile*>>();
utils::register_functor_converter<void(IProfile*, const QString&, const QString&), bpy::pointer_wrapper<IProfile*>>();
utils::register_functor_converter<void(IProfile*, IProfile*), bpy::pointer_wrapper<IProfile*>>();
utils::register_functor_converter<bool(const QString&)>();
utils::register_functor_converter<IFileTree::WalkReturn(const QString&, std::shared_ptr<const FileTreeEntry>)>();
utils::register_functor_converter<bool(const IOrganizer::FileInfo&), boost::reference_wrapper<const IOrganizer::FileInfo&>>();
utils::register_functor_converter<bool(std::shared_ptr<FileTreeEntry> const&)>();
utils::register_functor_converter<std::variant<QString, bool>(QString const&)>();
utils::register_functor_converter<void(IModInterface *), bpy::pointer_wrapper<IModInterface*>>();
utils::register_functor_converter<bool(IOrganizer*), bpy::pointer_wrapper<IOrganizer*>>();
utils::register_functor_converter<void(const IPlugin*), bpy::pointer_wrapper<const IPlugin*>>();
// This one is kept for backward-compatibility while we deprecate onModStateChanged for singl mod.
utils::register_functor_converter<void(const QString&, IModList::ModStates)>(); // converter for the onModStateChanged-callback (IModList).
utils::register_functor_converter<void(const QString&, IPluginList::PluginStates)>(); // converter for the onPluginStateChanged-callback (IPluginList).
//
// Class declarations:
//
bpy::enum_<MOBase::VersionInfo::ReleaseType>("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_<MOBase::VersionInfo::VersionScheme>("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>("VersionInfo")
.def(bpy::init<QString, VersionInfo::VersionScheme>(
(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<int, int, int, int, VersionInfo::ReleaseType>(
(bpy::arg("major"), "minor", "subminor", "subsubminor", bpy::arg("release_type") = VersionInfo::RELEASE_FINAL)))
.def(bpy::init<int, int, int, VersionInfo::ReleaseType>(
(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>(
"PluginSetting", bpy::init<const QString&, const QString&, const QVariant&>(
(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>("ExecutableInfo",
bpy::init<const QString&, const QFileInfo&>((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>("ExecutableForcedLoadSetting",
bpy::init<const QString&, const QString&>((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_<ISaveGameWrapper, bpy::bases<>, 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, bpy::bases<>, ISaveGameInfoWidgetWrapper*, boost::noncopyable>(
"ISaveGameInfoWidget", bpy::init<bpy::optional<QWidget*>>(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_<IPluginRequirement::Problem>("Problem",
bpy::init<QString, QString>((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, boost::noncopyable>("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_<IOrganizer::FileInfo>("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, boost::noncopyable>("IOrganizer", bpy::no_init)
.def("createNexusBridge", &IOrganizer::createNexusBridge, bpy::return_value_policy<bpy::reference_existing_object>())
.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::reference_existing_object>(), bpy::arg("name"))
.def("getGame", &IOrganizer::getGame, bpy::return_value_policy<bpy::reference_existing_object>(), 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::reference_existing_object>(), (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<bool(QString const&)> 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<bpy::reference_existing_object>())
.def("pluginList", &IOrganizer::pluginList, bpy::return_value_policy<bpy::reference_existing_object>())
.def("modList", &IOrganizer::modList, bpy::return_value_policy<bpy::reference_existing_object>())
.def("profile", &IOrganizer::profile, bpy::return_value_policy<bpy::reference_existing_object>())
// 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<bpy::return_by_value>())
.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<bpy::reference_existing_object>())
.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<void(const IPlugin*)> const& func) {
o->onPluginEnabled(func);
}, bpy::arg("callback"))
.def("onPluginEnabled", +[](IOrganizer* o, QString const& name, std::function<void()> const& func) {
o->onPluginEnabled(name, func);
}, (bpy::arg("name"), bpy::arg("callback")))
.def("onPluginDisabled", +[](IOrganizer* o, std::function<void(const IPlugin*)> const& func) {
o->onPluginDisabled(func);
}, bpy::arg("callback"))
.def("onPluginDisabled", +[](IOrganizer* o, QString const& name, std::function<void()> 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::reference_existing_object>(), 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<void(QString const&)>& 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, boost::noncopyable>("FileTreeEntry", bpy::no_init);
{
bpy::scope scope = fileTreeEntryClass;
bpy::enum_<FileTreeEntry::FileTypes>("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<std::shared_ptr<IFileTree>(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<std::shared_ptr<IFileTree>(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<FileTreeEntry> other) {
return entry == other.get();
})
// Special methods for debug:
.def("__repr__", +[](const FileTreeEntry* entry) { return "FileTreeEntry(\"" + entry->name() + "\")"; })
;
}
// IFileTree scope:
auto iFileTreeClass = bpy::class_<IFileTree, bpy::bases<FileTreeEntry>, boost::noncopyable>("IFileTree", bpy::no_init);
{
bpy::scope scope = iFileTreeClass;
bpy::enum_<IFileTree::InsertPolicy>("InsertPolicy")
.value("FAIL_IF_EXISTS", IFileTree::InsertPolicy::FAIL_IF_EXISTS)
.value("REPLACE", IFileTree::InsertPolicy::REPLACE)
.value("MERGE", IFileTree::InsertPolicy::MERGE)
.export_values()
;
bpy::enum_<IFileTree::WalkReturn>("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<bool(IFileTree::*)(QString, IFileTree::FileTypes) const>(&IFileTree::exists),
(bpy::arg("path"), bpy::arg("type") = IFileTree::FILE_OR_DIRECTORY))
.def("find", static_cast<std::shared_ptr<FileTreeEntry>(IFileTree::*)(QString, IFileTree::FileTypes)>(&IFileTree::find),
bpy::return_value_policy<utils::downcast_return<FileTreeEntry, IFileTree>>(), (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<IFileTree> other, bool returnOverwrites) -> std::variant<IFileTree::OverwritesType, std::size_t> {
IFileTree::OverwritesType overwrites;
auto result = p->merge(other, returnOverwrites ? &overwrites : nullptr);
if (result == IFileTree::MERGE_FAILED) {
throw std::logic_error("merge failed");
}
if (returnOverwrites) {
return { overwrites };
}
return { result };
}, (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<FileTreeEntry> 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<FileTreeEntry> 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<FileTreeEntry> entry, QString path, IFileTree::InsertPolicy insertPolicy) {
auto result = w->copy(entry, path, insertPolicy);
if (result == nullptr) {
throw std::logic_error("copy failed");
}
return result;
}, (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<std::shared_ptr<FileTreeEntry>(IFileTree::*)(std::size_t)>(&IFileTree::at),
bpy::return_value_policy<utils::downcast_return<FileTreeEntry, IFileTree>>())
.def("__iter__", bpy::range<bpy::return_value_policy<utils::downcast_return<FileTreeEntry, IFileTree>>>(
static_cast<IFileTree::iterator(IFileTree::*)()>(&IFileTree::begin),
static_cast<IFileTree::iterator(IFileTree::*)()>(&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, boost::noncopyable>("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, boost::noncopyable>("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>("ModRepositoryFileInfo", bpy::no_init)
.def(bpy::init<const ModRepositoryFileInfo &>(bpy::arg("other")))
.def(bpy::init<bpy::optional<QString, int, int>>((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, boost::noncopyable>("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, boost::noncopyable>("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<const MOBase::FileTreeEntry> entry) {
return m->createFile(utils::clean_shared_ptr(entry));
}, bpy::arg("entry"))
// accept both QString and GuessedValue<QString> 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<QString, GuessedValue<QString>> modName, QString archive, int modId) {
GuessedValue<QString> tmp;
if (auto* p = std::get_if<QString>(&modName)) {
tmp = *p;
}
else {
tmp = std::get<GuessedValue<QString>>(modName);
}
auto result = m->installArchive(tmp, archive, modId);
return std::make_tuple(result, static_cast<QString>(tmp), modId);
}, (bpy::arg("mod_name"), "archive", bpy::arg("mod_id") = 0))
;
bpy::enum_<EndorsedState>("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>("TrackedState")
.value("TRACKED_FALSE", TrackedState::TRACKED_FALSE)
.value("TRACKED_TRUE", TrackedState::TRACKED_TRUE)
.value("TRACKED_UNKNOWN", TrackedState::TRACKED_UNKNOWN)
;
bpy::class_<IModInterface, boost::noncopyable>("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_<MOBase::EGuessQuality>("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_<MOBase::GuessedValue<QString>, boost::noncopyable>("GuessedString")
.def(bpy::init<>())
.def(bpy::init<QString const&, EGuessQuality>((bpy::arg("value"), bpy::arg("quality") = EGuessQuality::GUESS_USER)))
.def("update",
static_cast<GuessedValue<QString>& (GuessedValue<QString>::*)(const QString&)>(&GuessedValue<QString>::update),
bpy::return_self<>(), bpy::arg("value"))
.def("update",
static_cast<GuessedValue<QString>& (GuessedValue<QString>::*)(const QString&, EGuessQuality)>(&GuessedValue<QString>::update),
bpy::return_self<>(), (bpy::arg("value"), "quality"))
// Methods to simulate the assignment operator:
.def("reset", +[](GuessedValue<QString>* gv) {
*gv = GuessedValue<QString>(); }, bpy::return_self<>())
.def("reset", +[](GuessedValue<QString>* gv, const QString& value, EGuessQuality eq) {
*gv = GuessedValue<QString>(value, eq); }, bpy::return_self<>(), (bpy::arg("value"), "quality"))
.def("reset", +[](GuessedValue<QString>* gv, const GuessedValue<QString>& 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<QString>* gv, std::function<std::variant<QString, bool>(QString const&)> fn) {
gv->setFilter([fn](QString& s) {
auto ret = fn(s);
return std::visit([&s](auto v) {
if constexpr (std::is_same_v<decltype(v), QString>) {
s = v;
return true;
}
else if constexpr (std::is_same_v<decltype(v), bool>) {
return v;
}
}, ret);
});
}, bpy::arg("filter"))
// This makes a copy in python but it more practical than exposing an iterator:
.def("variants", &GuessedValue<QString>::variants, bpy::return_value_policy<bpy::copy_const_reference>())
.def("__str__", &MOBase::GuessedValue<QString>::operator const QString&, bpy::return_value_policy<bpy::copy_const_reference>())
;
bpy::enum_<IPluginList::PluginState>("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, boost::noncopyable>("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<void(const QString&, IPluginList::PluginStates)>& 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_<IModList::ModState>("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, boost::noncopyable>("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::reference_existing_object>(), bpy::arg("name"))
.def("removeMod", &MOBase::IModList::removeMod, bpy::arg("mod"))
.def("renameMod", &MOBase::IModList::renameMod, bpy::return_value_policy<bpy::reference_existing_object>(), (bpy::arg("mod"), bpy::arg("name")))
.def("state", &MOBase::IModList::state, bpy::arg("name"))
.def("setActive",
static_cast<int(IModList::*)(QStringList const&, bool)>(&MOBase::IModList::setActive), (bpy::arg("names"), "active"))
.def("setActive",
static_cast<bool(IModList::*)(QString const&, bool)>(&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<void(const QString&, IModList::ModStates)>& 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_<IPluginWrapper, boost::noncopyable>("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_<IPluginDiagnoseWrapper, bpy::bases<IPlugin>, 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>("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_<IPluginFileMapperWrapper, bpy::bases<IPlugin>, 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_<MOBase::IPluginGame::LoadOrderMechanism>("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_<MOBase::IPluginGame::SortMechanism>("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_<MOBase::IPluginGame::ProfileSetting>("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_<IPluginGameWrapper, bpy::bases<IPlugin>, 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<std::add_pointer_t, MpGameFeaturesList>
>([&](auto* pt) {
using T = std::remove_pointer_t<decltype(pt)>;
typename bpy::reference_existing_object::apply<T*>::type converter;
// Retrieve the python class object:
const bpy::converter::registration* registration = bpy::converter::registry::query(bpy::type_id<T>());
bpy::object key = bpy::object(bpy::handle<>(bpy::borrowed(registration->get_class_object())));
// Set the object:
dict[key] = bpy::handle<>(converter(p->feature<T>()));
});
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<std::add_pointer_t, MpGameFeaturesList>
>([&](auto* pt) {
using T = std::remove_pointer_t<decltype(pt)>;
typename bpy::reference_existing_object::apply<T*>::type converter;
// Retrieve the python class object:
const bpy::converter::registration* registration = bpy::converter::registry::query(bpy::type_id<T>());
if (clsObj.ptr() == (PyObject*) registration->get_class_object()) {
feature = bpy::object(bpy::handle<>(converter(p->feature<T>())));
}
});
return feature;
}, bpy::arg("feature_type"))
;
bpy::enum_<MOBase::IPluginInstaller::EInstallResult>("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_<IPluginInstaller, bpy::bases<IPlugin>, 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_<IPluginInstallerSimpleWrapper, bpy::bases<IPluginInstaller>, 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<QString>& modName, std::shared_ptr<IFileTree>& tree, QString& version, int& nexusID)
-> std::variant<IPluginInstaller::EInstallResult, std::shared_ptr<IFileTree>, std::tuple<IPluginInstaller::EInstallResult, std::shared_ptr<IFileTree>, 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<bpy::return_by_value>())
.def("_manager", &IPluginInstallerSimpleWrapper::manager, bpy::return_value_policy<bpy::reference_existing_object>())
;
bpy::class_<IPluginInstallerCustomWrapper, bpy::bases<IPluginInstaller>, 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<bpy::return_by_value>())
.def("_manager", &IPluginInstallerCustomWrapper::manager, bpy::return_value_policy<bpy::reference_existing_object>())
;
bpy::class_<IPluginModPageWrapper, bpy::bases<IPlugin>, 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::return_by_value>())
;
bpy::class_<IPluginPreviewWrapper, bpy::bases<IPlugin>, 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::return_by_value>(),
(bpy::arg("filename"), "max_size"))
;
bpy::class_<IPluginToolWrapper, bpy::bases<IPlugin>, 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<bpy::return_by_value>())
;
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();
}
/**
*
*/
class PythonRunner : public IPythonRunner
{
public:
PythonRunner();
~PythonRunner();
bool initPython();
QList<QObject*> load(const QString& identifier);
void unload(const QString& identifier);
bool isPythonInitialized() const;
bool isPythonVersionSupported() const;
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 <class T>
void appendIfInstance(bpy::object const& obj, QList<QObject*>& interfaces);
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<QString, std::vector<bpy::object>> m_PythonObjects;
wchar_t* m_PythonHome;
};
IPythonRunner* CreatePythonRunner()
{
std::unique_ptr<PythonRunner> result = std::make_unique<PythonRunner>();
if (result->initPython()) {
return result.release();
}
else {
return nullptr;
}
}
PythonRunner::PythonRunner()
{
m_PythonHome = new wchar_t[MAX_PATH + 1];
}
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")();
}
}
m_PythonObjects.clear();
}
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, boost::noncopyable>("PrintWrapper", bpy::init<>())
.def("write", &PrintWrapper::write);
bpy::class_<ErrWrapper, boost::noncopyable>("ErrWrapper", bpy::init<>())
.def("instance", &ErrWrapper::instance, bpy::return_value_policy<bpy::reference_existing_object>()).staticmethod("instance")
.def("write", &ErrWrapper::write)
.def("startRecordingExceptionMessage", &ErrWrapper::startRecordingExceptionMessage)
.def("stopRecordingExceptionMessage", &ErrWrapper::stopRecordingExceptionMessage)
.def("getLastExceptionMessage", &ErrWrapper::getLastExceptionMessage);
utils::register_functor_converter<bool(QString, bool)>();
// Expose a function to create a particular tree, only for debugging purpose, not in mobase.
bpy::def("makeTree", +[](std::function<bool(QString, bool)> callback) -> std::shared_ptr<IFileTree> {
struct FileTree : IFileTree {
using callback_t = std::function<bool(QString, bool)>;
FileTree(std::shared_ptr<const IFileTree> parent, QString name, callback_t callback) :
FileTreeEntry(parent, name), IFileTree(), m_Callback(callback){ }
std::shared_ptr<FileTreeEntry> addFile(QString name, bool) override {
if (m_Callback && !m_Callback(name, false)) {
throw UnsupportedOperationException("File rejected by callback.");
}
return IFileTree::addFile(name);
}
std::shared_ptr<IFileTree> addDirectory(QString name) override {
if (m_Callback && !m_Callback(name, true)) {
throw UnsupportedOperationException("Directory rejected by callback.");
}
return IFileTree::addDirectory(name);
}
protected:
std::shared_ptr<IFileTree> makeDirectory(std::shared_ptr<const IFileTree> parent, QString name) const override {
return std::make_shared<FileTree>(parent, name, m_Callback);
}
bool doPopulate(std::shared_ptr<const IFileTree> parent, std::vector<std::shared_ptr<FileTreeEntry>>& entries) const override { return true; }
std::shared_ptr<IFileTree> doClone() const override { return std::make_shared<FileTree>(nullptr, name(), m_Callback); }
private:
callback_t m_Callback;
};
return std::make_shared<FileTree>(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())
PyEval_SaveThread();
return false;
}
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");
}
if (PyGILState_Check())
PyEval_SaveThread();
return false;
}
}
bool handled_exec_file(bpy::str filename, bpy::object globals = bpy::object(), bpy::object locals = bpy::object())
{
return bpy::handle_exception(std::bind<bpy::object(&)(bpy::str, bpy::object, bpy::object)>(bpy::exec_file, filename, globals, locals));
}
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<bpy::list>(sys.attr("path"));
// Converting to QStringList for Qt::CaseInsensitive and because .index()
// raise an exception:
QStringList currentPath = bpy::extract<QStringList>(sysPath);
if (!currentPath.contains(folder, Qt::CaseInsensitive)) {
sysPath.insert(0, folder);
}
}
template <class T>
void PythonRunner::appendIfInstance(bpy::object const& obj, QList<QObject*> &interfaces) {
bpy::extract<T*> extr{ obj };
if (extr.check()) {
interfaces.append(extr);
}
}
QList<QObject*> PythonRunner::load(const QString& identifier)
{
GILock 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 {
// Dictionary that will contain createPlugin() or createPlugins().
bpy::dict moduleDict;
if (identifier.endsWith(".py")) {
bpy::object mainModule = bpy::import("__main__");
bpy::dict moduleNamespace = bpy::extract<bpy::dict>(mainModule.attr("__dict__"))();
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__"));
}
if (bpy::len(moduleDict) == 0) {
MOBase::log::error("Failed to import plugin from {}.", identifier);
throw pyexcept::PythonError();
}
// Create the plugins:
std::vector<bpy::object> 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]);
}
}
// Clear for future call
bpy::delitem(moduleDict, bpy::str("createPlugins"));
}
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<QObject*>();
}
QList<QObject*> allInterfaceList;
for (bpy::object pluginObj : plugins) {
// Add the plugin to keep it alive:
m_PythonObjects[identifier].push_back(pluginObj);
QList<QObject*> interfaceList;
appendIfInstance<IPluginGame>(pluginObj, interfaceList);
// Must try the wrapper because it's only a plugin extension interface in C++, so doesn't extend QObject
appendIfInstance<IPluginDiagnoseWrapper>(pluginObj, interfaceList);
// Must try the wrapper because it's only a plugin extension interface in C++, so doesn't extend QObject
appendIfInstance<IPluginFileMapperWrapper>(pluginObj, interfaceList);
appendIfInstance<IPluginInstallerCustom>(pluginObj, interfaceList);
appendIfInstance<IPluginInstallerSimple>(pluginObj, interfaceList);
appendIfInstance<IPluginModPage>(pluginObj, interfaceList);
appendIfInstance<IPluginPreview>(pluginObj, interfaceList);
appendIfInstance<IPluginTool>(pluginObj, interfaceList);
if (interfaceList.isEmpty()) {
appendIfInstance<IPluginWrapper>(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()) {
GILock lock;
if (!identifier.endsWith(".py")) {
// 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<bpy::dict>(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<QString>(mod.attr("__path__")[0]);
if (!folder.relativeFilePath(mpath).startsWith("..")) {
// If the path is under identifier, we need to unload it.
log::debug("Unloading module {} from {} for {}.", bpy::extract<QString>(keys[i])(), mpath, identifier);
bpy::delitem(modules, keys[i]);
}
}
}
}
// 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
{
return Py_IsInitialized() != 0;
}