using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEngine; namespace XNode { /// Precaches reflection data in editor so we won't have to do it runtime public static class NodeDataCache { private static PortDataCache portDataCache; private static Dictionary> formerlySerializedAsCache; private static Dictionary typeQualifiedNameCache; private static bool Initialized { get { return portDataCache != null; } } public static string GetTypeQualifiedName(System.Type type) { if(typeQualifiedNameCache == null) typeQualifiedNameCache = new Dictionary(); string name; if (!typeQualifiedNameCache.TryGetValue(type, out name)) { name = type.AssemblyQualifiedName; typeQualifiedNameCache.Add(type, name); } return name; } /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. public static void UpdatePorts(Node node, Dictionary ports) { if (!Initialized) BuildCache(); Dictionary> removedPorts = new Dictionary>(); System.Type nodeType = node.GetType(); Dictionary formerlySerializedAs = null; if (formerlySerializedAsCache != null) formerlySerializedAsCache.TryGetValue(nodeType, out formerlySerializedAs); List dynamicListPorts = new List(); Dictionary staticPorts; if (!portDataCache.TryGetValue(nodeType, out staticPorts)) { staticPorts = new Dictionary(); } // Cleanup port dict - Remove nonexisting static ports - update static port types // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation. // Loop through current node ports foreach (NodePort port in ports.Values.ToArray()) { // If port still exists, check it it has been changed NodePort staticPort; if (staticPorts.TryGetValue(port.fieldName, out staticPort)) { // If port exists but with wrong settings, remove it. Re-add it later. if (port.IsDynamic || port.direction != staticPort.direction || port.connectionType != staticPort.connectionType || port.typeConstraint != staticPort.typeConstraint) { // If port is not dynamic and direction hasn't changed, add it to the list so we can try reconnecting the ports connections. if (!port.IsDynamic && port.direction == staticPort.direction) removedPorts.Add(port.fieldName, port.GetConnections()); port.ClearConnections(); ports.Remove(port.fieldName); } else port.ValueType = staticPort.ValueType; } // If port doesn't exist anymore, remove it else if (port.IsStatic) { //See if the field is tagged with FormerlySerializedAs, if so add the port with its new field name to removedPorts // so it can be reconnected in missing ports stage. string newName = null; if (formerlySerializedAs != null && formerlySerializedAs.TryGetValue(port.fieldName, out newName)) removedPorts.Add(newName, port.GetConnections()); port.ClearConnections(); ports.Remove(port.fieldName); } // If the port is dynamic and is managed by a dynamic port list, flag it for reference updates else if (IsDynamicListPort(port)) { dynamicListPorts.Add(port); } } // Add missing ports foreach (NodePort staticPort in staticPorts.Values) { if (!ports.ContainsKey(staticPort.fieldName)) { NodePort port = new NodePort(staticPort, node); //If we just removed the port, try re-adding the connections List reconnectConnections; if (removedPorts.TryGetValue(staticPort.fieldName, out reconnectConnections)) { for (int i = 0; i < reconnectConnections.Count; i++) { NodePort connection = reconnectConnections[i]; if (connection == null) continue; // CAVEAT: Ports connected under special conditions defined in graphEditor.CanConnect overrides will not auto-connect. // To fix this, this code would need to be moved to an editor script and call graphEditor.CanConnect instead of port.CanConnectTo. // This is only a problem in the rare edge case where user is using non-standard CanConnect overrides and changes port type of an already connected port if (port.CanConnectTo(connection)) port.Connect(connection); } } ports.Add(staticPort.fieldName, port); } } // Finally, make sure dynamic list port settings correspond to the settings of their "backing port" foreach (NodePort listPort in dynamicListPorts) { // At this point we know that ports here are dynamic list ports // which have passed name/"backing port" checks, ergo we can proceed more safely. string backingPortName = listPort.fieldName.Substring(0, listPort.fieldName.IndexOf(' ')); NodePort backingPort = staticPorts[backingPortName]; // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters. listPort.ValueType = GetBackingValueType(backingPort.ValueType); listPort.direction = backingPort.direction; listPort.connectionType = backingPort.connectionType; listPort.typeConstraint = backingPort.typeConstraint; } } /// /// Extracts the underlying types from arrays and lists, the only collections for dynamic port lists /// currently supported. If the given type is not applicable (i.e. if the dynamic list port was not /// defined as an array or a list), returns the given type itself. /// private static System.Type GetBackingValueType(System.Type portValType) { if (portValType.HasElementType) { return portValType.GetElementType(); } if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) { return portValType.GetGenericArguments()[0]; } return portValType; } /// Returns true if the given port is in a dynamic port list. private static bool IsDynamicListPort(NodePort port) { // Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have // no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port. // Thus, we need to check for attributes... (but at least we don't need to look at all fields this time) string[] fieldNameParts = port.fieldName.Split(' '); if (fieldNameParts.Length != 2) return false; FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]); if (backingPortInfo == null) return false; object[] attribs = backingPortInfo.GetCustomAttributes(true); return attribs.Any(x => { Node.InputAttribute inputAttribute = x as Node.InputAttribute; Node.OutputAttribute outputAttribute = x as Node.OutputAttribute; return inputAttribute != null && inputAttribute.dynamicPortList || outputAttribute != null && outputAttribute.dynamicPortList; }); } /// Cache node types private static void BuildCache() { portDataCache = new PortDataCache(); System.Type baseType = typeof(Node); List nodeTypes = new List(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); // Loop through assemblies and add node types to list foreach (Assembly assembly in assemblies) { // Skip certain dlls to improve performance string assemblyName = assembly.GetName().Name; int index = assemblyName.IndexOf('.'); if (index != -1) assemblyName = assemblyName.Substring(0, index); switch (assemblyName) { // The following assemblies, and sub-assemblies (eg. UnityEngine.UI) are skipped case "UnityEditor": case "UnityEngine": case "Unity": case "System": case "mscorlib": case "Microsoft": continue; default: nodeTypes.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); break; } } for (int i = 0; i < nodeTypes.Count; i++) { CachePorts(nodeTypes[i]); } } public static List GetNodeFields(System.Type nodeType) { List fieldInfo = new List(nodeType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)); // GetFields doesnt return inherited private fields, so walk through base types and pick those up System.Type tempType = nodeType; while ((tempType = tempType.BaseType) != typeof(XNode.Node)) { FieldInfo[] parentFields = tempType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); for (int i = 0; i < parentFields.Length; i++) { // Ensure that we do not already have a member with this type and name FieldInfo parentField = parentFields[i]; if (fieldInfo.TrueForAll(x => x.Name != parentField.Name)) { fieldInfo.Add(parentField); } } } return fieldInfo; } private static void CachePorts(System.Type nodeType) { List fieldInfo = GetNodeFields(nodeType); for (int i = 0; i < fieldInfo.Count; i++) { //Get InputAttribute and OutputAttribute object[] attribs = fieldInfo[i].GetCustomAttributes(true); Node.InputAttribute inputAttrib = attribs.FirstOrDefault(x => x is Node.InputAttribute) as Node.InputAttribute; Node.OutputAttribute outputAttrib = attribs.FirstOrDefault(x => x is Node.OutputAttribute) as Node.OutputAttribute; UnityEngine.Serialization.FormerlySerializedAsAttribute formerlySerializedAsAttribute = attribs.FirstOrDefault(x => x is UnityEngine.Serialization.FormerlySerializedAsAttribute) as UnityEngine.Serialization.FormerlySerializedAsAttribute; if (inputAttrib == null && outputAttrib == null) continue; if (inputAttrib != null && outputAttrib != null) Debug.LogError("Field " + fieldInfo[i].Name + " of type " + nodeType.FullName + " cannot be both input and output."); else { if (!portDataCache.ContainsKey(nodeType)) portDataCache.Add(nodeType, new Dictionary()); NodePort port = new NodePort(fieldInfo[i]); portDataCache[nodeType].Add(port.fieldName, port); } if (formerlySerializedAsAttribute != null) { if (formerlySerializedAsCache == null) formerlySerializedAsCache = new Dictionary>(); if (!formerlySerializedAsCache.ContainsKey(nodeType)) formerlySerializedAsCache.Add(nodeType, new Dictionary()); if (formerlySerializedAsCache[nodeType].ContainsKey(formerlySerializedAsAttribute.oldName)) Debug.LogError("Another FormerlySerializedAs with value '" + formerlySerializedAsAttribute.oldName + "' already exist on this node."); else formerlySerializedAsCache[nodeType].Add(formerlySerializedAsAttribute.oldName, fieldInfo[i].Name); } } } [System.Serializable] private class PortDataCache : Dictionary> { } } }