Skip to content

ToSignal(...) causes handler leak when awaiting dialogue_ended signal #935

@mikenlanggio

Description

@mikenlanggio

🔍 Summary
When calling await ToSignal(DialogueManager.Instance, "dialogue_ended") multiple times in C#, the number of connected signal handlers increases with each call. These handlers are never disconnected, leading to callback accumulation, and eventually triggering NullReferenceException on subsequent signal emissions.

💻 Environment
Godot version: 4.4

Language: C# (.NET 6 / Mono)

DialogueManager version: newest

Platform: Windows 10

Runtime context: Using DialogueManager.Instance.StartAsync(...) from C#

🧪 Steps to Reproduce
Call this method multiple times (e.g. triggered via player interaction):

public override async Task _InteractAsync()
{
    GD.Print("---Call Open");
    await DialogManager.Instance.StartAsync("res://dialogue/convesations/open_tulanh.dialogue", "start", this); //Don't mind my `DialogManager` function, it just internally does extra processing to make the Dialog follow the player.

    await ToSignal(DialogueManager.Instance, "dialogue_ended");
    GD.Print("---Call Close");
}

After the first interaction, everything works normally.

From the second interaction onward, the following error is logged:

E 0:00:09:775   void System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1.MoveNext(System.Threading.Thread): System.NullReferenceException: Object reference not set to an instance of an object.
  <C++ Error>   System.NullReferenceException
  <Stack Trace> SignalAwaiter.cs:58 @ Godot.SignalAwaiter.SignalCallback(...)

Despite the error, execution continues to the end of the method.

🔎 Additional Debug Info
I added this debug line before emit_signal("dialogue_ended") inside the GDScript DialogueManager:

print("Listeners count:", get_signal_connection_list("dialogue_ended").size())

The listener count increases with each interaction. Example:

1st run: Listeners count: 1

2nd run: Listeners count: 2

3rd run: Listeners count: 3

...

This confirms that ToSignal creates a new signal connection each time, but the connection is not cleaned up after completion.

The internal callback is still being triggered later, but it seems the C# side has already disposed the corresponding await state, leading to a NullReferenceException.

✅ Expected Behavior
ToSignal(...) should automatically disconnect the signal handler after it resumes the task — or the library should provide a safe wrapper to prevent this accumulation.

🧼 Workaround
I replaced the ToSignal call with a manual connection using TaskCompletionSource:

public Task WaitForDialogueEnded()
{
    var tcs = new TaskCompletionSource();

    void Handler(Variant st)
    {
        DialogueManager.Instance.Disconnect("dialogue_ended", Callable.From((Action)Handler));
        tcs.SetResult();
    }

    DialogueManager.Instance.Connect("dialogue_ended", Callable.From((Action)Handler));
    return tcs.Task;
}

Now I call:

await WaitForDialogueEnded();

This prevents signal handler buildup and eliminates the exception.

P/S: I also tested ToSignal(...) with both a custom C# signal and a custom GDScript signal emitted from C#, and everything worked as expected — the handler was automatically disconnected after completion. This issue appears to be specific to the way dialogue_ended is implemented or emitted within the DialogueManager library.

P/s 2. I've tried to use like this await ToSignal(GetNode("/root/DialogueManager"), "dialogue_ended"); but issue remain

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions