forked from Misaki/GhostEngine
- Updated Misaki.HighPerformance package versions in Core and Graphics projects. - Added IsTrimmable to Ghost.Engine.csproj for trimming support. - Renamed GetOrRegisterComponent to GetOrRegisterComponentID and updated all usages. - Component registration codegen now uses a static class with [ModuleInitializer], no longer requires [EngineEntry]. - Improved JSON serialization: added string support, introduced Utf8JsonObjectScope/ArrayScope, and new extension methods for cleaner JSON writing. - Removed [SkipLocalsInit] from Hierarchy and LocalToWorld. - Fixed Entity.Invalid to use INVALID_ID for both fields. - Minor cleanup: clarified comments, reorganized Ghost.Generator in solution, and disabled component serialization generator.
230 lines
10 KiB
C#
230 lines
10 KiB
C#
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Text;
|
|
|
|
namespace Ghost.Generator
|
|
{
|
|
[Generator]
|
|
public class ComponentSerializationGenerator : IIncrementalGenerator
|
|
{
|
|
private string GetJsonWriteCall(ITypeSymbol type, string fieldName)
|
|
{
|
|
switch (type.SpecialType)
|
|
{
|
|
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}]);";
|
|
case SpecialType.System_String:
|
|
return $@"writer.WriteString(""{fieldName}"", value.{fieldName});";
|
|
}
|
|
|
|
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()";
|
|
// Note: the size of IntPtr and UIntPtr varies by platform, we use Int64/UInt64 to ensure compatibility
|
|
case SpecialType.System_IntPtr: return "reader.GetInt64()";
|
|
case SpecialType.System_UIntPtr: return "reader.GetUInt64()";
|
|
case SpecialType.System_Boolean: return "reader.GetBoolean()";
|
|
case SpecialType.System_Char: return "reader.GetString()[0]";
|
|
case SpecialType.System_String: return "reader.GetString()";
|
|
}
|
|
|
|
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)
|
|
{
|
|
// 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;");
|
|
}
|
|
}
|
|
|
|
// 2. The Main Template using $@
|
|
// Watch the double braces {{ }} and double quotes "" ""
|
|
var sourceCode = $@"// <auto-generated/>
|
|
#nullable enable
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
{
|
|
return;
|
|
|
|
var componentCandidates = context.SyntaxProvider
|
|
.CreateSyntaxProvider(
|
|
predicate: (syntaxNode, _) => syntaxNode is StructDeclarationSyntax,
|
|
transform: (ctx, _) =>
|
|
{
|
|
var structSyntax = (StructDeclarationSyntax)ctx.Node;
|
|
|
|
if (!(ctx.SemanticModel.GetDeclaredSymbol(structSyntax) is INamedTypeSymbol symbol))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var compilation = ctx.SemanticModel.Compilation;
|
|
var iComponentSymbol = compilation.GetTypeByMetadataName("Ghost.Entities.IComponent");
|
|
|
|
if (iComponentSymbol == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (var iface in symbol.AllInterfaces)
|
|
{
|
|
if (SymbolEqualityComparer.Default.Equals(iface, iComponentSymbol))
|
|
{
|
|
return symbol;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
})
|
|
.Where(symbol => symbol != null)
|
|
.Collect();
|
|
|
|
context.RegisterSourceOutput(componentCandidates, GenerateJsonSerializer);
|
|
}
|
|
}
|
|
}
|