diff --git a/ScriptCore/Editor/EntityEditor.cs b/ScriptCore/Editor/EntityEditor.cs index dc7b0e7..7af0d12 100644 --- a/ScriptCore/Editor/EntityEditor.cs +++ b/ScriptCore/Editor/EntityEditor.cs @@ -296,7 +296,7 @@ internal class EntityEditor if (ImGui.BeginCombo("Create", "Create", ImGuiComboFlags.HeightSmall | ImGuiComboFlags.NoArrowButton)) { - foreach (Type t in FindDerivedTypes(fieldType)) + foreach (Type t in TypeExtension.FindDerivedTypes(fieldType)) { if (ImGui.Selectable(t.Name)) { @@ -818,20 +818,6 @@ internal class EntityEditor return newList; } - /// - /// Enumerates all types that derive from the given type. - /// - /// - /// - public static IEnumerable FindDerivedTypes(Type baseType) - { - foreach (Assembly domainAssembly in AppDomain.CurrentDomain.GetAssemblies()) - foreach (Type type in domainAssembly.GetTypes()) - { - if (baseType.IsAssignableFrom(type) && !type.IsAbstract) yield return type; - } - } - private static T ReadStaticField(string name) { FieldInfo field = typeof(T).GetField(name, BindingFlags.Public | BindingFlags.Static); diff --git a/ScriptCore/Extensions/KeyValuePairExtension.cs b/ScriptCore/Extensions/KeyValuePairExtension.cs new file mode 100644 index 0000000..2fbc0b7 --- /dev/null +++ b/ScriptCore/Extensions/KeyValuePairExtension.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GlitchyEngine.Extensions; + +public static class KeyValuePairExtension +{ + public static void Deconstruct(this KeyValuePair tuple, out T1 key, out T2 value) + { + key = tuple.Key; + value = tuple.Value; + } +} diff --git a/ScriptCore/Extensions/TypeExtension.cs b/ScriptCore/Extensions/TypeExtension.cs index b6fc91a..7f538cd 100644 --- a/ScriptCore/Extensions/TypeExtension.cs +++ b/ScriptCore/Extensions/TypeExtension.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Text; namespace GlitchyEngine.Extensions; @@ -15,4 +16,53 @@ public static class TypeExtension { return targetType.IsAssignableFrom(type); } + + /// + /// Enumerates all types in all assemblies. + /// + public static IEnumerable EnumerateAllTypes() + { + foreach (Assembly domainAssembly in AppDomain.CurrentDomain.GetAssemblies()) + foreach (Type type in domainAssembly.GetTypes()) + { + yield return type; + } + } + + /// + /// Enumerates all types that derive from the given type. + /// + /// + /// + public static IEnumerable FindDerivedTypes(Type baseType) + { + foreach (Assembly domainAssembly in AppDomain.CurrentDomain.GetAssemblies()) + foreach (Type type in domainAssembly.GetTypes()) + { + if (baseType.IsAssignableFrom(type) && !type.IsAbstract) yield return type; + } + } + + /// + /// Returns whether or not the type has an of the specified . + /// + /// The type of the attribute + /// if the type has the specified ; otherwise. + public static bool HasCustomAttribute(this Type type) where T: Attribute + { + return type.GetCustomAttribute() != null; + } + + /// + /// Returns whether or not the type has an of the specified . + /// + /// The type of the attribute + /// The attribute, or , if the type hasn't got the attribute specified. + /// if the type has the specified ; otherwise. + public static bool TryGetCustomAttribute(this Type type, out T attribute) where T: Attribute + { + attribute = type.GetCustomAttribute(); + + return attribute != null; + } } diff --git a/ScriptCore/Serialization/CustomSerializerAttribute.cs b/ScriptCore/Serialization/CustomSerializerAttribute.cs new file mode 100644 index 0000000..ad932d1 --- /dev/null +++ b/ScriptCore/Serialization/CustomSerializerAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GlitchyEngine.Serialization; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public sealed class CustomSerializerAttribute : Attribute +{ + public Type Type { get; private set; } + + public CustomSerializerAttribute(Type type) + { + Type = type; + } +} diff --git a/ScriptCore/Serialization/DeserializationObject.cs b/ScriptCore/Serialization/DeserializationObject.cs index 478ad77..c13b350 100644 --- a/ScriptCore/Serialization/DeserializationObject.cs +++ b/ScriptCore/Serialization/DeserializationObject.cs @@ -10,12 +10,13 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using GlitchyEngine.Editor; namespace GlitchyEngine.Serialization; -internal class DeserializationObject +public class DeserializationObject { - private class NoObject + public class NoObject { } @@ -23,7 +24,7 @@ internal class DeserializationObject /// /// Object used to specify, that no particular value was deserialized for the field and thus the value currently stored shall not be changed. /// - private static readonly NoObject NoValueDeserialized = new (); + public static readonly NoObject NoValueDeserialized = new (); private IntPtr _internalContext; @@ -36,6 +37,24 @@ internal class DeserializationObject public Dictionary DeserializedClasses; private object _instance; + + private Dictionary _fullNameToType = new(); + + private static Dictionary _customDeserializers = new(); + + + /// + /// Gets the type that was originally stored in the container, or null, if the type doesn't exist. + /// + public Type StoredType + { + get + { + ScriptGlue.Serialization_GetObjectTypeName(_internalContext, out string fullTypeName); + + return GetTypeFromName(fullTypeName); + } + } public DeserializationObject(IntPtr internalContext, UUID id, Dictionary deserializedClasses) { @@ -43,10 +62,30 @@ internal class DeserializationObject _id = id; DeserializedClasses = deserializedClasses; } + + static DeserializationObject() + { + foreach (Type type in TypeExtension.EnumerateAllTypes()) + { + if (type.TryGetCustomAttribute(out var attribute)) + { + MethodInfo deserializeMethod = type.GetMethod("Deserialize", BindingFlags.Static | BindingFlags.Public, + null, + new []{ typeof(DeserializationObject), typeof(string), typeof(Type) }, null); - private Dictionary _fullNameToType = new(); + if (deserializeMethod == null) + { + Log.Error($"No Deserialize-method found for type {type}"); + } + else + { + _customDeserializers.Add(attribute.Type, deserializeMethod); + } + } + } + } - private Type FindType(string fullName) + public Type FindType(string fullName) { foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies().Reverse()) { @@ -59,7 +98,7 @@ internal class DeserializationObject return null; } - private Type GetTypeFromName(string fullName) + public Type GetTypeFromName(string fullName) { if (_fullNameToType.TryGetValue(fullName, out Type storedType)) return storedType; @@ -71,7 +110,7 @@ internal class DeserializationObject return type; } - private DeserializationObject GetDeserializedObject(UUID id) + public DeserializationObject GetDeserializedObject(UUID id) { DeserializationObject context; @@ -87,14 +126,14 @@ internal class DeserializationObject return context; } - private void PushScope(string name) + public void PushScope(string name) { _structScope.Push(name); _structScopeName += $"{name}."; } - private void PopScope() + public void PopScope() { string scopeToRemove = _structScope.Pop(); _structScopeName = _structScopeName.Remove(_structScopeName.Length - scopeToRemove.Length - 1); @@ -115,12 +154,12 @@ internal class DeserializationObject public EngineObjectReferenceHelper EngineObjectReference; } - private T GetFieldValue(string fieldName, SerializationType serializationType) + public T GetFieldValue(string fieldName, SerializationType serializationType) { return (T)GetFieldValue(fieldName, serializationType); } - private unsafe object GetFieldValue(string fieldName, SerializationType serializationType) + public unsafe object GetFieldValue(string fieldName, SerializationType serializationType) { string completeFieldName = $"{_structScopeName}{fieldName}"; @@ -200,7 +239,7 @@ internal class DeserializationObject DeserializeFields(entity); } - private bool DeserializeFields(object obj) + public bool DeserializeFields(object obj) { Type type = obj.GetType(); @@ -224,8 +263,35 @@ internal class DeserializationObject return changed; } + + private bool TryCustomDeserializer(string fieldName, Type fieldType, out object deserializedValue) + { + deserializedValue = NoValueDeserialized; - private object DeserializeField(object fieldValue, Type fieldType, string fieldName) + try + { + // Try to match the concrete type first (e.g. Foo -> Foo and Foo -> List) + // Note: Foo wont match a serializer for Foo<> + if (_customDeserializers.TryGetValue(fieldType, out MethodInfo deserializeMethod)) + { + deserializedValue = deserializeMethod.Invoke(null, new object[] { this, fieldName, fieldType }); + return true; + } + + if (fieldType.IsGenericType && _customDeserializers.TryGetValue(fieldType.GetGenericTypeDefinition(), out deserializeMethod)) + { + deserializedValue = deserializeMethod.Invoke(null, new object[] { this, fieldName, fieldType }); + } return true; + } + catch (Exception e) + { + Log.Error(e); + } + + return false; + } + + public object DeserializeField(object fieldValue, Type fieldType, string fieldName) { if (fieldType.IsPrimitive) { @@ -245,6 +311,11 @@ internal class DeserializationObject } else if (fieldType.IsGenericType) { + if (TryCustomDeserializer(fieldName, fieldType, out object deserializedValue)) + { + return deserializedValue; + } + if (fieldType.GetGenericTypeDefinition() == typeof(List<>)) { return DeserializeList(fieldName, fieldType, fieldType.GetGenericArguments()[0]); @@ -261,10 +332,20 @@ internal class DeserializationObject } else if (fieldType.IsValueType) { + if (TryCustomDeserializer(fieldName, fieldType, out object deserializedValue)) + { + return deserializedValue; + } + return DeserializeStruct(fieldName, fieldValue); } else if (fieldType.IsClass) { + if (TryCustomDeserializer(fieldName, fieldType, out object deserializedValue)) + { + return deserializedValue; + } + return DeserializeClass(fieldName, fieldType); } else @@ -275,7 +356,7 @@ internal class DeserializationObject return NoValueDeserialized; } - private object DeserializePrimitive(string fieldName, Type fieldType) + public object DeserializePrimitive(string fieldName, Type fieldType) { Debug.Assert(fieldType.IsPrimitive, $"{fieldType} is not a primitive type."); @@ -315,20 +396,7 @@ internal class DeserializationObject return GetFieldValue(fieldName, expectedType); } - /// - /// Gets the type that was originally stored in the container, or null, if the type doesn't exist. - /// - public Type StoredType - { - get - { - ScriptGlue.Serialization_GetObjectTypeName(_internalContext, out string fullTypeName); - - return GetTypeFromName(fullTypeName); - } - } - - private object DeserializeList(string fieldName, Type fieldType, Type elementType) + public object DeserializeList(string fieldName, Type fieldType, Type elementType) { UUID id = (UUID)GetFieldValue(fieldName, SerializationType.ObjectReference); @@ -398,7 +466,7 @@ internal class DeserializationObject return deserializedObject._instance; } - private object DeserializeEnum(string fieldName, Type enumType) + public object DeserializeEnum(string fieldName, Type enumType) { if (GetFieldValue(fieldName, SerializationType.Enum) is not string valueName) return NoValueDeserialized; @@ -415,7 +483,7 @@ internal class DeserializationObject return NoValueDeserialized; } - private object DeserializeStruct(string fieldName, object targetInstance) + public object DeserializeStruct(string fieldName, object targetInstance) { PushScope(fieldName); @@ -426,7 +494,7 @@ internal class DeserializationObject return changed ? targetInstance : NoValueDeserialized; } - private unsafe object DeserializeClass(string fieldName, Type fieldType) + public unsafe object DeserializeClass(string fieldName, Type fieldType) { bool isEntity = typeof(Entity).IsAssignableFrom(fieldType); bool isComponent = fieldType.IsSubclassOf(typeof(Component)); diff --git a/ScriptCore/Serialization/DictionarySerializer.cs b/ScriptCore/Serialization/DictionarySerializer.cs new file mode 100644 index 0000000..94b6038 --- /dev/null +++ b/ScriptCore/Serialization/DictionarySerializer.cs @@ -0,0 +1,118 @@ +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using GlitchyEngine.Core; +using GlitchyEngine.Extensions; + +namespace GlitchyEngine.Serialization; + +/// +/// Provides Serialization and Deserialization for Dictionaries. +/// +[CustomSerializer(typeof(Dictionary<,>))] +public static class DictionarySerializer +{ + /// + /// Serializes a given Dictionary as a field of . + /// + /// The parent object. + /// Name of the field in + /// The dictionary + /// Type of or the type of the field if is + public static void Serialize(SerializedObject container, string fieldName, object? fieldValue, Type fieldType) + { + if (fieldValue == null) + { + // Write a null pointer early out + container.AddField(fieldName, SerializationType.ObjectReference, UUID.Zero); + return; + } + + // Get a container for our dictionary (Note: container is our parent object). + // isNewContext is true, if our dictionary wasn't serialized so far. + var (context, isNewContext) = container.GetSerializedObject(fieldValue); + + if (isNewContext) + { + Type keyType = fieldType.GetGenericArguments()[0]; + Type valueType = fieldType.GetGenericArguments()[1]; + + IDictionary dictionary = (IDictionary)fieldValue; + ICollection collection = (ICollection)fieldValue; + + context.AddField("Count", SerializationType.Int32, collection.Count); + + int index = 0; + + foreach (DictionaryEntry entry in dictionary) + { + context.PushScope(index.ToString()); + + context.SerializeField("Key", entry.Key, keyType); + context.SerializeField("Value", entry.Value, valueType); + + context.PopScope(); + + index++; + } + } + + // Write the reference to our dictionary into the field of the parent. + container.AddField(fieldName, SerializationType.ObjectReference, context.Id); + } + + /// + /// Deserializes a dictionary that is a field of the given . + /// + /// The parent object whose field with is a dictionary. + /// The name of the field. + /// The type of the field. + /// + public static object? Deserialize(DeserializationObject container, string fieldName, Type fieldType) + { + UUID id = container.GetFieldValue(fieldName, SerializationType.ObjectReference); + + if (id == UUID.Zero) + return null; + + // Get serialization container for the instance + DeserializationObject deserializedObject = container.GetDeserializedObject(id); + + Type type = deserializedObject.StoredType; + + if (type?.IsAssignableTo(fieldType) != true) + return DeserializationObject.NoValueDeserialized; + + Type keyType = fieldType.GetGenericArguments()[0]; + Type valueType = fieldType.GetGenericArguments()[1]; + + int count = deserializedObject.GetFieldValue("Count", SerializationType.Int32); + + // Create instance, pass in capacity + object? instance = ActivatorExtension.CreateInstanceSafe(type, count); + + Debug.Assert(instance != null); + + IDictionary dictionary = (IDictionary)instance!; + + for (int i = 0; i < count; i++) + { + deserializedObject.PushScope(i.ToString()); + + object? key = deserializedObject.DeserializeField(null, keyType, "Key"); + object value = deserializedObject.DeserializeField(null, valueType, "Value"); + + if (key != null) + { + dictionary.Add(key, value); + } + + deserializedObject.PopScope(); + } + + return instance; + } +} diff --git a/ScriptCore/Serialization/EntitySerializer.cs b/ScriptCore/Serialization/EntitySerializer.cs index a78e609..0168661 100644 --- a/ScriptCore/Serialization/EntitySerializer.cs +++ b/ScriptCore/Serialization/EntitySerializer.cs @@ -7,7 +7,7 @@ using GlitchyEngine.Extensions; namespace GlitchyEngine.Serialization; -internal enum SerializationType : int +public enum SerializationType : int { None, diff --git a/ScriptCore/Serialization/SerializedObject.cs b/ScriptCore/Serialization/SerializedObject.cs index 53ebeba..96c39d7 100644 --- a/ScriptCore/Serialization/SerializedObject.cs +++ b/ScriptCore/Serialization/SerializedObject.cs @@ -4,10 +4,11 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; +using GlitchyEngine.Extensions; namespace GlitchyEngine.Serialization; -internal class SerializedObject +public class SerializedObject { private IntPtr _internalContext; @@ -18,6 +19,10 @@ internal class SerializedObject private Stack _structScope = new(); private string _structScopeName; + + private static Dictionary _customSerializers = new(); + + public UUID Id => _id; public SerializedObject(IntPtr internalContext, UUID id, Dictionary serializedClasses) { @@ -25,8 +30,8 @@ internal class SerializedObject _id = id; _serializedClasses = serializedClasses; } - - private (SerializedObject context, bool newContext) GetSerializedObject(object o) + + public (SerializedObject context, bool newContext) GetSerializedObject(object o) { SerializedObject context; @@ -42,20 +47,42 @@ internal class SerializedObject return (context, true); } - private void PushScope(string name) + static SerializedObject() + { + foreach (Type type in TypeExtension.EnumerateAllTypes()) + { + if (type.TryGetCustomAttribute(out var attribute)) + { + MethodInfo serializeMethod = type.GetMethod("Serialize", BindingFlags.Static | BindingFlags.Public, + null, + new[] { typeof(SerializedObject), typeof(string), typeof(object), typeof(Type) }, null); + + if (serializeMethod == null) + { + Log.Error($"No Serialize-method found for type {type}"); + } + else + { + _customSerializers.Add(attribute.Type, serializeMethod); + } + } + } + } + + public void PushScope(string name) { _structScope.Push(name); _structScopeName += $"{name}."; } - private void PopScope() + public void PopScope() { string scopeToRemove = _structScope.Pop(); _structScopeName = _structScopeName.Remove(_structScopeName.Length - scopeToRemove.Length - 1); } - private void AddField(string fieldName, SerializationType serializationType, object value, string fullTypeName = null) + public void AddField(string fieldName, SerializationType serializationType, object value, string fullTypeName = null) { string completeFieldName = $"{_structScopeName}{fieldName}"; @@ -67,7 +94,7 @@ internal class SerializedObject SerializeFields(entity); } - private void SerializeFields(object obj) + public void SerializeFields(object obj) { Type type = obj.GetType(); @@ -83,7 +110,35 @@ internal class SerializedObject } } - private void SerializeField(string fieldName, object fieldValue, Type fieldType) + private bool TryCustomSerializer(string fieldName, object fieldValue, Type fieldType) + { + try + { + // Try to match the concrete type first (e.g. Foo -> Foo and Foo -> List) + // Note: Foo wont match a serializer for Foo<> + if (_customSerializers.TryGetValue(fieldType, out MethodInfo serializeMethod)) + { + serializeMethod.Invoke(null, new[] { this, fieldName, fieldValue, fieldType }); + return true; + } + + if (fieldType.IsGenericType && _customSerializers.TryGetValue(fieldType.GetGenericTypeDefinition(), out serializeMethod)) + { + serializeMethod.Invoke(null, new[] { this, fieldName, fieldValue, fieldType }); + return true; + } + } + catch (Exception e) + { + Log.Error(e); + // Don't attempt to use any other serializer after this error... + return true; + } + + return false; + } + + public void SerializeField(string fieldName, object fieldValue, Type fieldType) { if (fieldType.IsPrimitive) { @@ -110,6 +165,9 @@ internal class SerializedObject } else if (fieldType.IsGenericType) { + if (TryCustomSerializer(fieldName, fieldValue, fieldType)) + return; + if (fieldType.GetGenericTypeDefinition() == typeof(List<>)) { SerializeList(fieldName, fieldValue, fieldType, fieldType.GetGenericArguments()[0]); @@ -126,10 +184,16 @@ internal class SerializedObject } else if (fieldType.IsValueType) { + if (TryCustomSerializer(fieldName, fieldValue, fieldType)) + return; + SerializeStruct(fieldName, fieldValue, fieldType); } else if (fieldType.IsClass) { + if (TryCustomSerializer(fieldName, fieldValue, fieldType)) + return; + SerializeClass(fieldName, fieldValue, fieldType); } else @@ -138,7 +202,7 @@ internal class SerializedObject } } - private void SerializeList(string fieldName, object listObject, Type fieldType, Type elementType) + public void SerializeList(string fieldName, object listObject, Type fieldType, Type elementType) { if (listObject == null) { @@ -168,7 +232,7 @@ internal class SerializedObject } } - private void SerializePrimitive(string fieldName, object fieldValue, Type fieldType) + public void SerializePrimitive(string fieldName, object fieldValue, Type fieldType) { Debug.Assert(fieldType.IsPrimitive, $"{fieldType} is not a primitive type."); @@ -208,12 +272,12 @@ internal class SerializedObject AddField(fieldName, type, fieldValue); } - private void SerializeEnum(string fieldName, object fieldValue, Type fieldType) + public void SerializeEnum(string fieldName, object fieldValue, Type fieldType) { AddField(fieldName, SerializationType.Enum, fieldValue.ToString()); } - private void SerializeStruct(string fieldName, object fieldValue, Type fieldType) + public void SerializeStruct(string fieldName, object fieldValue, Type fieldType) { PushScope(fieldName); @@ -222,7 +286,7 @@ internal class SerializedObject PopScope(); } - private void SerializeClass(string fieldName, object fieldValue, Type fieldType) + public void SerializeClass(string fieldName, object fieldValue, Type fieldType) { if (typeof(Entity).IsAssignableFrom(fieldType)) {