using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using System;
using UnityEngine.Serialization;
using UnityEngine.SceneManagement;
namespace GraphProcessor
{
public class GraphChanges
{
public SerializableEdge removedEdge;
public SerializableEdge addedEdge;
public BaseNode removedNode;
public BaseNode addedNode;
public BaseNode nodeChanged;
public Group addedGroups;
public Group removedGroups;
public BaseStackNode addedStackNode;
public BaseStackNode removedStackNode;
public StickyNote addedStickyNotes;
public StickyNote removedStickyNotes;
}
///
/// Compute order type used to determine the compute order integer on the nodes
///
public enum ComputeOrderType
{
DepthFirst,
BreadthFirst,
}
[System.Serializable]
public class BaseGraph : ScriptableObject, ISerializationCallbackReceiver
{
static readonly int maxComputeOrderDepth = 1000;
/// Invalid compute order number of a node when it's inside a loop
public static readonly int loopComputeOrder = -2;
/// Invalid compute order number of a node can't process
public static readonly int invalidComputeOrder = -1;
///
/// Json list of serialized nodes only used for copy pasting in the editor. Note that this field isn't serialized
///
///
///
[SerializeField, Obsolete("Use BaseGraph.nodes instead")]
public List< JsonElement > serializedNodes = new List< JsonElement >();
///
/// List of all the nodes in the graph.
///
///
///
[SerializeReference]
public List< BaseNode > nodes = new List< BaseNode >();
///
/// Dictionary to access node per GUID, faster than a search in a list
///
///
///
///
[System.NonSerialized]
public Dictionary< string, BaseNode > nodesPerGUID = new Dictionary< string, BaseNode >();
///
/// Json list of edges
///
///
///
[SerializeField]
public List< SerializableEdge > edges = new List< SerializableEdge >();
///
/// Dictionary of edges per GUID, faster than a search in a list
///
///
///
///
[System.NonSerialized]
public Dictionary< string, SerializableEdge > edgesPerGUID = new Dictionary< string, SerializableEdge >();
///
/// All groups in the graph
///
///
///
[SerializeField, FormerlySerializedAs("commentBlocks")]
public List< Group > groups = new List< Group >();
///
/// All Stack Nodes in the graph
///
///
///
[SerializeField, SerializeReference] // Polymorphic serialization
public List< BaseStackNode > stackNodes = new List< BaseStackNode >();
///
/// All pinned elements in the graph
///
///
///
[SerializeField]
public List< PinnedElement > pinnedElements = new List< PinnedElement >();
///
/// All exposed parameters in the graph
///
///
///
[SerializeField, SerializeReference]
public List< ExposedParameter > exposedParameters = new List< ExposedParameter >();
[SerializeField, FormerlySerializedAs("exposedParameters")] // We keep this for upgrade
List< ExposedParameter > serializedParameterList = new List();
[SerializeField]
public List< StickyNote > stickyNotes = new List();
[System.NonSerialized]
Dictionary< BaseNode, int > computeOrderDictionary = new Dictionary< BaseNode, int >();
[NonSerialized]
Scene linkedScene;
// Trick to keep the node inspector alive during the editor session
[SerializeField]
internal UnityEngine.Object nodeInspectorReference;
//graph visual properties
public Vector3 position = Vector3.zero;
public Vector3 scale = Vector3.one;
///
/// Triggered when something is changed in the list of exposed parameters
///
public event Action onExposedParameterListChanged;
public event Action< ExposedParameter > onExposedParameterModified;
public event Action< ExposedParameter > onExposedParameterValueChanged;
///
/// Triggered when the graph is linked to an active scene.
///
public event Action< Scene > onSceneLinked;
///
/// Triggered when the graph is enabled
///
public event Action onEnabled;
///
/// Triggered when the graph is changed
///
public event Action< GraphChanges > onGraphChanges;
[System.NonSerialized]
bool _isEnabled = false;
public bool isEnabled { get => _isEnabled; private set => _isEnabled = value; }
public HashSet< BaseNode > graphOutputs { get; private set; } = new HashSet();
protected virtual void OnEnable()
{
if (isEnabled)
OnDisable();
MigrateGraphIfNeeded();
InitializeGraphElements();
DestroyBrokenGraphElements();
UpdateComputeOrder();
isEnabled = true;
onEnabled?.Invoke();
}
void InitializeGraphElements()
{
// Sanitize the element lists (it's possible that nodes are null if their full class name have changed)
// If you rename / change the assembly of a node or parameter, please use the MovedFrom() attribute to avoid breaking the graph.
nodes.RemoveAll(n => n == null);
exposedParameters.RemoveAll(e => e == null);
foreach (var node in nodes.ToList())
{
nodesPerGUID[node.GUID] = node;
node.Initialize(this);
}
foreach (var edge in edges.ToList())
{
edge.Deserialize();
edgesPerGUID[edge.GUID] = edge;
// Sanity check for the edge:
if (edge.inputPort == null || edge.outputPort == null)
{
Disconnect(edge.GUID);
continue;
}
// Add the edge to the non-serialized port data
edge.inputPort.owner.OnEdgeConnected(edge);
edge.outputPort.owner.OnEdgeConnected(edge);
}
}
protected virtual void OnDisable()
{
isEnabled = false;
foreach (var node in nodes)
node.DisableInternal();
}
public virtual void OnAssetDeleted() {}
///
/// Adds a node to the graph
///
///
///
public BaseNode AddNode(BaseNode node)
{
nodesPerGUID[node.GUID] = node;
nodes.Add(node);
node.Initialize(this);
onGraphChanges?.Invoke(new GraphChanges{ addedNode = node });
return node;
}
///
/// Removes a node from the graph
///
///
public void RemoveNode(BaseNode node)
{
node.DisableInternal();
node.DestroyInternal();
nodesPerGUID.Remove(node.GUID);
nodes.Remove(node);
onGraphChanges?.Invoke(new GraphChanges{ removedNode = node });
}
///
/// Connect two ports with an edge
///
/// input port
/// output port
/// is the edge allowed to disconnect another edge
/// the connecting edge
public SerializableEdge Connect(NodePort inputPort, NodePort outputPort, bool autoDisconnectInputs = true)
{
var edge = SerializableEdge.CreateNewEdge(this, inputPort, outputPort);
//If the input port does not support multi-connection, we remove them
if (autoDisconnectInputs && !inputPort.portData.acceptMultipleEdges)
{
foreach (var e in inputPort.GetEdges().ToList())
{
// TODO: do not disconnect them if the connected port is the same than the old connected
Disconnect(e);
}
}
// same for the output port:
if (autoDisconnectInputs && !outputPort.portData.acceptMultipleEdges)
{
foreach (var e in outputPort.GetEdges().ToList())
{
// TODO: do not disconnect them if the connected port is the same than the old connected
Disconnect(e);
}
}
edges.Add(edge);
// Add the edge to the list of connected edges in the nodes
inputPort.owner.OnEdgeConnected(edge);
outputPort.owner.OnEdgeConnected(edge);
onGraphChanges?.Invoke(new GraphChanges{ addedEdge = edge });
return edge;
}
///
/// Disconnect two ports
///
/// input node
/// input field name
/// output node
/// output field name
public void Disconnect(BaseNode inputNode, string inputFieldName, BaseNode outputNode, string outputFieldName)
{
edges.RemoveAll(r => {
bool remove = r.inputNode == inputNode
&& r.outputNode == outputNode
&& r.outputFieldName == outputFieldName
&& r.inputFieldName == inputFieldName;
if (remove)
{
r.inputNode?.OnEdgeDisconnected(r);
r.outputNode?.OnEdgeDisconnected(r);
onGraphChanges?.Invoke(new GraphChanges{ removedEdge = r });
}
return remove;
});
}
///
/// Disconnect an edge
///
///
public void Disconnect(SerializableEdge edge) => Disconnect(edge.GUID);
///
/// Disconnect an edge
///
///
public void Disconnect(string edgeGUID)
{
List<(BaseNode, SerializableEdge)> disconnectEvents = new List<(BaseNode, SerializableEdge)>();
edges.RemoveAll(r => {
if (r.GUID == edgeGUID)
{
disconnectEvents.Add((r.inputNode, r));
disconnectEvents.Add((r.outputNode, r));
onGraphChanges?.Invoke(new GraphChanges{ removedEdge = r });
}
return r.GUID == edgeGUID;
});
// Delay the edge disconnect event to avoid recursion
foreach (var (node, edge) in disconnectEvents)
node?.OnEdgeDisconnected(edge);
}
///
/// Add a group
///
///
public void AddGroup(Group block)
{
groups.Add(block);
onGraphChanges?.Invoke(new GraphChanges{ addedGroups = block });
}
///
/// Removes a group
///
///
public void RemoveGroup(Group block)
{
groups.Remove(block);
onGraphChanges?.Invoke(new GraphChanges{ removedGroups = block });
}
///
/// Add a StackNode
///
///
public void AddStackNode(BaseStackNode stackNode)
{
stackNodes.Add(stackNode);
onGraphChanges?.Invoke(new GraphChanges{ addedStackNode = stackNode });
}
///
/// Remove a StackNode
///
///
public void RemoveStackNode(BaseStackNode stackNode)
{
stackNodes.Remove(stackNode);
onGraphChanges?.Invoke(new GraphChanges{ removedStackNode = stackNode });
}
///
/// Add a sticky note
///
///
public void AddStickyNote(StickyNote note)
{
stickyNotes.Add(note);
onGraphChanges?.Invoke(new GraphChanges{ addedStickyNotes = note });
}
///
/// Removes a sticky note
///
///
public void RemoveStickyNote(StickyNote note)
{
stickyNotes.Remove(note);
onGraphChanges?.Invoke(new GraphChanges{ removedStickyNotes = note });
}
///
/// Invoke the onGraphChanges event, can be used as trigger to execute the graph when the content of a node is changed
///
///
public void NotifyNodeChanged(BaseNode node) => onGraphChanges?.Invoke(new GraphChanges { nodeChanged = node });
///
/// Open a pinned element of type viewType
///
/// type of the pinned element
/// the pinned element
public PinnedElement OpenPinned(Type viewType)
{
var pinned = pinnedElements.Find(p => p.editorType.type == viewType);
if (pinned == null)
{
pinned = new PinnedElement(viewType);
pinnedElements.Add(pinned);
}
else
pinned.opened = true;
return pinned;
}
///
/// Closes a pinned element of type viewType
///
/// type of the pinned element
public void ClosePinned(Type viewType)
{
var pinned = pinnedElements.Find(p => p.editorType.type == viewType);
pinned.opened = false;
}
public void OnBeforeSerialize()
{
// Cleanup broken elements
stackNodes.RemoveAll(s => s == null);
nodes.RemoveAll(n => n == null);
}
// We can deserialize data here because it's called in a unity context
// so we can load objects references
public void Deserialize()
{
// Disable nodes correctly before removing them:
if (nodes != null)
{
foreach (var node in nodes)
node.DisableInternal();
}
MigrateGraphIfNeeded();
InitializeGraphElements();
}
public void MigrateGraphIfNeeded()
{
#pragma warning disable CS0618
// Migration step from JSON serialized nodes to [SerializeReference]
if (serializedNodes.Count > 0)
{
nodes.Clear();
foreach (var serializedNode in serializedNodes.ToList())
{
var node = JsonSerializer.DeserializeNode(serializedNode) as BaseNode;
if (node != null)
nodes.Add(node);
}
serializedNodes.Clear();
// we also migrate parameters here:
var paramsToMigrate = serializedParameterList.ToList();
exposedParameters.Clear();
foreach (var param in paramsToMigrate)
{
if (param == null)
continue;
var newParam = param.Migrate();
if (newParam == null)
{
Debug.LogError($"Can't migrate parameter of type {param.type}, please create an Exposed Parameter class that implements this type.");
continue;
}
else
exposedParameters.Add(newParam);
}
}
#pragma warning restore CS0618
}
public void OnAfterDeserialize() {}
///
/// Update the compute order of the nodes in the graph
///
/// Compute order type
public void UpdateComputeOrder(ComputeOrderType type = ComputeOrderType.DepthFirst)
{
if (nodes.Count == 0)
return ;
// Find graph outputs (end nodes) and reset compute order
graphOutputs.Clear();
foreach (var node in nodes)
{
if (node.GetOutputNodes().Count() == 0)
graphOutputs.Add(node);
node.computeOrder = 0;
}
computeOrderDictionary.Clear();
infiniteLoopTracker.Clear();
switch (type)
{
default:
case ComputeOrderType.DepthFirst:
UpdateComputeOrderDepthFirst();
break;
case ComputeOrderType.BreadthFirst:
foreach (var node in nodes)
UpdateComputeOrderBreadthFirst(0, node);
break;
}
}
///
/// Add an exposed parameter
///
/// parameter name
/// parameter type (must be a subclass of ExposedParameter)
/// default value
/// The unique id of the parameter
public string AddExposedParameter(string name, Type type, object value = null)
{
if (!type.IsSubclassOf(typeof(ExposedParameter)))
{
Debug.LogError($"Can't add parameter of type {type}, the type doesn't inherit from ExposedParameter.");
}
var param = Activator.CreateInstance(type) as ExposedParameter;
// patch value with correct type:
if (param.GetValueType().IsValueType)
value = Activator.CreateInstance(param.GetValueType());
param.Initialize(name, value);
exposedParameters.Add(param);
onExposedParameterListChanged?.Invoke();
return param.guid;
}
///
/// Add an already allocated / initialized parameter to the graph
///
/// The parameter to add
/// The unique id of the parameter
public string AddExposedParameter(ExposedParameter parameter)
{
string guid = Guid.NewGuid().ToString(); // Generated once and unique per parameter
parameter.guid = guid;
exposedParameters.Add(parameter);
onExposedParameterListChanged?.Invoke();
return guid;
}
///
/// Remove an exposed parameter
///
/// the parameter to remove
public void RemoveExposedParameter(ExposedParameter ep)
{
exposedParameters.Remove(ep);
onExposedParameterListChanged?.Invoke();
}
///
/// Remove an exposed parameter
///
/// GUID of the parameter
public void RemoveExposedParameter(string guid)
{
if (exposedParameters.RemoveAll(e => e.guid == guid) != 0)
onExposedParameterListChanged?.Invoke();
}
internal void NotifyExposedParameterListChanged()
=> onExposedParameterListChanged?.Invoke();
///
/// Update an exposed parameter value
///
/// GUID of the parameter
/// new value
public void UpdateExposedParameter(string guid, object value)
{
var param = exposedParameters.Find(e => e.guid == guid);
if (param == null)
return;
if (value != null && !param.GetValueType().IsAssignableFrom(value.GetType()))
throw new Exception("Type mismatch when updating parameter " + param.name + ": from " + param.GetValueType() + " to " + value.GetType().AssemblyQualifiedName);
param.value = value;
onExposedParameterModified?.Invoke(param);
}
///
/// Update the exposed parameter name
///
/// The parameter
/// new name
public void UpdateExposedParameterName(ExposedParameter parameter, string name)
{
parameter.name = name;
onExposedParameterModified?.Invoke(parameter);
}
///
/// Update parameter visibility
///
/// The parameter
/// is Hidden
public void NotifyExposedParameterChanged(ExposedParameter parameter)
{
onExposedParameterModified?.Invoke(parameter);
}
public void NotifyExposedParameterValueChanged(ExposedParameter parameter)
{
onExposedParameterValueChanged?.Invoke(parameter);
}
///
/// Get the exposed parameter from name
///
/// name
/// the parameter or null
public ExposedParameter GetExposedParameter(string name)
{
return exposedParameters.FirstOrDefault(e => e.name == name);
}
///
/// Get exposed parameter from GUID
///
/// GUID of the parameter
/// The parameter
public ExposedParameter GetExposedParameterFromGUID(string guid)
{
return exposedParameters.FirstOrDefault(e => e?.guid == guid);
}
///
/// Set parameter value from name. (Warning: the parameter name can be changed by the user)
///
/// name of the parameter
/// new value
/// true if the value have been assigned
public bool SetParameterValue(string name, object value)
{
var e = exposedParameters.FirstOrDefault(p => p.name == name);
if (e == null)
return false;
e.value = value;
return true;
}
///
/// Get the parameter value
///
/// parameter name
/// value
public object GetParameterValue(string name) => exposedParameters.FirstOrDefault(p => p.name == name)?.value;
///
/// Get the parameter value template
///
/// parameter name
/// type of the parameter
/// value
public T GetParameterValue< T >(string name) => (T)GetParameterValue(name);
///
/// Link the current graph to the scene in parameter, allowing the graph to pick and serialize objects from the scene.
///
/// Target scene to link
public void LinkToScene(Scene scene)
{
linkedScene = scene;
onSceneLinked?.Invoke(scene);
}
///
/// Return true when the graph is linked to a scene, false otherwise.
///
public bool IsLinkedToScene() => linkedScene.IsValid();
///
/// Get the linked scene. If there is no linked scene, it returns an invalid scene
///
public Scene GetLinkedScene() => linkedScene;
HashSet infiniteLoopTracker = new HashSet();
int UpdateComputeOrderBreadthFirst(int depth, BaseNode node)
{
int computeOrder = 0;
if (depth > maxComputeOrderDepth)
{
Debug.LogError("Recursion error while updating compute order");
return -1;
}
if (computeOrderDictionary.ContainsKey(node))
return node.computeOrder;
if (!infiniteLoopTracker.Add(node))
return -1;
if (!node.canProcess)
{
node.computeOrder = -1;
computeOrderDictionary[node] = -1;
return -1;
}
foreach (var dep in node.GetInputNodes())
{
int c = UpdateComputeOrderBreadthFirst(depth + 1, dep);
if (c == -1)
{
computeOrder = -1;
break ;
}
computeOrder += c;
}
if (computeOrder != -1)
computeOrder++;
node.computeOrder = computeOrder;
computeOrderDictionary[node] = computeOrder;
return computeOrder;
}
void UpdateComputeOrderDepthFirst()
{
Stack dfs = new Stack();
GraphUtils.FindCyclesInGraph(this, (n) => {
PropagateComputeOrder(n, loopComputeOrder);
});
int computeOrder = 0;
foreach (var node in GraphUtils.DepthFirstSort(this))
{
if (node.computeOrder == loopComputeOrder)
continue;
if (!node.canProcess)
node.computeOrder = -1;
else
node.computeOrder = computeOrder++;
}
}
void PropagateComputeOrder(BaseNode node, int computeOrder)
{
Stack deps = new Stack();
HashSet loop = new HashSet();
deps.Push(node);
while (deps.Count > 0)
{
var n = deps.Pop();
n.computeOrder = computeOrder;
if (!loop.Add(n))
continue;
foreach (var dep in n.GetOutputNodes())
deps.Push(dep);
}
}
void DestroyBrokenGraphElements()
{
edges.RemoveAll(e => e.inputNode == null
|| e.outputNode == null
|| string.IsNullOrEmpty(e.outputFieldName)
|| string.IsNullOrEmpty(e.inputFieldName)
);
nodes.RemoveAll(n => n == null);
}
///
/// Tell if two types can be connected in the context of a graph
///
///
///
///
public static bool TypesAreConnectable(Type t1, Type t2)
{
if (t1 == null || t2 == null)
return false;
if (TypeAdapter.AreIncompatible(t1, t2))
return false;
//Check if there is custom adapters for this assignation
if (CustomPortIO.IsAssignable(t1, t2))
return true;
//Check for type assignability
if (t2.IsReallyAssignableFrom(t1))
return true;
// User defined type convertions
if (TypeAdapter.AreAssignable(t1, t2))
return true;
return false;
}
}
}