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

2026 lines
91 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 "gamefeatureswrappers.h"
#include "proxypluginwrappers.h"
#include "sipApiAccess.h"
#include "uibasewrappers.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QWidget>
#include <Windows.h>
#include <utility.h>
#include <tuple>
#include <variant>
#ifndef Q_MOC_RUN
#include <boost/mp11.hpp>
#include <boost/python.hpp>
#endif
#include <fmt/format.h>
#include <fmt/xchar.h>
#include "converters.h"
#include "pylogger.h"
#include "shared_ptr_converter.h"
#include "tuple_helper.h"
#include "variant_helper.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;
}