Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
27 changes: 27 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
16 changes: 8 additions & 8 deletions src/pyunrealsdk/commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<py::ssize_t>(str.size()))};

py::exec(code_block, globals);
py::exec(code_block);
} catch (const std::exception& ex) {
logging::log_python_exception(ex);
}
Expand Down Expand Up @@ -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);
}
Expand Down
76 changes: 74 additions & 2 deletions src/pyunrealsdk/unreal_bindings/uobject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand Down Expand Up @@ -126,8 +133,12 @@ void register_uobject(py::module_& mod) {
}
}

py_setattr_direct(py_find_field(py::cast<FName>(name), self->Class),
reinterpret_cast<uintptr_t>(self), value);
auto field = py_find_field(py::cast<FName>(name), self->Class);
py_setattr_direct(field, reinterpret_cast<uintptr_t>(self), value);

if (should_notify_counter > 0 && field->is_instance(find_class<UProperty>())) {
self->post_edit_change_property(reinterpret_cast<UProperty*>(field));
}
},
"Writes a value to an unreal field on the object.\n"
"\n"
Expand All @@ -144,6 +155,10 @@ void register_uobject(py::module_& mod) {
throw py::attribute_error("cannot access null attribute");
}
py_setattr_direct(field, reinterpret_cast<uintptr_t>(self), value);

if (should_notify_counter > 0 && field->is_instance(find_class<UProperty>())) {
self->post_edit_change_property(reinterpret_cast<UProperty*>(field));
}
},
"Writes a value to an unreal field on the object.\n"
"\n"
Expand All @@ -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<FName, UProperty*> 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<UProperty*> chain;
chain.reserve(args.size());

for (const auto& val : args) {
chain.push_back(py::cast<UProperty*>(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_<ContextManager>(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
Expand Down
3 changes: 2 additions & 1 deletion stubs/unrealsdk/unreal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -97,6 +97,7 @@ __all__: tuple[str, ...] = (
"WrappedMulticastDelegate",
"WrappedStruct",
"dir_includes_unreal",
"notify_changes",
)

def dir_includes_unreal(should_include: bool) -> None:
Expand Down
39 changes: 38 additions & 1 deletion stubs/unrealsdk/unreal/_uobject.pyi
Original file line number Diff line number Diff line change
@@ -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:
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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.
"""
Loading