Compare commits
18 Commits
13f69a0856
...
6a38f97830
Author | SHA1 | Date |
---|---|---|
copygirl | 6a38f97830 | 1 year ago |
copygirl | c97c4a530f | 1 year ago |
copygirl | 03b7e5ce42 | 1 year ago |
copygirl | fe54cc3637 | 1 year ago |
copygirl | 40cb0045df | 1 year ago |
copygirl | 931a86fcea | 1 year ago |
copygirl | 58989e949f | 1 year ago |
copygirl | d51c0331bb | 1 year ago |
copygirl | ef64ba4e08 | 1 year ago |
copygirl | 982741a364 | 1 year ago |
copygirl | c9a986f9bf | 1 year ago |
copygirl | 357e8b4bfa | 1 year ago |
copygirl | 64f7ece2a7 | 1 year ago |
copygirl | 8e9ff611d9 | 1 year ago |
copygirl | 20d0cd2f0e | 1 year ago |
copygirl | 976d9fd326 | 1 year ago |
copygirl | 03d672febf | 1 year ago |
copygirl | 2b877e0c5c | 1 year ago |
68 changed files with 2516 additions and 1763 deletions
@ -0,0 +1,25 @@ |
||||
using System; |
||||
using gaemstone.ECS; |
||||
using gaemstone.ECS.Utility; |
||||
|
||||
namespace Immersion; |
||||
|
||||
[Module] |
||||
public partial class ManagedComponentTest |
||||
{ |
||||
[Component] |
||||
public class BigManagedData |
||||
{ |
||||
public readonly byte[] BigArray = new byte[1024 * 1024]; |
||||
} |
||||
|
||||
[System] |
||||
public static void CreateLotsOfGarbageData<T>(World<T> world) |
||||
{ |
||||
var game = world.LookupPathOrThrow("/Game"); |
||||
game.Remove<BigManagedData>(); |
||||
game.Set(new BigManagedData()); |
||||
// This is to make sure the number of objects kept alive stays stable. |
||||
Console.WriteLine(ReferenceHandle.NumActiveHandles); |
||||
} |
||||
} |
@ -1 +1 @@ |
||||
Subproject commit 46e171940eea723f2ad8376b5056afd9b9d8a340 |
||||
Subproject commit eec968d6361930eebad180f6300d126c4bac70f3 |
@ -0,0 +1,230 @@ |
||||
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 AttributesRequireTypeOrPath = new( |
||||
$"gSG{_idCounter++:00}", "Entity attributes require type or path", |
||||
"Entity attributes require a type (such as [Entity]) or [Path].", |
||||
DiagnosticCategory, DiagnosticSeverity.Error, true); |
||||
|
||||
|
||||
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); |
||||
|
||||
public static readonly DiagnosticDescriptor BuiltInModuleMustNotHaveMethods = new( |
||||
$"gSG{_idCounter++:00}", "BuiltIn Module must not have System / Observer", |
||||
"A [BuiltIn, Module] must not have [System] or [Observer] methods.", |
||||
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); |
||||
|
||||
|
||||
// TODO: Make this more descriptive. (Special = Has<>, Not<> and [Tag]?) |
||||
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); |
||||
} |
@ -0,0 +1,398 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Text; |
||||
using gaemstone.SourceGen.Structure; |
||||
using gaemstone.SourceGen.Utility; |
||||
using Microsoft.CodeAnalysis; |
||||
using Microsoft.CodeAnalysis.CSharp; |
||||
|
||||
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" )}}; |
||||
|
||||
""");
|
||||
|
||||
// TODO: Built-in modules should not have dependencies. |
||||
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? |
||||
|
||||
if (module.HasInitializer) |
||||
sb.AppendLine("\t\tInitialize(module);"); |
||||
|
||||
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.EntityPath ?? e.Name).ToStringLiteral(); |
||||
|
||||
// When looking for a BuiltIn entity, error if it doesn't exist. |
||||
var lookupFunc = (e.IsBuiltIn ? "LookupPathOrThrow" : "New"); |
||||
sb.AppendLine($"\t\tvar {@var} = world.{lookupFunc}({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>()"); |
||||
|
||||
// Other than [Relation], we don't add anything to entities defined in [BuiltIn] modules. |
||||
if (module.IsBuiltIn) |
||||
{ |
||||
sb.AppendLine($"\t\t\t.CreateLookup<{e.FullName}>()"); |
||||
} |
||||
else |
||||
{ |
||||
if (e.EntitySymbol != null) |
||||
sb.AppendLine($"\t\t\t.Symbol({e.EntitySymbol.ToStringLiteral()})"); |
||||
|
||||
// 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"); |
||||
if (!e.IsBuiltIn) sb.Append(".Build()"); |
||||
if (e.IsComponent) sb.Append($".InitComponent<{e.FullName}>()"); |
||||
else sb.Append($".CreateLookup<{e.FullName}>()"); |
||||
sb.AppendLine(); |
||||
|
||||
// I don't think it makes sense to have singletons pre-initialized to zero. |
||||
// Especially for singletons that are reference types, which would default to null. |
||||
// if (e.IsSingleton) sb.AppendLine($"\t\t\t.Add<{e.FullName}>()"); |
||||
// TODO: Look into if it would be possible to detect if we have field initializers. |
||||
} |
||||
|
||||
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.EntityPath ?? 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) { |
||||
for (var i = 0; i < p.TermTypes.Count; i++) { |
||||
var term = p.TermTypes[i]; |
||||
var isLastTerm = (i == p.TermTypes.Count - 1); |
||||
|
||||
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(" }"); |
||||
} |
||||
|
||||
// The last term in a group of OR terms must not have the TermOperKind.Or set. |
||||
if (p.IsOr && !isLastTerm) sb.Append(".Or"); |
||||
|
||||
sb.Append(p.Kind switch { |
||||
ParameterKind.Has => ".None", |
||||
ParameterKind.Not => ".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()}>();"); |
||||
foreach (var (c, args) in e.ComponentsToAdd) |
||||
sb.AppendLine($"\t\t{@var}.Set(new {c.GetFullName()}({string.Join(", ", args.Select(a => a.ToCSharpString()))}));"); |
||||
|
||||
// If system doesn't have an explicit phase set, default to OnUpdate. |
||||
if (e is MethodEntityInfo { IsSystem: true, HasPhaseSet: false }) |
||||
sb.AppendLine($"\t\t{@var}.Add<gaemstone.Flecs.Core.DependsOn, gaemstone.Flecs.Pipeline.OnUpdate>();"); |
||||
} |
||||
} |
||||
|
||||
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});"); |
||||
|
||||
// When the shim method is called, there's guaranteed to be at least one match. |
||||
// iter.Count may be 0 when there's no variable entities that can be matched. |
||||
// For example for a query that only matches singleton entities. |
||||
sb.AppendLine("\t\t\tfor (var i = 0; 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) { |
||||
// TODO: Support [Or<...>] |
||||
if (param.IsOr && (param.Kind != ParameterKind.Has)) |
||||
throw new NotSupportedException($"Or<...> parameter not yet supported"); |
||||
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: |
||||
sb.Append("default"); |
||||
break; |
||||
} |
||||
sb.Append(", "); |
||||
} |
||||
if (method.Parameters.Any()) |
||||
sb.Length -= 2; |
||||
sb.AppendLine(");"); |
||||
|
||||
sb.AppendLine($$"""
|
||||
} |
||||
} |
||||
""");
|
||||
} |
||||
|
||||
|
||||
private void AppendTypeEntity( |
||||
StringBuilder sb, ModuleEntityInfo module, |
||||
ITypeSymbol type) |
||||
{ |
||||
// TODO: Cache entity lookup. |
||||
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 |
||||
"Path", // TODO: When referring to a pre-existing entity, only [Path] should be necessary, right? |
||||
"Symbol", |
||||
"Add", |
||||
"Set", |
||||
"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,97 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Collections.Immutable; |
||||
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? EntityPath { get; } |
||||
public string? EntitySymbol { get; } |
||||
|
||||
public List<INamedTypeSymbol> EntitiesToAdd { get; } = new(); |
||||
public List<(INamedTypeSymbol Relation, INamedTypeSymbol Target)> RelationsToAdd { get; } = new(); |
||||
public List<(INamedTypeSymbol Component, ImmutableArray<TypedConstant> Arguments)> ComponentsToAdd { get; } = new(); |
||||
|
||||
public virtual bool HasEntitiesToAdd => (EntitiesToAdd.Count > 0) |
||||
|| (RelationsToAdd.Count > 0) |
||||
|| (ComponentsToAdd.Count > 0); |
||||
|
||||
public BaseEntityInfo(ISymbol symbol) |
||||
: base(symbol) |
||||
{ |
||||
// TODO: Validate that these only contain valid characters. |
||||
EntityPath = Get("Path")?.ConstructorArguments.FirstOrDefault().Value as string; |
||||
EntitySymbol = (Get("Symbol") is AttributeData symbolAttr) |
||||
// If [Symbol] is present, use the given custom symbol (if given), .. |
||||
? (symbolAttr.ConstructorArguments.FirstOrDefault().Value as string) |
||||
?? EntityPath?.Split('/')[^1] // .. otherwise default to the name in [Path], .. |
||||
?? Name // .. or just use the default: The symbol's name. |
||||
: null; |
||||
} |
||||
|
||||
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); |
||||
} |
||||
|
||||
foreach (var attr in Symbol.GetAttributes()) { |
||||
// Add entities and relationships specified using [Add<...>] attributes. |
||||
for (var attrType = attr.AttributeClass; attrType != null; attrType = attrType.BaseType) { |
||||
var attrName = RelevantSymbolReceiver.ToRelevantAttributeName(attrType); |
||||
if (attrName is "Add" or "Set") { |
||||
|
||||
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) continue; |
||||
|
||||
switch (attrName) { |
||||
case "Add": |
||||
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"); |
||||
} |
||||
break; |
||||
|
||||
case "Set": |
||||
var component = (INamedTypeSymbol)attrType.TypeArguments.Single(); |
||||
var arguments = attr.ConstructorArguments.Single().Values; |
||||
// TODO: Verify arguments actually match a constructor. |
||||
// Right now this will just error in the generated code. Probably good enough? |
||||
ComponentsToAdd.Add((component, arguments)); |
||||
break; |
||||
|
||||
default: throw new InvalidOperationException( |
||||
"Invalid relevant attribute name"); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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,98 @@ |
||||
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 bool HasPhaseSet { get; private set; } |
||||
public override bool HasEntitiesToAdd => base.HasEntitiesToAdd || !HasPhaseSet; |
||||
|
||||
// TODO: Support [Source]. |
||||
|
||||
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 (Parent is ModuleEntityInfo { IsBuiltIn: true }) yield return Diagnostic.Create( |
||||
Descriptors.BuiltInModuleMustNotHaveMethods, Location); |
||||
|
||||
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++; |
||||
|
||||
// See if we have any [DependsOn<...>] attributes for this system. |
||||
// If not, ModuleGenerator will add [DependsOn<Flecs.Pipeline.OnUpdate>]. |
||||
HasPhaseSet = IsSystem && RelationsToAdd.Any(entry => entry.Relation |
||||
.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.DependsOnAttribute"); |
||||
|
||||
// TODO: Handle systems with [Source]. |
||||
// TODO: Validate ObserverEvents. |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
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 HasInitializer { get; } |
||||
|
||||
public ModuleEntityInfo(ISymbol symbol) |
||||
: base(symbol) |
||||
{ |
||||
var classDecl = (TypeDeclarationSyntax)Symbol.DeclaringSyntaxReferences.First().GetSyntax(); |
||||
IsPartial = classDecl.Modifiers.Any(t => t.IsKind(SyntaxKind.PartialKeyword)); |
||||
|
||||
HasInitializer = Symbol.AllInterfaces.Any(i => |
||||
i.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.IModuleInitializer"); |
||||
|
||||
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 && (EntityPath == 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,220 @@ |
||||
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 IsOr { get; } |
||||
|
||||
public bool HasTerm => (Kind != ParameterKind.Unique); |
||||
public bool HasField => HasTerm && !(Kind is ParameterKind.Has or ParameterKind.Not); |
||||
|
||||
public ParameterInfo(ISymbol symbol) |
||||
: base(symbol) |
||||
{ |
||||
var typeFullName = Symbol.Type.GetFullName(FullNameStyle.Metadata); |
||||
if (UniqueParameters.TryGetValue(typeFullName, out var replacement)) |
||||
{ |
||||
Source = null; |
||||
UniqueReplacement = replacement; |
||||
TermTypes = Array.Empty<ITypeSymbol>(); |
||||
Kind = ParameterKind.Unique; |
||||
IsOr = false; |
||||
} |
||||
else |
||||
{ |
||||
IsOr = typeFullName.StartsWith("gaemstone.ECS.Or"); |
||||
var isHas = typeFullName.StartsWith("gaemstone.ECS.Has"); |
||||
var isNot = typeFullName.StartsWith("gaemstone.ECS.Not"); |
||||
|
||||
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 [ ITypeSymbol relation, ITypeSymbol target ])) |
||||
{ |
||||
TermTypes = ImmutableList.Create(new Pair(relation, target)); |
||||
FieldType = null; |
||||
} |
||||
else |
||||
{ |
||||
TermTypes = args.ToImmutableList(); |
||||
FieldType = (IsNullable || isHas || isNot) ? args[0] : type; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
TermTypes = ImmutableList.Create(Symbol.Type); |
||||
FieldType = Symbol.Type; |
||||
|
||||
// If the type of the parameter has the [Tag] attribute, |
||||
// the only way to sensibly use it is to check for its (non-)existance. |
||||
// (This would also apply to [Relation, Tag] but should be no issue.) |
||||
if (Symbol.Type.HasAttribute("gaemstone.ECS.TagAttribute")) isHas = true; |
||||
// TODO: Make sure [Tag] is used appropriately. |
||||
} |
||||
|
||||
Source = Get("Source")?.AttributeClass!.TypeArguments[0] |
||||
// If the type of the parameter has the [Singleton] attribute, use it as the default Source. |
||||
?? ((FieldType?.HasAttribute("gaemstone.ECS.SingletonAttribute") == true) ? FieldType : null); |
||||
|
||||
Kind = isHas ? ParameterKind.Has |
||||
: 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 = IsOr || (Kind is ParameterKind.Has 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) |
||||
} |
||||
|
||||
// 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,96 @@ |
||||
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; } |
||||
|
||||
// Built-in entities error if they don't exist. |
||||
// When a module is built-in, most attributes become descriptive, |
||||
// and don't change or affect the entity they're attached to. |
||||
public bool IsBuiltIn { get; protected set; } |
||||
|
||||
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"); |
||||
|
||||
IsBuiltIn = !IsModule && !IsEntity && !IsTag |
||||
&& !IsComponent && !IsSingleton && !IsRelation |
||||
&& (EntityPath != null); |
||||
} |
||||
|
||||
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) |
||||
{ |
||||
// No attributes are only valid if [Path] is set. |
||||
case [ ]: |
||||
if (!IsBuiltIn) yield return Diagnostic.Create( |
||||
Descriptors.AttributesRequireTypeOrPath, Location, |
||||
$"[{string.Join(", ", attributeList)}]"); |
||||
break; |
||||
// 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; |
||||
} |
||||
|
||||
// In in a built-in module, this entity is built-in, too. |
||||
if (Parent is ModuleEntityInfo { IsBuiltIn: true }) |
||||
IsBuiltIn = true; |
||||
|
||||
// Singletons are special kinds of components. |
||||
if (IsSingleton) IsComponent = true; |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
using System.Collections.Generic; |
||||
|
||||
namespace gaemstone.SourceGen.Utility; |
||||
|
||||
public static class CollectionExtensions |
||||
{ |
||||
public static void Deconstruct<TKey, TValue>( |
||||
this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value) |
||||
{ key = kvp.Key; value = kvp.Value; } |
||||
} |
@ -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 |
||||
{ |
||||
} |
||||
} |
@ -0,0 +1,118 @@ |
||||
using System.Linq; |
||||
using System.Text; |
||||
using Microsoft.CodeAnalysis; |
||||
using Microsoft.CodeAnalysis.CSharp; |
||||
|
||||
namespace gaemstone.SourceGen.Utility; |
||||
|
||||
public static class SymbolExtensions |
||||
{ |
||||
public static bool IsNullable(this ITypeSymbol type) => type.IsValueType |
||||
? (type.OriginalDefinition?.SpecialType == SpecialType.System_Nullable_T) |
||||
: (type.NullableAnnotation == NullableAnnotation.Annotated); |
||||
|
||||
public static bool IsPrimitiveType(this ITypeSymbol symbol) |
||||
=> symbol.SpecialType switch { |
||||
SpecialType.System_Boolean or |
||||
SpecialType.System_SByte or |
||||
SpecialType.System_Int16 or |
||||
SpecialType.System_Int32 or |
||||
SpecialType.System_Int64 or |
||||
SpecialType.System_Byte or |
||||
SpecialType.System_UInt16 or |
||||
SpecialType.System_UInt32 or |
||||
SpecialType.System_UInt64 or |
||||
SpecialType.System_Single or |
||||
SpecialType.System_Double or |
||||
SpecialType.System_Char or |
||||
SpecialType.System_String or |
||||
SpecialType.System_Object => true, |
||||
_ => false, |
||||
}; |
||||
|
||||
public static string GetFullName( |
||||
this ISymbol symbol, FullNameStyle style = FullNameStyle.Full) |
||||
{ |
||||
var builder = new StringBuilder(); |
||||
AppendFullName(symbol, builder, style); |
||||
return builder.ToString(); |
||||
} |
||||
|
||||
public static void AppendFullName( |
||||
this ISymbol symbol, StringBuilder builder, |
||||
FullNameStyle style = FullNameStyle.Full) |
||||
{ |
||||
var withGeneric = (style != FullNameStyle.NoGeneric); |
||||
var withMetadata = (style == FullNameStyle.Metadata); |
||||
|
||||
if ((symbol.Kind != SymbolKind.TypeParameter) |
||||
&& (symbol.ContainingSymbol is ISymbol parent) |
||||
&& (parent is not INamespaceSymbol { IsGlobalNamespace: true })) |
||||
{ |
||||
AppendFullName(parent, builder, style); |
||||
builder.Append((withMetadata && (parent is ITypeSymbol)) ? '+' : '.'); |
||||
} |
||||
|
||||
if ((symbol is INamedTypeSymbol { IsGenericType: true } typeSymbol) |
||||
&& !(withGeneric && withMetadata)) |
||||
{ |
||||
var length = symbol.MetadataName.IndexOf('`'); |
||||
builder.Append(symbol.MetadataName, 0, length); |
||||
|
||||
if (withGeneric) { |
||||
builder.Append('<'); |
||||
foreach (var arg in typeSymbol.TypeArguments) { |
||||
AppendFullName(arg, builder, style); |
||||
builder.Append(','); |
||||
} |
||||
builder.Length--; // Remove the last ',' character. |
||||
builder.Append('>'); |
||||
} |
||||
} |
||||
else builder.Append(symbol.MetadataName); |
||||
} |
||||
|
||||
public static string? GetNamespace(this ISymbol symbol) |
||||
=> symbol.ContainingNamespace?.GetFullName(); |
||||
|
||||
public static bool HasAttribute(this ISymbol symbol, string name, |
||||
FullNameStyle matchStyle = FullNameStyle.Metadata) |
||||
=> symbol.GetAttributes().Any(attr => |
||||
attr.AttributeClass!.GetFullName(matchStyle) == name); |
||||
|
||||
public static AttributeData? FindAttribute(this ISymbol symbol, string name, |
||||
FullNameStyle matchStyle = FullNameStyle.Metadata) |
||||
=> symbol.GetAttributes().FirstOrDefault(attr => |
||||
attr.AttributeClass!.GetFullName(matchStyle) == name); |
||||
|
||||
public static string ToStringLiteral(this string? input) |
||||
=> (input != null) ? SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, |
||||
SyntaxFactory.Literal(input)).ToFullString() : "null"; |
||||
} |
||||
|
||||
public enum FullNameStyle |
||||
{ |
||||
Full, // Namespace.Foo.Bar<Baz<Quux>> |
||||
Metadata, // Namespace.Foo+Bar`1 |
||||
NoGeneric, // Namespace.Foo.Bar |
||||
} |
||||
|
||||
public struct StringifyOptions |
||||
{ |
||||
public static readonly StringifyOptions Default = new(); |
||||
public static readonly StringifyOptions StripGeneric = new(){ Generic = GenericDisplayMode.None }; |
||||
public static readonly StringifyOptions MetadataGeneric = new(){ Generic = GenericDisplayMode.Metadata }; |
||||
|
||||
public bool Namespace { get; set; } = true; |
||||
// TODO: public bool FriendlyNames { get; set; } = true; |
||||
public GenericDisplayMode Generic { get; set; } = GenericDisplayMode.Full; |
||||
|
||||
public StringifyOptions() { } |
||||
|
||||
public enum GenericDisplayMode |
||||
{ |
||||
None, // Foo |
||||
Metadata, // Foo`1 |
||||
Full, // Foo<Bar<Baz>> |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
||||
<PropertyGroup> |
||||
<LangVersion>preview</LangVersion> |
||||
<TargetFramework>netstandard2.0</TargetFramework> |
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
||||
<ImplicitUsings>disable</ImplicitUsings> |
||||
<Nullable>enable</Nullable> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<PackageReference Include="IndexRange" Version="1.0.2" /> |
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3"> |
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |
||||
<PrivateAssets>all</PrivateAssets> |
||||
</PackageReference> |
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" /> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -1,121 +0,0 @@ |
||||
using System; |
||||
using static gaemstone.Flecs.Core; |
||||
|
||||
namespace gaemstone.ECS; |
||||
|
||||
/// <summary> |
||||
/// When present on an attribute attached to a type that's part of a module |
||||
/// being registered automatically through <see cref="ModuleManager.Register"/>, |
||||
/// an entity is automatically created and <see cref="LookupExtensions.CreateLookup"/> |
||||
/// called on it, meaning it can be looked up using <see cref="World.LookupByType(Type)"/>. |
||||
/// </summary> |
||||
public interface ICreateEntityAttribute { } |
||||
|
||||
[AttributeUsage(AttributeTargets.Struct)] |
||||
public class EntityAttribute : Attribute, ICreateEntityAttribute { } |
||||
|
||||
/// <summary> |
||||
/// Use a custom name or path for this entity instead of the type's name. |
||||
/// </summary> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] |
||||
public class PathAttribute : Attribute, ICreateEntityAttribute |
||||
{ |
||||
public string Value { get; } |
||||
public PathAttribute(string value) => Value = value; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// Register the entity under a globally unique symbol. |
||||
/// Uses the type's name by default. |
||||
/// </summary> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] |
||||
public class SymbolAttribute : Attribute, ICreateEntityAttribute |
||||
{ |
||||
public string? Value { get; } |
||||
public SymbolAttribute() { } |
||||
public SymbolAttribute(string value) => Value = value; |
||||
} |
||||
|
||||
/// <summary> |
||||
/// A singleton is a single instance of a tag or component that can be retrieved |
||||
/// without explicitly specifying an entity in a query, where it is equivalent |
||||
/// to <see cref="SourceAttribute{}"/> with itself as the generic type parameter. |
||||
/// </summary> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] |
||||
public class SingletonAttribute : Attribute, ICreateEntityAttribute |
||||
{ public bool AutoAdd { get; init; } = true; } |
||||
|
||||
|
||||
/// <summary> |
||||
/// Marked entity automatically has the specified entity added to it when |
||||
/// automatically registered. Equivalent to <see cref="EntityBase.Add{T}"/>. |
||||
/// </summary> |
||||
public class AddAttribute<TEntity> : AddEntityAttribute |
||||
{ public AddAttribute() : base(typeof(TEntity)) { } } |
||||
|
||||
/// <summary> |
||||
/// Marked entity automatically has the specified relationship pair added to it when |
||||
/// automatically registered, Equivalent to <see cref="EntityBase.Add{TRelation, TTarget}"/>. |
||||
/// </summary> |
||||
public class AddAttribute<TRelation, TTarget> : AddRelationAttribute |
||||
{ public AddAttribute() : base(typeof(TRelation), typeof(TTarget)) { } } |
||||
|
||||
/// <summary> |
||||
/// Marked entity represents a relationship type. |
||||
/// It may be used as the "relation" in a pair. |
||||
/// </summary> |
||||
/// <remarks> |
||||
/// The relationship may have component data associated with |
||||
/// it when added to an entity under these circumstances: |
||||
/// <list type="bullet"> |
||||
/// <item>If marked as a <see cref="TagAttribute"/>, does not carry data.</item> |
||||
/// <item>If marked as a <see cref="ComponentAttribute"/>, carries the relation's data.</item> |
||||
/// <item>If marked with neither, will carry the target's data, if it's a component.</item> |
||||
/// </list> |
||||
/// </remarks> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] |
||||
public class RelationAttribute : Attribute, ICreateEntityAttribute { } |
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] |
||||
public class ComponentAttribute : Attribute, ICreateEntityAttribute { } |
||||
|
||||
/// <seealso cref="Tag"/> |
||||
[AttributeUsage(AttributeTargets.Struct)] |
||||
public class TagAttribute : AddAttribute<Tag>, ICreateEntityAttribute { } |
||||
|
||||
|
||||
/// <seealso cref="IsA"/> |
||||
public class IsAAttribute<TTarget> : AddAttribute<IsA, TTarget> { } |
||||
|
||||
/// <seealso cref="ChildOf"/> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] |
||||
public class ChildOfAttribute<TTarget> : AddAttribute<ChildOf, TTarget> { } |
||||
|
||||
/// <seealso cref="DependsOn"/> |
||||
public class DependsOnAttribute<TTarget> : AddAttribute<DependsOn, TTarget> { } |
||||
|
||||
|
||||
/// <seealso cref="Exclusive"/> |
||||
public class ExclusiveAttribute : AddAttribute<Exclusive> { } |
||||
|
||||
/// <seealso cref="With"/> |
||||
public class WithAttribute<TTarget> : AddAttribute<With, TTarget> { } |
||||
|
||||
|
||||
// Base attributes for other attributes. |
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)] |
||||
public class AddEntityAttribute : Attribute |
||||
{ |
||||
public Type Entity { get; } |
||||
internal AddEntityAttribute(Type entity) => Entity = entity; |
||||
} |
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)] |
||||
public class AddRelationAttribute : Attribute |
||||
{ |
||||
public Type Relation { get; } |
||||
public Type Target { get; } |
||||
internal AddRelationAttribute(Type relation, Type target) |
||||
{ Relation = relation; Target = target; } |
||||
} |
@ -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,14 +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> |
||||
[Singleton] |
||||
public struct Game { } |
||||
|
||||
/// <summary> Equivalent to <see cref="SourceAttribute{Game}"/>. </summary> |
||||
[AttributeUsage(AttributeTargets.Parameter)] |
||||
public class GameAttribute : SourceAttribute<Game> { } |
@ -0,0 +1,69 @@ |
||||
using System; |
||||
using static gaemstone.Flecs.Core; |
||||
|
||||
namespace gaemstone.ECS; |
||||
|
||||
/// <summary> |
||||
/// Entities marked with this attribute are automatically registered with a |
||||
/// <see cref="EntityRef.Symbol"/> specified in the attribute constructor, |
||||
/// defaulting to their <see cref="EntityRef.Name"/> if not given. |
||||
/// |
||||
/// Symbols are unique string identifiers used to look up their entities. |
||||
/// </summary> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |
||||
| AttributeTargets.Enum | AttributeTargets.Method)] |
||||
public class SymbolAttribute : Attribute |
||||
{ |
||||
public string? Value { get; } |
||||
public SymbolAttribute() { } |
||||
public SymbolAttribute(string value) => Value = value; |
||||
} |
||||
|
||||
|
||||
// TODO: Should this be renamed from [Add<...>] to something else? |
||||
/// <summary> |
||||
/// Marked entity automatically has the specified entity added to it when |
||||
/// automatically registered. Equivalent to <see cref="EntityBuilder.Add{T}"/>. |
||||
/// </summary> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |
||||
| AttributeTargets.Enum | AttributeTargets.Method, |
||||
AllowMultiple = true)] |
||||
public class AddAttribute<TEntity> : Attribute { } |
||||
|
||||
/// <summary> |
||||
/// Marked entity automatically has the specified relationship pair added to it when |
||||
/// automatically registered. Equivalent to <see cref="EntityBuilder.Add{TRelation, TTarget}"/>. |
||||
/// </summary> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |
||||
| AttributeTargets.Enum | AttributeTargets.Method, |
||||
AllowMultiple = true)] |
||||
public class AddAttribute<TRelation, TTarget> : Attribute { } |
||||
|
||||
|
||||
/// <summary> |
||||
/// Marked entity automatically has the specified component added to it when |
||||
/// automatically registered. Equivalent to <see cref="EntityBuilder.Set{TComponent}"/>. |
||||
/// Arguments are passed to a valid constructor with matching arguments. Only |
||||
/// attribute-safe primitives are allowed for the specified arguments. |
||||
/// </summary> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |
||||
| AttributeTargets.Enum | AttributeTargets.Method, |
||||
AllowMultiple = true)] |
||||
public class SetAttribute<TComponent> : Attribute |
||||
{ |
||||
public object[] Arguments { get; } |
||||
public SetAttribute(params object[] args) => Arguments = args; |
||||
} |
||||
|
||||
|
||||
/// <seealso cref="IsA"/> |
||||
public class IsAAttribute<TTarget> : AddAttribute<IsA, TTarget> { } |
||||
/// <seealso cref="ChildOf"/> |
||||
public class ChildOfAttribute<TTarget> : AddAttribute<ChildOf, TTarget> { } |
||||
/// <seealso cref="DependsOn"/> |
||||
public class DependsOnAttribute<TTarget> : AddAttribute<DependsOn, TTarget> { } |
||||
|
||||
/// <seealso cref="Exclusive"/> |
||||
public class ExclusiveAttribute : AddAttribute<Exclusive> { } |
||||
/// <seealso cref="With"/> |
||||
public class WithAttribute<TTarget> : AddAttribute<With, TTarget> { } |
@ -0,0 +1,46 @@ |
||||
using System; |
||||
|
||||
namespace gaemstone.ECS; |
||||
|
||||
/// <summary> Use a custom name or path for this entity instead of the type's name. </summary> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |
||||
| AttributeTargets.Enum | AttributeTargets.Method)] |
||||
public class PathAttribute : Attribute |
||||
{ |
||||
public string Value { get; } |
||||
public PathAttribute(string value) => Value = value; |
||||
} |
||||
|
||||
[AttributeUsage(AttributeTargets.Struct)] |
||||
public class EntityAttribute : Attribute { } |
||||
|
||||
// On types marked as [Relation], this has an effect on the relation's behavior. |
||||
// Otherwise this has the same behavior as [Entity], but informs people |
||||
[AttributeUsage(AttributeTargets.Struct)] |
||||
public class TagAttribute : Attribute { } |
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] |
||||
public class ComponentAttribute : Attribute { } |
||||
|
||||
/// <summary> |
||||
/// A singleton is a single instance of a [Component] that can be retrieved |
||||
/// without explicitly specifying an entity in a query, where it is equivalent |
||||
/// to <see cref="SourceAttribute{}"/> with itself as the generic type parameter. |
||||
/// </summary> |
||||
public class SingletonAttribute : ComponentAttribute { } |
||||
|
||||
/// <summary> |
||||
/// Marked entity represents a relationship type. |
||||
/// It may be used as the "relation" in a pair. |
||||
/// </summary> |
||||
/// <remarks> |
||||
/// The relationship may have component data associated with |
||||
/// it when added to an entity under these circumstances: |
||||
/// <list type="bullet"> |
||||
/// <item>If marked as a <see cref="TagAttribute"/>, does not carry data.</item> |
||||
/// <item>If marked as a <see cref="ComponentAttribute"/>, carries the relation's data.</item> |
||||
/// <item>If marked with neither, will carry the target's data, if it's a component.</item> |
||||
/// </list> |
||||
/// </remarks> |
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] |
||||
public class RelationAttribute : Attribute { } |
@ -1,11 +1,31 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
|
||||
namespace gaemstone.ECS; |
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] |
||||
public class ModuleAttribute : SingletonAttribute { } |
||||
|
||||
[AttributeUsage(AttributeTargets.Class)] |
||||
public class ModuleAttribute : Attribute { } |
||||
public class BuiltInAttribute : Attribute { } |
||||
|
||||
|
||||
/// <summary> |
||||
/// A concrete implementation of this interface is generated by a source |
||||
/// generator for each type marked as <see cref="ModuleAttribute"/>. |
||||
/// </summary> |
||||
public interface IModule |
||||
{ |
||||
static abstract string Path { get; } |
||||
|
||||
static abstract bool IsBuiltIn { get; } |
||||
|
||||
static abstract IReadOnlyList<string> Dependencies { get; } |
||||
|
||||
static abstract void Initialize<TContext>(Entity<TContext> module); |
||||
} |
||||
|
||||
public interface IModuleInitializer |
||||
{ |
||||
void Initialize(EntityRef module); |
||||
static abstract void Initialize<TContext>(Entity<TContext> module); |
||||
} |
||||
|
@ -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 class DeletionEvent |
||||
{ |
||||
[Relation, Tag] public struct OnDelete { } |
||||
[Relation, Tag] public struct OnDeleteTarget { } |
||||
} |
||||
|
||||
[Module, Path("/flecs/core")] |
||||
public static 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 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 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,224 +1,135 @@ |
||||
using System; |
||||
using System.Collections; |
||||
using System.Collections.Generic; |
||||
using System.Collections.Immutable; |
||||
using System.Linq; |
||||
using gaemstone.ECS; |
||||
using gaemstone.Utility; |
||||
using static gaemstone.Flecs.Core; |
||||
using BindingFlags = System.Reflection.BindingFlags; |
||||
using Module = gaemstone.Flecs.Core.Module; |
||||
|
||||
namespace gaemstone; |
||||
|
||||
public class ModuleManager |
||||
public class ModuleManager<TContext> |
||||
: IEnumerable<ModuleManager<TContext>.IModuleInfo> |
||||
{ |
||||
private readonly Dictionary<Entity, ModuleInfo> _modules = new(); |
||||
private readonly Dictionary<Entity<TContext>, IModuleInfo> _modules = new(); |
||||
|
||||
public Universe Universe { get; } |
||||
public ModuleManager(Universe universe) |
||||
public Universe<TContext> Universe { get; } |
||||
public ModuleManager(Universe<TContext> universe) |
||||
=> Universe = universe; |
||||
|
||||
internal ModuleInfo? Lookup(Entity entity) |
||||
internal IModuleInfo? Lookup(Entity<TContext> entity) |
||||
=> _modules.GetValueOrDefault(entity); |
||||
|
||||
public void RegisterAllFrom(System.Reflection.Assembly assembly) |
||||
=> RegisterAll(assembly.GetTypes()); |
||||
public void RegisterAll(IEnumerable<Type> types) |
||||
public Entity<TContext> Register<T>() |
||||
where T : IModule |
||||
{ |
||||
foreach (var type in types) |
||||
if (type.Has<ModuleAttribute>()) |
||||
Register(type); |
||||
} |
||||
|
||||
public EntityRef Register<T>() |
||||
where T : class => Register(typeof(T)); |
||||
public EntityRef Register(Type moduleType) |
||||
{ |
||||
if (!moduleType.IsClass || moduleType.IsGenericType || moduleType.IsGenericTypeDefinition) throw new Exception( |
||||
$"Module {moduleType} must be a non-generic class"); |
||||
if (moduleType.Get<ModuleAttribute>() is not ModuleAttribute moduleAttr) throw new Exception( |
||||
$"Module {moduleType} must be marked with ModuleAttribute"); |
||||
|
||||
// Check if module type is static. |
||||
if (moduleType.IsAbstract && moduleType.IsSealed) { |
||||
|
||||
// Static modules represent existing modules, as such they don't |
||||
// create entities, only look up existing ones to add type lookups |
||||
// for use with the Lookup(Type) method. |
||||
|
||||
if (moduleType.Get<PathAttribute>()?.Value is not string modulePathStr) throw new Exception( |
||||
$"Existing module {moduleType} must have {nameof(PathAttribute)} set"); |
||||
var modulePath = EntityPath.Parse(modulePathStr); |
||||
var moduleEntity = Universe.LookupByPath(modulePath) ?? throw new Exception( |
||||
$"Existing module {moduleType} with name '{modulePath}' not found"); |
||||
|
||||
// This implementation is pretty naive. It simply gets all nested |
||||
// types which are tagged with an ICreateEntityAttribute base |
||||
// attribute and creates a lookup mapping. No sanity checking. |
||||
|
||||
foreach (var type in moduleType.GetNestedTypes()) { |
||||
if (!type.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().Any()) continue; |
||||
|
||||
var attr = type.Get<PathAttribute>(); |
||||
var path = EntityPath.Parse(attr?.Value ?? type.Name); |
||||
var entity = Universe.LookupByPathOrThrow(moduleEntity, path); |
||||
entity.CreateLookup(type); |
||||
|
||||
if (type.Has<RelationAttribute>()) entity.Add<Doc.Relation>(); |
||||
} |
||||
// if (!typeof(T).IsAssignableTo(typeof(IModule))) throw new ArgumentException( |
||||
// $"The specified type {typeof(T)} does not implement IModule", nameof(T)); |
||||
|
||||
return moduleEntity; |
||||
|
||||
} else { |
||||
|
||||
var path = GetModulePath(moduleType); |
||||
var module = new ModuleInfo(Universe, moduleType, path); |
||||
_modules.Add(module.Entity, module); |
||||
TryEnableModule(module); |
||||
return module.Entity; |
||||
|
||||
} |
||||
var module = new ModuleInfo<T>(Universe); |
||||
_modules.Add(module.Entity, module); |
||||
TryEnableModule(module); |
||||
return module.Entity; |
||||
} |
||||
|
||||
private void TryEnableModule(ModuleInfo module) |
||||
private void TryEnableModule(IModuleInfo module) |
||||
{ |
||||
if (module.UnmetDependencies.Count > 0) return; |
||||
if (!module.Dependencies.All(dep => dep.IsDependencyMet)) return; |
||||
|
||||
Console.WriteLine($"Enabling module {module.Entity.Path}"); |
||||
|
||||
Console.WriteLine($"Enabling module {module.Path} ..."); |
||||
module.Enable(); |
||||
|
||||
// Find other modules that might be missing this module as a dependency. |
||||
foreach (var other in _modules.Values) { |
||||
if (other.IsActive) continue; |
||||
if (!other.UnmetDependencies.Contains(module.Entity)) continue; |
||||
if (other.IsInitialized) continue; |
||||
var dependency = other.Dependencies.FirstOrDefault(dep => dep.Entity == module.Entity); |
||||
if (dependency == null) continue; |
||||
|
||||
// Move the just enabled module from unmet to met depedencies. |
||||
other.UnmetDependencies.Remove(module.Entity); |
||||
other.MetDependencies.Add(module); |
||||
dependency.Info = module; |
||||
dependency.IsDependencyMet = true; |
||||
|
||||
TryEnableModule(other); |
||||
} |
||||
} |
||||
|
||||
public static EntityPath GetModulePath(Type type) |
||||
public interface IModuleInfo |
||||
{ |
||||
var attr = type.Get<ModuleAttribute>(); |
||||
if (attr == null) throw new ArgumentException( |
||||
$"Module {type} must be marked with ModuleAttribute", nameof(type)); |
||||
|
||||
var path = EntityPath.Parse( |
||||
(type.Get<PathAttribute>() is PathAttribute pathAttr) |
||||
? pathAttr.Value : type.Name); |
||||
Entity<TContext> Entity { get; } |
||||
IReadOnlyCollection<ModuleDependency> Dependencies { get; } |
||||
bool IsInitialized { get; } |
||||
void Enable(); |
||||
} |
||||
|
||||
// If specified path is absolute, return it now. |
||||
if (path.IsAbsolute) return path; |
||||
// IEnumerable implementation |
||||
public IEnumerator<IModuleInfo> GetEnumerator() => _modules.Values.GetEnumerator(); |
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
||||
|
||||
// Otherwise, create it based on the type's assembly, namespace and name. |
||||
var assemblyName = type.Assembly.GetName().Name!; |
||||
|
||||
if (!type.FullName!.StartsWith(assemblyName + '.')) throw new InvalidOperationException( |
||||
$"Module {type} must be defined under namespace {assemblyName}"); |
||||
var fullNameWithoutAssembly = type.FullName![(assemblyName.Length + 1)..]; |
||||
public class ModuleDependency |
||||
{ |
||||
public Entity<TContext> Entity { get; } |
||||
public IModuleInfo? Info { get; internal set; } |
||||
public bool IsDependencyMet { get; internal set; } |
||||
|
||||
var parts = fullNameWithoutAssembly.Split('.')[..^1]; |
||||
return new(true, parts.Prepend(assemblyName).Concat(path.GetParts()).ToArray()); |
||||
public ModuleDependency(Entity<TContext> entity, |
||||
IModuleInfo? info = null, bool isDependencyMet = false) |
||||
{ |
||||
Entity = entity; |
||||
Info = info; |
||||
IsDependencyMet = isDependencyMet; |
||||
} |
||||
} |
||||
|
||||
internal class ModuleInfo |
||||
internal class ModuleInfo<T> : IModuleInfo |
||||
where T : IModule |
||||
{ |
||||
public Universe Universe { get; } |
||||
public Type Type { get; } |
||||
public EntityPath Path { get; } |
||||
|
||||
public EntityRef Entity { get; } |
||||
public object? Instance { get; internal set; } |
||||
public bool IsActive => Instance != null; |
||||
public Entity<TContext> Entity { get; } |
||||
public IReadOnlyCollection<ModuleDependency> Dependencies { get; } |
||||
public bool IsInitialized { get; private set; } |
||||
|
||||
public HashSet<ModuleInfo> MetDependencies { get; } = new(); |
||||
public HashSet<Entity> UnmetDependencies { get; } = new(); |
||||
|
||||
public ModuleInfo(Universe universe, Type type, EntityPath path) |
||||
public ModuleInfo(Universe<TContext> universe) |
||||
{ |
||||
Universe = universe; |
||||
Type = type; |
||||
Path = path; |
||||
|
||||
if (Type.IsAbstract || Type.IsSealed) throw new Exception( |
||||
$"Module {Type} must not be abstract, sealed or static"); |
||||
if (Type.GetConstructor(Type.EmptyTypes) == null) throw new Exception( |
||||
$"Module {Type} must define public parameterless constructor"); |
||||
|
||||
var module = Universe.New(Path).Add<Module>(); |
||||
var world = universe.World; |
||||
|
||||
// Add module dependencies from [DependsOn<>] attributes. |
||||
foreach (var dependsAttr in Type.GetMultiple<AddRelationAttribute>().Where(attr => |
||||
attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) { |
||||
var dependsPath = GetModulePath(dependsAttr.Target); |
||||
var dependency = Universe.LookupByPath(dependsPath) ?? |
||||
Universe.New(dependsPath).Add<Module>().Disable().Build(); |
||||
|
||||
var depModule = Universe.Modules.Lookup(dependency); |
||||
if (depModule?.IsActive == true) MetDependencies.Add(depModule); |
||||
else { UnmetDependencies.Add(dependency); module.Disable(); } |
||||
|
||||
module.Add<DependsOn>(dependency); |
||||
if (T.IsBuiltIn) |
||||
{ |
||||
Entity = world.LookupPathOrThrow(T.Path); |
||||
Dependencies = Array.Empty<ModuleDependency>(); |
||||
} |
||||
else |
||||
{ |
||||
var builder = world.New(T.Path); |
||||
var deps = new List<ModuleDependency>(); |
||||
|
||||
builder.Add<Module>(); |
||||
foreach (var dependsPath in T.Dependencies) { |
||||
var dependency = world.LookupPathOrNull(dependsPath) ?? |
||||
world.New(dependsPath).Add<Module>().Add<Disabled>().Build(); |
||||
|
||||
var depModule = universe.Modules.Lookup(dependency); |
||||
var isDepInit = (depModule?.IsInitialized == true); |
||||
|
||||
deps.Add(new(dependency, depModule, isDepInit)); |
||||
if (!isDepInit) builder.Add<Disabled>(); |
||||
builder.Add<DependsOn>(dependency); |
||||
} |
||||
|
||||
Entity = builder.Build().CreateLookup<T>(); |
||||
Dependencies = deps.AsReadOnly(); |
||||
|
||||
// Ensure all parent entities have the Module tag set. |
||||
for (var p = Entity.Parent; p is Entity<TContext> parent; p = parent.Parent) |
||||
parent.Add<Module>(); |
||||
} |
||||
|
||||
Entity = module.Build().CreateLookup(Type); |
||||
|
||||
// Ensure all parent entities have Module set. |
||||
for (var p = Entity.Parent; p != null; p = p.Parent) |
||||
p.Add<Module>(); |
||||
} |
||||
|
||||
public void Enable() |
||||
{ |
||||
Entity.Enable(); |
||||
Instance = Activator.CreateInstance(Type)!; |
||||
foreach (var type in Type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) |
||||
RegisterNestedType(type); |
||||
(Instance as IModuleInitializer)?.Initialize(Entity); |
||||
RegisterMethods(Instance); |
||||
} |
||||
|
||||
private void RegisterNestedType(Type type) |
||||
{ |
||||
if (!type.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().Any()) return; |
||||
|
||||
if (!type.Has<ComponentAttribute>() && (!type.IsValueType || (type.GetFields().Length > 0))) |
||||
throw new Exception($"Type {type} must be an empty, used-defined struct."); |
||||
|
||||
var path = EntityPath.Parse(type.Get<PathAttribute>()?.Value ?? type.Name); |
||||
var builder = path.IsAbsolute ? Universe.New(path) : Entity.NewChild(path); |
||||
|
||||
if (type.Get<SymbolAttribute>() is SymbolAttribute symbolAttr) |
||||
builder.Symbol(symbolAttr.Value ?? path.Name); |
||||
|
||||
var entity = builder.Build(); |
||||
|
||||
EntityRef Lookup(Type toLookup) |
||||
=> (type != toLookup) ? Universe.LookupByTypeOrThrow(toLookup) : entity; |
||||
foreach (var attr in type.GetMultiple<AddEntityAttribute>()) |
||||
entity.Add(Lookup(attr.Entity)); |
||||
foreach (var attr in type.GetMultiple<AddRelationAttribute>()) |
||||
entity.Add(Lookup(attr.Relation), Lookup(attr.Target)); |
||||
|
||||
if (type.Get<SingletonAttribute>()?.AutoAdd == true) entity.Add(entity); |
||||
if (type.Has<ComponentAttribute>()) entity.InitComponent(type); |
||||
else entity.CreateLookup(type); |
||||
if (type.Has<RelationAttribute>()) entity.Add<Doc.Relation>(); |
||||
if (type.Has<TagAttribute>()) entity.Add<Tag>(); |
||||
} |
||||
|
||||
private void RegisterMethods(object? instance) |
||||
{ |
||||
foreach (var method in Type.GetMethods( |
||||
BindingFlags.Public | BindingFlags.NonPublic | |
||||
BindingFlags.Static | BindingFlags.Instance |
||||
)) { |
||||
if (method.Has<SystemAttribute>()) |
||||
Universe.InitSystem(instance, method).ChildOf(Entity); |
||||
if (method.Has<ObserverAttribute>()) |
||||
Universe.InitObserver(instance, method).ChildOf(Entity); |
||||
} |
||||
T.Initialize(Entity); |
||||
IsInitialized = true; |
||||
} |
||||
} |
||||
} |
||||
|
@ -1,35 +1,26 @@ |
||||
using System.Linq; |
||||
using gaemstone.ECS; |
||||
|
||||
namespace gaemstone; |
||||
|
||||
public class Universe : World |
||||
public class Universe<TContext> |
||||
{ |
||||
public ModuleManager Modules { get; } |
||||
public World<TContext> World { get; } |
||||
public ModuleManager<TContext> Modules { get; } |
||||
|
||||
public Universe(params string[] args) |
||||
: base(args) |
||||
{ |
||||
World = new(args); |
||||
Modules = new(this); |
||||
|
||||
// Bootstrap some stuff, so we can register flecs properly. |
||||
New("/gaemstone/Doc/DisplayType") |
||||
.Add(LookupByPathOrThrow("/flecs/core/Exclusive")) |
||||
.Build().CreateLookup<Doc.DisplayType>(); |
||||
New("/gaemstone/Doc/Relation").Build().CreateLookup<Doc.Relation>(); |
||||
LookupByPathOrThrow("/flecs/core/Module" ).CreateLookup<Flecs.Core.Module>(); |
||||
LookupByPathOrThrow("/flecs/core/Component").CreateLookup<Flecs.Core.Component>(); |
||||
LookupByPathOrThrow("/flecs/core/Tag" ).CreateLookup<Flecs.Core.Tag>(); |
||||
// Bootstrap [Relation] tag, since it will be added to some Flecs types. |
||||
World.New("/gaemstone/Doc/Relation").Build() |
||||
.CreateLookup<Doc.Relation>(); |
||||
|
||||
// Register built-in (static) modules, which |
||||
// are defined in the "gaemstone.Flecs" namespace. |
||||
Modules.RegisterAll(GetType().Assembly.GetTypes() |
||||
.Where(t => t.IsAbstract && t.IsSealed)); |
||||
// Bootstrap built-in (static) modules from Flecs. |
||||
Modules.Register<Flecs.Core>(); |
||||
Modules.Register<Flecs.Doc>(); |
||||
Modules.Register<Flecs.Pipeline>(); |
||||
|
||||
Modules.Register<Doc>(); |
||||
|
||||
// Create "Game" entity to store global state. |
||||
New("/Game").Symbol("Game").Build() |
||||
.CreateLookup<Game>().Add<Game>(); |
||||
} |
||||
} |
||||
|
@ -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