You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

256 lines
9.5 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace gaemstone.SourceGen.Generators;
[Generator]
public partial class AutoRegisterComponentsGenerator
: ISourceGenerator
{
private static readonly DiagnosticDescriptor ComponentNotPartOfModule = new(
"gaem0101", "Components must be part of a module",
"Type {0} isn't defined as part of a [Module]",
nameof(AutoRegisterComponentsGenerator), DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor ComponentMultipleTypes = new(
"gaem0102", "Components may not have multiple component types",
"Type {0} is marked with multiple component types ({1})",
nameof(AutoRegisterComponentsGenerator), DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor ComponentRelationInvalidType = new(
"gaem0103", "Relations may only be marked with [Component] or [Tag]",
"Type {0} marked as [Relation] may not be marked with [{1}], only [Component] or [Tag] are valid",
nameof(AutoRegisterComponentsGenerator), DiagnosticSeverity.Error, true);
public void Initialize(GeneratorInitializationContext context)
=> context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
private class SyntaxReceiver
: ISyntaxContextReceiver
{
public Dictionary<INamedTypeSymbol, HashSet<INamedTypeSymbol>> Modules { get; }
= new(SymbolEqualityComparer.Default);
public HashSet<INamedTypeSymbol> ComponentsNotInModule { get; }
= new(SymbolEqualityComparer.Default);
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is not AttributeSyntax attrNode) return;
var model = context.SemanticModel;
var attrType = model.GetTypeInfo(attrNode).Type!;
if ((attrType.GetNamespace() != "gaemstone.ECS") || !attrType.Name.EndsWith("Attribute")) return;
var attrName = attrType.Name.Substring(0, attrType.Name.Length - "Attribute".Length);
if (!Enum.TryParse<RegisterType>(attrName, out _)) return;
var symbol = (model.GetDeclaredSymbol(attrNode.Parent?.Parent!) as INamedTypeSymbol)!;
if ((symbol.ContainingSymbol is INamedTypeSymbol module)
&& module.HasAttribute("gaemstone.ECS.ModuleAttribute"))
{
if (!Modules.TryGetValue(module, out var components))
Modules.Add(module, components = new(SymbolEqualityComparer.Default));
components.Add(symbol);
}
else ComponentsNotInModule.Add(symbol);
}
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxContextReceiver is not SyntaxReceiver receiver) return;
foreach (var symbol in receiver.ComponentsNotInModule)
context.ReportDiagnostic(Diagnostic.Create(ComponentNotPartOfModule,
symbol.Locations.FirstOrDefault(), symbol.GetFullName()));
var modules = new Dictionary<INamedTypeSymbol, ModuleInfo>(
SymbolEqualityComparer.Default);
foreach (var pair in receiver.Modules) {
var moduleSymbol = pair.Key;
if (!modules.TryGetValue(moduleSymbol, out var module))
modules.Add(moduleSymbol, module = new(moduleSymbol));
foreach (var symbol in pair.Value) {
var componentTypes = symbol.GetAttributes()
.Where (attr => (attr.AttributeClass?.GetNamespace() == "gaemstone.ECS"))
.Select(attr => attr.AttributeClass!.Name)
.Where (name => name.EndsWith("Attribute"))
.Select(name => name.Substring(0, name.Length - "Attribute".Length))
.SelectMany(name => Enum.TryParse<RegisterType>(name, out var type)
? new[] { type } : Enumerable.Empty<RegisterType>())
.ToList();
if (componentTypes.Count == 0) continue;
var isRelation = componentTypes.Contains(RegisterType.Relation);
if (isRelation && (componentTypes.Count == 2)) {
var other = componentTypes.Where(type => (type != RegisterType.Relation)).Single();
if (other is RegisterType.Component or RegisterType.Tag)
componentTypes.Remove(RegisterType.Relation);
else context.ReportDiagnostic(Diagnostic.Create(ComponentRelationInvalidType,
symbol.Locations.FirstOrDefault(), symbol.GetFullName(), other.ToString()));
}
if (componentTypes.Count >= 2)
context.ReportDiagnostic(Diagnostic.Create(ComponentMultipleTypes,
symbol.Locations.FirstOrDefault(), symbol.GetFullName(),
string.Join(", ", componentTypes.Select(s => $"[{s}]"))));
var componentType = componentTypes[0];
var addedEntities = new List<ITypeSymbol>();
var addedRelations = new List<(ITypeSymbol, ITypeSymbol)>();
foreach (var attr in symbol.GetAttributes())
for (var type = attr.AttributeClass; type != null; type = type.BaseType)
switch (type.GetFullName(true)) {
case "gaemstone.ECS.AddAttribute`1": addedEntities.Add(type.TypeArguments[0]); break;
case "gaemstone.ECS.AddAttribute`2": addedRelations.Add((type.TypeArguments[0], type.TypeArguments[1])); break;
}
module.Components.Add(new(symbol, module, componentType,
addedEntities, addedRelations));
}
}
foreach (var module in modules.Values) {
var sb = new StringBuilder();
sb.AppendLine($$"""
// <auto-generated/>
using gaemstone.ECS;
namespace {{module.Namespace}};
""");
if (module.IsStatic) {
sb.AppendLine($$"""
public static partial class {{ module.Name }}
{
public static void RegisterComponents(World world)
{
var module = world.LookupByPathOrThrow({{ module.Path.ToStringLiteral() }});
""");
foreach (var c in module.Components) {
var @var = $"entity{c.Name}";
var path = (c.Path ?? c.Name).ToStringLiteral();
sb.AppendLine($$""" var {{ @var }} = world.LookupByPathOrThrow(module, {{ path }})""");
if (c.IsRelation) sb.AppendLine($$""" .Add<gaemstone.Doc.Relation>()""");
sb.AppendLine($$""" .CreateLookup<{{ c.Name }}>()""");
sb.Insert(sb.Length - 1, ";");
}
} else {
sb.AppendLine($$"""
public partial class {{module.Name}}
: IModuleAutoRegisterComponents
{
public void RegisterComponents(EntityRef module)
{
var world = module.World;
""");
foreach (var c in module.Components) {
var @var = $"entity{c.Name}";
var path = (c.Path ?? c.Name).ToStringLiteral();
sb.AppendLine($$""" var {{ @var }} = world.New(module, {{ path }})""");
if (c.IsSymbol) sb.AppendLine($$""" .Symbol("{{ c.Name }}")""");
if (c.IsRelation) sb.AppendLine($$""" .Add<gaemstone.Doc.Relation>()""");
if (c.IsTag) sb.AppendLine($$""" .Add<gaemstone.Flecs.Core.Tag>()""");
if (c.IsComponent) sb.AppendLine($$""" .Build().InitComponent<{{ c.Name }}>()""");
else sb.AppendLine($$""" .Build().CreateLookup<{{ c.Name }}>()""");
if (c.SingletonAddSelf) sb.AppendLine($$""" .Add<{{ c.Name }}>()""");
sb.Insert(sb.Length - 1, ";");
}
foreach (var c in module.Components) {
var @var = $"entity{c.Name}";
foreach (var e in c.EntitiesToAdd)
sb.AppendLine($$""" {{ @var }}.Add<{{ e.GetFullName() }}>();""");
foreach (var (r, t) in c.RelationsToAdd)
sb.AppendLine($$""" {{ @var }}.Add<{{ r.GetFullName() }}, {{ t.GetFullName() }}>();""");
}
}
sb.AppendLine($$"""
}
}
""");
context.AddSource($"{module.FullName}_Components.g.cs", sb.ToString());
}
}
private class ModuleInfo
{
public INamedTypeSymbol Symbol { get; }
public string Name => Symbol.Name;
public string FullName => Symbol.GetFullName();
public string Namespace => Symbol.GetNamespace()!;
public List<ComponentInfo> Components { get; } = new();
public string? Path { get; }
public bool IsStatic => Symbol.IsStatic;
public ModuleInfo(INamedTypeSymbol symbol)
{
Symbol = symbol;
Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value as string;
}
}
private class ComponentInfo
{
public INamedTypeSymbol Symbol { get; }
public string Name => Symbol.Name;
public ModuleInfo Module { get; }
public RegisterType Type { get; }
public List<ITypeSymbol> EntitiesToAdd { get; }
public List<(ITypeSymbol, ITypeSymbol)> RelationsToAdd { get; }
public string? Path { get; }
public bool IsSymbol { get; } // TODO: Get rid of this.
public bool IsRelation { get; }
public bool IsTag => (Type is RegisterType.Tag);
public bool IsComponent => (Type is RegisterType.Component
or RegisterType.Singleton);
public bool IsSingleton => (Type is RegisterType.Singleton);
public bool SingletonAddSelf { get; }
public ComponentInfo(INamedTypeSymbol symbol, ModuleInfo module, RegisterType type,
List<ITypeSymbol> entitiesToAdd, List<(ITypeSymbol, ITypeSymbol)> relationsToAdd)
{
Symbol = symbol;
Module = module;
Type = type;
EntitiesToAdd = entitiesToAdd;
RelationsToAdd = relationsToAdd;
Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value as string;
IsSymbol = symbol.HasAttribute("gaemstone.ECS.SymbolAttribute");
IsRelation = symbol.HasAttribute("gaemstone.ECS.RelationAttribute");
SingletonAddSelf = IsSingleton
&& (symbol.GetAttribute("gaemstone.ECS.SingletonAttribute")!
.NamedArguments.FirstOrDefault() is not ("AutoAdd", TypedConstant typedConst)
|| (bool)typedConst.Value!);
}
}
private enum RegisterType
{
Entity,
Singleton,
Relation,
Component,
Tag,
}
}