using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Immutable; using System.Text; namespace OECS.SourceGen; /// /// Incremental source generator that discovers all component types used with /// and generates a strongly-typed registry so that /// can save/load without reflection. /// [Generator] public class ComponentDiscoveryGenerator : IIncrementalGenerator { private static readonly HashSet _componentApiMethods = new() { // Component management "AddComponent", "RemoveComponent", "HasComponent", "GetComponent", "TryGetComponent", "ReadComponent", "MarkModified", // Singletons "SetSingleton", "GetSingleton", "ReadSingleton", "HasSingleton", "RemoveSingleton", // Observability "ObserveComponentChanges", // Query building "With", "Without", // Iteration and relationships "Select", "GetSources", }; private static readonly HashSet _singletonMethods = new() { "SetSingleton", "GetSingleton", "ReadSingleton", "HasSingleton", "RemoveSingleton", }; 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) .Select((t, _) => (t.FullyQualifiedName!, t.AssemblyQualifiedName!, t.IsRelationship, t.IsSingleton)); var allTypes = invocations.Collect(); 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; } private static (string? FullyQualifiedName, string? AssemblyQualifiedName, bool IsRelationship, bool IsSingleton) ExtractComponentType( 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; bool isRelationship = typeSymbol.AllInterfaces.Any(i => i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::OECS.IRelationship"); bool isSingleton = _singletonMethods.Contains(genericName.Identifier.Text); return (fqn, aqn, isRelationship, isSingleton); } private static void GenerateRegistry(SourceProductionContext context, ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName, bool IsRelationship, bool IsSingleton)> invocations) { // Collect unique types by fully-qualified name, deduplicating. var typeMap = new Dictionary(); foreach (var t in invocations) { 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); } if (typeMap.Count == 0) return; var sb = new StringBuilder(); sb.AppendLine("// "); 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(" {"); sb.AppendLine(" ComponentRegistry.Register(new ComponentDescriptor[]"); sb.AppendLine(" {"); bool first = true; foreach (var (fqn, aqn, isRel, isSingleton) in typeMap.Values.OrderBy(t => t.Fqn)) { 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),"); sb.AppendLine($" deserializeAndAdd: (world, entity, data) =>"); sb.AppendLine($" world.AddComponent(entity, MessagePackSerializer.Deserialize<{fqn}>(data)),"); sb.AppendLine($" deserialize: data => MessagePackSerializer.Deserialize<{fqn}>(data),"); sb.AppendLine($" isSingleton: {(isSingleton ? "true" : "false")}"); sb.Append(" )"); } sb.AppendLine(); sb.AppendLine(" });"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine("}"); context.AddSource("ComponentRegistry.g.cs", sb.ToString()); } }