using System; using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace NeoSaveGames.Serialization { /// /// A selection of useful methods and utilities for serialization / deserialization /// public static class NeoSerializationUtilities { private static List s_ComponentBuffer = new List(8); private static List s_ReferenceChain = new List(32); private static List s_ReverseChain = new List(32); private static TypeComparer s_TypeComparer = new TypeComparer(); private class TypeComparer : IComparer { public int Compare(Component x, Component y) { if (x == null || y == null) return 0; // "CompareTo()" method return x.GetType().Name.CompareTo(y.GetType().Name); } } /// /// Convert a string to a unique integer ID /// /// The string to generate a hash for /// A hash of the string public static int StringToHash(string key) { return Animator.StringToHash(key); } /// /// Get a consistent ID for a component. This is based off the type name and the index of the type on its GameObject. /// As long as the type name does not change and the component is not reshuffled on the GameObject (more components of the same type can be appended) /// then this method will return the same value. /// /// The component an ID is required for /// An ID value to use for serialization public static int GetPersistentComponentID(Component c) { Type t = c.GetType(); // Get number of component on object c.gameObject.GetComponents(t, s_ComponentBuffer); int i = 0; for (; i < s_ComponentBuffer.Count; ++i) if (s_ComponentBuffer[i] == c) break; s_ComponentBuffer.Clear(); return StringToHash(t.ToString() + i.ToString("D3")); } /// /// Write a serialized component reference. /// /// References are either stored as an ID if it is on the sourceNeoSerializedGameObject. If the component is on a different /// NeoSerializedGameObject then it is stored as a chain of serialization keys for the hierarchy to the object the component is stored on. /// /// The referenced component must be on an object that also contains a NeoSerializedGameObject component which is serialized and in the same scene as the nsgo parameter. /// /// The serializer to use to write the properties /// The component reference value to write /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `WriteProperties()` method /// The key for the reference property /// **true** if the write succeeded and **false** if there was a problem public static bool WriteComponentReference(INeoSerializer writer, T value, NeoSerializedGameObject pathFrom, string key) where T : class { if (string.IsNullOrEmpty(key)) { Debug.LogError("Invalid key when writing component reference (cannot be null or empty)"); return false; } else return WriteComponentReference(writer, value, pathFrom, StringToHash(key)); } /// /// Write a Transform reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced Transform must be on a GameObject with a NeoSerializedGameObject component which is properly serialized and in the same scene as the nsgo parameter. /// /// The serializer to use to write the properties /// The Transform reference value to write /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `WriteProperties()` method /// The key for the reference property /// **true** if the write succeeded and **false** if there was a problem public static bool WriteTransformReference(INeoSerializer writer, Transform value, NeoSerializedGameObject pathFrom, string key) { if (string.IsNullOrEmpty(key)) { Debug.LogError("Invalid key when writing component reference (cannot be null or empty)"); return false; } else return WriteTransformReference(writer, value, pathFrom, StringToHash(key)); } /// /// Write a GameObject reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced GameObject must be have a NeoSerializedGameObject component which is properly serialized and in the same scene as the nsgo parameter. /// /// The serializer to use to write the properties /// The GameObject reference value to write /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `WriteProperties()` method /// The key for the reference property /// **true** if the write succeeded and **false** if there was a problem public static bool WriteGameObjectReference(INeoSerializer writer, GameObject value, NeoSerializedGameObject pathFrom, string key) { if (string.IsNullOrEmpty(key)) { Debug.LogError("Invalid key when writing component reference (cannot be null or empty)"); return false; } else return WriteGameObjectReference(writer, value, pathFrom, StringToHash(key)); } /// /// Write a NeoSerializedGameObject reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced NeoSerializedGameObject must be properly serialized and in the same scene as the nsgo parameter. /// /// The serializer to use to write the properties /// The NeoSerializedGameObject reference value to write /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `WriteProperties()` method /// The key for the reference property /// **true** if the write succeeded and **false** if there was a problem public static bool WriteNeoSerializedGameObjectReference(INeoSerializer writer, NeoSerializedGameObject value, NeoSerializedGameObject pathFrom, string key) { if (string.IsNullOrEmpty(key)) { Debug.LogError("Invalid key when writing component reference (cannot be null or empty)"); return false; } else return WriteNeoSerializedGameObjectReference(writer, value, pathFrom, StringToHash(key)); } /// /// Write a Transform reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced Transform must be on a GameObject with a NeoSerializedGameObject component which is properly serialized and in the same scene as the nsgo parameter. /// /// The serializer to use to write the properties /// The Transform reference value to write /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `WriteProperties()` method /// The hash for the reference property /// **true** if the write succeeded and **false** if there was a problem public static bool WriteTransformReference(INeoSerializer writer, Transform value, NeoSerializedGameObject pathFrom, int hash) { if (value == null) return WriteObjectReference(writer, null, pathFrom, hash); else { var target = value.GetComponent(); if (target == null) { Debug.LogError("Can only write transform references for transforms with a NeoSerializedGameObject behaviour on the same object"); return false; } else return WriteObjectReference(writer, target, pathFrom, hash); } } /// /// Write a GameObject reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced GameObject must be have a NeoSerializedGameObject component which is properly serialized and in the same scene as the nsgo parameter. /// /// The serializer to use to write the properties /// The GameObject reference value to write /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `WriteProperties()` method /// The hash for the reference property /// **true** if the write succeeded and **false** if there was a problem public static bool WriteGameObjectReference(INeoSerializer writer, GameObject value, NeoSerializedGameObject pathFrom, int hash) { if (value == null) return WriteObjectReference(writer, null, pathFrom, hash); else { var target = value.GetComponent(); if (target == null) { Debug.LogError("Can only write GameObject references for transforms with a NeoSerializedGameObject behaviour on the same object"); return false; } else return WriteObjectReference(writer, target, pathFrom, hash); } } /// /// Write a NeoSerializedGameObject reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced NeoSerializedGameObject must be properly serialized and in the same scene as the nsgo parameter. /// /// The serializer to use to write the properties /// The NeoSerializedGameObject reference value to write /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `WriteProperties()` method /// The hash for the reference property /// **true** if the write succeeded and **false** if there was a problem public static bool WriteNeoSerializedGameObjectReference(INeoSerializer writer, NeoSerializedGameObject value, NeoSerializedGameObject pathFrom, int hash) { if (value == null) return WriteObjectReference(writer, null, pathFrom, hash); else return WriteObjectReference(writer, value, pathFrom, hash); } /// /// Write a serialized component reference. /// /// References are either stored as an ID if it is on the sourceNeoSerializedGameObject. If the component is on a different /// NeoSerializedGameObject then it is stored as a chain of serialization keys for the hierarchy to the object the component is stored on. /// /// The referenced component must be on an object that also contains a NeoSerializedGameObject component which is serialized and in the same scene as the nsgo parameter. /// /// The serializer to use to write the properties /// The component reference value to write /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `WriteProperties()` method /// The hash for the reference property /// **true** if the write succeeded and **false** if there was a problem public static bool WriteComponentReference(INeoSerializer writer, T value, NeoSerializedGameObject pathFrom, int hash) where T : class { // Check basic parameters if (writer == null || pathFrom == null) { Debug.LogError("Invalid parameters when writing component reference"); return false; } // Convert to component var cast = value as Component; if (value != null && cast == null) { Debug.LogError("Parameter is not a component"); return false; } // Check if componenet reference is null if (value == null) writer.WriteValues(hash, (int[])null); else { // Check if component has an attached NeoSerializedGameObject var otherNsgo = cast.GetComponent(); if (otherNsgo == null) { Debug.LogError("Can only write component references for components with a NeoSerializedGameObject behaviour on the same object: " + cast.name); return false; } // Check if component is on a different game object to the referencing object // If so, check the other object is valid and then create a chain from the scene root to that object if (otherNsgo != pathFrom) { // Get the serialized scene the object is in var targetScene = NeoSerializedScene.GetByPath(cast.gameObject.scene.path); if (targetScene == null) { Debug.LogError("Cannot write reference because the target object is not in a valid serialized scene.", cast.gameObject); return false; } // Get the serialized scene the object is in var sourceScene = NeoSerializedScene.GetByPath(pathFrom.gameObject.scene.path); if (sourceScene == null) { Debug.LogError("Cannot write reference because the source object is not in a valid serialized scene.", pathFrom.gameObject); return false; } // Check component is serialized if (!otherNsgo.willBeSerialized) { Debug.LogError("Cannot write component reference because attached NeoSerializedGameObject is not serialized by a parent object or in the root of a serialized scene: " + otherNsgo.name, otherNsgo.gameObject); return false; } // Build reference chain (needs reversing) do { s_ReferenceChain.Add(otherNsgo); otherNsgo = otherNsgo.GetParent(); } while (otherNsgo != null); // Start path chain with scene ID (0 if same scene as source) if (sourceScene.hashedPath == targetScene.hashedPath) s_ReverseChain.Add(0); else s_ReverseChain.Add(targetScene.hashedPath); // Walk up from root object to target component's object for (int i = s_ReferenceChain.Count - 1; i >= 0; --i) s_ReverseChain.Add(s_ReferenceChain[i].serializationKey); } // Add the component ID s_ReverseChain.Add(GetPersistentComponentID(cast)); // Write the chain writer.WriteValues(hash, s_ReverseChain); // Clean up s_ReferenceChain.Clear(); s_ReverseChain.Clear(); } return true; } /// /// Write a NeoSerializedGameObject reference. /// /// References are either stored as an ID if it is on the sourceNeoSerializedGameObject. If the component is on a different /// NeoSerializedGameObject then it is stored as a chain of serialization keys for the hierarchy to the object the component is stored on. /// /// The referenced component must be on an object that also contains a NeoSerializedGameObject component which is serialized and in the same scene as the nsgo parameter. /// /// The serializer to use to write the properties /// The component reference value to write /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `WriteProperties()` method /// The hash for the reference property /// **true** if the write succeeded and **false** if there was a problem private static bool WriteObjectReference(INeoSerializer writer, NeoSerializedGameObject value, NeoSerializedGameObject pathFrom, int hash) { // Check basic parameters if (writer == null || pathFrom == null) { Debug.LogError("Invalid parameters when writing component reference"); return false; } // Check if reference is null if (value == null) writer.WriteValues(hash, (int[])null); else { // Check if value is on a different game object to the referencing object // If so, check the other object is valid and then create a chain from the scene root to that object if (value != pathFrom) { // Get the serialized scene the object is in var targetScene = NeoSerializedScene.GetByPath(value.gameObject.scene.path); if (targetScene == null) { Debug.LogError("Cannot write reference because the target object is not in a valid serialized scene.", value.gameObject); return false; } // Get the serialized scene the object is in var sourceScene = NeoSerializedScene.GetByPath(pathFrom.gameObject.scene.path); if (sourceScene == null) { Debug.LogError("Cannot write reference because the source object is not in a valid serialized scene.", pathFrom.gameObject); return false; } // Check GameObject is serialized if (!value.willBeSerialized) { Debug.LogError("Cannot write reference because attached NeoSerializedGameObject is not serialized by a parent object or in the root of a serialized scene.", value.gameObject); return false; } // Walk parent chain and add to list do { s_ReferenceChain.Add(value); value = value.GetParent(); } while (value != null); // Start path chain with scene ID (0 if same scene as source) if (sourceScene.hashedPath == targetScene.hashedPath) s_ReverseChain.Add(0); else s_ReverseChain.Add(targetScene.hashedPath); // Walk up from root object to target object for (int i = s_ReferenceChain.Count - 1; i >= 0; --i) s_ReverseChain.Add(s_ReferenceChain[i].serializationKey); } // Write the chain writer.WriteValues(hash, s_ReverseChain); // Clean up s_ReferenceChain.Clear(); s_ReverseChain.Clear(); } return true; } /// /// Read a Transform reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced Transform must be on a GameObject with a NeoSerializedGameObject component which is properly serialized and in the same scene as the nsgo parameter. /// /// The deserializer to use to read the properties /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `ReadProperties()` method /// The key for the reference property /// The referenced Transform or null if not found public static bool TryReadTransformReference(INeoDeserializer reader, out Transform output, NeoSerializedGameObject pathFrom, string key) { return TryReadTransformReference(reader, out output, pathFrom, StringToHash(key)); } /// /// Read a Transform reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced Transform must be on a GameObject with a NeoSerializedGameObject component which is properly serialized and in the same scene as the nsgo parameter. /// /// The deserializer to use to read the properties /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `ReadProperties()` method /// The hash for the reference property /// The referenced Transform or null if not found public static bool TryReadTransformReference(INeoDeserializer reader, out Transform output, NeoSerializedGameObject pathFrom, int hash) { NeoSerializedGameObject result; if (TryReadNeoSerializedGameObjectReference(reader, out result, pathFrom, hash)) { output = (result != null) ? result.transform : null; return true; } else { output = null; return false; } } /// /// Read a GameObject reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced GameObject must be have a NeoSerializedGameObject component which is properly serialized and in the same scene as the nsgo parameter. /// /// The deserializer to use to read the properties /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `ReadProperties()` method /// The key for the reference property /// The referenced GameObject or null if not found public static bool TryReadGameObjectReference(INeoDeserializer reader, out GameObject output, NeoSerializedGameObject pathFrom, string key) { return TryReadGameObjectReference(reader, out output, pathFrom, StringToHash(key)); } /// /// Read a GameObject reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced GameObject must be have a NeoSerializedGameObject component which is properly serialized and in the same scene as the nsgo parameter. /// /// The deserializer to use to read the properties /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `ReadProperties()` method /// The hash for the reference property /// The referenced GameObject or null if not found public static bool TryReadGameObjectReference(INeoDeserializer reader, out GameObject output, NeoSerializedGameObject pathFrom, int hash) { NeoSerializedGameObject result; if (TryReadNeoSerializedGameObjectReference(reader, out result, pathFrom, hash)) { output = (result != null) ? result.gameObject : null; return true; } else { output = null; return false; } } /// /// Read a NeoSerializedGameObject reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced NeoSerializedGameObject must be properly serialized and in the same scene as the nsgo parameter. /// /// The deserializer to use to read the properties /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `ReadProperties()` method /// The key for the reference property /// The referenced NeoSerializedGameObject or null if not found public static bool TryReadNeoSerializedGameObjectReference(INeoDeserializer reader, out NeoSerializedGameObject output, NeoSerializedGameObject pathFrom, string key) { return TryReadNeoSerializedGameObjectReference(reader, out output, pathFrom, StringToHash(key)); } /// /// Read a NeoSerializedGameObject reference. /// /// References are stored as a chain of serialization keys for the object hierarchy, from the scene root to the target object. /// /// The referenced NeoSerializedGameObject must be properly serialized and in the same scene as the nsgo parameter. /// /// The deserializer to use to read the properties /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `ReadProperties()` method /// The hash for the reference property /// The referenced NeoSerializedGameObject or null if not found /// public static bool TryReadNeoSerializedGameObjectReference(INeoDeserializer reader, out NeoSerializedGameObject output, NeoSerializedGameObject pathFrom, int hash) { output = null; int[] chain = null; if (reader.TryReadValues(hash, out chain, null)) { // Check for null if (chain == null) return false; // Check if on the same object if (chain.Length == 0) return pathFrom; else { // Get the target scene NeoSerializedScene targetScene = null; if (chain[0] == 0) { // Get from source object if (pathFrom == null) return false; targetScene = NeoSerializedScene.GetByPath(pathFrom.gameObject.scene.path); } else targetScene = NeoSerializedScene.GetByPathHash(chain[0]); // Iterate through the chain if (targetScene != null) { // Grab start of chain var next = targetScene.sceneObjects[chain[1]]; if (next == null) return false; // Work along chain to target object for (int i = 2; i < chain.Length; ++i) { // Get the next object in the chain next = next.serializedChildren[chain[i]]; if (next == null) return false; } output = next; return true; } else return false; } } else return false; } /// /// Read a serialized component reference. /// /// References are either stored as an ID if it is on the sourceNeoSerializedGameObject. If the component is on a different /// NeoSerializedGameObject then it is stored as a chain of serialization keys for the hierarchy to the object the component is stored on. /// /// The referenced component must be on an object that also contains a NeoSerializedGameObject component which is serialized and in the same scene as the nsgo parameter. /// /// The component type required /// The deserializer to use to read the properties /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `ReadProperties()` method /// The key for the reference property /// The referenced component or null if not found public static bool TryReadComponentReference(INeoDeserializer reader, out T output, NeoSerializedGameObject pathFrom, string key) where T : class { return TryReadComponentReference(reader, out output, pathFrom, StringToHash(key)); } /// /// Read a serialized component reference. /// /// References are either stored as an ID if it is on the sourceNeoSerializedGameObject. If the component is on a different /// NeoSerializedGameObject then it is stored as a chain of serialization keys for the hierarchy to the object the component is stored on. /// /// The referenced component must be on an object that also contains a NeoSerializedGameObject component which is serialized and in the same scene as the nsgo parameter. /// /// The component type required /// The deserializer to use to read the properties /// The serialized game object to use as the source. If the reference property is in a serialized component, this should be the object passed to its `ReadProperties()` method /// The hash for the reference property /// The referenced component or null if not found public static bool TryReadComponentReference(INeoDeserializer reader, out T output, NeoSerializedGameObject pathFrom, int hash) where T : class { output = null; // Get the chain to the component int[] chain = null; if (!reader.TryReadValues(hash, out chain, null) | chain == null) return false; // Get the NeoSerializedGameObject the component is attached to NeoSerializedGameObject owner = null; switch (chain.Length) { case 0: return false; case 1: owner = pathFrom; break; default: { // Get the target scene NeoSerializedScene targetScene = null; if (chain[0] == 0) { // Get from source object if (pathFrom == null) return false; targetScene = NeoSerializedScene.GetByPath(pathFrom.gameObject.scene.path); } else { targetScene = NeoSerializedScene.GetByPathHash(chain[0]); } // Iterate through the chain if (targetScene != null) { // Grab start of chain var next = targetScene.sceneObjects[chain[1]]; if (next == null) return false; // Work along chain to containing object for (int i = 2; i < chain.Length - 1; ++i) { // Get the next object in the chain next = next.serializedChildren[chain[i]]; if (next == null) return false; } owner = next; } else { return false; } } break; } // Get all components on object and sort by type owner.GetComponents(s_ComponentBuffer); s_ComponentBuffer.Sort(s_TypeComparer); // Iterate through and check hashes Type lastType = null; int count = 0; for(int i = 0; i < s_ComponentBuffer.Count; ++i) { // Get type and check count on object var t = s_ComponentBuffer[i].GetType(); if (t != lastType) { lastType = t; count = 0; } else ++count; // Get hash and check against target int currentHash = StringToHash(t.ToString() + count.ToString("D3")); if (currentHash == chain[chain.Length - 1]) { //Debug.Log("Found component reference, index: " + count); output = s_ComponentBuffer[i] as T; return true; } } // Hash wasn't found //Debug.Log("Component reference not found"); return false; } } }