ScriptEngine: Serialize static fields

This commit is contained in:
Simon Lübeß
2024-04-13 00:10:35 +02:00
parent df13aa8596
commit 949ab4ebb0
16 changed files with 504 additions and 244 deletions
+108 -76
View File
@@ -145,6 +145,22 @@ internal class EntityEditor
return false;
}
public static bool ShowButtonInEditor(MethodInfo methodInfo)
{
var showButton = methodInfo.GetCustomAttribute<ShowButtonAttribute>();
if (showButton == null)
return false;
if (Application.IsInEditMode && !showButton.Visibility.HasFlag(ButtonVisibility.InEditMode))
return false;
if (Application.IsInPlayMode && !showButton.Visibility.HasFlag(ButtonVisibility.InPlayMode))
return false;
return true;
}
/// <summary>
/// Entry point for the engine to show the editor for the given script instance.
@@ -908,103 +924,119 @@ internal class EntityEditor
return newValue;
}
public static void ShowEditor(Type type, object? reference)
private static void ShowEditor(Type type, object? reference)
{
int i = 0;
// Iterate all fields
foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
//foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
foreach (MemberInfo member in type.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
{
i++;
ImGui.PushID(i);
if (ShowFieldInEditor(field))
switch (member)
{
object value = field.GetValue(reference);
IEnumerable<Attribute> attributes = field.GetCustomAttributes();
LabelAttribute? label = GetAttribute<LabelAttribute>(attributes);
string prettyName;
if (label != null)
{
prettyName = label.Label ?? field.Name;
}
else
{
prettyName = field.Name.ToPrettyName();
}
object? newValue = ShowFieldEditor(value, value?.GetType() ?? field.FieldType, prettyName, attributes);
if (newValue != DidNotChange)
{
// Reference was changed inside Field Editor -> write back to field (only necessary for value types)
field.SetValue(reference, newValue);
}
case FieldInfo field:
ShowField(field, reference);
break;
case MethodInfo method:
ShowButton(method, reference);
break;
}
ImGui.PopID();
}
ShowButtons(type, reference);
}
/// <summary>
/// Shows all buttons in the UI.
/// </summary>
private static void ShowButtons(Type type, object? reference)
{
ImGui.PushID("Buttons");
int i = 0;
// Iterate all methods
foreach (MethodInfo method in type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
{
var showButton = method.GetCustomAttribute<ShowButtonAttribute>();
if (showButton == null)
continue;
if (Application.IsInEditMode && !showButton.Visibility.HasFlag(ButtonVisibility.InEditMode))
continue;
bool showedHeader = false;
if (Application.IsInPlayMode && !showButton.Visibility.HasFlag(ButtonVisibility.InPlayMode))
continue;
foreach (FieldInfo field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static))
{
if (!showedHeader)
{
BeginNewRow();
if (!ImGui.TreeNodeEx("Static Fields", ImGuiTreeNodeFlags.AllowOverlap | ImGuiTreeNodeFlags.SpanAllColumns))
{
break;
}
showedHeader = true;
}
i++;
ImGui.PushID(i);
ImGui.TableNextRow();
ImGui.TableSetColumnIndex(0);
if (method.GetParameters().Length > 0)
{
Log.Error($"Method {method.Name} cannot be executed from editor, because it expects arguments.");
}
else if (ImGui.Button(showButton.ButtonText))
{
try
{
method.Invoke(reference, null);
}
catch (TargetInvocationException ex)
{
Log.Exception(ex.InnerException ?? ex);
}
catch (Exception ex)
{
Log.Exception(ex);
}
}
ShowField(field, null);
ImGui.PopID();
}
ImGui.PopID();
if (showedHeader)
{
ImGui.TreePop();
}
}
private static void ShowField(FieldInfo field, object? reference)
{
if (!ShowFieldInEditor(field))
return;
object value = field.GetValue(reference);
IEnumerable<Attribute> attributes = field.GetCustomAttributes();
LabelAttribute? label = GetAttribute<LabelAttribute>(attributes);
string prettyName;
if (label != null)
{
prettyName = label.Label ?? field.Name;
}
else
{
prettyName = field.Name.ToPrettyName();
}
object? newValue = ShowFieldEditor(value, value?.GetType() ?? field.FieldType, prettyName, attributes);
if (newValue != DidNotChange)
{
// Reference was changed inside Field Editor -> write back to field (only necessary for value types)
field.SetValue(reference, newValue);
}
}
private static void ShowButton(MethodInfo method, object? reference)
{
if (!ShowButtonInEditor(method))
return;
var showButton = method.GetCustomAttribute<ShowButtonAttribute>();
BeginNewRow();
if (method.GetParameters().Length > 0)
{
Log.Error($"Method {method.Name} cannot be executed from editor, because it expects arguments.");
}
else if (ImGui.Button(showButton.ButtonText))
{
try
{
method.Invoke(reference, null);
}
catch (TargetInvocationException ex)
{
Log.Exception(ex.InnerException ?? ex);
}
catch (Exception ex)
{
Log.Exception(ex);
}
}
}
struct ListPayload
@@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace GlitchyEngine.Extensions;
/// <summary>
/// Extends the <see cref="FieldInfo"/>-class with useful methods.
/// </summary>
public static class FieldInfoExtension
{
/// <summary>
/// Returns <see langword="true"/> if the field has the specified attribute.
/// </summary>
/// <typeparam name="T">The type of the attribte.</typeparam>
public static bool HasCustomAttribute<T>(this FieldInfo fiedInfo) where T : Attribute
{
return fiedInfo.GetCustomAttribute<T>() != null;
}
}
@@ -0,0 +1,19 @@
using System;
using System.Reflection;
namespace GlitchyEngine.Extensions;
/// <summary>
/// Extends <see cref="MemberInfo"/> with useful methods.
/// </summary>
public static class MemberInfoExtension
{
/// <summary>
/// Returns <see langword="true"/> if the member has the specified attribute.
/// </summary>
/// <typeparam name="T">The type of the attribute.</typeparam>
public static bool HasCustomAttribute<T>(this MemberInfo memberInfo) where T : Attribute
{
return memberInfo.GetCustomAttribute<T>() != null;
}
}
+13
View File
@@ -227,4 +227,17 @@ public static class TypeExtension
return nameBuilder.ToString();
}
/// <summary>
/// Returns <see langword="true"/> if the type has at least one static field; <see langword="false"/> otherwise.
/// </summary>
public static bool HasStaticFields(this Type type)
{
foreach (FieldInfo _ in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
{
return true;
}
return false;
}
}
+1 -1
View File
@@ -318,7 +318,7 @@ internal static class ScriptGlue
internal static extern void Serialization_SerializeField(IntPtr serializationContext, SerializationType type, string name, object? value, string? fullTypeName = null);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern void Serialization_CreateObject(IntPtr currentContext, string fullTypeName, out IntPtr context, out UUID id);
internal static extern void Serialization_CreateObject(IntPtr currentContext, bool isStatic, string fullTypeName, out IntPtr context, out UUID id);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern unsafe void Serialization_DeserializeField(IntPtr internalContext, SerializationType expectedType, string fieldName, byte* value, out SerializationType actualType);
@@ -28,6 +28,8 @@ public class DeserializationObject
private IntPtr _internalContext;
private bool _isStatic;
private UUID _id;
private Stack<string> _structScope = new();
@@ -57,9 +59,12 @@ public class DeserializationObject
}
}
public DeserializationObject(IntPtr internalContext, UUID id, Dictionary<UUID, DeserializationObject> deserializedClasses)
public bool IsStatic => _isStatic;
public DeserializationObject(IntPtr internalContext, bool isStatic, UUID id, Dictionary<UUID, DeserializationObject> deserializedClasses)
{
_internalContext = internalContext;
_isStatic = isStatic;
_id = id;
DeserializedClasses = deserializedClasses;
}
@@ -114,7 +119,7 @@ public class DeserializationObject
return null;
}
context = new DeserializationObject(contextPtr, id, DeserializedClasses);
context = new DeserializationObject(contextPtr, false, id, DeserializedClasses);
DeserializedClasses.Add(id, context);
@@ -250,6 +255,36 @@ public class DeserializationObject
DeserializeFields(entity);
}
public void Deserialize(Type type)
{
_instance = null;
DeserializeStaticFields(type);
}
public bool DeserializeStaticFields(Type type)
{
bool changed = false;
foreach (FieldInfo field in type.GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public))
{
if (!EntitySerializer.SerializeField(field, true))
continue;
object currentValue = field.GetValue(null);
object? deserializeValue = DeserializeField(currentValue, field.FieldType, field.Name);
if (deserializeValue != NoValueDeserialized)
{
field.SetValue(null, deserializeValue);
changed = true;
}
}
return changed;
}
public bool DeserializeFields(object obj)
{
@@ -259,7 +294,7 @@ public class DeserializationObject
foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
{
if (!EntitySerializer.SerializeField(field))
if (!EntitySerializer.SerializeField(field, false))
continue;
object currentValue = field.GetValue(obj);
+18 -80
View File
@@ -7,102 +7,40 @@ using GlitchyEngine.Extensions;
namespace GlitchyEngine.Serialization;
[Flags]
public enum SerializationType : uint
{
None = 0,
Bool = 1u << 31,
TextTypes = 1 << 30,
Char = TextTypes | 1,
String = TextTypes | 2,
Number = 1 << 29,
Integer = Number | 1 << 28,
Int8 = Integer | 1,
Int16 = Integer | 2,
Int32 = Integer | 3,
Int64 = Integer | 4,
UInt8 = Integer | 5,
UInt16 = Integer | 6,
UInt32 = Integer | 7,
UInt64 = Integer | 8,
FloatingPoint = Number | 1 << 27,
Float = FloatingPoint | 1,
Double = FloatingPoint | 2,
Decimal = FloatingPoint | 3,
Enum = 1 << 26,
EntityReference = 1 << 25,
ComponentReference = 1 << 24,
ObjectReference = 1 << 23,
}
public static class SerializationTypeExtension
{
public static bool CanConvertTo(this SerializationType currentType, SerializationType targetType)
{
if (currentType.HasFlag(SerializationType.Number) && targetType.HasFlag(SerializationType.Number))
return true;
return false;
}
public static Type? GetTypeInstance(this SerializationType currentType)
{
return currentType switch
{
SerializationType.Bool => typeof(bool),
SerializationType.Char => typeof(char),
SerializationType.String => typeof(string),
SerializationType.Int8 => typeof(sbyte),
SerializationType.Int16 => typeof(short),
SerializationType.Int32 => typeof(int),
SerializationType.Int64 => typeof(long),
SerializationType.UInt8 => typeof(byte),
SerializationType.UInt16 => typeof(ushort),
SerializationType.UInt32 => typeof(uint),
SerializationType.UInt64 => typeof(ulong),
SerializationType.Float => typeof(float),
SerializationType.Double => typeof(double),
SerializationType.Decimal => typeof(decimal),
SerializationType.Enum => typeof(Enum),
SerializationType.EntityReference => typeof(UUID),
SerializationType.ComponentReference => typeof(UUID),
SerializationType.ObjectReference => typeof(UUID),
_ => null
};
}
}
internal static class EntitySerializer
{
internal static void Serialize(Entity entity, IntPtr internalContext)
{
SerializedObject obj = new SerializedObject(internalContext, UUID.Zero, new Dictionary<object, SerializedObject>());
SerializedObject obj = new SerializedObject(internalContext, false, UUID.Zero, new Dictionary<object, SerializedObject>());
obj.Serialize(entity);
}
internal static void Deserialize(Entity entity, IntPtr internalContext)
{
DeserializationObject obj = new DeserializationObject(internalContext, UUID.Zero, new Dictionary<UUID, DeserializationObject>());
DeserializationObject obj = new DeserializationObject(internalContext, false, UUID.Zero, new Dictionary<UUID, DeserializationObject>());
obj.Deserialize(entity);
}
internal static void SerializeStaticFields(Type type, IntPtr internalContext)
{
SerializedObject obj = new SerializedObject(internalContext, true, UUID.Zero, new Dictionary<object, SerializedObject>());
obj.Serialize(type);
}
internal static void DeserializeStaticFields(Type type, IntPtr internalContext)
{
DeserializationObject obj = new DeserializationObject(internalContext, true, UUID.Zero, new Dictionary<UUID, DeserializationObject>());
obj.Deserialize(type);
}
/// <summary>
/// Returns whether or not the field described by the given <see cref="fieldInfo"/> should be serialized or not.
/// </summary>
internal static bool SerializeField(FieldInfo fieldInfo)
/// <param name="fieldInfo">The field info for which to check whether the field should be serialized or not.</param>
/// <param name="staticSerialization">If <see langword="true"/> the only Static fields can be serialized; if <see langword="false"/> only instance fields can be serialized.</param>
internal static bool SerializeField(FieldInfo fieldInfo, bool staticSerialization)
{
// TODO: We want to be able to serialize static fields in the future!
if (fieldInfo.IsStatic)
if (staticSerialization != fieldInfo.IsStatic)
return false;
var serializeField = fieldInfo.HasCustomAttribute<SerializeFieldAttribute>();
@@ -0,0 +1,77 @@
using System;
using GlitchyEngine.Core;
namespace GlitchyEngine.Serialization;
[Flags]
public enum SerializationType : uint
{
None = 0,
Bool = 1u << 31,
TextTypes = 1 << 30,
Char = TextTypes | 1,
String = TextTypes | 2,
Number = 1 << 29,
Integer = Number | 1 << 28,
Int8 = Integer | 1,
Int16 = Integer | 2,
Int32 = Integer | 3,
Int64 = Integer | 4,
UInt8 = Integer | 5,
UInt16 = Integer | 6,
UInt32 = Integer | 7,
UInt64 = Integer | 8,
FloatingPoint = Number | 1 << 27,
Float = FloatingPoint | 1,
Double = FloatingPoint | 2,
Decimal = FloatingPoint | 3,
Enum = 1 << 26,
EntityReference = 1 << 25,
ComponentReference = 1 << 24,
ObjectReference = 1 << 23,
}
public static class SerializationTypeExtension
{
public static bool CanConvertTo(this SerializationType currentType, SerializationType targetType)
{
if (currentType.HasFlag(SerializationType.Number) && targetType.HasFlag(SerializationType.Number))
return true;
return false;
}
public static Type? GetTypeInstance(this SerializationType currentType) =>
currentType switch
{
SerializationType.Bool => typeof(bool),
SerializationType.Char => typeof(char),
SerializationType.String => typeof(string),
SerializationType.Int8 => typeof(sbyte),
SerializationType.Int16 => typeof(short),
SerializationType.Int32 => typeof(int),
SerializationType.Int64 => typeof(long),
SerializationType.UInt8 => typeof(byte),
SerializationType.UInt16 => typeof(ushort),
SerializationType.UInt32 => typeof(uint),
SerializationType.UInt64 => typeof(ulong),
SerializationType.Float => typeof(float),
SerializationType.Double => typeof(double),
SerializationType.Decimal => typeof(decimal),
SerializationType.Enum => typeof(Enum),
SerializationType.EntityReference => typeof(UUID),
SerializationType.ComponentReference => typeof(UUID),
SerializationType.ObjectReference => typeof(UUID),
_ => null
};
}
+32 -8
View File
@@ -19,16 +19,21 @@ public class SerializedObject
private Stack<string> _structScope = new();
private string _structScopeName = "";
private bool _isStatic;
private delegate void SerializeMethod(SerializedObject container, string fieldName, object? fieldValue, Type fieldType);
private static Dictionary<Type, SerializeMethod> _customSerializers = new();
public UUID Id => _id;
public bool IsStatic => _isStatic;
public SerializedObject(IntPtr internalContext, UUID id, Dictionary<object, SerializedObject> serializedClasses)
public SerializedObject(IntPtr internalContext, bool isStatic, UUID id, Dictionary<object, SerializedObject> serializedClasses)
{
_internalContext = internalContext;
_isStatic = isStatic;
_id = id;
_serializedClasses = serializedClasses;
}
@@ -40,9 +45,9 @@ public class SerializedObject
if (_serializedClasses.TryGetValue(o, out context))
return (context, false);
ScriptGlue.Serialization_CreateObject(_internalContext, o.GetType().ToString(), out IntPtr contextPtr, out UUID id);
ScriptGlue.Serialization_CreateObject(_internalContext, false, o.GetType().ToString(), out IntPtr contextPtr, out UUID id);
context = new SerializedObject(contextPtr, id, _serializedClasses);
context = new SerializedObject(contextPtr, false, id, _serializedClasses);
_serializedClasses.Add(o, context);
@@ -95,16 +100,21 @@ public class SerializedObject
public void Serialize(Entity entity)
{
SerializeFields(entity);
SerializeInstanceFields(entity);
}
public void Serialize(Type type)
{
SerializeStaticTypeFields(type);
}
public void SerializeFields(object obj)
public void SerializeInstanceFields(object obj)
{
Type type = obj.GetType();
foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
{
if (!EntitySerializer.SerializeField(field))
if (!EntitySerializer.SerializeField(field, false))
continue;
object? fieldValue = field.GetValue(obj);
@@ -113,6 +123,20 @@ public class SerializedObject
SerializeField(field.Name, fieldValue, fieldType);
}
}
public void SerializeStaticTypeFields(Type type)
{
foreach (FieldInfo field in type.GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public))
{
if (!EntitySerializer.SerializeField(field, true))
continue;
object? fieldValue = field.GetValue(null);
Type fieldType = fieldValue?.GetType() ?? field.FieldType;
SerializeField(field.Name, fieldValue, fieldType);
}
}
private bool TryCustomSerializer(string fieldName, object? fieldValue, Type fieldType)
{
@@ -285,7 +309,7 @@ public class SerializedObject
{
PushScope(fieldName);
SerializeFields(fieldValue);
SerializeInstanceFields(fieldValue);
PopScope();
}
@@ -312,7 +336,7 @@ public class SerializedObject
if (newContext)
{
context.SerializeFields(fieldValue);
context.SerializeInstanceFields(fieldValue);
}
AddField(fieldName, SerializationType.ObjectReference, context._id);