├── .gitattributes ├── .gitignore ├── Editor.meta ├── Editor ├── AssetUtility.cs ├── AssetUtility.cs.meta ├── CHM.VisualScriptingKai.Editor.asmdef ├── CHM.VisualScriptingKai.Editor.asmdef.meta ├── ExceptionTrace.cs ├── ExceptionTrace.cs.meta ├── FlowCoroutineUnitDescriptor.cs ├── FlowCoroutineUnitDescriptor.cs.meta ├── FunctionUnitAnalyser.cs ├── FunctionUnitAnalyser.cs.meta ├── FunctionUnitDescriptor.cs ├── FunctionUnitDescriptor.cs.meta ├── GraphAnalyzerWindow.cs ├── GraphAnalyzerWindow.cs.meta ├── GraphAssetListTracker.cs ├── GraphAssetListTracker.cs.meta ├── GraphDebuggerWindow.cs ├── GraphDebuggerWindow.cs.meta ├── GraphLensWindow.cs ├── GraphLensWindow.cs.meta ├── GraphQueryWindowBase.cs ├── GraphQueryWindowBase.cs.meta ├── GraphRecursionContext.cs ├── GraphRecursionContext.cs.meta ├── GraphSource.cs ├── GraphSource.cs.meta ├── GraphTraversalUtility_StateTransitions.cs ├── GraphTraversalUtility_StateTransitions.cs.meta ├── GraphTraversalUtility_States.cs ├── GraphTraversalUtility_States.cs.meta ├── GraphTraversalUtility_StickyNotes.cs ├── GraphTraversalUtility_StickyNotes.cs.meta ├── GraphTraversalUtility_Units.cs ├── GraphTraversalUtility_Units.cs.meta ├── GraphUtility.cs ├── GraphUtility.cs.meta ├── IGraphElementTrace.cs ├── IGraphElementTrace.cs.meta ├── ListUtility.cs ├── ListUtility.cs.meta ├── NodeTrace.cs ├── NodeTrace.cs.meta ├── PackageUtility.cs ├── PackageUtility.cs.meta ├── QueryResultsListView.cs ├── QueryResultsListView.cs.meta ├── Resources.meta ├── Resources │ ├── GraphAnalyzerWindow.uxml │ ├── GraphAnalyzerWindow.uxml.meta │ ├── GraphDebuggerWindow.uxml │ ├── GraphDebuggerWindow.uxml.meta │ ├── GraphLensWindow.uxml │ ├── GraphLensWindow.uxml.meta │ ├── QueryResultEntry.uxml │ ├── QueryResultEntry.uxml.meta │ ├── QueryResultsListView.uxml │ └── QueryResultsListView.uxml.meta ├── StateTrace.cs ├── StateTrace.cs.meta ├── StateTransitionTrace.cs ├── StateTransitionTrace.cs.meta ├── StickyNoteTrace.cs ├── StickyNoteTrace.cs.meta ├── StringUtility.cs ├── StringUtility.cs.meta ├── TestUnitDescriptor.cs ├── TestUnitDescriptor.cs.meta ├── TestUnitWidget.cs ├── TestUnitWidget.cs.meta ├── UnitUtility.cs ├── UnitUtility.cs.meta ├── WarningTrace.cs └── WarningTrace.cs.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── CHM.VisualScriptingKai.asmdef ├── CHM.VisualScriptingKai.asmdef.meta ├── FlowCoroutineIsRunningUnit.cs ├── FlowCoroutineIsRunningUnit.cs.meta ├── FlowCoroutineManager.cs ├── FlowCoroutineManager.cs.meta ├── FlowCoroutineStartUnit.cs ├── FlowCoroutineStartUnit.cs.meta ├── FlowCoroutineStopAllUnit.cs ├── FlowCoroutineStopAllUnit.cs.meta ├── FlowCoroutineStopUnit.cs ├── FlowCoroutineStopUnit.cs.meta ├── FlowCoroutineWaitUnit.cs ├── FlowCoroutineWaitUnit.cs.meta ├── FunctionCallStack.cs ├── FunctionCallStack.cs.meta ├── FunctionCallUnit.cs ├── FunctionCallUnit.cs.meta ├── FunctionCheckUnit.cs ├── FunctionCheckUnit.cs.meta ├── FunctionImplementation.cs ├── FunctionImplementation.cs.meta ├── FunctionLookupTable.cs ├── FunctionLookupTable.cs.meta ├── FunctionReturnUnit.cs ├── FunctionReturnUnit.cs.meta ├── FunctionStartUnit.cs ├── FunctionStartUnit.cs.meta ├── FunctionUtility.cs ├── FunctionUtility.cs.meta ├── HasFunctionUnit.cs ├── HasFunctionUnit.cs.meta ├── IFunctionUnit.cs ├── IFunctionUnit.cs.meta ├── TestUnit.cs └── TestUnit.cs.meta ├── Samples~ ├── FunctionDemo.meta └── FunctionDemo │ ├── Bullet.prefab │ ├── Bullet.prefab.meta │ ├── FunctionDemo.unity │ ├── FunctionDemo.unity.meta │ ├── GetHP.asset │ ├── GetHP.asset.meta │ ├── SetBulletParameters.asset │ ├── SetBulletParameters.asset.meta │ ├── TakeDamage.asset │ └── TakeDamage.asset.meta ├── Tests.meta ├── Tests ├── Editor.meta └── Editor │ ├── CHM.VisualScriptingKai.Editor.Tests.asmdef │ ├── CHM.VisualScriptingKai.Editor.Tests.asmdef.meta │ ├── TestScene.unity │ ├── TestScene.unity.meta │ ├── Test_GraphUtility.cs │ └── Test_GraphUtility.cs.meta ├── package.json └── package.json.meta /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Uu]ser[Ss]ettings/ 12 | 13 | # MemoryCaptures can get excessive in size. 14 | # They also could contain extremely sensitive data 15 | /[Mm]emoryCaptures/ 16 | 17 | # Recordings can get excessive in size 18 | /[Rr]ecordings/ 19 | 20 | # Uncomment this line if you wish to ignore the asset store tools plugin 21 | # /[Aa]ssets/AssetStoreTools* 22 | 23 | # Autogenerated Jetbrains Rider plugin 24 | /[Aa]ssets/Plugins/Editor/JetBrains* 25 | 26 | # Visual Studio cache directory 27 | .vs/ 28 | 29 | # Gradle cache directory 30 | .gradle/ 31 | 32 | # Autogenerated VS/MD/Consulo solution and project files 33 | ExportedObj/ 34 | .consulo/ 35 | *.csproj 36 | *.unityproj 37 | *.sln 38 | *.suo 39 | *.tmp 40 | *.user 41 | *.userprefs 42 | *.pidb 43 | *.booproj 44 | *.svd 45 | *.pdb 46 | *.mdb 47 | *.opendb 48 | *.VC.db 49 | 50 | # Unity3D generated meta files 51 | *.pidb.meta 52 | *.pdb.meta 53 | *.mdb.meta 54 | 55 | # Unity3D generated file on crash reports 56 | sysinfo.txt 57 | 58 | # Builds 59 | *.apk 60 | *.aab 61 | *.unitypackage 62 | *.app 63 | 64 | # Crashlytics generated file 65 | crashlytics-build.properties 66 | 67 | # Packed Addressables 68 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 69 | 70 | # Temporary auto-generated Android Assets 71 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 72 | /[Aa]ssets/[Ss]treamingAssets/aa/* 73 | 74 | # Convenient aliases 75 | Samples 76 | Samples.meta 77 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 64667a551908fbd42aac022a8c87cebe 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/AssetUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEditor; 5 | 6 | namespace CHM.VisualScriptingKai.Editor 7 | { 8 | public static class AssetUtility 9 | { 10 | public static IEnumerable FindAssetsByType(string[] searchInFolders = null) where T : Object 11 | { 12 | // Do this check ourselves so AssetDatabase.FindAssets won't throw. 13 | if(searchInFolders != null) 14 | { 15 | foreach(string folder in searchInFolders) 16 | { 17 | if(!AssetDatabase.IsValidFolder(folder)) 18 | yield break; 19 | } 20 | } 21 | foreach(var guid in AssetDatabase.FindAssets($"t:{typeof(T)}", searchInFolders)) 22 | { 23 | string assetPath = AssetDatabase.GUIDToAssetPath(guid); 24 | T asset = AssetDatabase.LoadAssetAtPath(assetPath); 25 | if(asset != null) 26 | yield return asset; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Editor/AssetUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 85bf4be37eadb8f488b7a1e1df8270e8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/CHM.VisualScriptingKai.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CHM.VisualScriptingKai.Editor", 3 | "rootNamespace": "CHM.VisualScriptingKai.Editor", 4 | "references": [ 5 | "GUID:21b0c8d1703a94250bfac916590cea4f", 6 | "GUID:54cb1906aeb4e4264bc1d2aa3818a43f", 7 | "GUID:ea715009a4efd4c6cbc85be3ae097dd3", 8 | "GUID:23f3c6063e13e4fd9addce3606ff4e42", 9 | "GUID:315d634a9ac6d460a9515f5a56be6311", 10 | "GUID:f10496e9d78b943308af5e7e9e4543c8", 11 | "GUID:6d764832868c40c48ba87789bfceec2f" 12 | ], 13 | "includePlatforms": [ 14 | "Editor" 15 | ], 16 | "excludePlatforms": [], 17 | "allowUnsafeCode": false, 18 | "overrideReferences": false, 19 | "precompiledReferences": [], 20 | "autoReferenced": true, 21 | "defineConstraints": [], 22 | "versionDefines": [], 23 | "noEngineReferences": false 24 | } -------------------------------------------------------------------------------- /Editor/CHM.VisualScriptingKai.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6797e45ff6d060c4894f1b0f2dff6134 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/ExceptionTrace.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Unity.VisualScripting; 5 | using UnityEngine; 6 | 7 | namespace CHM.VisualScriptingKai.Editor 8 | { 9 | public struct ExceptionTrace : IGraphElementTrace 10 | { 11 | public readonly IGraphElement GraphElement => unit; 12 | public IUnit unit; 13 | public GraphReference Reference { get; set; } 14 | public GraphSource Source { get; set; } 15 | public long Score { get; set; } 16 | public readonly Vector2 GraphPosition => unit.position; 17 | public readonly int CompareTo(IGraphElementTrace other) 18 | { 19 | // Deeper nodes go first. Then sort by title. 20 | int compare = -Reference.parentElementGuids.Count().CompareTo(other.Reference.parentElementGuids.Count()); 21 | if(compare != 0) 22 | return compare; 23 | return GraphElement.Description().title.CompareTo(other.Description().title); 24 | } 25 | public readonly string GetInfo() 26 | { 27 | var exception = unit.GetException(Reference); 28 | return $"{unit.Name()}" 29 | + $"\n{Source.ShortInfo}" 30 | + $"\n{exception.GetType()}: {exception.Message}"; 31 | } 32 | public readonly Texture2D GetIcon(int resolution) 33 | { 34 | // Cursed operator overload. Gets texture with resolution. 35 | return unit.Icon()[resolution]; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Editor/ExceptionTrace.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 37573c696b1e25f429b669968bf93602 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/FlowCoroutineUnitDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using Unity.VisualScripting; 4 | using UnityEngine; 5 | 6 | namespace CHM.VisualScriptingKai.Editor 7 | { 8 | public class FlowCoroutineUnitDescriptor : UnitDescriptor 9 | where T : Unit 10 | { 11 | public FlowCoroutineUnitDescriptor(T target) : base(target) 12 | { 13 | } 14 | 15 | protected override EditorTexture DefinedIcon() 16 | { 17 | return typeof(Coroutine).Icon(); 18 | } 19 | } 20 | 21 | [Descriptor(typeof(FlowCoroutineStartUnit))] 22 | public class FlowCoroutineStartUnitDescriptor : FlowCoroutineUnitDescriptor 23 | { 24 | public FlowCoroutineStartUnitDescriptor(FlowCoroutineStartUnit target) : base(target) 25 | { 26 | } 27 | 28 | protected override string DefinedSummary() 29 | { 30 | return "Starts a new Flow Coroutine, independent from the flow that lead to this node." 31 | + "\nYou may want to use the Sequence node to do other stuff after starting the coroutine."; 32 | } 33 | 34 | protected override void DefinedPort(IUnitPort port, UnitPortDescription description) 35 | { 36 | base.DefinedPort(port, description); 37 | if (port == target.flowCoroutine) 38 | { 39 | description.icon = typeof(Coroutine).Icon(); 40 | description.summary = "The new coroutine as a Flow object." 41 | + "This can be used by other Flow Coroutine nodes like Stop Flow Coroutine and Wait For Flow Coroutine."; 42 | } 43 | else if (port == target.coroutineStart) 44 | { 45 | description.summary = "The flow from here on out is a coroutine, independent from the previous flow."; 46 | } 47 | } 48 | } 49 | 50 | [Descriptor(typeof(FlowCoroutineStopUnit))] 51 | public class FlowCoroutineStopUnitDescriptor : FlowCoroutineUnitDescriptor 52 | { 53 | public FlowCoroutineStopUnitDescriptor(FlowCoroutineStopUnit target) : base(target) 54 | { 55 | } 56 | 57 | protected override string DefinedSummary() 58 | { 59 | return "Stops the given Flow Coroutine."; 60 | } 61 | 62 | protected override void DefinedPort(IUnitPort port, UnitPortDescription description) 63 | { 64 | base.DefinedPort(port, description); 65 | if (port == target.flowCoroutine) 66 | { 67 | description.icon = typeof(Coroutine).Icon(); 68 | description.summary = "The coroutine to stop."; 69 | } 70 | } 71 | } 72 | 73 | [Descriptor(typeof(FlowCoroutineStopAllUnit))] 74 | public class FlowCoroutineStopAllUnitDescriptor : FlowCoroutineUnitDescriptor 75 | { 76 | public FlowCoroutineStopAllUnitDescriptor(FlowCoroutineStopAllUnit target) : base(target) 77 | { 78 | } 79 | 80 | protected override string DefinedSummary() 81 | { 82 | return "Stops all running Flow Coroutines."; 83 | } 84 | } 85 | 86 | [Descriptor(typeof(FlowCoroutineIsRunningUnit))] 87 | public class FlowCoroutineIsRunningUnitDescriptor : FlowCoroutineUnitDescriptor 88 | { 89 | public FlowCoroutineIsRunningUnitDescriptor(FlowCoroutineIsRunningUnit target) : base(target) 90 | { 91 | } 92 | 93 | protected override string DefinedSummary() 94 | { 95 | return "Checks if the given Flow Coroutine is still running. " 96 | + "If you want to wait for a Flow Coroutine to finish, consider using Wait For Flow Coroutine instead."; 97 | } 98 | 99 | protected override void DefinedPort(IUnitPort port, UnitPortDescription description) 100 | { 101 | base.DefinedPort(port, description); 102 | if (port == target.flowCoroutine) 103 | { 104 | description.icon = typeof(Coroutine).Icon(); 105 | description.summary = "The coroutine to check."; 106 | } 107 | } 108 | } 109 | 110 | [Descriptor(typeof(FlowCoroutineWaitUnit))] 111 | public class FlowCoroutineWaitUnitDescriptor : UnitDescriptor 112 | { 113 | // Note: Doesn't inherit from FlowCoroutine as we're using the default WaitUnit icon. 114 | public FlowCoroutineWaitUnitDescriptor(FlowCoroutineWaitUnit target) : base(target) 115 | { 116 | } 117 | 118 | protected override string DefinedSummary() 119 | { 120 | return "Pauses execution until the given Flow Coroutine is finished or stopped."; 121 | } 122 | 123 | protected override void DefinedPort(IUnitPort port, UnitPortDescription description) 124 | { 125 | base.DefinedPort(port, description); 126 | if (port == target.flowCoroutine) 127 | { 128 | description.icon = typeof(Coroutine).Icon(); 129 | description.summary = "The coroutine to wait for."; 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Editor/FlowCoroutineUnitDescriptor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b84066094add0954fb2aa11099d7cd00 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/FunctionUnitAnalyser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using Unity.VisualScripting; 4 | using UnityEngine; 5 | 6 | namespace CHM.VisualScriptingKai.Editor 7 | { 8 | public abstract class FunctionUnitAnalyser : UnitAnalyser 9 | where TFunctionUnit : Unit, IFunctionUnit 10 | { 11 | protected FunctionUnitAnalyser(GraphReference reference, TFunctionUnit target) : base(reference, target) 12 | { 13 | } 14 | 15 | protected override IEnumerable Warnings() 16 | { 17 | foreach(var warning in base.Warnings()) 18 | yield return warning; 19 | if(target.functionDefinition == null) 20 | yield return Warning.Caution("Function definition should not be null."); 21 | } 22 | } 23 | 24 | [Analyser(typeof(FunctionCheckUnit))] 25 | public sealed class FunctionCheckUnitAnalyser : FunctionUnitAnalyser 26 | { 27 | public FunctionCheckUnitAnalyser(GraphReference reference, FunctionCheckUnit target) : base(reference, target) 28 | { 29 | } 30 | } 31 | 32 | [Analyser(typeof(HasFunctionUnit))] 33 | public sealed class HasFunctionUnitAnalyser : FunctionUnitAnalyser 34 | { 35 | public HasFunctionUnitAnalyser(GraphReference reference, HasFunctionUnit target) : base(reference, target) 36 | { 37 | } 38 | } 39 | 40 | [Analyser(typeof(FunctionStartUnit))] 41 | public sealed class FunctionStartUnitAnalyser : FunctionUnitAnalyser 42 | { 43 | public FunctionStartUnitAnalyser(GraphReference reference, FunctionStartUnit target) : base(reference, target) 44 | { 45 | } 46 | } 47 | 48 | [Analyser(typeof(FunctionReturnUnit))] 49 | public sealed class FunctionReturnUnitAnalyser : FunctionUnitAnalyser 50 | { 51 | public FunctionReturnUnitAnalyser(GraphReference reference, FunctionReturnUnit target) : base(reference, target) 52 | { 53 | } 54 | } 55 | 56 | [Analyser(typeof(FunctionCallUnit))] 57 | public sealed class FunctionCallUnitAnalyser : FunctionUnitAnalyser 58 | { 59 | public FunctionCallUnitAnalyser(GraphReference reference, FunctionCallUnit target) : base(reference, target) 60 | { 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Editor/FunctionUnitAnalyser.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5ff7aa3c8b5dbdb4c9025cfdadd448d6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/FunctionUnitDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.VisualScripting; 5 | 6 | namespace CHM.VisualScriptingKai.Editor 7 | { 8 | public abstract class FunctionUnitDescriptor : UnitDescriptor 9 | where TFunctionUnit : Unit, IFunctionUnit 10 | { 11 | protected FunctionUnitDescriptor(TFunctionUnit target) : base(target) 12 | { 13 | } 14 | 15 | protected override string DefinedSubtitle() 16 | { 17 | if(target.functionDefinition != null) 18 | return target.functionDefinition.name; 19 | return base.DefinedSubtitle(); 20 | } 21 | } 22 | 23 | [Descriptor(typeof(FunctionCheckUnit))] 24 | public sealed class FunctionCheckUnitDescriptor : FunctionUnitDescriptor 25 | { 26 | public FunctionCheckUnitDescriptor(FunctionCheckUnit target) : base(target) 27 | { 28 | } 29 | 30 | protected override string DefinedTitle() 31 | { 32 | if(target.functionDefinition != null) 33 | return "Check " + target.functionDefinition.name; 34 | return base.DefinedTitle(); 35 | } 36 | 37 | protected override string DefinedSummary() 38 | { 39 | return "Check if the target GameObject implements a function " 40 | + "with the same function definition as this node."; 41 | } 42 | } 43 | 44 | [Descriptor(typeof(HasFunctionUnit))] 45 | public sealed class HasFunctionUnitDescriptor : FunctionUnitDescriptor 46 | { 47 | public HasFunctionUnitDescriptor(HasFunctionUnit target) : base(target) 48 | { 49 | } 50 | 51 | protected override string DefinedTitle() 52 | { 53 | if(target.functionDefinition != null) 54 | return "Has " + target.functionDefinition.name; 55 | return base.DefinedTitle(); 56 | } 57 | 58 | protected override string DefinedSummary() 59 | { 60 | return "Check if the target GameObject implements a function " 61 | + "with the same function definition as this node, and output the answer."; 62 | } 63 | } 64 | 65 | [Descriptor(typeof(FunctionStartUnit))] 66 | public sealed class FunctionStartUnitDescriptor : FunctionUnitDescriptor 67 | { 68 | public FunctionStartUnitDescriptor(FunctionStartUnit target) : base(target) 69 | { 70 | } 71 | 72 | protected override string DefinedTitle() 73 | { 74 | if(target.functionDefinition != null) 75 | return "Start " + target.functionDefinition.name; 76 | return base.DefinedTitle(); 77 | } 78 | 79 | protected override string DefinedSummary() 80 | { 81 | return "The start of a function implementation. " 82 | + "This node's ports will change according to its function definition."; 83 | } 84 | } 85 | 86 | [Descriptor(typeof(FunctionReturnUnit))] 87 | public sealed class FunctionReturnUnitDescriptor : FunctionUnitDescriptor 88 | { 89 | public FunctionReturnUnitDescriptor(FunctionReturnUnit target) : base(target) 90 | { 91 | } 92 | 93 | protected override string DefinedTitle() 94 | { 95 | if(target.functionDefinition != null) 96 | return "Return " + target.functionDefinition.name; 97 | return base.DefinedTitle(); 98 | } 99 | 100 | protected override string DefinedSummary() 101 | { 102 | return "The end of a function implementation. " 103 | + "This node's ports will change according to its function definition."; 104 | } 105 | } 106 | 107 | [Descriptor(typeof(FunctionCallUnit))] 108 | public sealed class FunctionCallUnitDescriptor : FunctionUnitDescriptor 109 | { 110 | public FunctionCallUnitDescriptor(FunctionCallUnit target) : base(target) 111 | { 112 | } 113 | 114 | protected override string DefinedTitle() 115 | { 116 | if(target.functionDefinition != null) 117 | return "Call " + target.functionDefinition.name; 118 | return base.DefinedTitle(); 119 | } 120 | 121 | protected override string DefinedSummary() 122 | { 123 | return "Call the specified function implemented by the target GameObject. " 124 | + "This node's ports will change according to its function definition."; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Editor/FunctionUnitDescriptor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8f7727799a0f84f4ba865eb5d6389889 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphAnalyzerWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEditor; 5 | using UnityEngine.UIElements; 6 | using System.Linq; 7 | using static CHM.VisualScriptingKai.Editor.GraphUtility; 8 | using Unity.VisualScripting; 9 | 10 | namespace CHM.VisualScriptingKai.Editor 11 | { 12 | public class GraphAnalyzerWindow : GraphQueryWindowBase 13 | { 14 | private DropdownField queryWarningLevel; 15 | private TextField queryFolders; 16 | private QueryResultsListView queryResultsListView; 17 | private readonly List graphAssetSourceCache = new(); 18 | private readonly List queryWarningLevels = new(){ 19 | nameof(WarningLevel.Info), 20 | nameof(WarningLevel.Caution), 21 | nameof(WarningLevel.Severe), 22 | nameof(WarningLevel.Error), 23 | }; 24 | private static class EditorPrefKeys 25 | { 26 | public static readonly string QueryWarningLevel = Application.dataPath + "/GraphAnalyzer/query-warning-level"; 27 | public static readonly string QueryFolders = Application.dataPath + "/GraphAnalyzer/query-folders"; 28 | } 29 | [MenuItem("Window/Visual Scripting/Graph Analyzer")] 30 | public static void OpenWindow() 31 | { 32 | GraphAnalyzerWindow wnd = GetWindow( 33 | typeof(GraphQueryWindowBase)); 34 | // TODO: Add Graph Lens icon to GUIContent. 35 | wnd.titleContent = new GUIContent("Graph Analyzer"); 36 | } 37 | protected override void OnCreateGUI() 38 | { 39 | // Each editor window contains a root VisualElement object 40 | VisualElement root = rootVisualElement; 41 | 42 | // Import UXML 43 | var visualTree = PackageUtility.LoadPackageAsset("Editor/Resources/GraphAnalyzerWindow.uxml"); 44 | VisualElement visualTreeRoot = visualTree.Instantiate(); 45 | root.Add(visualTreeRoot); 46 | 47 | // Set up references. 48 | FetchElement(visualTreeRoot, "query-warning-level", out queryWarningLevel); 49 | FetchElement(visualTreeRoot, "query-folders", out queryFolders); 50 | FetchElement(visualTreeRoot, "query-results", out queryResultsListView); 51 | 52 | queryFolders.value = EditorPrefs.GetString(EditorPrefKeys.QueryFolders, "Assets"); 53 | queryFolders.RegisterValueChangedCallback(changeEvent => 54 | { 55 | EditorPrefs.SetString(EditorPrefKeys.QueryFolders, queryFolders.value); 56 | // Folders changed, so the cache needs to be updated. 57 | UpdateGraphAssetSourceCache(); 58 | ExecuteQuery(); 59 | }); 60 | 61 | queryWarningLevel.choices = queryWarningLevels; 62 | queryWarningLevel.index = EditorPrefs.GetInt(EditorPrefKeys.QueryWarningLevel, 0); 63 | if(queryWarningLevel.index >= queryWarningLevels.Count) 64 | queryWarningLevel.index = 0; 65 | queryWarningLevel.RegisterValueChangedCallback(changeEvent => { 66 | EditorPrefs.SetInt(EditorPrefKeys.QueryWarningLevel, queryWarningLevel.index); 67 | ExecuteQuery(); 68 | }); 69 | queryResultsListView.style.backgroundColor = new Color(0.4f, 0.35f, 0f); 70 | 71 | // First query. 72 | UpdateGraphAssetSourceCache(); 73 | ExecuteQuery(); 74 | } 75 | protected override void OnGraphAssetListChanged() 76 | { 77 | UpdateGraphAssetSourceCache(); 78 | ExecuteQuery(); 79 | } 80 | protected override void OnPlayModeChanged(PlayModeStateChange stateChange) 81 | { 82 | // Redo ExecuteQuery here because GraphReferences become invalid 83 | // when switching between modes. 84 | if(stateChange == PlayModeStateChange.EnteredEditMode 85 | || stateChange == PlayModeStateChange.EnteredPlayMode) 86 | { 87 | ExecuteQuery(); 88 | } 89 | } 90 | void Update() 91 | { 92 | if(hasFocus && executeQueryScheduled) 93 | { 94 | // Possibly faster than a full query, because it doesn't need to 95 | // visit every node in scope. 96 | // Still needs to sort the query results again, though. 97 | // QueryEditedGraph(); 98 | 99 | // FIXME: Using this because QueryEditedGraph is buggy. 100 | ExecuteQuery(); 101 | executeQueryScheduled = false; 102 | } 103 | } 104 | private void ExecuteQuery() 105 | { 106 | var sources = graphAssetSourceCache.Concat(FindAllRuntimeGraphSources()); 107 | var editedGraph = GetEditedGraph(); 108 | if(editedGraph != null) 109 | sources.Append(editedGraph); 110 | queryResultsListView.LoadQueryResults( 111 | FindWarnings(sources, (WarningLevel)queryWarningLevel.index + (int)WarningLevel.Info)); 112 | } 113 | private void QueryEditedGraph() 114 | { 115 | var editedGraphSource = GetEditedGraph(); 116 | if (editedGraphSource != null) 117 | { 118 | queryResultsListView.UpdateQueryResults( 119 | editedGraphSource, FindWarnings(editedGraphSource, (WarningLevel)queryWarningLevel.index + (int)WarningLevel.Info)); 120 | } 121 | } 122 | private string[] GetQueryFolders() 123 | { 124 | return queryFolders.value.Split( 125 | new char[] { ' ', ',', '\n', '\t', '\r' }, 126 | System.StringSplitOptions.RemoveEmptyEntries); 127 | } 128 | private void UpdateGraphAssetSourceCache() 129 | { 130 | graphAssetSourceCache.Clear(); 131 | graphAssetSourceCache.AddRange(FindAllGraphAssets(GetQueryFolders())); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Editor/GraphAnalyzerWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 83b1dfbcffcca1e47b1dd6919601b83d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphAssetListTracker.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using Unity.VisualScripting; 4 | using UnityEditor; 5 | using UnityEngine; 6 | 7 | namespace CHM.VisualScriptingKai.Editor 8 | { 9 | public class GraphAssetListTracker : AssetPostprocessor 10 | { 11 | public static event System.Action OnGraphAssetListChanged; 12 | static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths, bool didDomainReload) 13 | { 14 | // Mark dirty if graph assets are found. 15 | if(didDomainReload 16 | || PathsContainGraphAssets(importedAssets) 17 | || PathsContainGraphAssets(deletedAssets) 18 | || PathsContainGraphAssets(movedAssets)) 19 | { 20 | OnGraphAssetListChanged?.Invoke(); 21 | } 22 | } 23 | private static bool PathsContainGraphAssets(string[] paths) 24 | { 25 | foreach(var path in paths) 26 | { 27 | var graphAsset = AssetDatabase.LoadAssetAtPath(path); 28 | if(graphAsset != null) 29 | { 30 | return true; 31 | } 32 | } 33 | return false; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Editor/GraphAssetListTracker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f3a5adaca68d36447a638e845499ca50 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphDebuggerWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using Unity.VisualScripting; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using UnityEngine.UIElements; 7 | 8 | namespace CHM.VisualScriptingKai.Editor 9 | { 10 | public class GraphDebuggerWindow : GraphQueryWindowBase, IHasCustomMenu 11 | { 12 | private Toggle debugEnabled; 13 | private QueryResultsListView queryResultsListView; 14 | [MenuItem("Window/Visual Scripting/Graph Debugger")] 15 | public static void OpenWindow() 16 | { 17 | GraphDebuggerWindow wnd = GetWindow( 18 | typeof(GraphQueryWindowBase)); 19 | // TODO: Add Graph Lens icon to GUIContent. 20 | wnd.titleContent = new GUIContent("Graph Debugger"); 21 | } 22 | private static class EditorPrefKeys 23 | { 24 | public static readonly string DebugEnabled = Application.dataPath + "/GraphDebugger/debug-enabled"; 25 | } 26 | protected override void OnCreateGUI() 27 | { 28 | // Each editor window contains a root VisualElement object 29 | VisualElement root = rootVisualElement; 30 | 31 | // Import UXML 32 | var visualTree = PackageUtility.LoadPackageAsset("Editor/Resources/GraphDebuggerWindow.uxml"); 33 | VisualElement visualTreeRoot = visualTree.Instantiate(); 34 | root.Add(visualTreeRoot); 35 | 36 | FetchElement(root, "debug-enabled", out debugEnabled); 37 | FetchElement(root, "query-results", out queryResultsListView); 38 | 39 | debugEnabled.value = EditorPrefs.GetBool(EditorPrefKeys.DebugEnabled, false); 40 | debugEnabled.RegisterValueChangedCallback(changeEvent => 41 | { 42 | EditorPrefs.SetBool(EditorPrefKeys.DebugEnabled, changeEvent.newValue); 43 | }); 44 | queryResultsListView = root.Q("query-results"); 45 | queryResultsListView.style.backgroundColor = new Color(0.5f, 0.25f, 0.25f); 46 | } 47 | void Update() 48 | { 49 | if(debugEnabled.value 50 | && Application.isPlaying 51 | && executeQueryScheduled) 52 | { 53 | // We only ever use this to remove entries, so an optimization here is 54 | // to skip if there's nothing left. 55 | if(queryResultsListView.Count > 0) 56 | ExecuteQuery(); 57 | executeQueryScheduled = false; 58 | } 59 | } 60 | private void ExecuteQuery() 61 | { 62 | // Expect the caller to pause first. 63 | // Debug.Assert(Application.isPlaying); 64 | var sources = GraphUtility.FindAllRuntimeGraphSources(); 65 | queryResultsListView.LoadQueryResults(GraphUtility.FindAllExceptions(sources)); 66 | // To make the results look consistent, we need to flush results on 67 | // EditorApplication.pauseStateChanged. 68 | } 69 | protected override void OnPlayModeChanged(PlayModeStateChange stateChange) 70 | { 71 | if(!debugEnabled.value) 72 | return; 73 | if(stateChange == PlayModeStateChange.ExitingEditMode) 74 | { 75 | Application.logMessageReceived += OnLogMessageReceived; 76 | EditorApplication.pauseStateChanged += OnPauseModeChanged; 77 | executeQueryScheduled = false; 78 | } 79 | else if(stateChange == PlayModeStateChange.ExitingPlayMode) 80 | { 81 | Application.logMessageReceived -= OnLogMessageReceived; 82 | EditorApplication.pauseStateChanged -= OnPauseModeChanged; 83 | } 84 | else if(stateChange == PlayModeStateChange.EnteredEditMode) 85 | { 86 | // TODO: Figure out a way to replace runtime graph references with editor ones. 87 | // Until then, we have no choice but to flush. 88 | queryResultsListView.FlushQueryResults(); 89 | // // Redo the query to replace runtime graph references with editor ones. 90 | // ExecuteQuery(); 91 | } 92 | } 93 | private void OnPauseModeChanged(PauseState pauseState) 94 | { 95 | if(!debugEnabled.value) 96 | return; 97 | if(pauseState == PauseState.Unpaused) 98 | { 99 | queryResultsListView.FlushQueryResults(); 100 | } 101 | } 102 | private void OnLogMessageReceived(string logString, string stackTrace, LogType type) 103 | { 104 | if(!debugEnabled.value) 105 | return; 106 | if(type == LogType.Error || type == LogType.Exception) 107 | { 108 | if(stackTrace.Contains("Unity.VisualScripting.Flow.Invoke")) 109 | { 110 | // Pause the editor, then look for exception nodes. 111 | Debug.Break(); 112 | // Highlight the debugger. 113 | GetWindow(false).Show(); 114 | ExecuteQuery(); 115 | } 116 | } 117 | } 118 | 119 | public void AddItemsToMenu(GenericMenu menu) 120 | { 121 | if(debugEnabled == null) 122 | return; 123 | if(menu == null) 124 | return; 125 | menu.AddItem( 126 | new GUIContent("Toggle Enabled"), 127 | debugEnabled.value, () => { 128 | debugEnabled.value = !debugEnabled.value; 129 | }); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Editor/GraphDebuggerWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f22f3c200fd056b4dbc679d9b6731f99 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphLensWindow.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | using UnityEngine.UIElements; 4 | using UnityEditor.UIElements; 5 | using System.Collections.Generic; 6 | using static CHM.VisualScriptingKai.Editor.GraphUtility; 7 | using Unity.VisualScripting; 8 | using System.Linq; 9 | 10 | namespace CHM.VisualScriptingKai.Editor 11 | { 12 | // TODO: Search for warnings ((IUnit.Analysis() as UnitAnalysis).warnings) 13 | // TODO: [Future work] Operations on query items (maybe replace-all is possible?) 14 | public class GraphLensWindow : GraphQueryWindowBase 15 | { 16 | private DropdownField queryType; 17 | private TextField queryFolders; 18 | private TextField queryString; 19 | private ObjectField queryObject; 20 | private Button queryRefresh; 21 | private QueryResultsListView queryResultsListView; 22 | private readonly List graphAssetSourceCache = new(); 23 | private static class QueryType 24 | { 25 | public static readonly string Nodes = "Nodes"; 26 | public static readonly string StickyNotes = "Sticky Notes"; 27 | public static readonly string States = "States"; 28 | public static readonly string StateTransitions = "State Transitions"; 29 | public static readonly string References = "References"; 30 | } 31 | private readonly List queryOptions = new(){ 32 | QueryType.Nodes, 33 | QueryType.StickyNotes, 34 | QueryType.States, 35 | QueryType.StateTransitions, 36 | QueryType.References, 37 | }; 38 | private static class EditorPrefKeys 39 | { 40 | public static readonly string QueryType = Application.dataPath + "/GraphLens/query-type"; 41 | public static readonly string QueryFolders = Application.dataPath + "/GraphLens/query-folders"; 42 | public static readonly string QueryString = Application.dataPath + "/GraphLens/query-string"; 43 | } 44 | [MenuItem("Window/Visual Scripting/Graph Lens")] 45 | public static void OpenWindow() 46 | { 47 | GraphLensWindow wnd = GetWindow( 48 | typeof(GraphQueryWindowBase)); 49 | // TODO: Add Graph Lens icon to GUIContent. 50 | wnd.titleContent = new GUIContent("Graph Lens"); 51 | } 52 | protected override void OnCreateGUI() 53 | { 54 | // Each editor window contains a root VisualElement object 55 | VisualElement root = rootVisualElement; 56 | 57 | // Import UXML 58 | var visualTree = PackageUtility.LoadPackageAsset("Editor/Resources/GraphLensWindow.uxml"); 59 | VisualElement visualTreeRoot = visualTree.Instantiate(); 60 | root.Add(visualTreeRoot); 61 | 62 | // Set up references. 63 | FetchElement(visualTreeRoot, "query-type", out queryType); 64 | FetchElement(visualTreeRoot, "query-folders", out queryFolders); 65 | FetchElement(visualTreeRoot, "query-string", out queryString); 66 | FetchElement(visualTreeRoot, "query-results", out queryResultsListView); 67 | queryObject = new ObjectField("Search Object"); 68 | queryRefresh = new Button(); 69 | queryRefresh.text = "Refresh"; 70 | 71 | queryFolders.value = EditorPrefs.GetString(EditorPrefKeys.QueryFolders, "Assets"); 72 | queryFolders.RegisterValueChangedCallback(changeEvent => 73 | { 74 | EditorPrefs.SetString(EditorPrefKeys.QueryFolders, queryFolders.value); 75 | // Folders changed, so the cache needs to be updated. 76 | UpdateGraphAssetSourceCache(); 77 | ExecuteQuery(); 78 | }); 79 | 80 | queryType.choices = queryOptions; 81 | queryType.index = EditorPrefs.GetInt(EditorPrefKeys.QueryType, 0); 82 | if(queryType.index >= queryOptions.Count) 83 | queryType.index = 0; 84 | queryType.RegisterValueChangedCallback(changeEvent => 85 | { 86 | EditorPrefs.SetInt(EditorPrefKeys.QueryType, queryType.index); 87 | if (queryType.index == 4) 88 | { 89 | queryFolders.parent.Insert(queryFolders.parent.IndexOf(queryString) + 1, queryRefresh); 90 | queryFolders.parent.Insert(queryFolders.parent.IndexOf(queryString) + 1, queryObject); 91 | queryFolders.parent.Remove(queryString); 92 | } 93 | else 94 | { 95 | queryFolders.parent.Insert(queryFolders.parent.IndexOf(queryObject) + 1, queryString); 96 | if (queryObject.parent == queryFolders.parent) 97 | queryFolders.parent.Remove(queryObject); 98 | if (queryRefresh.parent == queryFolders.parent) 99 | queryFolders.parent.Remove(queryRefresh); 100 | } 101 | ExecuteQuery(); 102 | }); 103 | 104 | queryString.value = EditorPrefs.GetString(EditorPrefKeys.QueryString, ""); 105 | queryString.RegisterValueChangedCallback(changeEvent => 106 | { 107 | // TrimStart because leading whitespaces make fuzzy search fail. 108 | queryString.SetValueWithoutNotify( 109 | changeEvent.newValue.TrimStart()); 110 | ExecuteQuery(); 111 | }); 112 | 113 | queryObject.objectType = typeof(Object); 114 | queryObject.RegisterValueChangedCallback(changeEvent => 115 | { 116 | ExecuteQuery(); 117 | }); 118 | 119 | if (queryType.index == 4) 120 | { 121 | queryFolders.parent.Insert(queryFolders.parent.IndexOf(queryString) + 1, queryRefresh); 122 | queryFolders.parent.Insert(queryFolders.parent.IndexOf(queryString) + 1, queryObject); 123 | queryFolders.parent.Remove(queryString); 124 | } 125 | 126 | queryRefresh.tooltip = "Manual refresh is required when you change the value of an Object literal."; 127 | queryRefresh.clicked += () => 128 | { 129 | ExecuteQuery(); 130 | }; 131 | 132 | // First query. 133 | UpdateGraphAssetSourceCache(); 134 | ExecuteQuery(); 135 | } 136 | protected override void OnGraphAssetListChanged() 137 | { 138 | UpdateGraphAssetSourceCache(); 139 | ExecuteQuery(); 140 | } 141 | protected override void OnPlayModeChanged(PlayModeStateChange stateChange) 142 | { 143 | // Redo ExecuteQuery here because GraphReferences become invalid 144 | // when switching between modes. 145 | if(stateChange == PlayModeStateChange.EnteredEditMode 146 | || stateChange == PlayModeStateChange.EnteredPlayMode) 147 | { 148 | ExecuteQuery(); 149 | } 150 | } 151 | void Update() 152 | { 153 | if(hasFocus && executeQueryScheduled) 154 | { 155 | // Possibly faster than a full query, because it doesn't need to 156 | // visit every node in scope. 157 | // Still needs to sort the query results again, though. 158 | // QueryEditedGraph(); 159 | 160 | // FIXME: Using this because QueryEditedGraph is buggy. 161 | ExecuteQuery(); 162 | executeQueryScheduled = false; 163 | } 164 | } 165 | private void ExecuteQuery() 166 | { 167 | EditorPrefs.SetString(EditorPrefKeys.QueryString, queryString.value); 168 | var sources = graphAssetSourceCache.Concat(FindAllRuntimeGraphSources()); 169 | var editedGraph = GetEditedGraph(); 170 | if(editedGraph != null) 171 | sources.Append(editedGraph); 172 | if (queryType.value == QueryType.Nodes) 173 | { 174 | queryResultsListView.LoadQueryResults(FindNodes( 175 | sources, 176 | queryString.value)); 177 | } 178 | else if (queryType.value == QueryType.StickyNotes) 179 | { 180 | queryResultsListView.LoadQueryResults(FindStickyNotes( 181 | sources, 182 | queryString.value)); 183 | } 184 | else if (queryType.value == QueryType.States) 185 | { 186 | queryResultsListView.LoadQueryResults(FindStates( 187 | sources, 188 | queryString.value)); 189 | } 190 | else if (queryType.value == QueryType.StateTransitions) 191 | { 192 | queryResultsListView.LoadQueryResults(FindStateTransitions( 193 | sources, 194 | queryString.value)); 195 | } 196 | else if (queryType.value == QueryType.References) 197 | { 198 | queryResultsListView.LoadQueryResults(FindReferencesToObject( 199 | sources, 200 | queryObject.value)); 201 | } 202 | else throw new System.ArgumentException($"Unknown query type: {queryType.text}"); 203 | } 204 | private void QueryEditedGraph() 205 | { 206 | var editedGraphSource = GetEditedGraph(); 207 | if (editedGraphSource != null) 208 | { 209 | if (queryType.value == QueryType.Nodes) 210 | { 211 | queryResultsListView.UpdateQueryResults( 212 | editedGraphSource, 213 | FindNodes( 214 | editedGraphSource, 215 | queryString.value)); 216 | } 217 | else if (queryType.value == QueryType.StickyNotes) 218 | { 219 | queryResultsListView.UpdateQueryResults( 220 | editedGraphSource, 221 | FindStickyNotes( 222 | editedGraphSource, 223 | queryString.value)); 224 | } 225 | else if (queryType.value == QueryType.States) 226 | { 227 | queryResultsListView.UpdateQueryResults( 228 | editedGraphSource, 229 | FindStates( 230 | editedGraphSource, 231 | queryString.value)); 232 | } 233 | else if (queryType.value == QueryType.StateTransitions) 234 | { 235 | queryResultsListView.UpdateQueryResults( 236 | editedGraphSource, 237 | FindStateTransitions( 238 | editedGraphSource, 239 | queryString.value)); 240 | } 241 | else throw new System.ArgumentException($"Unknown query type: {queryType.text}"); 242 | } 243 | else 244 | { 245 | if(LudiqEditorUtility.editedObject.value == null) 246 | { 247 | Debug.Log("No edited object found."); 248 | } 249 | else 250 | { 251 | Debug.Log("Edited object is type: " + LudiqEditorUtility.editedObject.value.GetType()); 252 | } 253 | } 254 | } 255 | private string[] GetQueryFolders() 256 | { 257 | return queryFolders.value.Split( 258 | new char[] { ' ', ',', '\n', '\t', '\r' }, 259 | System.StringSplitOptions.RemoveEmptyEntries); 260 | } 261 | private void UpdateGraphAssetSourceCache() 262 | { 263 | graphAssetSourceCache.Clear(); 264 | graphAssetSourceCache.AddRange(FindAllGraphAssets(GetQueryFolders())); 265 | } 266 | } 267 | } -------------------------------------------------------------------------------- /Editor/GraphLensWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7fd17959d95b7074a933f2c50a23f8d0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphQueryWindowBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using Unity.VisualScripting; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using UnityEngine.UIElements; 7 | 8 | namespace CHM.VisualScriptingKai.Editor 9 | { 10 | // ! You may need to close all windows every time you change EditorWindow code. 11 | public abstract class GraphQueryWindowBase : EditorWindow 12 | { 13 | protected bool executeQueryScheduled = false; 14 | public void CreateGUI() 15 | { 16 | OnCreateGUI(); 17 | 18 | GraphAssetListTracker.OnGraphAssetListChanged += OnGraphAssetListChanged; 19 | EditorApplication.playModeStateChanged += OnPlayModeChanged; 20 | GraphWindow.activeContextChanged += OnGraphWindowActiveContextChanged; 21 | OnGraphWindowActiveContextChanged(GraphWindow.activeContext); 22 | } 23 | protected virtual void OnDestroy() 24 | { 25 | GraphAssetListTracker.OnGraphAssetListChanged -= OnGraphAssetListChanged; 26 | EditorApplication.playModeStateChanged -= OnPlayModeChanged; 27 | GraphWindow.activeContextChanged -= OnGraphWindowActiveContextChanged; 28 | } 29 | protected abstract void OnCreateGUI(); 30 | protected virtual void OnGraphWindowActiveContextChanged(IGraphContext context) 31 | { 32 | if(context == null) 33 | return; 34 | if(context.graph == null) 35 | return; 36 | 37 | context.graph.elements.CollectionChanged += () => { 38 | // Defer ExecuteQuery to the next Update, because 39 | // CollectionChanged might be invoked by the serializer, 40 | // causing possible deadlocks. 41 | executeQueryScheduled = true; 42 | // Note: Adding/deleting nodes won't introduce new exceptions, but might 43 | // remove old ones. 44 | }; 45 | context.canvas.window = GraphWindow.active; 46 | } 47 | protected virtual void OnPlayModeChanged(PlayModeStateChange stateChange) 48 | { 49 | 50 | } 51 | 52 | protected virtual void OnGraphAssetListChanged() 53 | { 54 | 55 | } 56 | protected void FetchElement(VisualElement root, string name, out T result) where T : VisualElement 57 | { 58 | result = root.Q(name); 59 | if(result == null) 60 | Debug.LogWarning($"Couldn't find {typeof(T)} named {name}. Try reopening {titleContent.text}."); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Editor/GraphQueryWindowBase.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 407ce5d76a5c95c458fae25dd482967f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphRecursionContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.VisualScripting; 5 | using System; 6 | 7 | namespace CHM.VisualScriptingKai.Editor 8 | { 9 | /// 10 | /// Internal data structure that keeps track of the visited set to avoid repeats, 11 | /// as well as the current path. 12 | /// 13 | internal class GraphRecursionContext 14 | { 15 | public IGraphRoot root; 16 | public List path; 17 | public HashSet visited; 18 | public bool embedSubgraphsOnly = false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Editor/GraphRecursionContext.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a39b7e09fd9667f4c83aa8f40102dde7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphSource.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.VisualScripting; 5 | using UnityEditor; 6 | 7 | namespace CHM.VisualScriptingKai.Editor 8 | { 9 | /// 10 | /// Union class that lets all kinds of graph assets share the same API, without excessive code duplication. 11 | /// 12 | public class GraphSource 13 | { 14 | public readonly ScriptGraphAsset scriptGraphAsset; 15 | public readonly StateGraphAsset stateGraphAsset; 16 | public readonly ScriptMachine scriptMachine; 17 | public readonly StateMachine stateMachine; 18 | public GraphSource(ScriptGraphAsset asset) => scriptGraphAsset = asset; 19 | public GraphSource(StateGraphAsset asset) => stateGraphAsset = asset; 20 | public GraphSource(ScriptMachine machine) => scriptMachine = machine; 21 | public GraphSource(StateMachine machine) => stateMachine = machine; 22 | // Casting to Unity.Object extracts the union's non-null type. 23 | public static implicit operator Object(GraphSource source) 24 | { 25 | if(source.scriptGraphAsset != null) 26 | return source.scriptGraphAsset; 27 | else if(source.stateGraphAsset != null) 28 | return source.stateGraphAsset; 29 | else if(source.scriptMachine != null) 30 | return source.scriptMachine; 31 | else return source.stateMachine; 32 | } 33 | public static explicit operator ScriptableObject(GraphSource source) 34 | { 35 | return ((Object) source) as ScriptableObject; 36 | } 37 | public static explicit operator MonoBehaviour(GraphSource source) 38 | { 39 | return ((Object) source) as MonoBehaviour; 40 | } 41 | public string Name => ((Object) this).name; 42 | public string ShortInfo { 43 | get 44 | { 45 | // Casting to Unity.Object extracts the union. (See above) 46 | var obj = (Object)this; 47 | // Note: Uses VisualScripting's built-in extension methods here for prettifying. 48 | string info = $"Source: {obj.GetType().HumanName()}"; 49 | return info; 50 | } 51 | } 52 | public string Info { 53 | get 54 | { 55 | // Casting to Unity.Object extracts the union. (See above) 56 | var obj = (Object)this; 57 | // Note: Uses VisualScripting's built-in extension methods here for prettifying. 58 | string info = $"Source: {obj.name} ({obj.GetType().HumanName()})"; 59 | // Downcast as needed. 60 | if(obj is ScriptableObject so) 61 | { 62 | info += $"\nAsset Path: {AssetDatabase.GetAssetPath(so)}"; 63 | } 64 | else 65 | { 66 | var mono = obj as MonoBehaviour; 67 | var gameObject = mono.gameObject; 68 | // Only two cases: In a scene or in a prefab. 69 | // We don't care about prefab instances in a scene, as they're 70 | // just in a scene in that case. 71 | if(gameObject.scene != null) 72 | { 73 | info += $"\nScene Path: {gameObject.scene.path}"; 74 | } 75 | else 76 | { 77 | info += $"\nPrefab Path: {PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(gameObject)}"; 78 | } 79 | } 80 | return info; 81 | } 82 | } 83 | public int GetInstanceID() => ((Object) this).GetInstanceID(); 84 | public IGraphRoot GraphRoot 85 | { 86 | get { 87 | if(scriptGraphAsset != null) 88 | return scriptGraphAsset; 89 | else if(stateGraphAsset != null) 90 | return stateGraphAsset; 91 | else if(scriptMachine != null) 92 | return scriptMachine; 93 | else return stateMachine; 94 | } 95 | } 96 | public bool TryGetScenePath(out string scenePath) 97 | { 98 | var obj = (Object) this; 99 | if(obj is MonoBehaviour mono) 100 | { 101 | if(mono.gameObject.scene != null) 102 | scenePath = mono.gameObject.scene.path; 103 | else scenePath = string.Empty; 104 | return true; 105 | } 106 | else 107 | { 108 | scenePath = string.Empty; 109 | return false; 110 | } 111 | } 112 | public bool TryGetGameObject(out GameObject gameObject) 113 | { 114 | var obj = (Object) this; 115 | if(obj is MonoBehaviour mono) 116 | { 117 | gameObject = mono.gameObject; 118 | return true; 119 | } 120 | else 121 | { 122 | gameObject = null; 123 | return false; 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Editor/GraphSource.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4c69dc42bdf39de4281fc2b524bafd34 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphTraversalUtility_StateTransitions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.VisualScripting; 5 | using System.Linq; 6 | using UnityEditor; 7 | using System; 8 | 9 | namespace CHM.VisualScriptingKai.Editor 10 | { 11 | public static partial class GraphTraversalUtility 12 | { 13 | public static IEnumerable<(IStateTransition, List)> GetStateTransitionsRecursive(this GraphSource source, bool embedSubgraphsOnly = true, HashSet visited = null) 14 | { 15 | if(source.scriptGraphAsset != null) 16 | return GetStateTransitionsRecursive(source.scriptGraphAsset.graph, source.scriptGraphAsset, embedSubgraphsOnly, visited); 17 | else if(source.stateGraphAsset != null) 18 | return GetStateTransitionsRecursive(source.stateGraphAsset.graph, source.stateGraphAsset, embedSubgraphsOnly, visited); 19 | else if(source.scriptMachine != null) 20 | return GetStateTransitionsRecursive(source.scriptMachine.graph, source.scriptMachine, embedSubgraphsOnly, visited); 21 | else if(source.stateMachine != null) 22 | return GetStateTransitionsRecursive(source.stateMachine.graph, source.stateMachine, embedSubgraphsOnly, visited); 23 | else throw new System.NullReferenceException(); // Shouldn't be reachable. 24 | } 25 | public static IEnumerable<(IStateTransition, List)> GetStateTransitionsRecursive(FlowGraph graph, IGraphRoot root, bool embedSubgraphsOnly = true, HashSet visited = null) 26 | { 27 | return GetStateTransitionsRecursiveInternal( 28 | graph, 29 | new(){ 30 | root = root, 31 | path = new(), 32 | visited = visited ?? new(), 33 | embedSubgraphsOnly = embedSubgraphsOnly}); 34 | } 35 | public static IEnumerable<(IStateTransition, List)> GetStateTransitionsRecursive(StateGraph graph, IGraphRoot root, bool embedSubgraphsOnly = true, HashSet visited = null) 36 | { 37 | return GetStateTransitionsRecursiveInternal( 38 | graph, 39 | new(){ 40 | root = root, 41 | path = new(), 42 | visited = visited ?? new(), 43 | embedSubgraphsOnly = embedSubgraphsOnly}); 44 | } 45 | private static IEnumerable<(IStateTransition, List)> GetStateTransitionsRecursiveInternal(FlowGraph graph, GraphRecursionContext context) 46 | { 47 | if (graph == null) 48 | yield break; 49 | if(context.visited.Contains(graph)) 50 | yield break; 51 | context.visited.Add(graph); 52 | 53 | foreach(var unit in graph.units) 54 | { 55 | if(unit is SubgraphUnit subgraphUnit) 56 | { 57 | if(context.embedSubgraphsOnly 58 | && subgraphUnit.nest.source != Unity.VisualScripting.GraphSource.Embed) 59 | continue; 60 | var nestedGraph = subgraphUnit.nest.graph; 61 | if(nestedGraph != null) 62 | { 63 | context.path.Add(subgraphUnit.guid); 64 | foreach(var nestedUnit in GetStateTransitionsRecursiveInternal(nestedGraph, context)) 65 | { 66 | yield return nestedUnit; 67 | } 68 | context.path.RemoveAt(context.path.Count - 1); // Back-travel. 69 | } 70 | } 71 | else if(unit is StateUnit stateUnit) 72 | { 73 | // ! Warning: Mutual recursion with the StateGraph version of GetUnitsRecursiveInternal. 74 | if(context.embedSubgraphsOnly 75 | && stateUnit.nest.source != Unity.VisualScripting.GraphSource.Embed) 76 | continue; 77 | var nestedGraph = stateUnit.nest.graph; 78 | if(nestedGraph != null) 79 | { 80 | context.path.Add(stateUnit.guid); 81 | foreach(var nestedUnit in GetStateTransitionsRecursiveInternal(nestedGraph, context)) 82 | { 83 | yield return nestedUnit; 84 | } 85 | context.path.RemoveAt(context.path.Count - 1); // Back-travel. 86 | } 87 | } 88 | } 89 | } 90 | private static IEnumerable<(IStateTransition, List)> GetStateTransitionsRecursiveInternal(this StateGraph graph, GraphRecursionContext context) 91 | { 92 | if (graph == null) 93 | yield break; 94 | // Note that the base case here is handled by FlowGraph version of GetStateTransitionsRecursive. 95 | // We only care about units and not states here. 96 | if(context.visited.Contains(graph)) 97 | yield break; 98 | context.visited.Add(graph); 99 | 100 | // Traverse states. 101 | foreach (var state in graph.states) 102 | { 103 | // The code below may look almost identical, but the FlowState case uses the FlowGraph version of GetUnitsRecursive instead. 104 | // There's no common interface unfortunately so we have to live with code duplication. 105 | if (state is FlowState flowState) 106 | { 107 | if(context.embedSubgraphsOnly 108 | && flowState.nest.source != Unity.VisualScripting.GraphSource.Embed) 109 | continue; 110 | var nestedGraph = flowState.nest.graph; 111 | if(nestedGraph != null) 112 | { 113 | context.path.Add(flowState.guid); 114 | foreach (var nestedUnit in GetStateTransitionsRecursiveInternal(nestedGraph, context)) 115 | { 116 | yield return nestedUnit; 117 | } 118 | context.path.RemoveAt(context.path.Count - 1); 119 | } 120 | } 121 | else if(state is SuperState superState) 122 | { 123 | if(context.embedSubgraphsOnly 124 | && superState.nest.source != Unity.VisualScripting.GraphSource.Embed) 125 | continue; 126 | var nestedGraph = superState.nest.graph; 127 | if (nestedGraph != null) 128 | { 129 | context.path.Add(superState.guid); 130 | foreach (var nestedUnit in GetStateTransitionsRecursiveInternal(nestedGraph, context)) 131 | { 132 | yield return nestedUnit; 133 | } 134 | context.path.RemoveAt(context.path.Count - 1); 135 | } 136 | } 137 | } 138 | // Traverse transitions, whose graphs are always FlowGraphs. 139 | foreach(var transition in graph.transitions) 140 | { 141 | yield return (transition, context.path); 142 | if(transition is FlowStateTransition flowStateTransition) 143 | { 144 | if(context.embedSubgraphsOnly 145 | && flowStateTransition.nest.source != Unity.VisualScripting.GraphSource.Embed) 146 | continue; 147 | var nestedGraph = flowStateTransition.nest.graph; 148 | if (nestedGraph != null) 149 | { 150 | context.path.Add(flowStateTransition.guid); 151 | foreach (var nestedUnit in GetStateTransitionsRecursiveInternal(nestedGraph, context)) 152 | { 153 | yield return nestedUnit; 154 | } 155 | context.path.RemoveAt(context.path.Count - 1); 156 | } 157 | } 158 | // There doesn't seem to be other transition types. 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Editor/GraphTraversalUtility_StateTransitions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3711fc6631dec1a4cb2ba889129428a0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphTraversalUtility_States.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.VisualScripting; 5 | using System.Linq; 6 | using UnityEditor; 7 | using System; 8 | 9 | namespace CHM.VisualScriptingKai.Editor 10 | { 11 | public static partial class GraphTraversalUtility 12 | { 13 | public static IEnumerable<(IState, List)> GetStatesRecursive(this GraphSource source, bool embedSubgraphsOnly = true, HashSet visited = null) 14 | { 15 | if(source.scriptGraphAsset != null) 16 | return GetStatesRecursive(source.scriptGraphAsset.graph, source.scriptGraphAsset, embedSubgraphsOnly, visited); 17 | else if(source.stateGraphAsset != null) 18 | return GetStatesRecursive(source.stateGraphAsset.graph, source.stateGraphAsset, embedSubgraphsOnly, visited); 19 | else if(source.scriptMachine != null) 20 | return GetStatesRecursive(source.scriptMachine.graph, source.scriptMachine, embedSubgraphsOnly, visited); 21 | else if(source.stateMachine != null) 22 | return GetStatesRecursive(source.stateMachine.graph, source.stateMachine, embedSubgraphsOnly, visited); 23 | else throw new System.NullReferenceException(); // Shouldn't be reachable. 24 | } 25 | public static IEnumerable<(IState, List)> GetStatesRecursive(FlowGraph graph, IGraphRoot root, bool embedSubgraphsOnly = true, HashSet visited = null) 26 | { 27 | return GetStatesRecursiveInternal( 28 | graph, 29 | new(){ 30 | root = root, 31 | path = new(), 32 | visited = visited ?? new(), 33 | embedSubgraphsOnly = embedSubgraphsOnly}); 34 | } 35 | public static IEnumerable<(IState, List)> GetStatesRecursive(StateGraph graph, IGraphRoot root, bool embedSubgraphsOnly = true, HashSet visited = null) 36 | { 37 | return GetStatesRecursiveInternal( 38 | graph, 39 | new(){ 40 | root = root, 41 | path = new(), 42 | visited = visited ?? new(), 43 | embedSubgraphsOnly = embedSubgraphsOnly}); 44 | } 45 | private static IEnumerable<(IState, List)> GetStatesRecursiveInternal(FlowGraph graph, GraphRecursionContext context) 46 | { 47 | if (graph == null) 48 | yield break; 49 | if(context.visited.Contains(graph)) 50 | yield break; 51 | context.visited.Add(graph); 52 | 53 | foreach(var unit in graph.units) 54 | { 55 | if(unit is SubgraphUnit subgraphUnit) 56 | { 57 | if(context.embedSubgraphsOnly 58 | && subgraphUnit.nest.source != Unity.VisualScripting.GraphSource.Embed) 59 | continue; 60 | var nestedGraph = subgraphUnit.nest.graph; 61 | if(nestedGraph != null) 62 | { 63 | context.path.Add(subgraphUnit.guid); 64 | foreach(var nestedUnit in GetStatesRecursiveInternal(nestedGraph, context)) 65 | { 66 | yield return nestedUnit; 67 | } 68 | context.path.RemoveAt(context.path.Count - 1); // Back-travel. 69 | } 70 | } 71 | else if(unit is StateUnit stateUnit) 72 | { 73 | // ! Warning: Mutual recursion with the StateGraph version of GetUnitsRecursiveInternal. 74 | if(context.embedSubgraphsOnly 75 | && stateUnit.nest.source != Unity.VisualScripting.GraphSource.Embed) 76 | continue; 77 | var nestedGraph = stateUnit.nest.graph; 78 | if(nestedGraph != null) 79 | { 80 | context.path.Add(stateUnit.guid); 81 | foreach(var nestedUnit in GetStatesRecursiveInternal(nestedGraph, context)) 82 | { 83 | yield return nestedUnit; 84 | } 85 | context.path.RemoveAt(context.path.Count - 1); // Back-travel. 86 | } 87 | } 88 | } 89 | } 90 | private static IEnumerable<(IState, List)> GetStatesRecursiveInternal(this StateGraph graph, GraphRecursionContext context) 91 | { 92 | if (graph == null) 93 | yield break; 94 | // Note that the base case here is handled by FlowGraph version of GetStatesRecursive. 95 | // We only care about units and not states here. 96 | if(context.visited.Contains(graph)) 97 | yield break; 98 | context.visited.Add(graph); 99 | 100 | // Traverse states. 101 | foreach (var state in graph.states) 102 | { 103 | yield return (state, context.path); 104 | // The code below may look almost identical, but the FlowState case uses the FlowGraph version of GetUnitsRecursive instead. 105 | // There's no common interface unfortunately so we have to live with code duplication. 106 | if (state is FlowState flowState) 107 | { 108 | if(context.embedSubgraphsOnly 109 | && flowState.nest.source != Unity.VisualScripting.GraphSource.Embed) 110 | continue; 111 | var nestedGraph = flowState.nest.graph; 112 | if(nestedGraph != null) 113 | { 114 | context.path.Add(flowState.guid); 115 | foreach (var nestedUnit in GetStatesRecursiveInternal(nestedGraph, context)) 116 | { 117 | yield return nestedUnit; 118 | } 119 | context.path.RemoveAt(context.path.Count - 1); 120 | } 121 | } 122 | else if(state is SuperState superState) 123 | { 124 | if(context.embedSubgraphsOnly 125 | && superState.nest.source != Unity.VisualScripting.GraphSource.Embed) 126 | continue; 127 | var nestedGraph = superState.nest.graph; 128 | if (nestedGraph != null) 129 | { 130 | context.path.Add(superState.guid); 131 | foreach (var nestedUnit in GetStatesRecursiveInternal(nestedGraph, context)) 132 | { 133 | yield return nestedUnit; 134 | } 135 | context.path.RemoveAt(context.path.Count - 1); 136 | } 137 | } 138 | } 139 | // Traverse transitions, whose graphs are always FlowGraphs. 140 | foreach(var transition in graph.transitions) 141 | { 142 | if(transition is FlowStateTransition flowStateTransition) 143 | { 144 | if(context.embedSubgraphsOnly 145 | && flowStateTransition.nest.source != Unity.VisualScripting.GraphSource.Embed) 146 | continue; 147 | var nestedGraph = flowStateTransition.nest.graph; 148 | if (nestedGraph != null) 149 | { 150 | context.path.Add(flowStateTransition.guid); 151 | foreach (var nestedUnit in GetStatesRecursiveInternal(nestedGraph, context)) 152 | { 153 | yield return nestedUnit; 154 | } 155 | context.path.RemoveAt(context.path.Count - 1); 156 | } 157 | } 158 | // There doesn't seem to be other transition types. 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Editor/GraphTraversalUtility_States.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2df4c58b979ca8f4f97f899cfcf7f942 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphTraversalUtility_StickyNotes.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.VisualScripting; 5 | using System.Linq; 6 | using UnityEditor; 7 | using System; 8 | 9 | 10 | namespace CHM.VisualScriptingKai.Editor 11 | { 12 | public static partial class GraphTraversalUtility 13 | { 14 | public static IEnumerable<(StickyNote, List)> GetStickyNotesRecursive(this GraphSource source, bool embedSubgraphsOnly = true, HashSet visited = null) 15 | { 16 | if(source.scriptGraphAsset != null) 17 | return GetStickyNotesRecursive(source.scriptGraphAsset.graph, source.scriptGraphAsset, embedSubgraphsOnly, visited); 18 | else if(source.stateGraphAsset != null) 19 | return GetStickyNotesRecursive(source.stateGraphAsset.graph, source.stateGraphAsset, embedSubgraphsOnly, visited); 20 | else if(source.scriptMachine != null) 21 | return GetStickyNotesRecursive(source.scriptMachine.graph, source.scriptMachine, embedSubgraphsOnly, visited); 22 | else if(source.stateMachine != null) 23 | return GetStickyNotesRecursive(source.stateMachine.graph, source.stateMachine, embedSubgraphsOnly, visited); 24 | else throw new System.NullReferenceException(); // Shouldn't be reachable. 25 | } 26 | public static IEnumerable<(StickyNote, List)> GetStickyNotesRecursive(FlowGraph graph, IGraphRoot root, bool embedSubgraphsOnly = true, HashSet visited = null) 27 | { 28 | return GetStickyNotesRecursiveInternal( 29 | graph, 30 | new(){ 31 | root = root, 32 | path = new(), 33 | visited = visited ?? new(), 34 | embedSubgraphsOnly = embedSubgraphsOnly}); 35 | } 36 | public static IEnumerable<(StickyNote, List)> GetStickyNotesRecursive(StateGraph graph, IGraphRoot root, bool embedSubgraphsOnly = true, HashSet visited = null) 37 | { 38 | return GetStickyNotesRecursiveInternal( 39 | graph, 40 | new(){ 41 | root = root, 42 | path = new(), 43 | visited = visited ?? new(), 44 | embedSubgraphsOnly = embedSubgraphsOnly}); 45 | } 46 | private static IEnumerable<(StickyNote, List)> GetStickyNotesRecursiveInternal(FlowGraph graph, GraphRecursionContext context) 47 | { 48 | if (graph == null) 49 | yield break; 50 | if(context.visited.Contains(graph)) 51 | yield break; 52 | context.visited.Add(graph); 53 | 54 | foreach(var unit in graph.units) 55 | { 56 | if(unit is SubgraphUnit subgraphUnit) 57 | { 58 | if(context.embedSubgraphsOnly 59 | && subgraphUnit.nest.source != Unity.VisualScripting.GraphSource.Embed) 60 | continue; 61 | var nestedGraph = subgraphUnit.nest.graph; 62 | if(nestedGraph != null) 63 | { 64 | context.path.Add(subgraphUnit.guid); 65 | foreach(var nestedUnit in GetStickyNotesRecursiveInternal(nestedGraph, context)) 66 | { 67 | yield return nestedUnit; 68 | } 69 | context.path.RemoveAt(context.path.Count - 1); // Back-travel. 70 | } 71 | } 72 | else if(unit is StateUnit stateUnit) 73 | { 74 | // ! Warning: Mutual recursion with the StateGraph version of GetStickyNotesRecursiveInternal. 75 | if(context.embedSubgraphsOnly 76 | && stateUnit.nest.source != Unity.VisualScripting.GraphSource.Embed) 77 | continue; 78 | var nestedGraph = stateUnit.nest.graph; 79 | if(nestedGraph != null) 80 | { 81 | context.path.Add(stateUnit.guid); 82 | foreach(var nestedUnit in GetStickyNotesRecursiveInternal(nestedGraph, context)) 83 | { 84 | yield return nestedUnit; 85 | } 86 | context.path.RemoveAt(context.path.Count - 1); // Back-travel. 87 | } 88 | } 89 | } 90 | // Depth-first, so we yield the sticky notes after recursing. 91 | foreach(var stickyNote in graph.sticky) 92 | { 93 | yield return (stickyNote, context.path); 94 | } 95 | } 96 | private static IEnumerable<(StickyNote, List)> GetStickyNotesRecursiveInternal(this StateGraph graph, GraphRecursionContext context) 97 | { 98 | if (graph == null) 99 | yield break; 100 | // Note that the base case here is handled by FlowGraph version of GetUnitsRecursive. 101 | // We only care about units and not states here. 102 | if(context.visited.Contains(graph)) 103 | yield break; 104 | context.visited.Add(graph); 105 | 106 | // Traverse states. 107 | foreach (var state in graph.states) 108 | { 109 | // The code below may look almost identical, but the FlowState case uses the FlowGraph version of GetUnitsRecursive instead. 110 | // There's no common interface unfortunately so we have to live with code duplication. 111 | if (state is FlowState flowState) 112 | { 113 | if(context.embedSubgraphsOnly 114 | && flowState.nest.source != Unity.VisualScripting.GraphSource.Embed) 115 | continue; 116 | var nestedGraph = flowState.nest.graph; 117 | if(nestedGraph != null) 118 | { 119 | context.path.Add(flowState.guid); 120 | foreach (var nestedUnit in GetStickyNotesRecursiveInternal(nestedGraph, context)) 121 | { 122 | yield return nestedUnit; 123 | } 124 | context.path.RemoveAt(context.path.Count - 1); 125 | } 126 | } 127 | else if(state is SuperState superState) 128 | { 129 | if(context.embedSubgraphsOnly 130 | && superState.nest.source != Unity.VisualScripting.GraphSource.Embed) 131 | continue; 132 | var nestedGraph = superState.nest.graph; 133 | if (nestedGraph != null) 134 | { 135 | context.path.Add(superState.guid); 136 | foreach (var nestedUnit in GetStickyNotesRecursiveInternal(nestedGraph, context)) 137 | { 138 | yield return nestedUnit; 139 | } 140 | context.path.RemoveAt(context.path.Count - 1); 141 | } 142 | } 143 | } 144 | // Traverse transitions, whose graphs are always FlowGraphs. 145 | foreach(var transition in graph.transitions) 146 | { 147 | if(transition is FlowStateTransition flowStateTransition) 148 | { 149 | if(context.embedSubgraphsOnly 150 | && flowStateTransition.nest.source != Unity.VisualScripting.GraphSource.Embed) 151 | continue; 152 | var nestedGraph = flowStateTransition.nest.graph; 153 | if (nestedGraph != null) 154 | { 155 | context.path.Add(flowStateTransition.guid); 156 | foreach (var nestedUnit in GetStickyNotesRecursiveInternal(nestedGraph, context)) 157 | { 158 | yield return nestedUnit; 159 | } 160 | context.path.RemoveAt(context.path.Count - 1); 161 | } 162 | } 163 | // There doesn't seem to be other transition types. 164 | } 165 | // Depth-first, so we yield the sticky notes after recursing. 166 | foreach(var stickyNote in graph.sticky) 167 | { 168 | yield return (stickyNote, context.path); 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Editor/GraphTraversalUtility_StickyNotes.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 901051887fb066048afe052b25617644 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphTraversalUtility_Units.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.VisualScripting; 5 | using System.Linq; 6 | using UnityEditor; 7 | using System; 8 | 9 | namespace CHM.VisualScriptingKai.Editor 10 | { 11 | public static partial class GraphTraversalUtility 12 | { 13 | public static IEnumerable<(IUnit, List)> GetUnitsRecursive(this GraphSource source, bool embedSubgraphsOnly = true, HashSet visited = null) 14 | { 15 | if(source.scriptGraphAsset != null) 16 | return GetUnitsRecursive(source.scriptGraphAsset.graph, source.scriptGraphAsset, embedSubgraphsOnly, visited); 17 | else if(source.stateGraphAsset != null) 18 | return GetUnitsRecursive(source.stateGraphAsset.graph, source.stateGraphAsset, embedSubgraphsOnly, visited); 19 | else if(source.scriptMachine != null) 20 | return GetUnitsRecursive(source.scriptMachine.graph, source.scriptMachine, embedSubgraphsOnly, visited); 21 | else if(source.stateMachine != null) 22 | return GetUnitsRecursive(source.stateMachine.graph, source.stateMachine, embedSubgraphsOnly, visited); 23 | else throw new System.NullReferenceException(); // Shouldn't be reachable. 24 | } 25 | public static IEnumerable<(IUnit, List)> GetUnitsRecursive(FlowGraph graph, IGraphRoot root, bool embedSubgraphsOnly = true, HashSet visited = null) 26 | { 27 | return GetUnitsRecursiveInternal( 28 | graph, 29 | new(){ 30 | root = root, 31 | path = new(), 32 | visited = visited ?? new(), 33 | embedSubgraphsOnly = embedSubgraphsOnly}); 34 | } 35 | public static IEnumerable<(IUnit, List)> GetUnitsRecursive(StateGraph graph, IGraphRoot root, bool embedSubgraphsOnly = true, HashSet visited = null) 36 | { 37 | return GetUnitsRecursiveInternal( 38 | graph, 39 | new(){ 40 | root = root, 41 | path = new(), 42 | visited = visited ?? new(), 43 | embedSubgraphsOnly = embedSubgraphsOnly}); 44 | } 45 | private static IEnumerable<(IUnit, List)> GetUnitsRecursiveInternal(FlowGraph graph, GraphRecursionContext context) 46 | { 47 | if (graph == null) 48 | yield break; 49 | if(context.visited.Contains(graph)) 50 | yield break; 51 | context.visited.Add(graph); 52 | 53 | foreach(var unit in graph.units) 54 | { 55 | // NOT else, so SubgraphUnits get included as well. 56 | yield return (unit, context.path); 57 | if(unit is SubgraphUnit subgraphUnit) 58 | { 59 | if(context.embedSubgraphsOnly 60 | && subgraphUnit.nest.source != Unity.VisualScripting.GraphSource.Embed) 61 | continue; 62 | var nestedGraph = subgraphUnit.nest.graph; 63 | if(nestedGraph != null) 64 | { 65 | context.path.Add(subgraphUnit.guid); 66 | foreach(var nestedUnit in GetUnitsRecursiveInternal(nestedGraph, context)) 67 | { 68 | yield return nestedUnit; 69 | } 70 | context.path.RemoveAt(context.path.Count - 1); // Back-travel. 71 | } 72 | } 73 | else if(unit is StateUnit stateUnit) 74 | { 75 | // ! Warning: Mutual recursion with the StateGraph version of GetUnitsRecursiveInternal. 76 | if(context.embedSubgraphsOnly 77 | && stateUnit.nest.source != Unity.VisualScripting.GraphSource.Embed) 78 | continue; 79 | var nestedGraph = stateUnit.nest.graph; 80 | if(nestedGraph != null) 81 | { 82 | context.path.Add(stateUnit.guid); 83 | foreach(var nestedUnit in GetUnitsRecursiveInternal(nestedGraph, context)) 84 | { 85 | yield return nestedUnit; 86 | } 87 | context.path.RemoveAt(context.path.Count - 1); // Back-travel. 88 | } 89 | } 90 | } 91 | } 92 | private static IEnumerable<(IUnit, List)> GetUnitsRecursiveInternal(this StateGraph graph, GraphRecursionContext context) 93 | { 94 | if (graph == null) 95 | yield break; 96 | // Note that the base case here is handled by FlowGraph version of GetUnitsRecursive. 97 | // We only care about units and not states here. 98 | if(context.visited.Contains(graph)) 99 | yield break; 100 | context.visited.Add(graph); 101 | 102 | // Traverse states. 103 | foreach (var state in graph.states) 104 | { 105 | // The code below may look almost identical, but the FlowState case uses the FlowGraph version of GetUnitsRecursive instead. 106 | // There's no common interface unfortunately so we have to live with code duplication. 107 | if (state is FlowState flowState) 108 | { 109 | if(context.embedSubgraphsOnly 110 | && flowState.nest.source != Unity.VisualScripting.GraphSource.Embed) 111 | continue; 112 | var nestedGraph = flowState.nest.graph; 113 | if(nestedGraph != null) 114 | { 115 | context.path.Add(flowState.guid); 116 | foreach (var nestedUnit in GetUnitsRecursiveInternal(nestedGraph, context)) 117 | { 118 | yield return nestedUnit; 119 | } 120 | context.path.RemoveAt(context.path.Count - 1); 121 | } 122 | } 123 | else if(state is SuperState superState) 124 | { 125 | if(context.embedSubgraphsOnly 126 | && superState.nest.source != Unity.VisualScripting.GraphSource.Embed) 127 | continue; 128 | var nestedGraph = superState.nest.graph; 129 | if (nestedGraph != null) 130 | { 131 | context.path.Add(superState.guid); 132 | foreach (var nestedUnit in GetUnitsRecursiveInternal(nestedGraph, context)) 133 | { 134 | yield return nestedUnit; 135 | } 136 | context.path.RemoveAt(context.path.Count - 1); 137 | } 138 | } 139 | } 140 | // Traverse transitions, whose graphs are always FlowGraphs. 141 | foreach(var transition in graph.transitions) 142 | { 143 | if(transition is FlowStateTransition flowStateTransition) 144 | { 145 | if(context.embedSubgraphsOnly 146 | && flowStateTransition.nest.source != Unity.VisualScripting.GraphSource.Embed) 147 | continue; 148 | var nestedGraph = flowStateTransition.nest.graph; 149 | if (nestedGraph != null) 150 | { 151 | context.path.Add(flowStateTransition.guid); 152 | foreach (var nestedUnit in GetUnitsRecursiveInternal(nestedGraph, context)) 153 | { 154 | yield return nestedUnit; 155 | } 156 | context.path.RemoveAt(context.path.Count - 1); 157 | } 158 | } 159 | // There doesn't seem to be other transition types. 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Editor/GraphTraversalUtility_Units.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2873d096aa76c8f449611dabc35491a8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/GraphUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.VisualScripting; 5 | using System.Linq; 6 | using UnityEditor; 7 | using System.Text.RegularExpressions; 8 | using UnityEditor.Search; 9 | 10 | namespace CHM.VisualScriptingKai.Editor 11 | { 12 | public static class GraphUtility 13 | { 14 | // TODO: Maybe turn this into an Object Reference Finder feature? 15 | public static IEnumerable FindReferencesToObject(IEnumerable sources, Object target, bool embedSubgraphsOnly = true) 16 | { 17 | HashSet visited = new(); 18 | foreach(var source in sources) 19 | foreach(var exceptionTrace in FindReferencesToObject(source, target, embedSubgraphsOnly, visited)) 20 | yield return exceptionTrace; 21 | } 22 | public static IEnumerable FindReferencesToObject(GraphSource source, Object target, bool embedSubgraphsOnly = true, HashSet visited = null) 23 | { 24 | // Ignore missing references / null. 25 | if(!target) 26 | yield break; 27 | foreach(var (unit, path) in source.GetUnitsRecursive(embedSubgraphsOnly, visited)) 28 | { 29 | bool hasSameReference = false; 30 | foreach(var value in unit.defaultValues.Values) 31 | { 32 | if(value is Object valueAsObject 33 | && valueAsObject 34 | && valueAsObject == target) 35 | { 36 | hasSameReference = true; 37 | break; 38 | } 39 | } 40 | hasSameReference |= unit is Literal literal 41 | && literal.value is Object literalRef 42 | && literalRef 43 | && literalRef == target; 44 | if(hasSameReference) 45 | { 46 | var reference = GraphReference.New((Object) source, path, false); 47 | yield return new NodeTrace(){ 48 | unit = unit, 49 | Reference = reference, 50 | Source = source, 51 | Score = 0, 52 | }; 53 | } 54 | } 55 | } 56 | public static IEnumerable FindAllExceptions(IEnumerable sources, bool embedSubgraphsOnly = false) 57 | { 58 | HashSet visited = new(); 59 | foreach(var source in sources) 60 | foreach(var exceptionTrace in FindAllExceptions(source, embedSubgraphsOnly, visited)) 61 | yield return exceptionTrace; 62 | } 63 | public static IEnumerable FindAllExceptions(GraphSource source, bool embedSubgraphsOnly = false, HashSet visited = null) 64 | { 65 | foreach(var (unit, path) in source.GetUnitsRecursive(embedSubgraphsOnly, visited)) 66 | { 67 | var reference = GraphReference.New((Object) source, path, false); 68 | var exception = unit.GetException(reference); 69 | if(exception != null) 70 | { 71 | yield return new ExceptionTrace(){ 72 | unit = unit, 73 | Reference = reference, 74 | Source = source, 75 | }; 76 | } 77 | } 78 | } 79 | public static IEnumerable FindWarnings(IEnumerable sources, WarningLevel warningLevel, bool embedSubgraphsOnly = false) 80 | { 81 | HashSet visited = new(); 82 | foreach(var source in sources) 83 | foreach(var exceptionTrace in FindWarnings(source, warningLevel, embedSubgraphsOnly, visited)) 84 | yield return exceptionTrace; 85 | } 86 | public static IEnumerable FindWarnings(GraphSource source, WarningLevel warningLevel, bool embedSubgraphsOnly = true, HashSet visited = null) 87 | { 88 | foreach(var (unit, path) in source.GetUnitsRecursive(embedSubgraphsOnly, visited)) 89 | { 90 | var reference = GraphReference.New((Object) source, path, false); 91 | if(unit.Analysis(reference) is var unitAnalysis 92 | && unitAnalysis.warnings.Count > 0) 93 | { 94 | foreach(var warning in unitAnalysis.warnings) 95 | { 96 | if((int)warning.level >= (int)warningLevel) 97 | { 98 | yield return new WarningTrace(){ 99 | unit = unit, 100 | warning = warning, 101 | Reference = reference, 102 | Source = source, 103 | }; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | public static IEnumerable FindNodes(IEnumerable sources, string pattern, bool embedSubgraphsOnly = true) 110 | { 111 | HashSet visited = new(); 112 | foreach(var source in sources) 113 | foreach(var nodeTrace in FindNodes(source, pattern, embedSubgraphsOnly, visited)) 114 | yield return nodeTrace; 115 | } 116 | public static IEnumerable FindNodes(GraphSource source, string pattern, bool embedSubgraphsOnly = true, HashSet visited = null) 117 | { 118 | if(pattern.Length == 0) 119 | yield break; 120 | foreach (var (unit, path) in source.GetUnitsRecursive(embedSubgraphsOnly, visited)) 121 | { 122 | // Why isn't this out??? Even though it is always overwritten internally??? 123 | // Note: Bigger outScore -> better 124 | long outScore = 0; 125 | if(FuzzySearch.FuzzyMatch(pattern, unit.Name(), ref outScore)) 126 | { 127 | yield return new NodeTrace() 128 | { 129 | unit = unit, 130 | Reference = GraphReference.New((Object) source, path, false), 131 | Source = source, 132 | Score = outScore, 133 | }; 134 | } 135 | } 136 | } 137 | public static IEnumerable FindStickyNotes(IEnumerable sources, string pattern, bool embedSubgraphsOnly = true) 138 | { 139 | // TODO: Expose this visited set as parameter 140 | HashSet visited = new(); 141 | foreach(var source in sources) 142 | foreach(var nodeTrace in FindStickyNotes(source, pattern, embedSubgraphsOnly, visited)) 143 | yield return nodeTrace; 144 | } 145 | public static IEnumerable FindStickyNotes(GraphSource source, string pattern, bool embedSubgraphsOnly = true, HashSet visited = null) 146 | { 147 | if(pattern.Length == 0) 148 | yield break; 149 | foreach (var (stickyNote, path) in source.GetStickyNotesRecursive(embedSubgraphsOnly, visited)) 150 | { 151 | long outScore = 0; 152 | if(FuzzySearch.FuzzyMatch(pattern, stickyNote.title, ref outScore)) 153 | { 154 | yield return new StickyNoteTrace() 155 | { 156 | stickyNote = stickyNote, 157 | Reference = GraphReference.New((Object) source, path, false), 158 | Source = source, 159 | Score = outScore, 160 | }; 161 | } 162 | } 163 | } 164 | public static IEnumerable FindStates(IEnumerable sources, string pattern, bool embedSubgraphsOnly = true) 165 | { 166 | // TODO: Expose this visited set as parameter 167 | HashSet visited = new(); 168 | foreach(var source in sources) 169 | foreach(var nodeTrace in FindStates(source, pattern, embedSubgraphsOnly, visited)) 170 | yield return nodeTrace; 171 | } 172 | public static IEnumerable FindStates(GraphSource source, string pattern, bool embedSubgraphsOnly = true, HashSet visited = null) 173 | { 174 | if(pattern.Length == 0) 175 | yield break; 176 | foreach (var (state, path) in source.GetStatesRecursive(embedSubgraphsOnly, visited)) 177 | { 178 | long outScore = 0; 179 | if(FuzzySearch.FuzzyMatch(pattern, state.Name(), ref outScore)) 180 | { 181 | yield return new StateTrace() 182 | { 183 | state = state, 184 | Reference = GraphReference.New((Object) source, path, false), 185 | Source = source, 186 | Score = outScore, 187 | }; 188 | } 189 | } 190 | } 191 | public static IEnumerable FindStateTransitions(IEnumerable sources, string pattern, bool embedSubgraphsOnly = true) 192 | { 193 | // TODO: Expose this visited set as parameter 194 | HashSet visited = new(); 195 | foreach(var source in sources) 196 | foreach(var nodeTrace in FindStateTransitions(source, pattern, embedSubgraphsOnly, visited)) 197 | yield return nodeTrace; 198 | } 199 | public static IEnumerable FindStateTransitions(GraphSource source, string pattern, bool embedSubgraphsOnly = true, HashSet visited = null) 200 | { 201 | if(pattern.Length == 0) 202 | yield break; 203 | foreach (var (stateTransition, path) in source.GetStateTransitionsRecursive(embedSubgraphsOnly, visited)) 204 | { 205 | long outScore = 0; 206 | if(FuzzySearch.FuzzyMatch(pattern, stateTransition.Name(), ref outScore)) 207 | { 208 | yield return new StateTransitionTrace() 209 | { 210 | stateTransition = stateTransition, 211 | Reference = GraphReference.New((Object) source, path, false), 212 | Source = source, 213 | Score = outScore, 214 | }; 215 | } 216 | } 217 | } 218 | [MenuItem("Tools/Visual Scripting/Clear exceptions in active window")] 219 | public static void ClearExceptionsInActiveWindow() 220 | { 221 | if(GraphWindow.activeContext == null) 222 | return; 223 | if(GraphWindow.activeContext.graph is FlowGraph flowGraph) 224 | { 225 | foreach(var (unit, path) in GraphTraversalUtility.GetUnitsRecursive(flowGraph, GraphWindow.activeReference.root, false)) 226 | unit.SetException(GraphWindow.activeReference, null); 227 | } 228 | else if(GraphWindow.activeContext.graph is StateGraph stateGraph) 229 | { 230 | foreach(var (unit, path) in GraphTraversalUtility.GetUnitsRecursive(stateGraph, GraphWindow.activeReference.root, false)) 231 | unit.SetException(GraphWindow.activeReference, null); 232 | } 233 | else Debug.LogWarning($"Unknown graph type: {GraphWindow.activeContext.graph.GetType()}"); 234 | } 235 | [MenuItem("Tools/Visual Scripting/Print graph statistics to console")] 236 | public static void PrintGraphStatistics() 237 | { 238 | var sources = FindAllGraphSources().ToList(); 239 | int unitCount = 0, stickyNoteCount = 0, stateCount = 0, transitionCount = 0; 240 | foreach(var source in sources) 241 | foreach(var unit in source.GetUnitsRecursive()) 242 | unitCount++; 243 | foreach(var source in sources) 244 | foreach(var stickyNote in source.GetStickyNotesRecursive()) 245 | stickyNoteCount++; 246 | foreach(var source in sources) 247 | foreach(var state in source.GetStatesRecursive()) 248 | stateCount++; 249 | foreach(var source in sources) 250 | foreach(var stateTransition in source.GetStateTransitionsRecursive()) 251 | transitionCount++; 252 | Debug.Log($"Number of Units: {unitCount}"); 253 | Debug.Log($"Number of Sticky Notes: {stickyNoteCount}"); 254 | Debug.Log($"Number of States: {stateCount}"); 255 | Debug.Log($"Number of State Transitions: {transitionCount}"); 256 | } 257 | public static GraphSource GetEditedGraph() 258 | { 259 | var rootObject = GraphWindow.activeContext?.reference?.rootObject; 260 | if(rootObject != null) 261 | { 262 | if(rootObject is ScriptGraphAsset scriptGraphAsset) 263 | { 264 | return new GraphSource(scriptGraphAsset); 265 | } 266 | else if(rootObject is StateGraphAsset stateGraphAsset) 267 | { 268 | return new GraphSource(stateGraphAsset); 269 | } 270 | else if(rootObject is ScriptMachine scriptMachine) 271 | { 272 | return new GraphSource(scriptMachine); 273 | } 274 | else if(rootObject is StateMachine stateMachine) 275 | { 276 | return new GraphSource(stateMachine); 277 | } 278 | } 279 | return null; 280 | } 281 | /// 282 | /// Find all ScriptGraphAsset/StateGraphAssets in the specified folders. 283 | /// 284 | /// Folders to search in. 285 | /// 286 | public static IEnumerable FindAllGraphAssets(string[] searchInFolders = null) 287 | { 288 | return AssetUtility.FindAssetsByType(searchInFolders) 289 | .Select(x => new GraphSource(x)) 290 | .Concat( 291 | AssetUtility.FindAssetsByType(searchInFolders) 292 | .Select(x => new GraphSource(x))); 293 | } 294 | /// 295 | /// Runtime graph sources simply refer to loaded ScriptMachine/StateMachine instances. 296 | /// You may want to use runtime graph sources for runtime debugging purposes. 297 | /// 298 | /// 299 | public static IEnumerable FindAllRuntimeGraphSources() 300 | { 301 | FindObjectsInactive findObjectsInactive = FindObjectsInactive.Include; 302 | FindObjectsSortMode sortMode = FindObjectsSortMode.None; 303 | return Object.FindObjectsByType(findObjectsInactive, sortMode) 304 | .Select(x => new GraphSource(x)) 305 | .Concat( 306 | Object.FindObjectsByType(findObjectsInactive, sortMode) 307 | .Select(x => new GraphSource(x))); 308 | } 309 | /// 310 | /// Find all graph sources, which can then be used in queries. 311 | /// 312 | /// 313 | /// Note: ScriptGraphs/StateGraphs embedded in ScriptMachines/StateMachines 314 | /// can only be found in the currently-loaded scene/prefab. 315 | ///

316 | /// The alternative would be to additively-load all scenes and prefabs in searchInFolders, 317 | /// and would be very taxing on the editor application, so this is just not supported. 318 | ///

319 | /// In general, to make the best use of Visual Scripting Plus, 320 | /// avoid using embed graphs in ScriptMachines/StateMachines as much as possible. 321 | ///
322 | /// Folders to search in for ScriptGraphAssets/StateGraphAssets. 323 | /// 324 | public static IEnumerable FindAllGraphSources(string[] searchInFolders = null) 325 | { 326 | return FindAllGraphAssets(searchInFolders) 327 | .Concat(FindAllRuntimeGraphSources()); 328 | } 329 | 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /Editor/GraphUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0cd78ba46afeb3347a6ac6a8f1e8bf61 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/IGraphElementTrace.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.VisualScripting; 5 | using UnityEditor; 6 | 7 | namespace CHM.VisualScriptingKai.Editor 8 | { 9 | public interface IGraphElementTrace : System.IComparable 10 | { 11 | abstract IGraphElement GraphElement { get; } 12 | GraphReference Reference { get; set; } 13 | GraphSource Source { get; set; } 14 | public long Score { get; set; } 15 | public Vector2 GraphPosition { get; } 16 | string GetInfo(); 17 | Texture2D GetIcon(int resolution); 18 | /// 19 | /// Open the graph editor and make it jump to the graph element. 20 | /// 21 | void GraphWindowJumpToLocation() 22 | { 23 | if(!Reference.isValid) 24 | { 25 | Debug.LogWarning("Invalid graph reference. This may be a bug. Please try again."); 26 | return; 27 | } 28 | var sourceObject = (Object) Source; 29 | // TODO: This part of the code will destroy the graph reference. 30 | // Will probably need to restore the reference somehow afterwards. 31 | // For now this is okay though, any potential scene here must be 32 | // the currently-loaded scene. 33 | // // If the source is a GameObject, open its scene or prefab first. 34 | // if(source.TryGetGameObject(out var gameObject)) 35 | // { 36 | // if(gameObject.scene != null) 37 | // AssetDatabase.OpenAsset(AssetDatabase.LoadAssetAtPath(gameObject.scene.path)); 38 | // else AssetDatabase.OpenAsset(gameObject); 39 | // } 40 | 41 | // ! For some reasons, parent graphs that can be accessed after jumping 42 | // ! cause the VisualScriptingCanvas to lose its GraphWindow reference. 43 | 44 | // ! OnGUI -> current context's canvas's window is set 45 | // ! -> Clicking breadcrumb -> context changes, but new context's canvas's window is not set 46 | // ! -> OnGUI continues, null reference with canvas.window 47 | // ! Ultimately this is because the breadcrumb reference became invalid, which made the decorator get a new context. 48 | // * Hacky fix implemented in GraphLensWindow by using the context changed callback to fix the reference. 49 | 50 | // Either way, we still need to open the graph asset or MonoBehaviour script. 51 | AssetDatabase.OpenAsset(sourceObject); 52 | EditorGUIUtility.PingObject(sourceObject); 53 | GraphWindow.OpenActive(Reference); 54 | 55 | // Pan the graph to the node's position. 56 | // Consider tweening? Maybe not? 57 | GraphWindow.activeContext.graph.pan = GraphPosition; 58 | } 59 | } 60 | public static class GraphElementTraceExtensions 61 | { 62 | /// 63 | /// Default CompareTo implementation for IGraphElementTrace. 64 | /// 65 | /// 66 | /// 67 | /// 68 | public static int DefaultCompareTo(this IGraphElementTrace @this, IGraphElementTrace other) 69 | { 70 | // Sort descending. 71 | int compare = -@this.Score.CompareTo(other.Score); 72 | if(compare != 0) 73 | return compare; 74 | compare = -@this.Source.GetInstanceID().CompareTo(other.Source.GetInstanceID()); 75 | if(compare != 0) 76 | return compare; 77 | // Don't use this, graph is transient and you can end up with different instances pointing to the "same" graph. 78 | // compare = @this.Reference.graph.GetHashCode().CompareTo(other.Reference.graph.GetHashCode()); 79 | // if(compare != 0) 80 | // return compare; 81 | compare = @this.GraphPosition.x.CompareTo(other.GraphPosition.x); 82 | if(compare != 0) 83 | return compare; 84 | return @this.GraphPosition.y.CompareTo(other.GraphPosition.y); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Editor/IGraphElementTrace.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 39d872b5f2f91a242b081de6867fe5c1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ListUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System; 4 | 5 | namespace CHM.VisualScriptingKai.Editor 6 | { 7 | public static class ListUtility 8 | { 9 | public static void AddSorted(this List @this, T item) where T: IComparable 10 | { 11 | if (@this.Count == 0) 12 | { 13 | @this.Add(item); 14 | return; 15 | } 16 | if (@this[@this.Count-1].CompareTo(item) <= 0) 17 | { 18 | @this.Add(item); 19 | return; 20 | } 21 | if (@this[0].CompareTo(item) >= 0) 22 | { 23 | @this.Insert(0, item); 24 | return; 25 | } 26 | int index = @this.BinarySearch(item); 27 | if (index < 0) 28 | index = ~index; 29 | @this.Insert(index, item); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Editor/ListUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 71d66ad123da5724db594d318d4aa729 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/NodeTrace.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using Unity.VisualScripting; 5 | using UnityEditor; 6 | 7 | namespace CHM.VisualScriptingKai.Editor 8 | { 9 | 10 | /// 11 | /// Data structure that can be used to locate a node's location. 12 | /// 13 | public struct NodeTrace : IGraphElementTrace 14 | { 15 | public readonly IGraphElement GraphElement => unit; 16 | public IUnit unit; 17 | public GraphReference Reference { get; set; } 18 | public GraphSource Source { get; set; } 19 | public long Score { get; set; } 20 | public readonly Vector2 GraphPosition => unit.position; 21 | public readonly int CompareTo(IGraphElementTrace other) 22 | => this.DefaultCompareTo(other); 23 | public readonly string GetInfo() 24 | { 25 | return $"{unit.Name()}" 26 | + $"\n{Source.Info}"; 27 | } 28 | public readonly Texture2D GetIcon(int resolution) 29 | { 30 | // Cursed operator overload. Gets texture with resolution. 31 | return unit.Icon()[resolution]; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Editor/NodeTrace.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 91aed96af5995e440adae45db38ac606 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/PackageUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace CHM.VisualScriptingKai.Editor 7 | { 8 | public static class PackageUtility 9 | { 10 | public const string PackageRoot = "Packages/com.chocola-mint.visual-scripting-kai/"; 11 | /// 12 | /// Loads an asset from com.chocola-mint.visual-scripting-kai using relative paths. 13 | /// 14 | public static T LoadPackageAsset(string packageAssetPath) 15 | where T : Object 16 | { 17 | return AssetDatabase.LoadAssetAtPath(PackageRoot + packageAssetPath); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Editor/PackageUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 006908725ed1c594590dca2529943f4f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/QueryResultsListView.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using UnityEngine.UIElements; 7 | 8 | namespace CHM.VisualScriptingKai.Editor 9 | { 10 | public class QueryResultsListView : ListView 11 | { 12 | public new class UxmlFactory : UxmlFactory 13 | { 14 | } 15 | private class QueryResultsEntry 16 | { 17 | private Label infoLabel; 18 | private VisualElement icon; 19 | private VisualElement container; 20 | public QueryResultsEntry(VisualElement visualElement, VisualElement listContainer) 21 | { 22 | infoLabel = visualElement.Q