mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-15 23:02:34 +02:00
1115 lines
39 KiB
C#
1115 lines
39 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using Robust.Shared.Interfaces.Reflection;
|
|
using Robust.Shared.Interfaces.Serialization;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Utility;
|
|
using YamlDotNet.Core;
|
|
using YamlDotNet.RepresentationModel;
|
|
|
|
namespace Robust.Shared.Serialization
|
|
{
|
|
/// <summary>
|
|
/// Object serializer that serializes to/from YAML.
|
|
/// </summary>
|
|
public class YamlObjectSerializer : ObjectSerializer
|
|
{
|
|
private static readonly Dictionary<Type, TypeSerializer> _typeSerializers;
|
|
public static IReadOnlyDictionary<Type, TypeSerializer> TypeSerializers => _typeSerializers;
|
|
private static readonly StructSerializer _structSerializer;
|
|
|
|
private YamlMappingNode WriteMap;
|
|
private List<YamlMappingNode> ReadMaps;
|
|
private Context _context;
|
|
|
|
static YamlObjectSerializer()
|
|
{
|
|
_structSerializer = new StructSerializer();
|
|
_typeSerializers = new Dictionary<Type, TypeSerializer>
|
|
{
|
|
{ typeof(Color), new ColorSerializer() },
|
|
{ typeof(Vector2), new Vector2Serializer() },
|
|
{ typeof(Vector3), new Vector3Serializer() },
|
|
{ typeof(Vector4), new Vector4Serializer() },
|
|
{ typeof(Angle), new AngleSerializer() },
|
|
{ typeof(UIBox2), new UIBox2Serializer() },
|
|
{ typeof(Box2), new Box2Serializer() },
|
|
{ typeof(ResourcePath), new ResourcePathSerializer() },
|
|
{ typeof(GridId), new GridIdSerializer() },
|
|
{ typeof(MapId), new MapIdSerializer() },
|
|
{ typeof(SpriteSpecifier), new SpriteSpecifierSerializer() },
|
|
};
|
|
}
|
|
|
|
// Use NewReader or NewWriter instead.
|
|
private YamlObjectSerializer()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new serializer to be used for reading from YAML data.
|
|
/// </summary>
|
|
/// <param name="readMap">
|
|
/// The YAML mapping to read data from.
|
|
/// </param>
|
|
/// <param name="context">
|
|
/// An optional context that can provide additional capabitilies such as caching and custom type serializers.
|
|
/// </param>
|
|
public static YamlObjectSerializer NewReader(YamlMappingNode readMap, Context context = null)
|
|
{
|
|
return NewReader(new List<YamlMappingNode>(1) { readMap }, context);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new serializer to be used for reading from YAML data.
|
|
/// </summary>
|
|
/// <param name="readMaps">
|
|
/// A list of maps to read from. The first list will be used first,
|
|
/// then the second if the first does not contain a specific key, and so on.
|
|
/// </param>
|
|
/// <param name="context">
|
|
/// An optional context that can provide additional capabilities such as caching and custom type serializers.
|
|
/// </param>
|
|
public static YamlObjectSerializer NewReader(List<YamlMappingNode> readMaps, Context context = null)
|
|
{
|
|
return new YamlObjectSerializer
|
|
{
|
|
ReadMaps = readMaps,
|
|
_context = context,
|
|
Reading = true,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new serializer to be used from writing into YAML data.
|
|
/// </summary>
|
|
/// <param name="writeMap">
|
|
/// The mapping to write into.
|
|
/// Gets modified directly in place.
|
|
/// </param>
|
|
/// <param name="context">
|
|
/// An optional context that can provide additional capabitilies such as caching and custom type serializers.
|
|
/// </param>
|
|
public static YamlObjectSerializer NewWriter(YamlMappingNode writeMap, Context context = null)
|
|
{
|
|
return new YamlObjectSerializer
|
|
{
|
|
WriteMap = writeMap,
|
|
_context = context,
|
|
Reading = false,
|
|
};
|
|
}
|
|
|
|
// TODO: Theoretical optimization.
|
|
// Might be a good idea to make DataField<T> use caching for value types without references too.
|
|
/// <inheritdoc />
|
|
public override void DataField<T>(ref T value, string name, T defaultValue, WithFormat<T> format, bool alwaysWrite = false)
|
|
{
|
|
if (Reading) // read
|
|
{
|
|
foreach (var map in ReadMaps)
|
|
{
|
|
if (map.TryGetNode(name, out var node))
|
|
{
|
|
var customFormatter = format.GetYamlSerializer();
|
|
value = (T)customFormatter.NodeToType(typeof(T), node, this);
|
|
return;
|
|
}
|
|
}
|
|
value = defaultValue;
|
|
return;
|
|
}
|
|
else // write
|
|
{
|
|
// don't write if value is null or default
|
|
if (!alwaysWrite && IsValueDefault(name, value, defaultValue, format))
|
|
return;
|
|
|
|
var customFormatter = format.GetYamlSerializer();
|
|
var key = name;
|
|
var val = value == null ? customFormatter.TypeToNode(defaultValue, this) : customFormatter.TypeToNode(value, this);
|
|
|
|
// write the concrete type tag
|
|
if (typeof(T).IsAbstract || typeof(T).IsInterface)
|
|
{
|
|
var concreteType = value == null ? defaultValue.GetType() : value.GetType();
|
|
val.Tag = $"!type:{concreteType.Name}";
|
|
}
|
|
|
|
WriteMap.Add(key, val);
|
|
}
|
|
}
|
|
|
|
|
|
public override void DataField<TRoot, T>(TRoot o, Expression<Func<TRoot,T>> expr, string name, T defaultValue, bool alwaysWrite = false)
|
|
{
|
|
if (o == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(o));
|
|
}
|
|
|
|
if (expr == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(expr));
|
|
}
|
|
|
|
if (!(expr.Body is MemberExpression mExpr))
|
|
{
|
|
throw new NotSupportedException("Cannot handle expressions of types other than MemberExpression.");
|
|
}
|
|
|
|
|
|
WriteFunctionDelegate<T> getter;
|
|
ReadFunctionDelegate<T> setter;
|
|
switch (mExpr.Member)
|
|
{
|
|
case FieldInfo fi:
|
|
getter = () => (T) fi.GetValue(o);
|
|
setter = v => fi.SetValue(o, v);
|
|
break;
|
|
case PropertyInfo pi:
|
|
getter = () => (T) pi.GetValue(o);
|
|
setter = v => pi.SetValue(o, v);
|
|
break;
|
|
default:
|
|
throw new NotSupportedException("Cannot handle member expressions of types other than FieldInfo or PropertyInfo.");
|
|
}
|
|
|
|
DataReadWriteFunction(name, defaultValue, setter, getter, alwaysWrite);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void DataFieldCached<T>(ref T value, string name, T defaultValue, WithFormat<T> format, bool alwaysWrite = false)
|
|
{
|
|
if (Reading) // read
|
|
{
|
|
if (_context != null && _context.TryGetCachedField(name, out T theValue))
|
|
{
|
|
// Itermediate field so value doesn't get reset to default(T) if this fails.
|
|
value = theValue;
|
|
return;
|
|
}
|
|
foreach (var map in ReadMaps)
|
|
{
|
|
if (map.TryGetNode(name, out var node))
|
|
{
|
|
var customFormatter = format.GetYamlSerializer();
|
|
value = (T)customFormatter.NodeToType(typeof(T), node, this);
|
|
_context?.SetCachedField(name, value);
|
|
return;
|
|
}
|
|
}
|
|
value = defaultValue;
|
|
_context?.SetCachedField(name, value);
|
|
return;
|
|
}
|
|
else // write
|
|
{
|
|
DataField(ref value, name, defaultValue, format, alwaysWrite);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void DataField<TTarget, TSource>(
|
|
ref TTarget value,
|
|
string name,
|
|
TTarget defaultValue,
|
|
Func<TSource, TTarget> ReadConvertFunc,
|
|
Func<TTarget, TSource> WriteConvertFunc = null,
|
|
bool alwaysWrite = false)
|
|
{
|
|
if (Reading)
|
|
{
|
|
foreach (var map in ReadMaps)
|
|
{
|
|
if (map.TryGetNode(name, out var node))
|
|
{
|
|
value = ReadConvertFunc((TSource)NodeToType(typeof(TSource), node));
|
|
return;
|
|
}
|
|
}
|
|
value = defaultValue;
|
|
}
|
|
else
|
|
{
|
|
if (WriteConvertFunc == null)
|
|
{
|
|
// TODO: More verbosity diagnostics.
|
|
Logger.WarningS(LogCategory, "Field '{0}' not written due to lack of WriteConvertFunc.", name);
|
|
return;
|
|
}
|
|
|
|
// don't write if value is null or default
|
|
if (!alwaysWrite && IsValueDefault(name, value, defaultValue, WithFormat<TTarget>.NoFormat))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var key = name;
|
|
var val = value == null ? TypeToNode(WriteConvertFunc(defaultValue)) : TypeToNode(WriteConvertFunc(value));
|
|
|
|
// write the concrete type tag
|
|
if (typeof(TTarget).IsAbstract || typeof(TTarget).IsInterface)
|
|
{
|
|
var concreteType = value == null ? defaultValue.GetType() : value.GetType();
|
|
val.Tag = $"!type:{concreteType.Name}";
|
|
}
|
|
|
|
WriteMap.Add(key, val);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void DataFieldCached<TTarget, TSource>(
|
|
ref TTarget value,
|
|
string name,
|
|
TTarget defaultValue,
|
|
Func<TSource, TTarget> ReadConvertFunc,
|
|
Func<TTarget, TSource> WriteConvertFunc = null,
|
|
bool alwaysWrite = false)
|
|
{
|
|
if (Reading)
|
|
{
|
|
if (_context != null && _context.TryGetCachedField(name, out TTarget theValue))
|
|
{
|
|
// Itermediate field so value doesn't get reset to default(T) if this fails.
|
|
value = theValue;
|
|
return;
|
|
}
|
|
foreach (var map in ReadMaps)
|
|
{
|
|
if (map.TryGetNode(name, out var node))
|
|
{
|
|
value = ReadConvertFunc((TSource)NodeToType(typeof(TSource), node));
|
|
_context?.SetCachedField(name, value);
|
|
return;
|
|
}
|
|
}
|
|
value = defaultValue;
|
|
_context?.SetCachedField(name, value);
|
|
}
|
|
else
|
|
{
|
|
DataField(ref value, name, defaultValue, ReadConvertFunc, WriteConvertFunc, alwaysWrite);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override T ReadDataField<T>(string name, T defaultValue)
|
|
{
|
|
if (!Reading)
|
|
{
|
|
throw new InvalidOperationException("Cannot use ReadDataField while not reading.");
|
|
}
|
|
|
|
foreach (var map in ReadMaps)
|
|
{
|
|
if (map.TryGetNode(name, out var node))
|
|
{
|
|
return (T)NodeToType(typeof(T), node);
|
|
|
|
}
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override T ReadDataFieldCached<T>(string name, T defaultValue)
|
|
{
|
|
if (!Reading)
|
|
{
|
|
throw new InvalidOperationException("Cannot use ReadDataField while not reading.");
|
|
}
|
|
|
|
if (_context != null && _context.TryGetCachedField(name, out T val))
|
|
{
|
|
return val;
|
|
}
|
|
|
|
foreach (var map in ReadMaps)
|
|
{
|
|
if (map.TryGetNode(name, out var node))
|
|
{
|
|
val = (T)NodeToType(typeof(T), node);
|
|
_context?.SetCachedField(name, val);
|
|
return val;
|
|
}
|
|
}
|
|
_context?.SetCachedField(name, defaultValue);
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool TryReadDataField<T>(string name, WithFormat<T> format, out T value)
|
|
{
|
|
if (!Reading)
|
|
{
|
|
throw new InvalidOperationException("Cannot use ReadDataField while not reading.");
|
|
}
|
|
|
|
foreach (var map in ReadMaps)
|
|
{
|
|
if (map.TryGetNode(name, out var node))
|
|
{
|
|
var customFormatter = format.GetYamlSerializer();
|
|
value = (T)customFormatter.NodeToType(typeof(T), node, this);
|
|
return true;
|
|
}
|
|
}
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
public override bool TryReadDataFieldCached<T>(string name, WithFormat<T> format, out T value)
|
|
{
|
|
if (!Reading)
|
|
{
|
|
throw new InvalidOperationException("Cannot use ReadDataField while not reading.");
|
|
}
|
|
|
|
if (_context != null && _context.TryGetCachedField(name, out value))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
foreach (var map in ReadMaps)
|
|
{
|
|
if (map.TryGetNode(name, out var node))
|
|
{
|
|
var customFormatter = format.GetYamlSerializer();
|
|
value = (T)customFormatter.NodeToType(typeof(T), node, this);
|
|
_context?.SetCachedField(name, value);
|
|
return true;
|
|
}
|
|
}
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
|
|
/// <inheritdoc />
|
|
public override void DataReadFunction<T>(string name, T defaultValue, ReadFunctionDelegate<T> func)
|
|
{
|
|
if (!Reading) return;
|
|
|
|
foreach (var map in ReadMaps)
|
|
{
|
|
if (map.TryGetNode(name, out var node))
|
|
{
|
|
func((T)NodeToType(typeof(T), node));
|
|
return;
|
|
}
|
|
}
|
|
|
|
func(defaultValue);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void DataWriteFunction<T>(string name, T defaultValue, WriteFunctionDelegate<T> func, bool alwaysWrite = false)
|
|
{
|
|
if (Reading) return;
|
|
|
|
var value = func.Invoke();
|
|
|
|
// don't write if value is null or default
|
|
if (!alwaysWrite && IsValueDefault(name, value, defaultValue, WithFormat<T>.NoFormat))
|
|
return;
|
|
|
|
var key = name;
|
|
var val = value == null ? TypeToNode(defaultValue) : TypeToNode(value);
|
|
|
|
// write the concrete type tag
|
|
if (typeof(T).IsAbstract || typeof(T).IsInterface)
|
|
{
|
|
var concreteType = value == null ? defaultValue.GetType() : value.GetType();
|
|
val.Tag = $"!type:{concreteType.Name}";
|
|
}
|
|
|
|
WriteMap.Add(key, val);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void SetCacheData(string key, object value)
|
|
{
|
|
_context?.SetDataCache(key, value);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override T GetCacheData<T>(string key)
|
|
{
|
|
if (_context != null && _context.TryGetDataCache(key, out var value))
|
|
{
|
|
return (T)value;
|
|
}
|
|
throw new KeyNotFoundException();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool TryGetCacheData<T>(string key, out T data)
|
|
{
|
|
if (_context != null && _context.TryGetDataCache(key, out var value))
|
|
{
|
|
data = (T)value;
|
|
return true;
|
|
}
|
|
|
|
data = default;
|
|
return false;
|
|
}
|
|
|
|
public object NodeToType(Type type, YamlNode node)
|
|
{
|
|
// special snowflake string
|
|
if (type == typeof(String))
|
|
return node.ToString();
|
|
|
|
// val primitives
|
|
if (type.IsPrimitive || type == typeof(decimal))
|
|
{
|
|
return StringToType(type, node.ToString());
|
|
}
|
|
|
|
// val enum
|
|
if (type.IsEnum)
|
|
return Enum.Parse(type, node.ToString());
|
|
|
|
// List<T>
|
|
if (TryGenericListType(type, out var listType))
|
|
{
|
|
var listNode = (YamlSequenceNode)node;
|
|
var newList = (IList)Activator.CreateInstance(type);
|
|
|
|
foreach (var entryNode in listNode)
|
|
{
|
|
var value = NodeToType(listType, entryNode);
|
|
newList.Add(value);
|
|
}
|
|
|
|
return newList;
|
|
}
|
|
|
|
// Dictionary<K,V>
|
|
if (TryGenericDictType(type, out var keyType, out var valType))
|
|
{
|
|
var dictNode = (YamlMappingNode)node;
|
|
var newDict = (IDictionary)Activator.CreateInstance(type);
|
|
|
|
foreach (var kvEntry in dictNode.Children)
|
|
{
|
|
var keyValue = NodeToType(keyType, kvEntry.Key);
|
|
var valValue = NodeToType(valType, kvEntry.Value);
|
|
|
|
newDict.Add(keyValue, valValue);
|
|
}
|
|
|
|
return newDict;
|
|
}
|
|
|
|
// Hand it to the context.
|
|
if (_context != null && _context.TryNodeToType(node, type, out var contextObj))
|
|
{
|
|
return contextObj;
|
|
}
|
|
|
|
// custom TypeSerializer
|
|
if (_typeSerializers.TryGetValue(type, out var serializer))
|
|
return serializer.NodeToType(type, node, this);
|
|
|
|
// IExposeData.
|
|
if (typeof(IExposeData).IsAssignableFrom(type))
|
|
{
|
|
if (!(node is YamlMappingNode mapNode))
|
|
{
|
|
throw new InvalidOperationException("Cannot read from IExposeData on non-mapping node.");
|
|
}
|
|
|
|
var concreteType = type;
|
|
if (type.IsAbstract || type.IsInterface)
|
|
{
|
|
var tag = node.Tag;
|
|
if (string.IsNullOrWhiteSpace(tag))
|
|
throw new YamlException($"Type '{type}' is abstract, but there is no yaml tag for the concrete type.");
|
|
|
|
var args = tag.Split(':');
|
|
if (args.Length == 2 && args[0] == "!type")
|
|
{
|
|
concreteType = ResolveConcreteType(type, args[1]);
|
|
}
|
|
else
|
|
{
|
|
throw new YamlException($"Malformed type tag.");
|
|
}
|
|
}
|
|
|
|
var instance = (IExposeData)Activator.CreateInstance(concreteType);
|
|
// TODO: Might be worth it to cut down on allocations here by using ourselves instead of creating a fork.
|
|
// Seems doable.
|
|
if (_context != null)
|
|
{
|
|
_context.StackDepth++;
|
|
}
|
|
var fork = NewReader(mapNode, _context);
|
|
if (_context != null)
|
|
{
|
|
_context.StackDepth--;
|
|
}
|
|
instance.ExposeData(fork);
|
|
return instance;
|
|
}
|
|
|
|
// ISelfSerialize
|
|
if (typeof(ISelfSerialize).IsAssignableFrom(type))
|
|
{
|
|
var instance = (ISelfSerialize)Activator.CreateInstance(type);
|
|
instance.Deserialize(node.ToString());
|
|
return instance;
|
|
}
|
|
|
|
// other val (struct)
|
|
if (type.IsValueType)
|
|
return _structSerializer.NodeToType(type, (YamlMappingNode)node, this);
|
|
|
|
// ref type that isn't a custom TypeSerializer
|
|
throw new ArgumentException($"Type {type.FullName} is not supported.", nameof(type));
|
|
}
|
|
|
|
private static Type ResolveConcreteType(Type baseType, string typeName)
|
|
{
|
|
var reflection = IoCManager.Resolve<IReflectionManager>();
|
|
foreach (var derivedType in reflection.GetAllChildren(baseType))
|
|
{
|
|
if (derivedType.Name == typeName)
|
|
{
|
|
return derivedType;
|
|
}
|
|
}
|
|
|
|
throw new YamlException($"Type '{baseType}' is abstract, but could not find concrete type '{typeName}'.");
|
|
}
|
|
|
|
public YamlNode TypeToNode(object obj)
|
|
{
|
|
// special snowflake string
|
|
if (obj is string s)
|
|
return s;
|
|
|
|
var type = obj.GetType();
|
|
|
|
// val primitives and val enums
|
|
if (type.IsPrimitive || type.IsEnum || type == typeof(decimal))
|
|
{
|
|
// All primitives and enums implement IConvertible.
|
|
// Need it for the culture overload.
|
|
var convertible = (IConvertible) obj;
|
|
return convertible.ToString(CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
// List<T>
|
|
if (TryGenericListType(type, out var listType))
|
|
{
|
|
var node = new YamlSequenceNode();
|
|
|
|
foreach (var entry in (IEnumerable)obj)
|
|
{
|
|
var entryNode = TypeToNode(entry);
|
|
|
|
// write the concrete type tag
|
|
if (listType.IsAbstract || listType.IsInterface)
|
|
{
|
|
var concreteType = entry.GetType();
|
|
entryNode.Tag = $"!type:{concreteType.Name}";
|
|
}
|
|
|
|
node.Add(entryNode);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
// Dictionary<K,V>
|
|
if (TryGenericDictType(type, out var keyType, out var valType))
|
|
{
|
|
var node = new YamlMappingNode();
|
|
|
|
foreach (DictionaryEntry entry in (IDictionary)obj)
|
|
{
|
|
var keyNode = TypeToNode(entry.Key);
|
|
var valNode = TypeToNode(entry.Value);
|
|
|
|
// write the concrete type tag
|
|
if (valType.IsAbstract || valType.IsInterface)
|
|
{
|
|
var concreteType = entry.GetType();
|
|
valNode.Tag = $"!type:{concreteType.Name}";
|
|
}
|
|
|
|
node.Add(keyNode, valNode);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
// Hand it to the context.
|
|
if (_context != null && _context.TryTypeToNode(obj, out var contextNode))
|
|
{
|
|
return contextNode;
|
|
}
|
|
|
|
// custom TypeSerializer
|
|
if (_typeSerializers.TryGetValue(type, out var serializer))
|
|
return serializer.TypeToNode(obj, this);
|
|
|
|
// IExposeData.
|
|
if (obj is IExposeData exposable)
|
|
{
|
|
var mapping = new YamlMappingNode();
|
|
if (_context != null)
|
|
{
|
|
_context.StackDepth++;
|
|
}
|
|
var fork = NewWriter(mapping, _context);
|
|
if (_context != null)
|
|
{
|
|
_context.StackDepth--;
|
|
}
|
|
exposable.ExposeData(fork);
|
|
return mapping;
|
|
}
|
|
|
|
// ISelfSerialize
|
|
if (typeof(ISelfSerialize).IsAssignableFrom(type))
|
|
{
|
|
var instance = (ISelfSerialize)Activator.CreateInstance(type);
|
|
return instance.Serialize();
|
|
}
|
|
|
|
// other val (struct)
|
|
if (type.IsValueType)
|
|
return _structSerializer.TypeToNode(obj, this);
|
|
|
|
// ref type that isn't a custom TypeSerializer
|
|
throw new ArgumentException($"Type {type.FullName} is not supported.", nameof(obj));
|
|
}
|
|
|
|
bool IsValueDefault<T>(string field, T value, T providedDefault, WithFormat<T> format)
|
|
{
|
|
if ((value != null || providedDefault == null) && (value == null || IsSerializedEqual(value, providedDefault)))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (_context != null)
|
|
{
|
|
return _context.IsValueDefault(field, value, format);
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
internal static bool IsSerializedEqual(object a, object b)
|
|
{
|
|
var type = a?.GetType();
|
|
if (type != b?.GetType())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (a == null) // Also implies b is null since it'd have failed the type equality check otherwise.
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (TryGenericListType(type, out _))
|
|
{
|
|
var listA = (IList) a;
|
|
var listB = (IList) b;
|
|
|
|
if (listA.Count != listB.Count)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0; i < listA.Count; i++)
|
|
{
|
|
var elemA = listA[i];
|
|
var elemB = listB[i];
|
|
|
|
if (!IsSerializedEqual(elemA, elemB))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (typeof(IExposeData).IsAssignableFrom(type))
|
|
{
|
|
// Serialize both, see if output matches.
|
|
var testA = new YamlMappingNode();
|
|
var testB = new YamlMappingNode();
|
|
var serA = NewWriter(testA);
|
|
var serB = NewWriter(testB);
|
|
|
|
var expA = (IExposeData) a;
|
|
var expB = (IExposeData) b;
|
|
|
|
expA.ExposeData(serA);
|
|
expB.ExposeData(serB);
|
|
|
|
// Does deep equality.
|
|
return testA.Equals(testB);
|
|
}
|
|
|
|
return a.Equals(b);
|
|
}
|
|
|
|
|
|
private static object StringToType(Type type, string str)
|
|
{
|
|
var foo = TypeDescriptor.GetConverter(type);
|
|
return foo.ConvertFromInvariantString(str);
|
|
}
|
|
|
|
public static void RegisterTypeSerializer(Type type, TypeSerializer serializer)
|
|
{
|
|
if (!_typeSerializers.ContainsKey(type))
|
|
_typeSerializers.Add(type, serializer);
|
|
}
|
|
|
|
private static bool TryGenericListType(Type type, out Type listType)
|
|
{
|
|
var isList = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>);
|
|
|
|
if (isList)
|
|
{
|
|
listType = type.GetGenericArguments()[0];
|
|
return true;
|
|
}
|
|
|
|
listType = default;
|
|
return false;
|
|
}
|
|
|
|
private static bool TryGenericDictType(Type type, out Type keyType, out Type valType)
|
|
{
|
|
var isDict = type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>);
|
|
|
|
if (isDict)
|
|
{
|
|
var genArgs = type.GetGenericArguments();
|
|
keyType = genArgs[0];
|
|
valType = genArgs[1];
|
|
return true;
|
|
}
|
|
|
|
keyType = default;
|
|
valType = default;
|
|
return false;
|
|
}
|
|
|
|
public abstract class TypeSerializer
|
|
{
|
|
public abstract object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer);
|
|
public abstract YamlNode TypeToNode(object obj, YamlObjectSerializer serializer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Basically, when you're serializing say a map file, you gotta be a liiiittle smarter than "dump all these variables to YAML".
|
|
/// Stuff like entity references need to handled, for example.
|
|
/// This can do that.
|
|
/// </summary>
|
|
public abstract class Context
|
|
{
|
|
/// <summary>
|
|
/// Current depth of the serialization "stack".
|
|
/// Basically, when another sub-serializer gets made (e.g. to handle <see cref="IExposeData" />),
|
|
/// This context will be passed around and this property increased to signal that.
|
|
/// </summary>
|
|
public int StackDepth { get; protected internal set; } = 0;
|
|
|
|
public virtual bool TryTypeToNode(object obj, out YamlNode node)
|
|
{
|
|
node = null;
|
|
return false;
|
|
}
|
|
|
|
public virtual bool TryNodeToType(YamlNode node, Type type, out object obj)
|
|
{
|
|
obj = default;
|
|
return false;
|
|
}
|
|
|
|
public virtual bool IsValueDefault<T>(string field, T value, WithFormat<T> format)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public virtual bool TryGetCachedField<T>(string field, out T value)
|
|
{
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
public virtual void SetCachedField<T>(string field, T value)
|
|
{
|
|
}
|
|
|
|
public virtual bool TryGetDataCache(string field, out object value)
|
|
{
|
|
value = null;
|
|
return false;
|
|
}
|
|
|
|
public virtual void SetDataCache(string field, object value)
|
|
{
|
|
}
|
|
}
|
|
|
|
class StructSerializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
var mapNode = node as YamlMappingNode;
|
|
|
|
var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
var instance = Activator.CreateInstance(type);
|
|
var scalarNode = new YamlScalarNode();
|
|
|
|
foreach (var field in fields)
|
|
{
|
|
if (field.IsNotSerialized)
|
|
continue;
|
|
|
|
var fName = field.Name;
|
|
var fType = field.FieldType;
|
|
|
|
scalarNode.Value = fName;
|
|
|
|
if (mapNode.Children.TryGetValue(scalarNode, out var fNode))
|
|
{
|
|
var fVal = serializer.NodeToType(fType, fNode);
|
|
field.SetValue(instance, fVal);
|
|
}
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
var node = new YamlMappingNode();
|
|
var type = obj.GetType();
|
|
var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
|
|
|
foreach (var field in fields)
|
|
{
|
|
if (field.IsNotSerialized)
|
|
continue;
|
|
|
|
var fVal = field.GetValue(obj);
|
|
|
|
// Potential recursive infinite loop?
|
|
var fTypeNode = serializer.TypeToNode(fVal);
|
|
node.Add(field.Name, fTypeNode);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
}
|
|
|
|
class ColorSerializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
return node.AsColor();
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
var color = (Color)obj;
|
|
|
|
return new YamlScalarNode(color.ToHex());
|
|
}
|
|
}
|
|
|
|
class MapIdSerializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
var val = int.Parse(node.ToString(), CultureInfo.InvariantCulture);
|
|
return new MapId(val);
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
var val = (int)(MapId)obj;
|
|
return new YamlScalarNode(val.ToString());
|
|
}
|
|
}
|
|
|
|
class GridIdSerializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
return new GridId(node.AsInt());
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
var val = (int)(GridId)obj;
|
|
return new YamlScalarNode(val.ToString());
|
|
}
|
|
}
|
|
|
|
class Vector2Serializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
return node.AsVector2();
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
var vec = (Vector2)obj;
|
|
return new YamlScalarNode($"{vec.X.ToString(CultureInfo.InvariantCulture)},{vec.Y.ToString(CultureInfo.InvariantCulture)}");
|
|
}
|
|
}
|
|
|
|
class Vector3Serializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
return node.AsVector3();
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
var vec = (Vector3)obj;
|
|
return new YamlScalarNode($"{vec.X.ToString(CultureInfo.InvariantCulture)},{vec.Y.ToString(CultureInfo.InvariantCulture)},{vec.Z.ToString(CultureInfo.InvariantCulture)}");
|
|
}
|
|
}
|
|
|
|
class Vector4Serializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
return node.AsVector4();
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
var vec = (Vector4)obj;
|
|
return new YamlScalarNode($"{vec.X.ToString(CultureInfo.InvariantCulture)},{vec.Y.ToString(CultureInfo.InvariantCulture)},{vec.Z.ToString(CultureInfo.InvariantCulture)},{vec.W.ToString(CultureInfo.InvariantCulture)}");
|
|
}
|
|
}
|
|
|
|
class AngleSerializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
var nodeContents = node.AsString();
|
|
if (nodeContents.EndsWith("rad"))
|
|
{
|
|
return new Angle(double.Parse(nodeContents.Substring(0, nodeContents.Length - 3), CultureInfo.InvariantCulture));
|
|
}
|
|
return Angle.FromDegrees(double.Parse(nodeContents, CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
var val = ((Angle)obj).Theta;
|
|
return new YamlScalarNode($"{val.ToString(CultureInfo.InvariantCulture)} rad");
|
|
}
|
|
}
|
|
|
|
class UIBox2Serializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
var args = node.ToString().Split(',');
|
|
|
|
var t = float.Parse(args[0], CultureInfo.InvariantCulture);
|
|
var l = float.Parse(args[1], CultureInfo.InvariantCulture);
|
|
var b = float.Parse(args[2], CultureInfo.InvariantCulture);
|
|
var r = float.Parse(args[3], CultureInfo.InvariantCulture);
|
|
|
|
return new UIBox2(l, t, r, b);
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
var box = (UIBox2)obj;
|
|
return new YamlScalarNode($"{box.Top.ToString(CultureInfo.InvariantCulture)},{box.Left.ToString(CultureInfo.InvariantCulture)},{box.Bottom.ToString(CultureInfo.InvariantCulture)},{box.Right.ToString(CultureInfo.InvariantCulture)}");
|
|
}
|
|
}
|
|
|
|
class Box2Serializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
var args = node.ToString().Split(',');
|
|
|
|
var b = float.Parse(args[0], CultureInfo.InvariantCulture);
|
|
var l = float.Parse(args[1], CultureInfo.InvariantCulture);
|
|
var t = float.Parse(args[2], CultureInfo.InvariantCulture);
|
|
var r = float.Parse(args[3], CultureInfo.InvariantCulture);
|
|
|
|
return new Box2(l, b, r, t);
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
var box = (Box2)obj;
|
|
return new YamlScalarNode($"{box.Bottom.ToString(CultureInfo.InvariantCulture)},{box.Left.ToString(CultureInfo.InvariantCulture)},{box.Top.ToString(CultureInfo.InvariantCulture)},{box.Right.ToString(CultureInfo.InvariantCulture)}");
|
|
}
|
|
}
|
|
|
|
class ResourcePathSerializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
return node.AsResourcePath();
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
return new YamlScalarNode(obj.ToString());
|
|
}
|
|
}
|
|
|
|
class SpriteSpecifierSerializer : TypeSerializer
|
|
{
|
|
public override object NodeToType(Type type, YamlNode node, YamlObjectSerializer serializer)
|
|
{
|
|
return SpriteSpecifier.FromYaml(node);
|
|
}
|
|
|
|
public override YamlNode TypeToNode(object obj, YamlObjectSerializer serializer)
|
|
{
|
|
switch (obj)
|
|
{
|
|
case SpriteSpecifier.Texture tex:
|
|
return tex.TexturePath.ToString();
|
|
case SpriteSpecifier.Rsi rsi:
|
|
var mapping = new YamlMappingNode();
|
|
mapping.Add("sprite", rsi.RsiPath.ToString());
|
|
mapping.Add("state", rsi.RsiState);
|
|
return mapping;
|
|
}
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
}
|
|
}
|