using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using NUnit.Framework; namespace UnityEngine.Graphing { [Serializable] public class SerializableGraph : IGraph, ISerializationCallbackReceiver { [NonSerialized] List m_Edges = new List(); [NonSerialized] Dictionary> m_NodeEdges = new Dictionary>(); [NonSerialized] Dictionary m_Nodes = new Dictionary(); [SerializeField] List m_SerializableNodes = new List(); [SerializeField] List m_SerializableEdges = new List(); public IEnumerable GetNodes() where T : INode { return m_Nodes.Values.OfType(); } public IEnumerable edges { get { return m_Edges; } } public virtual void AddNode(INode node) { AddNodeNoValidate(node); ValidateGraph(); } void AddNodeNoValidate(INode node) { m_Nodes.Add(node.guid, node); node.owner = this; NotifyChange(new NodeAddedGraphChange(node)); } public virtual void RemoveNode(INode node) { if (!node.canDeleteNode) return; m_Nodes.Remove(node.guid); NotifyChange(new NodeRemovedGraphChange(node)); ValidateGraph(); } void RemoveNodeNoValidate(INode node) { if (!node.canDeleteNode) return; m_Nodes.Remove(node.guid); NotifyChange(new NodeRemovedGraphChange(node)); } void AddEdgeToNodeEdges(IEdge edge) { List inputEdges; if (!m_NodeEdges.TryGetValue(edge.inputSlot.nodeGuid, out inputEdges)) m_NodeEdges[edge.inputSlot.nodeGuid] = inputEdges = new List(); inputEdges.Add(edge); List outputEdges; if (!m_NodeEdges.TryGetValue(edge.outputSlot.nodeGuid, out outputEdges)) m_NodeEdges[edge.outputSlot.nodeGuid] = outputEdges = new List(); outputEdges.Add(edge); } public virtual Dictionary GetLegacyTypeRemapping() { return new Dictionary(); } public virtual IEdge Connect(SlotReference fromSlotRef, SlotReference toSlotRef) { if (fromSlotRef == null || toSlotRef == null) return null; var fromNode = GetNodeFromGuid(fromSlotRef.nodeGuid); var toNode = GetNodeFromGuid(toSlotRef.nodeGuid); if (fromNode == null || toNode == null) return null; // if fromNode is already connected to toNode // do now allow a connection as toNode will then // have an edge to fromNode creating a cycle. // if this is parsed it will lead to an infinite loop. var dependentNodes = new List(); NodeUtils.CollectNodesNodeFeedsInto(dependentNodes, toNode); if (dependentNodes.Contains(fromNode)) return null; var fromSlot = fromNode.FindSlot(fromSlotRef.slotId); var toSlot = toNode.FindSlot(toSlotRef.slotId); SlotReference outputSlot = null; SlotReference inputSlot = null; // output must connect to input if (fromSlot.isOutputSlot) outputSlot = fromSlotRef; else if (fromSlot.isInputSlot) inputSlot = fromSlotRef; if (toSlot.isOutputSlot) outputSlot = toSlotRef; else if (toSlot.isInputSlot) inputSlot = toSlotRef; if (inputSlot == null || outputSlot == null) return null; var slotEdges = GetEdges(inputSlot).ToList(); // remove any inputs that exits before adding foreach (var edge in slotEdges) { RemoveEdgeNoValidate(edge); } var newEdge = new Edge(outputSlot, inputSlot); m_Edges.Add(newEdge); NotifyChange(new EdgeAddedGraphChange(newEdge)); AddEdgeToNodeEdges(newEdge); Debug.Log("Connected edge: " + newEdge); ValidateGraph(); return newEdge; } public virtual void RemoveEdge(IEdge e) { RemoveEdgeNoValidate(e); ValidateGraph(); } public void RemoveElements(IEnumerable nodes, IEnumerable edges) { foreach (var edge in edges.ToArray()) RemoveEdgeNoValidate(edge); foreach (var serializableNode in nodes.ToArray()) RemoveNodeNoValidate(serializableNode); ValidateGraph(); } void RemoveEdgeNoValidate(IEdge e) { e = m_Edges.FirstOrDefault(x => x.Equals(e)); Assert.NotNull(e); m_Edges.Remove(e); List inputNodeEdges; if (m_NodeEdges.TryGetValue(e.inputSlot.nodeGuid, out inputNodeEdges)) inputNodeEdges.Remove(e); List outputNodeEdges; if (m_NodeEdges.TryGetValue(e.outputSlot.nodeGuid, out outputNodeEdges)) outputNodeEdges.Remove(e); NotifyChange(new EdgeRemovedGraphChange(e)); } public INode GetNodeFromGuid(Guid guid) { INode node; m_Nodes.TryGetValue(guid, out node); return node; } public bool ContainsNodeGuid(Guid guid) { return m_Nodes.ContainsKey(guid); } public T GetNodeFromGuid(Guid guid) where T : INode { var node = GetNodeFromGuid(guid); if (node is T) return (T)node; return default(T); } public IEnumerable GetEdges(SlotReference s) { if (s == null) return Enumerable.Empty(); var node = GetNodeFromGuid(s.nodeGuid); if (node == null) { Debug.LogWarning("Node does not exist"); return Enumerable.Empty(); } ISlot slot = slot = node.FindSlot(s.slotId); List candidateEdges; if (!m_NodeEdges.TryGetValue(s.nodeGuid, out candidateEdges)) return Enumerable.Empty(); return candidateEdges.Where(candidateEdge => { var cs = slot.isInputSlot ? candidateEdge.inputSlot : candidateEdge.outputSlot; return cs.nodeGuid == s.nodeGuid && cs.slotId == s.slotId; }); } public virtual void OnBeforeSerialize() { m_SerializableNodes = SerializationHelper.Serialize(m_Nodes.Values); m_SerializableEdges = SerializationHelper.Serialize(m_Edges); } public virtual void OnAfterDeserialize() { var nodes = SerializationHelper.Deserialize(m_SerializableNodes, GetLegacyTypeRemapping()); m_Nodes = new Dictionary(nodes.Count); foreach (var node in nodes) { node.owner = this; node.UpdateNodeAfterDeserialization(); m_Nodes.Add(node.guid, node); } m_SerializableNodes = null; m_Edges = SerializationHelper.Deserialize(m_SerializableEdges, null); m_SerializableEdges = null; foreach (var edge in m_Edges) AddEdgeToNodeEdges(edge); OnEnable(); ValidateGraph(); } public virtual void ValidateGraph() { //First validate edges, remove any //orphans. This can happen if a user //manually modifies serialized data //of if they delete a node in the inspector //debug view. foreach (var edge in edges.ToArray()) { var outputNode = GetNodeFromGuid(edge.outputSlot.nodeGuid); var inputNode = GetNodeFromGuid(edge.inputSlot.nodeGuid); if (outputNode == null || inputNode == null || outputNode.FindOutputSlot(edge.outputSlot.slotId) == null || inputNode.FindInputSlot(edge.inputSlot.slotId) == null) { //orphaned edge RemoveEdgeNoValidate(edge); } } foreach (var node in GetNodes()) node.ValidateNode(); } public virtual void ReplaceWith(IGraph other) { using (var pooledList = ListPool.GetDisposable()) { var removedNodeEdges = pooledList.value; foreach (var edge in m_Edges) { // Remove the edge if it doesn't exist in the other graph. if (!other.ContainsNodeGuid(edge.inputSlot.nodeGuid) || !other.GetEdges(edge.inputSlot).Any(otherEdge => otherEdge.outputSlot.Equals(edge.outputSlot))) removedNodeEdges.Add(edge); } foreach (var edge in removedNodeEdges) RemoveEdge(edge); } using (var removedNodesPooledObject = ListPool.GetDisposable()) using (var replacedNodesPooledObject = ListPool.GetDisposable()) { var removedNodeGuids = removedNodesPooledObject.value; var replacedNodes = replacedNodesPooledObject.value; foreach (var node in m_Nodes.Values) { if (!other.ContainsNodeGuid(node.guid)) // Remove the node if it doesn't exist in the other graph. removedNodeGuids.Add(node.guid); else // Replace the node with the one from the other graph otherwise. replacedNodes.Add(node); } foreach (var nodeGuid in removedNodeGuids) RemoveNode(m_Nodes[nodeGuid]); foreach (var node in replacedNodes) { var currentNode = other.GetNodeFromGuid(node.guid); currentNode.owner = this; m_Nodes[node.guid] = currentNode; currentNode.onModified = node.onModified; currentNode.onReplaced = node.onReplaced; // Notify listeners that the reference has changed. if (node.onReplaced != null) node.onReplaced(node, currentNode); if (currentNode.onModified != null) currentNode.onModified(node, ModificationScope.Node); node.onModified = null; node.onReplaced = null; } } // Add nodes from other graph which don't exist in this one. foreach (var node in other.GetNodes()) { if (!ContainsNodeGuid(node.guid)) AddNode(node); } // Add edges from other graph which don't exist in this one. foreach (var edge in other.edges) { if (!GetEdges(edge.inputSlot).Any(otherEdge => otherEdge.outputSlot.Equals(edge.outputSlot))) Connect(edge.outputSlot, edge.inputSlot); } } public void OnEnable() { foreach (var node in GetNodes().OfType()) { node.OnEnable(); } } public OnGraphChange onChange { get; set; } protected void NotifyChange(GraphChange change) { if (onChange != null) onChange(change); } } }