diff --git a/CMakeLists.txt b/CMakeLists.txt index 23a258f..4b14294 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.25) -project(pyunrealsdk VERSION 1.6.0) +project(pyunrealsdk VERSION 1.7.0) function(_pyunrealsdk_add_base_target_args target_name) target_compile_features(${target_name} PUBLIC cxx_std_20) diff --git a/changelog.md b/changelog.md index 55df488..c1d0820 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,32 @@ # Changelog +## v1.7.0 + +- Added support for sending property changed events. This is typically best done via the + `unrealsdk.unreal.notify_changes` context manager. + + [97e3e0c2](https://github.com/bl-sdk/unrealsdk/commit/97e3e0c2) + +- Fixed that it was possible for the `unrealsdk` module in the global namespace to get replaced, if + something during the init script messed with `sys.modules`. It is now imported during + initialization. + + [91cfee4b](https://github.com/bl-sdk/unrealsdk/commit/91cfee4b) + +### unrealsdk v1.8.0 +For reference, the unrealsdk v1.8.0 changes this includes are: + +> - Added support for sending property changed events, via `UObject::post_edit_change_property` and +> `UObject::post_edit_change_chain_property`. +> +> [a6040da4](https://github.com/bl-sdk/unrealsdk/commit/a6040da4) +> +> - Made the error message when assigning incompatible array types more clear. +> +> See also https://github.com/bl-sdk/unrealsdk/issues/60 . +> +> [6222756c](https://github.com/bl-sdk/unrealsdk/commit/6222756c) + ## v1.6.0 - `WrappedStruct` now supports being copied via the `copy` module. diff --git a/libs/unrealsdk b/libs/unrealsdk index fbf29de..bfe72ca 160000 --- a/libs/unrealsdk +++ b/libs/unrealsdk @@ -1 +1 @@ -Subproject commit fbf29dedc5b9dabbb521f90eeedd2f1a3fb7d349 +Subproject commit bfe72ca0c27e100c16533834352a21d0d884afac diff --git a/src/pyunrealsdk/commands.cpp b/src/pyunrealsdk/commands.cpp index d81dc0f..f5f2cc5 100644 --- a/src/pyunrealsdk/commands.cpp +++ b/src/pyunrealsdk/commands.cpp @@ -90,17 +90,10 @@ void py_cmd_handler(const wchar_t* line, size_t size, size_t cmd_len) { try { const py::gil_scoped_acquire gil{}; - // Make sure unrealsdk is already in globals, for convenience - // The init script and `pyexec` commands both use a local dict, so this won't affect them - auto globals = py::globals(); - if (!globals.contains("unrealsdk")) { - globals["unrealsdk"] = py::module_::import("unrealsdk"); - } - const py::str code_block{ PyUnicode_FromWideChar(str.c_str(), static_cast(str.size()))}; - py::exec(code_block, globals); + py::exec(code_block); } catch (const std::exception& ex) { logging::log_python_exception(ex); } @@ -178,6 +171,13 @@ void register_module(py::module_& mod) { } void register_commands(void) { + // Make sure unrealsdk is already in globals, for convenience + // The init script and `pyexec` commands both use a local dict, so this won't affect them + auto globals = py::globals(); + if (!globals.contains("unrealsdk")) { + globals["unrealsdk"] = py::module_::import("unrealsdk"); + } + unrealsdk::commands::add_command(L"pyexec", &pyexec_cmd_handler); unrealsdk::commands::add_command(L"py", &py_cmd_handler); } diff --git a/src/pyunrealsdk/unreal_bindings/uobject.cpp b/src/pyunrealsdk/unreal_bindings/uobject.cpp index 6c7eb11..f9ad258 100644 --- a/src/pyunrealsdk/unreal_bindings/uobject.cpp +++ b/src/pyunrealsdk/unreal_bindings/uobject.cpp @@ -6,6 +6,8 @@ #include "unrealsdk/format.h" #include "unrealsdk/unreal/classes/uclass.h" #include "unrealsdk/unreal/classes/uobject.h" +#include "unrealsdk/unreal/classes/uproperty.h" +#include "unrealsdk/unreal/find_class.h" #include "unrealsdk/unreal/structs/fname.h" #include "unrealsdk/unrealsdk.h" #include "unrealsdk/utils.h" @@ -29,6 +31,11 @@ UObject* uobject_init(const py::args& /* args */, const py::kwargs& /* kwargs */ throw py::type_error("Cannot create new instances of unreal objects."); } +// Dummy class to make the context manager on +struct ContextManager {}; + +size_t should_notify_counter = 0; + } // namespace void register_uobject(py::module_& mod) { @@ -126,8 +133,12 @@ void register_uobject(py::module_& mod) { } } - py_setattr_direct(py_find_field(py::cast(name), self->Class), - reinterpret_cast(self), value); + auto field = py_find_field(py::cast(name), self->Class); + py_setattr_direct(field, reinterpret_cast(self), value); + + if (should_notify_counter > 0 && field->is_instance(find_class())) { + self->post_edit_change_property(reinterpret_cast(field)); + } }, "Writes a value to an unreal field on the object.\n" "\n" @@ -144,6 +155,10 @@ void register_uobject(py::module_& mod) { throw py::attribute_error("cannot access null attribute"); } py_setattr_direct(field, reinterpret_cast(self), value); + + if (should_notify_counter > 0 && field->is_instance(find_class())) { + self->post_edit_change_property(reinterpret_cast(field)); + } }, "Writes a value to an unreal field on the object.\n" "\n" @@ -162,11 +177,68 @@ void register_uobject(py::module_& mod) { "\n" "Returns:\n" " This object's address.") + .def( + "_post_edit_change_property", + [](UObject* self, std::variant prop) { + std::visit([self](auto&& val) { self->post_edit_change_property(val); }, prop); + }, + "Notifies the engine that we've made an external change to a property.\n" + "\n" + "This only works on top level properties, those directly on the object.\n" + "\n" + "Also see the notify_changes() context manager, which calls this automatically.\n" + "\n" + "Args:\n" + " prop: The property, or the name of the property, which was changed.", + "args"_a) + .def( + "_post_edit_change_chain_property", + [](UObject* self, UProperty* prop, const py::args& args) { + std::vector chain; + chain.reserve(args.size()); + + for (const auto& val : args) { + chain.push_back(py::cast(val)); + } + self->post_edit_change_chain_property(prop, chain); + }, + "Notifies the engine that we've made an external change to a chain of properties.\n" + "\n" + "This version allows notifying about changes inside (nested) structs.\n" + "\n" + "Args:\n" + " prop: The property which was changed.\n" + " *chain: The chain of properties to follow.", + "prop"_a) .def_readwrite("ObjectFlags", &UObject::ObjectFlags) .def_readwrite("InternalIndex", &UObject::InternalIndex) .def_readwrite("Class", &UObject::Class) .def_readwrite("Name", &UObject::Name) .def_readwrite("Outer", &UObject::Outer); + + // Create under an empty handle to prevent this type being normally accessible + py::class_(py::handle(), "context_manager", pybind11::module_local()) + .def("__enter__", [](const py::object& /*self*/) { should_notify_counter++; }) + .def("__exit__", [](const py::object& /*self */, const py::object& /*exc_type*/, + const py::object& /*exc_value*/, const py::object& /*traceback*/) { + if (should_notify_counter > 0) { + should_notify_counter--; + } + }); + + mod.def( + "notify_changes", []() { return ContextManager{}; }, + "Context manager to automatically notify the engine when you edit an object.\n" + "\n" + "This essentially just automatically calls obj._post_edit_change_property() after\n" + "every setattr.\n" + "\n" + "Note that this only tracks top-level changes, it cannot track changes to inner\n" + "struct fields, You will have to manually call obj._post_edit_chain_property()\n" + "for them.\n" + "\n" + "Returns:\n" + " A new context manager."); } } // namespace pyunrealsdk::unreal diff --git a/stubs/unrealsdk/unreal/__init__.pyi b/stubs/unrealsdk/unreal/__init__.pyi index 06a8fc1..74d753f 100644 --- a/stubs/unrealsdk/unreal/__init__.pyi +++ b/stubs/unrealsdk/unreal/__init__.pyi @@ -4,7 +4,7 @@ from __future__ import annotations from ._bound_function import BoundFunction from ._uenum import UEnum -from ._uobject import UObject +from ._uobject import UObject, notify_changes from ._uobject_children import ( UArrayProperty, UBlueprintGeneratedClass, @@ -97,6 +97,7 @@ __all__: tuple[str, ...] = ( "WrappedMulticastDelegate", "WrappedStruct", "dir_includes_unreal", + "notify_changes", ) def dir_includes_unreal(should_include: bool) -> None: diff --git a/stubs/unrealsdk/unreal/_uobject.pyi b/stubs/unrealsdk/unreal/_uobject.pyi index 86d8ac0..4dc8b9c 100644 --- a/stubs/unrealsdk/unreal/_uobject.pyi +++ b/stubs/unrealsdk/unreal/_uobject.pyi @@ -1,8 +1,9 @@ from __future__ import annotations +from contextlib import AbstractContextManager from typing import Any, Never -from ._uobject_children import UClass, UField +from ._uobject_children import UClass, UField, UProperty class UObject: """ @@ -86,6 +87,27 @@ class UObject: Returns: This object's name. """ + def _post_edit_change_property(self, prop: str | UProperty) -> None: + """ + Notifies the engine that we've made an external change to a property. + + This only works on top level properties, those directly on the object. + + Also see the notify_changes() context manager, which calls this automatically. + + Args: + prop: The property, or the name of the property, which was changed. + """ + def _post_edit_change_chain_property(self, prop: UProperty, *chain: UProperty) -> None: + """ + Notifies the engine that we've made an external change to a chain of properties. + + This version allows notifying about changes inside (nested) structs. + + Args: + prop: The property which was changed. + *chain: The chain of properties to follow. + """ def _set_field(self, field: UField, value: Any) -> None: """ Writes a value to an unreal field on the object. @@ -99,3 +121,18 @@ class UObject: field: The field to set. value: The value to write. """ + +def notify_changes() -> AbstractContextManager[None]: + """ + Context manager to automatically notify the engine when you edit an object. + + This essentially just automatically calls obj._post_edit_change_property() after + every setattr. + + Note that this only tracks top-level changes, it cannot track changes to inner + struct fields, You will have to manually call obj._post_edit_chain_property() + for them. + + Returns: + A new context manager. + """