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);
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();
}
}
}