/* Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. */ using System; using System.Collections.Generic; using System.Linq; using System.Threading; using UnityEngine; using UnityEngine.Events; using UnityEngine.UIElements; using Yarn.Unity.Attributes; #nullable enable namespace Yarn.Unity { /// /// A Line Cancellation Token stores information about whether a dialogue /// presenter should stop its delivery. /// /// /// Dialogue presenters receive Line Cancellation Tokens as a parameter to /// . Line Cancellation /// Tokens indicate whether the user has requested that the line's delivery /// should be hurried up, and whether the dialogue presenter should stop showing /// the current line. /// public struct LineCancellationToken { /// /// A that becomes cancelled when a wishes all dialogue presenters to stop running /// the current line. For example, on-screen UI should be dismissed, and /// any ongoing audio playback should be stopped. /// [System.Obsolete("Please use NextContentToken instead")] public readonly CancellationToken NextLineToken => NextContentToken; public CancellationToken NextContentToken; // this token will ALWAYS be a dependant token on the above /// /// A that becomes cancelled when a wishes all dialogue presenters to speed up their /// delivery of their line, if appropriate. For example, UI animations /// should be played faster or skipped. /// /// This token is linked to : if the /// next line token is cancelled, then this token will become cancelled /// as well. public CancellationToken HurryUpToken; /// /// Gets a value indicating whether the dialogue runner has requested /// that the next line be shown. /// /// /// /// If this value is , dialogue presenters should /// presenting the current line, so that the next piece of content can /// be shown to the user. /// /// /// If this property is , then will also be true. /// [System.Obsolete("Please use IsNextContentRequested instead")] public readonly bool IsNextLineRequested => NextLineToken.IsCancellationRequested; public readonly bool IsNextContentRequested => NextContentToken.IsCancellationRequested; /// /// Gets a value indicating whether the user has requested that the line /// be hurried up. /// /// If this value is , Dialogue /// presenters should speed up any ongoing delivery of the line, such as /// on-screen animations, but are not required to finish delivering the /// line entirely (that is, UI elements may remain on screen). /// If is , then this property will also be . /// /// public readonly bool IsHurryUpRequested => HurryUpToken.IsCancellationRequested; } /// /// A that takes a single /// parameter. /// [System.Serializable] public class UnityEventString : UnityEvent { } [HelpURL("https://docs.yarnspinner.dev/using-yarnspinner-with-unity/components/dialogue-runner")] public sealed partial class DialogueRunner : MonoBehaviour { private Dialogue? dialogue; /// /// Gets the internal object that reads and /// executes the Yarn script. /// public Dialogue Dialogue { get { if (dialogue == null) { dialogue = new Yarn.Dialogue(VariableStorage); dialogue.LineHandler = OnLineReceived; dialogue.OptionsHandler = OnOptionsReceived; dialogue.CommandHandler = OnCommandReceived; dialogue.NodeStartHandler = OnNodeStarted; dialogue.NodeCompleteHandler = OnNodeCompleted; dialogue.DialogueCompleteHandler = OnDialogueCompleted; dialogue.PrepareForLinesHandler = OnPrepareForLines; dialogue.LogDebugMessage = delegate (string message) { if (verboseLogging) { Debug.Log(message, this); } }; dialogue.LogErrorMessage = delegate (string message) { Debug.LogError(message, this); }; if (yarnProject != null) { Dialogue.SetProgram(yarnProject.Program); } } return dialogue; } } enum SaliencyStrategy { RandomBestLeastRecentlyViewed, FirstBestLeastRecentlyViewed, Best, First, Custom } /// /// The Yarn Project containing the nodes that this Dialogue Runner /// runs. /// [SerializeField] internal YarnProject? yarnProject; /// /// The object that manages the Yarn variables used by this Dialogue Runner. /// [SerializeField] internal VariableStorageBehaviour? variableStorage; /// /// Gets the asset that this dialogue runner uses. /// /// public YarnProject? YarnProject => yarnProject; /// /// Gets the VariableStorage that this dialogue runner uses to store and /// access Yarn variables. /// public VariableStorageBehaviour VariableStorage { get { // If we don't have a variable storage, create an in // InMemoryVariableStorage and use that. if (variableStorage == null) { if (verboseLogging) { Debug.Log($"Dialogue Runner has no Variable Storage; creating a {nameof(InMemoryVariableStorage)}", this); } this.variableStorage = gameObject.AddComponent(); } if (this.variableStorage.Program == null && this.YarnProject != null) { this.variableStorage.Program = this.YarnProject.Program; } return variableStorage; } set => variableStorage = value; } [SerializeReference] internal LineProviderBehaviour? lineProvider; [SerializeField] private SaliencyStrategy saliencyStrategy = SaliencyStrategy.RandomBestLeastRecentlyViewed; /// /// Gets the that this dialogue runner uses /// to fetch localized line content. /// public ILineProvider LineProvider { get { if (lineProvider == null) { // No line provider was created. We'll need to create one. if (yarnProject != null && yarnProject.localizationType == LocalizationType.Unity) { // The Yarn Project uses Unity Localisation; without an // appropriately configured line provider, we can't show // any lines. Debug.LogWarning($"Yarn Project {yarnProject.name} uses Unity Localization, but the " + $"Dialogue Runner \"{this.name}\" isn't set up to use a {nameof(Yarn.Unity.UnityLocalization.UnityLocalisedLineProvider)}. " + $"Line text and assets will not be available.", this); } if (verboseLogging) { Debug.Log($"Dialogue Runner has no LineProvider; creating a {nameof(BuiltinLocalisedLineProvider)}.", this); } lineProvider = gameObject.AddComponent(); lineProvider.YarnProject = yarnProject; } return lineProvider; } } /// /// The list of dialogue presenters that the dialogue runner delivers content /// to. /// [Space] [UnityEngine.Serialization.FormerlySerializedAs("dialogueViews")] [SerializeField] List dialoguePresenters = new List(); /// /// If true, will print Debug.Log messages every time it enters a /// node, and other frequent events. /// [Tooltip("If true, will print Debug.Log messages every time it enters a node, and other frequent events")] public bool verboseLogging = false; /// /// Gets a value that indicates if the dialogue is actively /// running. /// public bool IsDialogueRunning => Dialogue.IsActive; /// /// Whether the dialogue runner will immediately start running dialogue /// after loading. /// [Group("Behaviour")] [Label("Start Automatically")] public bool autoStart = false; /// /// The name of the node that will start running immediately after /// loading. /// /// This value must be the name of a node present in . /// /// [Group("Behaviour")] [ShowIf(nameof(autoStart))] [Indent(1)] [YarnNode(nameof(yarnProject))] public string startNode = "Start"; /// /// If this value is set, when an option is selected, the line contained /// in it () will be delivered to the /// dialogue runner's dialogue presenters as though it had been written as a /// separate line. /// /// /// This allows a Yarn script to /// [Group("Behaviour")] public bool runSelectedOptionAsLine = false; [SerializeField] private bool allowOptionFallthrough = true; /// /// A Unity event that is called when a node starts running. /// /// /// This event receives as a parameter the name of the node that is /// about to start running. /// /// [Group("Events", foldOut: true)] public UnityEventString? onNodeStart; /// /// A Unity event that is called when a node is complete. /// /// /// This event receives as a parameter the name of the node that /// just finished running. /// /// [Group("Events", foldOut: true)] public UnityEventString? onNodeComplete; /// /// A Unity event that is called when the dialogue starts running. /// [Group("Events", foldOut: true)] public UnityEvent? onDialogueStart; /// /// A Unity event that is called once the dialogue has completed. /// /// [Group("Events", foldOut: true)] public UnityEvent? onDialogueComplete; /// /// A that is called when a is received and no command handler was able to /// handle it. /// /// /// /// Use this method to dispatch a command to other parts of your game. /// This method is only called if the has not been /// handled by a command handler that has been added to the , or by a method on a in the scene with the attribute . /// /// /// When a command is delivered in this way, the will not pause execution. If you want a /// command to make the DialogueRunner pause execution, see . /// /// /// This method receives the full text of the command, as it appears /// between the << and >> markers. /// /// /// /// [Group("Events", foldOut: true)] [UnityEngine.Serialization.FormerlySerializedAs("onCommand")] public UnityEventString? onUnhandledCommand; /// /// Gets or sets the collection of dialogue presenters attached to this /// dialogue runner. /// /// This property is deprecated. Use instead. [Obsolete("Use " + nameof(DialoguePresenters))] public IEnumerable DialogueViews { get => DialoguePresenters; set => DialoguePresenters = value; } /// /// Gets or sets the collection of dialogue presenters attached to this /// dialogue runner. /// public IEnumerable DialoguePresenters { get => dialoguePresenters; set => dialoguePresenters = value.ToList(); } /// /// Gets a completed that /// contains a value. /// /// Dialogue presenters can return this value from their public static YarnTask NoOptionSelected { get { return YarnTask.FromResult(null); } } private CancellationTokenSource? dialogueCancellationSource; private CancellationTokenSource? currentLineCancellationSource; private CancellationTokenSource? currentLineHurryUpSource; private CancellationTokenSource? currentOptionsHurryUpSource; private CancellationTokenSource? currentOptionsCancellationSource; private YarnTaskCompletionSource? dialogueCompletionSource; private YarnTaskCompletionSource? dialogueCancellationCompletion; internal ICommandDispatcher CommandDispatcher { get { EnsureCommandDispatcherReady(); if (_commandDispatcher != null) { return _commandDispatcher; } else { throw new InvalidOperationException($"{nameof(EnsureCommandDispatcherReady)} failed to set up command dispatcher"); } } } private void EnsureCommandDispatcherReady() { if (_commandDispatcher == null) { var actions = new Actions(this, Dialogue.Library); _commandDispatcher = actions; actions.RegisterActions(); } } private ICommandDispatcher? _commandDispatcher; /// /// Called by Unity to set up the object. /// private void Awake() { if (this.VariableStorage != null && this.YarnProject != null) { this.VariableStorage.Program = this.YarnProject.Program; } if (this.LineProvider != null && this.YarnProject != null) { this.LineProvider.YarnProject = this.YarnProject; } } /// /// Sets the saliency strategy based on the value set inside of the Inspector. /// This is called when StartDialogue is run. /// This does no checking and will obliterate any custom strategies if one has been set and you change the value. /// private void ApplySaliencyStrategy() { // if we don't have any dialogue then we can't apply a saliency strategy if (this.dialogue == null) { return; } // likewise for variable storage if (this.VariableStorage == null) { return; } switch (this.saliencyStrategy) { case SaliencyStrategy.RandomBestLeastRecentlyViewed: this.dialogue.ContentSaliencyStrategy = new Saliency.RandomBestLeastRecentlyViewedSaliencyStrategy(this.VariableStorage); return; case SaliencyStrategy.FirstBestLeastRecentlyViewed: this.dialogue.ContentSaliencyStrategy = new Yarn.Saliency.BestLeastRecentlyViewedSaliencyStrategy(this.VariableStorage); return; case SaliencyStrategy.Best: this.dialogue.ContentSaliencyStrategy = new Yarn.Saliency.BestSaliencyStrategy(); return; case SaliencyStrategy.First: this.dialogue.ContentSaliencyStrategy = new Yarn.Saliency.FirstSaliencyStrategy(); return; } } /// /// Called by Unity to start running dialogue if /// is enabled. /// private async void Start() { if (autoStart) { try { // there are numerous situations where kicking off dialogue immediately from Start causes annoying issues around timing of different game objects // while these can all be fixed it it much easier in our case to just wait one frame before starting. // This still has the same feel of the older start automatically but just simplifies so many things. // For situations where you absolutely must start immediately call StartDialogue yourself. await YarnTask.Yield(); await StartDialogue(startNode); } catch (System.OperationCanceledException ex) { Debug.LogException(ex); } } } /// /// Stops the dialogue immediately, and cancels any currently running /// dialogue presenters. /// public async YarnTask Stop() { dialogueCancellationCompletion = new YarnTaskCompletionSource(); CancelDialogue(); await dialogueCancellationCompletion.Task; dialogueCancellationCompletion = null; } /// /// Called by Unity to cancel the current dialogue when the Dialogue /// Runner is destroyed. /// private void OnDestroy() { CancelDialogue(); } /// /// Gets a that completes when the dialogue /// runner finishes its dialogue. /// /// /// If the dialogue is not currently running when this property is /// accessed, the property returns a task that is already complete. /// public YarnTask DialogueTask { get { if (dialogueCompletionSource == null) { return YarnTask.CompletedTask; } return dialogueCompletionSource.Task; } } private void CancelDialogue() { if (dialogueCancellationSource == null || Dialogue.IsActive == false) { // We're not running dialogue. There's nothing to cancel. return; } // Cancel the current line, if any. currentLineCancellationSource?.Cancel(); // Cancel the entire dialogue. dialogueCancellationSource?.Cancel(); // Stop the dialogue. This will cause OnDialogueCompleted to be called. Dialogue.Stop(); } private void OnPrepareForLines(IEnumerable lineIDs) { this.LineProvider.PrepareForLinesAsync(lineIDs, CancellationToken.None).Forget(); } private void OnDialogueCompleted() { OnDialogueCompleteAsync().Forget(); } private async YarnTask OnDialogueCompleteAsync() { // cleaning up the old cancellation token currentLineCancellationSource?.Dispose(); currentLineCancellationSource = null; currentLineHurryUpSource?.Dispose(); currentLineHurryUpSource = null; var pendingTasks = new HashSet(); foreach (var view in this.dialoguePresenters) { if (view == null) { // The view doesn't exist. Skip it. continue; } // Tell all of our views that the dialogue has finished async YarnTask RunCompletion() { try { await view.OnDialogueCompleteAsync(); } catch (System.Exception e) { Debug.LogException(e, view); } } YarnTask task = RunCompletion(); pendingTasks.Add(task); } // Wait for all views to finish doing their clean up await YarnTask.WhenAll(pendingTasks); // Finally, notify that dialogue is complete and tidy up. dialogueCompletionSource?.TrySetResult(); onDialogueComplete?.Invoke(); dialogueCancellationSource?.Dispose(); dialogueCancellationSource = null; dialogueCompletionSource = null; // finally we flag the cancellation as done // this lets stop know that all views have been informed as to the cancellation dialogueCancellationCompletion?.TrySetResult(); } private void OnNodeCompleted(string completedNodeName) { onNodeComplete?.Invoke(completedNodeName); foreach (var presenter in dialoguePresenters) { if (presenter == null) { continue; } if (presenter.enabled == false) { continue; } presenter.OnNodeExit(completedNodeName); } } private void OnNodeStarted(string startedNodeName) { onNodeStart?.Invoke(startedNodeName); foreach (var presenter in dialoguePresenters) { if (presenter == null) { continue; } if (presenter.enabled == false) { continue; } presenter.OnNodeEnter(startedNodeName); } } private void OnCommandReceived(Command command) { OnCommandReceivedAsync(command).Forget(); } private async YarnTask OnCommandReceivedAsync(Command command) { CommandDispatchResult dispatchResult = this.CommandDispatcher.DispatchCommand(command.Text, this); var parts = SplitCommandText(command.Text); string commandName = parts.ElementAtOrDefault(0); switch (dispatchResult.Status) { case CommandDispatchResult.StatusType.Succeeded: if (dispatchResult.Task.IsCompletedSuccessfully()) { // The command was dispatched and completed // synchronously. Dialogue.SignalContentComplete(); } else { // The command has successfully dispatched, but has not // yet finished running. Wait for it to finish. await dispatchResult.Task; } break; case CommandDispatchResult.StatusType.NoTargetFound: Debug.LogError($"Can't call command <<{command.Text}>>: failed to find a game object named {parts.ElementAtOrDefault(1)}", this); break; case CommandDispatchResult.StatusType.TargetMissingComponent: Debug.LogError($"Can't call command <<{command.Text}>>, because {parts.ElementAtOrDefault(1)} doesn't have the correct component"); break; case CommandDispatchResult.StatusType.InvalidParameterCount: Debug.LogError($"Can't call command <<{command.Text}>>: {dispatchResult.Message ?? "incorrect number of parameters"}"); break; case CommandDispatchResult.StatusType.CommandUnknown: // Attempt a last-ditch dispatch by invoking our 'onCommand' // Unity Event. if (onUnhandledCommand != null && onUnhandledCommand.GetPersistentEventCount() > 0) { // We can invoke the event! onUnhandledCommand.Invoke(command.Text); } else { // We're out of ways to handle this command! Log this as an // error. Debug.LogError($"No Command \"{commandName}\" was found. Did you remember to use the YarnCommand attribute or AddCommandHandler() function in C#?"); } return; default: throw new ArgumentOutOfRangeException($"Internal error: Unknown command dispatch result status {dispatchResult}"); } // Continue the Dialogue, unless dialogue cancellation was requested. if (dialogueCancellationSource?.IsCancellationRequested ?? false) { return; } Dialogue.Continue(); } private void OnLineReceived(Line line) { OnLineReceivedAsync(line).Forget(); } private async YarnTask OnLineReceivedAsync(Line line) { var localisedLine = await LineProvider.GetLocalizedLineAsync(line, dialogueCancellationSource?.Token ?? CancellationToken.None); localisedLine.Source = this; if (localisedLine == LocalizedLine.InvalidLine) { Debug.LogError($"Failed to get a localised line for {line.ID}!"); } await RunLocalisedLine(localisedLine); if (dialogueCancellationSource?.IsCancellationRequested == false) { Dialogue.Continue(); } } /// /// Runs a localised line on all dialogue presenters. /// /// /// This method can be called from two places: 1. when a line is being run, /// and 2. when an option has been selected and is . /// /// /// private async YarnTask RunLocalisedLine(LocalizedLine localisedLine) { // Create a new cancellation source for this line, linked to the // dialogue cancellation (if we have one). Dispose of the previous one, // if we have it. currentLineCancellationSource?.Dispose(); currentLineHurryUpSource?.Dispose(); if (dialogueCancellationSource != null) { currentLineCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(dialogueCancellationSource.Token); } else { currentLineCancellationSource = new CancellationTokenSource(); } // now we make a new dependant hurry up cancellation token currentLineHurryUpSource = CancellationTokenSource.CreateLinkedTokenSource(currentLineCancellationSource.Token); var metaToken = new LineCancellationToken { NextContentToken = currentLineCancellationSource.Token, HurryUpToken = currentLineHurryUpSource.Token, }; var pendingTasks = new HashSet(); foreach (var view in this.dialoguePresenters) { if (view == null) { // The view doesn't exist. Skip it. continue; } if (view.enabled == false) { // The view is not enabled. Skip it. continue; } // Tell all of our views to run this line, and give them a // cancellation token they can use to interrupt the line if needed. async YarnTask RunLineAndInvokeCompletion(DialoguePresenterBase view, LocalizedLine line, LineCancellationToken token) { try { // Run the line and wait for it to finish await view.RunLineAsync(localisedLine, token); } catch (System.OperationCanceledException) { // The line presenter cancelled (rather than returning.) // This probably wasn't intended - they should clean up // and return null. Debug.LogWarning($"Dialogue presenter {view.name} threw an {nameof(System.OperationCanceledException)} when running its {nameof(DialoguePresenterBase.RunLineAsync)} method. Dialogue presenters should not throw this exception; instead, clean up any needed user-facing content, and return.", view); } catch (System.Exception e) { // If a dialogue presenter throws an exception, we need // to return, because the dialogue runner is waiting for // our task to complete. We'll log the exception so that // it's not lost, and exit here. Debug.LogException(e, view); } } YarnTask task = RunLineAndInvokeCompletion(view, localisedLine, metaToken); pendingTasks.Add(task); } // Wait for all line view tasks to finish delivering the line. var waitForAllLines = YarnTask.WhenAll(pendingTasks); if (waitForAllLines.IsCompletedSuccessfully()) { // All lines completed synchronously. Signal that we're done. Dialogue.SignalContentComplete(); } else { await waitForAllLines; } // We're done; dispose of the cancellation sources. (Null-check them because if we're leaving play mode, then these references may no longer be valid.) currentLineCancellationSource?.Dispose(); currentLineCancellationSource = null; currentLineHurryUpSource?.Dispose(); currentLineHurryUpSource = null; } private void OnOptionsReceived(OptionSet options) { OnOptionsReceivedAsync(options).Forget(); } private async YarnTask OnOptionsReceivedAsync(OptionSet options) { // if we have an existing cancellation and hurry up source we want to clean those up first. currentOptionsCancellationSource?.Dispose(); currentOptionsHurryUpSource?.Dispose(); // Create a cancellation source that represents 'we don't need you to // select an option anymore'. Link it to the dialogue cancellation // source, so that if dialogue gets cancelled, all options get // cancelled. if (dialogueCancellationSource != null) { currentOptionsCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(dialogueCancellationSource.Token); } else { currentOptionsCancellationSource = new CancellationTokenSource(); } // now we make a new dependant hurry up cancellation token currentOptionsHurryUpSource = CancellationTokenSource.CreateLinkedTokenSource(currentOptionsCancellationSource.Token); var metaToken = new LineCancellationToken { NextContentToken = currentOptionsCancellationSource.Token, HurryUpToken = currentOptionsHurryUpSource.Token, }; DialogueOption[] localisedOptions = new DialogueOption[options.Options.Length]; for (int i = 0; i < options.Options.Length; i++) { var opt = options.Options[i]; LocalizedLine localizedLine = await LineProvider.GetLocalizedLineAsync(opt.Line, currentOptionsCancellationSource.Token); localizedLine.Source = this; if (localizedLine == LocalizedLine.InvalidLine) { Debug.LogError($"Failed to get a localised line for line {opt.Line.ID} (option {i + 1})!"); } localisedOptions[i] = new DialogueOption { DialogueOptionID = opt.ID, IsAvailable = opt.IsAvailable, Line = localizedLine, TextID = opt.Line.ID, }; } DialogueOption? selectedOption = null; async YarnTask WaitForOptionsView(DialoguePresenterBase? view) { if (view == null) { return; } try { var result = await view.RunOptionsAsync(localisedOptions, metaToken); if (result != null) { // We no longer need the other views, so tell them to stop // by cancelling the option selection. currentOptionsCancellationSource.Cancel(); selectedOption = result; } } catch (System.OperationCanceledException) { // The options presenter cancelled (rather than returning // null.) This probably wasn't intended - they should clean // up and return null. Debug.LogWarning($"Dialogue presenter {view.name} threw an {nameof(System.OperationCanceledException)} when running its {nameof(DialoguePresenterBase.RunOptionsAsync)} method. Dialogue presenters should not throw this exception; instead, clean up any needed user-facing content, and return null.", view); } catch (System.Exception ex) { // If a dialogue presenter throws an exception, we still // need to return a value, because the dialogue runner is // waiting for our task to complete. We'll log the exception // so that it's not lost, and exit here. Debug.LogException(ex, view); return; } } var pendingTasks = new List(); foreach (var view in this.dialoguePresenters) { pendingTasks.Add(WaitForOptionsView(view)); } await YarnTask.WhenAll(pendingTasks); // at this point now every view has finished their handling of the options // the first one to return a non-null value will be the one that is chosen option // or if everyone returned null that's an error currentOptionsCancellationSource.Dispose(); if (dialogueCancellationSource?.IsCancellationRequested ?? false) { // We received a request to cancel dialogue while waiting for a // choice. Stop here, and do not provide it to the Dialogue. return; } else if (selectedOption == null) { if (allowOptionFallthrough) { Dialogue.SetSelectedOption(Dialogue.NoOptionSelected); } else { // None of our option views returned an option, and our dialogue wasn't cancelled, and we've said we don't want to do fallthrough. // That's not allowed, because we don't know what to do next! Debug.LogError($"All presenters have returned from {nameof(DialoguePresenterBase.RunOptionsAsync)} but none returned an option, and fallthrough is disabled. This is not allowed."); return; } } else { Dialogue.SetSelectedOption(selectedOption.DialogueOptionID); if (runSelectedOptionAsLine) { // Run the selected option's line content as though we had received // it as a line. await RunLocalisedLine(selectedOption.Line); } } if (dialogueCancellationSource?.IsCancellationRequested ?? false) { // Our dialogue has been cancelled. Don't continue the dialogue. return; } else { // Proceed to the next piece of dialogue content. Dialogue.Continue(); } } /// /// Sets the dialogue runner's Yarn Project. /// /// /// If the dialogue runner is currently running (that is, is ), an is thrown. /// /// The new to be /// used. /// Thrown when attempting /// to set a new project while a dialogue is currently /// running. public void SetProject(YarnProject project) { if (this.IsDialogueRunning) { // Can't change project if we're already running. throw new InvalidOperationException("Can't set project, because dialogue is currently running."); } this.yarnProject = project; Dialogue.SetProgram(project.Program); } /// /// Starts running a node of dialogue. /// /// must be the name of a node in /// . /// The name of the node to run. public async YarnTask StartDialogue(string nodeName) { if (yarnProject == null) { Debug.LogError($"Can't start dialogue: no Yarn Project has been configured.", this); return; } if (yarnProject.Program == null) { // The Yarn Project asset reference is valid, but it doesn't // have a program, likely due to a compiler error. Debug.LogError($"Can't start dialogue: Yarn Project doesn't contain a valid program (possibly due to errors in the Yarn scripts?)", this); return; } dialogueCancellationSource?.Dispose(); dialogueCompletionSource = new YarnTaskCompletionSource(); dialogueCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(this.destroyCancellationToken); LineProvider.YarnProject = yarnProject; EnsureCommandDispatcherReady(); Dialogue.SetProgram(yarnProject.Program); Dialogue.SetNode(nodeName); ApplySaliencyStrategy(); onDialogueStart?.Invoke(); var tasks = new List(); foreach (var view in DialoguePresenters) { if (view == null) { continue; } tasks.Add(view.OnDialogueStartedAsync()); } await YarnTask.WhenAll(tasks); Dialogue.Continue(); } /// /// Requests that all dialogue presenters stop showing the current line, and /// prepare to show the next piece of content. /// /// /// /// The specific behaviour of what happens when this method is called /// depends on the implementation of the Dialogue Runner's current /// dialogue presenters. /// /// /// If the dialogue runner is not currently running a line (for example, /// if it is running options, or is not running dialogue at all), this /// method has no effect. /// /// public void RequestNextLine() { if (currentLineCancellationSource == null) { // We aren't running a line, so there's nothing to cancel. return; } // Cancel the current line. All currently pending tasks, which received // a CancellationToken, will be able to respond to the request to // cancel. currentLineCancellationSource.Cancel(); } /// /// Requests that all dialogue presenters speed up their delivery of the /// current line. /// /// /// /// The specific behaviour of what happens when this method is called /// depends on the implementation of the Dialogue Runner's current /// dialogue presenters. /// /// /// If the dialogue runner is not currently running a line (for example, /// if it is running options, or is not running dialogue at all), this /// method has no effect. /// /// public void RequestHurryUpLine() { if (currentLineCancellationSource == null) { // We aren't running a line, so there's nothing to cancel. return; } if (currentLineHurryUpSource == null) { // we are running a line but don't have a hurry up token // is this a bug..? return; } currentLineHurryUpSource.Cancel(); } public void RequestHurryUpOption() { if (currentOptionsCancellationSource == null) { return; } if (currentOptionsHurryUpSource == null) { return; } currentOptionsHurryUpSource.Cancel(); } } }