536 lines
20 KiB
C#
536 lines
20 KiB
C#
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
|
|
|
using System;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using FlaxEngine.Json.JsonCustomSerializers;
|
|
using FlaxEngine.Utilities;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Converters;
|
|
using Newtonsoft.Json.Serialization;
|
|
|
|
namespace FlaxEngine.Json
|
|
{
|
|
partial class JsonSerializer
|
|
{
|
|
internal class SerializerCache
|
|
{
|
|
public Newtonsoft.Json.JsonSerializer JsonSerializer;
|
|
public StringBuilder StringBuilder;
|
|
public StringWriter StringWriter;
|
|
public JsonTextWriter JsonWriter;
|
|
public JsonSerializerInternalWriter SerializerWriter;
|
|
public UnmanagedMemoryStream MemoryStream;
|
|
public StreamReader Reader;
|
|
public bool IsWriting;
|
|
public bool IsReading;
|
|
|
|
public unsafe SerializerCache(JsonSerializerSettings settings)
|
|
{
|
|
JsonSerializer = Newtonsoft.Json.JsonSerializer.CreateDefault(settings);
|
|
JsonSerializer.Formatting = Formatting.Indented;
|
|
JsonSerializer.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
|
|
StringBuilder = new StringBuilder(256);
|
|
StringWriter = new StringWriter(StringBuilder, CultureInfo.InvariantCulture);
|
|
SerializerWriter = new JsonSerializerInternalWriter(JsonSerializer);
|
|
MemoryStream = new UnmanagedMemoryStream((byte*)0, 0);
|
|
Reader = new StreamReader(MemoryStream, Encoding.UTF8, false);
|
|
JsonWriter = new JsonTextWriter(StringWriter)
|
|
{
|
|
IndentChar = '\t',
|
|
Indentation = 1,
|
|
Formatting = JsonSerializer.Formatting,
|
|
DateFormatHandling = JsonSerializer.DateFormatHandling,
|
|
DateTimeZoneHandling = JsonSerializer.DateTimeZoneHandling,
|
|
FloatFormatHandling = JsonSerializer.FloatFormatHandling,
|
|
StringEscapeHandling = JsonSerializer.StringEscapeHandling,
|
|
Culture = JsonSerializer.Culture,
|
|
DateFormatString = JsonSerializer.DateFormatString,
|
|
};
|
|
}
|
|
|
|
public void ReadBegin()
|
|
{
|
|
if (IsReading)
|
|
{
|
|
// TODO: Reset reading state (eg if previous deserialization got exception)
|
|
}
|
|
IsWriting = false;
|
|
IsReading = true;
|
|
}
|
|
|
|
public void ReadEnd()
|
|
{
|
|
IsReading = false;
|
|
}
|
|
|
|
public void WriteBegin()
|
|
{
|
|
if (IsWriting)
|
|
{
|
|
// Reset writing state (eg if previous serialization got exception)
|
|
SerializerWriter = new JsonSerializerInternalWriter(JsonSerializer);
|
|
JsonWriter = new JsonTextWriter(StringWriter)
|
|
{
|
|
IndentChar = '\t',
|
|
Indentation = 1,
|
|
Formatting = JsonSerializer.Formatting,
|
|
DateFormatHandling = JsonSerializer.DateFormatHandling,
|
|
DateTimeZoneHandling = JsonSerializer.DateTimeZoneHandling,
|
|
FloatFormatHandling = JsonSerializer.FloatFormatHandling,
|
|
StringEscapeHandling = JsonSerializer.StringEscapeHandling,
|
|
Culture = JsonSerializer.Culture,
|
|
DateFormatString = JsonSerializer.DateFormatString,
|
|
};
|
|
}
|
|
StringBuilder.Clear();
|
|
IsWriting = true;
|
|
IsReading = false;
|
|
}
|
|
|
|
public void WriteEnd()
|
|
{
|
|
IsWriting = false;
|
|
}
|
|
}
|
|
|
|
internal static JsonSerializerSettings Settings = CreateDefaultSettings(false);
|
|
internal static JsonSerializerSettings SettingsManagedOnly = CreateDefaultSettings(true);
|
|
internal static FlaxObjectConverter ObjectConverter;
|
|
internal static ThreadLocal<SerializerCache> Current = new ThreadLocal<SerializerCache>();
|
|
internal static ThreadLocal<SerializerCache> Cache = new ThreadLocal<SerializerCache>(() => new SerializerCache(Settings));
|
|
internal static ThreadLocal<SerializerCache> CacheManagedOnly = new ThreadLocal<SerializerCache>(() => new SerializerCache(SettingsManagedOnly));
|
|
internal static ThreadLocal<IntPtr> CachedGuidBuffer = new ThreadLocal<IntPtr>(() => Marshal.AllocHGlobal(32 * sizeof(char)), true);
|
|
internal static string CachedGuidDigits = "0123456789abcdef";
|
|
|
|
internal static JsonSerializerSettings CreateDefaultSettings(bool isManagedOnly)
|
|
{
|
|
//Newtonsoft.Json.Utilities.MiscellaneousUtils.ValueEquals = ValueEquals;
|
|
|
|
var settings = new JsonSerializerSettings
|
|
{
|
|
ContractResolver = new ExtendedDefaultContractResolver(isManagedOnly),
|
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
|
TypeNameHandling = TypeNameHandling.Auto,
|
|
NullValueHandling = NullValueHandling.Include,
|
|
ObjectCreationHandling = ObjectCreationHandling.Auto,
|
|
};
|
|
if (ObjectConverter == null)
|
|
ObjectConverter = new FlaxObjectConverter();
|
|
settings.Converters.Add(ObjectConverter);
|
|
settings.Converters.Add(new SceneReferenceConverter());
|
|
settings.Converters.Add(new SoftObjectReferenceConverter());
|
|
settings.Converters.Add(new SoftTypeReferenceConverter());
|
|
settings.Converters.Add(new BehaviorKnowledgeSelectorAnyConverter());
|
|
settings.Converters.Add(new MarginConverter());
|
|
settings.Converters.Add(new VersionConverter());
|
|
settings.Converters.Add(new LocalizedStringConverter());
|
|
settings.Converters.Add(new TagConverter());
|
|
//settings.Converters.Add(new GuidConverter());
|
|
return settings;
|
|
}
|
|
|
|
internal static void Dispose()
|
|
{
|
|
CachedGuidBuffer.Values.ForEach(Marshal.FreeHGlobal);
|
|
Current.Dispose();
|
|
Cache.Dispose();
|
|
CacheManagedOnly.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The default implementation of the values comparision function used by the serialization system.
|
|
/// </summary>
|
|
/// <param name="objA">The object a.</param>
|
|
/// <param name="objB">The object b.</param>
|
|
/// <returns>True if both objects are equal, otherwise false.</returns>
|
|
public static bool ValueEquals(object objA, object objB)
|
|
{
|
|
// Use default value comparision used by C# json serialization library
|
|
return Newtonsoft.Json.Utilities.MiscellaneousUtils.ValueEquals(objA, objB);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serializes the specified object.
|
|
/// </summary>
|
|
/// <param name="obj">The object.</param>
|
|
/// <param name="isManagedOnly">True if serialize only C# members and skip C++ members (marked with <see cref="UnmanagedAttribute"/>).</param>
|
|
/// <returns>The output json string.</returns>
|
|
public static string Serialize(object obj, bool isManagedOnly = false)
|
|
{
|
|
Type type = obj.GetType();
|
|
var cache = isManagedOnly ? CacheManagedOnly.Value : Cache.Value;
|
|
Current.Value = cache;
|
|
|
|
cache.WriteBegin();
|
|
cache.SerializerWriter.Serialize(cache.JsonWriter, obj, type);
|
|
cache.WriteEnd();
|
|
|
|
return cache.StringBuilder.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serializes the specified object.
|
|
/// </summary>
|
|
/// <param name="obj">The object.</param>
|
|
/// <param name="type">The object type. Can be typeof(object) to handle generic object serialization.</param>
|
|
/// <param name="isManagedOnly">True if serialize only C# members and skip C++ members (marked with <see cref="UnmanagedAttribute"/>).</param>
|
|
/// <returns>The output json string.</returns>
|
|
public static string Serialize(object obj, Type type, bool isManagedOnly = false)
|
|
{
|
|
var cache = isManagedOnly ? CacheManagedOnly.Value : Cache.Value;
|
|
Current.Value = cache;
|
|
|
|
cache.WriteBegin();
|
|
cache.SerializerWriter.Serialize(cache.JsonWriter, obj, type);
|
|
cache.WriteEnd();
|
|
|
|
return cache.StringBuilder.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serializes the specified object difference to the other object of the same type. Used to serialize modified properties of the object during prefab instance serialization.
|
|
/// </summary>
|
|
/// <param name="obj">The object.</param>
|
|
/// <param name="other">The reference object.</param>
|
|
/// <param name="isManagedOnly">True if serialize only C# members and skip C++ members (marked with <see cref="UnmanagedAttribute"/>).</param>
|
|
/// <returns>The output json string.</returns>
|
|
public static string SerializeDiff(object obj, object other, bool isManagedOnly = false)
|
|
{
|
|
Type type = obj.GetType();
|
|
var cache = isManagedOnly ? CacheManagedOnly.Value : Cache.Value;
|
|
Current.Value = cache;
|
|
|
|
cache.WriteBegin();
|
|
cache.SerializerWriter.SerializeDiff(cache.JsonWriter, obj, type, other);
|
|
cache.WriteEnd();
|
|
|
|
return cache.StringBuilder.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes the specified object (from the input json data).
|
|
/// </summary>
|
|
/// <param name="input">The object.</param>
|
|
/// <param name="json">The input json data.</param>
|
|
public static void Deserialize(object input, string json)
|
|
{
|
|
var cache = Cache.Value;
|
|
cache.ReadBegin();
|
|
using (JsonReader reader = new JsonTextReader(new StringReader(json)))
|
|
{
|
|
cache.JsonSerializer.Populate(reader, input);
|
|
if (cache.JsonSerializer.CheckAdditionalContent)
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
if (reader.TokenType != JsonToken.Comment)
|
|
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
|
|
}
|
|
}
|
|
}
|
|
cache.ReadEnd();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes the specified .NET object type (from the input json data).
|
|
/// </summary>
|
|
/// <param name="json">The input json data.</param>
|
|
/// <typeparam name="T">The type of the object to deserialize to.</typeparam>
|
|
/// <returns>The deserialized object from the JSON string.</returns>
|
|
public static T Deserialize<T>(string json)
|
|
{
|
|
return (T)Deserialize(json, typeof(T));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes the specified .NET object type (from the input json data).
|
|
/// </summary>
|
|
/// <param name="json">The input json data.</param>
|
|
/// <param name="objectType">The <see cref="T:System.Type" /> of object being deserialized.</param>
|
|
/// <returns>The deserialized object from the JSON string.</returns>
|
|
public static object Deserialize(string json, Type objectType)
|
|
{
|
|
object result;
|
|
var cache = Cache.Value;
|
|
cache.ReadBegin();
|
|
using (JsonReader reader = new JsonTextReader(new StringReader(json)))
|
|
{
|
|
result = cache.JsonSerializer.Deserialize(reader, objectType);
|
|
if (cache.JsonSerializer.CheckAdditionalContent)
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
if (reader.TokenType != JsonToken.Comment)
|
|
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
|
|
}
|
|
}
|
|
}
|
|
cache.ReadEnd();
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes the .NET object (from the input json data).
|
|
/// </summary>
|
|
/// <param name="json">The input json data.</param>
|
|
/// <returns>The deserialized object from the JSON string.</returns>
|
|
public static object Deserialize(string json)
|
|
{
|
|
object result;
|
|
var cache = Cache.Value;
|
|
cache.ReadBegin();
|
|
using (JsonReader reader = new JsonTextReader(new StringReader(json)))
|
|
{
|
|
result = cache.JsonSerializer.Deserialize(reader);
|
|
|
|
if (cache.JsonSerializer.CheckAdditionalContent)
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
if (reader.TokenType != JsonToken.Comment)
|
|
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
|
|
}
|
|
}
|
|
}
|
|
cache.ReadEnd();
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes the specified object (from the input json data).
|
|
/// </summary>
|
|
/// <param name="input">The object.</param>
|
|
/// <param name="jsonBuffer">The input json data buffer (raw, fixed memory buffer).</param>
|
|
/// <param name="jsonLength">The input json data buffer length (characters count).</param>
|
|
public static unsafe void Deserialize(object input, byte* jsonBuffer, int jsonLength)
|
|
{
|
|
var cache = Cache.Value;
|
|
cache.ReadBegin();
|
|
|
|
/*// Debug json string reading
|
|
cache.MemoryStream.Initialize(jsonBuffer, jsonLength);
|
|
cache.Reader.DiscardBufferedData();
|
|
string json = cache.Reader.ReadToEnd();*/
|
|
|
|
cache.MemoryStream.Initialize(jsonBuffer, jsonLength);
|
|
cache.Reader.DiscardBufferedData();
|
|
var jsonReader = new JsonTextReader(cache.Reader);
|
|
if (*jsonBuffer != (byte)'{' && input is LocalizedString asLocalizedString)
|
|
{
|
|
// Hack for objects that are serialized into sth different thant "{..}" (eg. LocalizedString can be saved as plain string if not using localization)
|
|
asLocalizedString.Id = null;
|
|
asLocalizedString.Value = jsonReader.ReadAsString();
|
|
}
|
|
else
|
|
{
|
|
cache.JsonSerializer.Populate(jsonReader, input);
|
|
}
|
|
if (cache.JsonSerializer.CheckAdditionalContent)
|
|
{
|
|
while (jsonReader.Read())
|
|
{
|
|
if (jsonReader.TokenType != JsonToken.Comment)
|
|
throw new Exception("Additional text found in JSON string after finishing deserializing object.");
|
|
}
|
|
}
|
|
|
|
cache.ReadEnd();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Guid type in Flax format (the same as C++ layer).
|
|
/// </summary>
|
|
internal struct GuidInterop
|
|
{
|
|
public uint A;
|
|
public uint B;
|
|
public uint C;
|
|
public uint D;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the string representation of the given object ID. It matches the internal serialization formatting rules.
|
|
/// </summary>
|
|
/// <param name="id">The object identifier.</param>
|
|
/// <returns>The serialized ID.</returns>
|
|
public static unsafe string GetStringID(Guid id)
|
|
{
|
|
return GetStringID(&id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the string representation of the given object ID. It matches the internal serialization formatting rules.
|
|
/// </summary>
|
|
/// <param name="id">The object identifier.</param>
|
|
/// <returns>The serialized ID.</returns>
|
|
public static unsafe string GetStringID(Guid* id)
|
|
{
|
|
GuidInterop* g = (GuidInterop*)id;
|
|
|
|
// Unoptimized version:
|
|
//return string.Format("{0:x8}{1:x8}{2:x8}{3:x8}", g->A, g->B, g->C, g->D);
|
|
|
|
// Optimized version:
|
|
var buffer = (char*)CachedGuidBuffer.Value.ToPointer();
|
|
for (int i = 0; i < 32; i++)
|
|
buffer[i] = '0';
|
|
var digits = CachedGuidDigits;
|
|
uint n = g->A;
|
|
char* p = buffer + 7;
|
|
do
|
|
{
|
|
*p-- = digits[(int)(n & 0xf)];
|
|
} while ((n >>= 4) != 0);
|
|
n = g->B;
|
|
p = buffer + 15;
|
|
do
|
|
{
|
|
*p-- = digits[(int)(n & 0xf)];
|
|
} while ((n >>= 4) != 0);
|
|
n = g->C;
|
|
p = buffer + 23;
|
|
do
|
|
{
|
|
*p-- = digits[(int)(n & 0xf)];
|
|
} while ((n >>= 4) != 0);
|
|
n = g->D;
|
|
p = buffer + 31;
|
|
do
|
|
{
|
|
*p-- = digits[(int)(n & 0xf)];
|
|
} while ((n >>= 4) != 0);
|
|
return new string(buffer, 0, 32);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the string representation of the given object. It matches the internal serialization formatting rules.
|
|
/// </summary>
|
|
/// <param name="obj">The object.</param>
|
|
/// <returns>The serialized ID.</returns>
|
|
public static unsafe string GetStringID(Object obj)
|
|
{
|
|
Guid id = Guid.Empty;
|
|
if (obj != null)
|
|
id = obj.ID;
|
|
return GetStringID(&id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the given object identifier represented in the internal serialization format.
|
|
/// </summary>
|
|
/// <param name="str">The ID string.</param>
|
|
/// <returns>The identifier.</returns>
|
|
public static Guid ParseID(string str)
|
|
{
|
|
ParseID(str, out var id);
|
|
return id;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the given object identifier represented in the internal serialization format.
|
|
/// </summary>
|
|
/// <param name="str">The ID string.</param>
|
|
/// <param name="id">The identifier.</param>
|
|
public static unsafe void ParseID(string str, out Guid id)
|
|
{
|
|
GuidInterop g;
|
|
|
|
// Broken after VS 15.5
|
|
/*fixed (char* a = str)
|
|
{
|
|
char* b = a + 8;
|
|
char* c = b + 8;
|
|
char* d = c + 8;
|
|
|
|
ParseHex(a, 8, out g.A);
|
|
ParseHex(b, 8, out g.B);
|
|
ParseHex(c, 8, out g.C);
|
|
ParseHex(d, 8, out g.D);
|
|
}*/
|
|
|
|
// Temporary fix (not using raw char* pointer)
|
|
ParseHex(str, 0, 8, out g.A);
|
|
ParseHex(str, 8, 8, out g.B);
|
|
ParseHex(str, 16, 8, out g.C);
|
|
ParseHex(str, 24, 8, out g.D);
|
|
|
|
id = *(Guid*)&g;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal static unsafe void ParseHex(char* str, int length, out uint result)
|
|
{
|
|
uint sum = 0;
|
|
char* p = str;
|
|
char* end = str + length;
|
|
|
|
if (*p == '0' && *(p + 1) == 'x')
|
|
p += 2;
|
|
|
|
while (p < end && *p != 0)
|
|
{
|
|
int c = *p - '0';
|
|
|
|
if (c < 0 || c > 9)
|
|
{
|
|
c = char.ToLower(*p) - 'a' + 10;
|
|
if (c < 10 || c > 15)
|
|
{
|
|
result = 0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
sum = 16 * sum + (uint)c;
|
|
|
|
p++;
|
|
}
|
|
|
|
result = sum;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal static void ParseHex(string str, int start, int length, out uint result)
|
|
{
|
|
uint sum = 0;
|
|
int p = start;
|
|
int end = start + length;
|
|
|
|
if (str.Length < end)
|
|
{
|
|
result = 0;
|
|
return;
|
|
}
|
|
|
|
if (str[p] == '0' && str[p + 1] == 'x')
|
|
p += 2;
|
|
|
|
while (p < end && str[p] != 0)
|
|
{
|
|
int c = str[p] - '0';
|
|
|
|
if (c < 0 || c > 9)
|
|
{
|
|
c = char.ToLower(str[p]) - 'a' + 10;
|
|
if (c < 10 || c > 15)
|
|
{
|
|
result = 0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
sum = 16 * sum + (uint)c;
|
|
|
|
p++;
|
|
}
|
|
|
|
result = sum;
|
|
}
|
|
}
|
|
}
|