Skip to content

Commit a0d8fdb

Browse files
authored
Merge pull request #71 from apple1417/master
support sending property change events
2 parents 4ed788b + 02665d9 commit a0d8fdb

File tree

7 files changed

+151
-14
lines changed

7 files changed

+151
-14
lines changed

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cmake_minimum_required(VERSION 3.25)
22

3-
project(pyunrealsdk VERSION 1.6.0)
3+
project(pyunrealsdk VERSION 1.7.0)
44

55
function(_pyunrealsdk_add_base_target_args target_name)
66
target_compile_features(${target_name} PUBLIC cxx_std_20)

changelog.md

+27
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# Changelog
22

3+
## v1.7.0
4+
5+
- Added support for sending property changed events. This is typically best done via the
6+
`unrealsdk.unreal.notify_changes` context manager.
7+
8+
[97e3e0c2](https://github.com/bl-sdk/unrealsdk/commit/97e3e0c2)
9+
10+
- Fixed that it was possible for the `unrealsdk` module in the global namespace to get replaced, if
11+
something during the init script messed with `sys.modules`. It is now imported during
12+
initialization.
13+
14+
[91cfee4b](https://github.com/bl-sdk/unrealsdk/commit/91cfee4b)
15+
16+
### unrealsdk v1.8.0
17+
For reference, the unrealsdk v1.8.0 changes this includes are:
18+
19+
> - Added support for sending property changed events, via `UObject::post_edit_change_property` and
20+
> `UObject::post_edit_change_chain_property`.
21+
>
22+
> [a6040da4](https://github.com/bl-sdk/unrealsdk/commit/a6040da4)
23+
>
24+
> - Made the error message when assigning incompatible array types more clear.
25+
>
26+
> See also https://github.com/bl-sdk/unrealsdk/issues/60 .
27+
>
28+
> [6222756c](https://github.com/bl-sdk/unrealsdk/commit/6222756c)
29+
330
## v1.6.0
431

532
- `WrappedStruct` now supports being copied via the `copy` module.

src/pyunrealsdk/commands.cpp

+8-8
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,10 @@ void py_cmd_handler(const wchar_t* line, size_t size, size_t cmd_len) {
9090
try {
9191
const py::gil_scoped_acquire gil{};
9292

93-
// Make sure unrealsdk is already in globals, for convenience
94-
// The init script and `pyexec` commands both use a local dict, so this won't affect them
95-
auto globals = py::globals();
96-
if (!globals.contains("unrealsdk")) {
97-
globals["unrealsdk"] = py::module_::import("unrealsdk");
98-
}
99-
10093
const py::str code_block{
10194
PyUnicode_FromWideChar(str.c_str(), static_cast<py::ssize_t>(str.size()))};
10295

103-
py::exec(code_block, globals);
96+
py::exec(code_block);
10497
} catch (const std::exception& ex) {
10598
logging::log_python_exception(ex);
10699
}
@@ -178,6 +171,13 @@ void register_module(py::module_& mod) {
178171
}
179172

180173
void register_commands(void) {
174+
// Make sure unrealsdk is already in globals, for convenience
175+
// The init script and `pyexec` commands both use a local dict, so this won't affect them
176+
auto globals = py::globals();
177+
if (!globals.contains("unrealsdk")) {
178+
globals["unrealsdk"] = py::module_::import("unrealsdk");
179+
}
180+
181181
unrealsdk::commands::add_command(L"pyexec", &pyexec_cmd_handler);
182182
unrealsdk::commands::add_command(L"py", &py_cmd_handler);
183183
}

src/pyunrealsdk/unreal_bindings/uobject.cpp

+74-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include "unrealsdk/format.h"
77
#include "unrealsdk/unreal/classes/uclass.h"
88
#include "unrealsdk/unreal/classes/uobject.h"
9+
#include "unrealsdk/unreal/classes/uproperty.h"
10+
#include "unrealsdk/unreal/find_class.h"
911
#include "unrealsdk/unreal/structs/fname.h"
1012
#include "unrealsdk/unrealsdk.h"
1113
#include "unrealsdk/utils.h"
@@ -29,6 +31,11 @@ UObject* uobject_init(const py::args& /* args */, const py::kwargs& /* kwargs */
2931
throw py::type_error("Cannot create new instances of unreal objects.");
3032
}
3133

34+
// Dummy class to make the context manager on
35+
struct ContextManager {};
36+
37+
size_t should_notify_counter = 0;
38+
3239
} // namespace
3340

3441
void register_uobject(py::module_& mod) {
@@ -126,8 +133,12 @@ void register_uobject(py::module_& mod) {
126133
}
127134
}
128135

129-
py_setattr_direct(py_find_field(py::cast<FName>(name), self->Class),
130-
reinterpret_cast<uintptr_t>(self), value);
136+
auto field = py_find_field(py::cast<FName>(name), self->Class);
137+
py_setattr_direct(field, reinterpret_cast<uintptr_t>(self), value);
138+
139+
if (should_notify_counter > 0 && field->is_instance(find_class<UProperty>())) {
140+
self->post_edit_change_property(reinterpret_cast<UProperty*>(field));
141+
}
131142
},
132143
"Writes a value to an unreal field on the object.\n"
133144
"\n"
@@ -144,6 +155,10 @@ void register_uobject(py::module_& mod) {
144155
throw py::attribute_error("cannot access null attribute");
145156
}
146157
py_setattr_direct(field, reinterpret_cast<uintptr_t>(self), value);
158+
159+
if (should_notify_counter > 0 && field->is_instance(find_class<UProperty>())) {
160+
self->post_edit_change_property(reinterpret_cast<UProperty*>(field));
161+
}
147162
},
148163
"Writes a value to an unreal field on the object.\n"
149164
"\n"
@@ -162,11 +177,68 @@ void register_uobject(py::module_& mod) {
162177
"\n"
163178
"Returns:\n"
164179
" This object's address.")
180+
.def(
181+
"_post_edit_change_property",
182+
[](UObject* self, std::variant<FName, UProperty*> prop) {
183+
std::visit([self](auto&& val) { self->post_edit_change_property(val); }, prop);
184+
},
185+
"Notifies the engine that we've made an external change to a property.\n"
186+
"\n"
187+
"This only works on top level properties, those directly on the object.\n"
188+
"\n"
189+
"Also see the notify_changes() context manager, which calls this automatically.\n"
190+
"\n"
191+
"Args:\n"
192+
" prop: The property, or the name of the property, which was changed.",
193+
"args"_a)
194+
.def(
195+
"_post_edit_change_chain_property",
196+
[](UObject* self, UProperty* prop, const py::args& args) {
197+
std::vector<UProperty*> chain;
198+
chain.reserve(args.size());
199+
200+
for (const auto& val : args) {
201+
chain.push_back(py::cast<UProperty*>(val));
202+
}
203+
self->post_edit_change_chain_property(prop, chain);
204+
},
205+
"Notifies the engine that we've made an external change to a chain of properties.\n"
206+
"\n"
207+
"This version allows notifying about changes inside (nested) structs.\n"
208+
"\n"
209+
"Args:\n"
210+
" prop: The property which was changed.\n"
211+
" *chain: The chain of properties to follow.",
212+
"prop"_a)
165213
.def_readwrite("ObjectFlags", &UObject::ObjectFlags)
166214
.def_readwrite("InternalIndex", &UObject::InternalIndex)
167215
.def_readwrite("Class", &UObject::Class)
168216
.def_readwrite("Name", &UObject::Name)
169217
.def_readwrite("Outer", &UObject::Outer);
218+
219+
// Create under an empty handle to prevent this type being normally accessible
220+
py::class_<ContextManager>(py::handle(), "context_manager", pybind11::module_local())
221+
.def("__enter__", [](const py::object& /*self*/) { should_notify_counter++; })
222+
.def("__exit__", [](const py::object& /*self */, const py::object& /*exc_type*/,
223+
const py::object& /*exc_value*/, const py::object& /*traceback*/) {
224+
if (should_notify_counter > 0) {
225+
should_notify_counter--;
226+
}
227+
});
228+
229+
mod.def(
230+
"notify_changes", []() { return ContextManager{}; },
231+
"Context manager to automatically notify the engine when you edit an object.\n"
232+
"\n"
233+
"This essentially just automatically calls obj._post_edit_change_property() after\n"
234+
"every setattr.\n"
235+
"\n"
236+
"Note that this only tracks top-level changes, it cannot track changes to inner\n"
237+
"struct fields, You will have to manually call obj._post_edit_chain_property()\n"
238+
"for them.\n"
239+
"\n"
240+
"Returns:\n"
241+
" A new context manager.");
170242
}
171243

172244
} // namespace pyunrealsdk::unreal

stubs/unrealsdk/unreal/__init__.pyi

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ from __future__ import annotations
44

55
from ._bound_function import BoundFunction
66
from ._uenum import UEnum
7-
from ._uobject import UObject
7+
from ._uobject import UObject, notify_changes
88
from ._uobject_children import (
99
UArrayProperty,
1010
UBlueprintGeneratedClass,
@@ -97,6 +97,7 @@ __all__: tuple[str, ...] = (
9797
"WrappedMulticastDelegate",
9898
"WrappedStruct",
9999
"dir_includes_unreal",
100+
"notify_changes",
100101
)
101102

102103
def dir_includes_unreal(should_include: bool) -> None:

stubs/unrealsdk/unreal/_uobject.pyi

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from __future__ import annotations
22

3+
from contextlib import AbstractContextManager
34
from typing import Any, Never
45

5-
from ._uobject_children import UClass, UField
6+
from ._uobject_children import UClass, UField, UProperty
67

78
class UObject:
89
"""
@@ -86,6 +87,27 @@ class UObject:
8687
Returns:
8788
This object's name.
8889
"""
90+
def _post_edit_change_property(self, prop: str | UProperty) -> None:
91+
"""
92+
Notifies the engine that we've made an external change to a property.
93+
94+
This only works on top level properties, those directly on the object.
95+
96+
Also see the notify_changes() context manager, which calls this automatically.
97+
98+
Args:
99+
prop: The property, or the name of the property, which was changed.
100+
"""
101+
def _post_edit_change_chain_property(self, prop: UProperty, *chain: UProperty) -> None:
102+
"""
103+
Notifies the engine that we've made an external change to a chain of properties.
104+
105+
This version allows notifying about changes inside (nested) structs.
106+
107+
Args:
108+
prop: The property which was changed.
109+
*chain: The chain of properties to follow.
110+
"""
89111
def _set_field(self, field: UField, value: Any) -> None:
90112
"""
91113
Writes a value to an unreal field on the object.
@@ -99,3 +121,18 @@ class UObject:
99121
field: The field to set.
100122
value: The value to write.
101123
"""
124+
125+
def notify_changes() -> AbstractContextManager[None]:
126+
"""
127+
Context manager to automatically notify the engine when you edit an object.
128+
129+
This essentially just automatically calls obj._post_edit_change_property() after
130+
every setattr.
131+
132+
Note that this only tracks top-level changes, it cannot track changes to inner
133+
struct fields, You will have to manually call obj._post_edit_chain_property()
134+
for them.
135+
136+
Returns:
137+
A new context manager.
138+
"""

0 commit comments

Comments
 (0)