using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using Unity.VisualScripting.FullSerializer.Internal; using UnityEngine; using UnityObject = UnityEngine.Object; #if !UNITY_EDITOR && UNITY_WSA // For System.Reflection.TypeExtensions using System.Reflection; #endif namespace Unity.VisualScripting.FullSerializer { public class fsSerializer { public fsSerializer() { _cachedConverterTypeInstances = new Dictionary(); _cachedConverters = new Dictionary(); _cachedProcessors = new Dictionary>(); _references = new fsCyclicReferenceManager(); _lazyReferenceWriter = new fsLazyCycleDefinitionWriter(); // note: The order here is important. Items at the beginning of this // list will be used before converters at the end. Converters // added via AddConverter() are added to the front of the list. _availableConverters = new List { new fsNullableConverter { Serializer = this }, new fsGuidConverter { Serializer = this }, new fsTypeConverter { Serializer = this }, new fsDateConverter { Serializer = this }, new fsEnumConverter { Serializer = this }, new fsPrimitiveConverter { Serializer = this }, new fsArrayConverter { Serializer = this }, new fsDictionaryConverter { Serializer = this }, new fsIEnumerableConverter { Serializer = this }, new fsKeyValuePairConverter { Serializer = this }, new fsWeakReferenceConverter { Serializer = this }, new fsReflectedConverter { Serializer = this } }; _availableDirectConverters = new Dictionary(); _processors = new List() { new fsSerializationCallbackProcessor() }; #if !NO_UNITY _processors.Add(new fsSerializationCallbackReceiverProcessor()); #endif _abstractTypeRemap = new Dictionary(); SetDefaultStorageType(typeof(ICollection<>), typeof(List<>)); SetDefaultStorageType(typeof(IList<>), typeof(List<>)); SetDefaultStorageType(typeof(IDictionary<,>), typeof(Dictionary<,>)); Context = new fsContext(); Config = new fsConfig(); // Register the converters from the registrar foreach (var converterType in fsConverterRegistrar.Converters) { AddConverter((fsBaseConverter)Activator.CreateInstance(converterType)); } } /// /// Converters that can be used for type registration. /// private readonly List _availableConverters; /// /// Direct converters (optimized _converters). We use these so we don't /// have to perform a scan through every item in _converters and can /// instead just do an O(1) lookup. This is potentially important to perf /// when there are a ton of direct converters. /// private readonly Dictionary _availableDirectConverters; /// /// Processors that are available. /// private readonly List _processors; /// /// Reference manager for cycle detection. /// private readonly fsCyclicReferenceManager _references; private readonly fsLazyCycleDefinitionWriter _lazyReferenceWriter; /// /// Allow the user to provide default storage types for interfaces and abstract /// classes. For example, a model could have IList{int} as a parameter, but the /// serialization data does not specify a List{int} type. A IList{} -> List{} /// remapping will cause List{} to be used as the default storage type. see /// https://github.com/jacobdufault/fullserializer/issues/120 for additional /// context. /// private readonly Dictionary _abstractTypeRemap; // of _cachedConverters, but there is a semantic difference because // _cachedConverters goes /// /// Converter type to converter instance lookup table. This /// could likely be stored inside /// from serialized type to converter. /// private Dictionary _cachedConverterTypeInstances; /// /// A cache from type to it's converter. /// private Dictionary _cachedConverters; /// /// A cache from type to the set of processors that are interested in it. /// private Dictionary> _cachedProcessors; /// /// A context object that fsConverters can use to customize how they /// operate. /// public fsContext Context; /// /// Configuration options. Also see fsGlobalConfig. /// public fsConfig Config; private void RemapAbstractStorageTypeToDefaultType(ref Type storageType) { if ((storageType.Resolve().IsInterface || storageType.Resolve().IsAbstract) == false) { return; } if (storageType.Resolve().IsGenericType) { Type remappedGenericType; if (_abstractTypeRemap.TryGetValue(storageType.Resolve().GetGenericTypeDefinition(), out remappedGenericType)) { var genericArguments = storageType.GetGenericArguments(); storageType = remappedGenericType.Resolve().MakeGenericType(genericArguments); } } else { Type remappedType; if (_abstractTypeRemap.TryGetValue(storageType, out remappedType)) { storageType = remappedType; } } } /// /// Add a new processor to the serializer. Multiple processors can run at /// the same time in the same order they were added in. /// /// The processor to add. public void AddProcessor(fsObjectProcessor processor) { _processors.Add(processor); // We need to reset our cached processor set, as it could be invalid // with the new processor. Ideally, _cachedProcessors should be empty // (as the user should fully setup the serializer before actually // using it), but there is no guarantee. _cachedProcessors = new Dictionary>(); } /// /// Remove all processors which derive from TProcessor. /// public void RemoveProcessor() { var i = 0; while (i < _processors.Count) { if (_processors[i] is TProcessor) { _processors.RemoveAt(i); } else { ++i; } } // We need to reset our cached processor set, as it could be invalid // with the new processor. Ideally, _cachedProcessors should be empty // (as the user should fully setup the serializer before actually // using it), but there is no guarantee. _cachedProcessors = new Dictionary>(); } /// /// Provide a default storage type for the given abstract or interface type. If /// a type is deserialized which contains an interface/abstract field type and a /// mapping is provided, the mapped type will be used by default. For example, /// IList{T} => List{T} or IDictionary{TKey, TValue} => Dictionary{TKey, TValue}. /// public void SetDefaultStorageType(Type abstractType, Type defaultStorageType) { if ((abstractType.Resolve().IsInterface || abstractType.Resolve().IsAbstract) == false) { throw new ArgumentException("|abstractType| must be an interface or abstract type"); } _abstractTypeRemap[abstractType] = defaultStorageType; } /// /// Fetches all of the processors for the given type. /// private List GetProcessors(Type type) { List processors; // Check to see if the user has defined a custom processor for the // type. If they have, then we don't need to scan through all of the // processor to check which one can process the type; instead, we // directly use the specified processor. var attr = fsPortableReflection.GetAttribute(type); if (attr != null && attr.Processor != null) { var processor = (fsObjectProcessor)Activator.CreateInstance(attr.Processor); processors = new List(); processors.Add(processor); _cachedProcessors[type] = processors; } else if (_cachedProcessors.TryGetValue(type, out processors) == false) { processors = new List(); for (var i = 0; i < _processors.Count; ++i) { var processor = _processors[i]; if (processor.CanProcess(type)) { processors.Add(processor); } } _cachedProcessors[type] = processors; } return processors; } /// /// Adds a new converter that can be used to customize how an object is /// serialized and deserialized. /// public void AddConverter(fsBaseConverter converter) { if (converter.Serializer != null) { throw new InvalidOperationException("Cannot add a single converter instance to " + "multiple fsConverters -- please construct a new instance for " + converter); } // TODO: wrap inside of a ConverterManager so we can control // _converters and _cachedConverters lifetime if (converter is fsDirectConverter) { var directConverter = (fsDirectConverter)converter; _availableDirectConverters[directConverter.ModelType] = directConverter; } else if (converter is fsConverter) { _availableConverters.Insert(0, (fsConverter)converter); } else { throw new InvalidOperationException("Unable to add converter " + converter + "; the type association strategy is unknown. Please use either " + "fsDirectConverter or fsConverter as your base type."); } converter.Serializer = this; // We need to reset our cached converter set, as it could be invalid // with the new converter. Ideally, _cachedConverters should be empty // (as the user should fully setup the serializer before actually // using it), but there is no guarantee. _cachedConverters = new Dictionary(); } /// /// Fetches a converter that can serialize/deserialize the given type. /// private fsBaseConverter GetConverter(Type type, Type overrideConverterType) { // Use an override converter type instead if that's what the user has // requested. if (overrideConverterType != null) { fsBaseConverter overrideConverter; if (_cachedConverterTypeInstances.TryGetValue(overrideConverterType, out overrideConverter) == false) { overrideConverter = (fsBaseConverter)Activator.CreateInstance(overrideConverterType); overrideConverter.Serializer = this; _cachedConverterTypeInstances[overrideConverterType] = overrideConverter; } return overrideConverter; } // Try to lookup an existing converter. fsBaseConverter converter; if (_cachedConverters.TryGetValue(type, out converter)) { return converter; } // Check to see if the user has defined a custom converter for the // type. If they have, then we don't need to scan through all of the // converters to check which one can process the type; instead, we // directly use the specified converter. { var attr = fsPortableReflection.GetAttribute(type); if (attr != null && attr.Converter != null) { converter = (fsBaseConverter)Activator.CreateInstance(attr.Converter); converter.Serializer = this; return _cachedConverters[type] = converter; } } // Check for a [fsForward] attribute. { var attr = fsPortableReflection.GetAttribute(type); if (attr != null) { converter = new fsForwardConverter(attr); converter.Serializer = this; return _cachedConverters[type] = converter; } } // There is no specific converter specified; try all of the general // ones to see which ones matches. if (_cachedConverters.TryGetValue(type, out converter) == false) { if (_availableDirectConverters.ContainsKey(type)) { converter = _availableDirectConverters[type]; return _cachedConverters[type] = converter; } else { for (var i = 0; i < _availableConverters.Count; ++i) { if (_availableConverters[i].CanProcess(type)) { converter = _availableConverters[i]; return _cachedConverters[type] = converter; } } } } throw new InvalidOperationException("Internal error -- could not find a converter for " + type); } /// /// Helper method that simply forwards the call to /// TrySerialize(typeof(T), instance, out data); /// public fsResult TrySerialize(T instance, out fsData data) { return TrySerialize(typeof(T), instance, out data); } /// /// Generic wrapper around TryDeserialize that simply forwards the call. /// public fsResult TryDeserialize(fsData data, ref T instance) { object boxed = instance; var fail = TryDeserialize(data, typeof(T), ref boxed); if (fail.Succeeded) { instance = (T)boxed; } return fail; } /// /// Serialize the given value. /// /// /// The type of field/property that stores the object instance. This is /// important particularly for inheritance, as a field storing an /// IInterface instance should have type information included. /// /// /// The actual object instance to serialize. /// /// The serialized state of the object. /// If serialization was successful. public fsResult TrySerialize(Type storageType, object instance, out fsData data) { return TrySerialize(storageType, null, instance, out data); } /// /// Serialize the given value. /// /// /// The type of field/property that stores the object instance. This is /// important particularly for inheritance, as a field storing an /// IInterface instance should have type information included. /// /// /// An fsBaseConverter derived type that will be used to serialize the /// object instead of the converter found via the normal discovery /// mechanisms. /// /// /// The actual object instance to serialize. /// /// The serialized state of the object. /// If serialization was successful. public fsResult TrySerialize(Type storageType, Type overrideConverterType, object instance, out fsData data) { var processors = GetProcessors(instance == null ? storageType : instance.GetType()); // LAZLO / LUDIQ FIX try { Invoke_OnBeforeSerialize(processors, storageType, instance); } catch (Exception ex) { data = new fsData(); return fsResult.Fail(ex.ToString()); } // We always serialize null directly as null if (ReferenceEquals(instance, null)) { data = new fsData(); Invoke_OnAfterSerialize(processors, storageType, instance, ref data); return fsResult.Success; } var result = InternalSerialize_1_ProcessCycles(storageType, overrideConverterType, instance, out data); try { Invoke_OnAfterSerialize(processors, storageType, instance, ref data); } catch (Exception ex) { result += fsResult.Fail(ex.ToString()); } return result; } private fsResult InternalSerialize_1_ProcessCycles(Type storageType, Type overrideConverterType, object instance, out fsData data) { // We have an object definition to serialize. try { // Note that we enter the reference group at the beginning of // serialization so that we support references that are at equal // serialization levels, not just nested serialization levels, // within the given subobject. A prime example is serialization a // list of references. _references.Enter(); // This type does not need cycle support. var converter = GetConverter(instance.GetType(), overrideConverterType); if (converter.RequestCycleSupport(instance.GetType()) == false) { return InternalSerialize_2_Inheritance(storageType, overrideConverterType, instance, out data); } // We've already serialized this object instance (or it is // pending higher up on the call stack). Just serialize a // reference to it to escape the cycle. // // note: We serialize the int as a string to so that we don't // lose any information in a conversion to/from double. if (_references.IsReference(instance)) { data = fsData.CreateDictionary(); _lazyReferenceWriter.WriteReference(_references.GetReferenceId(instance), data.AsDictionary); return fsResult.Success; } // Mark inside the object graph that we've serialized the // instance. We do this *before* serialization so that if we get // back into this function recursively, it'll already be marked // and we can handle the cycle properly without going into an // infinite loop. _references.MarkSerialized(instance); // We've created the cycle metadata, so we can now serialize the // actual object. InternalSerialize will handle inheritance // correctly for us. var result = InternalSerialize_2_Inheritance(storageType, overrideConverterType, instance, out data); if (result.Failed) { return result; } _lazyReferenceWriter.WriteDefinition(_references.GetReferenceId(instance), data); return result; } finally { if (_references.Exit()) { _lazyReferenceWriter.Clear(); } } } private fsResult InternalSerialize_2_Inheritance(Type storageType, Type overrideConverterType, object instance, out fsData data) { // Serialize the actual object with the field type being the same as // the object type so that we won't go into an infinite loop. var serializeResult = InternalSerialize_3_ProcessVersioning(overrideConverterType, instance, out data); if (serializeResult.Failed) { return serializeResult; } // Do we need to add type information? If the field type and the // instance type are different then we will not be able to recover // the correct instance type from the field type when we deserialize // the object. // // Note: We allow converters to request that we do *not* add type // information. if (storageType != instance.GetType() && GetConverter(storageType, overrideConverterType).RequestInheritanceSupport(storageType)) { var instanceType = instance.GetType(); // LAZLO / LUDIQ // We need to loosen the instance type hint of Unity objects when serializing // to counter a very specific issue that happens when: // - We serialize a reference to an object of an editor type // - That object type inherits a runtime type // - The field supports the runtime type, but will allow the editor type // - Therefore serializing as the editor type is valid, but will fail to deserialize in builds // The only current example of this bug is AudioMixers and AudioMixerGroups. // UnityEDITOR.Audio.AudioMixerController extends UnityENGINE.Audio.AudioMixer. // UnityEDITOR.Audio.AudioMixerGroupController extends UnityENGINE.Audio.AudioMixerGroup. // Therefore, if we serialize a type hint to the editor controller, e.g. AudioMixerGroupController, // builds will fail to deserialize the type hint, even though they don't actually need it // to properly fetch the Unity Object reference, because it's provided directly by the converter. // We must instead serialize a type hint to the runtime, non-controller type, e.g. AudioMixer. // However, when loosening our type, we must make sure not to go past the compatibility // with the defined storage type, because if we did, we would get the "Ignoring type specifier" // error defined below, as the instance type hint wouldn't be assignable to the storage type on deserialization. // Likewise, we must make sure not to go above UnityObject itself, because we need that much hinting // for FullSerializer to know that the proper converter to be used is our custom UnityObjectConverter. // See: https://support.ludiq.io/communities/5/topics/1032-audio-mixer-reference-gets-nulled-on-il2cpp-builds if (instance is UnityObject) { var looseType = instanceType; do { instanceType = looseType; looseType = looseType.BaseType; } while (looseType != null && instanceType != typeof(UnityObject) && storageType.IsAssignableFrom(looseType)); // Debug.Log($"Loosened instance type hint for {instance.GetType()} stored as {storageType} to {instanceType}"); } // Add the inheritance metadata EnsureDictionary(data); data.AsDictionary[Key_InstanceType] = new fsData(RuntimeCodebase.SerializeType(instanceType)); } return serializeResult; } private fsResult InternalSerialize_3_ProcessVersioning(Type overrideConverterType, object instance, out fsData data) { // note: We do not have to take a Type parameter here, since at this // point in the serialization algorithm inheritance has // *always* been handled. If we took a type parameter, it will // *always* be equal to instance.GetType(), so why bother taking the // parameter? // Check to see if there is versioning information for this type. If // so, then we need to serialize it. var optionalVersionedType = fsVersionManager.GetVersionedType(instance.GetType()); if (optionalVersionedType.HasValue) { var versionedType = optionalVersionedType.Value; // Serialize the actual object content; we'll just wrap it with // versioning metadata here. var result = InternalSerialize_4_Converter(overrideConverterType, instance, out data); if (result.Failed) { return result; } // Add the versioning information EnsureDictionary(data); data.AsDictionary[Key_Version] = new fsData(versionedType.VersionString); return result; } // This type has no versioning information -- directly serialize it // using the selected converter. return InternalSerialize_4_Converter(overrideConverterType, instance, out data); } private fsResult InternalSerialize_4_Converter(Type overrideConverterType, object instance, out fsData data) { var instanceType = instance.GetType(); return GetConverter(instanceType, overrideConverterType).TrySerialize(instance, out data, instanceType); } /// /// Attempts to deserialize a value from a serialized state. /// public fsResult TryDeserialize(fsData data, Type storageType, ref object result) { return TryDeserialize(data, storageType, null, ref result); } /// /// Attempts to deserialize a value from a serialized state. /// public fsResult TryDeserialize(fsData data, Type storageType, Type overrideConverterType, ref object result) { if (data.IsNull) { result = null; var processors = GetProcessors(storageType); Invoke_OnBeforeDeserialize(processors, storageType, ref data); Invoke_OnAfterDeserialize(processors, storageType, null); return fsResult.Success; } // Convert legacy data into modern style data ConvertLegacyData(ref data); try { // We wrap the entire deserialize call in a reference group so // that we can properly deserialize a "parallel" set of // references, ie, a list of objects that are cyclic w.r.t. the // list _references.Enter(); List processors; var r = InternalDeserialize_1_CycleReference(overrideConverterType, data, storageType, ref result, out processors); if (r.Succeeded) { // LAZLO / LUDIQ FIX // Converts exceptions thrown in OAD to a FS-understandable failure. // Extremely useful in collections, this will allow fsIEnumerableConverter // to continue (as it should) even if an item threw an exception try { Invoke_OnAfterDeserialize(processors, storageType, result); } catch (Exception ex) { r += fsResult.Fail(ex.ToString()); } } return r; } finally { _references.Exit(); } } private fsResult InternalDeserialize_1_CycleReference(Type overrideConverterType, fsData data, Type storageType, ref object result, out List processors) { // We handle object references first because we could be // deserializing a cyclic type that is inherited. If that is the // case, then if we handle references after inheritances we will try // to create an object instance for an abstract/interface type. // While object construction should technically be two-pass, we can // do it in one pass because of how serialization happens. We // traverse the serialization graph in the same order during // serialization and deserialization, so the first time we encounter // an object it'll always be the definition. Any times after that it // will be a reference. Because of this, if we encounter a reference // then we will have *always* already encountered the definition for // it. if (IsObjectReference(data)) { var refId = int.Parse(data.AsDictionary[Key_ObjectReference].AsString); result = _references.GetReferenceObject(refId); processors = GetProcessors(result.GetType()); return fsResult.Success; } return InternalDeserialize_2_Version(overrideConverterType, data, storageType, ref result, out processors); } private fsResult InternalDeserialize_2_Version(Type overrideConverterType, fsData data, Type storageType, ref object result, out List processors) { if (IsVersioned(data)) { // data is versioned, but we might not need to do a migration var version = data.AsDictionary[Key_Version].AsString; var versionedType = fsVersionManager.GetVersionedType(storageType); if (versionedType.HasValue && versionedType.Value.VersionString != version) { // we have to do a migration var deserializeResult = fsResult.Success; List path; deserializeResult += fsVersionManager.GetVersionImportPath(version, versionedType.Value, out path); if (deserializeResult.Failed) { processors = GetProcessors(storageType); return deserializeResult; } // deserialize as the original type deserializeResult += InternalDeserialize_3_Inheritance(overrideConverterType, data, path[0].ModelType, ref result, out processors); if (deserializeResult.Failed) { return deserializeResult; } // TODO: we probably should be invoking object processors all // along this pipeline for (var i = 1; i < path.Count; ++i) { result = path[i].Migrate(result); } // Our data contained an object definition ($id) that was // added to _references in step 4. However, in case we are // doing versioning, it will contain the old version. To make // sure future references to this object end up referencing // the migrated version, we must update the reference. if (IsObjectDefinition(data)) { var sourceId = int.Parse(data.AsDictionary[Key_ObjectDefinition].AsString); _references.AddReferenceWithId(sourceId, result); } processors = GetProcessors(deserializeResult.GetType()); return deserializeResult; } } return InternalDeserialize_3_Inheritance(overrideConverterType, data, storageType, ref result, out processors); } private fsResult InternalDeserialize_3_Inheritance(Type overrideConverterType, fsData data, Type storageType, ref object result, out List processors) { var deserializeResult = fsResult.Success; var objectType = storageType; // If the serialized state contains type information, then we need to // make sure to update our objectType and data to the proper values // so that when we construct an object instance later and run // deserialization we run it on the proper type. if (IsTypeSpecified(data)) { objectType = GetDataType(ref data, storageType, ref deserializeResult); } RemapAbstractStorageTypeToDefaultType(ref objectType); // We wait until here to actually Invoke_OnBeforeDeserialize because // we do not have the correct set of processors to invoke until // *after* we have resolved the proper type to use for // deserialization. processors = GetProcessors(objectType); if (deserializeResult.Failed) { return deserializeResult; } // LAZLO / LUDIQ FIX try { Invoke_OnBeforeDeserialize(processors, storageType, ref data); } catch (Exception ex) { deserializeResult += fsResult.Fail(ex.ToString()); return deserializeResult; } // Construct an object instance if we don't have one already. We also // need to construct an instance if the result type is of the wrong // type, which may be the case when we have a versioned import graph. if (ReferenceEquals(result, null) || result.GetType() != objectType) { result = GetConverter(objectType, overrideConverterType).CreateInstance(data, objectType); } // We call OnBeforeDeserializeAfterInstanceCreation here because we // still want to invoke the method even if the user passed in an // existing instance. try { Invoke_OnBeforeDeserializeAfterInstanceCreation(processors, storageType, result, ref data); } catch (Exception ex) { deserializeResult += fsResult.Fail(ex.ToString()); return deserializeResult; } // NOTE: It is critically important that we pass the actual // objectType down instead of using result.GetType() because it // is not guaranteed that result.GetType() will equal // objectType, especially because some converters are known to // return dummy values for CreateInstance() (for example, the // default behavior for structs is to just return the type of // the struct). deserializeResult += InternalDeserialize_4_Cycles(overrideConverterType, data, objectType, ref result); return deserializeResult; } private fsResult InternalDeserialize_4_Cycles(Type overrideConverterType, fsData data, Type resultType, ref object result) { if (IsObjectDefinition(data)) { // NOTE: object references are handled at stage 1 // If this is a definition, then we have a serialization // invariant that this is the first time we have encountered the // object (TODO: verify in the deserialization logic) // Since at this stage in the deserialization process we already // have access to the object instance, so we just need to sync // the object id to the references database so that when we // encounter the instance we lookup this same object. We want to // do this before actually deserializing the object because when // deserializing the object there may be references to itself. var sourceId = int.Parse(data.AsDictionary[Key_ObjectDefinition].AsString); _references.AddReferenceWithId(sourceId, result); } // Nothing special, go through the standard deserialization logic. return InternalDeserialize_5_Converter(overrideConverterType, data, resultType, ref result); } private fsResult InternalDeserialize_5_Converter(Type overrideConverterType, fsData data, Type resultType, ref object result) { if (IsWrappedData(data)) { data = data.AsDictionary[Key_Content]; } return GetConverter(resultType, overrideConverterType).TryDeserialize(data, ref result, resultType); } // This method only really exists to make the InternalDeserialize_3_Inheritance() method cleaner. private static Type GetDataType(ref fsData data, Type defaultType, ref fsResult deserializeResult) { var objectType = defaultType; var dict = data.AsDictionary; var typeNameData = dict[Key_InstanceType]; if (typeNameData.IsString == false) { deserializeResult.AddMessage(Key_InstanceType + " value must be a string (in " + data + ")"); return objectType; } var typeName = typeNameData.AsString; if (!RuntimeCodebase.TryDeserializeType(typeName, out var markedType)) { if (IsVisualScriptingUnit(data)) { //We store a copy of the node as a string in the hopes of being able to re-instantiate it later. dict[Key_UnitFormerValue] = new fsData(data.ToString()); // We store the type that the unit should be, we will try to re-instantiate it if it becomes available again. dict[Key_UnitFormerType] = typeNameData; dict[Key_InstanceType] = new fsData(TypeName_MissingType); // TODO: Ideally this would display as an error in the console instead of a warning. Using fsResult.Fail() aborts the deserialization. deserializeResult += fsResult.Warn($"Type definition for '{typeName}' is missing.\nConverted '{typeName}' unit to '{TypeName_MissingType}'. Did you delete the type's script file?"); return Type_MissingType; } // This message is redundant if the above warning gets logged. deserializeResult += fsResult.Warn("Unable to find type: \"" + typeName + "\""); return objectType; } // Check if the former type of the MissingType unit is defined again (if the user added back the unit script). else if (typeName == TypeName_MissingType) { if (dict.ContainsKey(Key_UnitFormerType) && IsVisualScriptingUnit(data)) { var formerTypeName = dict[Key_UnitFormerType].AsString; if (RuntimeCodebase.TryDeserializeType(formerTypeName, out var formerType)) { // If the user tries to create a new script for the type, we must ensure that it derives from VS units so that it can be properly deserialized. if (defaultType.IsAssignableFrom(formerType)) { //TODO: Add checks if the json can't be parsed to a unit. if (dict.ContainsKey(Key_UnitFormerValue)) { // The node may have been moved while in dummy form. fsData newPosition = dict[Key_UnitPosition]; data = fsJsonParser.Parse(dict[Key_UnitFormerValue].AsString); dict = data.AsDictionary; // 'dict' and 'data' are meant to represent the same object. dict[Key_UnitPosition] = newPosition; deserializeResult += fsResult.Warn($"Missing unit type '{formerTypeName}' was found.\nConverted '{TypeName_MissingType}' unit back to '{formerTypeName}'"); } else { // We want to restore the unit to its correct type. dict[Key_InstanceType] = new fsData(formerTypeName); deserializeResult += fsResult.Warn($"Missing unit type '{formerTypeName}' was found.\nConverted '{TypeName_MissingType}' unit back to '{formerTypeName}'\nNo former state can be found. Reverting node to defaults.\n" + data); } objectType = formerType; return objectType; } // TODO: Ideally this would display as an error in the console instead of a warning. Using fsResult.Fail() aborts the deserialization. deserializeResult += fsResult.Warn($"Missing unit type '{formerTypeName}' was found, but is not assignable to '{defaultType.FullName}'. Did you forget to inherit from '{TypeName_Unit}'?"); } else { // TODO: Ideally this would display as an error in the console instead of a warning. Using fsResult.Fail() aborts the deserialization. deserializeResult += fsResult.Warn($"Type definition for '{formerTypeName}' unit is missing. Did you remove its script file?"); } } else { deserializeResult += fsResult.Warn($"Serialized '{TypeName_MissingType}' unit has an unrecognized format."); } } if (defaultType.IsAssignableFrom(markedType) == false) { // It's possible that the user removes their custom node's inheritance from the VS unit type. if (IsVisualScriptingUnit(data)) { // We store the type that the unit should be, we will try to re-instantiate it if it becomes valid again. dict[Key_UnitFormerType] = typeNameData; dict[Key_InstanceType] = new fsData(TypeName_MissingType); // TODO: Ideally this would display as an error in the console instead of a warning. Using fsResult.Fail() aborts the deserialization. deserializeResult += fsResult.Warn($"Type '{typeName}' is no longer assignable to '{defaultType.FullName}'. Did you remove inheritance from '{TypeName_Unit}'?\nConverted '{typeName}' unit to '{TypeName_MissingType}'."); return Type_MissingType; } deserializeResult.AddMessage("Ignoring type specifier; a field/property of type " + defaultType + " cannot hold an instance of " + markedType); return objectType; } objectType = markedType; return objectType; } /// /// Ensures that the data is a dictionary. If it is not, then it is /// wrapped inside of one. /// private static void EnsureDictionary(fsData data) { if (data.IsDictionary == false) { var existingData = data.Clone(); data.BecomeDictionary(); data.AsDictionary[Key_Content] = existingData; } } /// /// This manages instance writing so that we do not write unnecessary $id /// fields. We only need to write out an $id field when there is a /// corresponding $ref field. This is able to write $id references lazily /// because the fsData instance is not actually written out to text until /// we have entirely finished serializing it. /// internal class fsLazyCycleDefinitionWriter { private Dictionary _pendingDefinitions = new Dictionary(); private HashSet _references = new HashSet(); public void WriteDefinition(int id, fsData data) { if (_references.Contains(id)) { EnsureDictionary(data); data.AsDictionary[Key_ObjectDefinition] = new fsData(id.ToString()); } else { _pendingDefinitions[id] = data; } } public void WriteReference(int id, Dictionary dict) { // Write the actual definition if necessary if (_pendingDefinitions.ContainsKey(id)) { var data = _pendingDefinitions[id]; EnsureDictionary(data); data.AsDictionary[Key_ObjectDefinition] = new fsData(id.ToString()); _pendingDefinitions.Remove(id); } else { _references.Add(id); } // Write the reference dict[Key_ObjectReference] = new fsData(id.ToString()); } public void Clear() { _pendingDefinitions.Clear(); _references.Clear(); } } #region Keys private static HashSet _reservedKeywords; static fsSerializer() { _reservedKeywords = new HashSet { Key_ObjectReference, Key_ObjectDefinition, Key_InstanceType, Key_Version, Key_Content }; } /// /// Returns true if the given key is a special keyword that full /// serializer uses to add additional metadata on top of the emitted /// JSON. /// public static bool IsReservedKeyword(string key) { return _reservedKeywords.Contains(key); } /// /// This is an object reference in part of a cyclic graph. /// private static readonly string Key_ObjectReference = $"{fsGlobalConfig.InternalFieldPrefix}ref"; /// /// This is an object definition, as part of a cyclic graph. /// private static readonly string Key_ObjectDefinition = $"{fsGlobalConfig.InternalFieldPrefix}id"; /// /// This specifies the actual type of an object (the instance type was /// different from the field type). /// private static readonly string Key_InstanceType = $"{fsGlobalConfig.InternalFieldPrefix}type"; /// /// The version string for the serialized data. /// private static readonly string Key_Version = $"{fsGlobalConfig.InternalFieldPrefix}version"; /// /// If we have to add metadata but the original serialized state was not /// a dictionary, then this will contain the original data. /// private static readonly string Key_Content = $"{fsGlobalConfig.InternalFieldPrefix}content"; // THESE KEYS ARE SPECIFIC TO VISUAL SCRIPTING UNIT DESERIALIZATION // It's important to note here that VS-specific fields should not get the internal FullSerializer field prefix when serialized to avoid ambiguity. /// /// Dictionary for the default values of each of the unit's value port. /// internal static readonly string Key_UnitDefault = "defaultValues"; /// /// Specifies the position of a unit in its respective graph. /// internal static readonly string Key_UnitPosition = "position"; /// /// Unique guid of the unit instance. /// internal static readonly string Key_UnitGuid = "guid"; /// /// Meant for 'missing type' units to remember the type of node they are supposed to be. /// This will allow us to try and convert back if the correct type becomes available again. /// internal static readonly string Key_UnitFormerType = "formerType"; /// /// Meant for 'missing type' units to remember the state of the node they are supposed to be. /// This will allow to preserve the serialized instance values of the node when its type was lost. /// internal static readonly string Key_UnitFormerValue = "formerValue"; /// /// Hard-coded fully qualified name of the 'unit' type. /// internal static readonly string TypeName_Unit = "Unity.VisualScripting.Unit"; /// /// The 'Unit' type defined in VS. /// Made static as to only be deserialized once and potentially used multiple times. /// static readonly Type Type_Unit = RuntimeCodebase.DeserializeType(TypeName_Unit); /// /// Hard-coded fully qualified name of the 'missing type unit' type. /// internal static readonly string TypeName_MissingType = "Unity.VisualScripting.MissingType"; /// /// The 'MissingType' type defined in VS. /// Made static as to only be deserialized once and potentially used multiple times. /// static readonly Type Type_MissingType = RuntimeCodebase.DeserializeType(TypeName_MissingType); private static bool IsObjectReference(fsData data) { if (data.IsDictionary == false) { return false; } return data.AsDictionary.ContainsKey(Key_ObjectReference); } private static bool IsObjectDefinition(fsData data) { if (data.IsDictionary == false) { return false; } return data.AsDictionary.ContainsKey(Key_ObjectDefinition); } private static bool IsVersioned(fsData data) { if (data.IsDictionary == false) { return false; } return data.AsDictionary.ContainsKey(Key_Version); } private static bool IsTypeSpecified(fsData data) { if (data.IsDictionary == false) { return false; } return data.AsDictionary.ContainsKey(Key_InstanceType); } private static bool IsWrappedData(fsData data) { if (data.IsDictionary == false) { return false; } return data.AsDictionary.ContainsKey(Key_Content); } /// /// Makes a guess as to if the data represents a VS unit. /// /// If this turns out to be too general and objects that are not meant to be units get /// converted to MissingType units, consider adding a dedicated serialized field exclusive to units /// as to be able to check against it. (Something named like "isVisualScriptingUnit") /// /// /// private static bool IsVisualScriptingUnit(fsData data) { if (data.IsDictionary == false) { return false; } var dictionary = data.AsDictionary; // If the following are true, we can make a reasonable // assumption that the dictionary represents a VS unit. return dictionary.ContainsKey(Key_UnitDefault) && dictionary.ContainsKey(Key_UnitPosition) && dictionary.ContainsKey(Key_UnitGuid) && // Maybe don't hard-code these? dictionary[Key_UnitPosition].AsDictionary.ContainsKey("x") && dictionary[Key_UnitPosition].AsDictionary.ContainsKey("y"); } /// /// Strips all deserialization metadata from the object, like $type and /// $content fields. /// /// /// After making this call, you will *not* be able to deserialize the /// same object instance. The metadata is strictly necessary for /// deserialization! /// public static void StripDeserializationMetadata(ref fsData data) { if (data.IsDictionary && data.AsDictionary.ContainsKey(Key_Content)) { data = data.AsDictionary[Key_Content]; } if (data.IsDictionary) { var dict = data.AsDictionary; dict.Remove(Key_ObjectReference); dict.Remove(Key_ObjectDefinition); dict.Remove(Key_InstanceType); dict.Remove(Key_Version); } } /// /// This function converts legacy serialization data into the new format, /// so that the import process can be unified and ignore the old format. /// private static void ConvertLegacyData(ref fsData data) { if (data.IsDictionary == false) { return; } var dict = data.AsDictionary; // fast-exit: metadata never had more than two items if (dict.Count > 2) { return; } // Key strings used in the legacy system var referenceIdString = "ReferenceId"; var sourceIdString = "SourceId"; var sourceDataString = "Data"; var typeString = "Type"; var typeDataString = "Data"; // type specifier if (dict.Count == 2 && dict.ContainsKey(typeString) && dict.ContainsKey(typeDataString)) { data = dict[typeDataString]; EnsureDictionary(data); ConvertLegacyData(ref data); data.AsDictionary[Key_InstanceType] = dict[typeString]; } // object definition else if (dict.Count == 2 && dict.ContainsKey(sourceIdString) && dict.ContainsKey(sourceDataString)) { data = dict[sourceDataString]; EnsureDictionary(data); ConvertLegacyData(ref data); data.AsDictionary[Key_ObjectDefinition] = dict[sourceIdString]; } // object reference else if (dict.Count == 1 && dict.ContainsKey(referenceIdString)) { data = fsData.CreateDictionary(); data.AsDictionary[Key_ObjectReference] = dict[referenceIdString]; } } #endregion Keys #region Utility Methods private static void Invoke_OnBeforeSerialize(List processors, Type storageType, object instance) { for (var i = 0; i < processors.Count; ++i) { processors[i].OnBeforeSerialize(storageType, instance); } } private static void Invoke_OnAfterSerialize(List processors, Type storageType, object instance, ref fsData data) { // We run the after calls in reverse order; this significantly // reduces the interaction burden between multiple processors - it // makes each one much more independent and ignorant of the other // ones. for (var i = processors.Count - 1; i >= 0; --i) { processors[i].OnAfterSerialize(storageType, instance, ref data); } } private static void Invoke_OnBeforeDeserialize(List processors, Type storageType, ref fsData data) { for (var i = 0; i < processors.Count; ++i) { processors[i].OnBeforeDeserialize(storageType, ref data); } } private static void Invoke_OnBeforeDeserializeAfterInstanceCreation(List processors, Type storageType, object instance, ref fsData data) { for (var i = 0; i < processors.Count; ++i) { processors[i].OnBeforeDeserializeAfterInstanceCreation(storageType, instance, ref data); } } private static void Invoke_OnAfterDeserialize(List processors, Type storageType, object instance) { for (var i = processors.Count - 1; i >= 0; --i) { processors[i].OnAfterDeserialize(storageType, instance); } } #endregion Utility Methods } }