Merge pull request #114328 from kleonc/node_duplicating_signal_source_node_fix

`CONNECT_APPEND_SOURCE_OBJECT` on signal emission
This commit is contained in:
Rémi Verschelde
2026-01-20 00:22:32 +01:00
6 changed files with 111 additions and 17 deletions

View File

@@ -1340,6 +1340,9 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int
Error err = OK; Error err = OK;
Vector<const Variant *> append_source_mem;
Variant source = this;
for (uint32_t i = 0; i < slot_count; ++i) { for (uint32_t i = 0; i < slot_count; ++i) {
const Callable &callable = slot_callables[i]; const Callable &callable = slot_callables[i];
const uint32_t &flags = slot_flags[i]; const uint32_t &flags = slot_flags[i];
@@ -1352,6 +1355,31 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int
const Variant **args = p_args; const Variant **args = p_args;
int argc = p_argcount; int argc = p_argcount;
if (flags & CONNECT_APPEND_SOURCE_OBJECT) {
// Source is being appended regardless of unbinds.
// Implemented by inserting before the first to-be-unbinded arg.
int source_index = p_argcount - callable.get_unbound_arguments_count();
if (source_index >= 0) {
append_source_mem.resize(p_argcount + 1);
const Variant **args_mem = append_source_mem.ptrw();
for (int j = 0; j < source_index; j++) {
args_mem[j] = p_args[j];
}
args_mem[source_index] = &source;
for (int j = source_index; j < p_argcount; j++) {
args_mem[j + 1] = p_args[j];
}
args = args_mem;
argc = p_argcount + 1;
} else {
// More args unbound than provided, call will fail.
// Since appended source is non-unbindable, the error
// about too many unbinds should be correct as is.
}
}
if (flags & CONNECT_DEFERRED) { if (flags & CONNECT_DEFERRED) {
MessageQueue::get_singleton()->push_callablep(callable, args, argc, true); MessageQueue::get_singleton()->push_callablep(callable, args, argc, true);
} else { } else {

View File

@@ -1051,7 +1051,17 @@
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. 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>
<constant name="CONNECT_APPEND_SOURCE_OBJECT" value="16" enum="ConnectFlags"> <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. On signal emission, the source object is automatically appended after the original arguments of the signal, regardless of the connected [Callable]'s unbinds which affect only the original arguments of the signal (see [method Callable.unbind], [method Callable.get_unbound_arguments_count]).
[codeblock]
extends Object
signal test_signal
func test():
print(self) # Prints e.g. &lt;Object#35332818393&gt;
test_signal.connect(prints.unbind(1), CONNECT_APPEND_SOURCE_OBJECT)
test_signal.emit("emit_arg_1", "emit_arg_2") # Prints emit_arg_1 &lt;Object#35332818393&gt;
[/codeblock]
</constant> </constant>
</constants> </constants>
</class> </class>

View File

@@ -1668,12 +1668,13 @@ void ConnectionsDock::update_tree() {
if (cd.flags & CONNECT_ONE_SHOT) { if (cd.flags & CONNECT_ONE_SHOT) {
path += " (one-shot)"; path += " (one-shot)";
} }
if (cd.flags & CONNECT_APPEND_SOURCE_OBJECT) {
path += " (source)";
}
if (cd.unbinds > 0) { if (cd.unbinds > 0) {
path += " unbinds(" + itos(cd.unbinds) + ")"; path += " unbinds(" + itos(cd.unbinds) + ")";
} }
// CONNECT_APPEND_SOURCE_OBJECT is not affected by unbinds, list it between unbinds/binds to better indicate the final order.
if (cd.flags & CONNECT_APPEND_SOURCE_OBJECT) {
path += " (source)";
}
if (!cd.binds.is_empty()) { if (!cd.binds.is_empty()) {
path += " binds("; path += " binds(";
for (int i = 0; i < cd.binds.size(); i++) { for (int i = 0; i < cd.binds.size(); i++) {

View File

@@ -84,11 +84,6 @@ public:
unbinds = ccu->get_unbinds(); unbinds = ccu->get_unbinds();
base_callable = ccu->get_callable(); base_callable = ccu->get_callable();
} }
// The source object may already be bound, ignore it to prevent display of the source object.
if ((flags & CONNECT_APPEND_SOURCE_OBJECT) && (source == binds[0])) {
binds.remove_at(0);
}
} else { } else {
base_callable = p_connection.callable; base_callable = p_connection.callable;
} }

View File

@@ -664,9 +664,6 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
Callable callable(cto, snames[c.method]); Callable callable(cto, snames[c.method]);
Array binds; Array binds;
if (c.flags & CONNECT_APPEND_SOURCE_OBJECT) {
binds.push_back(cfrom);
}
for (int bind : c.binds) { for (int bind : c.binds) {
binds.push_back(props[bind]); binds.push_back(props[bind]);
@@ -1193,11 +1190,6 @@ Error SceneState::_parse_connections(Node *p_owner, Node *p_node, HashMap<String
unbinds = ccu->get_unbinds(); unbinds = ccu->get_unbinds();
base_callable = ccu->get_callable(); 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 { } else {
base_callable = c.callable; base_callable = c.callable;
} }

View File

@@ -316,6 +316,29 @@ TEST_CASE("[Object] Absent name getter") {
"The returned value should equal nil variant."); "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") { TEST_CASE("[Object] Signals") {
Object object; Object object;
@@ -455,6 +478,51 @@ TEST_CASE("[Object] Signals") {
object.get_all_signal_connections(&signal_connections); object.get_all_signal_connections(&signal_connections);
CHECK(signal_connections.size() == 0); 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));
// Source should be appended regardless of unbinding.
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>{ &object });
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>{ &object, "bind_arg" });
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback2));
object.connect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback2).unbind(1).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 });
object.disconnect("my_custom_signal", callable_mp(&target, &SignalReceiver::callback2));
}
} }
class NotificationObjectSuperclass : public Object { class NotificationObjectSuperclass : public Object {