using System;
using System.Collections.Generic;
using System.Linq;
using TNodeCore.Runtime.Models;
using UnityEngine;
namespace TNodeCore.Runtime.Components{
public class RuntimeGraph:MonoBehaviour{
///
/// Graph data reference to be used in runtime
///
public GraphData graphData;
///
/// Runtime copy of scene node data to hold references to scene objects
///
public List sceneNodes;
///
/// Map of node id to runtime node
///
[NonSerialized]
public readonly Dictionary RuntimeNodes = new Dictionary();
///
/// The graph tool the current runtime graph is using
///
[NonSerialized]
private GraphTool _graphTool;
///
/// Inner graph tool to help with graph operations
///
private class GraphTool{
///
/// Topological order of the graph nodes
///
[NonSerialized]
public readonly List TopologicalOrder = new List();
public RuntimeGraph Parent;
///
/// Entry nodes of the graph. These are the nodes that has no input.
///
[NonSerialized]
public readonly List EntryNodes = 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.InputLink;
foreach (var link in links){
HandlingLink(link);
}
node.NodeData.Process();
}
}
///
/// 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
public void RunNodeDependently(RuntimeNode runtimeNode,int dependencyLevel=0){
var links = runtimeNode.InputLink;
foreach (var link in links){
var outputNode = RuntimeNodes[link.outPort.nodeDataId];
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.OutputLink.Count == 0 && dependencyLevel != 0){
return;
}
runtimeNode.NodeData.Process();
Parent.StartCoroutine(runtimeNode.NodeData.AfterProcess());
}
///
/// 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
public GraphTool(List list, Dictionary graphNodes,RuntimeGraph graph){
RuntimeNodes = graphNodes;
Parent = graph;
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.InputLink.Count);
}
if (inDegreeCounterForTopologicalSort[id] == 0){
queue.Enqueue(runtimeNode);
EntryNodes.Add(runtimeNode);
}
}
//Topological sort
while (queue.Count > 0){
var node = queue.Dequeue();
TopologicalOrder.Add(node);
foreach (var outputLink in node.OutputLink){
inDegreeCounterForTopologicalSort[outputLink.inPort.nodeDataId]--;
if (inDegreeCounterForTopologicalSort[outputLink.inPort.nodeDataId] == 0){
queue.Enqueue(RuntimeNodes[outputLink.inPort.nodeDataId]);
}
}
}
if(TopologicalOrder.Count!= list.Count){
throw new Exception("Topological sort failed,circular dependency detected");
}
inDegreeCounterForTopologicalSort.Clear();
queue.Clear();
}
}
///
/// Holding the reference of the blackboard ,but it will be override by the runtime graph
///
[SerializeReference]
public BlackboardData runtimeBlackboardData;
///
/// Check if the runtime graph is build .a built graph has a graph tool set up
///
[NonSerialized]
private bool _build = false;
///
/// Build the graph tool and other dependencies for the runtime graph
///
public void Build(){
if (_build) return;
var link = graphData.NodeLinks;
//iterate links and create runtime nodes
foreach (var linkData in link){
ModifyOrCreateInNode(linkData);
ModifyOrCreateOutNode(linkData);
}
var nodeList = RuntimeNodes.Values;
_graphTool = new GraphTool(nodeList.ToList(),RuntimeNodes,this);
var sceneNodes = RuntimeNodes.Values.Where(x => x.NodeData is SceneNodeData).Select(x => x.NodeData as SceneNodeData);
foreach (var sceneNode in sceneNodes){
if (sceneNode != null) sceneNode.BlackboardData = runtimeBlackboardData;
}
#if UNITY_EDITOR
BuildSceneNode();
#endif
_build = true;
}
///
/// Cast the node data to a runtime node
///
/// Node data you provided
///
public RuntimeNode Get(NodeData nodeData){
if(!_build)
Build();
if(RuntimeNodes.ContainsKey(nodeData.id)){
return RuntimeNodes[nodeData.id];
}
return null;
}
///
/// Get the runtime node from an id
///
///
///
public RuntimeNode Get(string id){
if (RuntimeNodes.ContainsKey(id)){
return RuntimeNodes[id];
}
return null;
}
//DFS search to run a node.
public bool RunOnDependency(NodeData startNode){
if(!_build)
Build();
if (_graphTool == null)
return false;
_graphTool.RunNodeDependently(Get(startNode));
return true;
}
public bool ResolveDependency(){
if(!_build)
Build();
if (_graphTool == null)
return false;
_graphTool.DirectlyTraversal();
return true;
}
#region build scene node data
#if UNITY_EDITOR
public void BuildSceneNodePersistentData(SceneNodeData sceneNodeData){
var tr = transform.Find("PersistentData");
GameObject go;
if (tr == null){
go = new GameObject("PersistentData");
go.transform.SetParent(transform);
go.AddComponent();
}
go = tr.gameObject;
var persistentData = go.GetComponent();
persistentData.SceneNodeDataDictionary.Add(sceneNodeData.id,sceneNodeData);
}
public void BuildSceneNode(){
var fetchedSceneNode = graphData.NodeDictionary.Values.Where(x => x is SceneNodeData and not BlackboardDragNodeData);
foreach (var nodeData in fetchedSceneNode){
if (transform.Find(nodeData.id.GetHashCode().ToString())){
var scenePersistent = transform.Find("PersistentData").GetComponent();
if (scenePersistent.SceneNodeDataDictionary.ContainsKey(nodeData.id)){
var sceneNodeData = scenePersistent.SceneNodeDataDictionary[nodeData.id];
RuntimeNodes[nodeData.id].NodeData = sceneNodeData;
}
}
else if (nodeData.Clone() is SceneNodeData clonedNodeData){
clonedNodeData.BlackboardData = runtimeBlackboardData;
RuntimeNodes.Remove(nodeData.id);
RuntimeNodes.Add(nodeData.id,new RuntimeNode(clonedNodeData));
BuildSceneNodePersistentData(clonedNodeData);
}
}
UpdatePersistentData();
}
private void UpdatePersistentData(){
var persistentData = transform.Find("PersistentData")?.GetComponent();
if (persistentData == null) return;
var fetchedSceneNode =
RuntimeNodes
.Where(x => x.Value.NodeData is SceneNodeData and not BlackboardDragNodeData)
.Select(x=>x.Value.NodeData).ToArray();
var dic = persistentData.SceneNodeDataDictionary;
foreach (var sceneNodeData in dic.Values){
if(!fetchedSceneNode.Contains(sceneNodeData)){
persistentData.SceneNodeDataDictionary.Remove(sceneNodeData.id);
}
}
}
#endif
#endregion
private void ModifyOrCreateInNode(NodeLink linkData){
var inNodeId = linkData.inPort.nodeDataId;
var inNode = graphData.NodeDictionary[inNodeId];
if (!RuntimeNodes.ContainsKey(inNode.id)){
var runtimeInNode = new RuntimeNode(inNode);
RuntimeNodes.Add(inNode.id,runtimeInNode);
}
RuntimeNodes[inNode.id].InputLink.Add(linkData);
}
public List GetRuntimeNodesOfType(){
return RuntimeNodes.Values.Where(x => typeof(T).IsAssignableFrom(x.NodeType)).ToList();
}
public List GetRuntimeNodesOfType(Type type){
return RuntimeNodes.Values.Where(x => type.IsAssignableFrom(type)).ToList();
}
public void RunNodesOfType(Type t,bool isCaching= false){
var nodes = GetRuntimeNodesOfType(t);
if(isCaching)
_graphTool.StartCachingPort();
foreach (var runtimeNode in nodes){
RunOnDependency(runtimeNode.NodeData);
}
if(isCaching)
_graphTool.EndCachingPort();
}
///
/// Run some nodes ,if the node is not in the graph ,then pass
///
///
///
public void RunNodes(List runtimeNodes,bool isCaching= false){
if (isCaching){
_graphTool.StartCachingPort();
}
foreach (var runtimeNode in runtimeNodes){
if(!RuntimeNodes.ContainsKey(runtimeNode.NodeData.id)){
continue;
}
RunOnDependency(runtimeNode.NodeData);
}
if (isCaching){
_graphTool.EndCachingPort();
}
}
private void ModifyOrCreateOutNode(NodeLink linkData){
var outNodeId = linkData.outPort.nodeDataId;
var outNode = graphData.NodeDictionary[outNodeId];
if(!RuntimeNodes.ContainsKey(outNode.id)){
var runtimeOutNode = new RuntimeNode(outNode);
RuntimeNodes.Add(outNode.id,runtimeOutNode);
}
RuntimeNodes[outNode.id].OutputLink.Add(linkData);
}
public void OnValidate(){
if(runtimeBlackboardData==null||runtimeBlackboardData.GetType()==typeof(BlackboardData)){
if (graphData != null)
runtimeBlackboardData = graphData.blackboardData?.Clone() as BlackboardData;
}
}
public void OnDisable(){
RuntimeNodes.Clear();
_build = false;
}
public void OnDestroy(){
RuntimeNodes.Clear();
_build = false;
}
public void Start(){
Build();
}
public virtual void RuntimeExecute(){
_graphTool.DirectlyTraversal();
}
}
public class SceneDataPersistent:MonoBehaviour,ISerializationCallbackReceiver{
public readonly Dictionary SceneNodeDataDictionary = new();
[SerializeReference]
public List sceneNodeData=new ();
public void OnBeforeSerialize(){
sceneNodeData.Clear();
foreach(var node in SceneNodeDataDictionary.Values){
sceneNodeData.Add(node);
}
}
public void OnAfterDeserialize(){
SceneNodeDataDictionary.Clear();
foreach(var node in sceneNodeData){
SceneNodeDataDictionary.Add(node.id,node);
}
}
}
public enum ProcessingStrategy{
BreadthFirst,
DepthFirst
}
}