using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using TNode.TNodeCore.Runtime.Components; using TNodeCore.Runtime; using TNodeCore.Runtime.Components; using TNodeCore.Runtime.Extensions; using TNodeCore.Runtime.Models; using TNodeCore.Runtime.RuntimeModels; using UnityEngine; namespace TNode.TNodeCore.Runtime.Tools{ /// /// Graph /// public class GraphTool{ /// /// Topological order of the graph nodes /// [NonSerialized] public readonly List TopologicalOrder = new List(); public IRuntimeNodeGraph Parent; public bool TopologicalSorted = false; /// /// Entry nodes of the graph. These are the nodes that has no input. /// [NonSerialized] public readonly List NonDependencyNode = new List(); /// /// Cached data for Dependency traversal. /// public readonly Dictionary OutputCached = new Dictionary(); /// /// Ssed to detect if the graph tool is caching the output data of the node /// private bool _isCachingOutput = false; /// /// elements are read only ,do not modify them /// public readonly Dictionary RuntimeNodes; //Traverse and process all nodes in a topological order,dependency of the node is already resolved.if you want to run specific node,you can use RunNodeDependently instead public void DirectlyTraversal(){ foreach (var node in TopologicalOrder){ var links = node.InputLinks; foreach (var link in links){ HandlingLink(link); } node.NodeData.Process(); } } //A IEnumerator version of the DirectlyTraversal,used to run the graph in a coroutine or somewhere you need public IEnumerator IterateDirectlyTraversal(){ if (TopologicalSorted==false){ throw new Exception("The graph is not sorted,there may be a circular dependency,use another access method instead"); } foreach (var node in TopologicalOrder){ var links = node.InputLinks; foreach (var link in links){ HandlingLink(link); } node.NodeData.Process(); yield return node; } } /// /// usually used in state transition /// /// public IEnumerator IterateNext(){ var currentNode = NonDependencyNode.FirstOrDefault(); if (currentNode == null){ yield break; } currentNode.NodeData.Process(); yield return currentNode; while(currentNode.OutputLinks.Any()){ if (currentNode is ConditionalRuntimeNode conditionalRuntimeNode){ var id = conditionalRuntimeNode.GetNextNodeId(); if (id != null && id.Trim(' ').Length > 0){ Debug.Log(currentNode.NodeData+" is going to run "+id); currentNode = RuntimeNodes[conditionalRuntimeNode.GetNextNodeId()]; } } else{ var link = currentNode.OutputLinks.FirstOrDefault(); if (link != null){ HandlingLink(link); currentNode = RuntimeNodes[link.inPort.nodeDataId]; } } currentNode.NodeData.Process(); yield return currentNode; } } //Try to enable state transition from node to node. public IEnumerator DeepFirstSearchWithCondition(){ //Define the basic data structure for a traversal of the graph Stack stack = new Stack(); HashSet alreadyContained = new HashSet(); HashSet visited = new HashSet(); foreach (var runtimeNode in NonDependencyNode){ stack.Push(runtimeNode); } while (stack.Count > 0){ var node = stack.Pop(); visited.Add(node); if (node is ConditionalRuntimeNode conditionalRuntimeNode){ var ids = conditionalRuntimeNode.GetConditionalNextIds(); var nextNodes = ids.Select(id=>RuntimeNodes[id]).ToList(); foreach (var runtimeNode in nextNodes){ AddToCollectionIfMeetCondition(alreadyContained, visited,runtimeNode, stack); } } else{ foreach (var runtimeNode in node.OutputLinks.Select(link => RuntimeNodes[link.inPort.nodeDataId])){ AddToCollectionIfMeetCondition(alreadyContained, visited,runtimeNode, stack); } } node.OutputLinks.ForEach(HandlingLink); node.NodeData.Process(); yield return node; } } /// /// Breath first search for the graph.Not a standard BFS algorithm since all entries will be executed first. /// /// The IEnumerator to iterate the node public IEnumerator BreathFirstSearch(){ //Define the basic data structure for a traversal of the graph Queue queue = new Queue(); //Already contained method to avoid duplicate traversal HashSet alreadyContained = new HashSet(); //Visited method to avoid duplicate traversal HashSet visited = new HashSet(); //Firstly add all entry node to the queue foreach (var runtimeNode in NonDependencyNode){ queue.Enqueue(runtimeNode); alreadyContained.Add(runtimeNode); } //Iterate the queue to implement bfs while (queue.Count > 0){ var node = queue.Dequeue(); visited.Add(node); //Conditional node will be traversed in a special way,only links fit the condition will be traversed if (node is ConditionalRuntimeNode conditionalRuntimeNode){ var ids = conditionalRuntimeNode.GetConditionalNextIds(); var nextNodes = ids.Select(id=>RuntimeNodes[id]).ToList(); foreach (var runtimeNode in nextNodes){ AddToCollectionIfMeetCondition(alreadyContained, visited,runtimeNode, queue); } } else{ foreach (var runtimeNode in node.OutputLinks.Select(link => RuntimeNodes[link.inPort.nodeDataId])){ AddToCollectionIfMeetCondition(alreadyContained, visited,runtimeNode, queue); } } node.NodeData.Process(); //Handle the links of the node node.OutputLinks.ForEach(HandlingLink); yield return node; } } private void AddToCollectionIfMeetCondition(HashSet alreadyContained,HashSet visited, RuntimeNode runtimeNode, Queue queue){ //Check if the node is already contained in the queue or already visited if (visited.Contains(runtimeNode)) return; //the already contained guard is used to avoid duplicate traversal because the graph may start with multiple entries and all entry node should be run first. //Thus cause the same node could be add to the queue multiple times. if (alreadyContained.Contains(runtimeNode)) return; //Check if the visited node has all previous node of the node var dependentNodes = runtimeNode.GetDependentNodesId().Select(x => RuntimeNodes[x]); var allDependenciesVisited = dependentNodes.Aggregate(true, (a, b) => alreadyContained.Contains(b) && a ); //If the current node is not prepared,another routine will execute it when all is ready if (allDependenciesVisited == false) return; //If all conditions are met, add the node to the queue queue.Enqueue(runtimeNode); alreadyContained.Add(runtimeNode); } private void AddToCollectionIfMeetCondition(HashSet alreadyContained,HashSet visited, RuntimeNode runtimeNode, Stack stack){ //Check if the node is already contained in the stack if (alreadyContained.Contains(runtimeNode)) return; if (visited.Contains(runtimeNode)) return; //Check if the visited node has all previous node of the node var dependentNodes = runtimeNode.GetDependentNodesId().Select(x => RuntimeNodes[x]); var allDependenciesVisited = dependentNodes.Aggregate(true, (a, b) => alreadyContained.Contains(b) && a ); //If the current node is not prepared,run it dependently. if (allDependenciesVisited == false){ RunNodeDependently(runtimeNode,0,false); } //If all conditions are met, add the node to the stack stack.Push(runtimeNode); alreadyContained.Add(runtimeNode); } private void AddNodeToStackIfMeetCondition(HashSet alreadyContained, RuntimeNode runtimeNode, Stack stack){ //Check if the node is already contained in the queue if (alreadyContained.Contains(runtimeNode)) return; //Check if the visited node has all previous node of the node var dependentNodes = runtimeNode.GetDependentNodesId().Select(x => RuntimeNodes[x]); var allDependenciesVisited = dependentNodes.Aggregate(true, (a, b) => alreadyContained.Contains(b) && a ); if (allDependenciesVisited == false) return; //If all conditions are met, add the node to the queue stack.Push(runtimeNode); alreadyContained.Add(runtimeNode); } /// /// Cache out port data in the graph tool so that we can directly access the output. /// The two function assume there will be no change happens in scene nodes or blackboard referenced data during the running,so in a dependency traversal for some /// batch of nodes.the nodes could directly access the output data in the graph tool instead of waiting dependency traversal resolve the result of the output. /// public void StartCachingPort(){ _isCachingOutput = true; } public void EndCachingPort(){ _isCachingOutput = false; OutputCached.Clear(); } /// /// Resolve dependencies by a deep first search,the depended nodes will be processed to satisfy the need of the the given runtime node /// Note it's a recursive function.if you want directly traverse all nodes with dependency resolved ,use DirectlyTraversal() instead. /// /// The node you want to resolve dependency /// search depth,no need provide a number when use outside /// if the the node of the 0 level should be processed,which is the node you want to run,be processed by the method public void RunNodeDependently(RuntimeNode runtimeNode,int dependencyLevel=0,bool processTargetNode=true){ var links = runtimeNode.InputLinks; foreach (var link in links){ var outputNode = RuntimeNodes[link.outPort.nodeDataId]; if (outputNode is ConditionalRuntimeNode){ continue; } RunNodeDependently(outputNode,dependencyLevel+1); HandlingLink(link); } if (dependencyLevel > DependencyLevelMax){ throw new Exception("Dependency anomaly detected,check if there is a loop in the graph"); } //if the runtime node has no output ,it will not be processed if (runtimeNode.OutputLinks.Count == 0 && dependencyLevel != 0){ return; } if (processTargetNode||dependencyLevel != 0){ runtimeNode.NodeData.Process(); } } /// /// Max depth of dependency traversal,in case of some special situation. the dependency level bigger than this number will be considered as a loop. /// private const int DependencyLevelMax = 1145; /// /// Handling a node link to transfer data from it's output side to the input side /// /// Link you want to process public void HandlingLink(NodeLink nodeLink){ //out node is node output data //in node is node receive data var inNode = RuntimeNodes[nodeLink.inPort.nodeDataId]; var outNode = RuntimeNodes[nodeLink.outPort.nodeDataId]; //TODO looks like this string would be too long to make a cache var cachedKey = $"{outNode.NodeData.id}-{nodeLink.inPort.portEntryName}"; var outValue = OutputCached.ContainsKey(cachedKey) ? OutputCached[cachedKey] : outNode.GetOutput(nodeLink.outPort.portEntryName); Debug.Log(outValue); if (_isCachingOutput){ OutputCached[cachedKey] = outValue; } inNode.SetInput(nodeLink.inPort.portEntryName, outValue); } /// /// Constructor of the graph tool,it will traverse the graph and build the topological order of the graph. /// /// List of nodes you need to traversal to build graph tool /// Map stores the mapping of node data id to runtime node /// The graph you want to build graph tool for public GraphTool(IRuntimeNodeGraph graph){ RuntimeNodes = graph.GetRuntimeNodesDictionary(); var list = graph.GetRuntimeNodes(); Parent = graph; if (Parent == null){ } if (list == null) return; Queue queue = new Queue(); Dictionary inDegreeCounterForTopologicalSort = new Dictionary(); foreach (var runtimeNode in list){ var id = runtimeNode.NodeData.id; if (!inDegreeCounterForTopologicalSort.ContainsKey(id)){ inDegreeCounterForTopologicalSort.Add(id,runtimeNode.InputLinks.Count); } if (inDegreeCounterForTopologicalSort[id] == 0){ queue.Enqueue(runtimeNode); NonDependencyNode.Add(runtimeNode); } } //Topological sort while (queue.Count > 0){ var node = queue.Dequeue(); TopologicalOrder.Add(node); foreach (var outputLink in node.OutputLinks){ inDegreeCounterForTopologicalSort[outputLink.inPort.nodeDataId]--; if (inDegreeCounterForTopologicalSort[outputLink.inPort.nodeDataId] == 0){ queue.Enqueue(RuntimeNodes[outputLink.inPort.nodeDataId]); } } } TopologicalSorted = TopologicalOrder.Count != list.Count; inDegreeCounterForTopologicalSort.Clear(); queue.Clear(); } } }