diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b54ee37 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/pyind11"] + path = lib/pyind11 + url = https://github.com/pybind/pybind11 +[submodule "pybind11"] + path = pybind11 + url = https://github.com/pybind/pybind11 diff --git a/CMakeLists.txt b/CMakeLists.txt index d8520f9..bc5194c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,15 @@ else() include(${CMAKE_CURRENT_LIST_DIR}/../cmake_common/mo2.cmake) endif() +set(PYTHON_EXECUTABLE ${PYTHON_ROOT}/PCBuild/amd64/python.exe) +set(PYTHON_INCLUDE_DIR ${PYTHON_ROOT}/Include) +set(PYTHON_LIBRARY ${PYTHON_ROOT}/PCBuild/amd64/python38.lib) + +add_subdirectory(pybind11) + project(plugin_python) # order matters! -add_subdirectory(src/runner) +# add_subdirectory(src/runner) +add_subdirectory(src/runner-pybind11) add_subdirectory(src/proxy) diff --git a/pybind11 b/pybind11 new file mode 160000 index 0000000..e8e229f --- /dev/null +++ b/pybind11 @@ -0,0 +1 @@ +Subproject commit e8e229fa0b486118bf91321b923f8158055c053c diff --git a/src/proxy/proxypython.cpp b/src/proxy/proxypython.cpp index b5656a3..65308fd 100644 --- a/src/proxy/proxypython.cpp +++ b/src/proxy/proxypython.cpp @@ -77,18 +77,18 @@ bool ProxyPython::init(IOrganizer *moInfo) } if (m_MOInfo && m_MOInfo->persistent(name(), "tryInit", false).toBool()) { - m_LoadFailure = FailureType::INITIALIZATION; - if (QMessageBox::question(parentWidget(), tr("Python Initialization failed"), - tr("On a previous start the Python Plugin failed to initialize.\n" - "Do you want to try initializing python again (at the risk of another crash)?\n" - "Suggestion: Select \"no\", and click the warning sign for further help. Afterwards you have to re-enable the python plugin."), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) { + // m_LoadFailure = FailureType::INITIALIZATION; + // if (QMessageBox::question(parentWidget(), tr("Python Initialization failed"), + // tr("On a previous start the Python Plugin failed to initialize.\n" + // "Do you want to try initializing python again (at the risk of another crash)?\n" + // "Suggestion: Select \"no\", and click the warning sign for further help. Afterwards you have to re-enable the python plugin."), + // QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) { - // we force enabled here (note: this is a persistent settings since MO2 2.4 or something), plugin - // usually should not handle enabled/disabled themselves but this is a base plugin so... - m_MOInfo->setPersistent(name(), "enabled", false, true); - return true; - } + // // we force enabled here (note: this is a persistent settings since MO2 2.4 or something), plugin + // // usually should not handle enabled/disabled themselves but this is a base plugin so... + // m_MOInfo->setPersistent(name(), "enabled", false, true); + // return true; + // } } if (m_MOInfo) { @@ -151,12 +151,15 @@ QStringList ProxyPython::pluginList(const QDir& pluginPath) const QString name = iter.next(); QFileInfo info = iter.fileInfo(); - - if (info.isFile() && name.endsWith(".py")) { + if (info.fileName() == "test.py") { result.append(name); } + + if (info.isFile() && name.endsWith(".py")) { + // result.append(name); + } else if (info.isDir() && QDir(info.absoluteFilePath()).exists("__init__.py")) { - result.append(name); + // result.append(name); } } diff --git a/src/runner-pybind11/CMakeLists.txt b/src/runner-pybind11/CMakeLists.txt new file mode 100644 index 0000000..f3c8867 --- /dev/null +++ b/src/runner-pybind11/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.16) + +# need to find Boost here with a dummy component to get Boost_LIBRARY_DIRS +find_package(Boost COMPONENTS thread REQUIRED) + +pybind11_add_module(pythonrunner SHARED) +mo2_configure_library(pythonrunner + WARNINGS OFF + AUTOMOC ON + TRANSLATIONS ON + PRIVATE_DEPENDS uibase boost Qt::Core +) +set_target_properties(pythonrunner + PROPERTIES + OUTPUT_NAME "pythonrunner" + PREFIX "" + SUFFIX ".dll") +# target_link_libraries(pythonrunner PRIVATE pybind11::embed) +# target_include_directories(pythonrunner PRIVATE ${PYTHON_ROOT}/Include) + +# this is kind of broken but it only works with this... +target_link_directories(pythonrunner + PRIVATE + ${MO2_INSTALL_LIBS_PATH} + ${Boost_LIBRARY_DIRS}) +target_compile_definitions(pythonrunner + PRIVATE QT_NO_KEYWORDS PYTHONRUNNER_LIBRARY) +mo2_install_target(pythonrunner INSTALLDIR bin/plugins/data) + +mo2_add_filter(NAME src/converters GROUPS + converters + pythonutils + shared_ptr_converter + tuple_helper + variant_helper +) + +mo2_add_filter(NAME src/runner GROUPS + pythonrunner + pylogger + widgets +) + +mo2_add_filter(NAME src/utils GROUPS + error + gilock + sipapiaccess +) + +mo2_add_filter(NAME src/wrappers GROUPS + gamefeatureswrappers + proxypluginwrappers + pythonwrapperutilities + uibasewrappers +) diff --git a/src/runner-pybind11/converters-old.h b/src/runner-pybind11/converters-old.h new file mode 100644 index 0000000..3759864 --- /dev/null +++ b/src/runner-pybind11/converters-old.h @@ -0,0 +1,581 @@ +#ifndef PYTHON_CONVERTERS_OLD_HPP +#define PYTHON_CONVERTERS_OLD_HPP + +#include +#include +#include +#include +#include +#include + +// sip and qt slots seems to conflict +#include + +// Include the container converters from utils: +#include "pythonutils.h" + +namespace utils { + + namespace bpy = boost::python; + + namespace QString_converter { + + /** + * We need this since sip does not expose QString but uses standard python str. + */ + struct QString_to_python_str + { + static PyObject* convert(const QString& str) { + // It's safer to explicitly convert to unicode as if we don't, this can return + // either str or unicode without it being easy to know which to expect + bpy::object pyStr = bpy::object(qUtf8Printable(str)); + if (SIPBytes_Check(pyStr.ptr())) + pyStr = pyStr.attr("decode")("utf-8"); + return bpy::incref(pyStr.ptr()); + } + }; + + struct QString_from_python_str + { + + static void* convertible(PyObject* objPtr) { + return SIPBytes_Check(objPtr) || PyUnicode_Check(objPtr) ? objPtr : nullptr; + } + + static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { + // Ensure the string uses 8-bit characters + PyObject* strPtr = PyUnicode_Check(objPtr) ? PyUnicode_AsUTF8String(objPtr) : objPtr; + + // Extract the character data from the python string + const char* value = SIPBytes_AsString(strPtr); + assert(value != nullptr); + + // allocate storage + void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; + + // construct QString in the allocated memory + new (storage) QString(value); + + data->convertible = storage; + + // Deallocate local copy if one was made + if (strPtr != objPtr) + Py_DecRef(strPtr); + } + }; + + } + + namespace Enum_converter { + + /** + * + */ + template + struct Enum_to_int + { + static PyObject* convert(const Enum& flags) { + return bpy::incref(bpy::object(static_cast(flags)).ptr()); + } + }; + + template + struct Enum_from_python_obj + { + + static void* convertible(PyObject* objPtr) { + return SIPLong_Check(objPtr) ? objPtr : nullptr; + } + + static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { + int intVersion = (int)SIPLong_AsLong(objPtr); + void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; + new (storage) Enum(static_cast(intVersion)); + data->convertible = storage; + } + }; + + } + + namespace QFlags_converter { + + /** + * + */ + template + struct QFlags_to_int + { + static PyObject* convert(const QFlags& flags) { + return bpy::incref(bpy::object(static_cast(flags)).ptr()); + } + }; + + template + struct QFlags_from_python_obj + { + + static void* convertible(PyObject* objPtr) { + return SIPLong_Check(objPtr) ? objPtr : nullptr; + } + + static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { + int intVersion = (int)SIPLong_AsLong(objPtr); + T tVersion = (T)intVersion; + void* storage = ((bpy::converter::rvalue_from_python_storage>*)data)->storage.bytes; + new (storage) QFlags(tVersion); + + data->convertible = storage; + } + }; + + } + + namespace QVariant_converter { + + struct QVariant_to_python_obj + { + static PyObject* convert(const QVariant& var) { + switch (var.type()) { + case QVariant::Invalid: return bpy::incref(Py_None); + case QVariant::Int: return SIPLong_FromLong(var.toInt()); + case QVariant::UInt: return PyLong_FromUnsignedLong(var.toUInt()); + case QVariant::Bool: return PyBool_FromLong(var.toBool()); + case QVariant::String: return bpy::incref(bpy::object(var.toString()).ptr()); + // We need to check for StringList here because these are not considered List + // since List is QList will StringList is QList: + case QVariant::StringList: return bpy::incref(bpy::object(var.toStringList()).ptr()); + case QVariant::List: { + return bpy::incref(bpy::object(var.toList()).ptr()); + } break; + case QVariant::Map: { + return bpy::incref(bpy::object(var.toMap()).ptr()); + } break; + default: { + PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.type()); + throw bpy::error_already_set(); + } break; + } + } + }; + + struct QVariant_from_python_obj + { + + static void* convertible(PyObject* objPtr) { + if (!SIPBytes_Check(objPtr) && !PyUnicode_Check(objPtr) && !PyLong_Check(objPtr) && + !PyBool_Check(objPtr) && !PyList_Check(objPtr) && !PyDict_Check(objPtr) && + objPtr != Py_None) { + return nullptr; + } + return objPtr; + } + + template + static void constructVariant(const T& value, bpy::converter::rvalue_from_python_stage1_data* data) { + void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; + + new (storage) QVariant(value); + + data->convertible = storage; + } + + static void constructVariant(bpy::converter::rvalue_from_python_stage1_data* data) { + void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; + + new (storage) QVariant(); + + data->convertible = storage; + } + + static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { + if (PyList_Check(objPtr)) { + // We could check if all the elements can be converted to QString and store a QStringList + // in the QVariant but I am not sure that is really useful. + constructVariant(bpy::extract(objPtr)(), data); + } + else if (objPtr == Py_None) { + constructVariant(data); + } + else if (PyDict_Check(objPtr)) { + constructVariant(bpy::extract(objPtr)(), data); + } + else if (SIPBytes_Check(objPtr) || PyUnicode_Check(objPtr)) { + constructVariant(bpy::extract(objPtr)(), data); + } + // PyBools will also return true for SIPLong_Check but not the other way around, so the order + // here is relevant. + else if (PyBool_Check(objPtr)) { + constructVariant(bpy::extract(objPtr)(), data); + } + else if (SIPLong_Check(objPtr)) { + // QVariant doesn't have long. It has int or long long. Given that on m/s, + // long is 32 bits for 32- and 64- bit code... + constructVariant(bpy::extract(objPtr)(), data); + } + else { + PyErr_SetString(PyExc_TypeError, "type unsupported"); + throw bpy::error_already_set(); + } + } + }; + + } + + namespace QClass_converter { + + template struct MetaData; + + template <> struct MetaData { static const char* className() { return "QObject"; } }; + template <> struct MetaData { static const char* className() { return "QWidget"; } }; + template <> struct MetaData { static const char* className() { return "QMainWindow"; } }; + template <> struct MetaData { static const char* className() { return "QDateTime"; } }; + template <> struct MetaData { static const char* className() { return "QDir"; } }; + template <> struct MetaData { static const char* className() { return "QFileInfo"; } }; + template <> struct MetaData { static const char* className() { return "QIcon"; } }; + template <> struct MetaData { static const char* className() { return "QSize"; } }; + template <> struct MetaData { static const char* className() { return "QUrl"; } }; + template <> struct MetaData { static const char* className() { return "QVariant"; } }; + + template + struct QClass_converters + { + struct QClass_to_PyQt + { + template + static typename std::enable_if_t, T*> getSafeCopy(T* qClass) + { + return new T(*qClass); + } + + template + static typename std::enable_if_t, T*> getSafeCopy(T* qClass) + { + return qClass; + } + + static PyObject* convert(const T& object) { + const sipTypeDef* type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); + if (type == nullptr) { + return bpy::incref(Py_None); + } + + PyObject* sipObj = sipAPIAccess::sipAPI()->api_convert_from_type((void*)getSafeCopy((T*)&object), type, 0); + if (sipObj == nullptr) { + return bpy::incref(Py_None); + } + + if (std::is_copy_constructible_v) + // Ensure Python deletes the C++ component + sipAPIAccess::sipAPI()->api_transfer_back(sipObj); + + return bpy::incref(sipObj); + } + + static PyObject* convert(T* object) { + if (object == nullptr) { + return bpy::incref(Py_None); + } + + const sipTypeDef* type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); + if (type == nullptr) { + return bpy::incref(Py_None); + } + + PyObject* sipObj = sipAPIAccess::sipAPI()->api_convert_from_type(getSafeCopy(object), type, 0); + if (sipObj == nullptr) { + return bpy::incref(Py_None); + } + + if (std::is_copy_constructible_v) + // Ensure Python deletes the C++ component + sipAPIAccess::sipAPI()->api_transfer_back(sipObj); + + return bpy::incref(sipObj); + } + + static PyObject* convert(const T* object) { + return convert((T*)object); + } + + static PyTypeObject const* get_pytype() { + const sipTypeDef* type = sipAPIAccess::sipAPI()->api_find_type(MetaData::className()); + if (type == nullptr) { + return bpy::incref(Py_None); + } + return bpy::incref(type->td_py_type); + } + }; + + static void* QClass_from_PyQt(PyObject* objPtr) + { + // This would transfer responsibility for deconstructing the object to C++, but Boost assumes l-value converters (such as this) don't do that + // Instead, this should be called within the wrappers for functions which return deletable pointers. + //sipAPI()->api_transfer_to(objPtr, Py_None); + if (PyObject_TypeCheck(objPtr, sipAPIAccess::sipAPI()->api_simplewrapper_type)) { + sipSimpleWrapper* wrapper; + wrapper = reinterpret_cast(objPtr); + return wrapper->data; + } + else if (PyObject_TypeCheck(objPtr, sipAPIAccess::sipAPI()->api_wrapper_type)) { + sipWrapper* wrapper; + wrapper = reinterpret_cast(objPtr); + return wrapper->super.data; + } + return nullptr; + } + }; + + } + + namespace details { + + inline bool has_arity(PyObject* object, std::size_t arity) { + // Mostly from https://stackoverflow.com/a/36143796/2666289 + bpy::object fn(bpy::handle<>(bpy::borrowed(object))); + + auto inspect = bpy::import("inspect"); + auto arg_spec = inspect.attr("getfullargspec")(fn); + bpy::object args = arg_spec.attr("args"), + varargs = arg_spec.attr("varargs"), + defaults = arg_spec.attr("defaults"); + + auto args_count = args ? bpy::len(args) : 0; + auto defaults_count = defaults ? bpy::len(defaults) : 0; + + if (static_cast(inspect.attr("ismethod")(fn)) && fn.attr("__self__")) { + --args_count; + } + + auto required_count = args_count - defaults_count; + + return required_count <= arity // Cannot require more parameters than given, + && (args_count >= arity || varargs); // Must accept enough parameters. + } + + template + struct wrap_impl; + + template <> + struct wrap_impl<> { + template + static decltype(auto) apply(T&& t) { return std::forward(t); } + }; + + template + struct wrap_impl, Ws... > : public wrap_impl<> { + using wrap_impl<>::apply; + + static auto apply(T t) { + return boost::ref(t); + } + }; + + template + struct wrap_impl, Ws... > : public wrap_impl<> { + using wrap_impl<>::apply; + + static auto apply(T t) { + return bpy::ptr(t); + } + }; + + template + struct wrap_impl { + template + static decltype(auto) apply(T&& t) { + return wrap::apply(std::forward(t)); + } + }; + + } + + /** + * @brief Convert a python callable to a valid C++ Callable object. Also works + * for None. + */ + template + struct Functor_converter; + + template + struct Functor_converter + { + + template + static decltype(auto) wrap(T&& t) { + return details::wrap_impl::apply(std::forward(t)); + } + + struct FunctorWrapper + { + FunctorWrapper(boost::python::object callable) : m_Callable(callable) { + } + + ~FunctorWrapper() { + GILock lock; + m_Callable = bpy::object(); + } + + R operator()(Args... params) { + GILock lock; + try { + if constexpr (std::is_same_v) { + m_Callable(wrap(params)... ); + } + else { + return bpy::extract(m_Callable(wrap(params)... )); + } + } + catch (const boost::python::error_already_set&) { + throw pyexcept::PythonError(); + } + catch (...) { + throw pyexcept::UnknownException(); + } + } + + boost::python::object m_Callable; + }; + + static void* convertible(PyObject* object) + { + // We allow None here, we will just default-construct a std::function: + if (object == Py_None) { + return object; + } + + // Otherwize we check that we have a callable object: + if (!PyCallable_Check(object) || !details::has_arity(object, sizeof...(Args))) { + return nullptr; + } + return object; + } + + static void construct(PyObject* object, bpy::converter::rvalue_from_python_stage1_data* data) + { + bpy::object callable(bpy::handle<>(bpy::borrowed(object))); + void* storage =((bpy::converter::rvalue_from_python_storage>*)data)->storage.bytes; + if (callable.is_none()) { + new (storage) std::function{}; + } + else { + new (storage) std::function(FunctorWrapper(callable)); + } + data->convertible = storage; + } + }; + + + + /** + * @brief Call policy that automatically downcast shared pointer of type FromType + * to shared pointer of type ToType. + */ + template + struct DowncastConverter { + + bool convertible() const { return true; } + + inline PyObject* operator()(std::shared_ptr p) const { + if (p == nullptr) { + return bpy::detail::none(); + } + else { + auto downcast_p = std::dynamic_pointer_cast(p); + bpy::object p_value = downcast_p == nullptr ? bpy::object{ p } : bpy::object{ downcast_p }; + return bpy::incref(p_value.ptr()); + } + } + + inline PyTypeObject const* get_pytype() const { + return bpy::converter::registered_pytype::get_pytype(); + } + + }; + + template + struct downcast_return { + + template + struct apply_; + + template + struct apply_> { + static_assert(std::is_convertible_v, std::shared_ptr>); + using type = DowncastConverter; + }; + + template + using apply = apply_>; + + }; + + // Functions: + inline void register_qstring_converter() { + using namespace QString_converter; + bpy::to_python_converter(); + bpy::converter::registry::push_back( + &QString_from_python_str::convertible, + &QString_from_python_str::construct, + bpy::type_id()); + } + + inline void register_qvariant_converter() { + using namespace QVariant_converter; + bpy::to_python_converter(); + bpy::converter::registry::push_back( + &QVariant_from_python_obj::convertible, + &QVariant_from_python_obj::construct, + bpy::type_id()); + } + + template + inline void register_qflags_converter() { + using T = typename Flags::enum_type; + using namespace QFlags_converter; + bpy::to_python_converter>(); + bpy::converter::registry::push_back( + &QFlags_from_python_obj::convertible, + &QFlags_from_python_obj::construct, + bpy::type_id()); + } + + template + inline void register_enum_converter() { + using namespace Enum_converter; + bpy::to_python_converter>(); + bpy::converter::registry::push_back( + &Enum_from_python_obj::convertible, + &Enum_from_python_obj::construct, + bpy::type_id()); + } + + template + inline void register_qclass_converter() { + using Converter = QClass_converter::QClass_converters; + bpy::converter::registry::insert(&Converter::QClass_from_PyQt, bpy::type_id()); + bpy::to_python_converter(); + bpy::to_python_converter(); + bpy::to_python_converter(); + } + + /** + * @brief Register a functor converter. + * + * @tparam Fn The function type to register. + * @tparam Wrappers... A list of wrapper (boost::python::pointer_wrapper or boost::reference_wrapper) + * indicating if parameters of the given (wrapped) type must be wrapped. + */ + template + inline void register_functor_converter() { + using Converter = Functor_converter; + bpy::converter::registry::push_back( + &Converter::convertible, + &Converter::construct, + bpy::type_id>()); + } + + + +} + +#endif diff --git a/src/runner-pybind11/converters.h b/src/runner-pybind11/converters.h new file mode 100644 index 0000000..c2005a6 --- /dev/null +++ b/src/runner-pybind11/converters.h @@ -0,0 +1,8 @@ +#ifndef PYTHON_CONVERTERS_HPP +#define PYTHON_CONVERTERS_HPP + +#include +#include "converters_qt.h" +#include "converters_qt_sip.h" + +#endif diff --git a/src/runner-pybind11/converters_qt.h b/src/runner-pybind11/converters_qt.h new file mode 100644 index 0000000..7948c69 --- /dev/null +++ b/src/runner-pybind11/converters_qt.h @@ -0,0 +1,296 @@ +#ifndef PYTHON_CONVERTERS_QT_HPP +#define PYTHON_CONVERTERS_QT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "converters_utils.h" + +// for enum casters +namespace mo2::details { + template <> + struct enum_type_name { + constexpr static const char name[] = "QMessageBox.StandardButton"; + }; + + template <> + struct enum_type_name { + constexpr static const char name[] = "QMessageBox.Icon"; + }; +} + +namespace pybind11::detail { + + // helper class for QMap because QMap do not follow the standard std:: maps interface, + // for other containers, the pybind11 built-in xxx_caster works + // + // this code is basically a copy/paste from the pybind11 stl stuff with minor + // modifications + // + template + struct qmap_caster { + using key_conv = make_caster; + using value_conv = make_caster; + + bool load(handle src, bool convert) { + if (!isinstance(src)) { + return false; + } + auto d = reinterpret_borrow(src); + value.clear(); + for (auto it : d) { + key_conv kconv; + value_conv vconv; + if (!kconv.load(it.first.ptr(), convert) || !vconv.load(it.second.ptr(), convert)) { + return false; + } + value[cast_op(std::move(kconv))] = cast_op(std::move(vconv)); + } + return true; + } + + template + static handle cast(T &&src, return_value_policy policy, handle parent) { + dict d; + return_value_policy policy_key = policy; + return_value_policy policy_value = policy; + if (!std::is_lvalue_reference::value) { + policy_key = return_value_policy_override::policy(policy_key); + policy_value = return_value_policy_override::policy(policy_value); + } + for (auto it = src.begin(); it != src.end(); ++it) { + auto key = reinterpret_steal( + key_conv::cast(forward_like(it.key()), policy_key, parent)); + auto value = reinterpret_steal( + value_conv::cast(forward_like(it.value()), policy_value, parent)); + if (!key || !value) { + return handle(); + } + d[key] = value; + } + return d.release(); + } + + PYBIND11_TYPE_CASTER(Type, + const_name("Dict[") + key_conv::name + const_name(", ") + value_conv::name + + const_name("]")); + }; + + // QString + // + template <> + struct type_caster { + PYBIND11_TYPE_CASTER(QString, const_name("str")); + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a QString + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool load(handle src, bool) { + + PyObject *objPtr = src.ptr(); + + if (!PyBytes_Check(objPtr) && !PyUnicode_Check(objPtr)) { + return false; + } + + // Ensure the string uses 8-bit characters + PyObject *strPtr = PyUnicode_Check(objPtr) ? PyUnicode_AsUTF8String(objPtr) : objPtr; + + // Extract the character data from the python string + value = QString::fromUtf8(PyBytes_AsString(strPtr)); + + // Deallocate local copy if one was made + if (strPtr != objPtr) { + Py_DecRef(strPtr); + } + + return true; + } + + /** + * Conversion part 2 (C++ -> Python): convert an QString instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + static handle cast(QString src, return_value_policy /* policy */, handle /* parent */) { + static_assert(sizeof(QChar) == 2); + return PyUnicode_FromKindAndData(PyUnicode_2BYTE_KIND, src.constData(), src.length()); + } + }; + + // QVariant - this needs to be defined BEFORE QVariantList + // + template <> + struct type_caster { + public: + PYBIND11_TYPE_CASTER(QVariant, const_name("MOVariant")); + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a QVariant + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool load(handle src, bool); + + /** + * Conversion part 2 (C++ -> Python): convert an QString instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + static handle cast(QVariant var, return_value_policy policy, handle parent); + }; + + // QList + // + template + struct type_caster> : list_caster, T> {}; + + // QSet + // + template + struct type_caster> : set_caster, T> {}; + + // QMap + // + template + struct type_caster> : qmap_caster, K, V> {}; + + // QStringList + // + template <> + struct type_caster : list_caster {}; + + // QVariantList + // + template <> + struct type_caster : list_caster {}; + + // QVariantMap + // + template <> + struct type_caster : qmap_caster {}; + + bool type_caster::load(handle src, bool implicit) { + if (PyList_Check(src.ptr())) { + // we could check if all the elements can be converted to QString and store a QStringList + // in the QVariant but I am not sure that is really useful. + value = src.cast(); + return true; + } + else if (src == Py_None) { + value = QVariant(); + return true; + } + else if (PyDict_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (PyBytes_Check(src.ptr()) || PyUnicode_Check(src.ptr())) { + value = src.cast(); + return true; + } + // PyBool will also return true for PyLong_Check but not the other way around, so + // the order here is relevant. + else if (PyBool_Check(src.ptr())) { + value = src.cast(); + return true; + } + else if (PyLong_Check(src.ptr())) { + // QVariant doesn't have long. It has int or long long. Given that on m/s, + // long is 32 bits for 32- and 64- bit code... + value = src.cast(); + return true; + } + else { + return false; + } + } + + handle type_caster::cast(QVariant var, return_value_policy policy, handle parent) { + switch (var.type()) { + case QVariant::Invalid: return Py_None; + case QVariant::Int: return PyLong_FromLong(var.toInt()); + case QVariant::UInt: return PyLong_FromUnsignedLong(var.toUInt()); + case QVariant::Bool: return PyBool_FromLong(var.toBool()); + case QVariant::String: return type_caster::cast(var.toString(), policy, parent); + // We need to check for StringList here because these are not considered List + // since List is QList will StringList is QList: + case QVariant::StringList: return type_caster::cast(var.toStringList(), policy, parent); + case QVariant::List: return type_caster::cast(var.toList(), policy, parent); + case QVariant::Map: // return type_caster::cast(var.toList(), policy, parent); + default: { + PyErr_Format(PyExc_TypeError, "type unsupported: %d", var.type()); + throw pybind11::error_already_set(); + } + } + } + + // QMessageBox's enums + // + template <> + struct type_caster : + mo2::details::enum_type_caster {}; + template <> + struct type_caster : + mo2::details::enum_type_caster {}; + + // QFlags + // + template + struct type_caster> { + PYBIND11_TYPE_CASTER(QFlags, const_name("QFlags[") + make_caster::name + const_name("]")); + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a QString + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool load(handle src, bool implicit) { + PyObject* tmp = PyNumber_Long(src.ptr()); + + if (!tmp) { + return false; + } + + // we do an intermediate extraction to T but this actually + // can contains multiple values + T flag_value = static_cast(PyLong_AsLong(tmp)); + Py_DECREF(tmp); + + value = QFlags(flag_value); + + return !PyErr_Occurred(); + } + + /** + * Conversion part 2 (C++ -> Python): convert an QString instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + static handle cast(QFlags const& src, return_value_policy /* policy */, handle /* parent */) { + return PyLong_FromLong(static_cast(src)); + } + + }; + + +} // namespace pybind11::detail + +#endif diff --git a/src/runner-pybind11/converters_qt_sip.h b/src/runner-pybind11/converters_qt_sip.h new file mode 100644 index 0000000..f123616 --- /dev/null +++ b/src/runner-pybind11/converters_qt_sip.h @@ -0,0 +1,144 @@ +#ifndef PYTHON_CONVERTERS_QT_SIP_HPP +#define PYTHON_CONVERTERS_QT_SIP_HPP + +#include "sipapiaccess.h" + +namespace mo2::details { + + template struct MetaData; + + template + struct MetaData>> : + MetaData> {}; + +#define METADATA(QClass) template <> struct MetaData { \ + constexpr static const char name[] = #QClass; } + + METADATA(QObject); + METADATA(QWidget); + METADATA(QMainWindow); + METADATA(QDateTime); + METADATA(QDir); + METADATA(QFileInfo); + METADATA(QIcon); + METADATA(QSize); + METADATA(QUrl); + METADATA(QPixmap); + +#undef METADATA + + // template class for most Qt types that have Python equivalent (QWidget, etc.) + // + template + struct qt_type_caster { + public: + PYBIND11_TYPE_CASTER(QtType, pybind11::detail::const_name(MetaData::name)); + + bool load(pybind11::handle src, bool) { + // this would transfer responsibility for deconstructing the object to C++, + // but pybind11 assumes l-value converters (such as this) don't do that + // instead, this should be called within the wrappers for functions which return + // deletable pointers. + // + // sipAPI()->api_transfer_to(objPtr, Py_None); + // + void* data = nullptr; + if (PyObject_TypeCheck(src.ptr(), sipAPIAccess::sipAPI()->api_simplewrapper_type)) { + data = reinterpret_cast(src.ptr())->data; + } + else if (PyObject_TypeCheck(src.ptr(), sipAPIAccess::sipAPI()->api_wrapper_type)) { + data = reinterpret_cast(src.ptr())->super.data; + } + + if (data) { + if constexpr (std::is_pointer_v) { + value = reinterpret_cast(data); + } + else { + value = *reinterpret_cast(data); + } + return true; + } + else { + return false; + } + } + + static pybind11::handle cast( + QtType src, pybind11::return_value_policy /* policy */, pybind11::handle /* parent */) { + + const sipTypeDef* type = + sipAPIAccess::sipAPI()->api_find_type(MetaData::name); + if (type == nullptr) { + return Py_None; + } + + PyObject* sipObj; + void* sipData; + + if constexpr (std::is_pointer_v) { + sipData = src; + } else if (std::is_copy_assignable_v) { + // we send to SIP a newly allocated object, and transfer the owernship to it + sipData = new QtType(src); + } + else { + sipData = &src; + } + + if constexpr (std::is_pointer_v) { + sipObj = sipAPIAccess::sipAPI()->api_convert_from_type(sipData, type, 0); + } + else { + sipObj = sipAPIAccess::sipAPI()->api_convert_from_type(sipData, type, 0); + } + + if (sipObj == nullptr) { + return Py_None; + } + + if constexpr (!std::is_pointer_v && std::is_copy_constructible_v) { + // ensure Python deletes the C++ component + sipAPIAccess::sipAPI()->api_transfer_back(sipObj); + } + + return sipObj; + } + }; + +} + +namespace pybind11::detail { + + template <> + struct type_caster : mo2::details::qt_type_caster {}; + + template <> + struct type_caster : mo2::details::qt_type_caster {}; + + template <> + struct type_caster : mo2::details::qt_type_caster {}; + + template <> + struct type_caster : mo2::details::qt_type_caster {}; + + template <> + struct type_caster : mo2::details::qt_type_caster {}; + + template <> + struct type_caster : mo2::details::qt_type_caster {}; + template <> + struct type_caster : mo2::details::qt_type_caster {}; + + template <> + struct type_caster : mo2::details::qt_type_caster {}; + + template <> + struct type_caster : mo2::details::qt_type_caster {}; + + template <> + struct type_caster : mo2::details::qt_type_caster {}; + +} // namespace pybind11::detail + +#endif diff --git a/src/runner-pybind11/converters_utils.h b/src/runner-pybind11/converters_utils.h new file mode 100644 index 0000000..723893e --- /dev/null +++ b/src/runner-pybind11/converters_utils.h @@ -0,0 +1,41 @@ +#ifndef PYTHON_CONVERTERS_UTILS_HPP +#define PYTHON_CONVERTERS_UTILS_HPP + +#include + +namespace mo2::details { + + template + struct enum_type_name; + + // helper caster for enum-like types + // + template + struct enum_type_caster { + static_assert(std::is_enum_v, + "enum_type_caster should only be used with enum types"); + + PYBIND11_TYPE_CASTER(E, pybind11::detail::const_name(enum_type_name::name)); + + bool load(pybind11::handle src, bool) { + PyObject* tmp = PyNumber_Long(src.ptr()); + + if (!tmp) { + return false; + } + + value = static_cast(PyLong_AsLong(tmp)); + Py_DECREF(tmp); + + return !PyErr_Occurred(); + } + + static pybind11::handle cast(E src, pybind11::return_value_policy, pybind11::handle) { + return PyLong_FromLongLong(static_cast(src)); + } + + }; + +} + +#endif diff --git a/src/runner-pybind11/error.cpp b/src/runner-pybind11/error.cpp new file mode 100644 index 0000000..2be1906 --- /dev/null +++ b/src/runner-pybind11/error.cpp @@ -0,0 +1,48 @@ +#ifndef Q_MOC_RUN +#include +#endif +#include +#include +#include "error.h" + +using namespace MOBase; +namespace bpy = boost::python; + +ErrWrapper & ErrWrapper::instance() +{ + static ErrWrapper err; + return err; +} + +void ErrWrapper::write(const char * message) +{ + buffer << message; + if (buffer.tellp() != 0 && buffer.str().back() == '\n') + { + // actually put the string in a variable so it doesn't get destroyed as soon as we get a pointer to its data + std::string string = buffer.str().substr(0, buffer.str().length() - 1); + qCritical().nospace().noquote() << string.c_str(); + buffer = std::stringstream(); + } + + if (recordingExceptionMessage) + { + lastException << message; + } +} + +void ErrWrapper::startRecordingExceptionMessage() +{ + recordingExceptionMessage = true; + lastException = std::stringstream(); +} + +void ErrWrapper::stopRecordingExceptionMessage() +{ + recordingExceptionMessage = false; +} + +QString ErrWrapper::getLastExceptionMessage() +{ + return QString::fromStdString(lastException.str()); +} diff --git a/src/runner-pybind11/error.h b/src/runner-pybind11/error.h new file mode 100644 index 0000000..89d0036 --- /dev/null +++ b/src/runner-pybind11/error.h @@ -0,0 +1,123 @@ +#ifndef ERROR_H +#define ERROR_H + +#include + +#include + +#include +#include + +struct ErrWrapper +{ + static ErrWrapper& instance(); + + void write(const char* message); + + void startRecordingExceptionMessage(); + + void stopRecordingExceptionMessage(); + + QString getLastExceptionMessage(); + + std::stringstream buffer; + bool recordingExceptionMessage; + std::stringstream lastException; +}; + +namespace pyexcept { + + /** + * @brief Exception to throw when a python implementation does not implement + * a pure virtual function. + */ + class MissingImplementation : public MOBase::Exception { + public: + MissingImplementation(std::string const& className, std::string const& methodName) : + Exception(QString::fromStdString( + fmt::format("Python class implementing \"{}\" has no implementation of method \"{}\".", + className, methodName))) { } + + }; + + /** + * @brief Exception to throw when a python error occurs. + */ + class PythonError : public MOBase::Exception { + public: + + /** + * @brief Create a new PythonError, fetching the error message from python. If the message + * cannot be retrieved, `defaultErrorMessage()` is used instead. + */ + PythonError() : Exception(getPythonErrorMessage()) { } + + /** + * @brief Create a new PythonError with the given message. + * + * @param message Message for the exception. + */ + PythonError(QString message) : Exception(message) { } + + protected: + + /** + * + */ + static QString defaultErrorMessage() { + return QObject::tr("An unexpected C++ exception was thrown in python code."); + } + + /** + * + */ + static QString getPythonErrorMessage() { + if (PyErr_Occurred()) { + ErrWrapper& errWrapper = ErrWrapper::instance(); + + errWrapper.startRecordingExceptionMessage(); + PyErr_Print(); + errWrapper.stopRecordingExceptionMessage(); + + return errWrapper.getLastExceptionMessage(); + } + else { + return defaultErrorMessage(); + } + } + }; + + /** + * @brief Exception to throw when an unknown error occured. This is typically thrown + * from a catch(...) block. + */ + class UnknownException : public MOBase::Exception { + public: + + /** + * @brief Create a new UnknownException with the default message. + * + * @see defaultErrorMessage + */ + UnknownException() : Exception(defaultErrorMessage()) { } + + /** + * @brief Create a new UnknownException with the given message. + * + * @param message Message for the exception. + */ + UnknownException(QString message) : Exception(message) { } + + protected: + + /** + * + */ + static QString defaultErrorMessage() { + return QObject::tr("An unknown exception was thrown in python code."); + } + }; + +} + +#endif // ERROR_H diff --git a/src/runner-pybind11/gamefeatureswrappers.cpp b/src/runner-pybind11/gamefeatureswrappers.cpp new file mode 100644 index 0000000..cbfa5cc --- /dev/null +++ b/src/runner-pybind11/gamefeatureswrappers.cpp @@ -0,0 +1,344 @@ +#include "gamefeatureswrappers.h" + +#include +#include + +#include +#include +#include +#include + +#include "shared_ptr_converter.h" +#include "ifiletree.h" +#include "pythonwrapperutilities.h" + +///////////////////////////// +/// BSAInvalidation Wrapper + + +bool BSAInvalidationWrapper::isInvalidationBSA(const QString &bsaName) +{ + return basicWrapperFunctionImplementation(this, "isInvalidationBSA", bsaName); +} + +void BSAInvalidationWrapper::deactivate(MOBase::IProfile *profile) +{ + return basicWrapperFunctionImplementation(this, "deactivate", boost::python::ptr(profile)); +} + +void BSAInvalidationWrapper::activate(MOBase::IProfile *profile) +{ + return basicWrapperFunctionImplementation(this, "activate", boost::python::ptr(profile)); +} + +bool BSAInvalidationWrapper::prepareProfile(MOBase::IProfile *profile) +{ + return basicWrapperFunctionImplementation(this, "prepareProfile", boost::python::ptr(profile)); +} +/// end BSAInvalidation Wrapper +///////////////////////////// +/// DataArchives Wrapper + + +QStringList DataArchivesWrapper::vanillaArchives() const +{ + return basicWrapperFunctionImplementation(this, "vanillaArchives"); +} + +QStringList DataArchivesWrapper::archives(const MOBase::IProfile *profile) const +{ + return basicWrapperFunctionImplementation(this, "archives", boost::python::ptr(profile)); +} + +void DataArchivesWrapper::addArchive(MOBase::IProfile *profile, int index, const QString &archiveName) +{ + return basicWrapperFunctionImplementation(this, "addArchive", boost::python::ptr(profile), index, archiveName); +} + +void DataArchivesWrapper::removeArchive(MOBase::IProfile *profile, const QString &archiveName) +{ + return basicWrapperFunctionImplementation(this, "removeArchive", boost::python::ptr(profile), archiveName); +} +/// end DataArchives Wrapper +///////////////////////////// +/// GamePlugins Wrapper + + +void GamePluginsWrapper::writePluginLists(const MOBase::IPluginList * pluginList) +{ + return basicWrapperFunctionImplementation(this, "writePluginLists", boost::python::ptr(pluginList)); +} + +void GamePluginsWrapper::readPluginLists(MOBase::IPluginList * pluginList) +{ + return basicWrapperFunctionImplementation(this, "readPluginLists", boost::python::ptr(pluginList)); +} + +QStringList GamePluginsWrapper::getLoadOrder() +{ + return basicWrapperFunctionImplementation(this, "getLoadOrder"); +} + +bool GamePluginsWrapper::lightPluginsAreSupported() +{ + return basicWrapperFunctionImplementation(this, "lightPluginsAreSupported"); +} + +/// end GamePlugins Wrapper +///////////////////////////// +/// LocalSavegames Wrapper + + +MappingType LocalSavegamesWrapper::mappings(const QDir & profileSaveDir) const +{ + return basicWrapperFunctionImplementation(this, "mappings", profileSaveDir); +} + +bool LocalSavegamesWrapper::prepareProfile(MOBase::IProfile * profile) +{ + return basicWrapperFunctionImplementation(this, "prepareProfile", boost::python::ptr(profile)); +} + +/// end LocalSavegames Wrapper +///////////////////////////// +/// ModDataChecker Wrapper + +ModDataChecker::CheckReturn ModDataCheckerWrapper::dataLooksValid(std::shared_ptr fileTree) const { + return basicWrapperFunctionImplementation(this, "dataLooksValid", fileTree); +} + +std::shared_ptr ModDataCheckerWrapper::fix(std::shared_ptr fileTree) const { + return utils::clean_shared_ptr( + basicWrapperFunctionImplementationWithDefault>( + this, [](auto&&... args) { return nullptr; }, "fix", fileTree)); +} + +/// end ModDataChecker Wrapper +///////////////////////////// +/// ModDataContent Wrapper + +std::vector ModDataContentWrapper::getAllContents() const { + return basicWrapperFunctionImplementation>(this, "getAllContents"); +} +std::vector ModDataContentWrapper::getContentsFor(std::shared_ptr fileTree) const { + return basicWrapperFunctionImplementation>(this, "getContentsFor", fileTree); +} + +/// end ModDataContent Wrapper +///////////////////////////// +/// SaveGameInfo Wrapper + +SaveGameInfoWrapper::MissingAssets SaveGameInfoWrapper::getMissingAssets(MOBase::ISaveGame const& save) const +{ + return basicWrapperFunctionImplementation(this, "getMissingAssets", boost::ref(save)); +} + +MOBase::ISaveGameInfoWidget* SaveGameInfoWrapper::getSaveGameWidget(QWidget* parent) const +{ + return basicWrapperFunctionImplementation(this, m_SaveGameWidget, "getSaveGameWidget", parent); +} + +/// end SaveGameInfo Wrapper +///////////////////////////// +/// ScriptExtender Wrapper + +QString ScriptExtenderWrapper::BinaryName() const +{ + return basicWrapperFunctionImplementation(this, "BinaryName"); +} + +QString ScriptExtenderWrapper::PluginPath() const +{ + return basicWrapperFunctionImplementation(this, "PluginPath"); +} + +QString ScriptExtenderWrapper::loaderName() const +{ + return basicWrapperFunctionImplementation(this, "loaderName"); +} + +QString ScriptExtenderWrapper::loaderPath() const +{ + return basicWrapperFunctionImplementation(this, "loaderPath"); +} + +QString ScriptExtenderWrapper::savegameExtension() const +{ + return basicWrapperFunctionImplementation(this, "savegameExtension"); +} + +bool ScriptExtenderWrapper::isInstalled() const +{ + return basicWrapperFunctionImplementation(this, "isInstalled"); +} + +QString ScriptExtenderWrapper::getExtenderVersion() const +{ + return basicWrapperFunctionImplementation(this, "getExtenderVersion"); +} + +WORD ScriptExtenderWrapper::getArch() const +{ + return basicWrapperFunctionImplementation(this, "getArch"); +} + +/// end ScriptExtender Wrapper +///////////////////////////// +/// UnmanagedMods Wrapper + + +QStringList UnmanagedModsWrapper::mods(bool onlyOfficial) const +{ + return basicWrapperFunctionImplementation(this, "mods", onlyOfficial); +} + +QString UnmanagedModsWrapper::displayName(const QString & modName) const +{ + return basicWrapperFunctionImplementation(this, "displayName", modName); +} + +QFileInfo UnmanagedModsWrapper::referenceFile(const QString & modName) const +{ + return basicWrapperFunctionImplementation(this, "referenceFile", modName); +} + +QStringList UnmanagedModsWrapper::secondaryFiles(const QString & modName) const +{ + return basicWrapperFunctionImplementation(this, "secondaryFiles", modName); +} +/// end UnmanagedMods Wrapper +///////////////////////////// + + +game_features_map_from_python::game_features_map_from_python() +{ + boost::python::converter::registry::push_back(&convertible, &construct, boost::python::type_id>()); +} + +void * game_features_map_from_python::convertible(PyObject * objPtr) +{ + return PyDict_Check(objPtr) ? objPtr : nullptr; +} + +template +void insertGameFeature(std::map& map, const boost::python::object& pyObject) +{ + map[std::type_index(typeid(T))] = boost::python::extract(pyObject)(); +} + +void game_features_map_from_python::construct(PyObject * objPtr, boost::python::converter::rvalue_from_python_stage1_data * data) +{ + void *storage = ((boost::python::converter::rvalue_from_python_storage>*)data)->storage.bytes; + std::map *result = new (storage) std::map(); + boost::python::dict source(boost::python::handle<>(boost::python::borrowed(objPtr))); + boost::python::list keys = source.keys(); + int len = boost::python::len(keys); + for (int i = 0; i < len; ++i) + { + boost::python::object pyKey = keys[i]; + boost::python::object pyValue = source[pyKey]; + + boost::mp11::mp_for_each< + // Must user pointers because mp_for_each construct object: + boost::mp11::mp_transform + >([&](auto* pt) { + using T = std::remove_pointer_t; + boost::python::extract extract(pyValue); + if (extract.check()) { + (*result)[std::type_index(typeid(T))] = extract(); + } + }); + } + + data->convertible = storage; +} + +void registerGameFeaturesPythonConverters() +{ + namespace bpy = boost::python; + + game_features_map_from_python(); + + // Features require defs for all methods as Python can access C++ features + bpy::class_("BSAInvalidation") + .def("isInvalidationBSA", bpy::pure_virtual(&BSAInvalidation::isInvalidationBSA), bpy::arg("name")) + .def("deactivate", bpy::pure_virtual(&BSAInvalidation::deactivate), bpy::arg("profile")) + .def("activate", bpy::pure_virtual(&BSAInvalidation::activate), bpy::arg("profile")) + ; + + bpy::class_("DataArchives") + .def("vanillaArchives", bpy::pure_virtual(&DataArchives::vanillaArchives)) + .def("archives", bpy::pure_virtual(&DataArchives::archives), bpy::arg("profile")) + .def("addArchive", bpy::pure_virtual(&DataArchives::addArchive), (bpy::arg("profile"), "index", "name")) + .def("removeArchive", bpy::pure_virtual(&DataArchives::removeArchive), (bpy::arg("profile"), "name")) + ; + + bpy::class_("GamePlugins") + .def("writePluginLists", bpy::pure_virtual(&GamePlugins::writePluginLists), bpy::arg("plugin_list")) + .def("readPluginLists", bpy::pure_virtual(&GamePlugins::readPluginLists), bpy::arg("plugin_list")) + .def("getLoadOrder", bpy::pure_virtual(&GamePlugins::getLoadOrder)) + .def("lightPluginsAreSupported", bpy::pure_virtual(&GamePlugins::lightPluginsAreSupported)) + ; + + bpy::class_("LocalSavegames") + .def("mappings", bpy::pure_virtual(&LocalSavegames::mappings), bpy::arg("profile_save_dir")) + .def("prepareProfile", bpy::pure_virtual(&LocalSavegames::prepareProfile), bpy::arg("profile")) + ; + + auto modDataCheckerClass = bpy::class_("ModDataChecker"); + { + bpy::scope scope = modDataCheckerClass; + + bpy::enum_("CheckReturn") + .value("INVALID", ModDataChecker::CheckReturn::INVALID) + .value("FIXABLE", ModDataChecker::CheckReturn::FIXABLE) + .value("VALID", ModDataChecker::CheckReturn::VALID) + .export_values() + ; + + modDataCheckerClass + .def("dataLooksValid", bpy::pure_virtual(&ModDataChecker::dataLooksValid), bpy::arg("filetree")) + .def("fix", bpy::pure_virtual(&ModDataChecker::fix), bpy::arg("filetree")) + ; + } + + { + bpy::scope scope = bpy::class_("ModDataContent") + .def("getAllContents", bpy::pure_virtual(&ModDataContent::getAllContents)) + .def("getContentsFor", bpy::pure_virtual(&ModDataContent::getContentsFor), bpy::arg("filetree")) + ; + + bpy::class_("Content", + bpy::init>((bpy::arg("id"), "name", "icon", bpy::arg("filter_only") = false))) + .add_property("id", &ModDataContent::Content::id) + .add_property("name", &ModDataContent::Content::name) + .add_property("icon", &ModDataContent::Content::icon) + .def("isOnlyForFilter", &ModDataContent::Content::isOnlyForFilter) + ; + + } + + bpy::class_("SaveGameInfo") + .def("getMissingAssets", bpy::pure_virtual(&SaveGameInfo::getMissingAssets), bpy::arg("save")) + .def("getSaveGameWidget", bpy::pure_virtual(&SaveGameInfo::getSaveGameWidget), bpy::return_value_policy(), + bpy::arg("parent"), "[optional]") + ; + + bpy::class_("ScriptExtender") + .def("BinaryName", bpy::pure_virtual(&ScriptExtender::BinaryName)) + .def("PluginPath", bpy::pure_virtual(&ScriptExtender::PluginPath)) + .def("loaderName", bpy::pure_virtual(&ScriptExtender::loaderName)) + .def("loaderPath", bpy::pure_virtual(&ScriptExtender::loaderPath)) + .def("savegameExtension", bpy::pure_virtual(&ScriptExtender::savegameExtension)) + .def("isInstalled", bpy::pure_virtual(&ScriptExtender::isInstalled)) + .def("getExtenderVersion", bpy::pure_virtual(&ScriptExtender::getExtenderVersion)) + .def("getArch", bpy::pure_virtual(&ScriptExtender::getArch)) + ; + + bpy::class_("UnmanagedMods") + .def("mods", bpy::pure_virtual(&UnmanagedMods::mods), bpy::arg("official_only")) + .def("displayName", bpy::pure_virtual(&UnmanagedMods::displayName), bpy::arg("mod_name")) + .def("referenceFile", bpy::pure_virtual(&UnmanagedMods::referenceFile), bpy::arg("mod_name")) + .def("secondaryFiles", bpy::pure_virtual(&UnmanagedMods::secondaryFiles), bpy::arg("mod_name")) + ; +} diff --git a/src/runner-pybind11/gamefeatureswrappers.h b/src/runner-pybind11/gamefeatureswrappers.h new file mode 100644 index 0000000..7155d8c --- /dev/null +++ b/src/runner-pybind11/gamefeatureswrappers.h @@ -0,0 +1,158 @@ +#ifndef GAMEFEATURESWRAPPERS_H +#define GAMEFEATURESWRAPPERS_H + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// this might need turning off if Q_MOC_RUN is defined +#include +#include + +// This is a simple MPL list that contains all the game features in one place: +using MpGameFeaturesList = boost::mp11::mp_list< + BSAInvalidation, + DataArchives, + GamePlugins, + LocalSavegames, + ModDataChecker, + ModDataContent, + SaveGameInfo, + ScriptExtender, + UnmanagedMods +>; + +///////////////////////////// +/// Wrapper declarations + +class BSAInvalidationWrapper : public BSAInvalidation, public boost::python::wrapper +{ +public: + static constexpr const char* className = "BSAInvalidationWrapper"; + using boost::python::wrapper::get_override; + + virtual bool isInvalidationBSA(const QString &bsaName) override; + virtual void deactivate(MOBase::IProfile *profile) override; + virtual void activate(MOBase::IProfile *profile) override; + virtual bool prepareProfile(MOBase::IProfile *profile) override; +}; + +class DataArchivesWrapper : public DataArchives, public boost::python::wrapper +{ +public: + static constexpr const char* className = "DataArchivesWrapper"; + using boost::python::wrapper::get_override; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile *profile) const override; + virtual void addArchive(MOBase::IProfile *profile, int index, const QString &archiveName) override; + virtual void removeArchive(MOBase::IProfile *profile, const QString &archiveName) override; +}; + +class GamePluginsWrapper : public GamePlugins, public boost::python::wrapper +{ +public: + static constexpr const char* className = "GamePluginsWrapper"; + using boost::python::wrapper::get_override; + + virtual void writePluginLists(const MOBase::IPluginList *pluginList) override; + virtual void readPluginLists(MOBase::IPluginList *pluginList) override; + virtual QStringList getLoadOrder() override; + virtual bool lightPluginsAreSupported() override; +}; + +class LocalSavegamesWrapper : public LocalSavegames, public boost::python::wrapper +{ +public: + static constexpr const char* className = "LocalSavegamesWrapper"; + using boost::python::wrapper::get_override; + + virtual MappingType mappings(const QDir &profileSaveDir) const override; + virtual bool prepareProfile(MOBase::IProfile *profile) override; +}; + +class ModDataCheckerWrapper : public ModDataChecker, public boost::python::wrapper +{ +public: + static constexpr const char* className = "ModDataCheckerWrapper"; + using boost::python::wrapper::get_override; + + virtual CheckReturn dataLooksValid(std::shared_ptr fileTree) const override; + virtual std::shared_ptr fix(std::shared_ptr fileTree) const override; +}; + +class ModDataContentWrapper : public ModDataContent, public boost::python::wrapper +{ +public: + static constexpr const char* className = "ModDataContentWrapper"; + using boost::python::wrapper::get_override; + + virtual std::vector getAllContents() const override; + virtual std::vector getContentsFor(std::shared_ptr fileTree) const override; + +}; + +class SaveGameInfoWrapper : public SaveGameInfo, public boost::python::wrapper +{ +public: + static constexpr const char* className = "SaveGameInfoWrapper"; + using boost::python::wrapper::get_override; + + virtual MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const override; + virtual MOBase::ISaveGameInfoWidget *getSaveGameWidget(QWidget *parent = 0) const override; + +private: + // We need to keep the python objects alive: + mutable std::map m_SaveGames; + mutable boost::python::object m_SaveGameWidget; +}; + +class ScriptExtenderWrapper : public ScriptExtender, public boost::python::wrapper +{ +public: + static constexpr const char* className = "ScriptExtenderWrapper"; + using boost::python::wrapper::get_override; + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; + virtual QString loaderName() const override; + virtual QString loaderPath() const override; + virtual QString savegameExtension() const override; + virtual bool isInstalled() const override; + virtual QString getExtenderVersion() const override; + virtual WORD getArch() const override; +}; + +class UnmanagedModsWrapper : public UnmanagedMods, public boost::python::wrapper +{ +public: + static constexpr const char* className = "UnmanagedModsWrapper"; + using boost::python::wrapper::get_override; + + virtual QStringList mods(bool onlyOfficial) const override; + virtual QString displayName(const QString &modName) const override; + virtual QFileInfo referenceFile(const QString &modName) const override; + virtual QStringList secondaryFiles(const QString &modName) const override; +}; + +/// end Wrapper declarations +///////////////////////////// + +struct game_features_map_from_python +{ + game_features_map_from_python(); + static void *convertible(PyObject *objPtr); + static void construct(PyObject *objPtr, boost::python::converter::rvalue_from_python_stage1_data *data); +}; + +void registerGameFeaturesPythonConverters(); + +#endif // GAMEFEATURESWRAPPERS_H diff --git a/src/runner-pybind11/gilock.cpp b/src/runner-pybind11/gilock.cpp new file mode 100644 index 0000000..8543935 --- /dev/null +++ b/src/runner-pybind11/gilock.cpp @@ -0,0 +1,12 @@ +#include "gilock.h" + +GILock::GILock() +{ + m_State = PyGILState_Ensure(); +} + +GILock::~GILock() +{ + PyErr_Clear(); + PyGILState_Release(m_State); +} diff --git a/src/runner-pybind11/gilock.h b/src/runner-pybind11/gilock.h new file mode 100644 index 0000000..c2425a6 --- /dev/null +++ b/src/runner-pybind11/gilock.h @@ -0,0 +1,18 @@ +#ifndef GILOCK_H +#define GILOCK_H + + +#ifndef Q_MOC_RUN +#include +#endif // Q_MOC_RUN + +class GILock { +public: + GILock(); + ~GILock(); +private: + PyGILState_STATE m_State; +}; + + +#endif // GILOCK_H diff --git a/src/runner-pybind11/proxypluginwrappers.cpp b/src/runner-pybind11/proxypluginwrappers.cpp new file mode 100644 index 0000000..bb64065 --- /dev/null +++ b/src/runner-pybind11/proxypluginwrappers.cpp @@ -0,0 +1,473 @@ +#include "proxypluginwrappers.h" + +#include "gilock.h" +#include +#include + +#include "shared_ptr_converter.h" +#include "pythonwrapperutilities.h" +#include "uibasewrappers.h" + +#include +#include + +namespace boost +{ + // See bug https://connect.microsoft.com/VisualStudio/Feedback/Details/2852624 +#if (_MSC_VER == 1900) + template<> const volatile MOBase::IOrganizer* get_pointer(const volatile MOBase::IOrganizer* p) { return p; } + template<> const volatile MOBase::IModInterface* get_pointer(const volatile MOBase::IModInterface* p) { return p; } + template<> const volatile MOBase::IPluginGame* get_pointer(const volatile MOBase::IPluginGame* p) { return p; } + template<> const volatile MOBase::IProfile* get_pointer(const volatile MOBase::IProfile* p) { return p; } + template<> const volatile MOBase::IModList* get_pointer(const volatile MOBase::IModList* p) { return p; } + template<> const volatile MOBase::IPluginList* get_pointer(const volatile MOBase::IPluginList* p) { return p; } + template<> const volatile MOBase::IDownloadManager* get_pointer(const volatile MOBase::IDownloadManager* p) { return p; } + template<> const volatile MOBase::IModRepositoryBridge* get_pointer(const volatile MOBase::IModRepositoryBridge* p) { return p; } +#endif +} + +using namespace MOBase; + +// See COMMON_I_PLUGIN_WRAPPER_DECLARATIONS__IMPL in proxypluginwrappers.h for explanation on +// the "include_requirements". +#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_IMPL(class_name, include_requirements) \ +bool class_name::init(MOBase::IOrganizer *moInfo) \ +{ \ + return basicWrapperFunctionImplementation(this, "init", boost::python::ptr(moInfo)); \ +} \ + \ +QString class_name::name() const \ +{ \ + return basicWrapperFunctionImplementation(this, "name"); \ +} \ + \ +QString class_name::localizedName() const \ +{ \ + return basicWrapperFunctionImplementationWithDefault(this, &class_name::localizedName_Default, "localizedName"); \ +} \ + \ +QString class_name::master() const \ +{ \ + return basicWrapperFunctionImplementationWithDefault(this, &class_name::master_Default, "master"); \ +} \ + \ +QString class_name::author() const \ +{ \ + return basicWrapperFunctionImplementation(this, "author"); \ +} \ + \ +QString class_name::description() const \ +{ \ + return basicWrapperFunctionImplementation(this, "description"); \ +} \ + \ +MOBase::VersionInfo class_name::version() const \ +{ \ + return basicWrapperFunctionImplementation(this, "version"); \ +} \ + \ +QList class_name::settings() const \ +{ \ + return basicWrapperFunctionImplementation>(this, "settings"); \ +} \ +QString class_name::localizedName_Default() const { return IPlugin::localizedName(); } \ +QString class_name::master_Default() const { return IPlugin::master(); } \ +BOOST_PP_EXPR_IF(include_requirements, \ + std::vector> class_name::requirements() const { \ + return basicWrapperFunctionImplementationWithDefault>>( \ + this, &class_name::requirements_Default, "requirements"); \ + } \ + std::vector> class_name::requirements_Default() const { return IPlugin::requirements(); } \ + bool class_name::enabledByDefault() const \ + { \ + return basicWrapperFunctionImplementationWithDefault(this, &class_name::enabledByDefault_Default, "enabledByDefault"); \ + } \ + bool class_name::enabledByDefault_Default() const { return IPlugin::enabledByDefault(); }) + +#define COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(class_name) COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_IMPL(class_name, 1) + +/// end COMMON_I_PLUGIN_WRAPPER_DEFINITIONS +///////////////////////////// +/// IPlugin Wrapper + + +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginWrapper) +/// end IPlugin Wrapper +///////////////////////////////////// +/// IPluginDiagnose Wrapper + + +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginDiagnoseWrapper) + +std::vector IPluginDiagnoseWrapper::activeProblems() const +{ + return basicWrapperFunctionImplementation>(this, "activeProblems"); +} + +QString IPluginDiagnoseWrapper::shortDescription(unsigned int key) const +{ + return basicWrapperFunctionImplementation(this, "shortDescription", key); +} + +QString IPluginDiagnoseWrapper::fullDescription(unsigned int key) const +{ + return basicWrapperFunctionImplementation(this, "fullDescription", key); +} + +bool IPluginDiagnoseWrapper::hasGuidedFix(unsigned int key) const +{ + return basicWrapperFunctionImplementation(this, "hasGuidedFix", key); +} + +void IPluginDiagnoseWrapper::startGuidedFix(unsigned int key) const +{ + basicWrapperFunctionImplementation(this, "startGuidedFix", key); +} + +/// end IPluginDiagnose Wrapper +///////////////////////////////////// +/// IPluginFileMapper Wrapper + + +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginFileMapperWrapper) + +MappingType IPluginFileMapperWrapper::mappings() const +{ + return basicWrapperFunctionImplementation(this, "mappings"); +} +/// end IPluginFileMapper Wrapper +///////////////////////////////////// +/// IPluginGame Wrapper + +void IPluginGameWrapper::detectGame() +{ + return basicWrapperFunctionImplementation(this, "detectGame"); +} + +QString IPluginGameWrapper::gameName() const +{ + return basicWrapperFunctionImplementation(this, "gameName"); +} + +void IPluginGameWrapper::initializeProfile(const QDir & directory, ProfileSettings settings) const +{ + basicWrapperFunctionImplementation(this, "initializeProfile", directory, settings); +} + +std::vector> IPluginGameWrapper::listSaves(QDir folder) const +{ + // Why do I not need to hold python references here? Is it because those are wrapped + // in shared_ptr? + return basicWrapperFunctionImplementation>>(this, "listSaves", folder); +} + +bool IPluginGameWrapper::isInstalled() const +{ + return basicWrapperFunctionImplementation(this, "isInstalled"); +} + +QIcon IPluginGameWrapper::gameIcon() const +{ + return basicWrapperFunctionImplementation(this, "gameIcon"); +} + +QDir IPluginGameWrapper::gameDirectory() const +{ + return basicWrapperFunctionImplementation(this, "gameDirectory"); +} + +QDir IPluginGameWrapper::dataDirectory() const +{ + return basicWrapperFunctionImplementation(this, "dataDirectory"); +} + +void IPluginGameWrapper::setGamePath(const QString & path) +{ + basicWrapperFunctionImplementation(this, "setGamePath", path); +} + +QDir IPluginGameWrapper::documentsDirectory() const +{ + return basicWrapperFunctionImplementation(this, "documentsDirectory"); +} + +QDir IPluginGameWrapper::savesDirectory() const +{ + return basicWrapperFunctionImplementation(this, "savesDirectory"); +} + +QList IPluginGameWrapper::executables() const +{ + return basicWrapperFunctionImplementation>(this, "executables"); +} + +QList IPluginGameWrapper::executableForcedLoads() const +{ + return basicWrapperFunctionImplementation>(this, "executableForcedLoads"); +} + +QString IPluginGameWrapper::steamAPPId() const +{ + return basicWrapperFunctionImplementation(this, "steamAPPId"); +} + +QStringList IPluginGameWrapper::primaryPlugins() const +{ + return basicWrapperFunctionImplementation(this, "primaryPlugins"); +} + +QStringList IPluginGameWrapper::gameVariants() const +{ + return basicWrapperFunctionImplementation(this, "gameVariants"); +} + +void IPluginGameWrapper::setGameVariant(const QString & variant) +{ + basicWrapperFunctionImplementation(this, "setGameVariant", variant); +} + +QString IPluginGameWrapper::binaryName() const +{ + return basicWrapperFunctionImplementation(this, "binaryName"); +} + +QString IPluginGameWrapper::gameShortName() const +{ + return basicWrapperFunctionImplementation(this, "gameShortName"); +} + +QStringList IPluginGameWrapper::primarySources() const +{ + return basicWrapperFunctionImplementation(this, "primarySources"); +} + +QStringList IPluginGameWrapper::validShortNames() const +{ + return basicWrapperFunctionImplementation(this, "validShortNames"); +} + +QString IPluginGameWrapper::gameNexusName() const +{ + return basicWrapperFunctionImplementation(this, "gameNexusName"); +} + +QStringList IPluginGameWrapper::iniFiles() const +{ + return basicWrapperFunctionImplementation(this, "iniFiles"); +} + +QStringList IPluginGameWrapper::DLCPlugins() const +{ + return basicWrapperFunctionImplementation(this, "DLCPlugins"); +} + +QStringList IPluginGameWrapper::CCPlugins() const +{ + return basicWrapperFunctionImplementation(this, "CCPlugins"); +} + +IPluginGame::LoadOrderMechanism IPluginGameWrapper::loadOrderMechanism() const +{ + return basicWrapperFunctionImplementation(this, "loadOrderMechanism"); +} + +IPluginGame::SortMechanism IPluginGameWrapper::sortMechanism() const +{ + return basicWrapperFunctionImplementation(this, "sortMechanism"); +} + +int IPluginGameWrapper::nexusModOrganizerID() const +{ + return basicWrapperFunctionImplementation(this, "nexusModOrganizerID"); +} + +int IPluginGameWrapper::nexusGameID() const +{ + return basicWrapperFunctionImplementation(this, "nexusGameID"); +} + +bool IPluginGameWrapper::looksValid(QDir const & dir) const +{ + return basicWrapperFunctionImplementation(this, "looksValid", dir); +} + +QString IPluginGameWrapper::gameVersion() const +{ + return basicWrapperFunctionImplementation(this, "gameVersion"); +} + +QString IPluginGameWrapper::getLauncherName() const +{ + return basicWrapperFunctionImplementation(this, "getLauncherName"); +} + +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS_IMPL(IPluginGameWrapper, 0) + +std::map IPluginGameWrapper::featureList() const +{ + return basicWrapperFunctionImplementation>(this, "_featureList"); +} +/// end IPluginGame Wrapper +///////////////////////////////////// +/// IPluginInstaller macro + +#define COMMON_I_PLUGIN_INSTALLER_WRAPPER_DEFINITIONS(class_name) \ +unsigned int class_name::priority() const { return basicWrapperFunctionImplementation(this, "priority"); } \ +bool class_name::isManualInstaller() const { return basicWrapperFunctionImplementation(this, "isManualInstaller"); } \ +void class_name::onInstallationStart(QString const& archive, bool reinstallation, MOBase::IModInterface* currentMod) { \ + basicWrapperFunctionImplementationWithDefault(this, &class_name::onInstallationStart_Default, "onInstallationStart", archive, reinstallation, boost::python::ptr(currentMod)); } \ +void class_name::onInstallationEnd(EInstallResult result, MOBase::IModInterface* newMod) { \ + basicWrapperFunctionImplementationWithDefault(this, &class_name::onInstallationEnd_Default, "onInstallationEnd", result, boost::python::ptr(newMod)); } \ +bool class_name::isArchiveSupported(std::shared_ptr tree) const { return basicWrapperFunctionImplementation(this, "isArchiveSupported", tree); } + +/// end IPluginInstaller macro +///////////////////////////////////// +/// IPluginInstaller Wrapper + +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginInstallerSimpleWrapper) +COMMON_I_PLUGIN_INSTALLER_WRAPPER_DEFINITIONS(IPluginInstallerSimpleWrapper) + +IPluginInstaller::EInstallResult IPluginInstallerSimpleWrapper::install( + GuessedValue& modName, std::shared_ptr& tree, + QString& version, int& nexusID) +{ + namespace bpy = boost::python; + + using return_type = std::variant< + IPluginInstaller::EInstallResult, + std::shared_ptr, + std::tuple, QString, int>>; + auto ret = basicWrapperFunctionImplementation(this, "install", boost::ref(modName), tree, version, nexusID); + + auto result = std::visit([&](auto const& t) { + using type = std::decay_t; + if constexpr (std::is_same_v) { + return t; + } + else if constexpr (std::is_same_v>) { + tree = t; + return IPluginInstaller::RESULT_SUCCESS; + } + else if constexpr (std::is_same_v, QString, int>>) { + tree = std::get<1>(t); + version = std::get<2>(t); + nexusID = std::get<3>(t); + return std::get<0>(t); + } + }, ret); + + tree = utils::clean_shared_ptr(tree); + return result; +} + +/// end IPluginInstallerSimple Wrapper +///////////////////////////////////// +/// IPluginInstallerCustom Wrapper +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginInstallerCustomWrapper) +COMMON_I_PLUGIN_INSTALLER_WRAPPER_DEFINITIONS(IPluginInstallerCustomWrapper) + +bool IPluginInstallerCustomWrapper::isArchiveSupported(const QString &archiveName) const +{ + return basicWrapperFunctionImplementation(this, "isArchiveSupported", archiveName); +} + +std::set IPluginInstallerCustomWrapper::supportedExtensions() const +{ + return basicWrapperFunctionImplementation>(this, "supportedExtensions"); +} + +IPluginInstaller::EInstallResult IPluginInstallerCustomWrapper::install( + GuessedValue &modName, QString gameName, const QString &archiveName, const QString &version, int modID) +{ + // Note: This requires far more less trouble than the "Simple" installer version since 1) there is no tree + // and 2) there version and modId cannot be modified: + return basicWrapperFunctionImplementation( + this, "install", boost::ref(modName), gameName, archiveName, version, modID); +} + +/// end IPluginInstallerCustom Wrapper +///////////////////////////// +/// IPluginModPage Wrapper + + +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginModPageWrapper) + +QString IPluginModPageWrapper::displayName() const +{ + return basicWrapperFunctionImplementation(this, "displayName"); +} + +QIcon IPluginModPageWrapper::icon() const +{ + return basicWrapperFunctionImplementation(this, "icon"); +} + +QUrl IPluginModPageWrapper::pageURL() const +{ + return basicWrapperFunctionImplementation(this, "pageURL"); +} + +bool IPluginModPageWrapper::useIntegratedBrowser() const +{ + return basicWrapperFunctionImplementation(this, "useIntegratedBrowser"); +} + +bool IPluginModPageWrapper::handlesDownload(const QUrl & pageURL, const QUrl & downloadURL, MOBase::ModRepositoryFileInfo & fileInfo) const +{ + return basicWrapperFunctionImplementation(this, "handlesDownload", pageURL, downloadURL, fileInfo); +} + +void IPluginModPageWrapper::setParentWidget(QWidget * widget) +{ + basicWrapperFunctionImplementationWithDefault(this, &IPluginModPageWrapper::setParentWidget_Default, "setParentWidget", widget); +} +/// end IPluginModPage Wrapper +///////////////////////////// +/// IPluginPreview Wrapper + + +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginPreviewWrapper) + +std::set IPluginPreviewWrapper::supportedExtensions() const +{ + return basicWrapperFunctionImplementation>(this, "supportedExtensions"); +} + +QWidget *IPluginPreviewWrapper::genFilePreview(const QString &fileName, const QSize &maxSize) const +{ + // We need responsibility for deleting the QWidget to be transferred to C++: + return wrapperFunctionImplementationWithApiTransfer(this, "genFilePreview", fileName, maxSize); +} +/// end IPluginPreview Wrapper +///////////////////////////// +/// IPluginTool Wrapper + + +COMMON_I_PLUGIN_WRAPPER_DEFINITIONS(IPluginToolWrapper) + +QString IPluginToolWrapper::displayName() const +{ + return basicWrapperFunctionImplementation(this, "displayName"); +} + +QString IPluginToolWrapper::tooltip() const +{ + return basicWrapperFunctionImplementation(this, "tooltip"); +} + +QIcon IPluginToolWrapper::icon() const +{ + return basicWrapperFunctionImplementation(this, "icon"); +} + +void IPluginToolWrapper::setParentWidget(QWidget *parent) +{ + basicWrapperFunctionImplementationWithDefault(this, &IPluginToolWrapper::setParentWidget_Default, "setParentWidget", parent); +} + +void IPluginToolWrapper::display() const +{ + basicWrapperFunctionImplementation(this, "display"); +} + +/// end IPluginTool Wrapper diff --git a/src/runner-pybind11/proxypluginwrappers.h b/src/runner-pybind11/proxypluginwrappers.h new file mode 100644 index 0000000..524adde --- /dev/null +++ b/src/runner-pybind11/proxypluginwrappers.h @@ -0,0 +1,268 @@ +#ifndef PROXYPLUGINWRAPPERS_H +#define PROXYPLUGINWRAPPERS_H + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_MOC_RUN +#include +#include +#endif + +// The wrapper for IPluginGame cannot override requirements or enabledByDefault since they're final, +// so we need to be able to exclude the declarations. +#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_IMPL(include_requirements) \ +public: \ +virtual bool init(MOBase::IOrganizer *moInfo) override; \ +virtual QString name() const override; \ +virtual QString localizedName() const override; \ +virtual QString master() const override; \ +virtual QString author() const override; \ +virtual QString description() const override; \ +virtual MOBase::VersionInfo version() const override; \ +virtual QList settings() const override; \ +QString localizedName_Default() const; \ +QString master_Default() const; \ +BOOST_PP_EXPR_IF(include_requirements, \ + virtual std::vector> requirements() const override; \ + std::vector> requirements_Default() const; \ + virtual bool enabledByDefault() const override; \ + bool enabledByDefault_Default() const;) + +#define COMMON_I_PLUGIN_WRAPPER_DECLARATIONS COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_IMPL(1) + +// Even though the base interface is not a QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. +// QObject must be the first base class because moc assumes the first base class is a QObject +class IPluginWrapper : public QObject, public MOBase::IPlugin, public boost::python::wrapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin) + + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS +public: + static constexpr const char* className = "IPluginWrapper"; + using boost::python::wrapper::get_override; +}; + + +// Even though the base interface is not an IPlugin or QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. +// QObject must be the first base class because moc assumes the first base class is a QObject +class IPluginDiagnoseWrapper : public QObject, public MOBase::IPluginDiagnose, public MOBase::IPlugin, public boost::python::wrapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose) + +public: + static constexpr const char* className = "IPluginDiagnoseWrapper"; + using boost::python::wrapper::get_override; + + // Bring in public scope: + using IPluginDiagnose::invalidate; + + virtual std::vector activeProblems() const override; + virtual QString shortDescription(unsigned int key) const override; + virtual QString fullDescription(unsigned int key) const override; + virtual bool hasGuidedFix(unsigned int key) const override; + virtual void startGuidedFix(unsigned int key) const override; + + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS +}; + + +// Even though the base interface is not an IPlugin or QObject, this has to be because we have no way to pass Mod Organizer a plugin that implements multiple interfaces. +// QObject must be the first base class because moc assumes the first base class is a QObject +class IPluginFileMapperWrapper : public QObject, public MOBase::IPluginFileMapper, public MOBase::IPlugin, public boost::python::wrapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginFileMapper) + +public: + static constexpr const char* className = "IPluginFileMapperWrapper"; + using boost::python::wrapper::get_override; + + virtual MappingType mappings() const override; + + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS +}; + + +class IPluginGameWrapper : public MOBase::IPluginGame, public boost::python::wrapper { + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginGame) + +public: + static constexpr const char* className = "IPluginGameWrapper"; + using boost::python::wrapper::get_override; + + virtual void detectGame() override; + virtual QString gameName() const override; + virtual void initializeProfile(const QDir &directory, ProfileSettings settings) const override; + virtual std::vector> listSaves(QDir folder) const override; + virtual bool isInstalled() const override; + virtual QIcon gameIcon() const override; + virtual QDir gameDirectory() const override; + virtual QDir dataDirectory() const override; + virtual void setGamePath(const QString &path) override; + virtual QDir documentsDirectory() const override; + virtual QDir savesDirectory() const override; + virtual QList executables() const override; + virtual QList executableForcedLoads() const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual void setGameVariant(const QString &variant) override; + virtual QString binaryName() const override; + virtual QString gameShortName() const override; + virtual QStringList primarySources() const override; + virtual QStringList validShortNames() const override; + virtual QString gameNexusName() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual SortMechanism sortMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + virtual bool looksValid(QDir const &dir) const override; + virtual QString gameVersion() const override; + virtual QString getLauncherName() const override; + + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS_IMPL(0) + +protected: + // Apparently, Python developers interpret an underscore in a function name as it being protected + virtual std::map featureList() const override; + + // Thankfully, the default implementation of the templated 'T *feature()' function should allow us to get away without overriding it. +}; + + +#define COMMON_I_PLUGIN_INSTALLER_WRAPPER_DECLARATIONS public: \ +using IPluginInstaller::parentWidget; \ +using IPluginInstaller::manager; \ +virtual unsigned int priority() const override; \ +virtual bool isManualInstaller() const override; \ +virtual void onInstallationStart(QString const& archive, bool reinstallation, MOBase::IModInterface* currentMod) override; \ +void onInstallationStart_Default(QString const& archive, bool reinstallation, MOBase::IModInterface* currentMod) { \ + return IPluginInstaller::onInstallationStart(archive, reinstallation, currentMod); } \ +virtual void onInstallationEnd(EInstallResult result, MOBase::IModInterface* newMod) override; \ +void onInstallationEnd_Default(EInstallResult result, MOBase::IModInterface* newMod) { \ + return IPluginInstaller::onInstallationEnd(result, newMod); } \ +virtual bool isArchiveSupported(std::shared_ptr tree) const override; + + +class IPluginInstallerSimpleWrapper : public MOBase::IPluginInstallerSimple, public boost::python::wrapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerSimple) + + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS + COMMON_I_PLUGIN_INSTALLER_WRAPPER_DECLARATIONS + +public: + static constexpr const char* className = "IPluginInstallerSimpleWrapper"; + using boost::python::wrapper::get_override; + + virtual EInstallResult install(MOBase::GuessedValue& modName, std::shared_ptr& tree, + QString& version, int& nexusID) override; +}; + +class IPluginInstallerCustomWrapper : public MOBase::IPluginInstallerCustom, public boost::python::wrapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerCustom) + + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS + + COMMON_I_PLUGIN_INSTALLER_WRAPPER_DECLARATIONS + +public: + static constexpr const char* className = "IPluginInstallerCustomWrapper"; + using boost::python::wrapper::get_override; + + virtual bool isArchiveSupported(const QString &archiveName) const override; + virtual std::set supportedExtensions() const override; + virtual EInstallResult install(MOBase::GuessedValue &modName, QString gameName, const QString &archiveName, + const QString &version, int modID) override; +}; + + +class IPluginModPageWrapper : public MOBase::IPluginModPage, public boost::python::wrapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginModPage) + + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS +public: + static constexpr const char* className = "IPluginModPageWrapper"; + using boost::python::wrapper::get_override; + + // Bring in public scope: + using IPluginModPage::parentWidget; + + virtual QString displayName() const override; + virtual QIcon icon() const override; + virtual QUrl pageURL() const override; + virtual bool useIntegratedBrowser() const override; + virtual bool handlesDownload(const QUrl &pageURL, const QUrl &downloadURL, MOBase::ModRepositoryFileInfo &fileInfo) const override; + virtual void setParentWidget(QWidget *widget) override; + + void setParentWidget_Default(QWidget* parent) { + IPluginModPage::setParentWidget(parent); + } +}; + + +class IPluginPreviewWrapper : public MOBase::IPluginPreview, public boost::python::wrapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview) + + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS +public: + static constexpr const char* className = "IPluginPreviewWrapper"; + using boost::python::wrapper::get_override; + + virtual std::set supportedExtensions() const override; + virtual QWidget *genFilePreview(const QString &fileName, const QSize &maxSize) const override; +}; + + +class IPluginToolWrapper: public MOBase::IPluginTool, public boost::python::wrapper +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginTool) + + COMMON_I_PLUGIN_WRAPPER_DECLARATIONS +public: + static constexpr const char* className = "IPluginToolWrapper"; + using boost::python::wrapper::get_override; + + // Bring in public scope: + using IPluginTool::parentWidget; + + virtual QString displayName() const override; + virtual QString tooltip() const override; + virtual QIcon icon() const override; + virtual void setParentWidget(QWidget *parent) override; + + void setParentWidget_Default(QWidget* parent) { + IPluginTool::setParentWidget(parent); + } + +public Q_SLOTS: + virtual void display() const override; +}; + + + + +#endif // PROXYPLUGINWRAPPERS_H diff --git a/src/runner-pybind11/pylogger.cpp b/src/runner-pybind11/pylogger.cpp new file mode 100644 index 0000000..f0793e3 --- /dev/null +++ b/src/runner-pybind11/pylogger.cpp @@ -0,0 +1,74 @@ +#include "pylogger.h" + +#include "log.h" + +namespace py = pybind11; + +// Small structure to hold the levels - There are copy paste from +// my Python version and I assume these will not change soon: +struct PyLogLevel { + static constexpr int CRITICAL = 50; + static constexpr int ERROR = 40; + static constexpr int WARNING = 30; + static constexpr int INFO = 20; + static constexpr int DEBUG = 10; +}; + +// This is the function we are going to use as our Handler .emit +// method. +void emit_function(py::object self, py::object record) { + + // There are other parameters that could be used, but this is minimal for + // now (filename, line number, etc.). + const int level = record.attr("levelno").cast(); + const std::wstring msg = py::str(record.attr("msg")).cast(); + + switch (level) { + case PyLogLevel::CRITICAL: + case PyLogLevel::ERROR: + MOBase::log::error("{}", msg); + break; + case PyLogLevel::WARNING: + MOBase::log::warn("{}", msg); + break; + case PyLogLevel::INFO: + MOBase::log::info("{}", msg); + break; + case PyLogLevel::DEBUG: + default: // There is a "NOTSET" level in theory: + MOBase::log::debug("{}", msg); + break; + } +}; + +void configure_python_logging(py::module_ mobase) +{ + // most of this is dealing with actual Python objects since it is not possible + // to derive from logging.Handler in C++ using Boost.Python, and since a lot of + // this would require extra register only for this. + + // see also https://github.com/pybind/pybind11/issues/1193#issuecomment-429451094 + + // retrieve the logging module and the Handler class. + auto logging = py::module_::import("logging"); + auto Handler = logging.attr("Handler"); + + // this is ugly but that's how it's done in C Python + auto type = py::reinterpret_borrow((PyObject*)&PyType_Type); + + // Create the "MO2Handler" python class: + auto methods = py::dict(); + methods["emit"] = py::cpp_function(emit_function); + auto MO2Handler = type("LogHandler", py::make_tuple(Handler), methods); + + // Create the default logger: + auto handler = MO2Handler(); + handler.attr("setLevel")(PyLogLevel::DEBUG); + auto logger = logging.attr("getLogger")(py::object(mobase.attr("__name__"))); + logger.attr("setLevel")(PyLogLevel::DEBUG); + logger.attr("addHandler")(handler); + + // Set mobase attributes: + mobase.attr("LogHandler") = MO2Handler; + mobase.attr("logger") = logger; +} diff --git a/src/runner-pybind11/pylogger.h b/src/runner-pybind11/pylogger.h new file mode 100644 index 0000000..5d0804b --- /dev/null +++ b/src/runner-pybind11/pylogger.h @@ -0,0 +1,13 @@ +#ifndef MO2_PYTHON_LOGGER_H +#define MO2_PYTHON_LOGGER_H + +#include + +/** + * @brief Configure logging for MO2 python plugin. + * + * @param mobase The mobase module. + */ +void configure_python_logging(pybind11::module_ mobase); + +#endif diff --git a/src/runner-pybind11/pythonrunner.cpp b/src/runner-pybind11/pythonrunner.cpp new file mode 100644 index 0000000..1d01701 --- /dev/null +++ b/src/runner-pybind11/pythonrunner.cpp @@ -0,0 +1,1607 @@ +#include "pythonrunner.h" + +#pragma warning( disable : 4100 ) +#pragma warning( disable : 4996 ) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#ifndef Q_MOC_RUN +#include +#include +// #include +// #include +#endif + +#include "converters.h" +#include "error.h" +// #include "gamefeatureswrappers.h" +// #include "proxypluginwrappers.h" +#include "pylogger.h" +// #include "shared_ptr_converter.h" +// #include "sipApiAccess.h" +// #include "tuple_helper.h" +// #include "uibasewrappers.h" +// #include "variant_helper.h" +// #include "widgets.h" + +using namespace MOBase; + +// namespace bpy = boost::python; +// namespace mp11 = boost::mp11; +namespace py = pybind11; + +/** + * This macro should be used within a bpy::class_ declaration and will define two + * methods: __getattr__ and Name, where Name will simply return the object as a QClass* + * object, while __getattr__ will delegate to the underlying QClass object when required. + * + * This allow access to Qt interface for object exposed using boost::python (e.g., signals, + * methods from QObject or QWidget, etc.). + */ +#define Q_DELEGATE(Class, QClass, Name) \ + .def(Name, +[](Class* w) -> QClass* { return w; }, bpy::return_value_policy()) \ + .def("__getattr__", +[](Class* w, bpy::str str) -> bpy::object { \ + return bpy::object{ (QClass*)w }.attr(str); \ + }) + +PYBIND11_MODULE(mobase, m) +{ + py::module_::import("PyQt5.QtCore"); + py::module_::import("PyQt5.QtWidgets"); + + py::detail::type_caster t1; + py::detail::type_caster t2; + + m.def("testQStringList", [](QStringList const& list) { + QStringList res = list; + for (QString& value : res) { + value = value + "_CPP"; + } + return res; + }); + + m.def("testQMap1", [](QMap const& map) { + QMap res; + for (auto it = map.begin(); it != map.end(); ++it) { + res[it.key()] = it.value().size(); + } + return res; + }); + + m.def("testQMap2", [](QMap const& map) { + QMap res; + for (auto it = map.begin(); it != map.end(); ++it) { + res[it.key()] = QString::number(it.value()); + } + return res; + }); + + m.def("testEnum", [](QMessageBox::StandardButton button) { + return py::make_tuple(button, QMessageBox::Icon::Information); + }); + + m.def("testPixmap", [](QPixmap const& pixmap) { + return pixmap.size(); + }); + + // utils::register_qstring_converter(); + // utils::register_qvariant_converter(); + + // utils::register_qclass_converter(); + // utils::register_qclass_converter(); + // utils::register_qclass_converter(); + // utils::register_qclass_converter(); + // utils::register_qclass_converter(); + // utils::register_qclass_converter(); + // utils::register_qclass_converter(); + // utils::register_qclass_converter(); + // utils::register_qclass_converter(); + + // // QFlags: + // utils::register_qflags_converter(); + // utils::register_qflags_converter(); + // utils::register_qflags_converter(); + + // // Enums: + // utils::register_enum_converter(); + // utils::register_enum_converter(); + + // // Pointers: + // bpy::register_ptr_to_python>(); + // bpy::register_ptr_to_python>(); + // bpy::implicitly_convertible, std::shared_ptr>(); + // bpy::register_ptr_to_python>(); + // bpy::register_ptr_to_python>(); + // bpy::implicitly_convertible, std::shared_ptr>(); + + // utils::shared_ptr_from_python>(); + // bpy::register_ptr_to_python>(); + + // utils::shared_ptr_from_python>(); + // bpy::register_ptr_to_python>(); + + // // Containers: + // utils::register_sequence_container>(); + // utils::register_sequence_container>(); + // utils::register_sequence_container>(); + // utils::register_sequence_container>(); + // utils::register_sequence_container>(); + // utils::register_sequence_container>(); + // utils::register_sequence_container(); + // utils::register_sequence_container>(); + // utils::register_sequence_container>(); + // utils::register_sequence_container>(); + // utils::register_sequence_container>(); // Required for QVariant since this is QVariantList. + // utils::register_sequence_container>>(); + // utils::register_sequence_container>>(); + // utils::register_sequence_container>>(); + // utils::register_sequence_container>(); + // utils::register_sequence_container>(); + // utils::register_sequence_container>(); + + // utils::register_set_container>(); + + // utils::register_associative_container>(); // Required for QVariant since this is QVariantMap. + // utils::register_associative_container>(); + // utils::register_associative_container>(); + // utils::register_associative_container>(); + + // utils::register_associative_container(); + + // utils::register_optional>(); + + // // Tuple: + // bpy::register_tuple>(); // IOrganizer::waitForApplication + // bpy::register_tuple>(); // IProfile::invalidationActive + // bpy::register_tuple>(); + // bpy::register_tuple, QString, int>>(); + // bpy::register_tuple>(); + + // // Variants: + // bpy::register_variant, + // std::tuple, QString, int>>>(); + // bpy::register_variant>(); + // bpy::register_variant>(); + // bpy::register_variant>>(); + // bpy::register_variant>>(); + + // // Functions: + // utils::register_functor_converter(); // converter for the onRefreshed-callback + // utils::register_functor_converter(); + // utils::register_functor_converter(); + // utils::register_functor_converter(); + // utils::register_functor_converter(); // converter for the onModMoved-callback and onPluginMoved callbacks + // utils::register_functor_converter&)>(); // converter for the onModStateChanged-callback (IModList) + // utils::register_functor_converter&)>(); // converter for the onPluginStateChanged-callback (IPluginList) + // utils::register_functor_converter(); + // utils::register_functor_converter(); + // utils::register_functor_converter>(); + // utils::register_functor_converter>(); + // utils::register_functor_converter>(); + // utils::register_functor_converter(); + // utils::register_functor_converter)>(); + // utils::register_functor_converter>(); + // utils::register_functor_converter const&)>(); + // utils::register_functor_converter(QString const&)>(); + // utils::register_functor_converter>(); + // utils::register_functor_converter>(); + // utils::register_functor_converter>(); + + // // This one is kept for backward-compatibility while we deprecate onModStateChanged for singl mod. + // utils::register_functor_converter(); // converter for the onModStateChanged-callback (IModList). + // utils::register_functor_converter(); // converter for the onPluginStateChanged-callback (IPluginList). + + // // + // // Class declarations: + // // + + // bpy::enum_("ReleaseType") + // .value("final", MOBase::VersionInfo::RELEASE_FINAL) + // .value("candidate", MOBase::VersionInfo::RELEASE_CANDIDATE) + // .value("beta", MOBase::VersionInfo::RELEASE_BETA) + // .value("alpha", MOBase::VersionInfo::RELEASE_ALPHA) + // .value("prealpha", MOBase::VersionInfo::RELEASE_PREALPHA) + + // .value("FINAL", MOBase::VersionInfo::RELEASE_FINAL) + // .value("CANDIDATE", MOBase::VersionInfo::RELEASE_CANDIDATE) + // .value("BETA", MOBase::VersionInfo::RELEASE_BETA) + // .value("ALPHA", MOBase::VersionInfo::RELEASE_ALPHA) + // .value("PRE_ALPHA", MOBase::VersionInfo::RELEASE_PREALPHA) + // ; + + // bpy::enum_("VersionScheme") + // .value("discover", MOBase::VersionInfo::SCHEME_DISCOVER) + // .value("regular", MOBase::VersionInfo::SCHEME_REGULAR) + // .value("decimalmark", MOBase::VersionInfo::SCHEME_DECIMALMARK) + // .value("numbersandletters", MOBase::VersionInfo::SCHEME_NUMBERSANDLETTERS) + // .value("date", MOBase::VersionInfo::SCHEME_DATE) + // .value("literal", MOBase::VersionInfo::SCHEME_LITERAL) + + // .value("DISCOVER", MOBase::VersionInfo::SCHEME_DISCOVER) + // .value("REGULAR", MOBase::VersionInfo::SCHEME_REGULAR) + // .value("DECIMAL_MARK", MOBase::VersionInfo::SCHEME_DECIMALMARK) + // .value("NUMBERS_AND_LETTERS", MOBase::VersionInfo::SCHEME_NUMBERSANDLETTERS) + // .value("DATE", MOBase::VersionInfo::SCHEME_DATE) + // .value("LITERAL", MOBase::VersionInfo::SCHEME_LITERAL) + // ; + + // bpy::class_("VersionInfo") + // .def(bpy::init( + // (bpy::arg("value"), bpy::arg("scheme") = VersionInfo::SCHEME_DISCOVER))) + // // Note: Order of the two init<> below is important because ReleaseType is a simple enum with an + // // implicit int conversion. + // .def(bpy::init( + // (bpy::arg("major"), "minor", "subminor", "subsubminor", bpy::arg("release_type") = VersionInfo::RELEASE_FINAL))) + // .def(bpy::init( + // (bpy::arg("major"), "minor", "subminor", bpy::arg("release_type") = VersionInfo::RELEASE_FINAL))) + // .def("clear", &VersionInfo::clear) + // .def("parse", &VersionInfo::parse, + // (bpy::arg("value"), bpy::arg("scheme") = VersionInfo::SCHEME_DISCOVER, bpy::arg("is_manual") = false)) + // .def("canonicalString", &VersionInfo::canonicalString) + // .def("displayString", &VersionInfo::displayString, bpy::arg("forced_segments") = 2) + // .def("isValid", &VersionInfo::isValid) + // .def("scheme", &VersionInfo::scheme) + // .def("__str__", &VersionInfo::canonicalString) + // .def(bpy::self < bpy::self) + // .def(bpy::self > bpy::self) + // .def(bpy::self <= bpy::self) + // .def(bpy::self >= bpy::self) + // .def(bpy::self != bpy::self) + // .def(bpy::self == bpy::self) + // ; + + // bpy::class_( + // "PluginSetting", bpy::init( + // (bpy::arg("key"), "description", "default_value"))) + // .def_readwrite("key", &PluginSetting::key) + // .def_readwrite("description", &PluginSetting::description) + // .def_readwrite("default_value", &PluginSetting::defaultValue); + + // bpy::class_("ExecutableInfo", + // bpy::init((bpy::arg("title"), "binary"))) + // .def("withArgument", &ExecutableInfo::withArgument, bpy::return_self<>(), bpy::arg("argument")) + // .def("withWorkingDirectory", &ExecutableInfo::withWorkingDirectory, bpy::return_self<>(), bpy::arg("directory")) + // .def("withSteamAppId", &ExecutableInfo::withSteamAppId, bpy::return_self<>(), bpy::arg("app_id")) + // .def("asCustom", &ExecutableInfo::asCustom, bpy::return_self<>()) + // .def("isValid", &ExecutableInfo::isValid) + // .def("title", &ExecutableInfo::title) + // .def("binary", &ExecutableInfo::binary) + // .def("arguments", &ExecutableInfo::arguments) + // .def("workingDirectory", &ExecutableInfo::workingDirectory) + // .def("steamAppID", &ExecutableInfo::steamAppID) + // .def("isCustom", &ExecutableInfo::isCustom) + // ; + + // bpy::class_("ExecutableForcedLoadSetting", + // bpy::init((bpy::arg("process"), "library"))) + // .def("withForced", &ExecutableForcedLoadSetting::withForced, bpy::return_self<>(), bpy::arg("forced")) + // .def("withEnabled", &ExecutableForcedLoadSetting::withEnabled, bpy::return_self<>(), bpy::arg("enabled")) + // .def("enabled", &ExecutableForcedLoadSetting::enabled) + // .def("forced", &ExecutableForcedLoadSetting::forced) + // .def("library", &ExecutableForcedLoadSetting::library) + // .def("process", &ExecutableForcedLoadSetting::process) + // ; + + // bpy::class_, boost::noncopyable>("ISaveGame") + // .def("getFilepath", bpy::pure_virtual(&ISaveGame::getFilepath)) + // .def("getCreationTime", bpy::pure_virtual(&ISaveGame::getCreationTime)) + // .def("getName", bpy::pure_virtual(&ISaveGame::getName)) + // .def("getSaveGroupIdentifier", bpy::pure_virtual(&ISaveGame::getSaveGroupIdentifier)) + // .def("allFiles", bpy::pure_virtual(&ISaveGame::allFiles)) + // ; + + // // See Q_DELEGATE for more details. + // bpy::class_, ISaveGameInfoWidgetWrapper*, boost::noncopyable>( + // "ISaveGameInfoWidget", bpy::init>(bpy::arg("parent"))) + // .def("setSave", bpy::pure_virtual(&ISaveGameInfoWidget::setSave), bpy::arg("save")) + + // Q_DELEGATE(ISaveGameInfoWidget, QWidget, "_widget") + // ; + + // // Plugin requirements: + // auto iPluginRequirementClass = bpy::class_< + // IPluginRequirementWrapper, bpy::bases<>, boost::noncopyable>("IPluginRequirement"); + // { + // bpy::scope scope = iPluginRequirementClass; + + // bpy::class_("Problem", + // bpy::init((bpy::arg("short_description"), bpy::arg("long_description") = ""))) + // .def("shortDescription", &IPluginRequirement::Problem::shortDescription) + // .def("longDescription", &IPluginRequirement::Problem::longDescription); + + // iPluginRequirementClass + // .def("check", bpy::pure_virtual(&IPluginRequirement::check), bpy::arg("organizer")) + // ; + // } + + // bpy::class_("PluginRequirementFactory") + // // pluginDependency + // .def("pluginDependency", +[](QStringList const& pluginNames) { + // return PluginRequirementFactory::pluginDependency(pluginNames); + // }, bpy::arg("plugins")) + // .def("pluginDependency", +[](QString const& pluginName) { + // return PluginRequirementFactory::pluginDependency(pluginName); + // }, bpy::arg("plugin")) + // .staticmethod("pluginDependency") + // // gameDependency + // .def("gameDependency", +[](QStringList const& gameNames) { + // return PluginRequirementFactory::gameDependency(gameNames); + // }, bpy::arg("games")) + // .def("gameDependency", +[](QString const& gameNames) { + // return PluginRequirementFactory::gameDependency(gameNames); + // }, bpy::arg("game")) + // .staticmethod("gameDependency") + // // diagnose + // .def("diagnose", &PluginRequirementFactory::diagnose, bpy::arg("diagnose")) + // .staticmethod("diagnose") + // // basic + // .def("basic", &PluginRequirementFactory::basic, (bpy::arg("checker"), "description")) + // .staticmethod("basic"); + + // bpy::class_("FileInfo", bpy::init<>()) + // .add_property("filePath", + // +[](const IOrganizer::FileInfo& info) { return info.filePath; }, + // +[](IOrganizer::FileInfo& info, QString value) { info.filePath = value; }) + // .add_property("archive", + // +[](const IOrganizer::FileInfo& info) { return info.archive; }, + // +[](IOrganizer::FileInfo& info, QString value) { info.archive = value; }) + // .add_property("origins", + // +[](const IOrganizer::FileInfo& info) { return info.origins; }, + // +[](IOrganizer::FileInfo& info, QStringList value) { info.origins = value; }) + // ; + + // bpy::class_("IOrganizer", bpy::no_init) + // .def("createNexusBridge", &IOrganizer::createNexusBridge, bpy::return_value_policy()) + // .def("profileName", &IOrganizer::profileName) + // .def("profilePath", &IOrganizer::profilePath) + // .def("downloadsPath", &IOrganizer::downloadsPath) + // .def("overwritePath", &IOrganizer::overwritePath) + // .def("basePath", &IOrganizer::basePath) + // .def("modsPath", &IOrganizer::modsPath) + // .def("appVersion", &IOrganizer::appVersion) + // .def("createMod", &IOrganizer::createMod, bpy::return_value_policy(), bpy::arg("name")) + // .def("getGame", &IOrganizer::getGame, bpy::return_value_policy(), bpy::arg("name")) + // .def("modDataChanged", &IOrganizer::modDataChanged, bpy::arg("mod")) + // .def("isPluginEnabled", +[](IOrganizer* o, IPlugin* plugin) { return o->isPluginEnabled(plugin); }, bpy::arg("plugin")) + // .def("isPluginEnabled", +[](IOrganizer* o, QString const& plugin) { return o->isPluginEnabled(plugin); }, bpy::arg("plugin")) + // .def("pluginSetting", &IOrganizer::pluginSetting, (bpy::arg("plugin_name"), "key")) + // .def("setPluginSetting", &IOrganizer::setPluginSetting, (bpy::arg("plugin_name"), "key", "value")) + // .def("persistent", &IOrganizer::persistent, (bpy::arg("plugin_name"), "key", bpy::arg("default") = QVariant())) + // .def("setPersistent", &IOrganizer::setPersistent, (bpy::arg("plugin_name"), "key", "value", bpy::arg("sync") = true)) + // .def("pluginDataPath", &IOrganizer::pluginDataPath) + // .def("installMod", &IOrganizer::installMod, bpy::return_value_policy(), (bpy::arg("filename"), bpy::arg("name_suggestion") = "")) + // .def("resolvePath", &IOrganizer::resolvePath, bpy::arg("filename")) + // .def("listDirectories", &IOrganizer::listDirectories, bpy::arg("directory")) + + // // Provide multiple overloads of findFiles: + // .def("findFiles", +[](const IOrganizer* o, QString const& p, std::function f) { return o->findFiles(p, f); }, + // (bpy::arg("path"), "filter")) + + // // In C++, it is possible to create a QStringList implicitly from a single QString. This is not possible with the current + // // converters in python (and I do not think it is a good idea to have it everywhere), but here it is nice to be able to + // // pass a single string, so we add an extra overload. + // // Important: the order matters, because a Python string can be converted to a QStringList since it is a sequence of + // // single-character strings: + // .def("findFiles", +[](const IOrganizer* o, QString const& p, const QStringList& gf) { return o->findFiles(p, gf); }, + // (bpy::arg("path"), "patterns")) + // .def("findFiles", +[](const IOrganizer* o, QString const& p, const QString& f) { return o->findFiles(p, QStringList{ f }); }, + // (bpy::arg("path"), "pattern")) + + // .def("getFileOrigins", &IOrganizer::getFileOrigins, bpy::arg("filename")) + // .def("findFileInfos", &IOrganizer::findFileInfos, (bpy::arg("path"), "filter")) + + // .def("virtualFileTree", &IOrganizer::virtualFileTree) + + // .def("downloadManager", &IOrganizer::downloadManager, bpy::return_value_policy()) + // .def("pluginList", &IOrganizer::pluginList, bpy::return_value_policy()) + // .def("modList", &IOrganizer::modList, bpy::return_value_policy()) + // .def("profile", &IOrganizer::profile, bpy::return_value_policy()) + + // // Custom implementation for startApplication and waitForApplication because 1) HANDLE (= void*) is not properly + // // converted from/to python, and 2) we need to convert the by-ptr argument to a return-tuple for waitForApplication: + // .def("startApplication", + // +[](IOrganizer* o, const QString& executable, const QStringList& args, const QString& cwd, const QString& profile, + // const QString& forcedCustomOverwrite, bool ignoreCustomOverwrite) { + // return (std::uintptr_t) o->startApplication(executable, args, cwd, profile, forcedCustomOverwrite, ignoreCustomOverwrite); + // }, (bpy::arg("executable"), (bpy::arg("args") = QStringList()), (bpy::arg("cwd") = ""), (bpy::arg("profile") = ""), + // (bpy::arg("forcedCustomOverwrite") = ""), (bpy::arg("ignoreCustomOverwrite") = false)), bpy::return_value_policy()) + // .def("waitForApplication", +[](IOrganizer *o, std::uintptr_t handle, bool refresh) { + // DWORD returnCode; + // bool result = o->waitForApplication((HANDLE)handle, refresh, &returnCode); + // return std::make_tuple(result, returnCode); + // }, (bpy::arg("handle"), bpy::arg("refresh") = true)) + // .def("refresh", &IOrganizer::refresh, (bpy::arg("save_changes") = true)) + // .def("managedGame", &IOrganizer::managedGame, bpy::return_value_policy()) + + // .def("onAboutToRun", &IOrganizer::onAboutToRun, bpy::arg("callback")) + // .def("onFinishedRun", &IOrganizer::onFinishedRun, bpy::arg("callback")) + // .def("onUserInterfaceInitialized", &IOrganizer::onUserInterfaceInitialized, bpy::arg("callback")) + // .def("onProfileCreated", &IOrganizer::onProfileCreated, bpy::arg("callback")) + // .def("onProfileRenamed", &IOrganizer::onProfileRenamed, bpy::arg("callback")) + // .def("onProfileRemoved", &IOrganizer::onProfileRemoved, bpy::arg("callback")) + // .def("onProfileChanged", &IOrganizer::onProfileChanged, bpy::arg("callback")) + + // .def("onPluginSettingChanged", &IOrganizer::onPluginSettingChanged, bpy::arg("callback")) + // .def("onPluginEnabled", +[](IOrganizer* o, std::function const& func) { + // o->onPluginEnabled(func); + // }, bpy::arg("callback")) + // .def("onPluginEnabled", +[](IOrganizer* o, QString const& name, std::function const& func) { + // o->onPluginEnabled(name, func); + // }, (bpy::arg("name"), bpy::arg("callback"))) + // .def("onPluginDisabled", +[](IOrganizer* o, std::function const& func) { + // o->onPluginDisabled(func); + // }, bpy::arg("callback")) + // .def("onPluginDisabled", +[](IOrganizer* o, QString const& name, std::function const& func) { + // o->onPluginDisabled(name, func); + // }, (bpy::arg("name"), bpy::arg("callback"))) + + // // DEPRECATED: + // .def("getMod", +[](IOrganizer* o, QString const& name) { + // utils::show_deprecation_warning("getMod", + // "IOrganizer::getMod(str) is deprecated, use IModList::getMod(str) instead."); + // return o->modList()->getMod(name); + // }, bpy::return_value_policy(), bpy::arg("name")) + // .def("removeMod", +[](IOrganizer* o, IModInterface *mod) { + // utils::show_deprecation_warning("removeMod", + // "IOrganizer::removeMod(IModInterface) is deprecated, use IModList::removeMod(IModInterface) instead."); + // return o->modList()->removeMod(mod); + // }, bpy::arg("mod")) + // .def("modsSortedByProfilePriority", +[](IOrganizer* o) { + // utils::show_deprecation_warning("modsSortedByProfilePriority", + // "IOrganizer::modsSortedByProfilePriority() is deprecated, use IModList::allModsByProfilePriority() instead."); + // return o->modList()->allModsByProfilePriority(); + // }) + // .def("refreshModList", +[](IOrganizer* o, bool s) { + // utils::show_deprecation_warning("refreshModList", + // "IOrganizer::refreshModList(bool) is deprecated, use IOrganizer::refresh(bool) instead."); + // o->refresh(s); + // }, (bpy::arg("save_changes") = true)) + // .def("onModInstalled", +[](IOrganizer* organizer, const std::function& func) { + // utils::show_deprecation_warning("onModInstalled", + // "IOrganizer::onModInstalled(Callable[[str], None]) is deprecated, " + // "use IModList::onModInstalled(Callable[[IModInterface], None]) instead."); + // return organizer->modList()->onModInstalled([func](MOBase::IModInterface* m) { func(m->name()); });; + // }, bpy::arg("callback")) + + // .def("getPluginDataPath", &IOrganizer::getPluginDataPath) + // .staticmethod("getPluginDataPath") + + // ; + + // // FileTreeEntry Scope: + // auto fileTreeEntryClass = bpy::class_("FileTreeEntry", bpy::no_init); + // { + + // bpy::scope scope = fileTreeEntryClass; + + // bpy::enum_("FileTypes") + // .value("FILE_OR_DIRECTORY", FileTreeEntry::FILE_OR_DIRECTORY) + // .value("FILE", FileTreeEntry::FILE) + // .value("DIRECTORY", FileTreeEntry::DIRECTORY) + // .export_values() + // ; + + // fileTreeEntryClass + + // .def("isFile", &FileTreeEntry::isFile) + // .def("isDir", &FileTreeEntry::isDir) + // // Forcing the conversion to FileTypeS to avoid having to expose FileType in python: + // .def("fileType", +[](FileTreeEntry* p) { return FileTreeEntry::FileTypes{ p->fileType() }; }) + // // This should probably not be exposed in python since we provide automatic downcast: + // // .def("getTree", static_cast(FileTreeEntry::*)()>(&FileTreeEntry::astree)) + // .def("name", &FileTreeEntry::name) + // .def("suffix", &FileTreeEntry::suffix) + // .def("hasSuffix", +[](FileTreeEntry* entry, QStringList suffixes) { return entry->hasSuffix(suffixes); }, bpy::arg("suffixes")) + // .def("hasSuffix", +[](FileTreeEntry* entry, QString suffix) { return entry->hasSuffix(suffix); }, bpy::arg("suffix")) + // .def("parent", static_cast(FileTreeEntry::*)()>(&FileTreeEntry::parent), "[optional]") + // .def("path", &FileTreeEntry::path, bpy::arg("sep") = "\\") + // .def("pathFrom", &FileTreeEntry::pathFrom, (bpy::arg("tree"), bpy::arg("sep") = "\\")) + + // // Mutable operation: + // .def("detach", &FileTreeEntry::detach) + // .def("moveTo", &FileTreeEntry::moveTo, bpy::arg("tree")) + + // // Special methods: + // .def("__eq__", +[](const FileTreeEntry* entry, QString other) { + // return entry->compare(other) == 0; + // }) + // .def("__eq__", +[](const FileTreeEntry* entry, std::shared_ptr other) { + // return entry == other.get(); + // }) + + // // Special methods for debug: + // .def("__repr__", +[](const FileTreeEntry* entry) { return "FileTreeEntry(\"" + entry->name() + "\")"; }) + // ; + // } + + // // IFileTree scope: + // auto iFileTreeClass = bpy::class_, boost::noncopyable>("IFileTree", bpy::no_init); + // { + + // bpy::scope scope = iFileTreeClass; + + // bpy::enum_("InsertPolicy") + // .value("FAIL_IF_EXISTS", IFileTree::InsertPolicy::FAIL_IF_EXISTS) + // .value("REPLACE", IFileTree::InsertPolicy::REPLACE) + // .value("MERGE", IFileTree::InsertPolicy::MERGE) + // .export_values() + // ; + + // bpy::enum_("WalkReturn") + // .value("CONTINUE", IFileTree::WalkReturn::CONTINUE) + // .value("STOP", IFileTree::WalkReturn::STOP) + // .value("SKIP", IFileTree::WalkReturn::SKIP) + // .export_values() + // ; + + // iFileTreeClass + + // // Non-mutable operations: + // .def("exists", static_cast(&IFileTree::exists), + // (bpy::arg("path"), bpy::arg("type") = IFileTree::FILE_OR_DIRECTORY)) + // .def("find", static_cast(IFileTree::*)(QString, IFileTree::FileTypes)>(&IFileTree::find), + // bpy::return_value_policy>(), (bpy::arg("path"), bpy::arg("type") = IFileTree::FILE_OR_DIRECTORY), "[optional]") + // .def("pathTo", &IFileTree::pathTo, (bpy::arg("entry"), bpy::arg("sep") = "\\")) + + // // Note: walk() would probably be better as a generator in python, but it is likely impossible to construct + // // from the C++ walk() method. + // .def("walk", &IFileTree::walk, (bpy::arg("callback"), bpy::arg("sep") = "\\")) + + // // Kind-of-static operations: + // .def("createOrphanTree", &IFileTree::createOrphanTree, bpy::arg("name") = "") + + // // addFile() and addDirectory throws exception instead of returning null pointer in order + // // to have better traces. + // .def("addFile", +[](IFileTree* w, QString path, bool replaceIfExists) { + // auto result = w->addFile(path, replaceIfExists); + // if (result == nullptr) { + // throw std::logic_error("addFile failed"); + // } + // return result; + // }, (bpy::arg("path"), bpy::arg("replace_if_exists") = false)) + // .def("addDirectory", +[](IFileTree* w, QString path) { + // auto result = w->addDirectory(path); + // if (result == nullptr) { + // throw std::logic_error("addDirectory failed"); + // } + // return result; + // }, bpy::arg("path")) + + // // Merge needs custom return types depending if the user wants overrides or not. A failure is translated + // // into an exception for easier tracing and handling. + // .def("merge", +[](IFileTree* p, std::shared_ptr other, bool returnOverwrites) -> std::variant { + // IFileTree::OverwritesType overwrites; + // auto result = p->merge(other, returnOverwrites ? &overwrites : nullptr); + // if (result == IFileTree::MERGE_FAILED) { + // throw std::logic_error("merge failed"); + // } + // if (returnOverwrites) { + // return { overwrites }; + // } + // return { result }; + // }, (bpy::arg("other"), bpy::arg("overwrites") = false)) + + // // Insert and erase returns an iterator, which makes no sense in python, so we convert it to bool. Erase is also + // // renamed "remove" since "erase" is very C++. + // .def("insert", +[](IFileTree* p, std::shared_ptr entry, IFileTree::InsertPolicy insertPolicy) { + // return p->insert(entry, insertPolicy) == p->end(); + // }, (bpy::arg("entry"), bpy::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS)) + + // .def("remove", +[](IFileTree* p, QString name) { return p->erase(name).first != p->end(); }, bpy::arg("name")) + // .def("remove", +[](IFileTree* p, std::shared_ptr entry) { return p->erase(entry) != p->end(); }, bpy::arg("entry")) + + // .def("move", &IFileTree::move, (bpy::arg("entry"), "path", bpy::arg("policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS)) + // .def("copy", +[](IFileTree* w, std::shared_ptr entry, QString path, IFileTree::InsertPolicy insertPolicy) { + // auto result = w->copy(entry, path, insertPolicy); + // if (result == nullptr) { + // throw std::logic_error("copy failed"); + // } + // return result; + // }, (bpy::arg("entry"), bpy::arg("path") = "", bpy::arg("insert_policy") = IFileTree::InsertPolicy::FAIL_IF_EXISTS)) + + // .def("clear", &IFileTree::clear) + // .def("removeAll", &IFileTree::removeAll, bpy::arg("names")) + // .def("removeIf", &IFileTree::removeIf, bpy::arg("filter")) + + // // Special methods: + // .def("__getitem__", static_cast(IFileTree::*)(std::size_t)>(&IFileTree::at), + // bpy::return_value_policy>()) + // .def("__iter__", bpy::range>>( + // static_cast(&IFileTree::begin), + // static_cast(&IFileTree::end))) + // .def("__len__", &IFileTree::size) + // .def("__bool__", +[](const IFileTree* tree) { return !tree->empty(); }) + // .def("__repr__", +[](const IFileTree* entry) { return "IFileTree(\"" + entry->name() + "\")"; }) + // ; + // } + + py::class_("IProfile") + .def("name", &IProfile::name) + .def("absolutePath", &IProfile::absolutePath) + .def("localSavesEnabled", &IProfile::localSavesEnabled) + .def("localSettingsEnabled", &IProfile::localSettingsEnabled) + .def("invalidationActive", [](const IProfile* p) { + bool supported; + bool active = p->invalidationActive(&supported); + return py::make_tuple(active, supported); + }) + .def("absoluteIniFilePath", &IProfile::absoluteIniFilePath, py::arg("inifile")) + ; + + // bpy::class_("IModRepositoryBridge", bpy::no_init) + // .def("requestDescription", &IModRepositoryBridge::requestDescription, (bpy::arg("game_name"), "mod_id", "user_data")) + // .def("requestFiles", &IModRepositoryBridge::requestFiles, (bpy::arg("game_name"), "mod_id", "user_data")) + // .def("requestFileInfo", &IModRepositoryBridge::requestFileInfo, (bpy::arg("game_name"), "mod_id", "file_id", "user_data")) + // .def("requestDownloadURL", &IModRepositoryBridge::requestDownloadURL, (bpy::arg("game_name"), "mod_id", "file_id", "user_data")) + // .def("requestToggleEndorsement", &IModRepositoryBridge::requestToggleEndorsement, (bpy::arg("game_name"), "mod_id", "mod_version", "endorse", "user_data")) + + // Q_DELEGATE(IModRepositoryBridge, QObject, "_object") + // ; + + // bpy::class_("ModRepositoryFileInfo", bpy::no_init) + // .def(bpy::init(bpy::arg("other"))) + // .def(bpy::init>((bpy::arg("game_name"), "mod_id", "file_id"))) + // .def("__str__", &ModRepositoryFileInfo::toString) + // .def("createFromJson", &ModRepositoryFileInfo::createFromJson, bpy::arg("data")).staticmethod("createFromJson") + // .def_readwrite("name", &ModRepositoryFileInfo::name) + // .def_readwrite("uri", &ModRepositoryFileInfo::uri) + // .def_readwrite("description", &ModRepositoryFileInfo::description) + // .def_readwrite("version", &ModRepositoryFileInfo::version) + // .def_readwrite("newestVersion", &ModRepositoryFileInfo::newestVersion) + // .def_readwrite("categoryID", &ModRepositoryFileInfo::categoryID) + // .def_readwrite("modName", &ModRepositoryFileInfo::modName) + // .def_readwrite("gameName", &ModRepositoryFileInfo::gameName) + // .def_readwrite("modID", &ModRepositoryFileInfo::modID) + // .def_readwrite("fileID", &ModRepositoryFileInfo::fileID) + // .def_readwrite("fileSize", &ModRepositoryFileInfo::fileSize) + // .def_readwrite("fileName", &ModRepositoryFileInfo::fileName) + // .def_readwrite("fileCategory", &ModRepositoryFileInfo::fileCategory) + // .def_readwrite("fileTime", &ModRepositoryFileInfo::fileTime) + // .def_readwrite("repository", &ModRepositoryFileInfo::repository) + // .def_readwrite("userData", &ModRepositoryFileInfo::userData) + // ; + + // bpy::class_("IDownloadManager", bpy::no_init) + // .def("startDownloadURLs", &IDownloadManager::startDownloadURLs, bpy::arg("urls")) + // .def("startDownloadNexusFile", &IDownloadManager::startDownloadNexusFile, (bpy::arg("mod_id"), "file_id")) + // .def("downloadPath", &IDownloadManager::downloadPath, bpy::arg("id")) + // .def("onDownloadComplete", &IDownloadManager::onDownloadComplete, bpy::arg("callback")) + // .def("onDownloadPaused", &IDownloadManager::onDownloadPaused, bpy::arg("callback")) + // .def("onDownloadFailed", &IDownloadManager::onDownloadFailed, bpy::arg("callback")) + // .def("onDownloadRemoved", &IDownloadManager::onDownloadRemoved, bpy::arg("callback")) + // ; + + // bpy::class_("IInstallationManager", bpy::no_init) + // .def("getSupportedExtensions", &IInstallationManager::getSupportedExtensions) + // .def("extractFile", &IInstallationManager::extractFile, (bpy::arg("entry"), bpy::arg("silent") = false)) + // .def("extractFiles", &IInstallationManager::extractFiles, (bpy::arg("entries"), bpy::arg("silent") = false)) + // .def("createFile", +[](IInstallationManager* m, std::shared_ptr entry) { + // return m->createFile(utils::clean_shared_ptr(entry)); + // }, bpy::arg("entry")) + + // // accept both QString and GuessedValue since the conversion is not automatic in Python, and + // // return a tuple to get back the mod name and the mod ID + // .def("installArchive", +[](IInstallationManager* m, std::variant> modName, QString archive, int modId) { + // GuessedValue tmp; + // if (auto* p = std::get_if(&modName)) { + // tmp = *p; + // } + // else { + // tmp = std::get>(modName); + // } + // auto result = m->installArchive(tmp, archive, modId); + // return std::make_tuple(result, static_cast(tmp), modId); + // }, (bpy::arg("mod_name"), "archive", bpy::arg("mod_id") = 0)) + // ; + + py::enum_("EndorsedState") + .value("ENDORSED_FALSE", EndorsedState::ENDORSED_FALSE) + .value("ENDORSED_TRUE", EndorsedState::ENDORSED_TRUE) + .value("ENDORSED_UNKNOWN", EndorsedState::ENDORSED_UNKNOWN) + .value("ENDORSED_NEVER", EndorsedState::ENDORSED_NEVER) + ; + + py::enum_("TrackedState") + .value("TRACKED_FALSE", TrackedState::TRACKED_FALSE) + .value("TRACKED_TRUE", TrackedState::TRACKED_TRUE) + .value("TRACKED_UNKNOWN", TrackedState::TRACKED_UNKNOWN) + ; + + // bpy::class_("IModInterface", bpy::no_init) + // .def("name", &IModInterface::name) + // .def("absolutePath", &IModInterface::absolutePath) + + // .def("comments", &IModInterface::comments) + // .def("notes", &IModInterface::notes) + // .def("gameName", &IModInterface::gameName) + // .def("repository", &IModInterface::repository) + // .def("nexusId", &IModInterface::nexusId) + // .def("version", &IModInterface::version) + // .def("newestVersion", &IModInterface::newestVersion) + // .def("ignoredVersion", &IModInterface::ignoredVersion) + // .def("installationFile", &IModInterface::installationFile) + // .def("converted", &IModInterface::converted) + // .def("validated", &IModInterface::validated) + // .def("color", &IModInterface::color) + // .def("url", &IModInterface::url) + // .def("primaryCategory", &IModInterface::primaryCategory) + // .def("categories", &IModInterface::categories) + // .def("trackedState", &IModInterface::trackedState) + // .def("endorsedState", &IModInterface::endorsedState) + // .def("fileTree", &IModInterface::fileTree) + // .def("isOverwrite", &IModInterface::isOverwrite) + // .def("isBackup", &IModInterface::isBackup) + // .def("isSeparator", &IModInterface::isSeparator) + // .def("isForeign", &IModInterface::isForeign) + + // .def("setVersion", &IModInterface::setVersion, bpy::arg("version")) + // .def("setNewestVersion", &IModInterface::setNewestVersion, bpy::arg("version")) + // .def("setIsEndorsed", &IModInterface::setIsEndorsed, bpy::arg("endorsed")) + // .def("setNexusID", &IModInterface::setNexusID, bpy::arg("nexus_id")) + // .def("addNexusCategory", &IModInterface::addNexusCategory, bpy::arg("category_id")) + // .def("addCategory", &IModInterface::addCategory, bpy::arg("name")) + // .def("removeCategory", &IModInterface::removeCategory, bpy::arg("name")) + // .def("setGameName", &IModInterface::setGameName, bpy::arg("name")) + // .def("setUrl", &IModInterface::setUrl, bpy::arg("url")) + // .def("pluginSetting", &IModInterface::pluginSetting, (bpy::arg("plugin_name"), "key", bpy::arg("default") = QVariant())) + // .def("pluginSettings", &IModInterface::pluginSettings, bpy::arg("plugin_name")) + // .def("setPluginSetting", &IModInterface::setPluginSetting, (bpy::arg("plugin_name"), "key", bpy::arg("value"))) + // .def("clearPluginSettings", &IModInterface::clearPluginSettings, bpy::arg("plugin_name")) + + // ; + + py::enum_(m, "GuessQuality") + .value("INVALID", MOBase::GUESS_INVALID) + .value("FALLBACK", MOBase::GUESS_FALLBACK) + .value("GOOD", MOBase::GUESS_GOOD) + .value("META", MOBase::GUESS_META) + .value("PRESET", MOBase::GUESS_PRESET) + .value("USER", MOBase::GUESS_USER) + ; + + // bpy::class_, boost::noncopyable>("GuessedString") + // .def(bpy::init<>()) + // .def(bpy::init((bpy::arg("value"), bpy::arg("quality") = EGuessQuality::GUESS_USER))) + // .def("update", + // static_cast& (GuessedValue::*)(const QString&)>(&GuessedValue::update), + // bpy::return_self<>(), bpy::arg("value")) + // .def("update", + // static_cast& (GuessedValue::*)(const QString&, EGuessQuality)>(&GuessedValue::update), + // bpy::return_self<>(), (bpy::arg("value"), "quality")) + + // // Methods to simulate the assignment operator: + // .def("reset", +[](GuessedValue* gv) { + // *gv = GuessedValue(); }, bpy::return_self<>()) + // .def("reset", +[](GuessedValue* gv, const QString& value, EGuessQuality eq) { + // *gv = GuessedValue(value, eq); }, bpy::return_self<>(), (bpy::arg("value"), "quality")) + // .def("reset", +[](GuessedValue* gv, const GuessedValue& other) { + // *gv = other; }, bpy::return_self<>(), bpy::arg("other")) + + // // Use an intermediate lambda to avoid having to register the std::function conversion: + // .def("setFilter", +[](GuessedValue* gv, std::function(QString const&)> fn) { + // gv->setFilter([fn](QString& s) { + // auto ret = fn(s); + // return std::visit([&s](auto v) { + // if constexpr (std::is_same_v) { + // s = v; + // return true; + // } + // else if constexpr (std::is_same_v) { + // return v; + // } + // }, ret); + // }); + // }, bpy::arg("filter")) + + // // This makes a copy in python but it more practical than exposing an iterator: + // .def("variants", &GuessedValue::variants, bpy::return_value_policy()) + // .def("__str__", &MOBase::GuessedValue::operator const QString&, bpy::return_value_policy()) + // ; + + py::enum_(m, "PluginState") + .value("missing", IPluginList::STATE_MISSING) + .value("inactive", IPluginList::STATE_INACTIVE) + .value("active", IPluginList::STATE_ACTIVE) + + .value("MISSING", IPluginList::STATE_MISSING) + .value("INACTIVE", IPluginList::STATE_INACTIVE) + .value("ACTIVE", IPluginList::STATE_ACTIVE) + ; + + m.def("testFlags1", [](IPluginList::PluginStates states) { + std::vector res; + if (states.testFlag(IPluginList::STATE_MISSING)) { + res.push_back("missing"); + } + if (states.testFlag(IPluginList::STATE_INACTIVE)) { + res.push_back("inactive"); + } + if (states.testFlag(IPluginList::STATE_ACTIVE)) { + res.push_back("active"); + } + return res; + }); + + m.def("testFlags2", [](QStringList const& states) { + IPluginList::PluginStates res; + if (states.contains("missing")) { + res |= IPluginList::STATE_MISSING; + } + if (states.contains("inactive")) { + res |= IPluginList::STATE_INACTIVE; + } + if (states.contains("active")) { + res |= IPluginList::STATE_ACTIVE; + } + return res; + }); + + // bpy::class_("IPluginList", bpy::no_init) + // .def("state", &MOBase::IPluginList::state, bpy::arg("name")) + // .def("priority", &MOBase::IPluginList::priority, bpy::arg("name")) + // .def("setPriority", &MOBase::IPluginList::setPriority, (bpy::arg("name"), "priority")) + // .def("loadOrder", &MOBase::IPluginList::loadOrder, bpy::arg("name")) + // .def("isMaster", &MOBase::IPluginList::isMaster, 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")) + + // // Kept but deprecated for backward compatibility: + // .def("onPluginStateChanged", +[](IPluginList* modList, const std::function& fn) { + // utils::show_deprecation_warning("onPluginStateChanged", + // "onPluginStateChanged(Callable[[str, IPluginList.PluginStates], None]) is deprecated, " + // "use onPluginStateChanged(Callable[[Dict[str, IPluginList.PluginStates], None]) instead."); + // return modList->onPluginStateChanged([fn](auto const& map) { + // for (const auto& entry : map) { + // fn(entry.first, entry.second); + // } + // }); + // }, bpy::arg("callback")) + // .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")) + // ; + + // bpy::enum_("ModState") + // .value("exists", IModList::STATE_EXISTS) + // .value("active", IModList::STATE_ACTIVE) + // .value("essential", IModList::STATE_ESSENTIAL) + // .value("empty", IModList::STATE_EMPTY) + // .value("endorsed", IModList::STATE_ENDORSED) + // .value("valid", IModList::STATE_VALID) + // .value("alternate", IModList::STATE_ALTERNATE) + + // .value("EXISTS", IModList::STATE_EXISTS) + // .value("ACTIVE", IModList::STATE_ACTIVE) + // .value("ESSENTIAL", IModList::STATE_ESSENTIAL) + // .value("EMPTY", IModList::STATE_EMPTY) + // .value("ENDORSED", IModList::STATE_ENDORSED) + // .value("VALID", IModList::STATE_VALID) + // .value("ALTERNATE", IModList::STATE_ALTERNATE) + // ; + + // bpy::class_("IModList", bpy::no_init) + // .def("displayName", &MOBase::IModList::displayName, bpy::arg("name")) + // .def("allMods", &MOBase::IModList::allMods) + // .def("allModsByProfilePriority", &MOBase::IModList::allModsByProfilePriority, bpy::arg("profile") = bpy::ptr((IProfile*)nullptr)) + + // .def("getMod", &MOBase::IModList::getMod, bpy::return_value_policy(), bpy::arg("name")) + // .def("removeMod", &MOBase::IModList::removeMod, bpy::arg("mod")) + // .def("renameMod", &MOBase::IModList::renameMod, bpy::return_value_policy(), (bpy::arg("mod"), bpy::arg("name"))) + + // .def("state", &MOBase::IModList::state, bpy::arg("name")) + // .def("setActive", + // static_cast(&MOBase::IModList::setActive), (bpy::arg("names"), "active")) + // .def("setActive", + // static_cast(&MOBase::IModList::setActive), (bpy::arg("name"), "active")) + // .def("priority", &MOBase::IModList::priority, bpy::arg("name")) + // .def("setPriority", &MOBase::IModList::setPriority, (bpy::arg("name"), "priority")) + + // // Kept but deprecated for backward compatibility: + // .def("onModStateChanged", +[](IModList* modList, const std::function& fn) { + // utils::show_deprecation_warning("onModStateChanged", + // "onModStateChanged(Callable[[str, IModList.ModStates], None]) is deprecated, " + // "use onModStateChanged(Callable[[Dict[str, IModList.ModStates], None]) instead."); + // return modList->onModStateChanged([fn](auto const& map) { + // for (const auto& entry : map) { + // fn(entry.first, entry.second); + // } + // }); + // }, bpy::arg("callback")) + + // .def("onModInstalled", &MOBase::IModList::onModInstalled, bpy::arg("callback")) + // .def("onModRemoved", &MOBase::IModList::onModRemoved, bpy::arg("callback")) + // .def("onModStateChanged", &MOBase::IModList::onModStateChanged, bpy::arg("callback")) + // .def("onModMoved", &MOBase::IModList::onModMoved, bpy::arg("callback")) + // ; + + // // Note: localizedName, master, requirements and enabledByDefault have to go in all the plugin wrappers declaration, + // // since the default functions are specific to each wrapper, otherwise in turns into an + // // infinite recursion mess. + // bpy::class_("IPlugin") + // .def("init", bpy::pure_virtual(&MOBase::IPlugin::init), bpy::arg("organizer")) + // .def("name", bpy::pure_virtual(&MOBase::IPlugin::name)) + // .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginWrapper::localizedName_Default) + // .def("master", &MOBase::IPlugin::master, &IPluginWrapper::master_Default) + // .def("author", bpy::pure_virtual(&MOBase::IPlugin::author)) + // .def("description", bpy::pure_virtual(&MOBase::IPlugin::description)) + // .def("version", bpy::pure_virtual(&MOBase::IPlugin::version)) + // .def("requirements", &MOBase::IPlugin::requirements, &IPluginWrapper::requirements_Default) + // .def("settings", bpy::pure_virtual(&MOBase::IPlugin::settings)) + // .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginWrapper::enabledByDefault_Default) + // ; + + // bpy::class_, boost::noncopyable>("IPluginDiagnose") + // .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginDiagnoseWrapper::localizedName_Default) + // .def("master", &MOBase::IPlugin::master, &IPluginDiagnoseWrapper::master_Default) + // .def("requirements", &MOBase::IPlugin::requirements, &IPluginDiagnoseWrapper::requirements_Default) + // .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginDiagnoseWrapper::enabledByDefault_Default) + + // .def("activeProblems", bpy::pure_virtual(&MOBase::IPluginDiagnose::activeProblems)) + // .def("shortDescription", bpy::pure_virtual(&MOBase::IPluginDiagnose::shortDescription), bpy::arg("key")) + // .def("fullDescription", bpy::pure_virtual(&MOBase::IPluginDiagnose::fullDescription), bpy::arg("key")) + // .def("hasGuidedFix", bpy::pure_virtual(&MOBase::IPluginDiagnose::hasGuidedFix), bpy::arg("key")) + // .def("startGuidedFix", bpy::pure_virtual(&MOBase::IPluginDiagnose::startGuidedFix), bpy::arg("key")) + // .def("_invalidate", &IPluginDiagnoseWrapper::invalidate) + // ; + + // bpy::class_("Mapping", bpy::init<>()) + // .def("__init__", bpy::make_constructor(+[](QString src, QString dst, bool dir, bool crt) -> Mapping* { + // return new Mapping{ src, dst, dir, crt }; + // }, bpy::default_call_policies(), + // (bpy::arg("source"), bpy::arg("destination"), bpy::arg("is_directory"), bpy::arg("create_target") = false))) + // .def_readwrite("source", &Mapping::source) + // .def_readwrite("destination", &Mapping::destination) + // .def_readwrite("isDirectory", &Mapping::isDirectory) + // .def_readwrite("createTarget", &Mapping::createTarget) + // .def("__str__", +[](Mapping * m) { + // return fmt::format(L"Mapping({}, {}, {}, {})", m->source.toStdWString(), m->destination.toStdWString(), m->isDirectory, m->createTarget); + // }) + // ; + + // bpy::class_, boost::noncopyable>("IPluginFileMapper") + // .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginFileMapperWrapper::localizedName_Default) + // .def("master", &MOBase::IPlugin::master, &IPluginFileMapperWrapper::master_Default) + // .def("requirements", &MOBase::IPlugin::requirements, &IPluginFileMapperWrapper::requirements_Default) + // .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginFileMapperWrapper::enabledByDefault_Default) + + // .def("mappings", bpy::pure_virtual(&MOBase::IPluginFileMapper::mappings)) + // ; + + // bpy::enum_("LoadOrderMechanism") + // .value("FileTime", MOBase::IPluginGame::LoadOrderMechanism::FileTime) + // .value("PluginsTxt", MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) + + // .value("FILE_TIME", MOBase::IPluginGame::LoadOrderMechanism::FileTime) + // .value("PLUGINS_TXT", MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) + // ; + + // bpy::enum_("SortMechanism") + // .value("NONE", MOBase::IPluginGame::SortMechanism::NONE) + // .value("MLOX", MOBase::IPluginGame::SortMechanism::MLOX) + // .value("BOSS", MOBase::IPluginGame::SortMechanism::BOSS) + // .value("LOOT", MOBase::IPluginGame::SortMechanism::LOOT) + // ; + + // // This doesn't actually do the conversion, but might be convenient for accessing the names for enum bits + // bpy::enum_("ProfileSetting") + // .value("mods", MOBase::IPluginGame::MODS) + // .value("configuration", MOBase::IPluginGame::CONFIGURATION) + // .value("savegames", MOBase::IPluginGame::SAVEGAMES) + // .value("preferDefaults", MOBase::IPluginGame::PREFER_DEFAULTS) + + // .value("MODS", MOBase::IPluginGame::MODS) + // .value("CONFIGURATION", MOBase::IPluginGame::CONFIGURATION) + // .value("SAVEGAMES", MOBase::IPluginGame::SAVEGAMES) + // .value("PREFER_DEFAULTS", MOBase::IPluginGame::PREFER_DEFAULTS) + // ; + + // bpy::class_, boost::noncopyable>("IPluginGame") + // .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginGameWrapper::localizedName_Default) + // .def("master", &MOBase::IPlugin::master, &IPluginGameWrapper::master_Default) + + // .def("detectGame", bpy::pure_virtual(&MOBase::IPluginGame::detectGame)) + // .def("gameName", bpy::pure_virtual(&MOBase::IPluginGame::gameName)) + // .def("initializeProfile", bpy::pure_virtual(&MOBase::IPluginGame::initializeProfile), (bpy::arg("directory"), "settings")) + // .def("listSaves", bpy::pure_virtual(&MOBase::IPluginGame::listSaves), bpy::arg("folder")) + // .def("isInstalled", bpy::pure_virtual(&MOBase::IPluginGame::isInstalled)) + // .def("gameIcon", bpy::pure_virtual(&MOBase::IPluginGame::gameIcon)) + // .def("gameDirectory", bpy::pure_virtual(&MOBase::IPluginGame::gameDirectory)) + // .def("dataDirectory", bpy::pure_virtual(&MOBase::IPluginGame::dataDirectory)) + // .def("setGamePath", bpy::pure_virtual(&MOBase::IPluginGame::setGamePath), bpy::arg("path")) + // .def("documentsDirectory", bpy::pure_virtual(&MOBase::IPluginGame::documentsDirectory)) + // .def("savesDirectory", bpy::pure_virtual(&MOBase::IPluginGame::savesDirectory)) + // .def("executables", bpy::pure_virtual(&MOBase::IPluginGame::executables)) + // .def("executableForcedLoads", bpy::pure_virtual(&MOBase::IPluginGame::executableForcedLoads)) + // .def("steamAPPId", bpy::pure_virtual(&MOBase::IPluginGame::steamAPPId)) + // .def("primaryPlugins", bpy::pure_virtual(&MOBase::IPluginGame::primaryPlugins)) + // .def("gameVariants", bpy::pure_virtual(&MOBase::IPluginGame::gameVariants)) + // .def("setGameVariant", bpy::pure_virtual(&MOBase::IPluginGame::setGameVariant), bpy::arg("variant")) + // .def("binaryName", bpy::pure_virtual(&MOBase::IPluginGame::binaryName)) + // .def("gameShortName", bpy::pure_virtual(&MOBase::IPluginGame::gameShortName)) + // .def("primarySources", bpy::pure_virtual(&MOBase::IPluginGame::primarySources)) + // .def("validShortNames", bpy::pure_virtual(&MOBase::IPluginGame::validShortNames)) + // .def("gameNexusName", bpy::pure_virtual(&MOBase::IPluginGame::gameNexusName)) + // .def("iniFiles", bpy::pure_virtual(&MOBase::IPluginGame::iniFiles)) + // .def("DLCPlugins", bpy::pure_virtual(&MOBase::IPluginGame::DLCPlugins)) + // .def("CCPlugins", bpy::pure_virtual(&MOBase::IPluginGame::CCPlugins)) + // .def("loadOrderMechanism", bpy::pure_virtual(&MOBase::IPluginGame::loadOrderMechanism)) + // .def("sortMechanism", bpy::pure_virtual(&MOBase::IPluginGame::sortMechanism)) + // .def("nexusModOrganizerID", bpy::pure_virtual(&MOBase::IPluginGame::nexusModOrganizerID)) + // .def("nexusGameID", bpy::pure_virtual(&MOBase::IPluginGame::nexusGameID)) + // .def("looksValid", bpy::pure_virtual(&MOBase::IPluginGame::looksValid), bpy::arg("directory")) + // .def("gameVersion", bpy::pure_virtual(&MOBase::IPluginGame::gameVersion)) + // .def("getLauncherName", bpy::pure_virtual(&MOBase::IPluginGame::getLauncherName)) + + // .def("featureList", +[](MOBase::IPluginGame* p) { + // // Constructing a dict from class name to actual object: + // bpy::dict dict; + // mp11::mp_for_each< + // // Must user pointers because mp_for_each construct object: + // mp11::mp_transform + // >([&](auto* pt) { + // using T = std::remove_pointer_t; + // typename bpy::reference_existing_object::apply::type converter; + + // // Retrieve the python class object: + // const bpy::converter::registration* registration = bpy::converter::registry::query(bpy::type_id()); + // bpy::object key = bpy::object(bpy::handle<>(bpy::borrowed(registration->get_class_object()))); + + // // Set the object: + // dict[key] = bpy::handle<>(converter(p->feature())); + // }); + // return dict; + // }) + + // .def("feature", +[](MOBase::IPluginGame* p, bpy::object clsObj) { + // bpy::object feature; + // mp11::mp_for_each< + // // Must user pointers because mp_for_each construct object: + // mp11::mp_transform + // >([&](auto* pt) { + // using T = std::remove_pointer_t; + // typename bpy::reference_existing_object::apply::type converter; + + // // Retrieve the python class object: + // const bpy::converter::registration* registration = bpy::converter::registry::query(bpy::type_id()); + + // if (clsObj.ptr() == (PyObject*) registration->get_class_object()) { + // feature = bpy::object(bpy::handle<>(converter(p->feature()))); + // } + // }); + // return feature; + // }, bpy::arg("feature_type")) + // ; + + // bpy::enum_("InstallResult") + // .value("SUCCESS", MOBase::IPluginInstaller::RESULT_SUCCESS) + // .value("FAILED", MOBase::IPluginInstaller::RESULT_FAILED) + // .value("CANCELED", MOBase::IPluginInstaller::RESULT_CANCELED) + // .value("MANUAL_REQUESTED", MOBase::IPluginInstaller::RESULT_MANUALREQUESTED) + // .value("NOT_ATTEMPTED", MOBase::IPluginInstaller::RESULT_NOTATTEMPTED) + // ; + + // bpy::class_, boost::noncopyable>("IPluginInstaller", bpy::no_init) + // .def("isArchiveSupported", &IPluginInstaller::isArchiveSupported, bpy::arg("tree")) + // .def("priority", &IPluginInstaller::priority) + // .def("onInstallationStart", &IPluginInstaller::onInstallationStart, (bpy::arg("archive"), bpy::arg("reinstallation"), bpy::arg("current_mod"))) + // .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, (bpy::arg("result"), bpy::arg("new_mod"))) + // .def("isManualInstaller", &IPluginInstaller::isManualInstaller) + // .def("setParentWidget", &IPluginInstaller::setParentWidget, bpy::arg("parent")) + // .def("setInstallationManager", &IPluginInstaller::setInstallationManager, bpy::arg("manager")) + // ; + + // bpy::class_, boost::noncopyable>("IPluginInstallerSimple") + // .def("onInstallationStart", &IPluginInstaller::onInstallationStart, (bpy::arg("archive"), bpy::arg("reinstallation"), bpy::arg("current_mod"))) + // .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, (bpy::arg("result"), bpy::arg("new_mod"))) + // .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginInstallerSimpleWrapper::localizedName_Default) + // .def("master", &MOBase::IPlugin::master, &IPluginInstallerSimpleWrapper::master_Default) + // .def("requirements", &MOBase::IPlugin::requirements, &IPluginInstallerSimpleWrapper::requirements_Default) + // .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginInstallerSimpleWrapper::enabledByDefault_Default) + + // // Note: Keeping the variant here even if we always return a tuple to be consistent with the wrapper and + // // have proper stubs generation. + // .def("install", +[](IPluginInstallerSimple* p, GuessedValue& modName, std::shared_ptr& tree, QString& version, int& nexusID) + // -> std::variant, std::tuple, QString, int>> { + // auto result = p->install(modName, tree, version, nexusID); + // return std::make_tuple(result, tree, version, nexusID); + // }, (bpy::arg("name"), "tree", "version", "nexus_id")) + // .def("_parentWidget", &IPluginInstallerSimpleWrapper::parentWidget, bpy::return_value_policy()) + // .def("_manager", &IPluginInstallerSimpleWrapper::manager, bpy::return_value_policy()) + // ; + + // bpy::class_, boost::noncopyable>("IPluginInstallerCustom") + // .def("onInstallationStart", &IPluginInstaller::onInstallationStart, (bpy::arg("archive"), bpy::arg("reinstallation"), bpy::arg("current_mod"))) + // .def("onInstallationEnd", &IPluginInstaller::onInstallationEnd, (bpy::arg("result"), bpy::arg("new_mod"))) + // .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginInstallerCustomWrapper::localizedName_Default) + // .def("master", &MOBase::IPlugin::master, &IPluginInstallerCustomWrapper::master_Default) + // .def("requirements", &MOBase::IPlugin::requirements, &IPluginInstallerCustomWrapper::requirements_Default) + // .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginInstallerCustomWrapper::enabledByDefault_Default) + + // // Needs to add both otherwize boost does not understand: + // .def("isArchiveSupported", &IPluginInstaller::isArchiveSupported, bpy::arg("tree")) + // .def("isArchiveSupported", &IPluginInstallerCustom::isArchiveSupported, bpy::arg("archive_name")) + // .def("supportedExtensions", &IPluginInstallerCustom::supportedExtensions) + // .def("install", &IPluginInstallerCustom::install, (bpy::arg("mod_name"), "game_name", "archive_name", "version", "nexus_id")) + // .def("_parentWidget", &IPluginInstallerSimpleWrapper::parentWidget, bpy::return_value_policy()) + // .def("_manager", &IPluginInstallerCustomWrapper::manager, bpy::return_value_policy()) + // ; + + // bpy::class_, boost::noncopyable>("IPluginModPage") + // .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginModPageWrapper::localizedName_Default) + // .def("master", &MOBase::IPlugin::master, &IPluginModPageWrapper::master_Default) + // .def("requirements", &MOBase::IPlugin::requirements, &IPluginModPageWrapper::requirements_Default) + // .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginModPageWrapper::enabledByDefault_Default) + + // .def("displayName", bpy::pure_virtual(&IPluginModPage::displayName)) + // .def("icon", bpy::pure_virtual(&IPluginModPage::icon)) + // .def("pageURL", bpy::pure_virtual(&IPluginModPage::pageURL)) + // .def("useIntegratedBrowser", bpy::pure_virtual(&IPluginModPage::useIntegratedBrowser)) + // .def("handlesDownload", bpy::pure_virtual(&IPluginModPage::handlesDownload), (bpy::arg("page_url"), "download_url", "fileinfo")) + // .def("setParentWidget", &IPluginModPage::setParentWidget, &IPluginModPageWrapper::setParentWidget_Default, bpy::arg("parent")) + // .def("_parentWidget", &IPluginModPageWrapper::parentWidget, bpy::return_value_policy()) + // ; + + // bpy::class_, boost::noncopyable>("IPluginPreview") + // .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginPreviewWrapper::localizedName_Default) + // .def("master", &MOBase::IPlugin::master, &IPluginPreviewWrapper::master_Default) + // .def("requirements", &MOBase::IPlugin::requirements, &IPluginPreviewWrapper::requirements_Default) + // .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginPreviewWrapper::enabledByDefault_Default) + + // .def("supportedExtensions", bpy::pure_virtual(&IPluginPreview::supportedExtensions)) + // .def("genFilePreview", bpy::pure_virtual(&IPluginPreview::genFilePreview), bpy::return_value_policy(), + // (bpy::arg("filename"), "max_size")) + // ; + + // bpy::class_, boost::noncopyable>("IPluginTool") + // .def("localizedName", &MOBase::IPlugin::localizedName, &IPluginToolWrapper::localizedName_Default) + // .def("master", &MOBase::IPlugin::master, &IPluginToolWrapper::master_Default) + // .def("requirements", &MOBase::IPlugin::requirements, &IPluginToolWrapper::requirements_Default) + // .def("enabledByDefault", &MOBase::IPlugin::enabledByDefault, &IPluginToolWrapper::enabledByDefault_Default) + + // .def("displayName", bpy::pure_virtual(&IPluginTool::displayName)) + // .def("tooltip", bpy::pure_virtual(&IPluginTool::tooltip)) + // .def("icon", bpy::pure_virtual(&IPluginTool::icon)) + // .def("display", bpy::pure_virtual(&IPluginTool::display)) + // .def("setParentWidget", &IPluginTool::setParentWidget, &IPluginToolWrapper::setParentWidget_Default, bpy::arg("parent")) + // .def("_parentWidget", &IPluginToolWrapper::parentWidget, bpy::return_value_policy()) + // ; + + // registerGameFeaturesPythonConverters(); + + m.def("getFileVersion", &MOBase::getFileVersion, py::arg("filepath")); + m.def("getProductVersion", &MOBase::getProductVersion, py::arg("executable")); + m.def("getIconForExecutable", &MOBase::iconForExecutable, py::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. + m.attr("MoVariant") = py::none(); +} + +/** + * + */ +class PythonRunner : public IPythonRunner +{ + +public: + PythonRunner(); + ~PythonRunner(); + + bool initPython(); + + QList 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 + void appendIfInstance(py::object const& obj, QList& 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> m_PythonObjects; +}; + +IPythonRunner* CreatePythonRunner() +{ + std::unique_ptr result = std::make_unique(); + if (result->initPython()) { + return result.release(); + } + else { + return nullptr; + } +} + +PythonRunner::PythonRunner() +{ +} + +PythonRunner::~PythonRunner() { + // We need the GIL lock when destroying Python objects. + py::gil_scoped_acquire lock; + // m_Interpreter.reset(); + + // Boost.Python does not handle cyclic garbace collection, so we need to release + // everything hold by the objects before deleting the objects themselves: + // for (auto& [name, objects] : m_PythonObjects) { + // for (auto& obj : objects) { + // obj.attr("__dict__").attr("clear")(); + // } + // } + + // 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 + +PYBIND11_MODULE(moprivate, m) +{ + py::class_(m, "PrintWrapper") + .def(py::init<>()) + .def("write", &PrintWrapper::write); + py::class_(m, "ErrWrapper") + .def(py::init<>()) + .def_static("instance", &ErrWrapper::instance, py::return_value_policy::reference) + .def("write", &ErrWrapper::write) + .def("startRecordingExceptionMessage", &ErrWrapper::startRecordingExceptionMessage) + .def("stopRecordingExceptionMessage", &ErrWrapper::stopRecordingExceptionMessage) + .def("getLastExceptionMessage", &ErrWrapper::getLastExceptionMessage); + + // utils::register_functor_converter(); + + // Expose a function to create a particular tree, only for debugging purpose, not in mobase. + // m.def("makeTree", [](std::function callback) -> std::shared_ptr { + // struct FileTree : IFileTree { + + // using callback_t = std::function; + + // FileTree(std::shared_ptr parent, QString name, callback_t callback) : + // FileTreeEntry(parent, name), IFileTree(), m_Callback(callback){ } + + // std::shared_ptr addFile(QString name, bool) override { + // if (m_Callback && !m_Callback(name, false)) { + // throw UnsupportedOperationException("File rejected by callback."); + // } + // return IFileTree::addFile(name); + // } + + // std::shared_ptr addDirectory(QString name) override { + // if (m_Callback && !m_Callback(name, true)) { + // throw UnsupportedOperationException("Directory rejected by callback."); + // } + // return IFileTree::addDirectory(name); + // } + + // protected: + + // std::shared_ptr makeDirectory(std::shared_ptr parent, QString name) const override { + // return std::make_shared(parent, name, m_Callback); + // } + + // bool doPopulate(std::shared_ptr parent, std::vector>& entries) const override { return true; } + // std::shared_ptr doClone() const override { return std::make_shared(nullptr, name(), m_Callback); } + + // private: + // callback_t m_Callback; + // }; + // return std::make_shared(nullptr, "", callback); + // }, py::arg("callback") = py::object{}); +} + +bool PythonRunner::initPython() +{ + if (Py_IsInitialized()) + return true; + try { + + // we initialize the interpreter "the old way" because scoped_interpreter does not + // seem to work well with PyQt + wchar_t argBuffer[MAX_PATH]; + const size_t cSize = strlen(argv0) + 1; + mbstowcs(argBuffer, argv0, MAX_PATH); + Py_SetProgramName(argBuffer); + + PyImport_AppendInittab("mobase", &PyInit_mobase); + PyImport_AppendInittab("moprivate", &PyInit_moprivate); + + Py_OptimizeFlag = 2; + Py_NoSiteFlag = 1; + + initPath(); + Py_InitializeEx(0); + + if (!Py_IsInitialized()) { + return false; + } + + py::module_ mainModule = py::module_::import("__main__"); + py::object mainNamespace = mainModule.attr("__dict__"); + mainNamespace["sys"] = py::module_::import("sys"); + mainNamespace["moprivate"] = py::module_::import("moprivate"); + py::module_::import("site"); + py::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"] = py::module_::import("mobase"); + configure_python_logging(mainNamespace["mobase"]); + + return true; + } catch (const py::error_already_set&) { + // construct an error to extract the message + pyexcept::PythonError err; + MOBase::log::error("failed to init python: {}", err.what()); + return false; + } +} + +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) { + py::module_ sys = py::module_::import("sys"); + py::list sysPath = sys.attr("path"); + + // Converting to QStringList for Qt::CaseInsensitive and because .index() + // raise an exception: + const QStringList currentPath = sysPath.cast(); + if (!currentPath.contains(folder, Qt::CaseInsensitive)) { + sysPath.insert(0, folder); + } +} + +template +void PythonRunner::appendIfInstance(py::object const& obj, QList &interfaces) { + if (py::isinstance(obj)) { + interfaces.append(obj.cast()); + } +} + +QList PythonRunner::load(const QString& identifier) +{ + py::gil_scoped_acquire lock; + + // `pluginName` can either be a python file (single-file plugin or a folder (whole module). + // + // For whole module, we simply add the parent folder to path, then we load the module with a simple + // bpy::import, and we retrieve the associated __dict__ from which we extract either createPlugin or + // createPlugins. + // + // For single file, we need to use py::eval_file, and we will use the context (global variables) + // from __main__ (already contains mobase, and other required module). Since the context is shared + // between called of `instantiate`, we need to make sure to remove createPlugin(s) from previous call. + try { + + // Dictionary that will contain createPlugin() or createPlugins(). + py::dict moduleDict; + + if (identifier.endsWith(".py")) { + py::object mainModule = py::module_::import("__main__"); + py::dict moduleNamespace = mainModule.attr("__dict__"); + + std::string temp = ToString(identifier); + py::eval_file(temp.c_str(), moduleNamespace).is_none(); + moduleDict = moduleNamespace; + } + else { + // Retrieve the module name: + QStringList parts = identifier.split("/"); + std::string moduleName = ToString(parts.takeLast()); + ensureFolderInPath(parts.join("/")); + moduleDict = py::module_::import(moduleName.c_str()).attr("__dict__"); + } + + if (py::len(moduleDict) == 0) { + MOBase::log::error("No plugins found in {}.", identifier); + return {}; + } + + // Create the plugins: + std::vector plugins; + + if (moduleDict.contains("createPlugin")) { + plugins.push_back(moduleDict["createPlugin"]()); + + // Clear for future call + PyDict_DelItemString(moduleDict.ptr(), "createPlugin"); + } + else if (moduleDict.contains("createPlugins")) { + py::object pyPlugins = moduleDict["createPlugins"](); + if (!PySequence_Check(pyPlugins.ptr())) { + MOBase::log::error("Plugin {}: createPlugins must return a list.", identifier); + } + else { + py::list pyList(pyPlugins); + int nPlugins = py::len(pyList); + for (int i = 0; i < nPlugins; ++i) { + plugins.push_back(pyList[i]); + } + } + + // Clear for future call + PyDict_DelItemString(moduleDict.ptr(), "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(); + } + + QList allInterfaceList; + + for (py::object pluginObj : plugins) { + + // Add the plugin to keep it alive: + m_PythonObjects[identifier].push_back(pluginObj); + + QList interfaceList; + + // appendIfInstance(pluginObj, interfaceList); + // Must try the wrapper because it's only a plugin extension interface in C++, so doesn't extend QObject + // appendIfInstance(pluginObj, interfaceList); + // Must try the wrapper because it's only a plugin extension interface in C++, so doesn't extend QObject + // appendIfInstance(pluginObj, interfaceList); + // appendIfInstance(pluginObj, interfaceList); + // appendIfInstance(pluginObj, interfaceList); + // appendIfInstance(pluginObj, interfaceList); + // appendIfInstance(pluginObj, interfaceList); + // appendIfInstance(pluginObj, interfaceList); + + if (interfaceList.isEmpty()) { + // appendIfInstance(pluginObj, interfaceList); + } + + if (interfaceList.isEmpty()) { + MOBase::log::error("Plugin {}: no plugin interface implemented.", identifier); + } + + // Append the plugins to the main list: + allInterfaceList.append(interfaceList); + } + + return allInterfaceList; + } + catch (const py::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()) { + + py::gil_scoped_acquire 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). + py::object sys = py::module_::import("sys"); + py::dict modules = sys.attr("modules"); + py::list keys = modules.attr("keys")(); + for (std::size_t i = 0; i < py::len(keys); ++i) { + py::object mod = modules[keys[i]]; + if (PyObject_HasAttrString(mod.ptr(), "__path__")) { + QString mpath = mod.attr("__path__")[0].cast(); + + if (!folder.relativeFilePath(mpath).startsWith("..")) { + // If the path is under identifier, we need to unload it. + log::debug("Unloading module {} from {} for {}.", keys[i].cast(), mpath, identifier); + + PyDict_DelItem(modules.ptr(), keys[i].ptr()); + } + } + } + } + + // Boost.Python does not handle cyclic garbace collection, so we need to release + // everything hold by the objects before deleting the objects themselves (done when + // erasing from m_PythonObjects). + for (auto& obj : it->second) { + obj.attr("__dict__").attr("clear")(); + } + + log::debug("Deleting {} python objects for {}.", it->second.size(), identifier); + m_PythonObjects.erase(it); + } +} + +bool PythonRunner::isPythonInitialized() const +{ + return Py_IsInitialized() != 0; +} diff --git a/src/runner-pybind11/pythonrunner.h b/src/runner-pybind11/pythonrunner.h new file mode 100644 index 0000000..b4f4ed9 --- /dev/null +++ b/src/runner-pybind11/pythonrunner.h @@ -0,0 +1,33 @@ +#ifndef PYTHONRUNNER_H +#define PYTHONRUNNER_H + +#include +#include +#include +#include +#include + + +class IPythonRunner { +public: + + virtual QList load(const QString& identifier) = 0; + virtual void unload(const QString& identifier) = 0; + + virtual bool isPythonInitialized() const = 0; + + virtual ~IPythonRunner() { } +}; + + +#ifdef PYTHONRUNNER_LIBRARY +#define PYDLLEXPORT Q_DECL_EXPORT +#else // PYTHONRUNNER_LIBRARY +#define PYDLLEXPORT Q_DECL_IMPORT +#endif // PYTHONRUNNER_LIBRARY + +extern "C" PYDLLEXPORT IPythonRunner *CreatePythonRunner(); + + + +#endif // PYTHONRUNNER_H diff --git a/src/runner-pybind11/pythonrunner_en.ts b/src/runner-pybind11/pythonrunner_en.ts new file mode 100644 index 0000000..ac63bc9 --- /dev/null +++ b/src/runner-pybind11/pythonrunner_en.ts @@ -0,0 +1,17 @@ + + + + + QObject + + + An unexpected C++ exception was thrown in python code. + + + + + An unknown exception was thrown in python code. + + + + diff --git a/src/runner-pybind11/pythonutils.cpp b/src/runner-pybind11/pythonutils.cpp new file mode 100644 index 0000000..37445c5 --- /dev/null +++ b/src/runner-pybind11/pythonutils.cpp @@ -0,0 +1,46 @@ +#include "pythonutils.h" + +#include +#include + +#include + +#include "log.h" + +namespace utils { + + void show_deprecation_warning(std::string_view name, std::string_view message, bool show_once) { + + // Contains the list of filename / line number for which a deprecation warning has already been shown. + static std::set> DeprecatedLines; + + // Find the caller: + auto inspect = bpy::import("inspect"); + auto current_frame = inspect.attr("currentframe")(); + auto callable_frame = inspect.attr("getouterframes")(current_frame, 2); + auto filename = bpy::extract(callable_frame[-1].attr("filename"))(); + auto function = bpy::extract(callable_frame[-1].attr("function"))(); + auto lineno = bpy::extract(callable_frame[-1].attr("lineno")); + + // Only show once if requested: + if (show_once && DeprecatedLines.contains({ filename, lineno })) { + return; + } + + // Register the deprecation: + DeprecatedLines.emplace(filename, lineno); + + auto path = relative(std::filesystem::path(filename), QCoreApplication::applicationDirPath().toStdWString()); + + // Show the message: + if (message.empty()) { + MOBase::log::warn( + "[deprecated] {} in {} [{}:{}].", name, function, path.native(), lineno); + } + else { + MOBase::log::warn( + "[deprecated] {} in {} [{}:{}]: {}", name, function, path.native(), lineno, message); + } + } + +} \ No newline at end of file diff --git a/src/runner-pybind11/pythonutils.h b/src/runner-pybind11/pythonutils.h new file mode 100644 index 0000000..6703557 --- /dev/null +++ b/src/runner-pybind11/pythonutils.h @@ -0,0 +1,271 @@ +#ifndef PYTHONRUNNER_UTILS_H +#define PYTHONRUNNER_UTILS_H + +#include + +#include "error.h" + +namespace utils { + + namespace bpy = boost::python; + + namespace details { + + template + struct is_stdmap_iterator : std::false_type {}; + + template + struct is_stdmap_iterator()->first)>> : std::true_type {}; + + // Note: QMap and standard maps do not have the same type of iterators: + template {}, int> = 0> + inline auto set_dict_entry(bpy::dict& result, It const& it) { + result[bpy::object{ it->first }] = bpy::object{ it->second }; + } + + template {}, int > = 0> + inline auto set_dict_entry(bpy::dict& result, It const& it) { + result[bpy::object{ it.key() }] = bpy::object{ it.value() }; + } + + } + + template + struct map_to_python { + static PyObject* convert(const Map& map) { + bpy::dict result; + for (auto it = map.begin(); it != map.end(); ++it) { + details::set_dict_entry(result, it); + } + return bpy::incref(result.ptr()); + } + }; + + template + struct map_from_python { + + using key_type = typename Map::key_type; + using value_type = typename Map::mapped_type; + + static void* convertible(PyObject* objPtr) { + return PyDict_Check(objPtr) ? objPtr : nullptr; + } + + static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { + void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; + Map* result = new (storage) Map(); + bpy::dict source(bpy::handle<>(bpy::borrowed(objPtr))); + bpy::list keys = source.keys(); + int len = bpy::len(keys); + for (int i = 0; i < len; ++i) { + bpy::object pyKey = keys[i]; + (*result)[bpy::extract(pyKey)] = bpy::extract(source[pyKey]); + } + + data->convertible = storage; + } + }; + + template + struct container_to_python_list { + static PyObject* convert(const Container& container) { + bpy::list pyList; + + try { + for (auto& item : container) { + pyList.append(item); + } + } + catch (const bpy::error_already_set&) { + throw pyexcept::PythonError(); + } + + return bpy::incref(pyList.ptr()); + } + }; + + + template + struct container_from_python_list { + + using value_type = typename Container::value_type; + + static void* convertible(PyObject* objPtr) { + // Check that the object can be iterated or is a sequence. There is no "clean" + // way checking that an object is iterable apparently (PyIter_Check checks that + // an object is an iterator, which is very different). + if (objPtr->ob_type->tp_iter != 0 || PySequence_Check(objPtr)) return objPtr; + return nullptr; + } + + static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { + void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; + Container* result = new (storage) Container(); + bpy::object source(bpy::handle<>(bpy::borrowed(objPtr))); + bpy::stl_input_iterator begin(source), end; + std::copy(begin, end, std::back_inserter(*result)); + data->convertible = storage; + } + }; + + template + struct set_to_python { + static PyObject* convert(const Container& container) { + bpy::list pyList; + + try { + for (auto& item : container) + pyList.append(item); + } + catch (const bpy::error_already_set&) { + throw pyexcept::PythonError(); + } + + return bpy::incref(pyList.ptr()); + } + }; + + + template + struct set_from_python { + + using value_type = typename Container::value_type; + + static void* convertible(PyObject* objPtr) { + // See container_from_python. + if (objPtr->ob_type->tp_iter != 0 && PySequence_Check(objPtr)) return objPtr; + return nullptr; + } + + static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { + void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; + Container* result = new (storage) Container(); + bpy::list source(bpy::handle<>(bpy::borrowed(objPtr))); + bpy::stl_input_iterator begin(source), end; + std::copy(begin, end, std::inserter(*result, result->begin())); + data->convertible = storage; + } + }; + + + template + struct optional_to_python { + static PyObject* convert(const Optional& optional) { + if (optional) { + return bpy::incref(bpy::object(*optional).ptr()); + } + else { + return bpy::incref(Py_None); + } + } + }; + + + template + struct optional_from_python { + + using value_type = typename Optional::value_type; + + static void* convertible(PyObject* objPtr) { + + if (objPtr == Py_None) { + return objPtr; + } + + bpy::object source(bpy::handle<>(bpy::borrowed(objPtr))); + return bpy::extract(source).check() ? objPtr : nullptr; + } + + static void construct(PyObject* objPtr, bpy::converter::rvalue_from_python_stage1_data* data) { + void* storage = ((bpy::converter::rvalue_from_python_storage*)data)->storage.bytes; + Optional* result = new (storage) Optional(); + + bpy::object source(bpy::handle<>(bpy::borrowed(objPtr))); + if (!source.is_none()) { + *result = bpy::extract(source)(); + } + + data->convertible = storage; + } + }; + + + /** + * @brief Register from and to python converters for (at least) map, unordered_map and QMap. + * + * Any standard compliant associative container with key/value should work here. + * + * @tparam Map The container type to register. + */ + template + void register_associative_container() { + bpy::to_python_converter>(); + bpy::converter::registry::push_back( + &map_from_python::convertible + , &map_from_python::construct + , bpy::type_id()); + }; + + /** + * @brief Register from and to python converters for (at least) set, unordered_set and QSet. + * + * Any standard compliant associative container with key should work here. + * + * @tparam Map The container type to register. + */ + template + void register_set_container() { + bpy::to_python_converter>(); + bpy::converter::registry::push_back( + &set_from_python::convertible + , &set_from_python::construct + , bpy::type_id()); + }; + + /** + * @brief Register from and to python converters for sequence container. + * + * Any standard compliant container should work here. + * + * @tparam Map The container type to register. + */ + template + void register_sequence_container() { + bpy::to_python_converter>(); + bpy::converter::registry::push_back( + &container_from_python_list::convertible + , &container_from_python_list::construct + , bpy::type_id()); + }; + + /** + * @brief Register from and to python converters for optional. + * + * @tparam T The optional type (std::optional or boost::optional). + */ + template + void register_optional() { + bpy::to_python_converter>(); + bpy::converter::registry::push_back( + &optional_from_python::convertible + , &optional_from_python::construct + , bpy::type_id()); + }; + + + /** + * @brief Show a deprecation warning. + * + * This methods will print a warning in MO2 log containing the location of the call to + * the deprecated function. If show_once is true, the deprecation warning will only be + * logged the first time the function is called at this location. + * + * @param name Name of the deprecated function. + * @param message Deprecation message. + * @param show_once Only show the message once per call location. + */ + void show_deprecation_warning(std::string_view name, std::string_view message = "", bool show_once = true); + +} + +#endif \ No newline at end of file diff --git a/src/runner-pybind11/pythonwrapperutilities.h b/src/runner-pybind11/pythonwrapperutilities.h new file mode 100644 index 0000000..a55025c --- /dev/null +++ b/src/runner-pybind11/pythonwrapperutilities.h @@ -0,0 +1,172 @@ +#ifndef PYTHONWRAPPERUTILITIES_H +#define PYTHONWRAPPERUTILITIES_H + +#include + +#include + +#include +#include + +#include "sipApiAccess.h" +#include "error.h" +#include "gilock.h" + +namespace details { + + /** + * @brief Common stuffs for all basicWrapperFunction methods. + */ + template + ReturnType wrapperFunctionImplementation(WrapperTypePtr wrapper, bool apiTransfer, Fn fn, boost::python::object* objPtr, const char *methodName, Args... args) { + boost::python::override implementation = [&]() { + GILock lock; + return wrapper->get_override(methodName); + }(); + if (!implementation) { + if constexpr (std::is_same_v) { + throw pyexcept::MissingImplementation(wrapper->className, methodName); + } + else { + return std::invoke(fn, wrapper, args...); + } + } + + GILock lock; + try { + boost::python::object result = implementation(args...); + if (objPtr) { + *objPtr = result; + } + else if (apiTransfer) { + sipAPIAccess::sipAPI()->api_transfer_to(result.ptr(), Py_None); + } + if constexpr (!std::is_same_v) { + return boost::python::extract(result)(); + } + } + catch (const boost::python::error_already_set&) { + throw pyexcept::PythonError(); + } + catch (...) { + throw pyexcept::UnknownException(); + } + } + +} + +/** + * @brief Call the given method on the wrapper with the given arguments, with proper + * exception handling. + * + * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly + * available `className` attribute. + * @param methodName The name of the method. + * @param args... Arguments for the method. + * + * @return the result of calling the given Python method on the wrapper. + * + * @throw pyexcept::MissingImplementation if the method does not exist. + * @throw pyexcept::PythonError if an error occurs while executing the python method. + * @throw pyexecpt::UnknownException if an unknown error occurs. + */ +template +ReturnType basicWrapperFunctionImplementation(const WrapperType *wrapper, const char *methodName, Args... args) +{ + return details::wrapperFunctionImplementation(wrapper, false, nullptr, nullptr, methodName, args...); +} + +/** + * @brief Call the given method on the wrapper with the given arguments, with proper + * exception handling, and store the intermediate result in the given python object. + * + * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly + * available `className` attribute. + * @param ref Python object to which the result of `get_override()` should be stored. + * @param methodName The name of the method. + * @param args... Arguments for the method. + * + * @return the result of calling the given Python method on the wrapper. + * + * @throw pyexcept::MissingImplementation if the method does not exist. + * @throw pyexcept::PythonError if an error occurs while executing the python method. + * @throw pyexecpt::UnknownException if an unknown error occurs. + */ +template +ReturnType basicWrapperFunctionImplementation(const WrapperType* wrapper, boost::python::object &ref, const char* methodName, Args... args) +{ + return details::wrapperFunctionImplementation(wrapper, false, nullptr, &ref, methodName, args...); +} + +/** + * @brief Call the given method on the wrapper with the given arguments, with proper + * exception handling, and transfer the responsibility of the returned object to + * the C++ side. + * + * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly + * available `className` attribute. + * @param methodName The name of the method. + * @param args... Arguments for the method. + * + * @return the result of calling the given Python method on the wrapper. + * + * @throw pyexcept::MissingImplementation if the method does not exist. + * @throw pyexcept::PythonError if an error occurs while executing the python method. + * @throw pyexecpt::UnknownException if an unknown error occurs. + */ +template +ReturnType wrapperFunctionImplementationWithApiTransfer(const WrapperType* wrapper, const char* methodName, Args... args) +{ + return details::wrapperFunctionImplementation(wrapper, true, nullptr, nullptr, methodName, args...); +} + +/** + * @brief Call the given method on the wrapper with the given arguments, with proper + * exception handling, falling back to the given function if the method does not exist. + * + * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly + * available `className` attribute. + * @param fn The function to call if the method does not exists. + * @param methodName The name of the method. + * @param args... Arguments for the method. + * + * Note: `fn` does not have to be a member-function of `wrapper` but `std::invoke(fn, wrapper, args...)` must be valid. + * + * @return the result of calling the given Python method on the wrapper. + * + * @throw pyexcept::PythonError if an error occurs while executing the python method. + * @throw pyexecpt::UnknownException if an unknown error occurs. + */ +template +ReturnType basicWrapperFunctionImplementationWithDefault(WrapperTypePtr wrapper, Fn fn, const char* methodName, Args... args) +{ + return details::wrapperFunctionImplementation(wrapper, false, fn, nullptr, methodName, args...); +} + +/** + * @brief Call the given method on the wrapper with the given arguments, with proper + * exception handling, and store the intermediate result in the given python object, + * falling back to the given function if the method does not exist. + * + * @param wrapper The wrapper object to use to retrieve the python method. Must have a publicly + * available `className` attribute. + * @param fn The function to call if the method does not exists. + * @param ref Python object to which the result of `get_override()` should be stored. + * @param methodName The name of the method. + * @param args... Arguments for the method. + * + * Note: `fn` does not have to be a member-function of `wrapper` but `std::invoke(fn, wrapper, args...)` must be valid. + * + * @return the result of calling the given Python method on the wrapper. + * + * @throw pyexcept::PythonError if an error occurs while executing the python method. + * @throw pyexecpt::UnknownException if an unknown error occurs. + */ +template +ReturnType basicWrapperFunctionImplementationWithDefault(const WrapperType* wrapper, Fn fn, boost::python::object& ref, const char* methodName, Args... args) +{ + return details::wrapperFunctionImplementation(wrapper, false, fn, &ref, methodName, args...); +} + + +#endif // PYTHONWRAPPERUTILITIES_H diff --git a/src/runner-pybind11/shared_ptr_converter.h b/src/runner-pybind11/shared_ptr_converter.h new file mode 100644 index 0000000..ab80ac3 --- /dev/null +++ b/src/runner-pybind11/shared_ptr_converter.h @@ -0,0 +1,125 @@ +#ifndef PYTHONRUNNER_SHARED_PTR_CONVERTER_H +#define PYTHONRUNNER_SHARED_PTR_CONVERTER_H + +#include + +#include "error.h" +#include "gilock.h" + +namespace utils { + + // Shared pointers are handled in a special way by Boost.Python since they hold + // the wrapped Python object and only release it when the ref counter of the shared + // ptr drops to 0 using shared_ptr_deleter. + // + // Unfortunately for us, this will happen outside of the Python proxy for some objects + // and thus without the GIL lock, making everything crash, so we need a custom deleter + // that holds the GIL while releasing the lock. + // + // Note that this is only useful for Python -> C++ conversion, and without this, Boost + // will automatically wrapped the pointer. The C++ -> Python conversion is handled + // separately by boost::python::register_ptr_to_python. + // + // This is an open Boost.Python problem: https://github.com/boostorg/python/pull/11 + + template + struct shared_ptr_from_python; + + namespace details { + + struct shared_ptr_deleter_with_gil_lock : boost::python::converter::shared_ptr_deleter { + + using shared_ptr_deleter::shared_ptr_deleter; + + void operator()(void const* o) { + GILock lock; + shared_ptr_deleter::operator()(o); + } + + }; + + template + struct shared_ptr_void; + + template + struct shared_ptr_void> { using type = std::shared_ptr; }; + + template + struct shared_ptr_void> { using type = boost::shared_ptr; }; + + template + using shared_ptr_void_t = typename shared_ptr_void::type; + + } + + template + struct shared_ptr_from_python + { + using T = typename SharedPtr::element_type; + + shared_ptr_from_python() + { + using namespace boost::python; + converter::registry::insert(&convertible, &construct, type_id() +#ifndef BOOST_PYTHON_NO_PY_SIGNATURES + , &converter::expected_from_python_type_direct::get_pytype +#endif + ); + } + + private: + static void* convertible(PyObject* p) + { + if (p == Py_None) + return p; + + return boost::python::converter::get_lvalue_from_python(p, boost::python::converter::registered::converters); + } + + static void construct(PyObject* source, boost::python::converter::rvalue_from_python_stage1_data* data) + { + using namespace boost::python; + void* const storage = ((converter::rvalue_from_python_storage*)data)->storage.bytes; + // Deal with the "None" case. + if (data->convertible == source) + new (storage) SharedPtr(); + else + { + details::shared_ptr_void_t hold_convertible_ref_count( + (void*)0, details::shared_ptr_deleter_with_gil_lock(handle<>(borrowed(source)))); + // use aliasing constructor + new (storage) SharedPtr(hold_convertible_ref_count, + static_cast(data->convertible)); + } + + data->convertible = storage; + } + }; + + // release the bpy::object associated with the deleter of the given shared_ptr, + // if the given shared_ptr has a Boost.Python deleter + // + // this should only be used when returning from Python objects that have been created + // on the C++ side, e.g. if IFileTree.createOrphanTree() from Python and then return + // the tree + // + // for reason yet to be known, Boost.Python had a custom deleter in this case that tries + // to delete the bpy::object and fails, so we have to release the object manually + // + template + SharedPtr clean_shared_ptr(SharedPtr&& ptr) { + if (auto* d = get_deleter(ptr); d != nullptr) { + // we cannot do a proper reset() here, even with the GIL lock, for unknown reason, + // so we only release + // + // this might create lost references to Python object but this should not happen + // too often so hopefully it's not a big issue + // + d->owner.release(); + } + return ptr; + } + +} + +#endif \ No newline at end of file diff --git a/src/runner-pybind11/sipapiaccess.cpp b/src/runner-pybind11/sipapiaccess.cpp new file mode 100644 index 0000000..b93d6f3 --- /dev/null +++ b/src/runner-pybind11/sipapiaccess.cpp @@ -0,0 +1,86 @@ +#include "sipapiaccess.h" +#include +#include +#include + +const sipAPIDef* sipAPIAccess::sipAPI() +{ + QString exception; + static const sipAPIDef* sipApi = nullptr; + if (sipApi == nullptr) { + #if defined(SIP_USE_PYCAPSULE) + PyImport_ImportModule("PyQt5.sip"); + + auto errorObj = PyErr_Occurred(); + if (errorObj != NULL) { + PyObject* type, * value, * traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + if (traceback != NULL) { + boost::python::handle<> h_type(type); + boost::python::handle<> h_val(value); + boost::python::handle<> h_tb(traceback); + boost::python::object tb(boost::python::import("traceback")); + boost::python::object fmt_exp(tb.attr("format_exception")); + boost::python::object exp_list(fmt_exp(h_type, h_val, h_tb)); + boost::python::object exp_str(boost::python::str("\n").join(exp_list)); + boost::python::extract returned(exp_str); + exception = QString::fromStdString(returned()); + } + PyErr_Restore(type, value, traceback); + throw MOBase::Exception(QString("Failed to load PyQt5: %1").arg(exception)); + } + + sipApi = (const sipAPIDef*)PyCapsule_Import("PyQt5.sip._C_API", 0); + if (sipApi == NULL) { + auto errorObj = PyErr_Occurred(); + if (errorObj != NULL) { + PyObject* type, * value, * traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + if (traceback != NULL) { + boost::python::handle<> h_type(type); + boost::python::handle<> h_val(value); + boost::python::handle<> h_tb(traceback); + boost::python::object tb(boost::python::import("traceback")); + boost::python::object fmt_exp(tb.attr("format_exception")); + boost::python::object exp_list(fmt_exp(h_type, h_val, h_tb)); + boost::python::object exp_str(boost::python::str("\n").join(exp_list)); + boost::python::extract returned(exp_str); + exception = QString::fromStdString(returned()); + } + PyErr_Restore(type, value, traceback); + } + throw MOBase::Exception(QString("Failed to load SIP API: %1").arg(exception)); + } + #else + PyObject* sip_module; + PyObject* sip_module_dict; + PyObject* c_api; + + /* Import the SIP module. */ + sip_module = PyImport_ImportModule("PyQt5.sip"); + + if (sip_module == NULL) + return NULL; + + /* Get the module's dictionary. */ + sip_module_dict = PyModule_GetDict(sip_module); + + /* Get the "_C_API" attribute. */ + c_api = PyDict_GetItemString(sip_module_dict, "_C_API"); + + if (c_api == NULL) + return NULL; + + /* Sanity check that it is the right type. */ + if (!PyCObject_Check(c_api)) + return NULL; + + /* Get the actual pointer from the object. */ + sipApi = (const sipAPIDef*)PyCObject_AsVoidPtr(c_api); + #endif + } + + return sipApi; +} \ No newline at end of file diff --git a/src/runner-pybind11/sipapiaccess.h b/src/runner-pybind11/sipapiaccess.h new file mode 100644 index 0000000..2706dda --- /dev/null +++ b/src/runner-pybind11/sipapiaccess.h @@ -0,0 +1,12 @@ +#ifndef SIPAPIACCESS_H +#define SIPAPIACCESS_H + +#include + +class sipAPIAccess +{ +public: + static const sipAPIDef* sipAPI(); +}; + +#endif // SIPAPIACCESS_H diff --git a/src/runner-pybind11/tuple_helper.h b/src/runner-pybind11/tuple_helper.h new file mode 100644 index 0000000..1d7b8b5 --- /dev/null +++ b/src/runner-pybind11/tuple_helper.h @@ -0,0 +1,99 @@ +#ifndef TUPLE_HELPER_H +#define TUPLE_HELPER_H + +#include +#include //len function + +namespace boost { + namespace python { + + template + struct to_py_tuple { + + static PyObject* convert(const TTuple& c_tuple) { + list values; + //add all c_tuple items to "values" list + convert_impl(c_tuple, values, std::make_index_sequence>{}); + //create Python tuple from the list + return incref(python::tuple(values).ptr()); + } + + private: + + template + static void convert_impl(const TTuple& c_tuple, list& values, std::index_sequence) { + (values.append(std::get(c_tuple)), ...); + } + + }; + + + template + struct from_py_sequence { + + using tuple_type = TTuple; + using index_sequence = std::make_index_sequence>; + + static void* convertible(PyObject* py_obj) { + + if (!PySequence_Check(py_obj)) { + return 0; + } + + if (!PyObject_HasAttrString(py_obj, "__len__")) { + return 0; + } + + python::object py_sequence(handle<>(borrowed(py_obj))); + + if (std::tuple_size_v != len(py_sequence)) { + return 0; + } + + if (convertible_impl(py_sequence, index_sequence{})) { + return py_obj; + } + else { + return 0; + } + } + + static void construct(PyObject* py_obj, converter::rvalue_from_python_stage1_data* data) { + typedef converter::rvalue_from_python_storage storage_t; + storage_t* the_storage = reinterpret_cast(data); + void* memory_chunk = the_storage->storage.bytes; + TTuple* c_tuple = new (memory_chunk) TTuple(); + data->convertible = memory_chunk; + + python::object py_sequence(handle<>(borrowed(py_obj))); + construct_impl(py_sequence, *c_tuple, index_sequence{}); + } + + private: + + template + static bool convertible_impl(const python::object& py_sequence, std::index_sequence) { + return (... && extract>(py_sequence[Is]).check()); + } + + template + static void construct_impl(const python::object& py_sequence, TTuple& c_tuple, std::index_sequence) { + c_tuple = tuple_type{ extract>(py_sequence[Is])... }; + } + + }; + + template< class TTuple> + void register_tuple() { + + to_python_converter< TTuple, to_py_tuple >(); + + converter::registry::push_back(&from_py_sequence::convertible + , &from_py_sequence::construct + , type_id()); + }; + + } +} //boost::python + +#endif \ No newline at end of file diff --git a/src/runner-pybind11/uibasewrappers.h b/src/runner-pybind11/uibasewrappers.h new file mode 100644 index 0000000..95e878a --- /dev/null +++ b/src/runner-pybind11/uibasewrappers.h @@ -0,0 +1,68 @@ +#ifndef UIBASEWRAPPERS_H +#define UIBASEWRAPPERS_H + + +#ifndef Q_MOC_RUN +#pragma warning (push, 0) +#include +#pragma warning (pop) +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "error.h" +#include "gilock.h" +#include "pythonwrapperutilities.h" + +// This can be extended in C++, so why not in Python: +class IPluginRequirementWrapper : public MOBase::IPluginRequirement, public boost::python::wrapper +{ +public: + static constexpr const char* className = "IPluginRequirement"; + using boost::python::wrapper::get_override; + + virtual std::optional check(MOBase::IOrganizer *o) const override { + return basicWrapperFunctionImplementation>(this, "check", boost::python::ptr(o)); + }; +}; + +// This needs to be extendable in Python, so actually needs a wrapper: +class ISaveGameWrapper : public MOBase::ISaveGame, public boost::python::wrapper +{ +public: + static constexpr const char* className = "ISaveGameWrapper"; + using boost::python::wrapper::get_override; + + virtual QString getFilepath() const override { return basicWrapperFunctionImplementation(this, "getFilepath"); }; + virtual QDateTime getCreationTime() const override { return basicWrapperFunctionImplementation(this, "getCreationTime"); }; + virtual QString getName() const override { return basicWrapperFunctionImplementation(this, "getName"); }; + virtual QString getSaveGroupIdentifier() const override { return basicWrapperFunctionImplementation(this, "getSaveGroupIdentifier"); }; + virtual QStringList allFiles() const override { return basicWrapperFunctionImplementation(this, "allFiles"); }; + +protected: + + friend class IPluginGameWrapper; +}; + +// This needs a wrapper but currently I have no idea how to expose this properly to python: +class ISaveGameInfoWidgetWrapper : public MOBase::ISaveGameInfoWidget, public boost::python::wrapper +{ +public: + static constexpr const char* className = "ISaveGameInfoWidgetWrapper"; + using boost::python::wrapper::get_override; + + // Bring the constructor: + using ISaveGameInfoWidget::ISaveGameInfoWidget; + + virtual void setSave(MOBase::ISaveGame const& save) override { basicWrapperFunctionImplementation(this, "setSave", boost::ref(save)); }; +}; + +#endif // UIBASEWRAPPERS_H diff --git a/src/runner-pybind11/variant_helper.h b/src/runner-pybind11/variant_helper.h new file mode 100644 index 0000000..08fa27d --- /dev/null +++ b/src/runner-pybind11/variant_helper.h @@ -0,0 +1,104 @@ +#ifndef VARIANT_HELPER_H +#define VARIANT_HELPER_H + +#include + +/** + * Register variant(s) from and to python object. Greatly inspired by register_tuple<>. + */ + +namespace boost { + namespace python { + + template + struct to_py_variant { + + static PyObject* convert(const TVariant& c_variant) { + object value = std::visit([](auto const& value) { + return object{ value }; + }, c_variant); + //create Python object from the list + return incref(value.ptr()); + } + + }; + + template + struct variant_from_python; + + template