using EasyTalk.Display; using EasyTalk.Localization; using EasyTalk.Nodes; using EasyTalk.Nodes.Common; using EasyTalk.Nodes.Core; using EasyTalk.Nodes.Flow; using EasyTalk.Nodes.Tags; using EasyTalk.Nodes.Variable; using EasyTalk.Settings; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using UnityEngine; namespace EasyTalk.Controller { /// /// This class is used to process a collection of linked nodes, such as a dialogue and handles all of the logic for moving from one node to the next, evaluating /// variable values, and sending messages (via callbacks) about what is happening in a dialogue as it is processed. /// public class NodeHandler { /// /// The dialogue to process. /// private Dialogue dialogue; /// /// The Dialogue Listener to use for callbacks as events happen during dialogue processing. /// private DialogueListener listener; /// /// A mapping of node input IDs to the nodes which those inputs belong to. /// protected Dictionary inputIdToNodeMap = new Dictionary(); /// /// A mapping of node output IDs to the nodes which those inputs belong to. /// protected Dictionary outputIdToNodeMap = new Dictionary(); /// /// A mapping of jump keys/IDs to the 'jump in' nodes they belong to. /// protected Dictionary jumpMap = new Dictionary(); /// /// A mapping of variable names to their definitions and values. /// protected Dictionary variables = new Dictionary(); /// /// A mapping of global variable names to their definitions and values. /// protected static Dictionary globalVariables = new Dictionary(); /// /// A mapping of entry IDs to the 'entry' nodes they belong to. /// protected Dictionary entryMap = new Dictionary(); /// /// The default 'entry' node which is used if an entry ID is not provided, or the 'entry' node with the ID specified can't be found. /// protected Node entryNode; /// /// The current node being processed/displayed. /// protected Node currentNode; /// /// The line index of the conversation when a 'conversation' node is being processed. /// protected int convoIdx = 0; /// /// The GameObject this NodeHandler is being used by. /// protected GameObject owner = null; /// /// Keeps track of whether the node handler is currently processing and waiting for an asynchronous node to finish. /// private bool isProcessingAsyncNode = false; /// /// Keeps track of whether the node handler waiting for an output value to be determined for the current node. /// private bool isWaitingForNodeValueDetermination = false; /// /// A Dictionary which maps node and output IDs to the values output by the respective components. /// private Dictionary nodeValues = new Dictionary(); /// /// The Dialogue Settings currently in use. /// private EasyTalkDialogueSettings dialogueSettings = null; /// /// A List of Tasks /// private List tasks = new List(); /// /// Creates a new NodeHandler. /// /// The GameObject which the NodeHandler is associated with. public NodeHandler(GameObject owner) { this.owner = owner; } /// /// Called by Dialogue Controllers to run queued up Tasks. /// public void Update() { for (int i = 0; i < tasks.Count; i++) { Task task = tasks[i]; tasks.RemoveAt(0); task.RunSynchronously(); i--; } } /// /// Adds a task to the current queue. /// /// The Task to add to the queue. public void AddTask(Task task) { tasks.Add(task); } /*public void PromptFinished() { Task promptFinishedTask = new Task(() => { listener.OnAIPromptFinished(); }); tasks.Add(promptFinishedTask); }*/ /// /// Creates a Task to call AsyncExecutionCompleted on the main thread. /// /// The AsyncNode which has completed execution. public void ExecutionCompleted(AsyncNode asyncNode) { Task executionCompletedTask = new Task(() => { AsyncExecutionCompleted(asyncNode); }); tasks.Add(executionCompletedTask); } /// /// Queues up a Task to call OnDisplayLine on the node handler's Dialogue Listener. Normally, this will cause a line of dialogue to be displayed. /// /// The line of dialogue to queue up. public void DisplayLine(ConversationLine line) { Task displayLineTask = new Task(() => { listener.OnDisplayLine(line); }); tasks.Add(displayLineTask); } public void HandleAppendNode() { ConversationLine line = GetConversationLine(); if (listener != null) { listener.OnAppendText(line.Text); } DisplayLine(line); } /// /// Initializes the NodeHandler by setting the dialogue, clearing various mappings, determining the default 'entry' node, and creating variables. /// /// The Dialogue to use. public void Initialize(Dialogue dialogue) { this.dialogue = dialogue; inputIdToNodeMap.Clear(); outputIdToNodeMap.Clear(); jumpMap.Clear(); variables.Clear(); entryNode = FindEntryNode(); foreach (Node node in dialogue.Nodes) { //Add each node to a map for each of its inputs so that when jumping from an output to a node, it can quickly be found via an input id. foreach (NodeConnection conn in node.Inputs) { inputIdToNodeMap.Add(conn.ID, node); } //Add each node to a map for each of its outputs so that when jumping from an input to a node, it can quickly be found via an output id. foreach (NodeConnection conn in node.Outputs) { outputIdToNodeMap.Add(conn.ID, node); } if (node.NodeType == NodeType.JUMPIN) { //Add jump nodes to jump map so whenever a jumpout is moved to, the jumped to node can quickly be found. jumpMap.Add(((JumpInNode)node).Key, node); } else if (node.NodeType == NodeType.BOOL_VARIABLE) { CreateBoolVariable(node); } else if (node.NodeType == NodeType.INT_VARIABLE) { CreateIntVariable(node); } else if (node.NodeType == NodeType.FLOAT_VARIABLE) { CreateFloatVariable(node); } else if (node.NodeType == NodeType.STRING_VARIABLE) { CreateStringVariable(node); } else if (node.NodeType == NodeType.ENTRY) { EntryNode entryNode = node as EntryNode; if (entryNode.EntryPointName != null && entryNode.EntryPointName.Length > 0) { entryMap.TryAdd(entryNode.EntryPointName, entryNode); } } } } /// /// Forces an exit to be triggered immediately on the Dialogue. /// public void ForceExit() { listener.OnDialogueExited(null); currentNode = null; } /// /// Jumps to the specified node after a certain amount of time. /// /// The delay, in seconds. /// The node to jump to. /// public IEnumerator JumpToNodeAfterDelay(float delay, Node node) { yield return new WaitForSeconds(delay); currentNode = node; ProcessNode(currentNode); } /// /// Chooses the option, and thus continues down the corresponding path, of the option specified. /// /// The option index to choose. This should be the index of the option as it was originally in the 'option' node, which may differ from /// the index of the option as it occurs in the List of options displayed to the player. It is recommended to use DialogueOption.OptionIndex for reliability. public void ChooseOption(int optionIdx) { if (!(currentNode is OptionNode)) { return; } NodeConnection outputConnection = currentNode.Outputs[optionIdx]; if (outputConnection.AttachedIDs.Count > 0) { int attachedId = outputConnection.AttachedIDs[0]; currentNode = inputIdToNodeMap[attachedId]; ProcessNode(currentNode); } else { listener.OnDialogueExited(null); } } /// /// Processes the node specified according to its type. /// /// The node to process. public void ProcessNode(Node node) { if (node == null) { listener.OnDialogueExited(null); return; } listener.OnNodeChanged(node); switch (node.NodeType) { case NodeType.CONVO: HandleConversation(); break; case NodeType.OPTION: HandleOptionNode(); break; case NodeType.JUMPOUT: HandleJump(); break; case NodeType.EXIT: HandleExit(); break; case NodeType.STORY: HandleStoryNode(); break; case NodeType.RANDOM: ProcessCurrentNode(); break; case NodeType.SEQUENCE: ProcessCurrentNode(); break; case NodeType.GET_VARIABLE_VALUE: ProcessCurrentNode(); break; case NodeType.SET_VARIABLE_VALUE: ProcessCurrentNode(); break; case NodeType.BUILD_STRING: ProcessCurrentNode(); break; case NodeType.MATH: ProcessCurrentNode(); break; case NodeType.TRIGGER: ProcessCurrentNode(); break; case NodeType.BOOL_LOGIC: ProcessCurrentNode(); break; case NodeType.NUMBER_COMPARE: ProcessCurrentNode(); break; case NodeType.STRING_COMPARE: ProcessCurrentNode(); break; case NodeType.PATH_SELECT: HandlePathSelectorNode(); break; case NodeType.VALUE_SELECT: ProcessCurrentNode(); break; case NodeType.WAIT: HandleWaitNode(); break; case NodeType.PAUSE: HandlePauseNode(); break; case NodeType.CONDITIONAL_VALUE: ProcessCurrentNode(); break; case NodeType.PLAYER_INPUT: ProcessCurrentNode(); break; case NodeType.AI_INIT: ProcessCurrentNode(); break; case NodeType.AI_ADD_MESSAGE: ProcessCurrentNode(); break; case NodeType.AI_PROMPT: ProcessCurrentNode(); break; case NodeType.AI_CLEAR: ProcessCurrentNode(); break; case NodeType.AI_READ: ProcessCurrentNode(); break; case NodeType.SHOW: ProcessCurrentNode(); break; case NodeType.HIDE: ProcessCurrentNode(); break; case NodeType.GOTO: HandleGotoNode(); break; case NodeType.APPEND: HandleAppendNode(); break; } } /// /// Loads the Dialogue asset specified in the GOTO node and enters the dialogue. /// private void HandleGotoNode() { GotoNode node = currentNode as GotoNode; this.Initialize(node.Dialogue); this.EnterDialogue(node.EntryID); } /// /// Called whenever asynchronous execution of an AsyncNode has been completed. /// /// The AsyncNode which execution completed on. private void AsyncExecutionCompleted(AsyncNode asyncNode) { isProcessingAsyncNode = false; listener.OnNodeEvaluationCompleted(asyncNode as Node); if (asyncNode.AsyncCompletionMode == AsyncCompletionMode.REPROCESS_CURRENT) { ProcessNode(currentNode); asyncNode.Reset(); } else if(asyncNode.AsyncCompletionMode == AsyncCompletionMode.PROCEED_TO_NEXT) { asyncNode.Reset(); currentNode = GetNextNode(); ProcessNode(currentNode); } else if(asyncNode.AsyncCompletionMode == AsyncCompletionMode.PROPAGATE_ONLY) { Propagate(nodeValues, asyncNode as Node); } } /// /// Waits for the configured duration of the wait node currently being processed before continuing. /// public void HandleWaitNode() { float timeout = ((WaitNode)currentNode).GetWaitTime(); listener.Wait(timeout); } /// /// Triggers the OnPause() callback on the Dialogue Listener and waits for a continue. /// public void HandlePauseNode() { PauseNode pauseNode = currentNode as PauseNode; listener.OnPause(pauseNode.Signal); } /// /// Triggers the OnStory() callback on the Dialogue Listener and waits for a continue. /// public void HandleStoryNode() { StoryNode storyNode = currentNode as StoryNode; listener.OnStory(storyNode.Summary); } /// /// Processes the current node, determining any dependent values used by the node and setting values as necessary on outputs. Once the node /// is processed, the next node will be determined and processed. /// public void ProcessCurrentNode() { //Check value output for connection. If connected to something, propogate forward. if(!isWaitingForNodeValueDetermination) { nodeValues.Clear(); } //Attempt tp propagate the values of the currently evaluated node to any downstream connected nodes. Node undeterminedNode = Propagate(nodeValues, currentNode); if (undeterminedNode == null) //Move to the next node since the values of all nodes used by the current node could be determined. { if (currentNode is ConditionalNode) { ConditionalNode condNode = currentNode as ConditionalNode; bool conditionState = (bool)nodeValues[currentNode.ID]; NodeConnection outputConnection = conditionState ? condNode.GetTrueOutput() : condNode.GetFalseOutput(); if (outputConnection.AttachedIDs.Count > 0) { int attachedId = outputConnection.AttachedIDs[0]; currentNode = inputIdToNodeMap[attachedId]; ProcessNode(currentNode); } else { listener.OnDialogueExited(null); } } else if(currentNode is AsyncNode && !((AsyncNode)currentNode).IsExecutionComplete()) { listener.OnWaitingForNodeEvaluation(currentNode); ((AsyncNode)currentNode).IsExecutingFromDialogueFlow = true; ((AsyncNode)currentNode).Reset(); ((AsyncNode)currentNode).Execute(this, nodeValues, owner); isProcessingAsyncNode = true; } else { currentNode = GetNextNode(); ProcessNode(currentNode); } isWaitingForNodeValueDetermination = false; } else //Since we couldn't fully process the node, do whatever needs to be done to process it. { isWaitingForNodeValueDetermination = true; //Process the node if it is an asyncronous node. if (undeterminedNode is AsyncNode) { if (!((AsyncNode)undeterminedNode).IsExecutionComplete()) { //Wait for async node. listener.OnWaitingForNodeEvaluation(undeterminedNode); ((AsyncNode)undeterminedNode).IsExecutingFromDialogueFlow = (currentNode.ID == undeterminedNode.ID); ((AsyncNode)undeterminedNode).Reset(); ((AsyncNode)undeterminedNode).Execute(this, nodeValues, owner); isProcessingAsyncNode = true; } } } } /// /// Evaluates the specified node to determine its output value based on its configuration and any incoming values that it depends upon, then pushes the /// evaluated final value to any nodes connected along the output value path. /// /// A map of node IDs and connection IDs to the output values that those IDs correspond to. /// The node to evaluate. /// private Node Propagate(Dictionary nodeOutputValues, Node node) { //Determine the value for the current node and store it. Node undeterminedNode = DetermineNodeValue(nodeOutputValues, node); if (undeterminedNode != null) { isWaitingForNodeValueDetermination = true; return undeterminedNode; } //If the node has connected outputs, check any value outputs and propagate forward to those if (node.HasConnectedOutputs()) { foreach (NodeConnection connection in node.Outputs) { foreach (int id in connection.AttachedIDs) { Node nextNode = inputIdToNodeMap[id]; if (!connection.IsDialogueFlowConnection()) { undeterminedNode = Propagate(nodeOutputValues, nextNode); if (undeterminedNode != null) { isWaitingForNodeValueDetermination = true; return undeterminedNode; } } } } } return null; } /// /// Recursively determines the output value of the specified node. /// /// A map of node IDs and connection IDs to the output values that those IDs correspond to. /// The node to evaluate. /// If the output values of the node could be determined successfully, this method returns null; otherwise this method returns the node whose value /// could not be determined (this may differ from the node originally passed into this method, since it could be a dependency node). private Node DetermineNodeValue(Dictionary nodeOutputValues, Node node) { if (node is FunctionalNode) { FunctionalNode vrNode = (FunctionalNode)node; //Evaluate all of the node dependencies coming in through inputs. if (vrNode.HasDependencies()) { List outputIds = vrNode.GetDependencyOutputIDs(); foreach (int id in outputIds) { if (!nodeOutputValues.ContainsKey(id)) { Node dependencyNode = outputIdToNodeMap[id]; Node undeterminedNode = DetermineNodeValue(nodeOutputValues, dependencyNode); if (undeterminedNode != null) { isWaitingForNodeValueDetermination = true; return undeterminedNode; } } } } //After all dependencies have been evaluated, determine the final value of the specified node. if (!vrNode.DetermineAndStoreValue(this, nodeOutputValues, owner)) { isWaitingForNodeValueDetermination = true; return vrNode as Node; } } isWaitingForNodeValueDetermination = false; return null; } /// /// Handles the 'jump out' node by finding the associated 'jump in' node and moving to the next node after the 'jump in'. /// public void HandleJump() { currentNode = jumpMap[((JumpOutNode)currentNode).Key]; currentNode = GetNextNode(); ProcessNode(currentNode); } /// /// Handles the current 'conversation' node, sending a signal via the onDisplayConversationLine callback to display the first line in the 'conversation' node. If /// the 'conversation' node only has 1 line, the atConversationNodeEnding callback will also be triggered. /// public void HandleConversation() { if (currentNode.NodeType == NodeType.CONVO) { convoIdx = 0; ConversationNode convoNode = currentNode as ConversationNode; ConversationLine convoLine = GetConversationLine(); listener.OnDisplayLine(convoLine); //Display options for the conversation if there is only one line in the convo. if (convoIdx >= ((ConversationNode)currentNode).Items.Count - 1) { listener.OnConversationEnding(convoLine, GetNextNode()); } } } /// /// Builds a ConversationLine object containing information about the current line of dialogue to be displayed. This method handles variable injection, tag extraction, /// and translation on the text in the 'conversation' node's current line of dialogue. /// /// A ConversationLine containing information about the current line of dialogue to be displayed. private ConversationLine GetConversationLine() { ConversationLine line = null; string text = ""; if (currentNode is ConversationNode) { ConversationNode convoNode = ((ConversationNode)currentNode); if (convoIdx < convoNode.Items.Count) { ConversationItem convoItem = convoNode.Items[convoIdx] as ConversationItem; line = new ConversationLine(); text = convoItem.Text; line.AudioClip = convoItem.AudioClip; line.OriginalCharacterName = convoNode.CharacterName; Node nextNode = GetNextNode(); if (nextNode != null && (nextNode is OptionNode) && convoIdx == convoNode.Items.Count - 1) { line.PrecedesOption = true; } } } else if(currentNode is AppendNode) { AppendNode appendNode = currentNode as AppendNode; line = new ConversationLine(); text = appendNode.Text; Node nextNode = GetNextNode(); if (nextNode != null && (nextNode is OptionNode)) { line.PrecedesOption = true; } line.AudioClip = appendNode.AudioClip; line.TextDisplayMode = TextDisplayMode.APPEND; } if (line != null) { Dictionary tags = new Dictionary(); text = NodeTag.ExtractTags(text, tags); string untranslatedText; text = Translate(text, out untranslatedText); line.Text = text; line.PreTranslationText = untranslatedText; if (tags.ContainsKey("append")) { line.TextDisplayMode = TextDisplayMode.APPEND; } if (tags.ContainsKey("key")) { line.Key = (tags["key"] as KeyTag).keyValue; } if (tags.ContainsKey("target")) { line.Target = (tags["target"] as TargetTag).target; } if (tags.ContainsKey("id")) { line.ID = (tags["id"] as IDTag).id; } if (tags.ContainsKey("name")) { NameTag nameTag = (tags["name"] as NameTag); line.OriginalCharacterName = nameTag.name; //If a particular icon was specified, set the icon for the line. line.IconID = nameTag.iconId; } if (tags.ContainsKey("autoplay")) { line.AutoPlay = true; AutoplayTag autoplayTag = (tags["autoplay"] as AutoplayTag); if (autoplayTag.overrideDelay) { line.OverrideAutoplayDelay = true; line.AutoPlayDelay = autoplayTag.delay; } } //Remove TextMeshPro tags from the character name before performing a translation, since the source/original name in the translation library is TMP tag free. string tagFreeCharacterName = TMPTag.RemoveTags(line.OriginalCharacterName); //Translate the character name. If the returned result is the same as the original, then we can just use the original character name (tags included). string translatedCharacterName = Translate(tagFreeCharacterName); if(translatedCharacterName.Equals(tagFreeCharacterName)) { line.TranslatedCharacterName = line.OriginalCharacterName; } else { line.TranslatedCharacterName = translatedCharacterName; } } return line; } /// /// Returns the next node along the Dialogue flow path. /// /// The next node to be moved to after the current node. public Node GetNextNode() { NodeConnection outputConnection = (currentNode as DialogueFlowNode).GetFlowOutput(); if (outputConnection.AttachedIDs.Count > 0) { int attachedId = outputConnection.AttachedIDs[0]; return inputIdToNodeMap[attachedId]; } else { return null; } } /// /// Evaluates the current 'path select' node to determine which path to continue down and the moves along that path. /// public void HandlePathSelectorNode() { Dictionary nodeValues = new Dictionary(); PathSelectorNode selectorNode = currentNode as PathSelectorNode; //Find the value of the input index node if there is one. List inputNodeOutputIds = currentNode.FindDependencyOutputIDs(); if(inputNodeOutputIds.Count > 0) { Node inputNode = outputIdToNodeMap[inputNodeOutputIds[0]]; if(inputNode is FunctionalNode) { DetermineNodeValue(nodeValues, inputNode); } } //Store the index value in the selector node. selectorNode.FindIndex(nodeValues); //Get the dialogue flow output of the node (which should be based on the index value). NodeConnection selectedOutputConnection = selectorNode.GetFlowOutput(); int attachedId = selectedOutputConnection.AttachedIDs[0]; //Handle the new node. currentNode = inputIdToNodeMap[attachedId]; ProcessNode(currentNode); } /// /// Builds a List of DialogueOptions to present to the player based on the current 'option' node. This method handles 'option modifier' nodes and also /// deals with variable injection, translation, and tag extraction. Once the List of DialogueOptions is created, this method calls the onDisplayOptions callback /// so that the options can be used by anything registered with the delegate. /// public void HandleOptionNode() { if(!isWaitingForNodeValueDetermination) { nodeValues.Clear(); } OptionNode optionNode = currentNode as OptionNode; List optionModifierOutputIds = currentNode.FindDependencyOutputIDs(); foreach (int outputId in optionModifierOutputIds) { Node inputNode = outputIdToNodeMap[outputId]; if (inputNode is OptionModifierNode) { Node undeterminedNode = DetermineNodeValue(nodeValues, inputNode); if(undeterminedNode != null) { isWaitingForNodeValueDetermination = true; //Process the node if it is an asyncronous node. if (undeterminedNode is AsyncNode) { if (!((AsyncNode)undeterminedNode).IsExecutionComplete()) { //Wait for async node. listener.OnWaitingForNodeEvaluation(undeterminedNode); ((AsyncNode)undeterminedNode).IsExecutingFromDialogueFlow = false; ((AsyncNode)undeterminedNode).Reset(); ((AsyncNode)undeterminedNode).Execute(this, nodeValues, owner); isProcessingAsyncNode = true; } } return; } } } List options = new List(); for (int i = 0; i < optionNode.Items.Count; i++) { DialogueOption option = new DialogueOption(); option.OptionIndex = i; NodeConnection modifierConnection = optionNode.Inputs[i + 1]; if (modifierConnection.AttachedIDs.Count > 0) { OptionModifier modifier = nodeValues[modifierConnection.AttachedIDs[0]] as OptionModifier; if (modifier.text != null) { option.OptionText = modifier.text; } else if (optionNode.Items[i] != null && (optionNode.Items[i] as OptionItem).text != null) { option.OptionText = (optionNode.Items[i] as OptionItem).text; } option.IsDisplayed = modifier.displayed; option.IsSelectable = modifier.selectable; } else { if (optionNode.Items[i] != null && (optionNode.Items[i] as OptionItem).text != null) { option.OptionText = (optionNode.Items[i] as OptionItem).text; } else { option.OptionText = "..."; } option.IsDisplayed = true; option.IsSelectable = true; } string optionText = option.OptionText; Dictionary tags = new Dictionary(); optionText = NodeTag.ExtractTags(optionText, tags); if (tags.ContainsKey("display")) { option.IsDisplayed = (tags["display"] as DisplayTag).display; } if (tags.ContainsKey("selectable")) { option.IsSelectable = (tags["selectable"] as SelectableTag).selectable; } if (tags.ContainsKey("id")) { option.ID = (tags["id"] as IDTag).id; } string untranslatedText; optionText = Translate(optionText, out untranslatedText); option.OptionText = optionText; option.PreTranslationText = untranslatedText; options.Add(option); } listener.OnDisplayOptions(options); } /// /// Performs a lookup in the active Translation Library for the text provided and returns a translation, if there is one. The text is attempted to be translated into /// whatever langauge is set on EasyTalkGameState.Instance.Language. /// /// The text to translate. /// A translation of the provided text, if there is a match in the current Translation Library. public string Translate(string text) { string preTranslationText; return Translate(text, out preTranslationText); } /// /// Performs a lookup in the active Translation Library for the text provided and returns a translation, if there is one. The text is attempted to be translated into /// whatever langauge is set on EasyTalkGameState.Instance.Language. This method also sets the provided preTranslationText string to the string being translated, immediately prior to translation. The /// preTranslationText may differ from the originally provided text if the EasyTalkDialogueSettings.Instance.TranslationEvaluationMode is set to TRANSLATE_AFTER_VARIABLE_EVALUATION, since in that mode, all /// variable tags are replaced with their values prior to translation. /// /// The text to translate. /// A string to store the text value which was translated in. This value is the text which is ultimately translated and may differ from the provided text string. /// A translation of the provided text, if there is a match in the current Translation Library. public string Translate(string text, out string preTranslationText) { string finalText = text; preTranslationText = finalText; if (dialogueSettings != null) { if (dialogueSettings.TranslationEvaluationMode == TranslationEvaluationMode.TRANSLATE_BEFORE_VARIABLE_EVALUATION) { finalText = TranslateText(finalText); } finalText = ReplaceVariablesInString(finalText); if (dialogueSettings.TranslationEvaluationMode == TranslationEvaluationMode.TRANSLATE_AFTER_VARIABLE_EVALUATION) { preTranslationText = finalText; finalText = TranslateText(finalText); } } return finalText; } /// /// Handles the current 'exit' node by exiting the dialogue and triggering the onDialogueExited callback. /// public void HandleExit() { if (currentNode != null) { ExitNode exitNode = currentNode as ExitNode; listener.OnDialogueExited(exitNode.ExitPointName); } else { listener.OnDialogueExited(null); } } /// /// This method does variable value injection, replacing all variable references in the specified string with their associated values. /// /// The string to inject variable values into. /// The modified string with variable references replaced by their respective values. public string ReplaceVariablesInString(string text) { try { string newText = text; int variableStartIdx = -1; int variableEndIdx = -1; while ((variableStartIdx = newText.IndexOf("(@")) > -1 && (variableEndIdx = newText.IndexOf(")", variableStartIdx + 2)) > -1) { string variableName = newText.Substring(variableStartIdx + 2, variableEndIdx - variableStartIdx - 2); NodeVariable variable = GetVariable(variableName); if (variable != null) { newText = newText.Substring(0, variableStartIdx) + variable.currentValue + newText.Substring(variableEndIdx + 1); } else { break; } } return newText; } catch { return text; } } /// /// Returns the value of the variable specified, if available; otherwise returns null. /// /// The name of the variable to retrieve the value of. /// The value of the specified variable. public object GetVariableValue(string variableName) { if(globalVariables.ContainsKey(variableName)) { return globalVariables[variableName]; } else if (variables.ContainsKey(variableName)) { return variables[variableName]; } return null; } /// /// Sets the value of the specified variable. /// /// The name of the variable to set. /// The value to set on the variable. public void SetVariableValue(string variableName, object variableValue) { if(globalVariables.ContainsKey(variableName)) { globalVariables[variableName].currentValue = variableValue; listener.OnVariableUpdated(variableName, variableValue); } else if (variables.ContainsKey(variableName)) { variables[variableName].currentValue = variableValue; listener.OnVariableUpdated(variableName, variableValue); } } /// /// Returns the NodeVariable associated with the specified variable name. /// /// The name of the variable to retrieve. /// The NodeVariable for the specified variable name. public NodeVariable GetVariable(string variableName) { if(globalVariables.ContainsKey(variableName)) { return globalVariables[variableName]; } else if (variables.ContainsKey(variableName)) { return variables[variableName]; } return null; } /// /// Finds and returns the first 'entry' node in the Dialogue. /// /// The first 'entry' node found int the Dialogue. public EntryNode FindEntryNode() { foreach (Node node in dialogue.Nodes) { if (node.NodeType == NodeType.ENTRY) { return (EntryNode)node; } } return null; } /// /// Enters and begins processing the Dialogue. If no entry point is specified, the Dialogue will enter at the first 'entry' node found in the Dialogue. /// /// The optional name of the entry point where the Dialogue should start being processed. public void EnterDialogue(string entryPointName = null) { if (entryPointName == null) { currentNode = entryNode; } else { if (entryMap.ContainsKey(entryPointName)) { currentNode = entryMap[entryPointName]; } else { currentNode = entryNode; } } if (currentNode == null) { return; } listener.OnDialogueEntered(entryPointName); currentNode = GetNextNode(); //Reset flags ResetVariablesOnEntry(); ProcessNode(currentNode); } /// /// Continues along to the next line of dialogue, or the next node if the last line is currently being displayed. /// public void Continue() { if(currentNode == null) { listener.OnDialogueExited(null); return; } if (currentNode.NodeType == NodeType.CONVO) { ConversationNode convoNode = ((ConversationNode)currentNode); convoIdx++; ConversationLine currentLine = GetConversationLine(); if (currentLine != null && convoIdx < convoNode.Items.Count) { listener.OnDisplayLine(currentLine); } if(currentLine != null && convoIdx == convoNode.Items.Count - 1) { Node nextNode = GetNextNode(); listener.OnConversationEnding(currentLine, nextNode); } else if (convoIdx > convoNode.Items.Count - 1) { Node nextNode = GetNextNode(); //If we are past the last conversation item, go to the next node. convoIdx = 0; currentNode = nextNode; ProcessNode(currentNode); } }else if(isProcessingAsyncNode) { //Interrupt the async operation, if permitted, and move on to the next node. if (currentNode is AsyncNode) { AsyncNode asyncNode = (AsyncNode)currentNode; if(asyncNode.IsSkippable()) { asyncNode.Interrupt(); isProcessingAsyncNode = false; currentNode = GetNextNode(); ProcessNode(currentNode); } } } else if (currentNode.NodeType == NodeType.STORY || currentNode.NodeType == NodeType.PAUSE || currentNode.NodeType == NodeType.WAIT || currentNode.NodeType == NodeType.AI_PROMPT || currentNode.NodeType == NodeType.APPEND) { currentNode = GetNextNode(); ProcessNode(currentNode); } } /// /// Resets the values of each variable which is set to "reset on entry". /// public void ResetVariablesOnEntry() { foreach (NodeVariable variable in variables.Values) { if (variable.resetOnEntry) { variable.currentValue = variable.initialValue; } } } /// /// Creates a new bool type NodeVariable from the provided bool variable node. /// /// The node to use. private void CreateBoolVariable(Node node) { VariableNode varNode = (VariableNode)node; bool boolValue = true; if (varNode.VariableValue != null) { bool.TryParse(varNode.VariableValue, out boolValue); } CreateVariable(typeof(bool), varNode.VariableName, boolValue, boolValue, varNode.ResetOnEntry, varNode.IsGlobal); } /// /// Creates a new int type NodeVariable from the provided int variable node. /// /// The node to use. private void CreateIntVariable(Node node) { VariableNode varNode = (VariableNode)node; int intValue = 0; if (varNode.VariableValue != null) { int.TryParse(varNode.VariableValue, out intValue); } CreateVariable(typeof(int), varNode.VariableName, intValue, intValue, varNode.ResetOnEntry, varNode.IsGlobal); } /// /// Creates a new float type NodeVariable from the provided float variable node. /// /// The node to use. private void CreateFloatVariable(Node node) { VariableNode varNode = (VariableNode)node; float floatValue = 0; if (varNode.VariableValue != null) { float.TryParse(varNode.VariableValue, out floatValue); } CreateVariable(typeof(float), varNode.VariableName, floatValue, floatValue, varNode.ResetOnEntry, varNode.IsGlobal); } /// /// Creates a new string type NodeVariable from the provided string variable node. /// /// The node to use. private void CreateStringVariable(Node node) { VariableNode varNode = (VariableNode)node; string stringValue = ""; if (varNode.VariableValue != null) { stringValue = varNode.VariableValue; } CreateVariable(typeof(string), varNode.VariableName, stringValue, stringValue, varNode.ResetOnEntry, varNode.IsGlobal); } /// /// Creates a variable for use by the node handler. Global variables persist for use by all node handlers. /// /// The type of the variable (int, float, string, or bool). /// The name of the variable. /// The initial/default value of the variable. /// The current value of the variable. /// Whether the variable should be reset whenever the dialogue is entered (only applicable for local variables, not global). /// Whether the variable is global (persists to all dialogues). private void CreateVariable(Type type, string variableName, object initialValue, object currentValue, bool resetOnEntry, bool isGlobal) { NodeVariable variable = new NodeVariable(); variable.variableType = type; variable.variableName = variableName; variable.initialValue = initialValue; variable.currentValue = currentValue; variable.resetOnEntry = resetOnEntry; variable.isGlobal = isGlobal; if (!isGlobal) { variables.Add(variableName, variable); } else { globalVariables.Add(variableName, variable); } } /// /// Saves the values of all local variables in the Dialogue to either a file, or to the PlayerPrefs. /// /// The prefix to use when saving. This prefix is appended to the beginning of the dialogue name. /// Whether the variable states should be saved to PlayerPrefs. If set to false, the variable states will be saved to /// a JSON file instead. public void SaveVariableValues(string prefix = "", bool saveToPlayerPrefs = false) { NodeVariableValueCollection valueCollection = GetVariableValues(); SaveVariableValues(valueCollection, dialogue.name, prefix, saveToPlayerPrefs); } /// /// Saves global variable values to PlayerPrefs or a JSON file. /// /// The prefix to use when saving. The prefix is appended before '_global' or '_global.json'. /// Whether variable values should be saved to PlayerPrefs rather than a JSON file. public void SaveGlobalVariableValues(string prefix = "", bool saveToPlayerPrefs = false) { NodeVariableValueCollection valueCollection = GetGlobalVariableValues(); SaveVariableValues(valueCollection, "global", prefix, saveToPlayerPrefs); } /// /// Saves variable values from the provided NodeVariableValueCollection to PlayerPrefs or a JSON file. /// /// The collection of variable values to save. /// The suffix to use when saving the values. /// The prefix to use when saving the values. /// Whether the values should be saved to PlayerPrefs rather than a JSON file. private void SaveVariableValues(NodeVariableValueCollection variableValues, string suffix, string prefix = "", bool saveToPlayerPrefs = false) { string json = JsonUtility.ToJson(variableValues); if (saveToPlayerPrefs) { PlayerPrefs.SetString(prefix + "_" + suffix, json); } else { string savePath = Application.persistentDataPath + "/" + prefix + "_" + suffix + ".json"; try { if (File.Exists(savePath)) { if (File.Exists(savePath + ".tmp")) { File.Delete(savePath + ".tmp"); } File.Move(savePath, savePath + ".tmp"); } StreamWriter streamWriter = new StreamWriter(savePath); streamWriter.Write(json); streamWriter.Close(); } catch (Exception e) { Debug.Log("Unable to save dialogue variable values to " + savePath + " " + e.StackTrace); } } } /// /// Loads variable values for the dialogue from a save if available. /// /// The prefix to use when loading. This prefix is appended to ('_' + the dialogue name) or )'_' + the dialogue name + '.json'), depending on whether /// values are to be loaded from PlayerPrefs or a JSON file. /// public void LoadVariableValues(string prefix = "", bool loadFromPlayerPrefs = false) { NodeVariableValueCollection localVariableValues = LoadVariableValueCollection(dialogue.name, prefix, loadFromPlayerPrefs); if (localVariableValues != null) { LoadVariableValues(localVariableValues); } } /// /// Loads global variable values from a save if available. /// /// The prefix to use when loading. This prefix is appended to '_global' or '_global.json', depending on whether /// values are to be loaded from player prefs or a JSON file. /// If true, variable states will be loaded from PlayerPrefs rather than a JSON file. public void LoadGlobalVariableValues(string prefix = "", bool loadFromPlayerPrefs = false) { NodeVariableValueCollection globalVariableValues = LoadVariableValueCollection("global", prefix, loadFromPlayerPrefs); if (globalVariableValues != null) { LoadGlobalVariableValues(globalVariableValues); } } /// /// Loads the states of variables from a save if available. /// /// The suffix to use when loading the values. /// The prefix to use when loading the values. /// If true, variable states will be loaded from PlayerPrefs rather than a JSON file. private NodeVariableValueCollection LoadVariableValueCollection(string suffix, string prefix = "", bool loadFromPlayerPrefs = false) { string json = null; if (loadFromPlayerPrefs) { json = PlayerPrefs.GetString(prefix + "_" + suffix); } else { string loadPath = Application.persistentDataPath + "/" + prefix + "_" + suffix + ".json"; if (File.Exists(loadPath)) { try { StreamReader streamReader = new StreamReader(loadPath); json = streamReader.ReadToEnd(); streamReader.Close(); } catch (Exception e) { Debug.LogError("Cannot read variable values fro dialogue from " + loadPath + " " + e.StackTrace); } } } if (json != null) { try { NodeVariableValueCollection valueCollection = JsonUtility.FromJson(json); return valueCollection; } catch (Exception e) { Debug.LogError("Cannot load variable values from json: '" + json + "' " + e.StackTrace); } } return null; } /// /// Loads the variable values from the specified NodeVariableValueCollection into the Dialogue. /// /// The collection of node variable values to load. public void LoadVariableValues(NodeVariableValueCollection valueCollection) { if (valueCollection != null) { LoadVariableValuesFromCollection(valueCollection, variables); } } /// /// Loads the values from the specified NodeVariableValueCollection into the collection of global variables. /// /// The collection of global variable values to load. public void LoadGlobalVariableValues(NodeVariableValueCollection valueCollection) { if (valueCollection != null) { LoadVariableValuesFromCollection(valueCollection, globalVariables); } } /// /// Loads the values from the specified NodeVariableValueCollection into the provided Dictionary of variable names mapped to NodeVariable values. /// /// The collection of variable values to load. /// The Dictionary to load variables into. private void LoadVariableValuesFromCollection(NodeVariableValueCollection valueCollection, Dictionary dialogueVariables) { foreach (NodeVariableValue nodeValue in valueCollection.values) { NodeVariable variable = null; if (dialogueVariables.ContainsKey(nodeValue.variableName)) { variable = dialogueVariables[nodeValue.variableName]; object value = null; if (variable.variableType == typeof(float)) { value = Convert.ToSingle(nodeValue.value); } else if (variable.variableType == typeof(int)) { value = Convert.ToInt32(nodeValue.value); } else if (variable.variableType == typeof(bool)) { value = Convert.ToBoolean(nodeValue.value); } else if (variable.variableType == typeof(string)) { value = Convert.ToString(nodeValue.value); } variable.currentValue = value; } } } /// /// Returns a new NodeVariableValueCollection containing the current values of each variable in the Dialogue. /// /// A collection of current variable values for the Dialogue. public NodeVariableValueCollection GetVariableValues() { return CreateNodeVariableValueCollection(variables); } /// /// Returns a new NodeVariableValueCollection containing the current values of all global dialogue variables. /// /// A collection of the current global dialogue variable values. public NodeVariableValueCollection GetGlobalVariableValues() { return CreateNodeVariableValueCollection(globalVariables); } /// /// Creates and returns a new NodeVariableValueCollection containing the current values of all of the variables in the provided Dictionary. /// /// The variable dictionary to populate from. /// A collection of variable values from the Dictionary provided. private NodeVariableValueCollection CreateNodeVariableValueCollection(Dictionary dialogueVariables) { NodeVariableValueCollection valueCollection = new NodeVariableValueCollection(); foreach (string varName in dialogueVariables.Keys) { NodeVariableValue varState = new NodeVariableValue(); varState.variableName = varName; varState.value = dialogueVariables[varName].currentValue.ToString(); valueCollection.values.Add(varState); } return valueCollection; } /// /// Translates the specified string to a localized string using the TranslationLibrary of the Dialogue. /// /// The line of text to translate. /// The translated text, if a translation is found that matches the text provided. public string TranslateText(string text) { if(dialogue.TranslationLibrary == null) { return text; } string languageCode = EasyTalkGameState.Instance.Language; if(languageCode == null || languageCode.Equals(dialogue.TranslationLibrary.originalLanguage)) { return text; } string newText = text; NodeTag translateTag; newText = NodeTag.ExtractTag(newText, "translate", out translateTag); if(translateTag != null) { if(!(translateTag as TranslateTag).translate) { return newText; } } if (languageCode != null) { if(languageCode.Equals(dialogue.TranslationLibrary.originalLanguage)) { return newText; } else if(newText != null) { Translation translation = dialogue.TranslationLibrary.GetTranslation(newText, languageCode); if (translation != null && translation.text != null && translation.text.Length > 0) { return translation.text; } } } return newText; } /// /// /// private void SaveVariables() { SaveVariableValues(EasyTalkGameState.Instance.SessionID); } /// /// /// private void SaveGlobalVariables() { SaveGlobalVariableValues(EasyTalkGameState.Instance.SessionID); } /// /// /// private void LoadVariables() { LoadVariableValues(EasyTalkGameState.Instance.SessionID); } /// /// /// private void LoadGlobalVariables() { LoadGlobalVariableValues(EasyTalkGameState.Instance.SessionID); } /// /// Sets the Dialogue Listener which the node handler should send events to during Dialogue processing. /// /// The Dialogue Listener to use. public void SetListener(DialogueListener listener) { this.listener = listener; } /// /// Gets the Dialogue Listener of the node handler. /// public DialogueListener Listener { get { return this.listener; } } /// /// Gets or sets the Dialogue Settings used by the node handler. /// public EasyTalkDialogueSettings DialogueSettings { get { return dialogueSettings; } set { dialogueSettings = value; } } /// /// Attempts to initialize all global variables, if they can be found in either the DialogueRegistry provided, or in a DialogueRegistry in the current /// Dialogue Settings, or in the Dialogue Settings of a Dialogue Display in the Hierarchy. /// /// A Dialogue Registry to initialize global variables from. public void InitializeGlobalVariables(DialogueRegistry registry = null) { DialogueRegistry dialogueRegistry = registry; if (registry == null) { if (dialogueSettings != null && dialogueSettings.DialogueRegistry != null) { dialogueRegistry = dialogueSettings.DialogueRegistry; } else if(dialogueSettings == null) { #if UNITY_6000_0_OR_NEWER DialogueDisplay display = GameObject.FindFirstObjectByType(FindObjectsInactive.Include); #else DialogueDisplay display = GameObject.FindObjectOfType(true); #endif if (display != null && display.DialogueSettings != null) { dialogueSettings = display.DialogueSettings; if (display.DialogueSettings.DialogueRegistry != null) { dialogueRegistry = display.DialogueSettings.DialogueRegistry; } } } } if(dialogueRegistry != null) { foreach (GlobalNodeVariable globalVar in dialogueRegistry.GlobalVariables) { if (!globalVariables.ContainsKey(globalVar.VariableName)) { switch (globalVar.VariableType) { case GlobalVariableType.STRING: CreateVariable(globalVar.GetTrueType(), globalVar.VariableName, globalVar.InitialValue, globalVar.InitialValue, false, true); break; case GlobalVariableType.INT: int intValue = 0; int.TryParse(globalVar.InitialValue, out intValue); CreateVariable(globalVar.GetTrueType(), globalVar.VariableName, intValue, intValue, false, true); break; case GlobalVariableType.FLOAT: float floatValue = 0.0f; float.TryParse(globalVar.InitialValue, out floatValue); CreateVariable(globalVar.GetTrueType(), globalVar.VariableName, floatValue, floatValue, false, true); break; case GlobalVariableType.BOOL: bool boolValue = true; bool.TryParse(globalVar.InitialValue, out boolValue); CreateVariable(globalVar.GetTrueType(), globalVar.VariableName, boolValue, boolValue, false, true); break; } } } } } } /// /// This class is used to store a collection of node variable values. /// [Serializable] public class NodeVariableValueCollection { /// /// The collection of node variable values. /// [SerializeField] public List values = new List(); } /// /// This class stores a node variable's name and value. /// [Serializable] public class NodeVariableValue { /// /// The name of the variable. /// [SerializeField] public string variableName; /// /// The value of the variable. /// [SerializeField] public string value; } }