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