diff --git a/changelog.md b/changelog.md index 1273bd9..2e91e47 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,20 @@ # Changelog +## Upcoming + +- Fixed weak pointer type hinting to allow for null pointers. This always worked at runtime. + + [1cbded47](https://github.com/bl-sdk/pyunrealsdk/commit/1cbded47) + +- Added support for Delegate and Multicast Delegate properties. + + [04d47f92](https://github.com/bl-sdk/pyunrealsdk/commit/04d47f92), + [2876f098](https://github.com/bl-sdk/pyunrealsdk/commit/2876f098) + +- Added a repr to `BoundFunction`, as these are now returned by delegates. + + [22082579](https://github.com/bl-sdk/pyunrealsdk/commit/22082579) + ## v1.3.0 Also see the unrealsdk v1.3.0 changelog [here](https://github.com/bl-sdk/unrealsdk/blob/master/changelog.md#v130). diff --git a/libs/unrealsdk b/libs/unrealsdk index da8c5e7..39a4111 160000 --- a/libs/unrealsdk +++ b/libs/unrealsdk @@ -1 +1 @@ -Subproject commit da8c5e748101aa2d1337f99fda7b52cba894be78 +Subproject commit 39a41115cb3ee5a338a9e05e8cd67a4389805729 diff --git a/src/pyunrealsdk/dllmain.cpp b/src/pyunrealsdk/dllmain.cpp index bf60a5a..27c39b4 100644 --- a/src/pyunrealsdk/dllmain.cpp +++ b/src/pyunrealsdk/dllmain.cpp @@ -30,7 +30,7 @@ DWORD WINAPI startup_thread(LPVOID /*unused*/) { * @param ul_reason_for_call Reason this is being called. * @return True if loaded successfully, false otherwise. */ -// NOLINTNEXTLINE(readability-identifier-naming) - for `DllMain` +// NOLINTNEXTLINE(misc-use-internal-linkage, readability-identifier-naming) BOOL APIENTRY DllMain(HMODULE h_module, DWORD ul_reason_for_call, LPVOID /*unused*/) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: diff --git a/src/pyunrealsdk/logging.cpp b/src/pyunrealsdk/logging.cpp index 71aae30..487122d 100644 --- a/src/pyunrealsdk/logging.cpp +++ b/src/pyunrealsdk/logging.cpp @@ -1,6 +1,7 @@ #include "pyunrealsdk/pch.h" -#include "unrealsdk/logging.h" +#include "pyunrealsdk/logging.h" #include "unrealsdk/format.h" +#include "unrealsdk/logging.h" #include "unrealsdk/unrealsdk.h" using unrealsdk::logging::Level; diff --git a/src/pyunrealsdk/pyunrealsdk.cpp b/src/pyunrealsdk/pyunrealsdk.cpp index 36acd94..c2f4554 100644 --- a/src/pyunrealsdk/pyunrealsdk.cpp +++ b/src/pyunrealsdk/pyunrealsdk.cpp @@ -1,4 +1,5 @@ #include "pyunrealsdk/pch.h" +#include "pyunrealsdk/pyunrealsdk.h" #include "pyunrealsdk/base_bindings.h" #include "pyunrealsdk/commands.h" #include "pyunrealsdk/env.h" diff --git a/src/pyunrealsdk/unreal_bindings/bindings.cpp b/src/pyunrealsdk/unreal_bindings/bindings.cpp index a8deeeb..27176b1 100644 --- a/src/pyunrealsdk/unreal_bindings/bindings.cpp +++ b/src/pyunrealsdk/unreal_bindings/bindings.cpp @@ -8,6 +8,7 @@ #include "pyunrealsdk/unreal_bindings/uobject_children.h" #include "pyunrealsdk/unreal_bindings/weak_pointer.h" #include "pyunrealsdk/unreal_bindings/wrapped_array.h" +#include "pyunrealsdk/unreal_bindings/wrapped_multicast_delegate.h" #include "pyunrealsdk/unreal_bindings/wrapped_struct.h" #include "unrealsdk/unreal/classes/ufield.h" #include "unrealsdk/unreal/classes/ustruct.h" @@ -32,6 +33,7 @@ void register_module(py::module_& mod) { register_bound_function(unreal); register_weak_pointer(unreal); register_persistent_object_properties(unreal); + register_wrapped_multicast_delegate(unreal); } } // namespace pyunrealsdk::unreal diff --git a/src/pyunrealsdk/unreal_bindings/bound_function.cpp b/src/pyunrealsdk/unreal_bindings/bound_function.cpp index f66bbd7..5ba2e3d 100644 --- a/src/pyunrealsdk/unreal_bindings/bound_function.cpp +++ b/src/pyunrealsdk/unreal_bindings/bound_function.cpp @@ -52,42 +52,37 @@ namespace { * @note While this is similar to `make_struct`, we need to do some extra processing on the params, * and we need to fail if an arg was missed. * - * @param params The params struct to fill. + * @param info The call info to write to. * @param args The python args. * @param kwargs The python kwargs. - * @return A pair of the return param (may be nullptr), and any out params (may be empty), to be - * passed to `get_py_return`. */ -std::pair> fill_py_params(WrappedStruct& params, - const py::args& args, - const py::kwargs& kwargs) { - UProperty* return_param = nullptr; - std::vector out_params{}; - +void fill_py_params(impl::PyCallInfo& info, const py::args& args, const py::kwargs& kwargs) { size_t arg_idx = 0; std::vector missing_required_args{}; - for (auto prop : params.type->properties()) { + for (auto prop : info.params.type->properties()) { if ((prop->PropertyFlags & UProperty::PROP_FLAG_PARAM) == 0) { continue; } - if ((prop->PropertyFlags & UProperty::PROP_FLAG_RETURN) != 0 && return_param == nullptr) { - return_param = prop; + if ((prop->PropertyFlags & UProperty::PROP_FLAG_RETURN) != 0 + && info.return_param == nullptr) { + info.return_param = prop; continue; } if ((prop->PropertyFlags & UProperty::PROP_FLAG_OUT) != 0) { - out_params.push_back(prop); + info.out_params.push_back(prop); } // If we still have positional args left if (arg_idx != args.size()) { - py_setattr_direct(prop, reinterpret_cast(params.base.get()), + py_setattr_direct(prop, reinterpret_cast(info.params.base.get()), args[arg_idx++]); if (kwargs.contains(prop->Name)) { - throw py::type_error(unrealsdk::fmt::format( - "{}() got multiple values for argument '{}'", params.type->Name, prop->Name)); + throw py::type_error( + unrealsdk::fmt::format("{}() got multiple values for argument '{}'", + info.params.type->Name, prop->Name)); } continue; @@ -97,7 +92,7 @@ std::pair> fill_py_params(WrappedStruct& par if (kwargs.contains(prop->Name)) { // Extract the value with pop, so we can check that kwargs are empty at the // end - py_setattr_direct(prop, reinterpret_cast(params.base.get()), + py_setattr_direct(prop, reinterpret_cast(info.params.base.get()), kwargs.attr("pop")(prop->Name)); continue; } @@ -115,69 +110,81 @@ std::pair> fill_py_params(WrappedStruct& par } if (!missing_required_args.empty()) { - throw_missing_required_args(params.type->Name, missing_required_args); + throw_missing_required_args(info.params.type->Name, missing_required_args); } if (!kwargs.empty()) { // Copying python, we only need to warn about one extra kwarg std::string bad_kwarg = py::str(kwargs.begin()->first); throw py::type_error(unrealsdk::fmt::format("{}() got an unexpected keyword argument '{}'", - params.type->Name, bad_kwarg)); + info.params.type->Name, bad_kwarg)); + } +} + +} // namespace + +namespace impl { + +PyCallInfo::PyCallInfo(const UFunction* func, const py::args& args, const py::kwargs& kwargs) + // Start by initializing a null struct, to avoid allocations + : params(func, nullptr) { + if (func->NumParams < args.size()) { + throw py::type_error( + unrealsdk::fmt::format("{}() takes {} positional args, but {} were given", func->Name, + func->NumParams, args.size())); + } + + // If we're given exactly one arg, and it's a wrapped struct of our function type, take it as + // the args directly + if (args.size() == 1 && kwargs.empty() && py::isinstance(args[0])) { + auto args_struct = py::cast(args[0]); + if (args_struct.type == func) { + this->params = std::move(args_struct); + + // Manually gather the return value and out params + for (auto prop : func->properties()) { + if ((prop->PropertyFlags & UProperty::PROP_FLAG_RETURN) != 0 + && return_param == nullptr) { + this->return_param = prop; + continue; + } + if ((prop->PropertyFlags & UProperty::PROP_FLAG_OUT) != 0) { + this->out_params.push_back(prop); + } + } + return; + } } - return {return_param, out_params}; + // Otherwise, allocate a new params struct + this->params = WrappedStruct{func}; + fill_py_params(*this, args, kwargs); } -/** - * @brief Get the python return value for a function call. - * - * @param params The params struct to read the value out of. - * @param return_param The return param. - * @param out_params A list of the out params. - * @return The value to return to python. - */ -py::object get_py_return(const WrappedStruct& params, - UProperty* return_param, - const std::vector& out_params) { - // NOLINTNEXTLINE(misc-const-correctness) - py::list ret{1 + out_params.size()}; +py::object PyCallInfo::get_py_return(void) const { + const py::list ret{1 + this->out_params.size()}; - if (return_param == nullptr) { + if (this->return_param == nullptr) { ret[0] = py::ellipsis{}; } else { ret[0] = - py_getattr(return_param, reinterpret_cast(params.base.get()), params.base); + py_getattr(this->return_param, reinterpret_cast(this->params.base.get()), + this->params.base); } auto idx = 1; - for (auto prop : out_params) { - ret[idx++] = py_getattr(prop, reinterpret_cast(params.base.get()), params.base); + for (auto prop : this->out_params) { + ret[idx++] = py_getattr(prop, reinterpret_cast(this->params.base.get()), + this->params.base); } - if (out_params.empty()) { + if (this->out_params.empty()) { return ret[0]; } return py::tuple(ret); } -py::object get_py_return(const WrappedStruct& params) { - // If only called with the struct, re-gather the return + out params - UProperty* return_param = nullptr; - std::vector out_params{}; - - for (auto prop : params.type->properties()) { - if ((prop->PropertyFlags & UProperty::PROP_FLAG_RETURN) != 0 && return_param == nullptr) { - return_param = prop; - continue; - } - if ((prop->PropertyFlags & UProperty::PROP_FLAG_OUT) != 0) { - out_params.push_back(prop); - } - } - - return get_py_return(params, return_param, out_params); -} -} // namespace +} // namespace impl void register_bound_function(py::module_& mod) { py::class_(mod, "BoundFunction") @@ -188,41 +195,33 @@ void register_bound_function(py::module_& mod) { " func: The function to bind.\n" " object: The object the function is bound to.", "func"_a, "object"_a) + .def( + "__repr__", + [](BoundFunction& self) { + return unrealsdk::fmt::format( + "", self.func->Name, + unrealsdk::utils::narrow(self.object->get_path_name())); + }, + "Gets a string representation of this function and the object it's bound to.\n" + "\n" + "Returns:\n" + " The string representation.") .def( "__call__", [](BoundFunction& self, const py::args& args, const py::kwargs& kwargs) { - if (self.func->NumParams < args.size()) { - throw py::type_error( - unrealsdk::fmt::format("{}() takes {} positional args, but {} were given", - self.func->Name, self.func->NumParams, args.size())); - } - - if (args.size() == 1 && kwargs.empty() && py::isinstance(args[0])) { - auto args_struct = py::cast(args[0]); - if (args_struct.type == self.func) { - { - // Release the GIL to avoid a deadlock if ProcessEvent is locking. - // If a hook tries to call into Python, it will be holding the process - // event lock, and it will try to acquire the GIL. - // If at the same time python code on a different thread tries to call - // an unreal function, it would be holding the GIL, and trying to - // acquire the process event lock. - const py::gil_scoped_release gil{}; - self.call(args_struct); - } - return get_py_return(args_struct); - } - } - - WrappedStruct params{self.func}; - auto [return_param, out_params] = fill_py_params(params, args, kwargs); + impl::PyCallInfo info{self.func, args, kwargs}; + // Release the GIL to avoid a deadlock if ProcessEvent is locking. + // If a hook tries to call into Python, it will be holding the process event lock, + // and it will try to acquire the GIL. + // If at the same time python code on a different thread tries to call an unreal + // function, it'd be holding the GIL, and trying to acquire the process event lock. { const py::gil_scoped_release gil{}; - self.call(params); + self.call(info.params); } - return get_py_return(params, return_param, out_params); + return info.get_py_return(); }, "Calls the function.\n" "\n" diff --git a/src/pyunrealsdk/unreal_bindings/bound_function.h b/src/pyunrealsdk/unreal_bindings/bound_function.h index de496ea..a42f71a 100644 --- a/src/pyunrealsdk/unreal_bindings/bound_function.h +++ b/src/pyunrealsdk/unreal_bindings/bound_function.h @@ -2,11 +2,48 @@ #define PYUNREALSDK_UNREAL_BINDINGS_BOUND_FUNCTION_H #include "pyunrealsdk/pch.h" +#include "unrealsdk/unreal/wrappers/wrapped_struct.h" #ifdef PYUNREALSDK_INTERNAL +namespace unrealsdk::unreal { + +class UFunction; +class UProperty; + +} // namespace unrealsdk::unreal + namespace pyunrealsdk::unreal { +namespace impl { + +// Type helping convert a python function call to an unreal one. +struct PyCallInfo { + unrealsdk::unreal::WrappedStruct params; + unrealsdk::unreal::UProperty* return_param{}; + std::vector out_params; + + /** + * @brief Converts python args into a params struct. + * + * @param func The function being called. + * @param args The python args. + * @param kwargs The python kwargs. + */ + PyCallInfo(const unrealsdk::unreal::UFunction* func, + const py::args& args, + const py::kwargs& kwargs); + + /** + * @brief Get the python return value for the function call from the contained params struct. + * + * @return The value to return to python. + */ + [[nodiscard]] py::object get_py_return(void) const; +}; + +} // namespace impl + /** * @brief Registers BoundFunction. * diff --git a/src/pyunrealsdk/unreal_bindings/property_access.cpp b/src/pyunrealsdk/unreal_bindings/property_access.cpp index ef7254c..e7c30e4 100644 --- a/src/pyunrealsdk/unreal_bindings/property_access.cpp +++ b/src/pyunrealsdk/unreal_bindings/property_access.cpp @@ -65,8 +65,8 @@ std::vector py_dir(const py::object& self, const UStruct* type) { if (dir_includes_unreal) { // Append our fields auto fields = type->fields(); - std::transform(fields.begin(), fields.end(), std::back_inserter(names), - [](auto obj) { return obj->Name; }); + std::ranges::transform(fields, std::back_inserter(names), + [](auto obj) { return obj->Name; }); } return names; @@ -120,7 +120,8 @@ py::object py_getattr(UField* field, unrealsdk::fmt::format("cannot bind function '{}' with null object", field->Name)); } - return py::cast(BoundFunction{reinterpret_cast(field), func_obj}); + return py::cast( + BoundFunction{.func = reinterpret_cast(field), .object = func_obj}); } if (field->is_instance(find_class())) { diff --git a/src/pyunrealsdk/unreal_bindings/uobject_children.cpp b/src/pyunrealsdk/unreal_bindings/uobject_children.cpp index e06d414..6dbc6be 100644 --- a/src/pyunrealsdk/unreal_bindings/uobject_children.cpp +++ b/src/pyunrealsdk/unreal_bindings/uobject_children.cpp @@ -1,4 +1,5 @@ #include "pyunrealsdk/pch.h" +#include "pyunrealsdk/unreal_bindings/uobject_children.h" #include "pyunrealsdk/unreal_bindings/bindings.h" #include "unrealsdk/unreal/classes/properties/attribute_property.h" #include "unrealsdk/unreal/classes/properties/copyable_property.h" @@ -8,8 +9,10 @@ #include "unrealsdk/unreal/classes/properties/ubyteproperty.h" #include "unrealsdk/unreal/classes/properties/uclassproperty.h" #include "unrealsdk/unreal/classes/properties/ucomponentproperty.h" +#include "unrealsdk/unreal/classes/properties/udelegateproperty.h" #include "unrealsdk/unreal/classes/properties/uenumproperty.h" #include "unrealsdk/unreal/classes/properties/uinterfaceproperty.h" +#include "unrealsdk/unreal/classes/properties/umulticastdelegateproperty.h" #include "unrealsdk/unreal/classes/properties/uobjectproperty.h" #include "unrealsdk/unreal/classes/properties/ustrproperty.h" #include "unrealsdk/unreal/classes/properties/ustructproperty.h" @@ -159,6 +162,9 @@ void register_uobject_children(py::module_& mod) { return interfaces; }); + PyUEClass(mod, "UDelegateProperty") + .def_property_readonly("Signature", &UDelegateProperty::get_signature); + PyUEClass(mod, "UDoubleProperty"); PyUEClass(mod, "UEnumProperty") @@ -189,6 +195,9 @@ void register_uobject_children(py::module_& mod) { PyUEClass(mod, "UIntProperty"); + PyUEClass(mod, "UMulticastDelegateProperty") + .def_property_readonly("Signature", &UMulticastDelegateProperty::get_signature); + PyUEClass(mod, "UNameProperty"); PyUEClass(mod, "UObjectProperty") diff --git a/src/pyunrealsdk/unreal_bindings/weak_pointer.cpp b/src/pyunrealsdk/unreal_bindings/weak_pointer.cpp index 0069b7f..0567f9a 100644 --- a/src/pyunrealsdk/unreal_bindings/weak_pointer.cpp +++ b/src/pyunrealsdk/unreal_bindings/weak_pointer.cpp @@ -1,6 +1,7 @@ #include "pyunrealsdk/pch.h" -#include "unrealsdk/unreal/wrappers/weak_pointer.h" +#include "pyunrealsdk/unreal_bindings/weak_pointer.h" #include "unrealsdk/unreal/classes/uobject.h" +#include "unrealsdk/unreal/wrappers/weak_pointer.h" #ifdef PYUNREALSDK_INTERNAL diff --git a/src/pyunrealsdk/unreal_bindings/wrapped_array_magic_methods.cpp b/src/pyunrealsdk/unreal_bindings/wrapped_array_magic_methods.cpp index 1b2686a..56eee4a 100644 --- a/src/pyunrealsdk/unreal_bindings/wrapped_array_magic_methods.cpp +++ b/src/pyunrealsdk/unreal_bindings/wrapped_array_magic_methods.cpp @@ -162,7 +162,7 @@ py::iterator array_py_reversed(const WrappedArray& self) { bool array_py_contains(const WrappedArray& self, const py::object& value) { return std::find_if(ArrayIterator::begin(self), ArrayIterator::end(self), - [&value](auto other) { return value.equal(other); }) + [&value](const auto& other) { return value.equal(other); }) != ArrayIterator::end(self); } diff --git a/src/pyunrealsdk/unreal_bindings/wrapped_array_methods.cpp b/src/pyunrealsdk/unreal_bindings/wrapped_array_methods.cpp index 13a554e..33ac379 100644 --- a/src/pyunrealsdk/unreal_bindings/wrapped_array_methods.cpp +++ b/src/pyunrealsdk/unreal_bindings/wrapped_array_methods.cpp @@ -24,7 +24,7 @@ void array_py_clear(WrappedArray& self) { size_t array_py_count(const WrappedArray& self, const py::object& value) { return std::count_if(ArrayIterator::begin(self), ArrayIterator::end(self), - [&value](auto other) { return value.equal(other); }); + [&value](const auto& other) { return value.equal(other); }); } py::list array_py_copy(WrappedArray& self) { @@ -66,7 +66,7 @@ size_t array_py_index(const WrappedArray& self, auto end = ArrayIterator{self, convert_py_idx(self, stop)}; auto location = std::find_if(ArrayIterator{self, convert_py_idx(self, start)}, end, - [&value](auto other) { return value.equal(other); }); + [&value](const auto& other) { return value.equal(other); }); if (location == end) { throw py::value_error( unrealsdk::fmt::format("{} is not in array", std::string(py::repr(value)))); diff --git a/src/pyunrealsdk/unreal_bindings/wrapped_multicast_delegate.cpp b/src/pyunrealsdk/unreal_bindings/wrapped_multicast_delegate.cpp new file mode 100644 index 0000000..eef217a --- /dev/null +++ b/src/pyunrealsdk/unreal_bindings/wrapped_multicast_delegate.cpp @@ -0,0 +1,270 @@ +#include "pyunrealsdk/pch.h" +#include "pyunrealsdk/unreal_bindings/wrapped_multicast_delegate.h" +#include "pyunrealsdk/unreal_bindings/bound_function.h" +#include "unrealsdk/unreal/structs/fscriptdelegate.h" +#include "unrealsdk/unreal/wrappers/bound_function.h" +#include "unrealsdk/unreal/wrappers/wrapped_multicast_delegate.h" + +#ifdef PYUNREALSDK_INTERNAL + +using namespace unrealsdk::unreal; + +namespace pyunrealsdk::unreal { + +// We treat this class as a pseudo MutableSet - we only allow one copy of each bound function, and +// don't specify any ordering. +// We don't however implement any of the logical set operators + +namespace { + +// Create a custom iterator type to convert script delegates to bound functions when iterating +struct DelegateIterator { + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = BoundFunction; + using pointer = void; + using reference = BoundFunction; + + const TArray* arr = nullptr; + size_t idx = 0; + + reference operator*(void) const { return *arr->data[this->idx].as_function(); } + + DelegateIterator& operator++() { + ++this->idx; + + // If we're on the last index, set the array to null to end the iterator + if (this->idx >= this->arr->size()) { + this->arr = nullptr; + } + + return *this; + } + DelegateIterator operator++(int) { + auto tmp = *this; + ++(*this); + return tmp; + } + + bool operator==(const DelegateIterator& rhs) const { + return this->arr == rhs.arr && (this->arr == nullptr || this->idx == rhs.idx); + } + bool operator!=(const DelegateIterator& rhs) const { return !(*this == rhs); } + + static DelegateIterator begin(const WrappedMulticastDelegate& delegate) { + if (delegate.base->size() == 0) { + return end(delegate); + } + return {.arr = delegate.base.get(), .idx = 0}; + } + static DelegateIterator end(const WrappedMulticastDelegate& /*delegate*/) { + return {.arr = nullptr, .idx = 0}; + } +}; + +/** + * @brief Searches through a multicast delegate looking for an entry matching a particular function. + * + * @param self The multicast delegate to search through. + * @param value The function to search for. + * @return A pointer to the matching entry, or nullptr if no matches. + */ +FScriptDelegate* find_matching_delegate(WrappedMulticastDelegate& self, + const BoundFunction& value) { + auto begin = &self.base->data[0]; + auto end = &self.base->data[self.base->count]; + + auto ptr = std::find_if(begin, end, [&value](FScriptDelegate other) { + // Do the cheap checks first + if (value.object != other.get_object() || value.func->Name != other.func_name) { + return false; + } + + // Now check it resolves to the same function, which is a more expensive check + // Manually iterate through fields to avoid the exceptions if we can't find what we're + // looking for + for (auto field : value.object->Class->fields()) { + if (field->Name == other.func_name) { + // Return immediately on the first name match + return field == value.func; + } + } + return false; + }); + + return ptr == end ? nullptr : ptr; +} + +/** + * @brief Removes an entry from the given multicast delegate by value. + * + * @param self The multicast delegate to remove from. + * @param value The function to remove. + * @return True if the function was removed, false if it wasn't found. + */ +bool remove_from_multicast_delegate(WrappedMulticastDelegate& self, const BoundFunction& value) { + auto ptr = find_matching_delegate(self, value); + if (ptr == nullptr) { + return false; + } + + auto end = &self.base->data[self.base->count]; + memmove(ptr, ptr + 1, (end - ptr) * sizeof(*ptr)); + + self.base->resize(self.base->size() - 1); + return true; +} + +// This needs to be a function to avoid an unreachable code warning in `pybind11/detail/init.h` in +// `no_nullptr` when compiling with MSVC. +// I assume this is something to do with inlining optimizations - since we don't actually return, it +// can assume it's nullptr, somehow ignoring the throw here, but not the one later? +// Not thrilled with this as a solution, but it works for now + +// __init__ +WrappedMulticastDelegate* delegate_init_new(const py::args& /* args */, + const py::kwargs& /* kwargs */) { + throw py::type_error("Cannot create new wrapped multicast delegate instances."); +} + +} // namespace + +void register_wrapped_multicast_delegate(py::module_& mod) { + py::class_(mod, "WrappedMulticastDelegate") + .def(py::init(&delegate_init_new)) + .def("__new__", &delegate_init_new) + .def( + "__call__", + [](WrappedMulticastDelegate& self, const py::args& args, const py::kwargs& kwargs) { + impl::PyCallInfo info{self.signature, args, kwargs}; + + // Release the GIL to avoid a deadlock if ProcessEvent is locking. + const py::gil_scoped_release gil{}; + self.call(info.params); + }, + "Calls all functions bound to this delegate.\n" + "\n" + "Args:\n" + " The unreal function's args. This has all the same semantics as calling a\n" + " BoundFunction.") + .def( + "__contains__", + [](WrappedMulticastDelegate& self, const BoundFunction& value) { + return find_matching_delegate(self, value) != nullptr; + }, + "Checks if a function is already bound to this delegate.\n" + "\n" + "Args:\n" + " value: The function to search for.\n" + "Returns:\n" + " True if the function is already bound.", + "value"_a) + .def( + "__iter__", + [](WrappedMulticastDelegate& self) { + return py::make_iterator(DelegateIterator::begin(self), + DelegateIterator::end(self)); + }, + // Keep the delegate alive as long as the iterator is + py::keep_alive<0, 1>(), + "Creates an iterator over all functions bound to this delegate.\n" + "\n" + "Returns:\n" + " An iterator over all functions bound to this delegate.") + .def( + "__len__", [](WrappedMulticastDelegate& self) { return self.base->size(); }, + "Gets the number of functions which are bound to this delegate.\n" + "\n" + "Returns:\n" + " The number of bound functions.") + .def( + "__repr__", + [](WrappedMulticastDelegate& self) { + if (self.base->size() == 0) { + return std::string{"WrappedMulticastDelegate()"}; + } + + std::ostringstream output; + output << "{"; + + for (size_t i = 0; i < self.base->size(); i++) { + if (i > 0) { + output << ", "; + } + output << py::repr(py::cast(self.base->data[i].as_function())); + } + + output << "}"; + return output.str(); + }, + "Gets a string representation of this delegate.\n" + "\n" + "Returns:\n" + " The string representation.") + .def( + "add", + [](WrappedMulticastDelegate& self, const BoundFunction& value) { + if (find_matching_delegate(self, value) == nullptr) { + self.push_back(value); + } + }, + "Binds a new function to this delegate.\n" + "\n" + "This has no effect if the function is already present.\n" + "\n" + "Args:\n" + " value: The function to bind.", + "value"_a) + .def( + "clear", [](WrappedMulticastDelegate& self) { self.clear(); }, + "Removes all functions bound to this delegate.") + .def( + "discard", + [](WrappedMulticastDelegate& self, const BoundFunction& value) { + remove_from_multicast_delegate(self, value); + }, + "Removes a function from this delegate if it is present.\n" + "\n" + "Args:\n" + " value: The function to remove.", + "value"_a) + .def( + "pop", + [](WrappedMulticastDelegate& self) { + if (self.base->size() == 0) { + throw py::key_error("pop from an empty delegate"); + } + + // The easiest arbitrary element is the one at the end + auto size = self.base->size(); + auto value = self.base->data[size - 1].as_function(); + self.base->resize(size - 1); + + return value; + }, + "Removes an arbitrary function from this delegate.\n" + "\n" + "Throws a KeyError if the delegate has no bound functions.\n" + "\n" + "Returns:\n" + " The function which was removed.") + .def( + "remove", + [](WrappedMulticastDelegate& self, const BoundFunction& value) { + if (!remove_from_multicast_delegate(self, value)) { + throw py::key_error(py::repr(py::cast(value))); + } + }, + "Removes a function from this delegate.\n" + "\n" + "Throws a KeyError if the function is not present.\n" + "\n" + "Args:\n" + " value: The function to remove.", + "value"_a) + .def_readwrite("_signature", &WrappedMulticastDelegate::signature); +} + +} // namespace pyunrealsdk::unreal + +#endif diff --git a/src/pyunrealsdk/unreal_bindings/wrapped_multicast_delegate.h b/src/pyunrealsdk/unreal_bindings/wrapped_multicast_delegate.h new file mode 100644 index 0000000..e09eebf --- /dev/null +++ b/src/pyunrealsdk/unreal_bindings/wrapped_multicast_delegate.h @@ -0,0 +1,21 @@ +#ifndef PYUNREALSDK_UNREAL_BINDINGS_WRAPPED_MULTICAST_DELEGATE_H +#define PYUNREALSDK_UNREAL_BINDINGS_WRAPPED_MULTICAST_DELEGATE_H + +#include "pyunrealsdk/pch.h" + +#ifdef PYUNREALSDK_INTERNAL + +namespace pyunrealsdk::unreal { + +/** + * @brief Registers WrappedMulticastDelegate. + * + * @param module The module to register within. + */ +void register_wrapped_multicast_delegate(py::module_& mod); + +} // namespace pyunrealsdk::unreal + +#endif + +#endif /* PYUNREALSDK_UNREAL_BINDINGS_WRAPPED_MULTICAST_DELEGATE_H */ diff --git a/src/pyunrealsdk/unreal_bindings/wrapped_struct.cpp b/src/pyunrealsdk/unreal_bindings/wrapped_struct.cpp index ea0ed8d..270da76 100644 --- a/src/pyunrealsdk/unreal_bindings/wrapped_struct.cpp +++ b/src/pyunrealsdk/unreal_bindings/wrapped_struct.cpp @@ -22,11 +22,11 @@ WrappedStruct make_struct(const UScriptStruct* type, // Convert the kwarg keys to FNames, to make them case insensitive // This should also in theory speed up lookups, since hashing is simpler std::unordered_map converted_kwargs{}; - std::transform(kwargs.begin(), kwargs.end(), - std::inserter(converted_kwargs, converted_kwargs.end()), [](const auto& pair) { - return std::make_pair(py::cast(pair.first), - py::reinterpret_borrow(pair.second)); - }); + std::ranges::transform( + kwargs, std::inserter(converted_kwargs, converted_kwargs.end()), [](const auto& pair) { + return std::make_pair(py::cast(pair.first), + py::reinterpret_borrow(pair.second)); + }); size_t arg_idx = 0; for (auto prop : type->properties()) { diff --git a/stubs/unrealsdk/unreal/__init__.pyi b/stubs/unrealsdk/unreal/__init__.pyi index 21d40a6..3aa3e52 100644 --- a/stubs/unrealsdk/unreal/__init__.pyi +++ b/stubs/unrealsdk/unreal/__init__.pyi @@ -40,6 +40,7 @@ from ._uobject_children import ( ) from ._weak_pointer import WeakPointer from ._wrapped_array import WrappedArray +from ._wrapped_multicast_delegate import WrappedMulticastDelegate from ._wrapped_struct import WrappedStruct __all__: tuple[str, ...] = ( @@ -79,6 +80,7 @@ __all__: tuple[str, ...] = ( "UUInt64Property", "WeakPointer", "WrappedArray", + "WrappedMulticastDelegate", "WrappedStruct", "dir_includes_unreal", ) diff --git a/stubs/unrealsdk/unreal/_bound_function.pyi b/stubs/unrealsdk/unreal/_bound_function.pyi index 68164c9..c292a97 100644 --- a/stubs/unrealsdk/unreal/_bound_function.pyi +++ b/stubs/unrealsdk/unreal/_bound_function.pyi @@ -35,3 +35,10 @@ class BoundFunction: func: The function to bind. object: The object the function is bound to. """ + def __repr__(self) -> str: + """ + Gets a string representation of this function and the object it's bound to. + + Returns: + The string representation. + """ diff --git a/stubs/unrealsdk/unreal/_uobject_children.pyi b/stubs/unrealsdk/unreal/_uobject_children.pyi index 94e66c8..1b85870 100644 --- a/stubs/unrealsdk/unreal/_uobject_children.pyi +++ b/stubs/unrealsdk/unreal/_uobject_children.pyi @@ -126,6 +126,10 @@ class UClass(UStruct): True if this class implements the interface, false otherwise. """ +class UDelegateProperty(UProperty): + @property + def Signature(self) -> UFunction: ... + class UDoubleProperty(UProperty): ... class UEnumProperty(UProperty): @@ -159,6 +163,11 @@ class UInterfaceProperty(UProperty): def InterfaceClass(self) -> UClass: ... class UIntProperty(UProperty): ... + +class UMulticastDelegateProperty(UProperty): + @property + def Signature(self) -> UFunction: ... + class UNameProperty(UProperty): ... class UObjectProperty(UProperty): diff --git a/stubs/unrealsdk/unreal/_wrapped_multicast_delegate.pyi b/stubs/unrealsdk/unreal/_wrapped_multicast_delegate.pyi new file mode 100644 index 0000000..f969cd1 --- /dev/null +++ b/stubs/unrealsdk/unreal/_wrapped_multicast_delegate.pyi @@ -0,0 +1,94 @@ +from collections.abc import Iterator +from typing import Any, Never + +from ._bound_function import BoundFunction +from ._uobject_children import UFunction + +class WrappedMulticastDelegate: + _signature: UFunction + + def __call__(self, *args: Any, **kwargs: Any) -> Any: # noqa: D417 + """ + Calls all functions bound to this delegate. + + Args: + The unreal function's args. This has all the same semantics as calling a + BoundFunction. + """ + + def __contains__(self, value: BoundFunction) -> bool: + """ + Checks if a function is already bound to this delegate. + + Args: + value: The function to search for. + Returns: + True if the function is already bound. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> Never: ... + def __iter__(self) -> Iterator[BoundFunction]: + """ + Creates an iterator over all functions bound to this delegate. + + Returns: + An iterator over all functions bound to this delegate. + """ + + def __len__(self) -> int: + """ + Gets the number of functions which are bound to this delegate. + + Returns: + The number of bound functions. + """ + + def __new__(cls, *args: Any, **kwargs: Any) -> Never: ... + def __repr__(self) -> str: + """ + Gets a string representation of this delegate. + + Returns: + The string representation. + """ + + def add(self, value: BoundFunction) -> None: + """ + Binds a new function to this delegate. + + This has no effect if the function is already present. + + Args: + value: The function to bind. + """ + + def clear(self) -> None: + """Removes all functions bound to this delegate.""" + + def discard(self, value: BoundFunction) -> None: + """ + Removes a function from this delegate if it is present. + + Args: + value: The function to remove. + """ + + def pop(self) -> BoundFunction: + """ + Removes an arbitrary function from this delegate. + + Throws a KeyError if the delegate has no bound functions. + + Returns: + The function which was removed. + """ + + def remove(self, value: BoundFunction) -> None: + """ + Removes a function from this delegate. + + Throws a KeyError if the function is not present. + + Args: + value: The function to remove. + """