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.
261 lines
9.8 KiB
261 lines
9.8 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; |
|
|
|
// TODO: "Jump to Definition" feature to open the file + line in which a module / component / system is defined. |
|
|
|
[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.IsPublic) 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 IsPublic { get; } |
|
public bool IsStatic => Symbol.IsStatic; |
|
|
|
public ModuleInfo(INamedTypeSymbol symbol) |
|
{ |
|
Symbol = symbol; |
|
Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")? |
|
.ConstructorArguments.FirstOrDefault().Value as string; |
|
IsPublic = symbol.HasAttribute("gaemstone.ECS.PublicAttribute"); |
|
} |
|
} |
|
|
|
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 IsPublic { get; } |
|
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; |
|
IsPublic = symbol.HasAttribute("gaemstone.ECS.PublicAttribute") |
|
|| (Module.IsPublic && !symbol.HasAttribute("gaemstone.ECS.PrivateAttribute")); |
|
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, |
|
} |
|
}
|
|
|