Skip to content
Open
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
14 changes: 12 additions & 2 deletions core/object/object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1340,15 +1340,25 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int

Error err = OK;

// Temporary used for binding source with CONNECT_APPEND_SOURCE_OBJECT.
// Fine when combined with CONNECT_DEFERRED as the Callable is copied when enqueued.
Callable callable_with_source_object_binded;

for (uint32_t i = 0; i < slot_count; ++i) {
const Callable &callable = slot_callables[i];
const Callable *callable_ptr = &slot_callables[i];
const uint32_t &flags = slot_flags[i];

if (!callable.is_valid()) {
if (!callable_ptr->is_valid()) {
// Target might have been deleted during signal callback, this is expected and OK.
continue;
}

if (flags & CONNECT_APPEND_SOURCE_OBJECT) {
callable_with_source_object_binded = callable_ptr->bind(this);
Copy link
Contributor

@Rindbee Rindbee Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using bind() again (like a Russian nesting doll) will cancel the source's emission (not the original arguments) if unbinding is enabled (unbind > 0).

1

callable_ptr = &callable_with_source_object_binded;
}
const Callable &callable = *callable_ptr;

const Variant **args = p_args;
int argc = p_argcount;

Expand Down
2 changes: 1 addition & 1 deletion doc/classes/Object.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@
Reference-counted connections can be assigned to the same [Callable] multiple times. Each disconnection decreases the internal counter. The signal fully disconnects only when the counter reaches 0.
</constant>
<constant name="CONNECT_APPEND_SOURCE_OBJECT" value="16" enum="ConnectFlags">
The source object is automatically bound when a [PackedScene] is instantiated. If this flag bit is enabled, the source object will be appended right after the original arguments of the signal.
The source object is automatically bound to the connected [Callable] on signal emission. If this flag bit is enabled, the source object will be appended right after the original arguments of the signal.
</constant>
</constants>
</class>
8 changes: 0 additions & 8 deletions scene/resources/packed_scene.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -664,9 +664,6 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
Callable callable(cto, snames[c.method]);

Array binds;
if (c.flags & CONNECT_APPEND_SOURCE_OBJECT) {
binds.push_back(cfrom);
}

for (int bind : c.binds) {
binds.push_back(props[bind]);
Expand Down Expand Up @@ -1193,11 +1190,6 @@ Error SceneState::_parse_connections(Node *p_owner, Node *p_node, HashMap<String
unbinds = ccu->get_unbinds();
base_callable = ccu->get_callable();
}

// The source object may already be bound, ignore it to avoid saving the source object.
if ((c.flags & CONNECT_APPEND_SOURCE_OBJECT) && (p_node == binds[0])) {
binds.remove_at(0);
}
} else {
base_callable = c.callable;
}
Expand Down
72 changes: 72 additions & 0 deletions tests/core/object/test_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,29 @@ TEST_CASE("[Object] Absent name getter") {
"The returned value should equal nil variant.");
}

class SignalReceiver : public Object {
GDCLASS(SignalReceiver, Object);

public:
Vector<Variant> received_args;

void callback0() {
received_args = Vector<Variant>{};
}

void callback1(Variant p_arg1) {
received_args = Vector<Variant>{ p_arg1 };
}

void callback2(Variant p_arg1, Variant p_arg2) {
received_args = Vector<Variant>{ p_arg1, p_arg2 };
}

void callback3(Variant p_arg1, Variant p_arg2, Variant p_arg3) {
received_args = Vector<Variant>{ p_arg1, p_arg2, p_arg3 };
}
};

TEST_CASE("[Object] Signals") {
Object object;

Expand Down Expand Up @@ -455,6 +478,55 @@ TEST_CASE("[Object] Signals") {
object.get_all_signal_connections(&signal_connections);
CHECK(signal_connections.size() == 0);
}

SUBCASE("Connecting with CONNECT_APPEND_SOURCE_OBJECT flag") {
SignalReceiver target;

object.connect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback1), Object::CONNECT_APPEND_SOURCE_OBJECT);
object.emit_signal("my_custom_signal");
CHECK_EQ(target.received_args, Vector<Variant>{ &object });
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback1));

object.connect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback2), Object::CONNECT_APPEND_SOURCE_OBJECT);
object.emit_signal("my_custom_signal", "emit_arg");
CHECK_EQ(target.received_args, Vector<Variant>{ "emit_arg", &object });
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback2));

object.connect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback2).bind("bind_arg"), Object::CONNECT_APPEND_SOURCE_OBJECT);
object.emit_signal("my_custom_signal");
CHECK_EQ(target.received_args, Vector<Variant>{ &object, "bind_arg" });
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback2));

object.connect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback3).bind("bind_arg"), Object::CONNECT_APPEND_SOURCE_OBJECT);
object.emit_signal("my_custom_signal", "emit_arg");
CHECK_EQ(target.received_args, Vector<Variant>{ "emit_arg", &object, "bind_arg" });
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback3));

object.connect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback3).bind(&object), Object::CONNECT_APPEND_SOURCE_OBJECT);
object.emit_signal("my_custom_signal", &object);
CHECK_EQ(target.received_args, Vector<Variant>{ &object, &object, &object });
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback3));

object.connect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback0).unbind(1), Object::CONNECT_APPEND_SOURCE_OBJECT);
object.emit_signal("my_custom_signal");
CHECK_EQ(target.received_args, Vector<Variant>{});
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback0));

object.connect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback1).unbind(1), Object::CONNECT_APPEND_SOURCE_OBJECT);
object.emit_signal("my_custom_signal", "emit_arg");
CHECK_EQ(target.received_args, Vector<Variant>{ "emit_arg" });
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback1));

object.connect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback1).bind("bind_arg").unbind(1), Object::CONNECT_APPEND_SOURCE_OBJECT);
object.emit_signal("my_custom_signal");
CHECK_EQ(target.received_args, Vector<Variant>{ "bind_arg" });
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback1));

object.connect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback2).bind("bind_arg").unbind(1), Object::CONNECT_APPEND_SOURCE_OBJECT);
object.emit_signal("my_custom_signal", "emit_arg");
CHECK_EQ(target.received_args, Vector<Variant>{ "emit_arg", "bind_arg" });
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback2));
}
}

class NotificationObjectSuperclass : public Object {
Expand Down