ECS refactor: new ComponentSet, serialization, generators
Major ECS API overhaul: added ComponentSet, refactored ComponentRegistry, and updated all entity/component creation methods. Introduced robust custom serialization infrastructure and per-component source generators for registration and (de)serialization. Updated editor, engine, and test code to use new APIs. Improved code quality, naming, and performance throughout. Removed obsolete code and updated dependencies.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -10,67 +9,200 @@ namespace Ghost.Generator
|
||||
[Generator]
|
||||
public class ComponentSerializationGenerator : IIncrementalGenerator
|
||||
{
|
||||
private void GenerateJsonContext(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> symbols)
|
||||
private string GetJsonWriteCall(ITypeSymbol type, string fieldName)
|
||||
{
|
||||
if (symbols.IsDefaultOrEmpty)
|
||||
// 1. PRIMITIVES (Fastest)
|
||||
switch (type.SpecialType)
|
||||
{
|
||||
return;
|
||||
case SpecialType.System_Byte:
|
||||
case SpecialType.System_SByte:
|
||||
case SpecialType.System_Int16:
|
||||
case SpecialType.System_Int32:
|
||||
case SpecialType.System_Int64:
|
||||
case SpecialType.System_Single:
|
||||
case SpecialType.System_Double:
|
||||
case SpecialType.System_Decimal:
|
||||
case SpecialType.System_UInt64:
|
||||
case SpecialType.System_UInt16:
|
||||
case SpecialType.System_UInt32:
|
||||
case SpecialType.System_IntPtr:
|
||||
case SpecialType.System_UIntPtr:
|
||||
return $@"writer.WriteNumber(""{fieldName}"", value.{fieldName});";
|
||||
case SpecialType.System_Boolean:
|
||||
return $@"writer.WriteBoolean(""{fieldName}"", value.{fieldName});";
|
||||
case SpecialType.System_Char:
|
||||
return $@"writer.WriteString(""{fieldName}"", [value.{fieldName}]);";
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(@"
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Ghost.Engine.Components.Serialization
|
||||
{
|
||||
[JsonSourceGenerationOptions(WriteIndented = true, IncludeFields = true)]");
|
||||
|
||||
foreach (var symbol in symbols.Distinct(SymbolEqualityComparer.Default))
|
||||
// TODO: Add well-known types like float3, Vector3, etc.
|
||||
#if false
|
||||
// 2. KNOWN MATH TYPES (Optimized Arrays)
|
||||
// Checking by name is simple and effective for standard math libs
|
||||
if (type.Name == "float3" || type.Name == "Vector3")
|
||||
{
|
||||
if (symbol is null)
|
||||
return $@"writer.WritePropertyName(""{fieldName}"");
|
||||
writer.WriteStartArray();
|
||||
writer.WriteNumberValue(value.{fieldName}.x);
|
||||
writer.WriteNumberValue(value.{fieldName}.y);
|
||||
writer.WriteNumberValue(value.{fieldName}.z);
|
||||
writer.WriteEndArray();";
|
||||
}
|
||||
#endif
|
||||
|
||||
// 3. FALLBACK: System.Text.Json (Reflection)
|
||||
// If we don't know what it is, let the standard serializer handle it.
|
||||
// This handles float4x4, List<T>, and other nested structs automatically.
|
||||
return $@"writer.WritePropertyName(""{fieldName}""); global::System.Text.Json.JsonSerializer.Serialize(writer, value.{fieldName}, options);";
|
||||
}
|
||||
|
||||
private string GetJsonReadCall(ITypeSymbol type)
|
||||
{
|
||||
switch (type.SpecialType)
|
||||
{
|
||||
case SpecialType.System_Byte: return "reader.GetByte()";
|
||||
case SpecialType.System_SByte: return "reader.GetSByte()";
|
||||
case SpecialType.System_Int16: return "reader.GetInt16()";
|
||||
case SpecialType.System_Int32: return "reader.GetInt32()";
|
||||
case SpecialType.System_Int64: return "reader.GetInt64()";
|
||||
case SpecialType.System_Single: return "reader.GetSingle()";
|
||||
case SpecialType.System_Double: return "reader.GetDouble()";
|
||||
case SpecialType.System_Decimal: return "reader.GetDecimal()";
|
||||
case SpecialType.System_UInt16: return "reader.GetUInt16()";
|
||||
case SpecialType.System_UInt32: return "reader.GetUInt32()";
|
||||
case SpecialType.System_UInt64: return "reader.GetUInt64()";
|
||||
case SpecialType.System_IntPtr: return "reader.GetInt64()"; // Note: IntPtr size varies by platform
|
||||
case SpecialType.System_UIntPtr: return "reader.GetUInt64()"; // Note: UIntPtr size varies by platform
|
||||
case SpecialType.System_Boolean: return "reader.GetBoolean()";
|
||||
case SpecialType.System_Char: return "reader.GetString()[0]";
|
||||
}
|
||||
|
||||
#if false
|
||||
// For custom math types, you'd need to generate code to read the array back
|
||||
if (type.Name == "float3") return "new float3(reader.GetSingle(), reader.GetSingle(), reader.GetSingle())"; // Simplified
|
||||
#endif
|
||||
|
||||
return $"global::System.Text.Json.JsonSerializer.Deserialize<{type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(ref reader, options)";
|
||||
}
|
||||
|
||||
private void GenerateJsonSerializer(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> symbols)
|
||||
{
|
||||
var sbWrites = new StringBuilder();
|
||||
var sbReads = new StringBuilder();
|
||||
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
var namespaceName = symbol.ContainingNamespace.ToDisplayString();
|
||||
var structName = symbol.Name;
|
||||
var fullTypeName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
|
||||
// 1. Build Field Logic (Same as before)
|
||||
sbWrites.Clear();
|
||||
sbReads.Clear();
|
||||
|
||||
var fields = symbol.GetMembers()
|
||||
.OfType<IFieldSymbol>()
|
||||
.Where(f => !f.IsStatic && f.DeclaredAccessibility == Accessibility.Public);
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
continue;
|
||||
// Note: GetJsonWriteCall returns a string ending with ";"
|
||||
var writeCall = GetJsonWriteCall(field.Type, field.Name);
|
||||
if (writeCall != null)
|
||||
{
|
||||
sbWrites.Append(" "); // Indentation
|
||||
sbWrites.AppendLine(writeCall);
|
||||
}
|
||||
|
||||
var readCall = GetJsonReadCall(field.Type);
|
||||
if (readCall != null)
|
||||
{
|
||||
// Note the double quotes ""{field.Name}"" for the case string
|
||||
sbReads.Append(" "); // Indentation
|
||||
sbReads.AppendLine($@"case ""{field.Name}"": result.{field.Name} = {readCall}; break;");
|
||||
}
|
||||
}
|
||||
|
||||
var fqtn = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
sb.Append($@"
|
||||
[JsonSerializable(typeof({fqtn}))]");
|
||||
// 2. The Main Template using $@
|
||||
// Watch the double braces {{ }} and double quotes "" ""
|
||||
var sourceCode = $@"// <auto-generated/>
|
||||
|
||||
namespace {namespaceName}
|
||||
{{
|
||||
public unsafe static class {structName}_Serializer
|
||||
{{
|
||||
[global::System.Runtime.CompilerServices.ModuleInitializer]
|
||||
internal static void Init()
|
||||
{{
|
||||
var id = Ghost.Entities.ComponentTypeID<{fullTypeName}>.Value;
|
||||
Ghost.Engine.IO.ComponentSerializerRegistry.Register(id, SerializeBinaryUnsafe, SerializeJsonUnsafe);
|
||||
}}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// BINARY (Fast Path)
|
||||
// ---------------------------------------------------------
|
||||
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe void SerializeBinaryUnsafe(global::System.IO.BinaryWriter writer, void* value)
|
||||
{{
|
||||
writer.Write(new global::System.ReadOnlySpan<byte>(value, sizeof({fullTypeName})));
|
||||
}}
|
||||
|
||||
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static void SerializeBinary(this global::System.IO.BinaryWriter writer, ref {fullTypeName} value)
|
||||
{{
|
||||
unsafe {{ writer.Write(new global::System.ReadOnlySpan<byte>(global::System.Runtime.CompilerServices.Unsafe.AsPointer(ref value), sizeof({fullTypeName}))); }}
|
||||
}}
|
||||
|
||||
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static void DeserializeBinary(this global::System.IO.BinaryReader reader, ref {fullTypeName} value)
|
||||
{{
|
||||
unsafe {{ reader.Read(new global::System.Span<byte>(global::System.Runtime.CompilerServices.Unsafe.AsPointer(ref value), sizeof({fullTypeName}))); }}
|
||||
}}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// JSON WRITE
|
||||
// ---------------------------------------------------------
|
||||
public static unsafe void SerializeJsonUnsafe(System.Text.Json.Utf8JsonWriter writer, void* ptr, System.Text.Json.JsonSerializerOptions? options)
|
||||
{{
|
||||
SerializeJson(writer, ref *( {fullTypeName}*)ptr, options);
|
||||
}}
|
||||
|
||||
public static void SerializeJson(this global::System.Text.Json.Utf8JsonWriter writer, ref {fullTypeName} value, global::System.Text.Json.JsonSerializerOptions? options)
|
||||
{{
|
||||
writer.WriteStartObject();
|
||||
{sbWrites}
|
||||
writer.WriteEndObject();
|
||||
}}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// JSON READ
|
||||
// ---------------------------------------------------------
|
||||
public static {fullTypeName} DeserializeJson(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Text.Json.JsonSerializerOptions? options)
|
||||
{{
|
||||
var result = default({fullTypeName});
|
||||
|
||||
if (reader.TokenType != global::System.Text.Json.JsonTokenType.StartObject) throw new global::System.Text.Json.JsonException();
|
||||
|
||||
while (reader.Read())
|
||||
{{
|
||||
if (reader.TokenType == global::System.Text.Json.JsonTokenType.EndObject) return result;
|
||||
if (reader.TokenType != global::System.Text.Json.JsonTokenType.PropertyName) throw new global::System.Text.Json.JsonException();
|
||||
|
||||
var propName = reader.GetString();
|
||||
reader.Read();
|
||||
|
||||
switch (propName)
|
||||
{{
|
||||
{sbReads}
|
||||
default: reader.Skip(); break;
|
||||
}}
|
||||
}}
|
||||
return result;
|
||||
}}
|
||||
}}
|
||||
}}";
|
||||
|
||||
context.AddSource($"{structName}.Serializer.gen.cs", sourceCode);
|
||||
}
|
||||
|
||||
sb.Append(@"
|
||||
public partial class ComponentJsonContext : JsonSerializerContext
|
||||
{
|
||||
private static readonly Dictionary<Type, JsonTypeInfo> _typeLookup = new()
|
||||
{");
|
||||
|
||||
foreach (var symbol in symbols.Distinct(SymbolEqualityComparer.Default))
|
||||
{
|
||||
if (symbol is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fqtn = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
sb.Append($@"
|
||||
{{ typeof({fqtn}), ComponentJsonContext.{symbol.Name} }},");
|
||||
}
|
||||
|
||||
sb.Append(@"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Tries to retrieve the generated JsonTypeInfo for a given component type.
|
||||
/// </summary>
|
||||
public static bool TryGetTypeInfo(Type componentType, out JsonTypeInfo jsonTypeInfo) =>
|
||||
_typeLookup.TryGetValue(componentType, out jsonTypeInfo);
|
||||
}
|
||||
}");
|
||||
|
||||
context.AddSource("ComponentJsonContext.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
|
||||
}
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
@@ -88,16 +220,16 @@ namespace Ghost.Engine.Components.Serialization
|
||||
}
|
||||
|
||||
var compilation = ctx.SemanticModel.Compilation;
|
||||
var iComponentDataSymbol = compilation.GetTypeByMetadataName("Ghost.Entities.Components.IComponentData");
|
||||
var iComponentSymbol = compilation.GetTypeByMetadataName("Ghost.Entities.IComponent");
|
||||
|
||||
if (iComponentDataSymbol == null)
|
||||
if (iComponentSymbol == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var iface in symbol.AllInterfaces)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(iface, iComponentDataSymbol))
|
||||
if (SymbolEqualityComparer.Default.Equals(iface, iComponentSymbol))
|
||||
{
|
||||
return symbol;
|
||||
}
|
||||
@@ -108,7 +240,7 @@ namespace Ghost.Engine.Components.Serialization
|
||||
.Where(symbol => symbol != null)
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(componentCandidates, GenerateJsonContext);
|
||||
context.RegisterSourceOutput(componentCandidates, GenerateJsonSerializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user