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 { /// /// Object serializer that serializes to/from YAML. /// public class YamlObjectSerializer : ObjectSerializer { private static readonly Dictionary _typeSerializers; public static IReadOnlyDictionary TypeSerializers => _typeSerializers; private static readonly StructSerializer _structSerializer; private YamlMappingNode WriteMap; private List ReadMaps; private Context _context; static YamlObjectSerializer() { _structSerializer = new StructSerializer(); _typeSerializers = new Dictionary { { 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() { } /// /// Creates a new serializer to be used for reading from YAML data. /// /// /// The YAML mapping to read data from. /// /// /// An optional context that can provide additional capabitilies such as caching and custom type serializers. /// public static YamlObjectSerializer NewReader(YamlMappingNode readMap, Context context = null) { return NewReader(new List(1) { readMap }, context); } /// /// Creates a new serializer to be used for reading from YAML data. /// /// /// 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. /// /// /// An optional context that can provide additional capabilities such as caching and custom type serializers. /// public static YamlObjectSerializer NewReader(List readMaps, Context context = null) { return new YamlObjectSerializer { ReadMaps = readMaps, _context = context, Reading = true, }; } /// /// Creates a new serializer to be used from writing into YAML data. /// /// /// The mapping to write into. /// Gets modified directly in place. /// /// /// An optional context that can provide additional capabitilies such as caching and custom type serializers. /// 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 use caching for value types without references too. /// public override void DataField(ref T value, string name, T defaultValue, WithFormat 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 o, Expression> 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 getter; ReadFunctionDelegate 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); } /// public override void DataFieldCached(ref T value, string name, T defaultValue, WithFormat 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); } } /// public override void DataField( ref TTarget value, string name, TTarget defaultValue, Func ReadConvertFunc, Func 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.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); } } /// public override void DataFieldCached( ref TTarget value, string name, TTarget defaultValue, Func ReadConvertFunc, Func 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); } } /// public override T ReadDataField(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; } /// public override T ReadDataFieldCached(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; } /// public override bool TryReadDataField(string name, WithFormat 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(string name, WithFormat 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; } /// public override void DataReadFunction(string name, T defaultValue, ReadFunctionDelegate 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); } /// public override void DataWriteFunction(string name, T defaultValue, WriteFunctionDelegate 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.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); } /// public override void SetCacheData(string key, object value) { _context?.SetDataCache(key, value); } /// public override T GetCacheData(string key) { if (_context != null && _context.TryGetDataCache(key, out var value)) { return (T)value; } throw new KeyNotFoundException(); } /// public override bool TryGetCacheData(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 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 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(); 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 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 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(string field, T value, T providedDefault, WithFormat 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); } /// /// 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. /// public abstract class Context { /// /// Current depth of the serialization "stack". /// Basically, when another sub-serializer gets made (e.g. to handle ), /// This context will be passed around and this property increased to signal that. /// 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(string field, T value, WithFormat format) { return false; } public virtual bool TryGetCachedField(string field, out T value) { value = default; return false; } public virtual void SetCachedField(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(); } } } }