2026-07-18 23:41:28 +08:00
|
|
|
using Microsoft.CodeAnalysis;
|
|
|
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
|
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
|
|
|
using System.Collections.Immutable;
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
|
|
|
|
namespace OECS.SourceGen;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Incremental source generator that discovers all component types used with
|
|
|
|
|
/// <see cref="OECS.World"/> and generates a strongly-typed registry so that
|
|
|
|
|
/// <see cref="OECS.WorldSerializer"/> can save/load without reflection.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[Generator]
|
|
|
|
|
public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
|
|
|
|
{
|
|
|
|
|
private static readonly HashSet<string> _componentApiMethods = new()
|
|
|
|
|
{
|
|
|
|
|
// Component management
|
|
|
|
|
"AddComponent",
|
|
|
|
|
"RemoveComponent",
|
|
|
|
|
"HasComponent",
|
|
|
|
|
"GetComponent",
|
|
|
|
|
"TryGetComponent",
|
|
|
|
|
"ReadComponent",
|
|
|
|
|
"MarkModified",
|
|
|
|
|
|
|
|
|
|
// Singletons
|
|
|
|
|
"SetSingleton",
|
|
|
|
|
"GetSingleton",
|
|
|
|
|
"ReadSingleton",
|
|
|
|
|
"HasSingleton",
|
|
|
|
|
"RemoveSingleton",
|
|
|
|
|
|
|
|
|
|
// Observability
|
|
|
|
|
"ObserveComponentChanges",
|
|
|
|
|
|
|
|
|
|
// Query building
|
|
|
|
|
"With",
|
|
|
|
|
"Without",
|
2026-07-20 17:05:17 +08:00
|
|
|
|
|
|
|
|
// Iteration and relationships
|
|
|
|
|
"Select",
|
|
|
|
|
"GetSources",
|
2026-07-18 23:41:28 +08:00
|
|
|
};
|
|
|
|
|
|
2026-07-21 09:15:27 +08:00
|
|
|
private static readonly HashSet<string> _singletonMethods = new()
|
|
|
|
|
{
|
|
|
|
|
"SetSingleton",
|
|
|
|
|
"GetSingleton",
|
|
|
|
|
"ReadSingleton",
|
|
|
|
|
"HasSingleton",
|
|
|
|
|
"RemoveSingleton",
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-22 16:14:58 +08:00
|
|
|
|
2026-07-18 23:41:28 +08:00
|
|
|
|
|
|
|
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
|
|
|
{
|
|
|
|
|
// Collect all generic method invocations that reference component types.
|
|
|
|
|
var invocations = context.SyntaxProvider
|
|
|
|
|
.CreateSyntaxProvider(
|
|
|
|
|
predicate: IsCandidateInvocation,
|
|
|
|
|
transform: ExtractComponentType)
|
|
|
|
|
.Where(t => t.FullyQualifiedName != null)
|
2026-07-21 09:15:27 +08:00
|
|
|
.Select((t, _) => (t.FullyQualifiedName!, t.AssemblyQualifiedName!, t.IsRelationship, t.IsSingleton));
|
2026-07-18 23:41:28 +08:00
|
|
|
|
2026-07-22 16:14:58 +08:00
|
|
|
var allTypes = invocations.Collect();
|
2026-07-18 23:41:28 +08:00
|
|
|
|
|
|
|
|
context.RegisterSourceOutput(allTypes, GenerateRegistry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsCandidateInvocation(SyntaxNode node, CancellationToken _)
|
|
|
|
|
{
|
|
|
|
|
if (node is not InvocationExpressionSyntax invocation)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
|
|
|
|
|
{
|
|
|
|
|
if (memberAccess.Name is GenericNameSyntax genericName)
|
|
|
|
|
{
|
|
|
|
|
return _componentApiMethods.Contains(genericName.Identifier.Text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 09:15:27 +08:00
|
|
|
private static (string? FullyQualifiedName, string? AssemblyQualifiedName, bool IsRelationship, bool IsSingleton) ExtractComponentType(
|
2026-07-18 23:41:28 +08:00
|
|
|
GeneratorSyntaxContext ctx, CancellationToken _)
|
|
|
|
|
{
|
|
|
|
|
if (ctx.Node is not InvocationExpressionSyntax invocation)
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
if (memberAccess.Name is not GenericNameSyntax genericName)
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
var typeArg = genericName.TypeArgumentList.Arguments.FirstOrDefault();
|
|
|
|
|
if (typeArg == null)
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
var symbolInfo = ctx.SemanticModel.GetSymbolInfo(typeArg);
|
|
|
|
|
if (symbolInfo.Symbol is not INamedTypeSymbol typeSymbol)
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
// Only public struct types are valid components.
|
|
|
|
|
if (!typeSymbol.IsValueType || typeSymbol.IsAbstract)
|
|
|
|
|
return default;
|
|
|
|
|
if (typeSymbol.DeclaredAccessibility != Accessibility.Public)
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
|
|
|
|
|
|
|
|
|
// Assembly-qualified name for stable serialization (without assembly version).
|
|
|
|
|
var aqn = typeSymbol.ContainingAssembly != null
|
|
|
|
|
? $"{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}, {typeSymbol.ContainingAssembly.Name}"
|
|
|
|
|
: fqn;
|
|
|
|
|
|
2026-07-20 17:05:17 +08:00
|
|
|
bool isRelationship = typeSymbol.AllInterfaces.Any(i =>
|
|
|
|
|
i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::OECS.IRelationship");
|
|
|
|
|
|
2026-07-21 09:15:27 +08:00
|
|
|
bool isSingleton = _singletonMethods.Contains(genericName.Identifier.Text);
|
|
|
|
|
|
|
|
|
|
return (fqn, aqn, isRelationship, isSingleton);
|
2026-07-18 23:41:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static void GenerateRegistry(SourceProductionContext context,
|
2026-07-22 16:14:58 +08:00
|
|
|
ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName, bool IsRelationship, bool IsSingleton)> invocations)
|
2026-07-18 23:41:28 +08:00
|
|
|
{
|
|
|
|
|
|
|
|
|
|
// Collect unique types by fully-qualified name, deduplicating.
|
2026-07-21 09:15:27 +08:00
|
|
|
var typeMap = new Dictionary<string, (string Fqn, string Aqn, bool IsRel, bool IsSingleton)>();
|
2026-07-18 23:41:28 +08:00
|
|
|
foreach (var t in invocations)
|
2026-07-21 09:15:27 +08:00
|
|
|
{
|
|
|
|
|
if (typeMap.TryGetValue(t.FullyQualifiedName, out var existing))
|
|
|
|
|
typeMap[t.FullyQualifiedName] = (existing.Fqn, existing.Aqn, existing.IsRel || t.IsRelationship, existing.IsSingleton || t.IsSingleton);
|
|
|
|
|
else
|
|
|
|
|
typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName, t.IsRelationship, t.IsSingleton);
|
|
|
|
|
}
|
2026-07-18 23:41:28 +08:00
|
|
|
|
|
|
|
|
if (typeMap.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
sb.AppendLine("// <auto-generated/>");
|
|
|
|
|
sb.AppendLine("using System.Runtime.CompilerServices;");
|
|
|
|
|
sb.AppendLine("using MessagePack;");
|
|
|
|
|
sb.AppendLine();
|
|
|
|
|
sb.AppendLine("namespace OECS");
|
|
|
|
|
sb.AppendLine("{");
|
|
|
|
|
sb.AppendLine(" file static class ComponentRegistryInitializer");
|
|
|
|
|
sb.AppendLine(" {");
|
|
|
|
|
sb.AppendLine(" [ModuleInitializer]");
|
|
|
|
|
sb.AppendLine(" public static void Initialize()");
|
|
|
|
|
sb.AppendLine(" {");
|
2026-07-20 17:05:17 +08:00
|
|
|
sb.AppendLine(" ComponentRegistry.Register(new ComponentDescriptor[]");
|
2026-07-18 23:41:28 +08:00
|
|
|
sb.AppendLine(" {");
|
|
|
|
|
|
|
|
|
|
bool first = true;
|
2026-07-21 09:15:27 +08:00
|
|
|
foreach (var (fqn, aqn, isRel, isSingleton) in typeMap.Values.OrderBy(t => t.Fqn))
|
2026-07-18 23:41:28 +08:00
|
|
|
{
|
|
|
|
|
if (!first)
|
|
|
|
|
sb.AppendLine(",");
|
|
|
|
|
first = false;
|
|
|
|
|
|
|
|
|
|
sb.AppendLine($" new ComponentDescriptor(");
|
|
|
|
|
sb.AppendLine($" typeName: \"{aqn}\",");
|
|
|
|
|
sb.AppendLine($" type: typeof({fqn}),");
|
|
|
|
|
sb.AppendLine($" serialize: obj => MessagePackSerializer.Serialize(({fqn})obj),");
|
2026-07-18 23:44:10 +08:00
|
|
|
sb.AppendLine($" deserializeAndAdd: (world, entity, data) =>");
|
2026-07-20 17:05:17 +08:00
|
|
|
sb.AppendLine($" world.AddComponent(entity, MessagePackSerializer.Deserialize<{fqn}>(data)),");
|
2026-07-21 09:15:27 +08:00
|
|
|
sb.AppendLine($" deserialize: data => MessagePackSerializer.Deserialize<{fqn}>(data),");
|
|
|
|
|
sb.AppendLine($" isSingleton: {(isSingleton ? "true" : "false")}");
|
2026-07-20 17:05:17 +08:00
|
|
|
|
2026-07-18 23:41:28 +08:00
|
|
|
sb.Append(" )");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sb.AppendLine();
|
|
|
|
|
sb.AppendLine(" });");
|
|
|
|
|
sb.AppendLine(" }");
|
|
|
|
|
sb.AppendLine(" }");
|
|
|
|
|
sb.AppendLine("}");
|
|
|
|
|
|
|
|
|
|
context.AddSource("ComponentRegistry.g.cs", sb.ToString());
|
|
|
|
|
}
|
2026-07-20 17:05:17 +08:00
|
|
|
}
|