- Update to new gaemstone.ECS' new handling of generic context with World<T>, Entity<T>, etc - Completely rewrite the source generator - Remove AutoAdd from [Singleton] [Add<Flecs.Core.Disabled>] can be used instead - Replace some term attributes with generics For example, "[Has] T foo" is now "Has<T> foo" - Move some Flecs types into Flecs.Core - Remove IL generator related code - Remove [Game] attribute and entity - Turn Canvas and GameWindow into singletons - Probably some stuff I forgotwip/source-generators
parent
20d0cd2f0e
commit
8e9ff611d9
57 changed files with 2102 additions and 1899 deletions
@ -1 +1 @@ |
|||||||
Subproject commit 46e171940eea723f2ad8376b5056afd9b9d8a340 |
Subproject commit f7d17d46ab68ae91487d5d6275c3a501e1ee0980 |
@ -0,0 +1,220 @@ |
|||||||
|
using Microsoft.CodeAnalysis; |
||||||
|
|
||||||
|
|
||||||
|
namespace gaemstone.SourceGen; |
||||||
|
|
||||||
|
public static class Descriptors |
||||||
|
{ |
||||||
|
public const string DiagnosticCategory = "gaemstone.SourceGen"; |
||||||
|
|
||||||
|
// TODO: Replace this counter with proper hardcoded IDs. |
||||||
|
private static int _idCounter = 1; |
||||||
|
|
||||||
|
|
||||||
|
// Diagnostics relating to where the symbol occurs. |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ModuleMustNotBeNested = new( |
||||||
|
$"gSG{_idCounter++:00}", "Module must not be a nested class", |
||||||
|
"A [Module] must be defined as a top-level class.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor EntityMustBeInModule = new( |
||||||
|
$"gSG{_idCounter++:00}", "Entity must be part of a Module", |
||||||
|
"Entity type must be defined within a [Module].", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor InstanceMethodOnlyValidInSingleton = new( |
||||||
|
$"gSG{_idCounter++:00}", "Non-static method is only valid in Singleton Module", |
||||||
|
"Non-static [System] or [Observer] is only valid in a [Module] which is also marked as a [Singleton].", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ParamMustBeInMethod = new( |
||||||
|
$"gSG{_idCounter++:00}", "Parameter must be part of a System or Observer", |
||||||
|
"This parameter must be part of a method marked as [System] or [Observer].", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
|
||||||
|
// Diagnostics relating to the combined usage of attributes. |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor InvalidAttributeCombination = new( |
||||||
|
$"gSG{_idCounter++:00}", "Invalid combination of entity attributes", |
||||||
|
"The combination of entity attributes {0} is not valid.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ValidModuleAttributesHint = new( |
||||||
|
$"gSG{_idCounter++:00}", "Module may be a Singleton", |
||||||
|
"A [Module] may be marked as a [Singleton].", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Info, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ValidRelationAttributesHint = new( |
||||||
|
$"gSG{_idCounter++:00}", "Relation may be a Tag or Component", |
||||||
|
"A [Relation] may be marked as a [Tag] or [Component].", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Info, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor SingletonImpliesComponentHint = new( |
||||||
|
$"gSG{_idCounter++:00}", "Singleton implies Component", |
||||||
|
"A [Singleton] is already implied to be a [Component].", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Info, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor BuiltInModuleMustHavePath = new( |
||||||
|
$"gSG{_idCounter++:00}", "BuiltIn Module must have Path", |
||||||
|
"A [BuiltIn, Module] must also have a [Path] set.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Info, true); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Diagnostics relating to keywords / modifiers used with the symbol. |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ModuleMustBePartial = new( |
||||||
|
$"gSG{_idCounter++:00}", "ModuleMustBePartial", |
||||||
|
"A [Module] type must be marked as partial.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor TypeMustNotBeStatic = new( |
||||||
|
$"gSG{_idCounter++:00}", "Entity type must not be static", |
||||||
|
"Entity type must not be static.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor TypeMustNotBeAbstract = new( |
||||||
|
$"gSG{_idCounter++:00}", "Entity type must not be abstract", |
||||||
|
"Entity type must not be abstract.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor MethodMustNotBeAbstract = new( |
||||||
|
$"gSG{_idCounter++:00}", "System / Observer must not be abstract", |
||||||
|
"A [System] or [Observer] method must not be marked as abstract.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor MethodMustNotBeAsync = new( |
||||||
|
$"gSG{_idCounter++:00}", "System / Observer must not be async", |
||||||
|
"A [System] or [Observer] method must not be marked as async.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
// TODO: Check for any other weird modifiers we don't want. |
||||||
|
|
||||||
|
|
||||||
|
// Diagnostics relating to (generic) parameters on the attributes themselves. |
||||||
|
|
||||||
|
// TODO: Be more specific? |
||||||
|
public static readonly DiagnosticDescriptor InvalidTypeArgument = new( |
||||||
|
$"gSG{_idCounter++:00}", "Invalid type argument", |
||||||
|
"The specified type argument is not valid here.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
|
||||||
|
// Diagnostics relating to system / observer generic parameters. |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor MethodMustNotBeExtension = new( |
||||||
|
$"gSG{_idCounter++:00}", "System / Observer must not be extension method", |
||||||
|
"A [System] or [Observer] method must not be an extension method.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor MethodMustHaveParameters = new( |
||||||
|
$"gSG{_idCounter++:00}", "System / Observer must have parameters", |
||||||
|
"A [System] or [Observer] must have parameters.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor MethodMustHaveExpression = new( |
||||||
|
$"gSG{_idCounter++:00}", "Iterator-only System / Observer must have expression", |
||||||
|
"An Iterator-only [System] or [Observer] must have an [Expression] set.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor MethodGenericParamAtMostOne = new( |
||||||
|
$"gSG{_idCounter++:00}", "System / Observer must have at most one generic parameter", |
||||||
|
"A [System] or [Observer] must at most one generic parameter.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
// TODO: See if we can change this wording, but try to use the correct one. (Does "open generic" work?) |
||||||
|
public static readonly DiagnosticDescriptor MethodGenericParamMustNotBeSubstutited = new( |
||||||
|
$"gSG{_idCounter++:00}", "System / Observer generic parameter must not be substituted", |
||||||
|
"The generic parameter of a [System] or [Observer] must not be substituted.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor MethodGenericParamMustNotBeConstrained = new( |
||||||
|
$"gSG{_idCounter++:00}", "System / Observer generic parameter must not be substituted", |
||||||
|
"The generic parameter of a [System] or [Observer] must not be constrained.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
|
||||||
|
// Diagnostics relating to system / observer parameters. |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ParamMustNotBeArray = new( |
||||||
|
$"gSG{_idCounter++:00}", "Parameter must not be array", |
||||||
|
"Parameter must not be an array type.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ParamMustNotBePointer = new( |
||||||
|
$"gSG{_idCounter++:00}", "Parameter must not be pointer", |
||||||
|
"Parameter must not be a pointer type.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ParamMustNotBeGenericType = new( |
||||||
|
$"gSG{_idCounter++:00}", "Parameter must not be generic type", |
||||||
|
"Parameter must not be a generic type parameter.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ParamMustNotBePrimitive = new( |
||||||
|
$"gSG{_idCounter++:00}", "Parameter must not be primitive", |
||||||
|
"Parameter must not be a primitive type.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ParamMustNotBeGeneric = new( |
||||||
|
$"gSG{_idCounter++:00}", "Parameter must not be geric", |
||||||
|
"Parameter must not be generic (except Iterator, World, Entity, Nullable, Has, Not and Or).", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ParamDoesNotSupportOptional = new( |
||||||
|
$"gSG{_idCounter++:00}", "Parameter does not support optional", |
||||||
|
"Parameter does not support optional value syntax.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ParamByRefMustBeValueType = new( |
||||||
|
$"gSG{_idCounter++:00}", "ByRef parameter must be a value type", |
||||||
|
"ByRef (in/out/ref) parameter must be a value type (struct).", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor ParamByRefMustNotBeNullable = new( |
||||||
|
$"gSG{_idCounter++:00}", "ByRef parameter must not be nullable", |
||||||
|
"ByRef (in/out/ref) parameter must not be nullable.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor SpecialMustNotBeByRef = new( |
||||||
|
$"gSG{_idCounter++:00}", "Special parameter must not be ByRef", |
||||||
|
"Special parameter must not be ByRef (in/out/ref).", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor SpecialArgMustNotBeNullable = new( |
||||||
|
$"gSG{_idCounter++:00}", "Special type argument must not be nullable", |
||||||
|
"Special type argument must not be nullable.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor SpecialArgMustNotBeGeneric = new( |
||||||
|
$"gSG{_idCounter++:00}", "Special type argument must not be generic", |
||||||
|
"Special type argument must not be generic.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor UniqueParamMustNotBeByRef = new( |
||||||
|
$"gSG{_idCounter++:00}", "Unique parameter must not be ByRef", |
||||||
|
"Unique parameter must not be ByRef (in/out/ref).", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor UniqueParamMustNotBeNullable = new( |
||||||
|
$"gSG{_idCounter++:00}", "Unique parameter must not be nullable", |
||||||
|
"Unique parameter must not be nullable.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor UniqueParamGenericMustMatch = new( |
||||||
|
$"gSG{_idCounter++:00}", "Unique parameter generic type parameter must match method", |
||||||
|
"Unique parameter's generic type parameter must match the method's type parameter.", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
|
||||||
|
public static readonly DiagnosticDescriptor UniqueParamNotSupported = new( |
||||||
|
$"gSG{_idCounter++:00}", "Unique parameter does not support this attribute", |
||||||
|
"Unique parameter does not support {0}", |
||||||
|
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||||
|
} |
@ -1,261 +0,0 @@ |
|||||||
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, |
|
||||||
} |
|
||||||
} |
|
@ -1,141 +0,0 @@ |
|||||||
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 class ModuleGenerator |
|
||||||
: ISourceGenerator |
|
||||||
{ |
|
||||||
private static readonly DiagnosticDescriptor ModuleMayNotBeNested = new( |
|
||||||
"gaem0001", "Module may not be nested", |
|
||||||
"Type {0} marked with [Module] may not be a nested type", |
|
||||||
nameof(ModuleGenerator), DiagnosticSeverity.Error, true); |
|
||||||
|
|
||||||
private static readonly DiagnosticDescriptor ModuleMustBePartial = new( |
|
||||||
"gaem0002", "Module must be partial", |
|
||||||
"Type {0} marked with [Module] must be a partial type", |
|
||||||
nameof(ModuleGenerator), DiagnosticSeverity.Error, true); |
|
||||||
|
|
||||||
private static readonly DiagnosticDescriptor ModuleBuiltInMustHavePath = new( |
|
||||||
"gaem0003", "Built-in module must have [Path]", |
|
||||||
"Type {0} marked with [Module] is a built-in module (static), and therefore must have [Path] set", |
|
||||||
nameof(ModuleGenerator), DiagnosticSeverity.Error, true); |
|
||||||
|
|
||||||
|
|
||||||
public void Initialize(GeneratorInitializationContext context) |
|
||||||
=> context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); |
|
||||||
|
|
||||||
private class SyntaxReceiver |
|
||||||
: ISyntaxContextReceiver |
|
||||||
{ |
|
||||||
public HashSet<INamedTypeSymbol> Symbols { 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.GetFullName(true) != "gaemstone.ECS.ModuleAttribute") return; |
|
||||||
|
|
||||||
var memberNode = attrNode.Parent?.Parent!; |
|
||||||
var memberSymbol = model.GetDeclaredSymbol(memberNode) as INamedTypeSymbol; |
|
||||||
Symbols.Add(memberSymbol!); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public void Execute(GeneratorExecutionContext context) |
|
||||||
{ |
|
||||||
if (context.SyntaxContextReceiver is not SyntaxReceiver receiver) return; |
|
||||||
|
|
||||||
foreach (var symbol in receiver.Symbols) { |
|
||||||
var isNested = (symbol.ContainingType != null); |
|
||||||
if (isNested) |
|
||||||
context.ReportDiagnostic(Diagnostic.Create(ModuleMayNotBeNested, |
|
||||||
symbol.Locations.FirstOrDefault(), symbol.GetFullName())); |
|
||||||
|
|
||||||
var isPartial = symbol.DeclaringSyntaxReferences |
|
||||||
.Any(r => (r.GetSyntax() as ClassDeclarationSyntax)?.Modifiers |
|
||||||
.Any(t => t.IsKind(SyntaxKind.PartialKeyword)) ?? false); |
|
||||||
if (!isPartial) |
|
||||||
context.ReportDiagnostic(Diagnostic.Create(ModuleMustBePartial, |
|
||||||
symbol.Locations.FirstOrDefault(), symbol.GetFullName())); |
|
||||||
|
|
||||||
if (symbol.IsStatic && (symbol.GetAttribute("gaemstone.ECS.PathAttribute")? |
|
||||||
.ConstructorArguments.FirstOrDefault().Value == null)) |
|
||||||
context.ReportDiagnostic(Diagnostic.Create(ModuleBuiltInMustHavePath, |
|
||||||
symbol.Locations.FirstOrDefault(), symbol.GetFullName())); |
|
||||||
|
|
||||||
// Static classes can't implement interfaces. |
|
||||||
if (symbol.IsStatic) continue; |
|
||||||
|
|
||||||
var modulePath = GetModulePath(symbol).ToStringLiteral(); |
|
||||||
var dependencies = new List<string>(); |
|
||||||
foreach (var attr in symbol.GetAttributes()) |
|
||||||
for (var type = attr.AttributeClass; type != null; type = type.BaseType) |
|
||||||
if ((type.GetFullName(true) == "gaemstone.ECS.AddAttribute`2") |
|
||||||
&& type.TypeArguments[0].GetFullName() == "gaemstone.Flecs.Core.DependsOn") |
|
||||||
dependencies.Add(GetModulePath(type.TypeArguments[1])); |
|
||||||
|
|
||||||
var sb = new StringBuilder(); |
|
||||||
sb.AppendLine($$"""
|
|
||||||
// <auto-generated/> |
|
||||||
using System.Collections.Generic; |
|
||||||
using System.Linq; |
|
||||||
using gaemstone.ECS; |
|
||||||
|
|
||||||
namespace {{ symbol.GetNamespace() }}; |
|
||||||
|
|
||||||
public partial class {{ symbol.Name }} |
|
||||||
: IModule |
|
||||||
{ |
|
||||||
public static string ModulePath { get; } |
|
||||||
= {{ modulePath }}; |
|
||||||
|
|
||||||
""");
|
|
||||||
|
|
||||||
if (dependencies.Count > 0) { |
|
||||||
sb.AppendLine($$"""
|
|
||||||
public static IEnumerable<string> Dependencies { get; } = new[] { |
|
||||||
""");
|
|
||||||
foreach (var dependency in dependencies) |
|
||||||
sb.AppendLine($$""" {{ dependency.ToStringLiteral() }},"""); |
|
||||||
sb.AppendLine($$"""
|
|
||||||
}; |
|
||||||
""");
|
|
||||||
} else sb.AppendLine($$"""
|
|
||||||
public static IEnumerable<string> Dependencies { get; } = Enumerable.Empty<string>(); |
|
||||||
""");
|
|
||||||
|
|
||||||
|
|
||||||
sb.AppendLine($$"""
|
|
||||||
} |
|
||||||
|
|
||||||
""");
|
|
||||||
|
|
||||||
context.AddSource($"{symbol.GetFullName()}_Module.g.cs", sb.ToString()); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private static string GetModulePath(ISymbol module) |
|
||||||
{ |
|
||||||
var path = module.GetAttribute("gaemstone.ECS.PathAttribute")? |
|
||||||
.ConstructorArguments.FirstOrDefault().Value as string; |
|
||||||
var isAbsolute = (path?.FirstOrDefault() == '/'); |
|
||||||
if (isAbsolute) return path!; |
|
||||||
|
|
||||||
var fullPath = module.GetFullName().Replace('.', '/'); |
|
||||||
if (path != null) { |
|
||||||
var index = fullPath.LastIndexOf('/'); |
|
||||||
fullPath = $"{fullPath.Substring(0, index)}/{path}"; |
|
||||||
} |
|
||||||
return $"/{fullPath}"; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,369 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Text; |
||||||
|
using gaemstone.SourceGen.Structure; |
||||||
|
using gaemstone.SourceGen.Utility; |
||||||
|
using Microsoft.CodeAnalysis; |
||||||
|
|
||||||
|
namespace gaemstone.SourceGen; |
||||||
|
|
||||||
|
[Generator] |
||||||
|
public class ModuleGenerator |
||||||
|
: ISourceGenerator |
||||||
|
{ |
||||||
|
public void Initialize(GeneratorInitializationContext context) |
||||||
|
{ |
||||||
|
#if DEBUG |
||||||
|
// while (!System.Diagnostics.Debugger.IsAttached) |
||||||
|
// System.Threading.Thread.Sleep(500); |
||||||
|
#endif |
||||||
|
context.RegisterForSyntaxNotifications( |
||||||
|
() => new RelevantSymbolReceiver()); |
||||||
|
} |
||||||
|
|
||||||
|
private Dictionary<ISymbol, BaseInfo>? _symbolToInfoLookup; |
||||||
|
|
||||||
|
public BaseInfo? Lookup(ISymbol symbol) |
||||||
|
=> (_symbolToInfoLookup ?? throw new InvalidOperationException()) |
||||||
|
.TryGetValue(symbol, out var info) ? info : null; |
||||||
|
|
||||||
|
public void Execute(GeneratorExecutionContext context) |
||||||
|
{ |
||||||
|
if (context.SyntaxContextReceiver is not RelevantSymbolReceiver receiver) return; |
||||||
|
_symbolToInfoLookup = receiver.Symbols; |
||||||
|
var infos = receiver.Symbols.Values; |
||||||
|
|
||||||
|
// Go through all entity infos (types and methods), populate their |
||||||
|
// Parent property and add them to their parent's Children property. |
||||||
|
foreach (var info in infos.OfType<BaseEntityInfo>()) { |
||||||
|
if ((info.Symbol.ContainingType is INamedTypeSymbol parentSymbol) |
||||||
|
&& (Lookup(parentSymbol) is TypeEntityInfo parentInfo)) |
||||||
|
{ |
||||||
|
info.Parent = parentInfo; |
||||||
|
parentInfo.Children.Add(info); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Go through all the method infos and lookup / construct |
||||||
|
// their parameter infos from the method's parameters. |
||||||
|
foreach (var info in infos.OfType<MethodEntityInfo>()) { |
||||||
|
foreach (var paramSymbol in info.Symbol.Parameters) { |
||||||
|
if (Lookup(paramSymbol) is not ParameterInfo paramInfo) |
||||||
|
paramInfo = new ParameterInfo(paramSymbol); |
||||||
|
info.Parameters.Add(paramInfo); |
||||||
|
paramInfo.Parent = info; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Validate all instances of base info without a Parent / Method set. |
||||||
|
// Nested infos are validated by their containing info. |
||||||
|
foreach (var info in infos.Where(info => info.Parent == null)) |
||||||
|
info.Validate(); |
||||||
|
|
||||||
|
// Report all the diagnostics we found. |
||||||
|
foreach (var info in infos) |
||||||
|
foreach (var diag in info.Diagnostics) |
||||||
|
context.ReportDiagnostic(diag); |
||||||
|
|
||||||
|
foreach (var module in infos.OfType<ModuleEntityInfo>()) { |
||||||
|
if (module.IsErrored) continue; |
||||||
|
var sb = new StringBuilder(); |
||||||
|
AppendHeader(sb, module.Namespace); |
||||||
|
AppendModuleType(sb, module); |
||||||
|
context.AddSource($"{module.FullName}.g.cs", sb.ToString()); |
||||||
|
} |
||||||
|
|
||||||
|
_symbolToInfoLookup = null; |
||||||
|
} |
||||||
|
|
||||||
|
private void AppendHeader(StringBuilder sb, string @namespace) |
||||||
|
=> sb.AppendLine($$"""
|
||||||
|
// <auto-generated/> |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Collections.Immutable; |
||||||
|
using gaemstone.ECS; |
||||||
|
using gaemstone.ECS.Utility; |
||||||
|
|
||||||
|
namespace {{ @namespace }}; |
||||||
|
|
||||||
|
""");
|
||||||
|
|
||||||
|
private void AppendModuleType(StringBuilder sb, ModuleEntityInfo module) |
||||||
|
{ |
||||||
|
var type = module.Symbol.IsValueType ? "struct" : "class"; |
||||||
|
sb.AppendLine($$"""
|
||||||
|
public partial {{ type }} {{ module.Name }} |
||||||
|
: IModule |
||||||
|
""");
|
||||||
|
|
||||||
|
var modulePath = module.GetModulePath().ToStringLiteral(); |
||||||
|
sb.AppendLine($$"""
|
||||||
|
{ |
||||||
|
static string IModule.Path { get; } = {{ modulePath }}; |
||||||
|
|
||||||
|
static bool IModule.IsBuiltIn { get; } = {{( module.IsBuiltIn ? "true" : "false" )}}; |
||||||
|
|
||||||
|
""");
|
||||||
|
|
||||||
|
var dependencies = module.GetDependencies().ToList(); |
||||||
|
sb.Append("\tstatic IReadOnlyList<string> IModule.Dependencies { get; } = "); |
||||||
|
if (dependencies.Count > 0) { |
||||||
|
sb.AppendLine("ImmutableList.Create("); |
||||||
|
foreach (var dependency in dependencies) |
||||||
|
sb.AppendLine($"\t\t{dependency.ToStringLiteral()},"); |
||||||
|
sb.Length -= 2; sb.AppendLine(); |
||||||
|
sb.AppendLine("\t);"); |
||||||
|
} else sb.AppendLine("ImmutableList.Create<string>();"); |
||||||
|
sb.AppendLine(); |
||||||
|
|
||||||
|
sb.AppendLine($$"""
|
||||||
|
static void IModule.Initialize<T>(Entity<T> module) |
||||||
|
{ |
||||||
|
var world = module.World; |
||||||
|
""");
|
||||||
|
|
||||||
|
// TODO: Might want to add things related to the module entity. |
||||||
|
|
||||||
|
AppendEntityRegistration(sb, module); |
||||||
|
AppendMethodRegistration(sb, module); |
||||||
|
AppendEntityToAdd(sb, module); |
||||||
|
AppendShimMethods(sb, module); |
||||||
|
|
||||||
|
// TODO: Can BuiltIn modules have systems and such? |
||||||
|
|
||||||
|
sb.AppendLine("\t}"); |
||||||
|
|
||||||
|
sb.AppendLine("}"); |
||||||
|
} |
||||||
|
|
||||||
|
private void AppendEntityRegistration( |
||||||
|
StringBuilder sb, ModuleEntityInfo module) |
||||||
|
{ |
||||||
|
var entities = module.Children |
||||||
|
.OfType<TypeEntityInfo>() |
||||||
|
.Where(e => !e.IsErrored) |
||||||
|
.ToList(); |
||||||
|
if (entities.Count == 0) return; |
||||||
|
|
||||||
|
sb.AppendLine(); |
||||||
|
sb.AppendLine("\t\t// Register entities."); |
||||||
|
foreach (var e in entities) { |
||||||
|
var @var = $"_{e.Name}_Entity"; |
||||||
|
var path = (e.Path ?? e.Name).ToStringLiteral(); |
||||||
|
sb.AppendLine($"\t\tvar {@var} = world.New({path}, module)"); |
||||||
|
|
||||||
|
// Since this is a custom gaemstone tag, we want to add it even for [BuiltIn] modules. |
||||||
|
if (e.IsRelation) sb.AppendLine("\t\t\t.Add<gaemstone.Doc.Relation>()"); |
||||||
|
|
||||||
|
if (module.IsBuiltIn) |
||||||
|
{ |
||||||
|
sb.AppendLine($"\t\t\t.Build().CreateLookup<{e.FullName}>()"); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
// TODO: if (e.IsPublic) sb.AppendLine($"\t\t\t.Symbol(\"{e.Name}\")"); |
||||||
|
|
||||||
|
// Tags and relations in Flecs are marked as empty components. |
||||||
|
if (e.IsTag || e.IsRelation) sb.AppendLine("\t\t\t.Add<gaemstone.Flecs.Core.Component>()"); |
||||||
|
if (e.IsTag && e.IsRelation) sb.AppendLine("\t\t\t.Add<gaemstone.Flecs.Core.Tag>()"); |
||||||
|
|
||||||
|
sb.Append( "\t\t\t.Build()"); |
||||||
|
if (e.IsComponent) sb.Append($".InitComponent<{e.FullName}>()"); |
||||||
|
else sb.Append($".CreateLookup<{e.FullName}>()"); |
||||||
|
sb.AppendLine(); |
||||||
|
|
||||||
|
if (e.IsSingleton) sb.AppendLine($"\t\t\t.Add<{e.FullName}>()"); |
||||||
|
} |
||||||
|
|
||||||
|
sb.Insert(sb.Length - 1, ";"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void AppendMethodRegistration( |
||||||
|
StringBuilder sb, ModuleEntityInfo module) |
||||||
|
{ |
||||||
|
var methods = module.Children |
||||||
|
.OfType<MethodEntityInfo>() |
||||||
|
.Where(e => !e.IsErrored) |
||||||
|
.ToList(); |
||||||
|
if (methods.Count == 0) return; |
||||||
|
|
||||||
|
sb.AppendLine(); |
||||||
|
sb.AppendLine("\t\t// Register systems / observers."); |
||||||
|
foreach (var m in methods) { |
||||||
|
var @var = $"_{m.Name}_Entity"; |
||||||
|
var path = (m.Path ?? m.Name).ToStringLiteral(); |
||||||
|
sb.AppendLine($"\t\tvar {@var} = world.New({path}, module)"); |
||||||
|
|
||||||
|
sb.Append("\t\t\t.Build()"); |
||||||
|
if (m.IsSystem) sb.AppendLine(".InitSystem("); |
||||||
|
if (m.IsObserver) sb.AppendLine(".InitObserver("); |
||||||
|
|
||||||
|
if (m.IsIteratorOnly) { |
||||||
|
var expression = m.Expression.ToStringLiteral(); |
||||||
|
sb.AppendLine($"\t\t\t\tnew({expression}), {m.Name},"); |
||||||
|
} else { |
||||||
|
sb.AppendLine("\t\t\t\tnew("); |
||||||
|
foreach (var p in m.Parameters) |
||||||
|
if (p.HasTerm) { |
||||||
|
// TODO: Throw error if multiple Or terms appear next to each other. |
||||||
|
foreach (var term in p.TermTypes) { |
||||||
|
sb.Append($"\t\t\t\t\tnew Term("); |
||||||
|
switch (term) { |
||||||
|
case ITypeSymbol type: |
||||||
|
AppendTypeEntity(sb, module, type); |
||||||
|
break; |
||||||
|
case ParameterInfo.Pair pair: |
||||||
|
AppendTypeEntity(sb, module, pair.Relation); |
||||||
|
sb.Append(','); |
||||||
|
AppendTypeEntity(sb, module, pair.Target); |
||||||
|
break; |
||||||
|
default: throw new InvalidOperationException( |
||||||
|
$"Unexpected term type {term.GetType()}"); |
||||||
|
} |
||||||
|
sb.Append(')'); |
||||||
|
if (p.Source != null) { |
||||||
|
sb.Append("{ Source = "); |
||||||
|
AppendTypeEntity(sb, module, p.Source); |
||||||
|
sb.Append(" }"); |
||||||
|
} |
||||||
|
sb.Append(p.Kind switch { |
||||||
|
ParameterKind.Or => ".Or", |
||||||
|
ParameterKind.HasOr => ".Or.None", |
||||||
|
ParameterKind.Has => ".None", |
||||||
|
ParameterKind.Not => ".None.Not", |
||||||
|
ParameterKind.Ref => ".InOut", |
||||||
|
ParameterKind.Out => ".Out", |
||||||
|
_ when !p.IsValueType => ".InOut", // Reference types always imply writability. |
||||||
|
_ => ".In", |
||||||
|
}); |
||||||
|
if (p.IsNullable) sb.Append(".Optional"); |
||||||
|
sb.AppendLine(","); |
||||||
|
} |
||||||
|
} |
||||||
|
if (m.Parameters.Any(p => p.HasTerm)) |
||||||
|
{ sb.Length -= 2; sb.AppendLine(); } |
||||||
|
sb.AppendLine( "\t\t\t\t),"); |
||||||
|
sb.AppendLine($"\t\t\t\t_{m.Name}_Shim,"); |
||||||
|
} |
||||||
|
|
||||||
|
if (m.IsObserver) |
||||||
|
foreach (var ev in m.ObserverEvents!) { |
||||||
|
sb.Append("\t\t\t\t"); |
||||||
|
AppendTypeEntity(sb, module, ev); |
||||||
|
sb.AppendLine(","); |
||||||
|
} |
||||||
|
|
||||||
|
sb.Length -= 2; |
||||||
|
sb.AppendLine(");"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void AppendEntityToAdd( |
||||||
|
StringBuilder sb, ModuleEntityInfo module) |
||||||
|
{ |
||||||
|
var entities = module.Children |
||||||
|
.Where(e => !e.IsErrored) |
||||||
|
.Where(e => e.HasEntitiesToAdd) |
||||||
|
.ToList(); |
||||||
|
if (entities.Count == 0) return; |
||||||
|
|
||||||
|
sb.AppendLine(); |
||||||
|
sb.AppendLine("\t\t// Add things to entities."); |
||||||
|
foreach (var e in entities) { |
||||||
|
var @var = $"_{e.Name}_Entity"; |
||||||
|
foreach (var a in e.EntitiesToAdd) |
||||||
|
sb.AppendLine($"\t\t{@var}.Add<{a.GetFullName()}>();"); |
||||||
|
foreach (var (r, t) in e.RelationsToAdd) |
||||||
|
sb.AppendLine($"\t\t{@var}.Add<{r.GetFullName()}, {t.GetFullName()}>();"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void AppendShimMethods( |
||||||
|
StringBuilder sb, ModuleEntityInfo module) |
||||||
|
{ |
||||||
|
var methods = module.Children |
||||||
|
.OfType<MethodEntityInfo>() |
||||||
|
.Where(m => !m.IsErrored) |
||||||
|
.Where(m => !m.IsIteratorOnly) |
||||||
|
.ToList(); |
||||||
|
|
||||||
|
foreach (var method in methods) |
||||||
|
AppendShimMethod(sb, module, method); |
||||||
|
} |
||||||
|
|
||||||
|
private void AppendShimMethod( |
||||||
|
StringBuilder sb, ModuleEntityInfo module, |
||||||
|
MethodEntityInfo method) |
||||||
|
{ |
||||||
|
sb.AppendLine(); |
||||||
|
sb.AppendLine($$"""
|
||||||
|
void _{{ method.Name }}_Shim(Iterator<T> iter) |
||||||
|
{ |
||||||
|
""");
|
||||||
|
|
||||||
|
foreach (var param in method.Parameters) |
||||||
|
if (param.HasField) |
||||||
|
sb.Append($"\t\t\tvar _{param.Name}_Field = ") |
||||||
|
.Append(param.IsNullable ? "iter.FieldOrEmpty" : "iter.Field") |
||||||
|
.AppendLine($"<{param.FieldType!.GetFullName()}>({param.TermIndex});"); |
||||||
|
|
||||||
|
sb.AppendLine("\t\t\tfor (var i = 0; i < iter.Count; i++) {"); |
||||||
|
|
||||||
|
sb.Append($"\t\t\t\t{method.Name}"); |
||||||
|
if (method.IsGeneric) sb.Append("<T>"); |
||||||
|
sb.Append($"("); |
||||||
|
foreach (var param in method.Parameters) { |
||||||
|
switch (param.Kind) { |
||||||
|
case ParameterKind.Unique: |
||||||
|
sb.Append(param.UniqueReplacement); |
||||||
|
break; |
||||||
|
|
||||||
|
case ParameterKind.In: |
||||||
|
case ParameterKind.Out: |
||||||
|
case ParameterKind.Ref: |
||||||
|
var modifier = param.Kind.ToString().ToLowerInvariant(); |
||||||
|
sb.Append(modifier).Append(' '); |
||||||
|
goto case ParameterKind.Normal; |
||||||
|
|
||||||
|
case ParameterKind.Normal: |
||||||
|
case ParameterKind.Nullable: |
||||||
|
// FIXME: Handle pairs. |
||||||
|
sb.Append($"_{param.Name}_Field") |
||||||
|
.Append(param.IsNullable ? ".GetOrNull(i)" : "[i]"); |
||||||
|
break; |
||||||
|
|
||||||
|
case ParameterKind.Has: |
||||||
|
case ParameterKind.Not: |
||||||
|
case ParameterKind.HasOr: |
||||||
|
sb.Append("default"); |
||||||
|
break; |
||||||
|
|
||||||
|
case ParameterKind.Or: |
||||||
|
throw new NotSupportedException( |
||||||
|
$"ParameterKind {param.Kind} not supported"); |
||||||
|
} |
||||||
|
sb.Append(", "); |
||||||
|
} |
||||||
|
if (method.Parameters.Any()) |
||||||
|
sb.Length -= 2; |
||||||
|
sb.AppendLine(");"); |
||||||
|
|
||||||
|
sb.AppendLine($$"""
|
||||||
|
} |
||||||
|
} |
||||||
|
""");
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private void AppendTypeEntity( |
||||||
|
StringBuilder sb, ModuleEntityInfo module, |
||||||
|
ITypeSymbol type) |
||||||
|
{ |
||||||
|
var found = module.Children.Where(c => !c.IsErrored) |
||||||
|
.Any(c => SymbolEqualityComparer.Default.Equals(c.Symbol, type)); |
||||||
|
sb.Append(found ? $"_{type.Name}_Entity" |
||||||
|
: $"world.Entity<{type.GetFullName()}>()"); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,82 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using gaemstone.SourceGen.Structure; |
||||||
|
using gaemstone.SourceGen.Utility; |
||||||
|
using Microsoft.CodeAnalysis; |
||||||
|
using Microsoft.CodeAnalysis.CSharp; |
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax; |
||||||
|
|
||||||
|
namespace gaemstone.SourceGen; |
||||||
|
|
||||||
|
public class RelevantSymbolReceiver |
||||||
|
: ISyntaxContextReceiver |
||||||
|
{ |
||||||
|
private static readonly HashSet<string> RelevantAttributeNames = new(){ |
||||||
|
// Base entity attributes |
||||||
|
"Module", // Can also be [Singleton] |
||||||
|
"Entity", |
||||||
|
"Relation", // Can also be [Tag] or [Component] |
||||||
|
"Tag", |
||||||
|
"Component", |
||||||
|
"Singleton", // Implies [Component] |
||||||
|
"System", |
||||||
|
"Observer", |
||||||
|
|
||||||
|
// Entity properties that specify additional info / behavior |
||||||
|
"Public", |
||||||
|
"Private", |
||||||
|
"Path", |
||||||
|
"Add", |
||||||
|
"BuiltIn", // Valid on [Module] |
||||||
|
"Expression", // Valid on [System] and [Observer] |
||||||
|
|
||||||
|
// Term properties (on [System] and [Observer] parameters) |
||||||
|
"Source", |
||||||
|
"Pair", |
||||||
|
"Has", |
||||||
|
"Not", |
||||||
|
"Or", |
||||||
|
}; |
||||||
|
|
||||||
|
public Dictionary<ISymbol, BaseInfo> Symbols { get; } = new(SymbolEqualityComparer.Default); |
||||||
|
|
||||||
|
public void OnVisitSyntaxNode(GeneratorSyntaxContext context) |
||||||
|
{ |
||||||
|
var model = context.SemanticModel; |
||||||
|
if (context.Node is not AttributeSyntax node) return; |
||||||
|
if (node.Parent?.Parent is not SyntaxNode parent) return; |
||||||
|
if (model.GetDeclaredSymbol(parent) is not ISymbol symbol) return; |
||||||
|
if (model.GetTypeInfo(node).Type is not INamedTypeSymbol type) return; |
||||||
|
|
||||||
|
// Go through the attribute's type hierarchy to see if it matches any |
||||||
|
// of the attributes we care about. This is to make sure attributes |
||||||
|
// based on [Add<...>] are picked up correctly, including custom ones. |
||||||
|
for (var baseType = type; baseType != null; baseType = baseType.BaseType) { |
||||||
|
if ((ToRelevantAttributeName(baseType) is string name) |
||||||
|
&& RelevantAttributeNames.Contains(name) // Check if we found a relevant attribute. |
||||||
|
&& !Symbols.ContainsKey(symbol)) // Check if this is already a known symbol. |
||||||
|
{ |
||||||
|
Symbols.Add(symbol, symbol switch { |
||||||
|
INamedTypeSymbol typeSymbol => |
||||||
|
typeSymbol.GetAttributes().Any(attr => attr.AttributeClass! |
||||||
|
.GetFullName() == "gaemstone.ECS.ModuleAttribute") |
||||||
|
? new ModuleEntityInfo(typeSymbol) |
||||||
|
: new TypeEntityInfo(typeSymbol), |
||||||
|
IMethodSymbol methodSymbol => new MethodEntityInfo(methodSymbol), |
||||||
|
IParameterSymbol paramSymbol => new ParameterInfo(paramSymbol), |
||||||
|
_ => throw new InvalidOperationException( |
||||||
|
$"Unhandled symbol type {symbol.GetType()}"), |
||||||
|
}); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static string? ToRelevantAttributeName(INamedTypeSymbol symbol) |
||||||
|
{ |
||||||
|
if (symbol.GetNamespace() != "gaemstone.ECS") return null; |
||||||
|
var name = symbol.MetadataName.Split('`')[0]; |
||||||
|
return name.EndsWith("Attribute") ? name[..^"Attribute".Length] : null; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,78 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using Microsoft.CodeAnalysis; |
||||||
|
|
||||||
|
namespace gaemstone.SourceGen.Structure; |
||||||
|
|
||||||
|
public abstract class BaseEntityInfo : BaseInfo |
||||||
|
{ |
||||||
|
public new TypeEntityInfo? Parent { get => (TypeEntityInfo?)base.Parent; set => base.Parent = value; } |
||||||
|
|
||||||
|
public string? Path { get; } |
||||||
|
// TODO: Rename this to something sensible, like [Symbol]. |
||||||
|
// public bool IsPublic { get; private set; } |
||||||
|
// private bool IsPrivate { get; } |
||||||
|
|
||||||
|
public List<INamedTypeSymbol> EntitiesToAdd { get; } = new(); |
||||||
|
public List<(INamedTypeSymbol, INamedTypeSymbol)> RelationsToAdd { get; } = new(); |
||||||
|
public bool HasEntitiesToAdd => (EntitiesToAdd.Count > 0) || (RelationsToAdd.Count > 0); |
||||||
|
|
||||||
|
public BaseEntityInfo(ISymbol symbol) |
||||||
|
: base(symbol) |
||||||
|
{ |
||||||
|
Path = Get("Path")?.ConstructorArguments.FirstOrDefault().Value as string; |
||||||
|
|
||||||
|
// IsPublic = Symbol.HasAttribute("gaemstone.ECS.PublicAttribute"); |
||||||
|
// IsPrivate = Symbol.HasAttribute("gaemstone.ECS.PrivateAttribute"); |
||||||
|
} |
||||||
|
|
||||||
|
protected override IEnumerable<Diagnostic> ValidateSelf() |
||||||
|
{ |
||||||
|
if (this is ModuleEntityInfo) { |
||||||
|
// If this entity is a module, it must not be nested. |
||||||
|
if (Symbol.ContainingType != null) yield return Diagnostic.Create( |
||||||
|
Descriptors.ModuleMustNotBeNested, Location); |
||||||
|
} else { |
||||||
|
// Otherwise, it must occur within a module |
||||||
|
if (Parent is not ModuleEntityInfo) yield return Diagnostic.Create( |
||||||
|
Descriptors.EntityMustBeInModule, Location); |
||||||
|
} |
||||||
|
|
||||||
|
// var moduleIsPublic = (Parent?.IsPublic == true); |
||||||
|
// var inheritsPublic = (this is MethodEntityInfo); // Observers and systems don't inherit [Public] from their module. |
||||||
|
// IsPublic = IsPublic || (moduleIsPublic && inheritsPublic && !IsPrivate); |
||||||
|
|
||||||
|
// Add entities and relationships specified using [Add<...>] attributes. |
||||||
|
foreach (var attr in Symbol.GetAttributes()) { |
||||||
|
for (var attrType = attr.AttributeClass; attrType != null; attrType = attrType.BaseType) { |
||||||
|
if (RelevantSymbolReceiver.ToRelevantAttributeName(attrType) != "Add") continue; |
||||||
|
|
||||||
|
var allTypeArgumentsValid = true; |
||||||
|
for (var i = 0; i < attrType.TypeArguments.Length; i++) { |
||||||
|
var arg = attrType.TypeArguments[i]; |
||||||
|
var param = attrType.TypeParameters[i]; |
||||||
|
if (arg is not INamedTypeSymbol) { |
||||||
|
yield return Diagnostic.Create( |
||||||
|
Descriptors.InvalidTypeArgument, param.Locations.Single()); |
||||||
|
allTypeArgumentsValid = false; |
||||||
|
} |
||||||
|
// TODO: Make sure entities being added have appropriate attributes as well. |
||||||
|
} |
||||||
|
|
||||||
|
if (allTypeArgumentsValid) { |
||||||
|
switch (attrType.TypeArguments) { |
||||||
|
case [ INamedTypeSymbol entity ]: |
||||||
|
EntitiesToAdd.Add(entity); |
||||||
|
break; |
||||||
|
case [ INamedTypeSymbol relation, INamedTypeSymbol target ]: |
||||||
|
RelationsToAdd.Add((relation, target)); |
||||||
|
break; |
||||||
|
default: throw new InvalidOperationException( |
||||||
|
"Type argument pattern matching failed"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using gaemstone.SourceGen.Utility; |
||||||
|
using Microsoft.CodeAnalysis; |
||||||
|
|
||||||
|
namespace gaemstone.SourceGen.Structure; |
||||||
|
|
||||||
|
public abstract class BaseInfo |
||||||
|
{ |
||||||
|
public ISymbol Symbol { get; } |
||||||
|
|
||||||
|
public BaseEntityInfo? Parent { get; set; } |
||||||
|
|
||||||
|
public string Name => Symbol.Name; |
||||||
|
public string FullName => Symbol.GetFullName(); |
||||||
|
public string Namespace => Symbol.GetNamespace()!; |
||||||
|
|
||||||
|
public Location Location => Symbol.Locations.First(); |
||||||
|
public List<Diagnostic> Diagnostics { get; } = new(); |
||||||
|
public bool IsErrored => Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error); |
||||||
|
|
||||||
|
public BaseInfo(ISymbol symbol) => Symbol = symbol; |
||||||
|
|
||||||
|
public void Validate() |
||||||
|
{ |
||||||
|
// All of the children are validated before the parent is, |
||||||
|
// in case their state affects the parent in some way. |
||||||
|
foreach (var child in GetChildren()) |
||||||
|
child.Validate(); |
||||||
|
foreach (var diag in ValidateSelf()) |
||||||
|
Diagnostics.Add(diag); |
||||||
|
} |
||||||
|
|
||||||
|
protected virtual IEnumerable<BaseInfo> GetChildren() => Enumerable.Empty<BaseInfo>(); |
||||||
|
protected abstract IEnumerable<Diagnostic> ValidateSelf(); |
||||||
|
|
||||||
|
protected bool Has(string name) |
||||||
|
=> Get(name) != null; |
||||||
|
|
||||||
|
protected AttributeData? Get(string name) |
||||||
|
=> Symbol.GetAttributes().FirstOrDefault(attr => |
||||||
|
RelevantSymbolReceiver.ToRelevantAttributeName(attr.AttributeClass!) == name); |
||||||
|
} |
@ -0,0 +1,85 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using gaemstone.SourceGen.Utility; |
||||||
|
using Microsoft.CodeAnalysis; |
||||||
|
|
||||||
|
namespace gaemstone.SourceGen.Structure; |
||||||
|
|
||||||
|
public class MethodEntityInfo : BaseEntityInfo |
||||||
|
{ |
||||||
|
public new IMethodSymbol Symbol => (IMethodSymbol)base.Symbol; |
||||||
|
|
||||||
|
public List<ParameterInfo> Parameters { get; } = new(); |
||||||
|
protected override IEnumerable<BaseInfo> GetChildren() => Parameters; |
||||||
|
|
||||||
|
public bool IsSystem { get; } |
||||||
|
public bool IsObserver { get; } |
||||||
|
|
||||||
|
public bool IsIteratorOnly { get; } |
||||||
|
public string? Expression { get; } |
||||||
|
public ITypeSymbol[]? ObserverEvents { get; } |
||||||
|
|
||||||
|
public bool IsStatic => Symbol.IsStatic; |
||||||
|
public bool IsGeneric => Symbol.IsGenericMethod; |
||||||
|
|
||||||
|
public MethodEntityInfo(ISymbol symbol) |
||||||
|
: base(symbol) |
||||||
|
{ |
||||||
|
IsSystem = Has("System"); |
||||||
|
IsObserver = Has("Observer"); |
||||||
|
|
||||||
|
IsIteratorOnly = (Parameters.Count == 1) && (Parameters[0].Symbol.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.Iterator"); |
||||||
|
Expression = Get("Expression")?.ConstructorArguments.FirstOrDefault().Value as string; |
||||||
|
ObserverEvents = Get("Observer")?.AttributeClass!.TypeArguments.ToArray(); |
||||||
|
} |
||||||
|
|
||||||
|
protected override IEnumerable<Diagnostic> ValidateSelf() |
||||||
|
{ |
||||||
|
foreach (var diag in base.ValidateSelf()) yield return diag; |
||||||
|
|
||||||
|
if (IsSystem && IsObserver) yield return Diagnostic.Create( |
||||||
|
Descriptors.InvalidAttributeCombination, Location, "[System, Observer]"); |
||||||
|
|
||||||
|
if (Symbol.IsAbstract) yield return Diagnostic.Create( |
||||||
|
Descriptors.MethodMustNotBeAbstract, Location); |
||||||
|
if (Symbol.IsAsync) yield return Diagnostic.Create( |
||||||
|
Descriptors.MethodMustNotBeAsync, Location); |
||||||
|
|
||||||
|
if (Symbol.Parameters.Length == 0) yield return Diagnostic.Create( |
||||||
|
Descriptors.MethodMustHaveParameters, Location); |
||||||
|
if (Symbol.IsExtensionMethod) yield return Diagnostic.Create( |
||||||
|
Descriptors.MethodMustNotBeExtension, Location); |
||||||
|
|
||||||
|
if (IsIteratorOnly && (Expression == null)) yield return Diagnostic.Create( |
||||||
|
Descriptors.MethodMustHaveExpression, Location); |
||||||
|
|
||||||
|
if (!IsStatic && Parent is ModuleEntityInfo { IsSingleton: false }) |
||||||
|
yield return Diagnostic.Create( |
||||||
|
Descriptors.InstanceMethodOnlyValidInSingleton, Location); |
||||||
|
|
||||||
|
if (IsGeneric) { |
||||||
|
if (Symbol.TypeParameters.Length > 1) yield return Diagnostic.Create( |
||||||
|
Descriptors.MethodGenericParamAtMostOne, Location); |
||||||
|
else { |
||||||
|
var param = Symbol.TypeParameters[0]; |
||||||
|
if (!SymbolEqualityComparer.Default.Equals(param, Symbol.TypeArguments[0])) |
||||||
|
yield return Diagnostic.Create( |
||||||
|
Descriptors.MethodGenericParamMustNotBeSubstutited, param.Locations.Single()); |
||||||
|
if (param.HasReferenceTypeConstraint || param.HasValueTypeConstraint |
||||||
|
|| param.HasUnmanagedTypeConstraint || param.HasNotNullConstraint |
||||||
|
|| param.HasConstructorConstraint || (param.ConstraintTypes.Length > 0)) |
||||||
|
yield return Diagnostic.Create( |
||||||
|
Descriptors.MethodGenericParamMustNotBeConstrained, param.Locations.Single()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Set TermIndex of parameters. |
||||||
|
var termIndex = 1; |
||||||
|
foreach (var param in Parameters) |
||||||
|
if (param.HasTerm) |
||||||
|
param.TermIndex = termIndex++; |
||||||
|
|
||||||
|
// TODO: Handle systems with [Source]. |
||||||
|
// TODO: Validate ObserverEvents. |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using gaemstone.SourceGen.Utility; |
||||||
|
using Microsoft.CodeAnalysis; |
||||||
|
using Microsoft.CodeAnalysis.CSharp; |
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax; |
||||||
|
|
||||||
|
namespace gaemstone.SourceGen.Structure; |
||||||
|
|
||||||
|
public class ModuleEntityInfo : TypeEntityInfo |
||||||
|
{ |
||||||
|
public bool IsPartial { get; } |
||||||
|
public bool IsBuiltIn { get; } |
||||||
|
|
||||||
|
// TODO: Support [Source]. |
||||||
|
|
||||||
|
public ModuleEntityInfo(ISymbol symbol) |
||||||
|
: base(symbol) |
||||||
|
{ |
||||||
|
var classDecl = (TypeDeclarationSyntax)Symbol.DeclaringSyntaxReferences.First().GetSyntax(); |
||||||
|
IsPartial = classDecl.Modifiers.Any(t => t.IsKind(SyntaxKind.PartialKeyword)); |
||||||
|
IsBuiltIn = Has("BuiltIn"); |
||||||
|
} |
||||||
|
|
||||||
|
protected override IEnumerable<Diagnostic> ValidateSelf() |
||||||
|
{ |
||||||
|
foreach (var diag in base.ValidateSelf()) yield return diag; |
||||||
|
|
||||||
|
if (!IsPartial) yield return Diagnostic.Create( |
||||||
|
Descriptors.ModuleMustBePartial, Location); |
||||||
|
|
||||||
|
if (IsBuiltIn && (Path == null)) yield return Diagnostic.Create( |
||||||
|
Descriptors.BuiltInModuleMustHavePath, Location); |
||||||
|
} |
||||||
|
|
||||||
|
public IEnumerable<string> GetDependencies() |
||||||
|
{ |
||||||
|
foreach (var (relation, target) in RelationsToAdd) |
||||||
|
if (relation.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.Flecs.Core.DependsOn") |
||||||
|
yield return GetModulePath(target); |
||||||
|
} |
||||||
|
|
||||||
|
public string GetModulePath() |
||||||
|
=> GetModulePath(Symbol); |
||||||
|
|
||||||
|
private static string GetModulePath(ISymbol module) |
||||||
|
{ |
||||||
|
var pathAttr = module.GetAttributes().FirstOrDefault(attr => |
||||||
|
attr.AttributeClass!.GetFullName() == "gaemstone.ECS.PathAttribute"); |
||||||
|
var path = pathAttr?.ConstructorArguments.FirstOrDefault().Value as string; |
||||||
|
|
||||||
|
var isAbsolute = (path?.FirstOrDefault() == '/'); |
||||||
|
if (isAbsolute) return path!; |
||||||
|
|
||||||
|
var fullPath = module.GetFullName().Replace('.', '/'); |
||||||
|
return (path != null) |
||||||
|
? $"/{fullPath[..(fullPath.LastIndexOf('/'))]}/{path}" |
||||||
|
: $"/{fullPath}"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,216 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Collections.Immutable; |
||||||
|
using System.Linq; |
||||||
|
using gaemstone.SourceGen.Utility; |
||||||
|
using Microsoft.CodeAnalysis; |
||||||
|
|
||||||
|
namespace gaemstone.SourceGen.Structure; |
||||||
|
|
||||||
|
public class ParameterInfo : BaseInfo |
||||||
|
{ |
||||||
|
public record Pair(ITypeSymbol Relation, ITypeSymbol Target); |
||||||
|
|
||||||
|
private static readonly Dictionary<string, string> UniqueParameters = new() { |
||||||
|
["gaemstone.ECS.Iterator`1"] = "iter", |
||||||
|
["gaemstone.ECS.World"] = "iter.World", |
||||||
|
["gaemstone.ECS.World`1"] = "iter.World", |
||||||
|
["gaemstone.ECS.Entity"] = "iter.Entity(i)", |
||||||
|
["gaemstone.ECS.Entity`1"] = "iter.Entity(i)", |
||||||
|
["System.TimeSpan"] = "iter.DeltaTime", |
||||||
|
}; |
||||||
|
|
||||||
|
public new IParameterSymbol Symbol => (IParameterSymbol)base.Symbol; |
||||||
|
public new MethodEntityInfo? Parent { get => (MethodEntityInfo?)base.Parent; set => base.Parent = value; } |
||||||
|
|
||||||
|
public bool IsGeneric => Symbol.Type is INamedTypeSymbol { IsGenericType: true }; |
||||||
|
public bool IsValueType => Symbol.Type.IsValueType; |
||||||
|
public bool IsNullable => Symbol.Type.IsNullable(); |
||||||
|
public bool IsByRef => (Symbol.RefKind != RefKind.None); |
||||||
|
|
||||||
|
public ITypeSymbol? Source { get; } // TODO: Support [Source] for each term? |
||||||
|
public int? TermIndex { get; set; } // Assigned by MethodEntityInfo |
||||||
|
|
||||||
|
public string? UniqueReplacement { get; } |
||||||
|
public IReadOnlyList<object> TermTypes { get; } // Either ITypeSymbol or Pair for relationships. |
||||||
|
public ITypeSymbol? FieldType { get; } |
||||||
|
public ParameterKind Kind { get; } |
||||||
|
|
||||||
|
public bool HasTerm => (Kind != ParameterKind.Unique); |
||||||
|
public bool HasField => HasTerm && !(Kind is ParameterKind.Has or ParameterKind.Not or ParameterKind.HasOr); |
||||||
|
|
||||||
|
public ParameterInfo(ISymbol symbol) |
||||||
|
: base(symbol) |
||||||
|
{ |
||||||
|
Source = Get("Source")?.AttributeClass!.TypeArguments[0] |
||||||
|
// If the type of the parameter has the [Singleton] attribute, use it as the default Source. |
||||||
|
?? (Symbol.Type.HasAttribute("gaemstone.ECS.SingletonAttribute") ? Symbol.Type : null); |
||||||
|
|
||||||
|
var typeFullName = Symbol.Type.GetFullName(FullNameStyle.Metadata); |
||||||
|
if (UniqueParameters.TryGetValue(typeFullName, out var replacement)) |
||||||
|
{ |
||||||
|
Kind = ParameterKind.Unique; |
||||||
|
TermTypes = Array.Empty<ITypeSymbol>(); |
||||||
|
UniqueReplacement = replacement; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
var isHas = typeFullName.StartsWith("gaemstone.ECS.Has"); |
||||||
|
var isNot = typeFullName.StartsWith("gaemstone.ECS.Not"); |
||||||
|
var isOr = typeFullName.StartsWith("gaemstone.ECS.Or"); |
||||||
|
|
||||||
|
if (IsGeneric) |
||||||
|
{ |
||||||
|
var type = (INamedTypeSymbol)Symbol.Type; // Checked by IsGeneric. |
||||||
|
var args = type.TypeArguments; |
||||||
|
|
||||||
|
// Has<...> usually doesn't support a generic type as its own type parameter. |
||||||
|
// However, Has<Or<...>> is an exception to this rule, so we check for this here. |
||||||
|
if (isHas && (args is [ INamedTypeSymbol { IsGenericType: true } argType ]) |
||||||
|
&& argType.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.Or") |
||||||
|
{ |
||||||
|
TermTypes = argType.TypeArguments.ToImmutableList(); |
||||||
|
FieldType = null; |
||||||
|
isOr = true; |
||||||
|
} |
||||||
|
else if ((isHas || isNot) && (args is [ INamedTypeSymbol relation, INamedTypeSymbol target ])) |
||||||
|
{ |
||||||
|
TermTypes = ImmutableList.Create(new Pair(relation, target)); |
||||||
|
FieldType = null; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
TermTypes = args.ToImmutableList(); |
||||||
|
FieldType = IsNullable ? args[0] : type; |
||||||
|
} |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
TermTypes = ImmutableList.Create(Symbol.Type); |
||||||
|
FieldType = Symbol.Type; |
||||||
|
} |
||||||
|
|
||||||
|
Kind = (isHas && isOr) ? ParameterKind.HasOr |
||||||
|
: isHas ? ParameterKind.Has |
||||||
|
: isOr ? ParameterKind.Or |
||||||
|
: isNot ? ParameterKind.Not |
||||||
|
: IsNullable ? ParameterKind.Nullable |
||||||
|
: Symbol.RefKind switch { |
||||||
|
RefKind.In => ParameterKind.In, |
||||||
|
RefKind.Out => ParameterKind.Out, |
||||||
|
RefKind.Ref => ParameterKind.Ref, |
||||||
|
_ => ParameterKind.Normal, |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected override IEnumerable<Diagnostic> ValidateSelf() |
||||||
|
{ |
||||||
|
if (Parent == null) { yield return Diagnostic.Create( |
||||||
|
Descriptors.ParamMustBeInMethod, Location); yield break; } |
||||||
|
|
||||||
|
// TODO: Support optionals. |
||||||
|
if (Symbol.IsOptional) yield return Diagnostic.Create( |
||||||
|
Descriptors.ParamDoesNotSupportOptional, Location); |
||||||
|
|
||||||
|
if (IsByRef && !IsValueType) { yield return Diagnostic.Create( |
||||||
|
Descriptors.ParamByRefMustBeValueType, Location); yield break; } |
||||||
|
if (IsByRef && IsNullable) { yield return Diagnostic.Create( |
||||||
|
Descriptors.ParamByRefMustNotBeNullable, Location); yield break; } |
||||||
|
|
||||||
|
if (UniqueReplacement != null) |
||||||
|
{ |
||||||
|
if (Source != null) yield return Diagnostic.Create( |
||||||
|
Descriptors.UniqueParamNotSupported, Location, "[Source]"); |
||||||
|
|
||||||
|
if (IsByRef) { yield return Diagnostic.Create( |
||||||
|
Descriptors.UniqueParamMustNotBeByRef, Location); yield break; } |
||||||
|
if (IsNullable) { yield return Diagnostic.Create( |
||||||
|
Descriptors.UniqueParamMustNotBeNullable, Location); yield break; } |
||||||
|
|
||||||
|
if (IsGeneric) { |
||||||
|
if (!Parent.IsGeneric) { yield return Diagnostic.Create( |
||||||
|
Descriptors.UniqueParamGenericMustMatch, Location); yield break; } |
||||||
|
var paramParam = ((INamedTypeSymbol)Symbol.Type).TypeArguments[0]; |
||||||
|
var methodParam = Parent.Symbol.TypeParameters[0]; |
||||||
|
if (!SymbolEqualityComparer.Default.Equals(paramParam, methodParam)) { |
||||||
|
yield return Diagnostic.Create(Descriptors.UniqueParamGenericMustMatch, Location); |
||||||
|
yield break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
var isSpecial = Kind is ParameterKind.Has or ParameterKind.HasOr |
||||||
|
or ParameterKind.Or or ParameterKind.Not; |
||||||
|
if (isSpecial && IsByRef) yield return Diagnostic.Create( |
||||||
|
Descriptors.SpecialMustNotBeByRef, Location); |
||||||
|
if (isSpecial && IsNullable) yield return Diagnostic.Create( |
||||||
|
Descriptors.SpecialArgMustNotBeNullable, Location); |
||||||
|
|
||||||
|
foreach (var termTypeBase in TermTypes) { |
||||||
|
var termTypes = termTypeBase switch { |
||||||
|
ITypeSymbol type => new[]{ type }, |
||||||
|
Pair pair => new[]{ pair.Relation, pair.Target }, |
||||||
|
_ => throw new InvalidOperationException( |
||||||
|
$"Unexpected term type {termTypeBase.GetType()}") |
||||||
|
}; |
||||||
|
|
||||||
|
foreach (var termType in termTypes) { |
||||||
|
var location = termType.Locations.Single(); |
||||||
|
|
||||||
|
switch (termType.TypeKind) { |
||||||
|
case TypeKind.Array: yield return Diagnostic.Create( |
||||||
|
Descriptors.ParamMustNotBeArray, location); continue; |
||||||
|
case TypeKind.Pointer: yield return Diagnostic.Create( |
||||||
|
Descriptors.ParamMustNotBePointer, location); continue; |
||||||
|
case TypeKind.TypeParameter: yield return Diagnostic.Create( |
||||||
|
Descriptors.ParamMustNotBeGenericType, location); continue; |
||||||
|
} |
||||||
|
|
||||||
|
if (termType.IsPrimitiveType()) yield return Diagnostic.Create( |
||||||
|
Descriptors.ParamMustNotBePrimitive, location); |
||||||
|
|
||||||
|
if (isSpecial) { |
||||||
|
if (termType is INamedTypeSymbol { IsGenericType: true }) |
||||||
|
yield return Diagnostic.Create( |
||||||
|
Descriptors.SpecialArgMustNotBeGeneric, location); |
||||||
|
if (termType.IsNullable()) |
||||||
|
yield return Diagnostic.Create( |
||||||
|
Descriptors.SpecialArgMustNotBeNullable, location); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (IsGeneric && !isSpecial && !(IsValueType && IsNullable)) { |
||||||
|
yield return Diagnostic.Create( |
||||||
|
Descriptors.ParamMustNotBeGeneric, Location); |
||||||
|
yield return Diagnostic.Create( |
||||||
|
Descriptors.ParamMustNotBeGeneric, Location); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public enum ParameterKind |
||||||
|
{ |
||||||
|
Unique, // for example "Iterator<T>", "Entity<T>", "Entity" |
||||||
|
Normal, // Pass by value (copy) |
||||||
|
Nullable, // Pass by value, but may be missing (null) |
||||||
|
In, // Pass by reference (read-only) |
||||||
|
Out, // Pass by reference (write-only) |
||||||
|
Ref, // Pass by reference (read/write) |
||||||
|
Has, // Required present (no read/write) |
||||||
|
Not, // Required missing (no read/write) |
||||||
|
Or, // Only one of multiple terms is required |
||||||
|
HasOr, // Both Has and Or at the same time |
||||||
|
} |
||||||
|
|
||||||
|
// public enum TermKind |
||||||
|
// { |
||||||
|
// In, // Pass by reference (read-only) |
||||||
|
// Out, // Pass by reference (write-only) |
||||||
|
// Ref, // Pass by reference (read/write) |
||||||
|
// Has, // Required present (no read/write) |
||||||
|
// Not, // Required missing (no read/write) |
||||||
|
// } |
@ -0,0 +1,76 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using Microsoft.CodeAnalysis; |
||||||
|
|
||||||
|
namespace gaemstone.SourceGen.Structure; |
||||||
|
|
||||||
|
public class TypeEntityInfo : BaseEntityInfo |
||||||
|
{ |
||||||
|
public new INamedTypeSymbol Symbol => (INamedTypeSymbol)base.Symbol; |
||||||
|
|
||||||
|
public List<BaseEntityInfo> Children { get; } = new(); |
||||||
|
protected override IEnumerable<BaseInfo> GetChildren() => Children; |
||||||
|
|
||||||
|
public bool IsModule { get; } |
||||||
|
private bool IsEntity { get; } |
||||||
|
public bool IsTag { get; } |
||||||
|
public bool IsComponent { get; private set; } |
||||||
|
public bool IsSingleton { get; } |
||||||
|
public bool IsRelation { get; } |
||||||
|
|
||||||
|
public bool IsValueType => Symbol.IsValueType; |
||||||
|
public bool IsReferenceType => Symbol.IsReferenceType; |
||||||
|
// TODO: Make sure that component has (instance) fields, non-component entity does not have fields. |
||||||
|
|
||||||
|
public TypeEntityInfo(ISymbol symbol) |
||||||
|
: base(symbol) |
||||||
|
{ |
||||||
|
IsModule = Has("Module"); |
||||||
|
IsEntity = Has("Entity"); |
||||||
|
IsTag = Has("Tag"); |
||||||
|
IsComponent = Has("Component"); |
||||||
|
IsSingleton = Has("Singleton"); |
||||||
|
IsRelation = Has("Relation"); |
||||||
|
} |
||||||
|
|
||||||
|
protected override IEnumerable<Diagnostic> ValidateSelf() |
||||||
|
{ |
||||||
|
foreach (var diag in base.ValidateSelf()) yield return diag; |
||||||
|
|
||||||
|
if (Symbol.IsStatic) yield return Diagnostic.Create( |
||||||
|
Descriptors.TypeMustNotBeStatic, Location); |
||||||
|
if (Symbol.IsAbstract) yield return Diagnostic.Create( |
||||||
|
Descriptors.TypeMustNotBeAbstract, Location); |
||||||
|
|
||||||
|
var attributeList = new List<string>(); |
||||||
|
if (IsModule) attributeList.Add("Module"); |
||||||
|
if (IsEntity) attributeList.Add("Entity"); |
||||||
|
if (IsRelation) attributeList.Add("Relation"); |
||||||
|
if (IsTag) attributeList.Add("Tag"); |
||||||
|
if (IsComponent) attributeList.Add("Component"); |
||||||
|
if (IsSingleton) attributeList.Add("Singleton"); |
||||||
|
|
||||||
|
switch (attributeList) { |
||||||
|
// A single attribute is valid. |
||||||
|
case [ _ ]: |
||||||
|
// The following are valid attribute combinations. |
||||||
|
case [ "Module", "Singleton" ]: |
||||||
|
case [ "Relation", "Tag" ]: |
||||||
|
case [ "Relation", "Component" ]: |
||||||
|
break; |
||||||
|
default: |
||||||
|
yield return Diagnostic.Create( |
||||||
|
Descriptors.InvalidAttributeCombination, Location, |
||||||
|
$"[{string.Join(", ", attributeList)}]"); |
||||||
|
if (IsModule) yield return Diagnostic.Create( |
||||||
|
Descriptors.ValidModuleAttributesHint, Location); |
||||||
|
if (IsRelation) yield return Diagnostic.Create( |
||||||
|
Descriptors.ValidRelationAttributesHint, Location); |
||||||
|
if (IsSingleton && IsComponent) yield return Diagnostic.Create( |
||||||
|
Descriptors.SingletonImpliesComponentHint, Location); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// Singletons are special kinds of components. |
||||||
|
if (IsSingleton) IsComponent = true; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
// Unfortunately, this is necessary for record types or init |
||||||
|
// properties to work under netstandard2.0 target framework. |
||||||
|
namespace System.Runtime.CompilerServices |
||||||
|
{ |
||||||
|
public static class IsExternalInit |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
@ -1,16 +0,0 @@ |
|||||||
using System; |
|
||||||
using System.Linq; |
|
||||||
using gaemstone.Utility.IL; |
|
||||||
|
|
||||||
namespace gaemstone.ECS; |
|
||||||
|
|
||||||
public static class FilterExtensions |
|
||||||
{ |
|
||||||
public static void RunOnce(World world, Delegate action) |
|
||||||
{ |
|
||||||
var gen = IterActionGenerator.GetOrBuild(world, action.Method); |
|
||||||
var desc = new FilterDesc(gen.Terms.ToArray()); |
|
||||||
using var filter = new Filter(world, desc); |
|
||||||
foreach (var iter in filter.Iter()) gen.RunWithTryCatch(action.Target, iter); |
|
||||||
} |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
using System; |
|
||||||
|
|
||||||
namespace gaemstone.ECS; |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Entity for storing global game state and configuration. |
|
||||||
/// Parameters can use <see cref="GameAttribute"/> to source this entity. |
|
||||||
/// </summary> |
|
||||||
public struct Game { } |
|
||||||
|
|
||||||
/// <summary> Equivalent to <see cref="SourceAttribute{Game}"/>. </summary> |
|
||||||
[AttributeUsage(AttributeTargets.Parameter)] |
|
||||||
public class GameAttribute : SourceAttribute<Game> { } |
|
@ -1,45 +0,0 @@ |
|||||||
using System; |
|
||||||
using System.Linq; |
|
||||||
using System.Reflection; |
|
||||||
using gaemstone.Utility; |
|
||||||
using gaemstone.Utility.IL; |
|
||||||
|
|
||||||
namespace gaemstone.ECS; |
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method)] |
|
||||||
public class ObserverAttribute : Attribute |
|
||||||
{ |
|
||||||
public Type Event { get; } |
|
||||||
internal ObserverAttribute(Type @event) => Event = @event; // Use generic type instead. |
|
||||||
} |
|
||||||
public class ObserverAttribute<TEvent> : ObserverAttribute |
|
||||||
{ public ObserverAttribute() : base(typeof(TEvent)) { } } |
|
||||||
|
|
||||||
public static class ObserverExtensions |
|
||||||
{ |
|
||||||
public static EntityRef InitObserver(this World world, |
|
||||||
object? instance, MethodInfo method) |
|
||||||
{ |
|
||||||
var attr = method.Get<ObserverAttribute>() ?? throw new ArgumentException( |
|
||||||
"Observer must specify ObserverAttribute", nameof(method)); |
|
||||||
var expr = method.Get<ExpressionAttribute>()?.Value; |
|
||||||
FilterDesc filter; |
|
||||||
Action<Iterator> iterAction; |
|
||||||
|
|
||||||
var param = method.GetParameters(); |
|
||||||
if ((param.Length == 1) && (param[0].ParameterType == typeof(Iterator))) { |
|
||||||
filter = new(expr ?? throw new Exception( |
|
||||||
"Observer must specify ExpressionAttribute")); |
|
||||||
if (method.IsStatic) instance = null; |
|
||||||
iterAction = (Action<Iterator>)Delegate.CreateDelegate( |
|
||||||
typeof(Action<Iterator>), instance, method); |
|
||||||
} else { |
|
||||||
var gen = IterActionGenerator.GetOrBuild(world, method); |
|
||||||
filter = (expr != null) ? new(expr) : new(gen.Terms.ToArray()); |
|
||||||
iterAction = iter => gen.RunWithTryCatch(instance, iter); |
|
||||||
} |
|
||||||
|
|
||||||
var @event = world.LookupByTypeOrThrow(attr.Event); |
|
||||||
return world.New(method.Name).Build().InitObserver(@event, filter, iterAction); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,88 @@ |
|||||||
|
using System; |
||||||
|
using gaemstone.Utility; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] |
||||||
|
public class SourceAttribute<T> : Attribute { } |
||||||
|
|
||||||
|
|
||||||
|
// TODO: Implement [Pair] somehow. |
||||||
|
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.GenericParameter)] |
||||||
|
public class PairAttribute<TRelation, TTarget> : Attribute { } |
||||||
|
|
||||||
|
public static class Pair<TRelation, TTarget> |
||||||
|
{ |
||||||
|
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.GenericParameter)] |
||||||
|
public class RelationAttribute : Attribute { } |
||||||
|
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.GenericParameter)] |
||||||
|
public class TargetAttribute : Attribute { } |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public readonly ref struct Has<T> { } |
||||||
|
public readonly ref struct Has<TRelation, TTarget> { } |
||||||
|
|
||||||
|
public readonly ref struct Not<T> { } |
||||||
|
public readonly ref struct Not<TRelation, TTarget> { } |
||||||
|
|
||||||
|
|
||||||
|
public readonly struct Or<T1, T2> |
||||||
|
{ |
||||||
|
private readonly Union<T1, T2> _union; |
||||||
|
public int Case { get; } |
||||||
|
|
||||||
|
public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; } |
||||||
|
public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; } |
||||||
|
|
||||||
|
public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException(); |
||||||
|
public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException(); |
||||||
|
} |
||||||
|
|
||||||
|
public readonly struct Or<T1, T2, T3> |
||||||
|
{ |
||||||
|
private readonly Union<T1, T2, T3> _union; |
||||||
|
public int Case { get; } |
||||||
|
|
||||||
|
public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; } |
||||||
|
public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; } |
||||||
|
public Or(T3 value) { Case = 3; _union = new() { Value3 = value }; } |
||||||
|
|
||||||
|
public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException(); |
||||||
|
public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException(); |
||||||
|
public T3 Value3 => (Case == 3) ? _union.Value3 : throw new InvalidOperationException(); |
||||||
|
} |
||||||
|
|
||||||
|
public readonly struct Or<T1, T2, T3, T4> |
||||||
|
{ |
||||||
|
private readonly Union<T1, T2, T3, T4> _union; |
||||||
|
public int Case { get; } |
||||||
|
|
||||||
|
public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; } |
||||||
|
public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; } |
||||||
|
public Or(T3 value) { Case = 3; _union = new() { Value3 = value }; } |
||||||
|
public Or(T4 value) { Case = 4; _union = new() { Value4 = value }; } |
||||||
|
|
||||||
|
public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException(); |
||||||
|
public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException(); |
||||||
|
public T3 Value3 => (Case == 3) ? _union.Value3 : throw new InvalidOperationException(); |
||||||
|
public T4 Value4 => (Case == 4) ? _union.Value4 : throw new InvalidOperationException(); |
||||||
|
} |
||||||
|
|
||||||
|
public readonly struct Or<T1, T2, T3, T4, T5> |
||||||
|
{ |
||||||
|
private readonly Union<T1, T2, T3, T4, T5> _union; |
||||||
|
public int Case { get; } |
||||||
|
|
||||||
|
public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; } |
||||||
|
public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; } |
||||||
|
public Or(T3 value) { Case = 3; _union = new() { Value3 = value }; } |
||||||
|
public Or(T4 value) { Case = 4; _union = new() { Value4 = value }; } |
||||||
|
public Or(T5 value) { Case = 5; _union = new() { Value5 = value }; } |
||||||
|
|
||||||
|
public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException(); |
||||||
|
public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException(); |
||||||
|
public T3 Value3 => (Case == 3) ? _union.Value3 : throw new InvalidOperationException(); |
||||||
|
public T4 Value4 => (Case == 4) ? _union.Value4 : throw new InvalidOperationException(); |
||||||
|
public T5 Value5 => (Case == 5) ? _union.Value5 : throw new InvalidOperationException(); |
||||||
|
} |
@ -1,32 +0,0 @@ |
|||||||
using System; |
|
||||||
|
|
||||||
namespace gaemstone.ECS; |
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] |
|
||||||
public class SourceAttribute : Attribute |
|
||||||
{ |
|
||||||
public Type Type { get; } |
|
||||||
// TODO: Support path as source too. |
|
||||||
internal SourceAttribute(Type type) => Type = type; |
|
||||||
} |
|
||||||
public class SourceAttribute<TEntity> : SourceAttribute |
|
||||||
{ public SourceAttribute() : base(typeof(TEntity)) { } } |
|
||||||
|
|
||||||
|
|
||||||
// Parameters types marked with [Tag] are equivalent to [Has]. |
|
||||||
[AttributeUsage(AttributeTargets.Parameter)] |
|
||||||
public class HasAttribute : Attribute { } |
|
||||||
|
|
||||||
// Parameters with "in" modifier are equivalent to [In]. |
|
||||||
[AttributeUsage(AttributeTargets.Parameter)] |
|
||||||
public class InAttribute : Attribute { } |
|
||||||
|
|
||||||
// Parameters with "out" modifier are equivalent to [Out]. |
|
||||||
[AttributeUsage(AttributeTargets.Parameter)] |
|
||||||
public class OutAttribute : Attribute { } |
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Parameter)] |
|
||||||
public class OrAttribute : Attribute { } |
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Parameter)] |
|
||||||
public class NotAttribute : Attribute { } |
|
@ -1,18 +0,0 @@ |
|||||||
using gaemstone.ECS; |
|
||||||
|
|
||||||
namespace gaemstone.Flecs; |
|
||||||
|
|
||||||
[Module, Path("/flecs/core")] |
|
||||||
public static partial class DeletionEvent |
|
||||||
{ |
|
||||||
[Relation, Tag] public struct OnDelete { } |
|
||||||
[Relation, Tag] public struct OnDeleteTarget { } |
|
||||||
} |
|
||||||
|
|
||||||
[Module, Path("/flecs/core")] |
|
||||||
public static partial class DeletionBehavior |
|
||||||
{ |
|
||||||
[Tag] public struct Remove { } |
|
||||||
[Tag] public struct Delete { } |
|
||||||
[Tag] public struct Panic { } |
|
||||||
} |
|
@ -1,14 +0,0 @@ |
|||||||
using gaemstone.ECS; |
|
||||||
|
|
||||||
namespace gaemstone.Flecs; |
|
||||||
|
|
||||||
[Module, Path("/flecs/core")] |
|
||||||
public static partial class ObserverEvent |
|
||||||
{ |
|
||||||
[Entity] public struct OnAdd { } |
|
||||||
[Entity] public struct OnRemove { } |
|
||||||
[Entity] public struct OnSet { } |
|
||||||
[Entity] public struct UnSet { } |
|
||||||
[Entity] public struct OnTableEmpty { } |
|
||||||
[Entity] public struct OnTableFilled { } |
|
||||||
} |
|
@ -1,91 +0,0 @@ |
|||||||
using gaemstone.ECS; |
|
||||||
using static gaemstone.Flecs.Pipeline; |
|
||||||
|
|
||||||
namespace gaemstone.Flecs; |
|
||||||
|
|
||||||
[Module, Path("/flecs/pipeline")] |
|
||||||
public static partial class SystemPhase |
|
||||||
{ |
|
||||||
[Entity, Add<Phase>] |
|
||||||
public struct PreFrame { } |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// This phase contains all the systems that load data into your ECS. |
|
||||||
/// This would be a good place to load keyboard and mouse inputs. |
|
||||||
/// </summary> |
|
||||||
[Entity, Add<Phase>] |
|
||||||
[DependsOn<PreFrame>] |
|
||||||
public struct OnLoad { } |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Often the imported data needs to be processed. Maybe you want to associate |
|
||||||
/// your keypresses with high level actions rather than comparing explicitly |
|
||||||
/// in your game code if the user pressed the 'K' key. |
|
||||||
/// </summary> |
|
||||||
[Entity, Add<Phase>] |
|
||||||
[DependsOn<OnLoad>] |
|
||||||
public struct PostLoad { } |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Now that the input is loaded and processed, it's time to get ready to |
|
||||||
/// start processing our game logic. Anything that needs to happen after |
|
||||||
/// input processing but before processing the game logic can happen here. |
|
||||||
/// This can be a good place to prepare the frame, maybe clean up some |
|
||||||
/// things from the previous frame, etcetera. |
|
||||||
/// </summary> |
|
||||||
[Entity, Add<Phase>] |
|
||||||
[DependsOn<PostLoad>] |
|
||||||
public struct PreUpdate { } |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// This is usually where the magic happens! This is where you put all of |
|
||||||
/// your gameplay systems. By default systems are added to this phase. |
|
||||||
/// </summary> |
|
||||||
[Entity, Add<Phase>] |
|
||||||
[DependsOn<PreUpdate>] |
|
||||||
public struct OnUpdate { } |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// This phase was introduced to deal with validating the state of the game |
|
||||||
/// after processing the gameplay systems. Sometimes you entities too close |
|
||||||
/// to each other, or the speed of an entity is increased too much. |
|
||||||
/// This phase is for righting that wrong. A typical feature to implement |
|
||||||
/// in this phase would be collision detection. |
|
||||||
/// </summary> |
|
||||||
[Entity, Add<Phase>] |
|
||||||
[DependsOn<OnUpdate>] |
|
||||||
public struct OnValidate { } |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// When your game logic has been updated, and your validation pass has ran, |
|
||||||
/// you may want to apply some corrections. For example, if your collision |
|
||||||
/// detection system detected collisions in the <c>OnValidate</c> phase, |
|
||||||
/// you may want to move the entities so that they no longer overlap. |
|
||||||
/// </summary> |
|
||||||
[Entity, Add<Phase>] |
|
||||||
[DependsOn<OnValidate>] |
|
||||||
public struct PostUpdate { } |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Now that all of the frame data is computed, validated and corrected for, |
|
||||||
/// it is time to prepare the frame for rendering. Any systems that need to |
|
||||||
/// run before rendering, but after processing the game logic should go here. |
|
||||||
/// A good example would be a system that calculates transform matrices from |
|
||||||
/// a scene graph. |
|
||||||
/// </summary> |
|
||||||
[Entity, Add<Phase>] |
|
||||||
[DependsOn<PostUpdate>] |
|
||||||
public struct PreStore { } |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// This is where it all comes together. Your frame is ready to be |
|
||||||
/// rendered, and that is exactly what you would do in this phase. |
|
||||||
/// </summary> |
|
||||||
[Entity, Add<Phase>] |
|
||||||
[DependsOn<PreStore>] |
|
||||||
public struct OnStore { } |
|
||||||
|
|
||||||
[Entity, Add<Phase>] |
|
||||||
[DependsOn<OnStore>] |
|
||||||
public struct PostFrame { } |
|
||||||
} |
|
@ -1,299 +0,0 @@ |
|||||||
using System; |
|
||||||
using System.Collections.Generic; |
|
||||||
using System.Linq; |
|
||||||
using System.Reflection; |
|
||||||
using System.Reflection.Emit; |
|
||||||
using System.Text; |
|
||||||
|
|
||||||
namespace gaemstone.Utility.IL; |
|
||||||
|
|
||||||
public class ILGeneratorWrapper |
|
||||||
{ |
|
||||||
private readonly DynamicMethod _method; |
|
||||||
private readonly ILGenerator _il; |
|
||||||
private readonly List<ILocal> _locals = new(); |
|
||||||
private readonly List<(int Offset, int Indent, OpCode Code, object? Arg)> _instructions = new(); |
|
||||||
private readonly Dictionary<Label, int> _labelToOffset = new(); |
|
||||||
private readonly Stack<BlockImpl> _indents = new(); |
|
||||||
|
|
||||||
public ILGeneratorWrapper(DynamicMethod method) |
|
||||||
{ |
|
||||||
_method = method; |
|
||||||
_il = method.GetILGenerator(); |
|
||||||
} |
|
||||||
|
|
||||||
public string ToReadableString() |
|
||||||
{ |
|
||||||
var sb = new StringBuilder(); |
|
||||||
sb.AppendLine("Parameters:"); |
|
||||||
foreach (var (param, index) in _method.GetParameters().Select((p, i) => (p, i))) |
|
||||||
sb.AppendLine($" Argument({index}, {param.ParameterType.GetFriendlyName()})"); |
|
||||||
sb.AppendLine("Return:"); |
|
||||||
sb.AppendLine($" {_method.ReturnType.GetFriendlyName()}"); |
|
||||||
sb.AppendLine(); |
|
||||||
|
|
||||||
sb.AppendLine("Locals:"); |
|
||||||
foreach (var local in _locals) |
|
||||||
sb.AppendLine($" {local}"); |
|
||||||
sb.AppendLine(); |
|
||||||
|
|
||||||
sb.AppendLine("Instructions:"); |
|
||||||
foreach (var (offset, indent, code, arg) in _instructions) { |
|
||||||
sb.Append(" "); |
|
||||||
|
|
||||||
// Append instruction offset. |
|
||||||
if (offset < 0) sb.Append(" "); |
|
||||||
else sb.Append($"0x{offset:X4} "); |
|
||||||
|
|
||||||
// Append instruction opcode. |
|
||||||
if (code == OpCodes.Nop) sb.Append(" "); |
|
||||||
else sb.Append($"{code.Name,-12}"); |
|
||||||
|
|
||||||
// Append indents. |
|
||||||
for (var i = 0; i < indent; i++) |
|
||||||
sb.Append("| "); |
|
||||||
|
|
||||||
// Append instruction argument. |
|
||||||
if (code == OpCodes.Nop) sb.Append("// "); |
|
||||||
switch (arg) { |
|
||||||
case Label label: sb.Append($"Label(0x{_labelToOffset.GetValueOrDefault(label, -1):X4})"); break; |
|
||||||
case not null: sb.Append(arg); break; |
|
||||||
} |
|
||||||
|
|
||||||
sb.AppendLine(); |
|
||||||
} |
|
||||||
return sb.ToString(); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
public IArgument Argument(int index) |
|
||||||
{ |
|
||||||
var type = _method.GetParameters()[index].ParameterType; |
|
||||||
if (type.IsByRefLike) return new ArgumentImpl(index, type); |
|
||||||
return (IArgument)Activator.CreateInstance(typeof(ArgumentImpl<>).MakeGenericType(type), index)!; |
|
||||||
} |
|
||||||
public IArgument<T> Argument<T>(int index) => (IArgument<T>)Argument(index); |
|
||||||
|
|
||||||
public ILocal Local(Type type, string? name = null) |
|
||||||
{ |
|
||||||
var builder = _il.DeclareLocal(type); |
|
||||||
var local = type.IsByRefLike ? new LocalImpl(builder, name) |
|
||||||
: (ILocal)Activator.CreateInstance(typeof(LocalImpl<>).MakeGenericType(type), builder, name)!; |
|
||||||
_locals.Add(local); |
|
||||||
return local; |
|
||||||
} |
|
||||||
public ILocal<T> Local<T>(string? name = null) => (ILocal<T>)Local(typeof(T), name); |
|
||||||
public ILocal<Array> LocalArray(Type type, string? name = null) => (ILocal<Array>)Local(type.MakeArrayType(), name); |
|
||||||
public ILocal<T[]> LocalArray<T>(string? name = null) => (ILocal<T[]>)Local(typeof(T).MakeArrayType(), name); |
|
||||||
|
|
||||||
public Label DefineLabel() => _il.DefineLabel(); |
|
||||||
public void MarkLabel(Label label) |
|
||||||
{ |
|
||||||
_instructions.Add((-1, _indents.Count, OpCodes.Nop, label)); |
|
||||||
_labelToOffset.Add(label, _il.ILOffset); |
|
||||||
_il.MarkLabel(label); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
private void AddInstr(OpCode code, object? arg = null) => _instructions.Add((_il.ILOffset, _indents.Count, code, arg)); |
|
||||||
public void Comment(string comment) => _instructions.Add((-1, _indents.Count, OpCodes.Nop, comment)); |
|
||||||
|
|
||||||
private static readonly MethodInfo _writeLine = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) })!; |
|
||||||
public void Log(string str) { Load(str); Call(_writeLine); } |
|
||||||
|
|
||||||
internal void Emit(OpCode code) { AddInstr(code, null); _il.Emit(code); } |
|
||||||
internal void Emit(OpCode code, int arg) { AddInstr(code, arg); _il.Emit(code, arg); } |
|
||||||
internal void Emit(OpCode code, string arg) { AddInstr(code, arg); _il.Emit(code, arg); } |
|
||||||
internal void Emit(OpCode code, Type type) { AddInstr(code, type); _il.Emit(code, type); } |
|
||||||
internal void Emit(OpCode code, Label label) { AddInstr(code, label); _il.Emit(code, label); } |
|
||||||
internal void Emit(OpCode code, ILocal local) { AddInstr(code, local); _il.Emit(code, local.Builder); } |
|
||||||
internal void Emit(OpCode code, IArgument arg) { AddInstr(code, arg); _il.Emit(code, arg.Index); } |
|
||||||
internal void Emit(OpCode code, MethodInfo method) { AddInstr(code, method); _il.Emit(code, method); } |
|
||||||
internal void Emit(OpCode code, ConstructorInfo constr) { AddInstr(code, constr); _il.Emit(code, constr); } |
|
||||||
|
|
||||||
public void Dup() => Emit(OpCodes.Dup); |
|
||||||
public void Pop() => Emit(OpCodes.Pop); |
|
||||||
|
|
||||||
public void LoadNull() => Emit(OpCodes.Ldnull); |
|
||||||
public void LoadConst(int value) => Emit(OpCodes.Ldc_I4, value); |
|
||||||
public void Load(string value) => Emit(OpCodes.Ldstr, value); |
|
||||||
|
|
||||||
private static readonly MethodInfo _typeFromHandleMethod = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!; |
|
||||||
public void Typeof(Type value) { Emit(OpCodes.Ldtoken, value); Call(_typeFromHandleMethod); } |
|
||||||
|
|
||||||
public void Load(IArgument arg) => Emit(OpCodes.Ldarg, arg); |
|
||||||
public void LoadAddr(IArgument arg) => Emit(OpCodes.Ldarga, arg); |
|
||||||
|
|
||||||
public void Load(ILocal local) => Emit(OpCodes.Ldloc, local); |
|
||||||
public void LoadAddr(ILocal local) => Emit(OpCodes.Ldloca, local); |
|
||||||
public void Store(ILocal local) => Emit(OpCodes.Stloc, local); |
|
||||||
public void Set(ILocal<int> local, int value) { LoadConst(value); Store(local); } |
|
||||||
|
|
||||||
public void LoadLength() { Emit(OpCodes.Ldlen); Emit(OpCodes.Conv_I4); } |
|
||||||
public void LoadLength(IArgument<Array> array) { Load(array); LoadLength(); } |
|
||||||
public void LoadLength(ILocal<Array> array) { Load(array); LoadLength(); } |
|
||||||
|
|
||||||
public void LoadObj(Type type) => Emit(OpCodes.Ldobj, type); |
|
||||||
public void LoadObj<T>() where T : struct => LoadObj(typeof(T)); |
|
||||||
public void LoadObj(ILocal local) { LoadAddr(local); LoadObj(local.LocalType); } |
|
||||||
public void LoadObj(IArgument arg) { LoadAddr(arg); LoadObj(arg.ArgumentType); } |
|
||||||
|
|
||||||
public void LoadElem(Type type) => Emit(OpCodes.Ldelem, type); |
|
||||||
public void LoadElem(Type type, int index) { LoadConst(index); LoadElem(type); } |
|
||||||
public void LoadElem(Type type, ILocal<int> index) { Load(index); LoadElem(type); } |
|
||||||
public void LoadElem(Type type, IArgument<Array> array, int index) { Load(array); LoadElem(type, index); } |
|
||||||
public void LoadElem(Type type, IArgument<Array> array, ILocal<int> index) { Load(array); LoadElem(type, index); } |
|
||||||
public void LoadElem(Type type, ILocal<Array> array, int index) { Load(array); LoadElem(type, index); } |
|
||||||
public void LoadElem(Type type, ILocal<Array> array, ILocal<int> index) { Load(array); LoadElem(type, index); } |
|
||||||
|
|
||||||
public void LoadElemRef() => Emit(OpCodes.Ldelem_Ref); |
|
||||||
public void LoadElemRef(int index) { LoadConst(index); LoadElemRef(); } |
|
||||||
public void LoadElemRef(ILocal<int> index) { Load(index); LoadElemRef(); } |
|
||||||
public void LoadElemRef(IArgument<Array> array, int index) { Load(array); LoadElemRef(index); } |
|
||||||
public void LoadElemRef(IArgument<Array> array, ILocal<int> index) { Load(array); LoadElemRef(index); } |
|
||||||
public void LoadElemRef(ILocal<Array> array, int index) { Load(array); LoadElemRef(index); } |
|
||||||
public void LoadElemRef(ILocal<Array> array, ILocal<int> index) { Load(array); LoadElemRef(index); } |
|
||||||
|
|
||||||
public void LoadElemEither(Type type) { if (type.IsValueType) LoadElem(type); else LoadElemRef(); } |
|
||||||
public void LoadElemEither(Type type, int index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); } |
|
||||||
public void LoadElemEither(Type type, ILocal<int> index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); } |
|
||||||
public void LoadElemEither(Type type, IArgument<Array> array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } |
|
||||||
public void LoadElemEither(Type type, IArgument<Array> array, ILocal<int> index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } |
|
||||||
public void LoadElemEither(Type type, ILocal<Array> array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } |
|
||||||
public void LoadElemEither(Type type, ILocal<Array> array, ILocal<int> index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } |
|
||||||
|
|
||||||
public void LoadElemAddr(Type type) => Emit(OpCodes.Ldelema, type); |
|
||||||
public void LoadElemAddr(Type type, int index) { LoadConst(index); LoadElemAddr(type); } |
|
||||||
public void LoadElemAddr(Type type, ILocal<int> index) { Load(index); LoadElemAddr(type); } |
|
||||||
public void LoadElemAddr(Type type, IArgument<Array> array, int index) { Load(array); LoadElemAddr(type, index); } |
|
||||||
public void LoadElemAddr(Type type, IArgument<Array> array, ILocal<int> index) { Load(array); LoadElemAddr(type, index); } |
|
||||||
public void LoadElemAddr(Type type, ILocal<Array> array, int index) { Load(array); LoadElemAddr(type, index); } |
|
||||||
public void LoadElemAddr(Type type, ILocal<Array> array, ILocal<int> index) { Load(array); LoadElemAddr(type, index); } |
|
||||||
|
|
||||||
public void Load(PropertyInfo info) => CallVirt(info.GetMethod!); |
|
||||||
public void Load(ILocal obj, PropertyInfo info) { Load(obj); Load(info); } |
|
||||||
public void Load(IArgument obj, PropertyInfo info) { Load(obj); Load(info); } |
|
||||||
|
|
||||||
public void Add() => Emit(OpCodes.Add); |
|
||||||
public void Increment(ILocal<int> local) { Load(local); LoadConst(1); Add(); Store(local); } |
|
||||||
|
|
||||||
public void Init(Type type) => Emit(OpCodes.Initobj, type); |
|
||||||
public void Init<T>() where T : struct => Init(typeof(T)); |
|
||||||
|
|
||||||
public void New(ConstructorInfo constructor) => Emit(OpCodes.Newobj, constructor); |
|
||||||
public void New(Type type) => New(type.GetConstructors().Single()); |
|
||||||
public void New(Type type, params Type[] paramTypes) => New(type.GetConstructor(paramTypes)!); |
|
||||||
|
|
||||||
public void Cast(Type type) => Emit(OpCodes.Castclass, type); |
|
||||||
public void Cast<T>() => Cast(typeof(T)); |
|
||||||
public void Box(Type type) => Emit(OpCodes.Box, type); |
|
||||||
public void Box<T>() => Box(typeof(T)); |
|
||||||
|
|
||||||
public void Goto(Label label) => Emit(OpCodes.Br, label); |
|
||||||
public void GotoIfTrue(Label label) => Emit(OpCodes.Brtrue, label); |
|
||||||
public void GotoIfFalse(Label label) => Emit(OpCodes.Brfalse, label); |
|
||||||
|
|
||||||
public void GotoIf(Label label, ILocal<int> a, Comparison op, ILocal<int> b) |
|
||||||
{ Load(a); Load(b); Emit(op.Code, label); } |
|
||||||
public void GotoIfNull(Label label, ILocal<object> local) |
|
||||||
{ Load(local); Emit(OpCodes.Brfalse, label); } |
|
||||||
public void GotoIfNotNull(Label label, ILocal<object> local) |
|
||||||
{ Load(local); Emit(OpCodes.Brtrue, label); } |
|
||||||
|
|
||||||
public void Call(MethodInfo method) => Emit(OpCodes.Call, method); |
|
||||||
public void CallVirt(MethodInfo method) => Emit(OpCodes.Callvirt, method); |
|
||||||
|
|
||||||
private static readonly MethodInfo _consoleWriteLineMethod = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) })!; |
|
||||||
public void Print(string str) { Load(str); Call(_consoleWriteLineMethod); } |
|
||||||
|
|
||||||
public void Return() => Emit(OpCodes.Ret); |
|
||||||
|
|
||||||
|
|
||||||
public delegate void WhileBodyAction(Label continueLabel, Label breakLabel); |
|
||||||
public delegate void WhileTestAction(Label continueLabel); |
|
||||||
|
|
||||||
public void While(string name, WhileTestAction testAction, WhileBodyAction bodyAction) { |
|
||||||
var bodyLabel = DefineLabel(); |
|
||||||
var testLabel = DefineLabel(); |
|
||||||
var breakLabel = DefineLabel(); |
|
||||||
|
|
||||||
Goto(testLabel); |
|
||||||
Comment("BEGIN LOOP " + name); |
|
||||||
MarkLabel(bodyLabel); |
|
||||||
using (Indent()) bodyAction(testLabel, breakLabel); |
|
||||||
Comment("TEST LOOP " + name); |
|
||||||
MarkLabel(testLabel); |
|
||||||
using (Indent()) testAction(bodyLabel); |
|
||||||
Comment("END LOOP " + name); |
|
||||||
MarkLabel(breakLabel); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
public IDisposable Block(Action onClose) |
|
||||||
=> new BlockImpl(onClose); |
|
||||||
public IDisposable Indent() |
|
||||||
{ |
|
||||||
BlockImpl indent = null!; |
|
||||||
indent = new(() => { if (_indents.Pop() != indent) throw new InvalidOperationException(); }); |
|
||||||
_indents.Push(indent); |
|
||||||
return indent; |
|
||||||
} |
|
||||||
|
|
||||||
internal class BlockImpl : IDisposable |
|
||||||
{ |
|
||||||
public Action OnClose { get; } |
|
||||||
public BlockImpl(Action onClose) => OnClose = onClose; |
|
||||||
public void Dispose() => OnClose(); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
internal class ArgumentImpl : IArgument |
|
||||||
{ |
|
||||||
public int Index { get; } |
|
||||||
public Type ArgumentType { get; } |
|
||||||
public ArgumentImpl(int index, Type type) { Index = index; ArgumentType = type; } |
|
||||||
public override string ToString() => $"Argument({Index}, {ArgumentType.GetFriendlyName()})"; |
|
||||||
} |
|
||||||
internal class ArgumentImpl<T> : ArgumentImpl, IArgument<T> |
|
||||||
{ public ArgumentImpl(int index) : base(index, typeof(T)) { } } |
|
||||||
|
|
||||||
internal class LocalImpl : ILocal |
|
||||||
{ |
|
||||||
public LocalBuilder Builder { get; } |
|
||||||
public string? Name { get; } |
|
||||||
public Type LocalType => Builder.LocalType; |
|
||||||
public LocalImpl(LocalBuilder builder, string? name) { Builder = builder; Name = name; } |
|
||||||
public override string ToString() => $"Local({Builder.LocalIndex}, {LocalType.GetFriendlyName()}){(Name != null ? $" // {Name}" : "")}"; |
|
||||||
} |
|
||||||
internal class LocalImpl<T> : LocalImpl, ILocal<T> |
|
||||||
{ public LocalImpl(LocalBuilder builder, string? name) : base(builder, name) { } } |
|
||||||
} |
|
||||||
|
|
||||||
public class Comparison |
|
||||||
{ |
|
||||||
public static Comparison NotEqual { get; } = new(OpCodes.Bne_Un); |
|
||||||
public static Comparison LessThan { get; } = new(OpCodes.Blt); |
|
||||||
public static Comparison LessOrEq { get; } = new(OpCodes.Ble); |
|
||||||
public static Comparison Equal { get; } = new(OpCodes.Beq); |
|
||||||
public static Comparison GreaterOrEq { get; } = new(OpCodes.Bge); |
|
||||||
public static Comparison GreaterThan { get; } = new(OpCodes.Bgt); |
|
||||||
|
|
||||||
public OpCode Code { get; } |
|
||||||
private Comparison(OpCode code) => Code = code; |
|
||||||
} |
|
||||||
|
|
||||||
public interface IArgument |
|
||||||
{ |
|
||||||
int Index { get; } |
|
||||||
Type ArgumentType { get; } |
|
||||||
} |
|
||||||
public interface IArgument<out T> |
|
||||||
: IArgument { } |
|
||||||
|
|
||||||
public interface ILocal |
|
||||||
{ |
|
||||||
LocalBuilder Builder { get; } |
|
||||||
Type LocalType { get; } |
|
||||||
} |
|
||||||
public interface ILocal<out T> |
|
||||||
: ILocal { } |
|
@ -1,404 +0,0 @@ |
|||||||
using System; |
|
||||||
using System.Collections.Generic; |
|
||||||
using System.Collections.Immutable; |
|
||||||
using System.Linq; |
|
||||||
using System.Reflection; |
|
||||||
using System.Reflection.Emit; |
|
||||||
using System.Runtime.CompilerServices; |
|
||||||
using gaemstone.ECS; |
|
||||||
|
|
||||||
namespace gaemstone.Utility.IL; |
|
||||||
|
|
||||||
// TODO: Support tuple syntax to match relationship pairs. |
|
||||||
public unsafe class IterActionGenerator |
|
||||||
{ |
|
||||||
private static readonly ConstructorInfo _entityRefCtor = typeof(EntityRef).GetConstructors().Single(); |
|
||||||
|
|
||||||
private static readonly PropertyInfo _iterWorldProp = typeof(Iterator).GetProperty(nameof(Iterator.World))!; |
|
||||||
private static readonly PropertyInfo _iterDeltaTimeProp = typeof(Iterator).GetProperty(nameof(Iterator.DeltaTime))!; |
|
||||||
private static readonly PropertyInfo _iterCountProp = typeof(Iterator).GetProperty(nameof(Iterator.Count))!; |
|
||||||
private static readonly MethodInfo _iterEntityMethod = typeof(Iterator).GetMethod(nameof(Iterator.Entity))!; |
|
||||||
private static readonly MethodInfo _iterFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field ), 1, new[] { typeof(int) })!; |
|
||||||
private static readonly MethodInfo _iterFieldOrEmptyMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldOrEmpty), 1, new[] { typeof(int) })!; |
|
||||||
private static readonly MethodInfo _iterFieldRefMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field ), 1, new[] { typeof(int), Type.MakeGenericMethodParameter(0) })!; |
|
||||||
private static readonly MethodInfo _iterFieldOrEmptyRefMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldOrEmpty), 1, new[] { typeof(int), Type.MakeGenericMethodParameter(0) })!; |
|
||||||
|
|
||||||
private static readonly ConditionalWeakTable<MethodInfo, IterActionGenerator> _cache = new(); |
|
||||||
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>>> _globalUniqueParameters = new() { |
|
||||||
[typeof(World)] = (IL, iter) => { IL.Load(iter, _iterWorldProp); }, |
|
||||||
[typeof(Universe)] = (IL, iter) => { IL.Load(iter, _iterWorldProp); IL.Cast(typeof(Universe)); }, |
|
||||||
[typeof(TimeSpan)] = (IL, iter) => { IL.Load(iter, _iterDeltaTimeProp); }, |
|
||||||
}; |
|
||||||
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>, ILocal<int>>> _uniqueParameters = new() { |
|
||||||
[typeof(Iterator)] = (IL, iter, i) => { IL.Load(iter); }, |
|
||||||
[typeof(EntityRef)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iterEntityMethod); }, |
|
||||||
}; |
|
||||||
|
|
||||||
public World World { get; } |
|
||||||
public MethodInfo Method { get; } |
|
||||||
public IReadOnlyList<ParamInfo> Parameters { get; } |
|
||||||
|
|
||||||
public IReadOnlyList<Term> Terms { get; } |
|
||||||
public Action<object?, Iterator> GeneratedAction { get; } |
|
||||||
public string ReadableString { get; } |
|
||||||
|
|
||||||
public void RunWithTryCatch(object? instance, Iterator iter) |
|
||||||
{ |
|
||||||
try { GeneratedAction(instance, iter); } |
|
||||||
catch { Console.Error.WriteLine(ReadableString); throw; } |
|
||||||
} |
|
||||||
|
|
||||||
public IterActionGenerator(World world, MethodInfo method) |
|
||||||
{ |
|
||||||
World = world; |
|
||||||
Method = method; |
|
||||||
Parameters = method.GetParameters().Select(ParamInfo.Build).ToImmutableArray(); |
|
||||||
|
|
||||||
var name = "<>Query_" + string.Join("_", method.Name); |
|
||||||
var genMethod = new DynamicMethod(name, null, new[] { typeof(object), typeof(Iterator) }); |
|
||||||
var IL = new ILGeneratorWrapper(genMethod); |
|
||||||
|
|
||||||
var instanceArg = IL.Argument<object?>(0); |
|
||||||
var iteratorArg = IL.Argument<Iterator>(1); |
|
||||||
|
|
||||||
var fieldIndex = 0; |
|
||||||
var paramData = new List<(ParamInfo Info, Term? Term, ILocal? FieldLocal, ILocal? TempLocal)>(); |
|
||||||
foreach (var p in Parameters) { |
|
||||||
// If the parameter is unique, we don't create a term for it. |
|
||||||
if (p.Kind <= ParamKind.Unique) |
|
||||||
{ paramData.Add((p, null, null, null)); continue; } |
|
||||||
|
|
||||||
// Create a term to add to the query. |
|
||||||
var term = new Term(world.LookupByTypeOrThrow(p.UnderlyingType)) { |
|
||||||
Source = (p.Source != null) ? (TermId)World.LookupByTypeOrThrow(p.Source) : null, |
|
||||||
InOut = p.Kind switch { |
|
||||||
ParamKind.In => TermInOutKind.In, |
|
||||||
ParamKind.Out => TermInOutKind.Out, |
|
||||||
ParamKind.Not or ParamKind.Not => TermInOutKind.None, |
|
||||||
_ => default, |
|
||||||
}, |
|
||||||
Oper = p.Kind switch { |
|
||||||
ParamKind.Not => TermOperKind.Not, |
|
||||||
ParamKind.Or => TermOperKind.Or, |
|
||||||
_ when !p.IsRequired => TermOperKind.Optional, |
|
||||||
_ => default, |
|
||||||
}, |
|
||||||
}; |
|
||||||
|
|
||||||
// If this and the previous parameter are marked with [Or], do not advance the field index. |
|
||||||
if ((fieldIndex == 0) || (p.Kind != ParamKind.Or) || (paramData[^1].Info.Kind != ParamKind.Or)) |
|
||||||
fieldIndex++; |
|
||||||
|
|
||||||
var spanType = typeof(Span<>).MakeGenericType(p.FieldType); |
|
||||||
var fieldLocal = (ILocal?)null; |
|
||||||
var tempLocal = (ILocal?)null; |
|
||||||
|
|
||||||
switch (p.Kind) { |
|
||||||
// FIXME: Currently would not work with [Or]'d components. |
|
||||||
case ParamKind.Has or ParamKind.Not or ParamKind.Or: |
|
||||||
if (!p.ParameterType.IsValueType) break; |
|
||||||
// If parameter is a struct, we require a temporary local that we can |
|
||||||
// later load onto the stack when loading the arguments for the action. |
|
||||||
IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});"); |
|
||||||
tempLocal = IL.Local(p.ParameterType); |
|
||||||
IL.LoadAddr(tempLocal); |
|
||||||
IL.Init(tempLocal.LocalType); |
|
||||||
break; |
|
||||||
|
|
||||||
case ParamKind.Nullable or ParamKind.Or: |
|
||||||
IL.Comment($"{p.Info.Name}Field = iterator.FieldOrEmpty<{p.FieldType.Name}>({fieldIndex})"); |
|
||||||
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field"); |
|
||||||
IL.Load(iteratorArg); |
|
||||||
IL.LoadConst(fieldIndex); |
|
||||||
IL.Call(_iterFieldOrEmptyMethod.MakeGenericMethod(p.FieldType)); |
|
||||||
IL.Store(fieldLocal); |
|
||||||
|
|
||||||
if (p.UnderlyingType.IsValueType) { |
|
||||||
IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});"); |
|
||||||
tempLocal = IL.Local(p.ParameterType); |
|
||||||
IL.LoadAddr(tempLocal); |
|
||||||
IL.Init(tempLocal.LocalType); |
|
||||||
} |
|
||||||
break; |
|
||||||
|
|
||||||
default: |
|
||||||
IL.Comment($"{p.Info.Name}Field = iterator.Field<{p.FieldType.Name}>({fieldIndex})"); |
|
||||||
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field"); |
|
||||||
IL.Load(iteratorArg); |
|
||||||
IL.LoadConst(fieldIndex); |
|
||||||
IL.Call(_iterFieldMethod.MakeGenericMethod(p.FieldType)); |
|
||||||
IL.Store(fieldLocal); |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
paramData.Add((p, term, fieldLocal, tempLocal)); |
|
||||||
} |
|
||||||
|
|
||||||
var indexLocal = IL.Local<int>("iter_index"); |
|
||||||
var countLocal = IL.Local<int>("iter_count"); |
|
||||||
|
|
||||||
IL.Set(indexLocal, 0); |
|
||||||
IL.Load(iteratorArg, _iterCountProp); |
|
||||||
IL.Store(countLocal); |
|
||||||
|
|
||||||
// If all parameters are fixed, iterator count will be 0, but since |
|
||||||
// the query matched, we want to run the callback at least once. |
|
||||||
IL.Comment("if (iter_count == 0) iter_count = 1;"); |
|
||||||
var dontIncrementLabel = IL.DefineLabel(); |
|
||||||
IL.Load(countLocal); |
|
||||||
IL.GotoIfTrue(dontIncrementLabel); |
|
||||||
IL.LoadConst(1); |
|
||||||
IL.Store(countLocal); |
|
||||||
IL.MarkLabel(dontIncrementLabel); |
|
||||||
|
|
||||||
IL.While("IteratorLoop", (@continue) => { |
|
||||||
IL.GotoIf(@continue, indexLocal, Comparison.LessThan, countLocal); |
|
||||||
}, (_, _) => { |
|
||||||
if (!Method.IsStatic) |
|
||||||
IL.Load(instanceArg); |
|
||||||
|
|
||||||
foreach (var (info, term, fieldLocal, tempLocal) in paramData) { |
|
||||||
var isValueType = info.UnderlyingType.IsValueType; |
|
||||||
var paramName = info.ParameterType.GetFriendlyName(); |
|
||||||
switch (info.Kind) { |
|
||||||
|
|
||||||
case ParamKind.GlobalUnique: |
|
||||||
IL.Comment($"Global unique parameter {paramName}"); |
|
||||||
_globalUniqueParameters[info.ParameterType](IL, iteratorArg); |
|
||||||
break; |
|
||||||
|
|
||||||
case ParamKind.Unique: |
|
||||||
IL.Comment($"Unique parameter {paramName}"); |
|
||||||
_uniqueParameters[info.ParameterType](IL, iteratorArg, indexLocal!); |
|
||||||
break; |
|
||||||
|
|
||||||
// FIXME: Currently would not work with [Or]'d components. |
|
||||||
case ParamKind.Has or ParamKind.Not or ParamKind.Or: |
|
||||||
IL.Comment($"Has parameter {paramName}"); |
|
||||||
if (isValueType) IL.LoadObj(tempLocal!); |
|
||||||
else IL.LoadNull(); |
|
||||||
break; |
|
||||||
|
|
||||||
default: |
|
||||||
var spanType = isValueType ? typeof(Span<>) : typeof(Iterator.SpanToRef<>); |
|
||||||
var concreteSpanType = spanType.MakeGenericType(info.UnderlyingType); |
|
||||||
var spanItemMethod = concreteSpanType.GetProperty("Item")!.GetMethod!; |
|
||||||
var spanLengthMethod = concreteSpanType.GetProperty("Length")!.GetMethod!; |
|
||||||
|
|
||||||
IL.Comment($"Parameter {paramName}"); |
|
||||||
if (info.IsByRef) { |
|
||||||
IL.LoadAddr(fieldLocal!); |
|
||||||
if (info.IsFixed) IL.LoadConst(0); |
|
||||||
else IL.Load(indexLocal!); |
|
||||||
IL.Call(spanItemMethod); |
|
||||||
} else if (info.IsRequired) { |
|
||||||
IL.LoadAddr(fieldLocal!); |
|
||||||
if (info.IsFixed) IL.LoadConst(0); |
|
||||||
else IL.Load(indexLocal!); |
|
||||||
IL.Call(spanItemMethod); |
|
||||||
if (isValueType) IL.LoadObj(info.FieldType); |
|
||||||
} else { |
|
||||||
var elseLabel = IL.DefineLabel(); |
|
||||||
var doneLabel = IL.DefineLabel(); |
|
||||||
IL.LoadAddr(fieldLocal!); |
|
||||||
IL.Call(spanLengthMethod); |
|
||||||
IL.GotoIfFalse(elseLabel); |
|
||||||
IL.LoadAddr(fieldLocal!); |
|
||||||
if (info.IsFixed) IL.LoadConst(0); |
|
||||||
else IL.Load(indexLocal!); |
|
||||||
IL.Call(spanItemMethod); |
|
||||||
if (isValueType) { |
|
||||||
IL.LoadObj(info.FieldType); |
|
||||||
IL.New(info.ParameterType); |
|
||||||
} |
|
||||||
IL.Goto(doneLabel); |
|
||||||
IL.MarkLabel(elseLabel); |
|
||||||
if (isValueType) IL.LoadObj(tempLocal!); |
|
||||||
else IL.LoadNull(); |
|
||||||
IL.MarkLabel(doneLabel); |
|
||||||
} |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
IL.Call(Method); |
|
||||||
|
|
||||||
IL.Increment(indexLocal); |
|
||||||
}); |
|
||||||
|
|
||||||
IL.Return(); |
|
||||||
|
|
||||||
Terms = paramData.Where(p => p.Term != null).Select(p => p.Term!).ToImmutableList(); |
|
||||||
GeneratedAction = genMethod.CreateDelegate<Action<object?, Iterator>>(); |
|
||||||
ReadableString = IL.ToReadableString(); |
|
||||||
} |
|
||||||
|
|
||||||
public static IterActionGenerator GetOrBuild(World world, MethodInfo method) |
|
||||||
=>_cache.GetValue(method, m => new IterActionGenerator(world, m)); |
|
||||||
|
|
||||||
public class ParamInfo |
|
||||||
{ |
|
||||||
public ParameterInfo Info { get; } |
|
||||||
|
|
||||||
public ParamKind Kind { get; } |
|
||||||
public Type ParameterType { get; } |
|
||||||
public Type UnderlyingType { get; } |
|
||||||
public Type FieldType { get; } |
|
||||||
|
|
||||||
public Type? Source { get; } |
|
||||||
|
|
||||||
public bool IsRequired => (Kind < ParamKind.Nullable); |
|
||||||
public bool IsByRef => (Kind is ParamKind.In or ParamKind.Ref); |
|
||||||
public bool IsFixed => (Kind == ParamKind.GlobalUnique) || (Source != null); |
|
||||||
|
|
||||||
private ParamInfo(ParameterInfo info, ParamKind kind, |
|
||||||
Type paramType, Type underlyingType) |
|
||||||
{ |
|
||||||
Info = info; |
|
||||||
Kind = kind; |
|
||||||
ParameterType = paramType; |
|
||||||
UnderlyingType = underlyingType; |
|
||||||
// Reference types have a backing type of ReferenceHandle. |
|
||||||
FieldType = underlyingType.IsValueType ? underlyingType : typeof(ReferenceHandle); |
|
||||||
|
|
||||||
if (UnderlyingType.Has<SingletonAttribute>()) Source = UnderlyingType; |
|
||||||
if (Info.Get<SourceAttribute>()?.Type is Type type) Source = type; |
|
||||||
} |
|
||||||
|
|
||||||
public static ParamInfo Build(ParameterInfo info) |
|
||||||
{ |
|
||||||
if (info.IsOptional) throw new ArgumentException($"Optional parameters are not supported\nParameter: {info}"); |
|
||||||
if (info.ParameterType.IsArray) throw new ArgumentException($"Arrays are not supported\nParameter: {info}"); |
|
||||||
if (info.ParameterType.IsPointer) throw new ArgumentException($"Pointers are not supported\nParameter: {info}"); |
|
||||||
if (info.ParameterType.IsPrimitive) throw new ArgumentException($"Primitives are not supported\nParameter: {info}"); |
|
||||||
|
|
||||||
// Find out initial parameter kind from provided attribute. |
|
||||||
var fromAttributes = new List<ParamKind>(); |
|
||||||
if (info.Has< InAttribute>()) fromAttributes.Add(ParamKind.In); |
|
||||||
if (info.Has<OutAttribute>()) fromAttributes.Add(ParamKind.Out); |
|
||||||
if (info.Has<HasAttribute>()) fromAttributes.Add(ParamKind.Has); |
|
||||||
if (info.Has< OrAttribute>()) fromAttributes.Add(ParamKind.Or); |
|
||||||
if (info.Has<NotAttribute>()) fromAttributes.Add(ParamKind.Not); |
|
||||||
// Throw an error if multiple incompatible attributes were found. |
|
||||||
if (fromAttributes.Count > 1) throw new ArgumentException( |
|
||||||
"Parameter must not be marked with multiple attributes: " |
|
||||||
+ string.Join(", ", fromAttributes.Select(a => $"[{a}]")) |
|
||||||
+ $"\nParameter: {info}"); |
|
||||||
var kind = fromAttributes.FirstOrNull() ?? ParamKind.Normal; |
|
||||||
|
|
||||||
// Handle unique parameters such as Universe, EntityRef, ... |
|
||||||
var isGlobalUnique = _globalUniqueParameters.ContainsKey(info.ParameterType); |
|
||||||
var isUnique = _uniqueParameters.ContainsKey(info.ParameterType); |
|
||||||
if (isGlobalUnique || isUnique) { |
|
||||||
if (kind != ParamKind.Normal) throw new ArgumentException( |
|
||||||
$"Unique parameter {info.ParameterType.Name} does not support [{kind}]\nParameter: {info}"); |
|
||||||
kind = isGlobalUnique ? ParamKind.GlobalUnique : ParamKind.Unique; |
|
||||||
return new(info, kind, info.ParameterType, info.ParameterType); |
|
||||||
} |
|
||||||
|
|
||||||
var isNullable = info.IsNullable(); |
|
||||||
var isByRef = info.ParameterType.IsByRef; |
|
||||||
|
|
||||||
if (info.ParameterType.Has<TagAttribute>() && (kind is not (ParamKind.Has or ParamKind.Not or ParamKind.Or))) { |
|
||||||
if (kind is not ParamKind.Normal) throw new ArgumentException($"Parameter does not support [{kind}]\nParameter: {info}"); |
|
||||||
kind = ParamKind.Has; |
|
||||||
} |
|
||||||
|
|
||||||
if (kind is ParamKind.Not or ParamKind.Has) { |
|
||||||
if (isNullable) throw new ArgumentException($"Parameter does not support Nullable\nParameter: {info}"); |
|
||||||
if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}"); |
|
||||||
return new(info, kind, info.ParameterType, info.ParameterType); |
|
||||||
} |
|
||||||
|
|
||||||
var underlyingType = info.ParameterType; |
|
||||||
|
|
||||||
if (isNullable) { |
|
||||||
if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}"); |
|
||||||
if (info.ParameterType.IsValueType) |
|
||||||
underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!; |
|
||||||
kind = ParamKind.Nullable; |
|
||||||
} |
|
||||||
|
|
||||||
if (info.ParameterType.IsByRef) { |
|
||||||
if (kind != ParamKind.Normal) throw new ArgumentException( |
|
||||||
$"Parameter does not support [{kind}]\nParameter: {info}"); |
|
||||||
underlyingType = info.ParameterType.GetElementType()!; |
|
||||||
if (!underlyingType.IsValueType) throw new ArgumentException( |
|
||||||
$"Reference types can't also be ByRef\nParameter: {info}"); |
|
||||||
kind = info.IsIn ? ParamKind.In |
|
||||||
: info.IsOut ? ParamKind.Out |
|
||||||
: ParamKind.Ref; |
|
||||||
} |
|
||||||
|
|
||||||
if (underlyingType.IsPrimitive) throw new ArgumentException( |
|
||||||
$"Primitives are not supported\nParameter: {info}"); |
|
||||||
|
|
||||||
return new(info, kind, info.ParameterType, underlyingType); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public enum ParamKind |
|
||||||
{ |
|
||||||
/// <summary> |
|
||||||
/// Not part of the resulting query's terms. |
|
||||||
/// Same value across a single invocation of a callback. |
|
||||||
/// For example <see cref="ECS.World"/> or <see cref="TimeSpan"/>. |
|
||||||
/// </summary> |
|
||||||
GlobalUnique, |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Not part of the resulting query's terms. |
|
||||||
/// Unique value for each iterated entity. |
|
||||||
/// For example <see cref="EntityRef"/>. |
|
||||||
/// </summary> |
|
||||||
Unique, |
|
||||||
|
|
||||||
/// <summary> Passed by value. </summary> |
|
||||||
Normal, |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Struct passed with the "in" modifier, allowing direct pointer access. |
|
||||||
/// Manually applied with <see cref="InAttribute"/>. |
|
||||||
/// Marks a component as being read from. |
|
||||||
/// </summary> |
|
||||||
In, |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Struct passed with the "out" modifier, allowing direct pointer access. |
|
||||||
/// Manually applied with <see cref="HasAttribute"/>. |
|
||||||
/// Marks a component as being written to. |
|
||||||
/// </summary> |
|
||||||
Out, |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Struct passed with the "ref" modifier, allowing direct pointer access. |
|
||||||
/// Marks a component as being read from and written to. |
|
||||||
/// </summary> |
|
||||||
Ref, |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Only checks for presence. |
|
||||||
/// Manually applied with <see cref="HasAttribute"/>. |
|
||||||
/// Automatically applied for types with <see cref="TagAttribute"/>. |
|
||||||
/// Marks a component as not being accessed. |
|
||||||
/// </summary> |
|
||||||
Has, |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Struct or class passed as <see cref="T?"/>. |
|
||||||
/// </summary> |
|
||||||
Nullable, |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Only checks for absence. |
|
||||||
/// Applied with <see cref="NotAttribute"/>. |
|
||||||
/// </summary> |
|
||||||
Not, |
|
||||||
|
|
||||||
/// <summary> |
|
||||||
/// Matches any terms in a chain of "or" terms. |
|
||||||
/// Applied with <see cref="OrAttribute"/>. |
|
||||||
/// Implies <see cref="Nullable"/>. |
|
||||||
/// </summary> |
|
||||||
Or, |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,37 @@ |
|||||||
|
using System.Runtime.InteropServices; |
||||||
|
|
||||||
|
namespace gaemstone.Utility; |
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)] |
||||||
|
internal struct Union<T1, T2> |
||||||
|
{ |
||||||
|
[FieldOffset(0)] public T1 Value1; |
||||||
|
[FieldOffset(0)] public T2 Value2; |
||||||
|
} |
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)] |
||||||
|
internal struct Union<T1, T2, T3> |
||||||
|
{ |
||||||
|
[FieldOffset(0)] public T1 Value1; |
||||||
|
[FieldOffset(0)] public T2 Value2; |
||||||
|
[FieldOffset(0)] public T3 Value3; |
||||||
|
} |
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)] |
||||||
|
internal struct Union<T1, T2, T3, T4> |
||||||
|
{ |
||||||
|
[FieldOffset(0)] public T1 Value1; |
||||||
|
[FieldOffset(0)] public T2 Value2; |
||||||
|
[FieldOffset(0)] public T3 Value3; |
||||||
|
[FieldOffset(0)] public T4 Value4; |
||||||
|
} |
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)] |
||||||
|
internal struct Union<T1, T2, T3, T4, T5> |
||||||
|
{ |
||||||
|
[FieldOffset(0)] public T1 Value1; |
||||||
|
[FieldOffset(0)] public T2 Value2; |
||||||
|
[FieldOffset(0)] public T3 Value3; |
||||||
|
[FieldOffset(0)] public T4 Value4; |
||||||
|
[FieldOffset(0)] public T5 Value5; |
||||||
|
} |
Loading…
Reference in new issue