Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a way to set Saliency Strategy from godot without having to write C# #85

Closed
misha-cilantro opened this issue Feb 16, 2025 · 35 comments

Comments

@misha-cilantro
Copy link

For folks who don't want to deal with C#, I suggest adding a simple way to set the Saliency Strategy of their DialogueRunner instance, via a node script, something like:

using Godot;
using System;
using Yarn.Saliency;
using YarnSpinnerGodot;

/// <summary>
/// Lets you set the saliency strategy of the chosen DialogueRunner.
/// Saliency Strategy determines how group nodes are chosen.
/// </summary>
[GlobalClass]
public partial class SaliencySetter : Node
{
    public enum SaliencyType
    {
        First,
        Best,
        BestLeastRecentlyViewed,
        RandomBestLeastRecentlyViewed
    }
    
    [Export]
    public DialogueRunner dialogueRunner;

    [Export]
    public VariableStorageBehaviour variableStorageBehaviour;
    
    [Export]
    public SaliencyType SaliencyStrategy = SaliencyType.First;

    override public void _Ready()
    {
        IContentSaliencyStrategy chosenStrategy;
        
        switch (SaliencyStrategy)
        {
            case SaliencyType.First:
                chosenStrategy = new FirstSaliencyStrategy();
                break;
            
            case SaliencyType.Best:
                chosenStrategy = new BestSaliencyStrategy();
                break;
            
            case SaliencyType.BestLeastRecentlyViewed:
                chosenStrategy = new BestLeastRecentlyViewedSalienceStrategy(variableStorageBehaviour);
                break;
            
            case SaliencyType.RandomBestLeastRecentlyViewed:
                chosenStrategy = new RandomBestLeastRecentlyViewedSalienceStrategy(variableStorageBehaviour);
                break;
            
            default:
                throw new Exception("Unknown Saliency Strategy");
        }
        
        dialogueRunner.Dialogue.ContentSaliencyStrategy = chosenStrategy;
    }
}
@misha-cilantro
Copy link
Author

misha-cilantro commented Feb 16, 2025

This could also be a field on the DialogueRunner itself, but that becomes iffy if people decide to set a custom strategy, while this extra node is totally optional. I suppose the enum could include a "Custom" value which doesn't set anything if the on-runner approach seems better.

@dogboydog
Copy link
Collaborator

dogboydog commented Feb 17, 2025

I tried this out locally, but when adding this to the space sample scene and hooking up the inspector fields, it crashes the game when talking to the Sally character, because Program in the variable storage somehow becomes null. I have no idea why though . The existing one liner added to SpaceSample.cs doesn't cause the same crash.

This is my version of your script that seems to be causing the Program to become null somehow as a side effect

using Godot;
using System;
using Yarn.Saliency;

namespace YarnSpinnerGodot;

/// <summary>
/// Lets you set the saliency strategy of the chosen DialogueRunner.
/// Saliency Strategy determines how group nodes are chosen.
///
/// Note: Does not support custom saliency strategies. For that use case you would want your own script
/// that implements and sets your custom strategy. 
/// </summary>
[GlobalClass]
public partial class SaliencySetter : Node
{
    public enum SaliencyType
    {
        First,
        Best,
        BestLeastRecentlyViewed,
        RandomBestLeastRecentlyViewed
    }

    [Export] public DialogueRunner dialogueRunner;

    [Export] public VariableStorageBehaviour variableStorageBehaviour;

    [Export] public SaliencyType SaliencyStrategy = SaliencyType.First;

    public override void _Ready()
    {
        if (!IsInstanceValid(dialogueRunner))
        {
            GD.PushError($"{nameof(dialogueRunner)} is not set on {nameof(SaliencySetter)}! You must set it to use this script");
            return;
        }
        if (!IsInstanceValid(variableStorageBehaviour))
        {
            GD.PushError($"{nameof(variableStorageBehaviour)} is not set on {nameof(SaliencySetter)}! You must set it to use this script");
            return;
        }
        IContentSaliencyStrategy chosenStrategy = SaliencyStrategy switch
        {
            SaliencyType.First => new FirstSaliencyStrategy(),
            SaliencyType.Best => new BestSaliencyStrategy(),
            SaliencyType.BestLeastRecentlyViewed => new BestLeastRecentlyViewedSalienceStrategy(
                variableStorageBehaviour),
            SaliencyType.RandomBestLeastRecentlyViewed => new RandomBestLeastRecentlyViewedSalienceStrategy(
                variableStorageBehaviour),
            _ => throw new Exception("Unknown Saliency Strategy")
        };

        dialogueRunner.Dialogue.ContentSaliencyStrategy = chosenStrategy;
    }
}

@misha-cilantro
Copy link
Author

Hmm weird. It looks fine, and it worked just fine dropped into my project. I tried getting the samples working but failed; I'll try again tomorrow to see if I can repro. Possibly some change between your latest and what I pulled down causing the issue?

@misha-cilantro
Copy link
Author

Or maybe a race condition if multiple things are trying to mess with the dialogue runner on ready 🤔

@misha-cilantro
Copy link
Author

Okay had no issue using this code when pulling down from the PR. Going to try latest.

@misha-cilantro
Copy link
Author

Are there docs somewhere for getting latest to build? It complains about a lot of missing dependencies, only some of which I'm finding on nuget.

@dogboydog
Copy link
Collaborator

I hit the error when running the Space sample talking to the Sally character on the left and choosing the first dialogue option.

What do you mean by latest versus the PR? The PR is currently the latest version of v3. All dependencies in the v3 branch are available on nuget (see CI builds)

@dogboydog
Copy link
Collaborator

If you mean the develop branch that's the 0.2 version which doesn't have the saliency strategy concept

@misha-cilantro
Copy link
Author

Ah, I'm just a little confused by github. I don't usually use it for much more than my own stuff. (Azure for work heh.) So if what I download here contains all the latest commits, then everything works fine for me. I'm able to start the dialogue, talk to Sally, select the first dialog options, get a response ("Same old nebula" etc.)

How do you have your scene set up for testing this? Could you send me a version of the scene that has the error so I can dig into what the difference might be?

This is my setup:

Image

Also, what strategy did you select? Could be some issue with the strategy chosen, I'll try the others when I get back to my PC.

@misha-cilantro
Copy link
Author

Okay, all four saliency strategies worked fine, I was able to progress through the Sally dialog without issue.

@dogboydog
Copy link
Collaborator

I'm not sure why the difference. My setup seems the same as yours. Are you using a different Godot version? I have 4.3 stable

E 0:00:08:0382   void YarnSpinnerGodot.YarnTask+<Forget>d__17.MoveNext(): System.NullReferenceException: Object reference not set to an instance of an object.
   at YarnSpinnerGodot.VariableStorageBehaviour.GetVariableKind(String name) in C:\Users\chris\YarnSpinner-Godot\addons\YarnSpinner-Godot\Runtime\VariableStorageBehaviour.cs:line 91
   at YarnSpinnerGodot.InMemoryVariableStorage.TryGetValue[T](String variableName, T& result) in C:\Users\chris\YarnSpinner-Godot\addons\YarnSpinner-Godot\Runtime\InMemoryVariableStorage.cs:line 191
   at Yarn.Saliency.RandomBestLeastRecentlyViewedSalienceStrategy.<QueryBestContent>b__5_1(ContentSaliencyOption c)
   at System.Linq.Enumerable.ListSelectIterator`2.MoveNext()
   at System.Linq.Lookup`2.Create(IEnumerable`1 source, Func`2 keySelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.GroupByIterator`2.MoveNext()
   at System.Linq.Enumerable.OrderedIterator`2.TryGetFirst(Boolean& found)
   at System.Linq.Enumerable.First[TSource](IEnumerable`1 source)
   at Yarn.Saliency.RandomBestLeastRecentlyViewedSalienceStrategy.QueryBestContent(IEnumerable`1 content)
   at Yarn.VirtualMachine.RunInstruction(Instruction i)
   at Yarn.VirtualMachine.Continue()
   at Yarn.Dialogue.Continue()
   at YarnSpinnerGodot.DialogueRunner.OnLineReceivedAsync(Line line) in C:\Users\chris\YarnSpinner-Godot\addons\YarnSpinner-Godot\Runtime\DialogueRunner.cs:line 639
   at YarnSpinnerGodot.YarnTask.Forget() in C:\Users\chris\YarnSpinner-Godot\addons\YarnSpinner-Godot\Runtime\Async\YarnTask\YarnTask.Task.cs:line 32
  <C# Source>    YarnTask.Task.cs:32 @ void YarnSpinnerGodot.YarnTask+<Forget>d__17.MoveNext()
  <Stack Trace>  GD.cs:382 @ void Godot.GD.PushError(System.Object[])
                 YarnTask.Task.cs:32 @ void YarnSpinnerGodot.YarnTask+<Forget>d__17.MoveNext()
                 :0 @ void System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1.MoveNext(System.Threading.Thread)
                 :0 @ void System.Threading.Tasks.AwaitTaskContinuation.RunCallback(System.Threading.ContextCallback, object, System.Threading.Tasks.Task&)
                 :0 @ void System.Threading.Tasks.Task.RunContinuations(object)
                 :0 @ void System.Threading.Tasks.Task.FinishSlow(bool)
                 :0 @ bool System.Threading.Tasks.Task.TrySetException(object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetException(System.Exception, System.Threading.Tasks.Task`1[TResult]&)
                 YarnTask.AsyncMethodBuilder.cs:47 @ void YarnSpinnerGodot.YarnTaskMethodBuilder.SetException(System.Exception)
                 DialogueRunner.cs:639 @ void YarnSpinnerGodot.DialogueRunner+<OnLineReceivedAsync>d__55.MoveNext()
                 :0 @ void System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1.MoveNext(System.Threading.Thread)
                 :0 @ void System.Threading.Tasks.AwaitTaskContinuation.RunCallback(System.Threading.ContextCallback, object, System.Threading.Tasks.Task&)
                 :0 @ void System.Threading.Tasks.Task.RunContinuations(object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()
                 YarnTask.AsyncMethodBuilder.cs:52 @ void YarnSpinnerGodot.YarnTaskMethodBuilder.SetResult()
                 DialogueRunner.cs:760 @ void YarnSpinnerGodot.DialogueRunner+<RunLocalisedLine>d__56.MoveNext()
                 :0 @ void System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1.MoveNext(System.Threading.Thread)
                 :0 @ void System.Threading.Tasks.AwaitTaskContinuation.RunCallback(System.Threading.ContextCallback, object, System.Threading.Tasks.Task&)
                 :0 @ void System.Threading.Tasks.Task.RunContinuations(object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()
                 YarnTask.AsyncMethodBuilder.cs:52 @ void YarnSpinnerGodot.YarnTaskMethodBuilder.SetResult()
                 YarnTask.Task.cs:92 @ void YarnSpinnerGodot.YarnTask+<WhenAll>d__6.MoveNext()
                 :0 @ void System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1.MoveNext(System.Threading.Thread)
                 :0 @ void System.Threading.Tasks.AwaitTaskContinuation.RunCallback(System.Threading.ContextCallback, object, System.Threading.Tasks.Task&)
                 :0 @ void System.Threading.Tasks.Task.RunContinuations(object)
                 :0 @ bool System.Threading.Tasks.Task.TrySetResult()
                 :0 @ void System.Threading.Tasks.Task+WhenAllPromise.Invoke(System.Threading.Tasks.Task)
                 :0 @ void System.Threading.Tasks.Task.RunContinuations(object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()
                 YarnTask.AsyncMethodBuilder.cs:52 @ void YarnSpinnerGodot.YarnTaskMethodBuilder.SetResult()
                 DialogueRunner.cs:715 @ void YarnSpinnerGodot.DialogueRunner+<>c__DisplayClass56_2+<<RunLocalisedLine>g__RunLineAndInvokeCompletion|1>d.MoveNext()
                 :0 @ void System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1.MoveNext(System.Threading.Thread)
                 :0 @ void System.Threading.Tasks.AwaitTaskContinuation.RunCallback(System.Threading.ContextCallback, object, System.Threading.Tasks.Task&)
                 :0 @ void System.Threading.Tasks.Task.RunContinuations(object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()
                 YarnTask.AsyncMethodBuilder.cs:52 @ void YarnSpinnerGodot.YarnTaskMethodBuilder.SetResult()
                 AsyncLineView.cs:464 @ void YarnSpinnerGodot.AsyncLineView+<RunLineAsync>d__24.MoveNext()
                 :0 @ void System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1.MoveNext(System.Threading.Thread)
                 :0 @ void System.Threading.Tasks.AwaitTaskContinuation.RunCallback(System.Threading.ContextCallback, object, System.Threading.Tasks.Task&)
                 :0 @ void System.Threading.Tasks.Task.RunContinuations(object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()
                 YarnTask.AsyncMethodBuilder.cs:52 @ void YarnSpinnerGodot.YarnTaskMethodBuilder.SetResult()
                 YarnTask.Task.cs:46 @ void YarnSpinnerGodot.YarnTask+<WaitUntilCanceled>d__0.MoveNext()
                 :0 @ void System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1.MoveNext(System.Threading.Thread)
                 :0 @ void System.Threading.Tasks.AwaitTaskContinuation.RunCallback(System.Threading.ContextCallback, object, System.Threading.Tasks.Task&)
                 :0 @ void System.Threading.Tasks.Task.RunContinuations(object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetResult()
                 DefaultActions.cs:24 @ void YarnSpinnerGodot.DefaultActions+<Wait>d__0.MoveNext()
                 :0 @ void System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, object)
                 :0 @ void System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1.MoveNext(System.Threading.Thread)
                 SignalAwaiter.cs:58 @ void Godot.SignalAwaiter.SignalCallback(nint, Godot.NativeInterop.godot_variant**, int, Godot.NativeInterop.godot_bool*)

Image

@misha-cilantro
Copy link
Author

Mysterious! I'm on v4.3.stable.mono.official [77dcf97d8] according to the Help/About dialog.

Do you have any other local changes, either to code or to the script? Does this happen with a fresh download of the PR with nothing added but the SaliencySetter code?

@dogboydog
Copy link
Collaborator

The only other change I have is removing setting the saliency from SpaceSample since that would be redundant

Image

@misha-cilantro
Copy link
Author

Aww heck yeah that was it, I get the error now too! I have some time now so am gonna ⛏️

@dogboydog
Copy link
Collaborator

Thanks

@misha-cilantro
Copy link
Author

misha-cilantro commented Feb 18, 2025

Hmm okay some data: SpaceSample.cs, when passing the variable storage to the saliency object, uses dialogueRunner.VariableStorage. In our SaliencySetter.cs class we just used whatever was passed in. Even though I'm pretty sure we're giving it the same YarnSpinnerCanvasLayer/InMemoryVariableStorage object, if we change the SaliencySetter.cs code to ALSO use dialogueRunner.VariableStorage ... it works fine.

So this fixes the problem:

IContentSaliencyStrategy chosenStrategy = SaliencyStrategy switch
        {
            SaliencyType.First => new FirstSaliencyStrategy(),
            SaliencyType.Best => new BestSaliencyStrategy(),
            SaliencyType.BestLeastRecentlyViewed => new BestLeastRecentlyViewedSalienceStrategy(
                dialogueRunner.VariableStorage),
            SaliencyType.RandomBestLeastRecentlyViewed => new RandomBestLeastRecentlyViewedSalienceStrategy(
                dialogueRunner.VariableStorage),
            _ => throw new Exception("Unknown Saliency Strategy")
        };

Inspecting things, it doesn't look like the two objects are actually the same? One is a Godot Node, the other is just a VariableStorageBehaviour class. I'm still trying to understand the inheritance happening here and how godot's stuff mixes in there.

@misha-cilantro
Copy link
Author

Before anything happens, the exported variableStorage.Program is already null.

@dogboydog
Copy link
Collaborator

Huh that is odd and I don't know why the difference either but maybe the fix is just to use the same way of setting the variable storage on the saliency strategy

@misha-cilantro
Copy link
Author

Okay it's getting weirder. This ALSO fixes it:

IContentSaliencyStrategy chosenStrategy = SaliencyStrategy switch
        {
            SaliencyType.First => new FirstSaliencyStrategy(),
            SaliencyType.Best => new BestSaliencyStrategy(),
            SaliencyType.BestLeastRecentlyViewed => new BestLeastRecentlyViewedSalienceStrategy(
                variableStorageBehaviour),
            SaliencyType.RandomBestLeastRecentlyViewed => new RandomBestLeastRecentlyViewedSalienceStrategy(
                variableStorageBehaviour),
            _ => throw new Exception("Unknown Saliency Strategy")
        };
        
        GD.Print(variableStorageBehaviour);
        GD.Print(dialogueRunner.VariableStorage);

Just adding the GD.Print. wtf. race condition???

@misha-cilantro
Copy link
Author

I do think using the dialogue runner's variable storage feels really safe, bc there doesn't seem to be any reason to set different ones except that you like errors.

Does accessing the dialogue runner's variable storage have some kind of side effect to "set up" the storage?

@misha-cilantro
Copy link
Author

Oh it shore does

if (variableStorage == null)
            {
                var memoryStorage = new InMemoryVariableStorage();
                AddChild(memoryStorage);
                memoryStorage.Name = nameof(InMemoryVariableStorage);
                variableStorage = memoryStorage;
            }

            return variableStorage;

@dogboydog
Copy link
Collaborator

That should only make a difference if there was no variable storage set on the dialogue runner though . I'm not sure what is going on

@misha-cilantro
Copy link
Author

You're right, that code is skipped. Jumped the gun.

Here's a weird error:

Image

Idk what to make of that. Can the Node part of an object be disposed while the C# object remains?

@misha-cilantro
Copy link
Author

Using a basic InMemoryVariableStorage node works, too, which is probably why it works fine in my project. This is so bizarre.

I did some checks and it doesn't seem like a race condition despite having all the stink of one, so some kind of weird combination of order of operation and how Godot and C# inheritance interact? 🙃 I mean using dialogueRunner.VariableStorage seems safer and better and works, so maybe just do that... but I'll keep digging for a bit, it's frustrating not knowing why.

@misha-cilantro
Copy link
Author

Okay, wait, maybe my print statements are throwing an error and stopping execution of the broken variableStorage usage. Hmm hmm. I forget that Godot keeps running through errors.

@misha-cilantro
Copy link
Author

Sorry you're getting like a live feed of my debugging this >..<

@misha-cilantro
Copy link
Author

Okay, ignore all the GD.Print "fixes" -- those are the ramblings of a confused dummy. They throw errors, which breaks in the function but has the effect of just not setting saliency. But at least that makes things somewhat less mysterious.

@misha-cilantro
Copy link
Author

Okay. I think there's something borked with DialogueRunner.VariableStorage.

When DialogueRunner is instantiated, it assumes the presence of a VariableStorage:

    public Dialogue Dialogue
    {
        get
        {
            if (dialogue == null)
            {
                if (!IsInstanceValid(VariableStorage))
                {
                    GD.Print("NOT VALID MFER");
                }
                
                dialogue = new Yarn.Dialogue(VariableStorage);

But that GD.Print will fire, because actually VariableStorage is not valid at the time this is called. But the constructor looks like this:

    public Dialogue(IVariableStorage variableStorage)
    {
      this.Library = new Library();
      this.VariableStorage = variableStorage ?? throw new ArgumentNullException(nameof (variableStorage));

But I don't think ?? is valid here, because is IS an object, some kind of Godot object, that has no internal value yet but is NOT null.

So I think VariableStorage gets set to this weird empty-but-not-null object, which is not the same as the actual node we pass in successfully in SaliencySetter.

More evidence, in Player.cs:

if (colliderNode.HasNode(nameof(DialogueTarget)))
                    {
                        var target = colliderNode.GetNode<DialogueTarget>(nameof(DialogueTarget));
                        _dialoguePlaying = true;
                        if (!IsInstanceValid(DialogueRunner.VariableStorage))
                        {
                            GD.Print("FUCK SHIT");
                        }
                        DialogueRunner.StartDialogue(target.nodeName);
                        break;
                    }

That inappropriate GD.Print will fire every time. We don't actually have correct VariableStorage here, even though it looks like we do.

Final evidence, this single line of code in SaliencySetter.cs fixes the issues:

dialogueRunner.VariableStorage = variableStorageBehaviour;

Full ready function I'm testing:

public override void _Ready()
    {
        dialogueRunner.VariableStorage = variableStorageBehaviour; // THE MAGIC
        
        if (!IsInstanceValid(dialogueRunner))
        {
            GD.PushError($"{nameof(dialogueRunner)} is not set on {nameof(SaliencySetter)}! You must set it to use this script");
            return;
        }
        if (!IsInstanceValid(variableStorageBehaviour))
        {
            GD.PushError($"{nameof(variableStorageBehaviour)} is not set on {nameof(SaliencySetter)}! You must set it to use this script");
            return;
        }

        // var strat = new BestLeastRecentlyViewedSalienceStrategy(
        //     variableStorageBehaviour);
        var strat = new BestLeastRecentlyViewedSalienceStrategy(
            variableStorageBehaviour);

        dialogueRunner.Dialogue.ContentSaliencyStrategy = strat;
        
        if (!IsInstanceValid(dialogueRunner.VariableStorage))
        {
            GD.Print("OH LAWD"); // this line will NOT fire if the magic line is set
        }
    }

@misha-cilantro
Copy link
Author

So my theory is that the way DialogueRunner.cs tries to set its VariableStorage using the export is flawed and creates this strange zombie object that is a placeholder(?) for the export, but is not a reference to the exported value that will eventually be there. OR SOMETHING.

But I do think it's being set wrong, maybe before the node is ready to be looked at.

@misha-cilantro
Copy link
Author

I have reverted all my test changes and JUST added

dialogueRunner.VariableStorage = variableStorageBehaviour;

...and it works.

@dogboydog
Copy link
Collaborator

dogboydog commented Feb 19, 2025

Thank you for your help trying to investigate this. May I ask that you please try to wait before posting and consolidate your findings some so that I'm notified less often? No hard feelings !

I actually think this is some kind of race condition with multiple nodes running code in _Ready at the same time. Without the Saliency setter script , the variables storage is set properly. The checks you are talking about where variable storage is not null do not fire in normal usage when you run dialogue - if you have set a variable storage in the DialogueRunner's inspector, it works as intended. I think what is happening is the Saliency setter is trying to access the variable storage too early.

I wonder if the original code I posted would work if the last part, the switch statement and dialogueRunner.Dialogue.ContentSaliencyStrategy = chosenStrategy; were wrapped into a method that was called with CallDeferred . I also think we could just get rid of the need to export the variable storage and just use dialogueRunner.variableStorage as the second argument to the saliency strategy constructor

@misha-cilantro
Copy link
Author

Yes, heard chef. I did realize I was spamming and tried to step back, thus the longer post with better info :)

I think I have the fix. It is some kind of race condition, and it's specifically an issue with SaliencySetter being a regular child of the scene, while the DialogueRunner and InMemoryVariableStorage are part of a child scene. The breakthrough was when I used the "Make Local" button and suddenly the error went away.

So to resolve the issue even when not local, I added this code to SaliencySetter.cs:

dialogueRunner.Ready += () =>
        {
            dialogueRunner.Dialogue.ContentSaliencyStrategy = chosenStrategy;
        };

Here is the complete class:

using Godot;
using System;
using Yarn.Saliency;

namespace YarnSpinnerGodot;

/// <summary>
/// Lets you set the saliency strategy of the chosen DialogueRunner.
/// Saliency Strategy determines how group nodes are chosen.
///
/// Note: Does not support custom saliency strategies. For that use case you would want your own script
/// that implements and sets your custom strategy. 
/// </summary>
[GlobalClass]
public partial class SaliencySetter : Node
{
    public enum SaliencyType
    {
        First,
        Best,
        BestLeastRecentlyViewed,
        RandomBestLeastRecentlyViewed
    }

    [Export] public DialogueRunner dialogueRunner;

    [Export] public VariableStorageBehaviour variableStorageBehaviour;

    [Export] public SaliencyType SaliencyStrategy = SaliencyType.First;

    public override void _Ready()
    {
        GD.Print("SaliencySetter ready");
        
        if (!IsInstanceValid(dialogueRunner))
        {
            GD.PushError($"{nameof(dialogueRunner)} is not set on {nameof(SaliencySetter)}! You must set it to use this script");
            return;
        }
        if (!IsInstanceValid(variableStorageBehaviour))
        {
            GD.PushError($"{nameof(variableStorageBehaviour)} is not set on {nameof(SaliencySetter)}! You must set it to use this script");
            return;
        }
        IContentSaliencyStrategy chosenStrategy = SaliencyStrategy switch
        {
            SaliencyType.First => new FirstSaliencyStrategy(),
            SaliencyType.Best => new BestSaliencyStrategy(),
            SaliencyType.BestLeastRecentlyViewed => new BestLeastRecentlyViewedSalienceStrategy(
                variableStorageBehaviour),
            SaliencyType.RandomBestLeastRecentlyViewed => new RandomBestLeastRecentlyViewedSalienceStrategy(
                variableStorageBehaviour),
            _ => throw new Exception("Unknown Saliency Strategy")
        };

        dialogueRunner.Ready += () =>
        {
            dialogueRunner.Dialogue.ContentSaliencyStrategy = chosenStrategy;
        };
    }
}

In other findings, even without a SaliencySetter this error from Player.cs will print:

if (colliderNode.HasNode(nameof(DialogueTarget)))
                    {
                        var target = colliderNode.GetNode<DialogueTarget>(nameof(DialogueTarget));
                        _dialoguePlaying = true;
                        if (!IsInstanceValid(DialogueRunner.VariableStorage))
                        {
                            GD.PrintErr("~~~ Dialogue variable storage is invalid");
                        }
                        DialogueRunner.StartDialogue(target.nodeName);
                        break;
                    }

I have no idea what this means. Maybe it means nothing. I just find it strange.

@dogboydog
Copy link
Collaborator

Thank you! I'll use your updates and credit you in the next release and also take a look into the Player.cs situation

@misha-cilantro
Copy link
Author

Yeah sorry I'm such a sloppy hot mess on the way but I get there eventually.

@dogboydog
Copy link
Collaborator

Thanks again, now available in #75

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants