using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Reflection;
using Unity.Jobs;
using System.Linq;
namespace GraphProcessor
{
public delegate IEnumerable< PortData > CustomPortBehaviorDelegate(List< SerializableEdge > edges);
public delegate IEnumerable< PortData > CustomPortTypeBehaviorDelegate(string fieldName, string displayName, object value);
[Serializable]
public abstract class BaseNode
{
///
/// Name of the node, it will be displayed in the title section
///
///
public virtual string name => GetType().Name;
///
/// The accent color of the node
///
public virtual Color color => Color.clear;
///
/// Set a custom uss file for the node. We use a Resources.Load to get the stylesheet so be sure to put the correct resources path
/// https://docs.unity3d.com/ScriptReference/Resources.Load.html
///
public virtual string layoutStyle => string.Empty;
///
/// If the node can be locked or not
///
public virtual bool unlockable => true;
///
/// Is the node is locked (if locked it can't be moved)
///
public virtual bool isLocked => nodeLock;
//id
public string GUID;
public int computeOrder = -1;
/// Tell wether or not the node can be processed. Do not check anything from inputs because this step happens before inputs are sent to the node
public virtual bool canProcess => true;
/// Show the node controlContainer only when the mouse is over the node
public virtual bool showControlsOnHover => false;
/// True if the node can be deleted, false otherwise
public virtual bool deletable => true;
///
/// Container of input ports
///
[NonSerialized]
public readonly NodeInputPortContainer inputPorts;
///
/// Container of output ports
///
[NonSerialized]
public readonly NodeOutputPortContainer outputPorts;
//Node view datas
public Rect position;
///
/// Is the node expanded
///
public bool expanded;
///
/// Is debug visible
///
public bool debug;
///
/// Node locked state
///
public bool nodeLock;
public delegate void ProcessDelegate();
///
/// Triggered when the node is processes
///
public event ProcessDelegate onProcessed;
public event Action< string, NodeMessageType > onMessageAdded;
public event Action< string > onMessageRemoved;
///
/// Triggered after an edge was connected on the node
///
public event Action< SerializableEdge > onAfterEdgeConnected;
///
/// Triggered after an edge was disconnected on the node
///
public event Action< SerializableEdge > onAfterEdgeDisconnected;
///
/// Triggered after a single/list of port(s) is updated, the parameter is the field name
///
public event Action< string > onPortsUpdated;
[NonSerialized]
bool _needsInspector = false;
///
/// Does the node needs to be visible in the inspector (when selected).
///
public virtual bool needsInspector => _needsInspector;
[NonSerialized]
internal Dictionary< string, NodeFieldInformation > nodeFields = new Dictionary< string, NodeFieldInformation >();
[NonSerialized]
internal Dictionary< Type, CustomPortTypeBehaviorDelegate> customPortTypeBehaviorMap = new Dictionary();
[NonSerialized]
List< string > messages = new List();
[NonSerialized]
protected BaseGraph graph;
internal class NodeFieldInformation
{
public string name;
public string fieldName;
public FieldInfo info;
public bool input;
public bool isMultiple;
public string tooltip;
public CustomPortBehaviorDelegate behavior;
public bool vertical;
public NodeFieldInformation(FieldInfo info, string name, bool input, bool isMultiple, string tooltip, bool vertical, CustomPortBehaviorDelegate behavior)
{
this.input = input;
this.isMultiple = isMultiple;
this.info = info;
this.name = name;
this.fieldName = info.Name;
this.behavior = behavior;
this.tooltip = tooltip;
this.vertical = vertical;
}
}
struct PortUpdate
{
public List fieldNames;
public BaseNode node;
public void Deconstruct(out List fieldNames, out BaseNode node)
{
fieldNames = this.fieldNames;
node = this.node;
}
}
// Used in port update algorithm
Stack fieldsToUpdate = new Stack();
HashSet updatedFields = new HashSet();
///
/// Creates a node of type T at a certain position
///
/// position in the graph in pixels
/// type of the node
/// the node instance
public static T CreateFromType< T >(Vector2 position) where T : BaseNode
{
return CreateFromType(typeof(T), position) as T;
}
///
/// Creates a node of type nodeType at a certain position
///
/// position in the graph in pixels
/// type of the node
/// the node instance
public static BaseNode CreateFromType(Type nodeType, Vector2 position)
{
if (!nodeType.IsSubclassOf(typeof(BaseNode)))
return null;
var node = Activator.CreateInstance(nodeType) as BaseNode;
node.position = new Rect(position, new Vector2(100, 100));
ExceptionToLog.Call(() => node.OnNodeCreated());
return node;
}
#region Initialization
// called by the BaseGraph when the node is added to the graph
public void Initialize(BaseGraph graph)
{
this.graph = graph;
ExceptionToLog.Call(() => Enable());
InitializePorts();
}
void InitializeCustomPortTypeMethods()
{
MethodInfo[] methods = new MethodInfo[0];
Type baseType = GetType();
while (true)
{
methods = baseType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var method in methods)
{
var typeBehaviors = method.GetCustomAttributes().ToArray();
if (typeBehaviors.Length == 0)
continue;
CustomPortTypeBehaviorDelegate deleg = null;
try
{
deleg = Delegate.CreateDelegate(typeof(CustomPortTypeBehaviorDelegate), this, method) as CustomPortTypeBehaviorDelegate;
} catch (Exception e)
{
Debug.LogError(e);
Debug.LogError($"Cannot convert method {method} to a delegate of type {typeof(CustomPortTypeBehaviorDelegate)}");
}
foreach (var typeBehavior in typeBehaviors)
customPortTypeBehaviorMap[typeBehavior.type] = deleg;
}
// Try to also find private methods in the base class
baseType = baseType.BaseType;
if (baseType == null)
break;
}
}
///
/// Use this function to initialize anything related to ports generation in your node
/// This will allow the node creation menu to correctly recognize ports that can be connected between nodes
///
public virtual void InitializePorts()
{
InitializeCustomPortTypeMethods();
foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info)))
{
var nodeField = nodeFields[key.Name];
if (HasCustomBehavior(nodeField))
{
UpdatePortsForField(nodeField.fieldName);
}
else
{
// If we don't have a custom behavior on the node, we just have to create a simple port
AddPort(nodeField.input, nodeField.fieldName, new PortData { acceptMultipleEdges = nodeField.isMultiple, displayName = nodeField.name, tooltip = nodeField.tooltip, vertical = nodeField.vertical });
}
}
}
///
/// Override the field order inside the node. It allows to re-order all the ports and field in the UI.
///
/// List of fields to sort
/// Sorted list of fields
public virtual IEnumerable OverrideFieldOrder(IEnumerable fields)
{
long GetFieldInheritanceLevel(FieldInfo f)
{
int level = 0;
var t = f.DeclaringType;
while (t != null)
{
t = t.BaseType;
level++;
}
return level;
}
// Order by MetadataToken and inheritance level to sync the order with the port order (make sure FieldDrawers are next to the correct port)
return fields.OrderByDescending(f => (long)(((GetFieldInheritanceLevel(f) << 32)) | (long)f.MetadataToken));
}
protected BaseNode()
{
inputPorts = new NodeInputPortContainer(this);
outputPorts = new NodeOutputPortContainer(this);
InitializeInOutDatas();
}
///
/// Update all ports of the node
///
public bool UpdateAllPorts()
{
bool changed = false;
foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info)))
{
var field = nodeFields[key.Name];
changed |= UpdatePortsForField(field.fieldName);
}
return changed;
}
///
/// Update all ports of the node without updating the connected ports. Only use this method when you need to update all the nodes ports in your graph.
///
public bool UpdateAllPortsLocal()
{
bool changed = false;
foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info)))
{
var field = nodeFields[key.Name];
changed |= UpdatePortsForFieldLocal(field.fieldName);
}
return changed;
}
///
/// Update the ports related to one C# property field (only for this node)
///
///
public bool UpdatePortsForFieldLocal(string fieldName)
{
bool changed = false;
if (!nodeFields.ContainsKey(fieldName))
return false;
var fieldInfo = nodeFields[fieldName];
if (!HasCustomBehavior(fieldInfo))
return false;
List< string > finalPorts = new List< string >();
var portCollection = fieldInfo.input ? (NodePortContainer)inputPorts : outputPorts;
// Gather all fields for this port (before to modify them)
var nodePorts = portCollection.Where(p => p.fieldName == fieldName);
// Gather all edges connected to these fields:
var edges = nodePorts.SelectMany(n => n.GetEdges()).ToList();
if (fieldInfo.behavior != null)
{
foreach (var portData in fieldInfo.behavior(edges))
AddPortData(portData);
}
else
{
var customPortTypeBehavior = customPortTypeBehaviorMap[fieldInfo.info.FieldType];
foreach (var portData in customPortTypeBehavior(fieldName, fieldInfo.name, fieldInfo.info.GetValue(this)))
AddPortData(portData);
}
void AddPortData(PortData portData)
{
var port = nodePorts.FirstOrDefault(n => n.portData.identifier == portData.identifier);
// Guard using the port identifier so we don't duplicate identifiers
if (port == null)
{
AddPort(fieldInfo.input, fieldName, portData);
changed = true;
}
else
{
// in case the port type have changed for an incompatible type, we disconnect all the edges attached to this port
if (!BaseGraph.TypesAreConnectable(port.portData.displayType, portData.displayType))
{
foreach (var edge in port.GetEdges().ToList())
graph.Disconnect(edge.GUID);
}
// patch the port data
if (port.portData != portData)
{
port.portData.CopyFrom(portData);
changed = true;
}
}
finalPorts.Add(portData.identifier);
}
// TODO
// Remove only the ports that are no more in the list
if (nodePorts != null)
{
var currentPortsCopy = nodePorts.ToList();
foreach (var currentPort in currentPortsCopy)
{
// If the current port does not appear in the list of final ports, we remove it
if (!finalPorts.Any(id => id == currentPort.portData.identifier))
{
RemovePort(fieldInfo.input, currentPort);
changed = true;
}
}
}
// Make sure the port order is correct:
portCollection.Sort((p1, p2) => {
int p1Index = finalPorts.FindIndex(id => p1.portData.identifier == id);
int p2Index = finalPorts.FindIndex(id => p2.portData.identifier == id);
if (p1Index == -1 || p2Index == -1)
return 0;
return p1Index.CompareTo(p2Index);
});
onPortsUpdated?.Invoke(fieldName);
return changed;
}
bool HasCustomBehavior(NodeFieldInformation info)
{
if (info.behavior != null)
return true;
if (customPortTypeBehaviorMap.ContainsKey(info.info.FieldType))
return true;
return false;
}
///
/// Update the ports related to one C# property field and all connected nodes in the graph
///
///
public bool UpdatePortsForField(string fieldName)
{
bool changed = false;
fieldsToUpdate.Clear();
updatedFields.Clear();
fieldsToUpdate.Push(new PortUpdate{fieldNames = new List(){fieldName}, node = this});
// Iterate through all the ports that needs to be updated, following graph connection when the
// port is updated. This is required ton have type propagation multiple nodes that changes port types
// are connected to each other (i.e. the relay node)
while (fieldsToUpdate.Count != 0)
{
var (fields, node) = fieldsToUpdate.Pop();
// Avoid updating twice a port
if (updatedFields.Any((t) => t.node == node && fields.SequenceEqual(t.fieldNames)))
continue;
updatedFields.Add(new PortUpdate{fieldNames = fields, node = node});
foreach (var field in fields)
{
if (node.UpdatePortsForFieldLocal(field))
{
foreach (var port in node.IsFieldInput(field) ? (NodePortContainer)node.inputPorts : node.outputPorts)
{
if (port.fieldName != field)
continue;
foreach(var edge in port.GetEdges())
{
var edgeNode = (node.IsFieldInput(field)) ? edge.outputNode : edge.inputNode;
var fieldsWithBehavior = edgeNode.nodeFields.Values.Where(f => HasCustomBehavior(f)).Select(f => f.fieldName).ToList();
fieldsToUpdate.Push(new PortUpdate{fieldNames = fieldsWithBehavior, node = edgeNode});
}
}
changed = true;
}
}
}
return changed;
}
HashSet portUpdateHashSet = new HashSet();
internal void DisableInternal() => ExceptionToLog.Call(() => Disable());
internal void DestroyInternal() => ExceptionToLog.Call(() => Destroy());
///
/// Called only when the node is created, not when instantiated
///
public virtual void OnNodeCreated() => GUID = Guid.NewGuid().ToString();
public virtual FieldInfo[] GetNodeFields()
=> GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
void InitializeInOutDatas()
{
var fields = GetNodeFields();
var methods = GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var field in fields)
{
var inputAttribute = field.GetCustomAttribute< InputAttribute >();
var outputAttribute = field.GetCustomAttribute< OutputAttribute >();
var tooltipAttribute = field.GetCustomAttribute< TooltipAttribute >();
var showInInspector = field.GetCustomAttribute< ShowInInspector >();
var vertical = field.GetCustomAttribute< VerticalAttribute >();
bool isMultiple = false;
bool input = false;
string name = field.Name;
string tooltip = null;
if (showInInspector != null)
_needsInspector = true;
if (inputAttribute == null && outputAttribute == null)
continue ;
//check if field is a collection type
isMultiple = (inputAttribute != null) ? inputAttribute.allowMultiple : (outputAttribute.allowMultiple);
input = inputAttribute != null;
tooltip = tooltipAttribute?.tooltip;
if (!String.IsNullOrEmpty(inputAttribute?.name))
name = inputAttribute.name;
if (!String.IsNullOrEmpty(outputAttribute?.name))
name = outputAttribute.name;
// By default we set the behavior to null, if the field have a custom behavior, it will be set in the loop just below
nodeFields[field.Name] = new NodeFieldInformation(field, name, input, isMultiple, tooltip, vertical != null, null);
}
foreach (var method in methods)
{
var customPortBehaviorAttribute = method.GetCustomAttribute< CustomPortBehaviorAttribute >();
CustomPortBehaviorDelegate behavior = null;
if (customPortBehaviorAttribute == null)
continue ;
// Check if custom port behavior function is valid
try {
var referenceType = typeof(CustomPortBehaviorDelegate);
behavior = (CustomPortBehaviorDelegate)Delegate.CreateDelegate(referenceType, this, method, true);
} catch {
Debug.LogError("The function " + method + " cannot be converted to the required delegate format: " + typeof(CustomPortBehaviorDelegate));
}
if (nodeFields.ContainsKey(customPortBehaviorAttribute.fieldName))
nodeFields[customPortBehaviorAttribute.fieldName].behavior = behavior;
else
Debug.LogError("Invalid field name for custom port behavior: " + method + ", " + customPortBehaviorAttribute.fieldName);
}
}
#endregion
#region Events and Processing
public void OnEdgeConnected(SerializableEdge edge)
{
bool input = edge.inputNode == this;
NodePortContainer portCollection = (input) ? (NodePortContainer)inputPorts : outputPorts;
portCollection.Add(edge);
UpdateAllPorts();
onAfterEdgeConnected?.Invoke(edge);
}
protected virtual bool CanResetPort(NodePort port) => true;
public void OnEdgeDisconnected(SerializableEdge edge)
{
if (edge == null)
return ;
bool input = edge.inputNode == this;
NodePortContainer portCollection = (input) ? (NodePortContainer)inputPorts : outputPorts;
portCollection.Remove(edge);
// Reset default values of input port:
bool haveConnectedEdges = edge.inputNode.inputPorts.Where(p => p.fieldName == edge.inputFieldName).Any(p => p.GetEdges().Count != 0);
if (edge.inputNode == this && !haveConnectedEdges && CanResetPort(edge.inputPort))
edge.inputPort?.ResetToDefault();
UpdateAllPorts();
onAfterEdgeDisconnected?.Invoke(edge);
}
public void OnProcess()
{
inputPorts.PullDatas();
ExceptionToLog.Call(() => Process());
InvokeOnProcessed();
outputPorts.PushDatas();
}
public void InvokeOnProcessed() => onProcessed?.Invoke();
///
/// Called when the node is enabled
///
protected virtual void Enable() {}
///
/// Called when the node is disabled
///
protected virtual void Disable() {}
///
/// Called when the node is removed
///
protected virtual void Destroy() {}
///
/// Override this method to implement custom processing
///
protected virtual void Process() {}
#endregion
#region API and utils
///
/// Add a port
///
/// is input port
/// C# field name
/// Data of the port
public void AddPort(bool input, string fieldName, PortData portData)
{
// Fixup port data info if needed:
if (portData.displayType == null)
portData.displayType = nodeFields[fieldName].info.FieldType;
if (input)
inputPorts.Add(new NodePort(this, fieldName, portData));
else
outputPorts.Add(new NodePort(this, fieldName, portData));
}
///
/// Remove a port
///
/// is input port
/// the port to delete
public void RemovePort(bool input, NodePort port)
{
if (input)
inputPorts.Remove(port);
else
outputPorts.Remove(port);
}
///
/// Remove port(s) from field name
///
/// is input
/// C# field name
public void RemovePort(bool input, string fieldName)
{
if (input)
inputPorts.RemoveAll(p => p.fieldName == fieldName);
else
outputPorts.RemoveAll(p => p.fieldName == fieldName);
}
///
/// Get all the nodes connected to the input ports of this node
///
/// an enumerable of node
public IEnumerable< BaseNode > GetInputNodes()
{
foreach (var port in inputPorts)
foreach (var edge in port.GetEdges())
yield return edge.outputNode;
}
///
/// Get all the nodes connected to the output ports of this node
///
/// an enumerable of node
public IEnumerable< BaseNode > GetOutputNodes()
{
foreach (var port in outputPorts)
foreach (var edge in port.GetEdges())
yield return edge.inputNode;
}
///
/// Return a node matching the condition in the dependencies of the node
///
/// Condition to choose the node
/// Matched node or null
public BaseNode FindInDependencies(Func condition)
{
Stack dependencies = new Stack();
dependencies.Push(this);
int depth = 0;
while (dependencies.Count > 0)
{
var node = dependencies.Pop();
// Guard for infinite loop (faster than a HashSet based solution)
depth++;
if (depth > 2000)
break;
if (condition(node))
return node;
foreach (var dep in node.GetInputNodes())
dependencies.Push(dep);
}
return null;
}
///
/// Get the port from field name and identifier
///
/// C# field name
/// Unique port identifier
///
public NodePort GetPort(string fieldName, string identifier)
{
return inputPorts.Concat(outputPorts).FirstOrDefault(p => {
var bothNull = String.IsNullOrEmpty(identifier) && String.IsNullOrEmpty(p.portData.identifier);
return p.fieldName == fieldName && (bothNull || identifier == p.portData.identifier);
});
}
///
/// Return all the ports of the node
///
///
public IEnumerable GetAllPorts()
{
foreach (var port in inputPorts)
yield return port;
foreach (var port in outputPorts)
yield return port;
}
///
/// Return all the connected edges of the node
///
///
public IEnumerable GetAllEdges()
{
foreach (var port in GetAllPorts())
foreach (var edge in port.GetEdges())
yield return edge;
}
///
/// Is the port an input
///
///
///
public bool IsFieldInput(string fieldName) => nodeFields[fieldName].input;
///
/// Add a message on the node
///
///
///
public void AddMessage(string message, NodeMessageType messageType)
{
if (messages.Contains(message))
return;
onMessageAdded?.Invoke(message, messageType);
messages.Add(message);
}
///
/// Remove a message on the node
///
///
public void RemoveMessage(string message)
{
onMessageRemoved?.Invoke(message);
messages.Remove(message);
}
///
/// Remove a message that contains
///
///
public void RemoveMessageContains(string subMessage)
{
string toRemove = messages.Find(m => m.Contains(subMessage));
messages.Remove(toRemove);
onMessageRemoved?.Invoke(toRemove);
}
///
/// Remove all messages on the node
///
public void ClearMessages()
{
foreach (var message in messages)
onMessageRemoved?.Invoke(message);
messages.Clear();
}
#endregion
}
}