IEnumerator RunDialogue(string startNode = "Start")
        {
            // Get lines, options and commands from the Dialogue object, one at a time.
            foreach (Yarn.Dialogue.RunnerResult step in dialogue.Run(startNode))
            {
                CurrentNode = dialogue.currentNode;
                if (step is Yarn.Dialogue.LineResult)
                {
                    // Wait for line to finish displaying
                    var lineResult = step as Yarn.Dialogue.LineResult;
                    yield return(this.StartCoroutine(RunLine(lineResult.line)));
                }
                else if (step is Yarn.Dialogue.OptionSetResult)
                {
                    // Wait for user to finish picking an option
                    var optionSetResult = step as Yarn.Dialogue.OptionSetResult;
                    RunOptions(optionSetResult.options, optionSetResult.setSelectedOptionDelegate);
                    yield return(new WaitWhile(() => inputOption < 0));
                }
                else if (step is Yarn.Dialogue.CommandResult)
                {
                    // Wait for command to finish running
                    var commandResult = step as Yarn.Dialogue.CommandResult;
                    yield return(this.StartCoroutine(RunCommand(commandResult.command)));
                }
            }

            MerinoDebug.Log(LoggingLevel.Info, "Reached the end of the dialogue.");
            CurrentNode = null;

            // No more results! The dialogue is done.
            yield return(new WaitUntil(() => MerinoPrefs.stopOnDialogueEnd));

            StopPlaytest_Internal();
        }
        /// <summary>
        /// Guesses the line number of a given line. Used for the View Node Source button.
        /// </summary>
        int GuessLineNumber(int nodeID, string lineText)
        {
            var node = MerinoData.GetNode(nodeID);

            // this is a really bad way of doing it, but Yarn Spinner's DialogueRunner doesn't offer any access to line numbers.
            if (node != null && node.nodeBody.Contains(lineText))
            {
                var lines = node.nodeBody.Split('\n');
                for (int i = 0; i < lines.Length; i++)
                {
                    if (lines[i].Contains(lineText))
                    {
                        return(i + 1);
                    }
                }
                return(-1);
            }

            if (node == null)
            {
                MerinoDebug.LogFormat(LoggingLevel.Warning, "Couldn't find node ID {0}. It might've been deleted or the Yarn file might be corrupted.", nodeID);
            }

            return(-1);
        }
Exemple #3
0
        public static void CheckFileList(IList <string> paths, List <string> allowedExtensions)
        {
            if (paths.Count == 0)
            {
                MerinoDebug.Log(LoggingLevel.Warning, "No files provided.");
                return;
            }

            var invalid = new List <string>();

            foreach (var path in paths)
            {
                // Does this file exist?
                var exists = System.IO.File.Exists(path);

                // Does this file have the right extension?
                var hasAllowedExtension = allowedExtensions.FindIndex(item => path.EndsWith(item)) != -1;

                if (!exists || !hasAllowedExtension)
                {
                    invalid.Add(string.Format("\"{0}\"", path));
                }
            }

            if (invalid.Count != 0)
            {
                var message = string.Format("The file{0} {1} {2}.",
                                            invalid.Count == 1 ? "" : "s",
                                            string.Join(", ", invalid.ToArray()),
                                            invalid.Count == 1 ? "is not valid" : "are not valid"
                                            );

                MerinoDebug.Log(LoggingLevel.Error, message);
            }
        }
Exemple #4
0
		public static TextAsset GetDefaultData()
		{
			var defaultData = Resources.Load<TextAsset>(MerinoPrefs.newFileTemplatePath);
			if (defaultData == null)
			{
				MerinoDebug.Log(LoggingLevel.Warning, "Merino couldn't find the new file template at Resources/" + MerinoPrefs.newFileTemplatePath + ". Double-check the file exists there, or you can override this path in EditorPrefs.");
				return null;
			}
			return defaultData;
		}
Exemple #5
0
        // strip forbidden characters from node titles
        public static string CleanNodeTitle(string newName)
        {
            // v0.6, added regex to disallow forbidden characters in node titles
            string newNameClean = Regex.Replace(newName, forbiddenCharactersInNodeTitles_regex, "");

            if (newName.Length != newNameClean.Length)
            {
                // GUI.Label( GUILayoutUtility.GetLastRect(), new GUIContent("Yarn node titles cannot use <>[]{}|:#$ or whitespace", helpIcon), EditorStyles.helpBox );
                MerinoDebug.Log(LoggingLevel.Verbose, "Merino stripped forbidden characters <>[]{}|:#$ (and whitespace) from your node title.\n" + newName + " ... " + newNameClean);
                return(newNameClean);
            }
            return(newName);
        }
Exemple #6
0
        public static int ConvertToYarn(ConvertFormatOptions options)
        {
            foreach (var file in options.files)
            {
                if (YarnSpinnerLoader.GetFormatFromFileName(file) == NodeFormat.Text)
                {
                    MerinoDebug.LogFormat(LoggingLevel.Warning, "Not converting file {0}, because its name implies it's already in Yarn format", file);
                    continue;
                }

                ConvertNodesInFile(options, file, "yarn.txt", ConvertNodesToYarnText);
            }
            return(0);
        }
Exemple #7
0
        public static int ConvertToJSON(ConvertFormatOptions options)
        {
            foreach (var file in options.files)
            {
                if (YarnSpinnerLoader.GetFormatFromFileName(file) == NodeFormat.JSON)
                {
                    MerinoDebug.LogFormat(LoggingLevel.Warning, "Not converting file {0}, because its name implies it's already in JSON format", file);
                    continue;
                }

                ConvertNodesInFile(options, file, "json", (IEnumerable <YarnSpinnerLoader.NodeInfo> nodes) => JsonConvert.SerializeObject(nodes, Formatting.Indented));
            }
            return(0);
        }
Exemple #8
0
		public static TextAsset LoadYarnFileAtFullPath(string path, bool isRelativePath=false)
		{
			var newFile = AssetDatabase.LoadAssetAtPath<TextAsset>( isRelativePath ? path : "Assets" + path.Substring(Application.dataPath.Length) );
			if (MerinoData.CurrentFiles.Contains(newFile) == false)
			{
				MerinoData.CurrentFiles.Add(newFile);
			}
			else
			{
				MerinoDebug.Log(LoggingLevel.Warning, "Merino: file at " + path + " is already loaded!");
			}

			return newFile;
		}
        /// Erase all variables and reset to default values
        public void ResetToDefaults()
        {
            Clear();

            // For each default variable that's been defined, parse the string
            // that the user typed in in Unity and store the variable
            foreach (var variable in defaultVariables)
            {
                object value;

                switch (variable.type)
                {
                case Yarn.Value.Type.Number:
                    float f = 0.0f;
                    float.TryParse(variable.value, out f);
                    value = f;
                    break;

                case Yarn.Value.Type.String:
                    value = variable.value;
                    break;

                case Yarn.Value.Type.Bool:
                    bool b = false;
                    bool.TryParse(variable.value, out b);
                    value = b;
                    break;

                case Yarn.Value.Type.Variable:
                    // We don't support assigning default variables from other variables
                    // yet
                    MerinoDebug.LogFormat(LoggingLevel.Error,
                                          "Can't set variable {0} to {1}: You can't set a default variable to be another variable, because it may not have been initialised yet.",
                                          variable.name, variable.value);
                    continue;

                case Yarn.Value.Type.Null:
                    value = null;
                    break;

                default:
                    throw new System.ArgumentOutOfRangeException();
                }

                var v = new Yarn.Value(value);

                SetValue("$" + variable.name, v);
            }
        }
Exemple #10
0
        private void InitIfNeeded()
        {
            // create the main Dialogue runner, and pass our variableStorage to it
            varStorage = new MerinoVariableStorage();
            dialogue   = new Yarn.Dialogue(varStorage);

            // setup the logging system.
            dialogue.LogDebugMessage = message => MerinoDebug.Log(LoggingLevel.Verbose, message);
            dialogue.LogErrorMessage = PlaytestErrorLog;

            // icons
            if (errorIcon == null)
            {
                errorIcon = EditorGUIUtility.Load("icons/d_console.erroricon.sml.png") as Texture;
            }
        }
        /// <summary>
        /// Logs errors from the playtest engine and Yarn Loader.
        /// </summary>
        public void PlaytestErrorLog(string message)
        {
            string fileName   = "unknown";
            string nodeName   = "unknown";
            int    lineNumber = -1;

            // detect file name
            if (message.Contains("file"))
            {
                fileName = message.Split(new string[] { "file" }, StringSplitOptions.None)[1].Split(new string[] { " ", ":" }, StringSplitOptions.None)[1];
            }

            // detect node name
            if (message.Contains("node"))
            {
                nodeName = message.Split(new string[] { "node" }, StringSplitOptions.None)[1].Split(new string[] { " ", ":" }, StringSplitOptions.None)[1];

                // detect line numbers, if any, by grabbing the first digit after nodeName
                string numberLog = "";
                for (int index = message.IndexOf(nodeName); index < message.Length; index++)
                {
                    if (Char.IsDigit(message[index]))
                    {
                        numberLog += message[index];
                    }
                    else if (numberLog.Length > 0)                      // did we hit a non-number, after already hitting numbers? then stop
                    {
                        break;
                    }
                }

                int.TryParse(numberLog, out lineNumber);
            }

            var nodeRef = MerinoData.TreeElements.Find(x => x.name == nodeName);

            // v0.6, resolved: "todo: replace "<input>" in parsing errors with the name of the file instead."
            // if filename is default "<input>" then guess filename via cached parentID data in TreeElements
            if (fileName == "<input>" && nodeRef != null)
            {
                fileName = MerinoData.GetFileParent(nodeRef.id).name;
            }

            MerinoEditorWindow.errorLog.Add(new MerinoEditorWindow.MerinoErrorLine(message, fileName, nodeRef != null ? nodeRef.id : -1, Mathf.Max(0, lineNumber)));
            MerinoDebug.Log(LoggingLevel.Error, message);
        }
Exemple #12
0
        public static void ConvertNodesInFile(ConvertFormatOptions options, string file, string fileExtension, ConvertNodesToText convert)
        {
            //	var d = new Dialogue(null);

            var text = File.ReadAllText(file);

            IEnumerable <YarnSpinnerLoader.NodeInfo> nodes;

            try {
                nodes = YarnSpinnerLoader.GetNodesFromText(text, YarnSpinnerLoader.GetFormatFromFileName(file));
            } catch (FormatException e) {
                MerinoDebug.Log(LoggingLevel.Error, e.Message);
                return;
            }

            var serialisedText = convert(nodes);

            var destinationDirectory = options.outputDirectory;

            if (destinationDirectory == null)
            {
                destinationDirectory = Path.GetDirectoryName(file);
            }

            var fileName = Path.GetFileName(file);

            // ChangeExtension thinks that the file "Foo.yarn.txt" has the extension "txt", so
            // to simplify things, just lop that extension off right away if it's there
            fileName = fileName.Replace(".yarn.txt", "");

            // change the filename's extension
            fileName = Path.ChangeExtension(fileName, fileExtension);

            // figure out where we're writing this file
            var destinationFilePath = Path.Combine(destinationDirectory, fileName);

            File.WriteAllText(destinationFilePath, serialisedText);

            if (options.verbose)
            {
                MerinoDebug.Log(LoggingLevel.Verbose, "Wrote " + destinationFilePath);
            }
        }
Exemple #13
0
 public static void SaveDataToFiles()
 {
     if (MerinoData.CurrentFiles.Count > 0)
     {
         foreach (var file in MerinoData.CurrentFiles)
         {
             if (MerinoData.FileToNodeID.ContainsKey(file))
             {
                 File.WriteAllText(AssetDatabase.GetAssetPath(file), SaveFileNodesAsString(MerinoData.FileToNodeID[file]));
                 EditorUtility.SetDirty(file);
                 LastSaveTime = EditorApplication.timeSinceStartup;
             }
             else
             {
                 MerinoDebug.Log(LoggingLevel.Warning, file.name + " has not been mapped to a NodeID and cannot be saved, reload the file and try again.");
             }
         }
     }
 }
Exemple #14
0
        /// <summary>
        /// Logs errors from the playtest engine and Yarn Loader.
        /// </summary>
        public void PlaytestErrorLog(string message)
        {
            string fileName   = "unknown";
            string nodeName   = "unknown";
            int    lineNumber = -1;

            // detect file name
            if (message.Contains("file"))
            {
                fileName = message.Split(new string[] { "file" }, StringSplitOptions.None)[1].Split(new string[] { " ", ":" }, StringSplitOptions.None)[1];
            }

            // detect node name
            if (message.Contains("node"))
            {
                nodeName = message.Split(new string[] { "node" }, StringSplitOptions.None)[1].Split(new string[] { " ", ":" }, StringSplitOptions.None)[1];

                // detect line numbers, if any, by grabbing the first digit after nodeName
                string numberLog = "";
                for (int index = message.IndexOf(nodeName); index < message.Length; index++)
                {
                    if (Char.IsDigit(message[index]))
                    {
                        numberLog += message[index];
                    }
                    else if (numberLog.Length > 0)                      // did we hit a non-number, after already hitting numbers? then stop
                    {
                        break;
                    }
                }

                int.TryParse(numberLog, out lineNumber);
            }

            // also output to Unity console
            var nodeRef = MerinoData.TreeElements.Find(x => x.name == nodeName);

            MerinoEditorWindow.errorLog.Add(new MerinoEditorWindow.MerinoErrorLine(message, fileName, nodeRef != null ? nodeRef.id : -1, Mathf.Max(0, lineNumber)));
            MerinoDebug.Log(LoggingLevel.Error, message);
        }
Exemple #15
0
        public static void SaveDataToFiles()
        {
            if (MerinoData.CurrentFiles.Count > 0)
            {
                for (int i = 0; i < MerinoData.CurrentFiles.Count; i++)
                {
                    TextAsset file = MerinoData.CurrentFiles[i];
                    if (MerinoData.FileToNodeID.ContainsKey(file))
                    {
                        if (file == null)
                        {
                            var missingDataID = MerinoData.FileToNodeID[file];
                            MerinoData.DirtyFiles.Remove(file);                             // wait how does this work? it's null, but the pointer isn't?
                            MerinoData.CurrentFiles.RemoveAt(i);
                            i--;
                            var missingFileName = MerinoData.GetNode(missingDataID) != null?MerinoData.GetNode(missingDataID).name : "<cannot recover filename>";

                            if (EditorUtility.DisplayDialog("Can't save file " + missingFileName, "The file has been deleted, moved outside of the project's assets folder, or otherwise hidden. Merino can't find it.", "Save backup of current data as new file", "Do nothing"))
                            {
                                MerinoEditorWindow.GetEditorWindow().CreateNewYarnFile("YarnBackupOfFile", SaveAllNodesAsString(missingDataID));
                            }
                            continue;
                        }
                        else
                        {
                            File.WriteAllText(AssetDatabase.GetAssetPath(file), SaveFileNodesAsString(MerinoData.FileToNodeID[file]));
                            EditorUtility.SetDirty(file);
                            LastSaveTime = EditorApplication.timeSinceStartup;
                        }
                    }
                    else
                    {
                        MerinoDebug.Log(LoggingLevel.Warning, file.name + " has not been mapped to a NodeID and cannot be saved, reload the file and try again.");
                    }
                }
                EditorUtility.SetDirty(MerinoData.Instance);
            }
        }
Exemple #16
0
        public static string ConvertNodesToYarnText(IEnumerable <YarnSpinnerLoader.NodeInfo> nodes)
        {
            var sb = new System.Text.StringBuilder();

            var properties = typeof(YarnSpinnerLoader.NodeInfo).GetProperties();

            foreach (var node in nodes)
            {
                foreach (var property in properties)
                {
                    // ignore the body attribute
                    if (property.Name == "body")
                    {
                        continue;
                    }

                    // piggy-back off the JsonIgnoreAttribute to sense items that should not be serialised
                    if (property.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).Length > 0)
                    {
                        continue;
                    }

                    var field = property.Name;

                    string value;

                    var propertyType = property.PropertyType;
                    if (propertyType.IsAssignableFrom(typeof(string)))
                    {
                        value = (string)property.GetValue(node, null);

                        // avoid storing nulls when we could store the empty string instead
                        if (value == null)
                        {
                            value = "";
                        }
                    }
                    else if (propertyType.IsAssignableFrom(typeof(int)))
                    {
                        value = ((int)property.GetValue(node, null)).ToString();
                    }
                    else if (propertyType.IsAssignableFrom(typeof(YarnSpinnerLoader.NodeInfo.Position)))
                    {
                        var position = (YarnSpinnerLoader.NodeInfo.Position)property.GetValue(node, null);

                        value = string.Format("{0},{1}", position.x, position.y);
                    }
                    else
                    {
                        MerinoDebug.LogFormat(LoggingLevel.Error, "Internal error: Node {0}'s property {1} has unsupported type {2}", node.title, property.Name, propertyType.FullName);

                        // will never be run, but prevents the compiler being mean about us not returning a value
                        throw new Exception();
                    }

                    var header = string.Format("{0}: {1}", field, value);

                    sb.Append(MerinoPrefs.lineEnding + header);
                }
                // now write the body

                // 5 May 2019, changed all AppendLine to use regular Append, with manual line breaks
                // this is to preserve compatibility with the base Yarn Editor, which doesn't like "\r\n" (which AppendLine uses)
                // see: https://github.com/radiatoryang/merino/issues/26

                sb.Append(MerinoPrefs.lineEnding + "---");

                sb.Append(MerinoPrefs.lineEnding + node.body);

                sb.Append(MerinoPrefs.lineEnding + "===");
            }

            return(sb.ToString());
        }
Exemple #17
0
        public static IList <MerinoTreeElement> GetDataFromFile(TextAsset source, int startID = 1, bool useFastMode = false)
        {
            var treeElements = new List <MerinoTreeElement>();

            if (!useFastMode)
            {
                AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(source)); // TODO: only reload assets that need it? how to do that
                //var format = YarnSpinnerLoader.GetFormatFromFileName(AssetDatabase.GetAssetPath(currentFile)); // TODO: add JSON and ByteCode support?
            }

            // ROOT: create a root node for the file itself
            var fileRoot = new MerinoTreeElement(source.name, 0, startID);

            fileRoot.leafType = MerinoTreeElement.LeafType.File;
            fileRoot.children = new List <TreeElement>();
            treeElements.Add(fileRoot);
            if (MerinoData.FileToNodeID.ContainsKey(source))
            {
                MerinoData.FileToNodeID[source] = startID;
            }
            else
            {
                MerinoData.FileToNodeID.Add(source, startID);
            }

            // load nodes

            // if there's no header sentinel in the text file, then just return an empty list
            if (!source.text.Contains("---"))
            {
                return(treeElements);
            }

            // otherwise, load nodes from file
            var nodes   = YarnSpinnerLoader.GetNodesFromText(source.text, NodeFormat.Text);
            var parents = new Dictionary <MerinoTreeElement, string>();

            foreach (var node in nodes)
            {
                // clean some of the stuff to help prevent file corruption
                string cleanName   = MerinoUtils.CleanYarnField(node.title, true);
                string cleanBody   = MerinoUtils.CleanYarnField(node.body);
                string cleanTags   = MerinoUtils.CleanYarnField(node.tags, true);
                string cleanParent = string.IsNullOrEmpty(node.parent) ? "" : MerinoUtils.CleanYarnField(node.parent, true);

                // write data to the objects
                var newItem = new MerinoTreeElement(cleanName, 0, startID + treeElements.Count);
                newItem.nodeBody     = cleanBody;
                newItem.nodePosition = new Vector2Int(node.position.x, node.position.y);
                newItem.nodeTags     = cleanTags;
                if (string.IsNullOrEmpty(cleanParent) || cleanParent == "Root")
                {
                    newItem.parent         = fileRoot;
                    newItem.cachedParentID = fileRoot.id;
                    fileRoot.children.Add(newItem);
                }
                else
                {
                    parents.Add(newItem, cleanParent);                     // we have to assign parents in a second pass later on, not right now
                }
                treeElements.Add(newItem);
            }

            // second pass: now that all nodes have been created, we can finally assign parents
            foreach (var kvp in parents)
            {
                var parent = treeElements.Find(x => x.name == kvp.Value);
                if (parent == null)
                {
                    MerinoDebug.LogFormat(LoggingLevel.Error, "Merino couldn't assign parent for node {0}: can't find a parent called {1}", kvp.Key.name, kvp.Value);
                }
                else
                {
                    // tell child about it's parent
                    kvp.Key.parent         = parent;
                    kvp.Key.cachedParentID = parent.id;
                    // tell parent about it's child
                    if (kvp.Key.parent.children == null)                     // init parent's list of children if not already initialized
                    {
                        kvp.Key.parent.children = new List <TreeElement>();
                    }
                    kvp.Key.parent.children.Add(kvp.Key);
                }
            }
            return(treeElements);
        }
Exemple #18
0
        // ensure unique node titles, very important for YarnSpinner
        private static void ValidateNodeTitles(List <MerinoTreeElement> nodes = null)
        {
            if (!MerinoPrefs.validateNodeTitles)
            {
                return;
            }

            if (nodes == null)             // if null, then let's just use all currently loaded nodes
            {
                nodes = MerinoData.TreeElements;
            }

            // make sure we're not doing this to any folder or file nodes, ONLY YARN NODES
            nodes = nodes.Where(x => x.leafType == MerinoTreeElement.LeafType.Node).ToList();

            // validate data: ensure unique node names
            var  nodeTitles     = new Dictionary <int, string>();      // index, newTitle
            var  duplicateCount = new Dictionary <string, int>();      // increment counter for each duplicate name, and use for rename suffix
            bool foundDuplicate = false;

            for (int i = 0; i < nodes.Count; i++)
            {
                // if there's a node already with that name, then append unique suffix
                if (nodeTitles.Values.Contains(nodes[i].name))
                {
                    // count duplicates
                    if (!duplicateCount.ContainsKey(nodes[i].name))
                    {
                        duplicateCount.Add(nodes[i].name, 2);
                    }

                    // when we make a new name, we have to ensure it's unique...
                    if (nodeTitles.ContainsKey(nodes[i].id) == false)
                    {
                        nodeTitles.Add(nodes[i].id, nodes[i].name);
                    }

                    nodeTitles[nodes[i].id] = nodes[i].name + "_" + duplicateCount[nodes[i].name]++;
                    foundDuplicate          = true;
                }                 // but if there's not already a node with that name, we should still make a note of it
                else if (nodeTitles.ContainsKey(nodes[i].id) == false)
                {
                    nodeTitles.Add(nodes[i].id, nodes[i].name);
                }
            }

            if (foundDuplicate)
            {
                string renamedNodes = "Merino found nodes with duplicate names (which aren't allowed for Yarn) and renamed them. This might break node links, you can undo it. The following nodes were renamed: ";
                Undo.RecordObject(MerinoData.Instance, "Merino: AutoRename");

                foreach (var kvp in nodeTitles)
                {
                    if (MerinoData.TreeElements[kvp.Key].name != kvp.Value)
                    {
                        renamedNodes += string.Format("\n* {0} > {1}", MerinoData.TreeElements[kvp.Key].name, kvp.Value);
                        MerinoData.TreeElements[kvp.Key].name = kvp.Value;
                    }
                }
                EditorUtility.SetDirty(MerinoData.Instance);
                MerinoDebug.Log(LoggingLevel.Warning, renamedNodes);
                //todo: repaint MerinoEditorWindow tree view so names get updated

                // this is bad, but we're gonna do some recursion here, just to make extra sure there's STILL no duplicates...
                ValidateNodeTitles(MerinoData.TreeElements);
            }
            else if (MerinoPrefs.useAutosave)
            {
                SaveDataToFiles();
            }
        }
        void DrawToolbar(Event e)
        {
            EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);

            if (validDialogue)
            {
                GUILayout.Space(2);                 //small space to mimic unity editor

                // jump to node button
                var jumpOptions      = dialogue.allNodes.ToList();
                int currentJumpIndex = jumpOptions.IndexOf(dialogue.currentNode);
                if (!IsDialogueRunning)
                {
                    // if there is no current node, then inform the user that
                    currentJumpIndex = 0;
                    jumpOptions.Insert(0, "<Stopped> Jump to Node?...");
                }
                GUI.SetNextControlName(popupControl);
                int newJumpIndex = EditorGUILayout.Popup(
                    new GUIContent("", "The node you're currently playing; you can also jump to any other node."),
                    currentJumpIndex,
                    jumpOptions.Select(x => x.StartsWith("<Stopped>") ? new GUIContent(x) : new GUIContent("Node: " + x)).ToArray(),
                    EditorStyles.toolbarDropDown,
                    GUILayout.Width(160));
                if (currentJumpIndex != newJumpIndex)
                {
                    if (IsDialogueRunning || newJumpIndex > 0)
                    {
                        PlaytestFrom_Internal(jumpOptions[newJumpIndex], false);
                    }
                }
                GUILayout.Space(6);

                GUI.enabled = IsDialogueRunning;                 // disable if dialogue isn't running
                // attempt to get current node
                var matchingNode = MerinoData.GetNode(dialogue.currentNode);
                if (lastFileParent > 0)                     // if we know the file where the playtest started, we can be more specific
                {
                    matchingNode = MerinoData.GetAllCachedChildren(lastFileParent).Find(node => node.name == dialogue.currentNode);
                    // ok if that search failed for some reason, then just give up and fallback
                    if (matchingNode == null)
                    {
                        matchingNode = MerinoData.GetNode(dialogue.currentNode);
                    }
                }
                var content = new GUIContent(" View Node Source", MerinoEditorResources.TextAsset, "Click to see Yarn script code for this node.");
                if (GUILayout.Button(content, EditorStyles.toolbarButton, GUILayout.Width(EditorStyles.toolbarButton.CalcSize(content).x - 40)))
                {
                    if (matchingNode != null)
                    {
                        // display in yarn editor window
                        GetWindow <MerinoEditorWindow>(MerinoEditorWindow.windowTitle, true).
                        SelectNodeAndZoomToLine(matchingNode.id, GuessLineNumber(matchingNode.id, displayStringFull));
                    }
                    else
                    {
                        MerinoDebug.LogFormat(LoggingLevel.Warning, "Merino culdn't find any node called {0}. It might've been deleted or the Yarn file is corrupted.", dialogue.currentNode);
                    }
                }
                if (GUILayout.Button(new GUIContent(" View in Node Map", MerinoEditorResources.Nodemap, "Click to see this node in the node map window."), EditorStyles.toolbarButton))
                {
                    if (matchingNode != null)
                    {
                        MerinoNodemapWindow.GetNodemapWindow().FocusNode(matchingNode.id);
                    }
                    else
                    {
                        MerinoDebug.LogFormat(LoggingLevel.Warning, "Merino couldn't find any node called {0}. It might've been deleted or the Yarn file is corrupted.", dialogue.currentNode);
                    }
                }
                GUI.enabled = true;
            }

            GUILayout.FlexibleSpace();

            // playtesting preferences popup
            if (GUILayout.Button(new GUIContent("Preferences"), EditorStyles.toolbarDropDown))
            {
                PopupWindow.Show(prefButtonRect, new PlaytestPrefsPopup(IsDocked()));
            }
            //grab popup button's rect do determine the height of the toolbar
            if (e.type == EventType.Repaint)
            {
                prefButtonRect = GUILayoutUtility.GetLastRect();
            }

            GUILayout.Space(2);             //small space to mimic unity editor

            EditorGUILayout.EndHorizontal();

            // eat clicks
            if (e.type == EventType.MouseDown)
            {
                // clicked on toolbar
                if (e.mousePosition.y < prefButtonRect.height)
                {
                    e.Use();
                }

                // clicked out of popup
                if (GUI.GetNameOfFocusedControl() == popupControl)
                {
                    e.Use();
                    GUI.FocusControl(null);
                }
            }
        }