//============= Copyright (c) Ludic GmbH, All rights reserved. ============== // // Purpose: Part of the My Behaviour Tree Code // //============================================================================= using System.Collections; using System.Collections.Generic; using System.Text; using System; #if UNITY_EDITOR using UnityEditor; using UnityEditor.SceneManagement; #endif using UnityEngine; namespace MyBT { public class CodeInspector : ScriptableObject { #if UNITY_EDITOR #region type definitions [System.Serializable] public class DictionaryOfIntAndNodeList : MyBT.SerializableDictionary { } #endregion [SerializeField] private List openNodeLines; private List closeNodeLines; public SerializedProperty openNodeLinesAsSerializedProperty { get { SerializedObject sObj = new SerializedObject(this); return sObj.FindProperty("openNodeLines"); } } [SerializeField] private TaskController taskController; public void Init(TaskController _taskController) { taskController = _taskController; } public bool hasTaskControllerDefined { get { return (taskController != null); } } private string nodeLineNumberListGenerationState { get { return (openNodeLines != null) + "&&" + (closeNodeLines != null); } } public int nodeLineCount { get { int sum = 0; if (openNodeLines != null) { sum += openNodeLines.Count; } if (closeNodeLines != null) { sum += closeNodeLines.Count; } return sum; } } [SerializeField] public FileHash __lineNumberListBtFileHashes = new FileHash(); public FileHash lineNumberListBtFileHashes { get { return __lineNumberListBtFileHashes; } private set { __lineNumberListBtFileHashes = value; } } public bool CheckLineNumberHashOutdated(bool allowNull = false) { if (taskController != null) { return lineNumberListBtFileHashes.HashChanged(taskController.generatedCodeBtFileHashes, allowNull); } Debug.LogWarning("CheckLineNumberHashOutdated: taskController is undefined!"); if (nodeLineNumberListError) { return true; } return true; } public string GetLineNumberHashes () { return $"codeHash: {lineNumberListBtFileHashes} fileHash:{taskController.generatedCodeBtFileHashes} "; } public void ClearLineNumberHashes() { lineNumberListBtFileHashes.ClearHashCache(); } private bool __nodeLineNumberListError = false; public bool nodeLineNumberListError { get { return __nodeLineNumberListError; } private set { __nodeLineNumberListError = value; } } public bool nodeLineNumberListGenerated { get { if ((openNodeLines != null) && (closeNodeLines != null)) { int tokenCount = taskController.tokens != null ? taskController.tokens.Count : 0; bool generated = (tokenCount == openNodeLines.Count) && (tokenCount == closeNodeLines.Count); return generated; } else { return false; } } } private void UpdateLineNumberHash() { lineNumberListBtFileHashes.UpdateHashCache(taskController.generatedCodeBtFileHashes); } public void ResetNodeLinenumberList() { if (taskController != null) { if (taskController.generatorLogging) Debug.Log("CodeInspector.ResetNodeLinenumberList"); } nodeLineNumberListError = false; openNodeLines = null; closeNodeLines = null; ClearLineNumberHashes(); } public new void SetDirty() { if (!Application.isPlaying) { if (taskController != null) { EditorSceneManager.MarkSceneDirty(taskController.gameObject.scene); EditorUtility.SetDirty(taskController); taskController.SetBehaviourTreeDirty(); } } } public void RegenerateNodeLineNumberListIfRequired (bool forceUpdate = false) { if (CheckLineNumberHashOutdated() || forceUpdate) { if (taskController.generatorLogging) { Debug.LogWarning($"TaskController.Update: regenerating behaviour tree {GetLineNumberHashes()} {forceUpdate}"); } ResetNodeLinenumberList(); GenerateNodeLineNumberList(); } } public void GenerateNodeLineNumberList() { if (taskController) { if (!taskController.generationError && !taskController.bindingError && CheckLineNumberHashOutdated() && !nodeLineNumberListGenerated) { if (taskController.generatorLogging) Debug.Log($"CodeInspector.GenerateNodeLineNumberList: running " + $"(!{taskController.generationError}&&!{taskController.bindingError}&&{CheckLineNumberHashOutdated()}&&!{nodeLineNumberListGenerated})"); openNodeLines = new List(); closeNodeLines = new List(); foreach (NodeList treeRoots in taskController.treeRootNodes) { // Debug.Log("Handling "+treeRoots); DictionaryOfIntAndNodeList openNodeLine = new DictionaryOfIntAndNodeList(); DictionaryOfIntAndNodeList closeNodeLine = new DictionaryOfIntAndNodeList(); foreach (Node treeNode in treeRoots) { // Debug.Log("- - "+treeNode); foreach (Node childNode in treeNode.getAllNodesRecursively(false)) { // Debug.Log("- - - "+childNode); if (!openNodeLine.ContainsKey(childNode.lineNumber)) { // Debug.Log("- - - new openNodeLine: "+childNode.lineNumber); openNodeLine[childNode.lineNumber] = new NodeList(); } openNodeLine[childNode.lineNumber].Add(childNode); if (!closeNodeLine.ContainsKey(childNode.lastLineNumber)) { // Debug.Log("- - - new closeNodeLine: "+childNode.lineNumber); closeNodeLine[childNode.lastLineNumber] = new NodeList(); } closeNodeLine[childNode.lastLineNumber].Add(childNode); } } openNodeLines.Add(openNodeLine); closeNodeLines.Add(closeNodeLine); } if (nodeLineNumberListGenerated) { UpdateLineNumberHash(); // Debug.Log("CodeInspector.GenerateNodeLineNumberList: generated " + lineNumberListBtFileHashes); } else { Debug.LogError("CodeInspector.GenerateNodeLineNumberList: failed" + lineNumberListBtFileHashes); } } else { if (taskController.generatorLogging) Debug.Log($"CodeInspector.GenerateNodeLineNumberList: not run (!{taskController.generationError} && {CheckLineNumberHashOutdated()} &&! {nodeLineNumberListGenerated} )"); } } } public void DrawCodeInspector() { // string codeDisplayLog = ""; EditorGUILayout.BeginVertical(EditorStyles.helpBox); StringBuilder errorMessageSB = new StringBuilder(); if (taskController == null) { errorMessageSB.Append("TaskController undefined! "); } else { if (taskController.generationError) { errorMessageSB.Append("generationError! "); } if (taskController.bindingError) { errorMessageSB.Append("generationError! "); } if (!taskController.hasScript) { errorMessageSB.Append("Missing Behaviour Tree Script! "); } } if (nodeLineNumberListError) { errorMessageSB.Append("nodeLineNumberListError! "); } if (errorMessageSB.Length > 0) { GUI.contentColor = Color.red; GUILayout.TextArea(errorMessageSB.ToString()); GUI.contentColor = Color.black; } // create a style based on the default label style5 GUIStyle myButtonStyle = new GUIStyle(GUI.skin.button); // do whatever you want with this style, e.g.: myButtonStyle.margin = new RectOffset(0, 0, -3, 0); myButtonStyle.border = new RectOffset(0, 0, 0, 0); myButtonStyle.overflow = new RectOffset(-4, -4, -4, -4); myButtonStyle.fixedHeight = 25; myButtonStyle.fixedWidth = 25; // GUISkin guiSkin = (GUISkin)AssetDatabase.LoadAssetAtPath("Assets/Editor Default Resources/MyBTSkin.guiskin", typeof(GUISkin)); // if (guiSkin == null) { // Debug.LogError($"GUISkin for BT not found MyBTSkin"); // } // AssetDatabase.FindAssets("MyBTSkin") as GUISkin; // GUISkin guiSkin = MyBtResources.myBtGuiSkin; bool btSettingsChanged = false; //Debug.Log($"--- CODE INSPECTOR --- {taskController.gameObject.name} --- {Time.frameCount} -------------------------------------"); if (taskController != null) { if (nodeLineNumberListGenerated && !nodeLineNumberListError) { // for "the number of task scripts" for (int i = 0; i < taskController.taskScripts.Count; i++) { if (taskController.taskScripts[i] != null) { EditorGUILayout.LabelField(("_ Script _ " + taskController.taskScripts[i].name + " ").PadRight(120, '_')); // EditorGUILayout.LabelField(taskController.taskScripts[i].name); TokenList tokens = taskController.tokens[i]; DictionaryOfIntAndNodeList openNodeLine = openNodeLines[i]; DictionaryOfIntAndNodeList closeNodeLine = closeNodeLines[i]; // get the line number of the last token int numberOfLines = tokens[tokens.Count - 1].location.line; // codeDisplayLog += $"- numberOfLines {numberOfLines}\n"; int tokenIndex = 0; // int nodeDepthCached = 0; // bool foldCache = false; // int foldAbove = int.MaxValue; Node lastNodeCache = null; Node thisNode = null; bool isFolded = false; StringBuilder sbLineText = new StringBuilder(200); string lineText; StringBuilder sbLineNumber = new StringBuilder(4); string lineNumberText; NodeList logNodes = new NodeList(); bool isClosing; // isOpening int nodeDepth = 0; for (int currentLineNumber = 1; currentLineNumber < numberOfLines + 1; currentLineNumber++) { // codeDisplayLog += $"iterate line {currentLineNumber}\n"; sbLineNumber.Clear(); lineNumberText = sbLineNumber.AppendFormat("{0:D4}", tokens[tokenIndex].location.line).ToString(); // lineNumberText = String.Format("{0:D4}", tokens[tokenIndex].location.line); thisNode = null; isClosing = false; // isOpening = false; if (closeNodeLine.ContainsKey(currentLineNumber)) { lastNodeCache = closeNodeLine[currentLineNumber][0]; isClosing = true; nodeDepth -= 1; } // generate text of code to display by iterating trough the tokens sbLineText.Clear(); if (nodeDepth >= 0) sbLineText.Append(' ', nodeDepth * 4); while ((tokenIndex < tokens.Count) && (tokens[tokenIndex].location.line == currentLineNumber)) { sbLineText.Append(tokens[tokenIndex].location.code.Replace("\r", "").Replace("\n", "").Replace("\t", "").TrimStart(' ').TrimEnd(' ')); tokenIndex += 1; // Debug.Log("tokenIndex "+tokenIndex + " on line "+ currentLineNumber + " " + s); } lineText = sbLineText.ToString(); // + $"{nodeDepth}"; // find depth of current Node if (openNodeLine.ContainsKey(currentLineNumber)) { lastNodeCache = openNodeLine[currentLineNumber][0]; thisNode = openNodeLine[currentLineNumber][0]; if (thisNode != null) { if ((thisNode.GetType() == typeof(CompositeNode)) || (thisNode.GetType() == typeof(TreeNode)) || (thisNode.GetType() == typeof(DecoratorNode)) ) { // isOpening = true; nodeDepth += 1; } } // thisnode is a weakreference, and is deleted externally on script change // https://docs.unity3d.com/Manual/script-Serialization.html else { // the object has been destroyed Debug.LogWarning($"DrawCodeInspector: detected error on line {currentLineNumber} -> clearing nodelinenumberlists"); //requireResetNodeLinenumberList = true; nodeLineNumberListError = true; } } // if this line contains a node, we fold it when the parent is folded. because a composite node would be invisible if it hides itself. isFolded = (lastNodeCache != null) ? lastNodeCache.parentFolded : false; // this node is undefined, it is a comment, a closing bracket or a empty line. in this case use the lastNodecache.isFolded, as we are surely not a composite node if (thisNode == null) { // dont fold closing brackets if (isClosing) { if (lastNodeCache != null) { if (lastNodeCache.parentNode != null) { isFolded = lastNodeCache.parentFolded; } } } else { if (lastNodeCache != null) { // fold comments in composites isFolded = lastNodeCache.isFolded || isFolded; } } if (nodeDepth == 0) { isFolded = false; } } if (!isFolded) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(lineNumberText, GUILayout.Width(32)); // dont show fold on ActionNode & RunTreeNode if ((thisNode != null) && (thisNode.GetType() != typeof(ActionNode)) && (thisNode.GetType() != typeof(RunTreeNode))) { bool newIsFolded = EditorGUILayout.Toggle(thisNode.isFolded, MyBtResources.popoutToggleGuiStyle, GUILayout.MaxWidth(16)); // label, if (newIsFolded != thisNode.isFolded) { thisNode.isFolded = newIsFolded; btSettingsChanged |= true; } } else { EditorGUILayout.LabelField("", MyBtResources.emptyGuiStyle, GUILayout.Width(MyBtResources.defaultGuiIconWidth), GUILayout.Height(MyBtResources.defaultGuiHeight)); } EditorGUILayout.LabelField(lineText, GUILayout.ExpandWidth(true), GUILayout.Height(MyBtResources.defaultGuiHeight)); // logNodes = new NodeList(); logNodes.Clear(); // list of nodes that should show logging if (thisNode != null) { foreach (Node lineNode in openNodeLine[currentLineNumber]) { if ((lineNode != null) && (lineNode.nodeRuntimeDataList != null)) { //foreach (NodeRuntimeData runtimeData in lineNode.nodeRuntimeDataList) { // if (runtimeData != null) { // //GUI.color = MyBtResources.NodeStateColors[runtimeData.nodeState]; // //EditorGUILayout.LabelField("", MyBtResources.NodeStateGuiStyle[runtimeData.nodeState], GUILayout.Width(defaultGuiIconWidth), GUILayout.Height(defaultGuiHeight)); // GUI.color = MyBtResources.NodeResultColors[runtimeData.nodeResult]; // EditorGUILayout.LabelField($"R{lineNode.nodeRuntimeDataList.Count}", MyBtResources.NodeResultGuiStyle[runtimeData.nodeResult], GUILayout.Width(defaultGuiIconWidth), GUILayout.Height(defaultGuiHeight)); // } //} //Debug.Log(lineNode.NodeLogger("DEBUG", "")); if ((lineNode.lastResults != null) && (taskController.UiChangedAt(lineNode.lastResultsTime))) { for (int j=0; j< lineNode.lastResults.Count; j++) { foreach (NodeResult nrs in lineNode.lastResults[j]) { GUI.color = MyBtResources.NodeResultColors[nrs]; EditorGUILayout.LabelField("", MyBtResources.NodeResultGuiStyle[nrs], GUILayout.Width(MyBtResources.defaultGuiIconWidth), GUILayout.Height(MyBtResources.defaultGuiHeight)); } //for (int k = 0; k < lineNode.lastResults[j].Count; k++) { //GUI.color = MyBtResources.NodeResultColors[lineNode.lastResults[j][k]]; //EditorGUILayout.LabelField("", MyBtResources.NodeResultGuiStyle[lineNode.lastResults[j][k]], GUILayout.Width(defaultGuiIconWidth), GUILayout.Height(defaultGuiHeight)); //} //lineNode.lastResults[j] = NodeResult.Undefined; } //foreach (NodeResult lastResult in lineNode.lastResults) { //} } else { EditorGUILayout.LabelField("", MyBtResources.nodeUninitializedState, GUILayout.Width(MyBtResources.defaultGuiIconWidth), GUILayout.Height(MyBtResources.defaultGuiHeight)); } GUI.color = lineNode.logStringDisplay ? Color.yellow : Color.white; bool newLogStringDisplay = EditorGUILayout.Toggle(lineNode.logStringDisplay, MyBtResources.logStringToggleGuiStyle, GUILayout.Width(MyBtResources.defaultGuiIconWidth), GUILayout.Height(MyBtResources.defaultGuiHeight)); if (newLogStringDisplay != lineNode.logStringDisplay) { lineNode.logStringDisplay = newLogStringDisplay; btSettingsChanged |= true; } if (lineNode.logStringDisplay || lineNode.debugChangesActive) { logNodes.Add(lineNode); } if (taskController.runtimeLogging) { GUI.color = lineNode.debugChangesActive ? Color.cyan : Color.white; bool newDebugChangesActive = EditorGUILayout.Toggle(lineNode.debugChangesActive, MyBtResources.alertToggleGuiStyle, GUILayout.Width(MyBtResources.defaultGuiIconWidth), GUILayout.Height(MyBtResources.defaultGuiHeight)); if (newDebugChangesActive != lineNode.debugChangesActive) { lineNode.debugChangesActive = newDebugChangesActive; btSettingsChanged |= true; } GUI.color = lineNode.debugInternalActive ? Color.red : Color.white; bool newDebugInternalActive = EditorGUILayout.Toggle(lineNode.debugInternalActive, MyBtResources.debugToggleGuiStyle, GUILayout.Width(MyBtResources.defaultGuiIconWidth), GUILayout.Height(MyBtResources.defaultGuiHeight)); if (newDebugInternalActive != lineNode.debugInternalActive) { lineNode.debugInternalActive = newDebugInternalActive; btSettingsChanged |= true; } } } } GUI.color = Color.white; } else { // to fix layout lines with a comment Rect iconRect2 = GUILayoutUtility.GetRect(GUIContent.none, new GUIStyle(), new GUILayoutOption[] { GUILayout.Width(MyBtResources.defaultGuiIconWidth), GUILayout.Height(MyBtResources.defaultGuiHeight) }); iconRect2.height += 2; iconRect2.y += 1; iconRect2.x += 1; } EditorGUILayout.EndHorizontal(); if (logNodes.Count > 0) { foreach (Node logNode in logNodes) { foreach (NodeRuntimeData runtimeData in logNode.nodeRuntimeDataList) { if (runtimeData != null) { GUI.color = Color.yellow; if (logNode.logStringDisplay) { if (logNode is ActionNode) { EditorGUILayout.BeginHorizontal(); string logString = (runtimeData.logString != null) ? runtimeData.logString.ToString().Replace("\n", "\\n") : ""; EditorGUILayout.LabelField("logs: " + logString); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); ActionNodeRuntimeData anrd = (runtimeData as ActionNodeRuntimeData); if (anrd != null) { string userDataString = (anrd.userData != null) ? anrd.userData.ToString().Replace("\n", "\\n") : ""; EditorGUILayout.LabelField("data: " + userDataString); } EditorGUILayout.EndHorizontal(); } } if (logNode.debugChangesActive) { EditorGUILayout.BeginHorizontal(); string statusString = $"state={runtimeData.nodeState} result={runtimeData.nodeResult}"; if (logNode is CompositeNode) { statusString += $" curIndex={runtimeData.currentIndex}"; } EditorGUILayout.LabelField("" + statusString); EditorGUILayout.EndHorizontal(); } GUI.color = Color.white; } } } } } } } } } else { string logMessage = $"CodeInspector.DrawCodeInspector: not drawing ({nodeLineNumberListGenerated}) ({nodeLineNumberListGenerationState})"; GUI.contentColor = Color.red; EditorGUILayout.LabelField(logMessage); GUI.contentColor = Color.black; } if (btSettingsChanged) { SetDirty(); } // GUILayout.Label(codeDisplayLog); } EditorGUILayout.EndVertical(); } #endif } }