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<Type, fsBaseConverter>();
_cachedConverters = new Dictionary<Type, fsBaseConverter>();
_cachedProcessors = new Dictionary<Type, List<fsObjectProcessor>>();
_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<fsConverter>
{
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<Type, fsDirectConverter>();
_processors = new List<fsObjectProcessor>()
{
new fsSerializationCallbackProcessor()
};
#if !NO_UNITY
_processors.Add(new fsSerializationCallbackReceiverProcessor());
#endif
_abstractTypeRemap = new Dictionary<Type, Type>();
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));
}
}
/// <summary>
/// Converters that can be used for type registration.
/// </summary>
private readonly List<fsConverter> _availableConverters;
/// <summary>
/// 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.
/// </summary>
private readonly Dictionary<Type, fsDirectConverter> _availableDirectConverters;
/// <summary>
/// Processors that are available.
/// </summary>
private readonly List<fsObjectProcessor> _processors;
/// <summary>
/// Reference manager for cycle detection.
/// </summary>
private readonly fsCyclicReferenceManager _references;
private readonly fsLazyCycleDefinitionWriter _lazyReferenceWriter;
/// <summary>
/// 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.
/// </summary>
private readonly Dictionary<Type, Type> _abstractTypeRemap;
// of _cachedConverters, but there is a semantic difference because
// _cachedConverters goes
/// <summary>
/// Converter type to converter instance lookup table. This
/// could likely be stored inside
/// from serialized type to converter.
/// </summary>
private Dictionary<Type, fsBaseConverter> _cachedConverterTypeInstances;
/// <summary>
/// A cache from type to it's converter.
/// </summary>
private Dictionary<Type, fsBaseConverter> _cachedConverters;
/// <summary>
/// A cache from type to the set of processors that are interested in it.
/// </summary>
private Dictionary<Type, List<fsObjectProcessor>> _cachedProcessors;
/// <summary>
/// A context object that fsConverters can use to customize how they
/// operate.
/// </summary>
public fsContext Context;
/// <summary>
/// Configuration options. Also see fsGlobalConfig.
/// </summary>
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;
}
}
}
/// <summary>
/// Add a new processor to the serializer. Multiple processors can run at
/// the same time in the same order they were added in.
/// </summary>
/// <param name="processor">The processor to add.</param>
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<Type, List<fsObjectProcessor>>();
}
/// <summary>
/// Remove all processors which derive from TProcessor.
/// </summary>
public void RemoveProcessor<TProcessor>()
{
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<Type, List<fsObjectProcessor>>();
}
/// <summary>
/// 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}.
/// </summary>
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;
}
/// <summary>
/// Fetches all of the processors for the given type.
/// </summary>
private List<fsObjectProcessor> GetProcessors(Type type)
{
List<fsObjectProcessor> 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<fsObjectAttribute>(type);
if (attr != null && attr.Processor != null)
{
var processor = (fsObjectProcessor)Activator.CreateInstance(attr.Processor);
processors = new List<fsObjectProcessor>();
processors.Add(processor);
_cachedProcessors[type] = processors;
}
else if (_cachedProcessors.TryGetValue(type, out processors) == false)
{
processors = new List<fsObjectProcessor>();
for (var i = 0; i < _processors.Count; ++i)
{
var processor = _processors[i];
if (processor.CanProcess(type))
{
processors.Add(processor);
}
}
_cachedProcessors[type] = processors;
}
return processors;
}
/// <summary>
/// Adds a new converter that can be used to customize how an object is
/// serialized and deserialized.
/// </summary>
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<Type, fsBaseConverter>();
}
/// <summary>
/// Fetches a converter that can serialize/deserialize the given type.
/// </summary>
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<fsObjectAttribute>(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<fsForwardAttribute>(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);
}
/// <summary>
/// Helper method that simply forwards the call to
/// TrySerialize(typeof(T), instance, out data);
/// </summary>
public fsResult TrySerialize<T>(T instance, out fsData data)
{
return TrySerialize(typeof(T), instance, out data);
}
/// <summary>
/// Generic wrapper around TryDeserialize that simply forwards the call.
/// </summary>
public fsResult TryDeserialize<T>(fsData data, ref T instance)
{
object boxed = instance;
var fail = TryDeserialize(data, typeof(T), ref boxed);
if (fail.Succeeded)
{
instance = (T)boxed;
}
return fail;
}
/// <summary>
/// Serialize the given value.
/// </summary>
/// <param name="storageType">
/// 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.
/// </param>
/// <param name="instance">
/// The actual object instance to serialize.
/// </param>
/// <param name="data">The serialized state of the object.</param>
/// <returns>If serialization was successful.</returns>
public fsResult TrySerialize(Type storageType, object instance, out fsData data)
{
return TrySerialize(storageType, null, instance, out data);
}
/// <summary>
/// Serialize the given value.
/// </summary>
/// <param name="storageType">
/// 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.
/// </param>
/// <param name="overrideConverterType">
/// An fsBaseConverter derived type that will be used to serialize the
/// object instead of the converter found via the normal discovery
/// mechanisms.
/// </param>
/// <param name="instance">
/// The actual object instance to serialize.
/// </param>
/// <param name="data">The serialized state of the object.</param>
/// <returns>If serialization was successful.</returns>
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);
}
/// <summary>
/// Attempts to deserialize a value from a serialized state.
/// </summary>
public fsResult TryDeserialize(fsData data, Type storageType, ref object result)
{
return TryDeserialize(data, storageType, null, ref result);
}
/// <summary>
/// Attempts to deserialize a value from a serialized state.
/// </summary>
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<fsObjectProcessor> 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<fsObjectProcessor> 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<fsObjectProcessor> 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<fsVersionedType> 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<fsObjectProcessor> 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;
}
/// <summary>
/// Ensures that the data is a dictionary. If it is not, then it is
/// wrapped inside of one.
/// </summary>
private static void EnsureDictionary(fsData data)
{
if (data.IsDictionary == false)
{
var existingData = data.Clone();
data.BecomeDictionary();
data.AsDictionary[Key_Content] = existingData;
}
}
/// <summary>
/// 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.
/// </summary>
internal class fsLazyCycleDefinitionWriter
{
private Dictionary<int, fsData> _pendingDefinitions = new Dictionary<int, fsData>();
private HashSet<int> _references = new HashSet<int>();
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<string, fsData> 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<string> _reservedKeywords;
static fsSerializer()
{
_reservedKeywords = new HashSet<string>
{
Key_ObjectReference,
Key_ObjectDefinition,
Key_InstanceType,
Key_Version,
Key_Content
};
}
/// <summary>
/// Returns true if the given key is a special keyword that full
/// serializer uses to add additional metadata on top of the emitted
/// JSON.
/// </summary>
public static bool IsReservedKeyword(string key)
{
return _reservedKeywords.Contains(key);
}
/// <summary>
/// This is an object reference in part of a cyclic graph.
/// </summary>
private static readonly string Key_ObjectReference = $"{fsGlobalConfig.InternalFieldPrefix}ref";
/// <summary>
/// This is an object definition, as part of a cyclic graph.
/// </summary>
private static readonly string Key_ObjectDefinition = $"{fsGlobalConfig.InternalFieldPrefix}id";
/// <summary>
/// This specifies the actual type of an object (the instance type was
/// different from the field type).
/// </summary>
private static readonly string Key_InstanceType = $"{fsGlobalConfig.InternalFieldPrefix}type";
/// <summary>
/// The version string for the serialized data.
/// </summary>
private static readonly string Key_Version = $"{fsGlobalConfig.InternalFieldPrefix}version";
/// <summary>
/// If we have to add metadata but the original serialized state was not
/// a dictionary, then this will contain the original data.
/// </summary>
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.
/// <summary>
/// Dictionary for the default values of each of the unit's value port.
/// </summary>
internal static readonly string Key_UnitDefault = "defaultValues";
/// <summary>
/// Specifies the position of a unit in its respective graph.
/// </summary>
internal static readonly string Key_UnitPosition = "position";
/// <summary>
/// Unique guid of the unit instance.
/// </summary>
internal static readonly string Key_UnitGuid = "guid";
/// <summary>
/// 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.
/// </summary>
internal static readonly string Key_UnitFormerType = "formerType";
/// <summary>
/// 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.
/// </summary>
internal static readonly string Key_UnitFormerValue = "formerValue";
/// <summary>
/// Hard-coded fully qualified name of the 'unit' type.
/// </summary>
internal static readonly string TypeName_Unit = "Unity.VisualScripting.Unit";
/// <summary>
/// The 'Unit' type defined in VS.
/// Made static as to only be deserialized once and potentially used multiple times.
/// </summary>
static readonly Type Type_Unit = RuntimeCodebase.DeserializeType(TypeName_Unit);
/// <summary>
/// Hard-coded fully qualified name of the 'missing type unit' type.
/// </summary>
internal static readonly string TypeName_MissingType = "Unity.VisualScripting.MissingType";
/// <summary>
/// The 'MissingType' type defined in VS.
/// Made static as to only be deserialized once and potentially used multiple times.
/// </summary>
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);
}
/// <summary>
/// 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")
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
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");
}
/// <summary>
/// Strips all deserialization metadata from the object, like $type and
/// $content fields.
/// </summary>
/// <remarks>
/// After making this call, you will *not* be able to deserialize the
/// same object instance. The metadata is strictly necessary for
/// deserialization!
/// </remarks>
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);
}
}
/// <summary>
/// This function converts legacy serialization data into the new format,
/// so that the import process can be unified and ignore the old format.
/// </summary>
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<fsObjectProcessor> processors, Type storageType, object instance)
{
for (var i = 0; i < processors.Count; ++i)
{
processors[i].OnBeforeSerialize(storageType, instance);
}
}
private static void Invoke_OnAfterSerialize(List<fsObjectProcessor> 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<fsObjectProcessor> 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<fsObjectProcessor> 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<fsObjectProcessor> processors, Type storageType, object instance)
{
for (var i = processors.Count - 1; i >= 0; --i)
{
processors[i].OnAfterDeserialize(storageType, instance);
}
}
#endregion Utility Methods
}
}