Rework and introduce more attributes

- Update LangVersion to "preview", which allows us
  to use the generic attributes feature from C# 11
- Any attribute implementing ICreateEntityAttribute
  now registers an entity for the marked type
- [Proxy<T>] registers type not owned by the module
- [Add<TEntity>] and [Add<TRelation, TTarget>]
  which will call EntityBuilder.Set(...) on registration
- A number of shorthand attributes for [Add<...>]
- Re-introduce [Singleton]
- IterActionGenerator has been cleaned up a bit ..
- .. and as a result now supports queries, which don't
  by design don't match any (non-sourced) entities
- Add [DependsOn] to modules that were missing them
wip/source-generators
copygirl 2 years ago
parent 740d289ac8
commit 4a5494bfa3
  1. 1
      src/Immersion/Immersion.csproj
  2. 7
      src/Immersion/ObserverTest.cs
  3. 4
      src/Immersion/Program.cs
  4. 1
      src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs
  5. 1
      src/gaemstone.Bloxel/gaemstone.Bloxel.csproj
  6. 12
      src/gaemstone.Client/Components/ResourceComponents.cs
  7. 6
      src/gaemstone.Client/Systems/FreeCameraController.cs
  8. 7
      src/gaemstone.Client/Systems/Input.cs
  9. 3
      src/gaemstone.Client/Systems/MeshManager.cs
  10. 14
      src/gaemstone.Client/Systems/Renderer.cs
  11. 7
      src/gaemstone.Client/Systems/TextureManager.cs
  12. 2
      src/gaemstone.Client/Systems/Windowing.cs
  13. 1
      src/gaemstone.Client/gaemstone.Client.csproj
  14. 107
      src/gaemstone/ECS/Attributes.cs
  15. 18
      src/gaemstone/ECS/Entity.cs
  16. 10
      src/gaemstone/ECS/Game.cs
  17. 3
      src/gaemstone/ECS/Identifier.cs
  18. 9
      src/gaemstone/ECS/Module.cs
  19. 7
      src/gaemstone/ECS/Observer.cs
  20. 4
      src/gaemstone/ECS/System.cs
  21. 7
      src/gaemstone/ECS/TermAttributes.cs
  22. 68
      src/gaemstone/ECS/Universe+Modules.cs
  23. 3
      src/gaemstone/ECS/Universe.cs
  24. 40
      src/gaemstone/Flecs/SystemPhase.cs
  25. 5
      src/gaemstone/Utility/CStringExtensions.cs
  26. 260
      src/gaemstone/Utility/IL/IterActionGenerator.cs
  27. 1
      src/gaemstone/gaemstone.csproj

@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<LangVersion>preview</LangVersion>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings> <ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>

@ -2,14 +2,15 @@ using System;
using gaemstone.ECS; using gaemstone.ECS;
using gaemstone.Flecs; using gaemstone.Flecs;
using static gaemstone.Bloxel.Components.CoreComponents; using static gaemstone.Bloxel.Components.CoreComponents;
using static gaemstone.Client.Components.RenderingComponents;
namespace Immersion; namespace Immersion;
[Module] [Module]
[DependsOn<gaemstone.Bloxel.Components.CoreComponents>]
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
public class ObserverTest public class ObserverTest
{ {
[Observer(typeof(ObserverEvent.OnSet))] [Observer<ObserverEvent.OnSet>(Expression = "[in] Chunk, [none] (Mesh, *)")]
public static void DoObserver(in Chunk chunk, in MeshHandle _) public static void DoObserver(in Chunk chunk)
=> Console.WriteLine($"Chunk at {chunk.Position} now has a Mesh!"); => Console.WriteLine($"Chunk at {chunk.Position} now has a Mesh!");
} }

@ -1,4 +1,4 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
using gaemstone.Bloxel; using gaemstone.Bloxel;
@ -31,6 +31,8 @@ var window = Window.Create(WindowOptions.Default with {
window.Initialize(); window.Initialize();
window.Center(); window.Center();
// universe.Modules.Register<ObserverTest>();
universe.Modules.Register<gaemstone.Client.Systems.Windowing>(); universe.Modules.Register<gaemstone.Client.Systems.Windowing>();
game.Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window))); game.Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window)));
game.Set(new GameWindow(window)); game.Set(new GameWindow(window));

@ -6,6 +6,7 @@ using static gaemstone.Bloxel.Constants;
namespace gaemstone.Bloxel.Systems; namespace gaemstone.Bloxel.Systems;
[Module] [Module]
[DependsOn<gaemstone.Bloxel.Components.CoreComponents>]
public class BasicWorldGenerator public class BasicWorldGenerator
{ {
private readonly FastNoiseLite _noise; private readonly FastNoiseLite _noise;

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<LangVersion>preview</LangVersion>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings> <ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>

@ -5,18 +5,20 @@ namespace gaemstone.Client.Components;
[Module] [Module]
public class ResourceComponents public class ResourceComponents
{ {
// Entities can have for example Texture as a tag, in which case
// they're the actual resource holding the data or handle.
[Tag] [Tag]
public struct Resource { } public struct Resource { }
// Entities can have for example Texture as a tag, in which case
// they're the actual resource holding the data or handle.
//
// Entities can also have a (Texture, $T) pair where $T is a resource, // Entities can also have a (Texture, $T) pair where $T is a resource,
// meaning the entity has that resource assigned as their texture. // meaning the entity has that resource assigned as their texture.
[Tag, Relation] // TODO: Reintroduce IsA when flecs bug is fixed.
// https://github.com/SanderMertens/flecs/issues/858
[Tag, Relation]//, IsA<Resource>]
public struct Texture { } public struct Texture { }
[Tag, Relation] [Tag, Relation]//, IsA<Resource>]
public struct Mesh { } public struct Mesh { }
} }

@ -1,5 +1,4 @@
using System; using System;
using gaemstone.Client.Components;
using gaemstone.ECS; using gaemstone.ECS;
using Silk.NET.Input; using Silk.NET.Input;
using Silk.NET.Maths; using Silk.NET.Maths;
@ -10,8 +9,9 @@ using static gaemstone.Components.TransformComponents;
namespace gaemstone.Client.Systems; namespace gaemstone.Client.Systems;
[Module] [Module]
[DependsOn(typeof(CameraComponents))] [DependsOn<gaemstone.Client.Components.CameraComponents>]
[DependsOn(typeof(Input))] [DependsOn<gaemstone.Client.Systems.Input>]
[DependsOn<gaemstone.Components.TransformComponents>]
public class FreeCameraController public class FreeCameraController
{ {
[Component] [Component]

@ -10,7 +10,7 @@ using static gaemstone.Client.Systems.Windowing;
namespace gaemstone.Client.Systems; namespace gaemstone.Client.Systems;
[Module] [Module]
[DependsOn(typeof(Windowing))] [DependsOn<gaemstone.Client.Systems.Windowing>]
public class Input public class Input
{ {
[Component] [Component]
@ -36,8 +36,9 @@ public class Input
public bool Released; public bool Released;
} }
[System(typeof(SystemPhase.OnLoad))] [System<SystemPhase.OnLoad>]
public static void ProcessInput(GameWindow window, RawInput input, TimeSpan delta) public static void ProcessInput(TimeSpan delta,
GameWindow window, RawInput input)
{ {
window.Handle.DoEvents(); window.Handle.DoEvents();

@ -11,6 +11,9 @@ using ModelRoot = SharpGLTF.Schema2.ModelRoot;
namespace gaemstone.Client.Systems; namespace gaemstone.Client.Systems;
[Module] [Module]
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
[DependsOn<gaemstone.Client.Components.ResourceComponents>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
public class MeshManager public class MeshManager
{ {
private const uint PositionAttribIndex = 0; private const uint PositionAttribIndex = 0;

@ -15,10 +15,10 @@ using static gaemstone.Components.TransformComponents;
namespace gaemstone.Client.Systems; namespace gaemstone.Client.Systems;
[Module] [Module]
[DependsOn(typeof(gaemstone.Components.TransformComponents))] [DependsOn<gaemstone.Client.Components.CameraComponents>]
[DependsOn(typeof(gaemstone.Client.Components.CameraComponents))] [DependsOn<gaemstone.Client.Components.RenderingComponents>]
[DependsOn(typeof(gaemstone.Client.Components.RenderingComponents))] [DependsOn<gaemstone.Client.Systems.Windowing>]
[DependsOn(typeof(gaemstone.Client.Systems.Windowing))] [DependsOn<gaemstone.Components.TransformComponents>]
public class Renderer public class Renderer
: IModuleInitializer : IModuleInitializer
{ {
@ -54,7 +54,7 @@ public class Renderer
_modelMatrixUniform = GL.GetUniformLocation(_program, "modelMatrix"); _modelMatrixUniform = GL.GetUniformLocation(_program, "modelMatrix");
} }
[System(typeof(SystemPhase.PreStore))] [System<SystemPhase.PreStore>]
public void Clear(Canvas canvas) public void Clear(Canvas canvas)
{ {
var GL = canvas.GL; var GL = canvas.GL;
@ -64,7 +64,7 @@ public class Renderer
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
} }
[System(typeof(SystemPhase.OnStore))] [System<SystemPhase.OnStore>]
public void Render(Universe universe, [Game] Canvas canvas, public void Render(Universe universe, [Game] Canvas canvas,
in GlobalTransform cameraTransform, in Camera camera, CameraViewport? viewport) in GlobalTransform cameraTransform, in Camera camera, CameraViewport? viewport)
{ {
@ -125,7 +125,7 @@ public class Renderer
} }
} }
[System(typeof(SystemPhase.PostFrame))] [System<SystemPhase.PostFrame>]
public static void SwapBuffers(GameWindow window) public static void SwapBuffers(GameWindow window)
=> window.Handle.SwapBuffers(); => window.Handle.SwapBuffers();

@ -1,6 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using gaemstone.Client.Components;
using gaemstone.ECS; using gaemstone.ECS;
using Silk.NET.OpenGL; using Silk.NET.OpenGL;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
@ -12,9 +11,9 @@ using Texture = gaemstone.Client.Components.ResourceComponents.Texture;
namespace gaemstone.Client.Systems; namespace gaemstone.Client.Systems;
[Module] [Module]
[DependsOn(typeof(RenderingComponents))] [DependsOn<gaemstone.Client.Components.RenderingComponents>]
[DependsOn(typeof(ResourceComponents))] [DependsOn<gaemstone.Client.Components.ResourceComponents>]
[DependsOn(typeof(Windowing))] [DependsOn<gaemstone.Client.Systems.Windowing>]
public class TextureManager public class TextureManager
: IModuleInitializer : IModuleInitializer
{ {

@ -26,7 +26,7 @@ public class Windowing
public GameWindow(IWindow handle) => Handle = handle; public GameWindow(IWindow handle) => Handle = handle;
} }
[System(typeof(SystemPhase.PreFrame))] [System<SystemPhase.PreFrame>]
public static void ProcessWindow(GameWindow window, Canvas canvas) public static void ProcessWindow(GameWindow window, Canvas canvas)
=> canvas.Size = window.Handle.Size; => canvas.Size = window.Handle.Size;
} }

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<LangVersion>preview</LangVersion>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings> <ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>

@ -0,0 +1,107 @@
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="Universe.Lookup(Type)"/>.
/// </summary>
public interface ICreateEntityAttribute { }
/// <summary>
/// By default, entities registered automatically have their symbol
/// (a globally unique identifier) set equal to their name. If they
/// are also marked with this attribute, the symbol won't be set.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class PrivateAttribute : Attribute { }
/// <summary>
/// Register the proxied type instead of the one marked with this attribute.
/// This can be used to make types not registered in a module available,
/// including types registered in other assemblies.
/// </summary>
public class ProxyAttribute<T> : ProxyAttribute
{ public ProxyAttribute() : base(typeof(T)) { } }
/// <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)) { } }
/// <seealso cref="Tag"/>
[AttributeUsage(AttributeTargets.Struct)]
public class TagAttribute : AddAttribute<Tag>, ICreateEntityAttribute { }
/// <summary>
/// Marked entity represents a relationship type, meaning it may be used as
/// the "relation" in a pair. However, this attribute is purely informational.
/// </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)]
public class RelationAttribute : Attribute, ICreateEntityAttribute { }
/// <seealso cref="IsA"/>
public class IsAAttribute<TTarget> : AddAttribute<IsA, TTarget> { }
/// <seealso cref="ChildOf"/>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
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.Struct)]
public class ProxyAttribute : Attribute
{
public Type Type { get; }
internal ProxyAttribute(Type type) => Type = type;
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
public class AddEntityAttribute : Attribute
{
public Type Entity { get; }
internal AddEntityAttribute(Type entity) => Entity = entity;
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
public class AddRelationAttribute : Attribute
{
public Type Relation { get; }
public Type Target { get; }
internal AddRelationAttribute(Type relation, Type target)
{ Relation = relation; Target = target; }
}

@ -4,15 +4,15 @@ using static flecs_hub.flecs;
namespace gaemstone.ECS; namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class EntityAttribute : Attribute public class EntityAttribute : Attribute, ICreateEntityAttribute
{ public string? Name { get; set; } } { public string? Name { get; init; } }
[AttributeUsage(AttributeTargets.Struct)] /// <summary>
public class TagAttribute : EntityAttribute { } /// 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
/// <summary> Unused, purely informational. </summary> /// to <see cref="SourceAttribute{}"/> with itself as the generic type parameter.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] /// </summary>
public class RelationAttribute : Attribute { } public class SingletonAttribute : EntityAttribute { }
public readonly struct Entity public readonly struct Entity
: IEquatable<Entity> : IEquatable<Entity>

@ -6,13 +6,9 @@ namespace gaemstone.ECS;
/// Entity for storing global game state and configuration. /// Entity for storing global game state and configuration.
/// Parameters can use <see cref="GameAttribute"/> to source this entity. /// Parameters can use <see cref="GameAttribute"/> to source this entity.
/// </summary> /// </summary>
[Entity, Tag] [Entity]
public struct Game { } public struct Game { }
/// <summary> Short for <c>[Source(typeof(Game))]</c>. </summary> /// <summary> Equivalent to <see cref="SourceAttribute{Game}"/>. </summary>
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
public class GameAttribute : SourceAttribute public class GameAttribute : SourceAttribute<Game> { }
{
public GameAttribute()
: base(typeof(Game)) { }
}

@ -11,9 +11,10 @@ public readonly struct Identifier
public bool IsPair => ecs_id_is_pair(Value); public bool IsPair => ecs_id_is_pair(Value);
public bool IsWildcard => ecs_id_is_wildcard(Value); public bool IsWildcard => ecs_id_is_wildcard(Value);
public IdentifierFlags Flags => (IdentifierFlags)(Value & ECS_ID_FLAGS_MASK);
public Entity RelationUnsafe => new(new() { Data = (Value & ECS_COMPONENT_MASK) >> 32 }); public Entity RelationUnsafe => new(new() { Data = (Value & ECS_COMPONENT_MASK) >> 32 });
public Entity TargetUnsafe => new(new() { Data = Value & ECS_ENTITY_MASK }); public Entity TargetUnsafe => new(new() { Data = Value & ECS_ENTITY_MASK });
public IdentifierFlags Flags => (IdentifierFlags)(Value & ECS_ID_FLAGS_MASK);
public Identifier(ecs_id_t value) => Value = value; public Identifier(ecs_id_t value) => Value = value;

@ -16,15 +16,6 @@ public class ModuleAttribute : Attribute
} }
} }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DependsOnAttribute : Attribute
{
public Type Type { get; }
public DependsOnAttribute(Type type)
{ Type = type; }
}
public interface IModuleInitializer public interface IModuleInitializer
{ {
void Initialize(EntityRef module); void Initialize(EntityRef module);

@ -11,10 +11,11 @@ namespace gaemstone.ECS;
public class ObserverAttribute : Attribute public class ObserverAttribute : Attribute
{ {
public Type Event { get; } public Type Event { get; }
public string? Expression { get; set; } public string? Expression { get; init; }
internal ObserverAttribute(Type @event) => Event = @event; // Use generic type instead.
public ObserverAttribute(Type @event) => Event = @event;
} }
public class ObserverAttribute<TEvent> : ObserverAttribute
{ public ObserverAttribute() : base(typeof(TEvent)) { } }
public static class ObserverExtensions public static class ObserverExtensions
{ {

@ -17,8 +17,10 @@ public class SystemAttribute : Attribute
public string? Expression { get; set; } public string? Expression { get; set; }
public SystemAttribute() : this(typeof(SystemPhase.OnUpdate)) { } public SystemAttribute() : this(typeof(SystemPhase.OnUpdate)) { }
public SystemAttribute(Type phase) => Phase = phase; internal SystemAttribute(Type phase) => Phase = phase; // Use generic type instead.
} }
public class SystemAttribute<TPhase> : SystemAttribute
{ public SystemAttribute() : base(typeof(TPhase)) { } }
public static class SystemExtensions public static class SystemExtensions
{ {

@ -3,13 +3,15 @@ using System;
namespace gaemstone.ECS; namespace gaemstone.ECS;
// TODO: Make it possible to use [Source] on systems. // TODO: Make it possible to use [Source] on systems.
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
public class SourceAttribute : Attribute public class SourceAttribute : Attribute
{ {
public Type Type { get; } public Type Type { get; }
public SourceAttribute(Type type) => Type = type; public SourceAttribute(Type type) => Type = type;
} }
public class SourceAttribute<TEntity> : SourceAttribute
{ public SourceAttribute() : base(typeof(TEntity)) { } }
// Parameters types marked with [Tag] are equivalent to [Has]. // Parameters types marked with [Tag] are equivalent to [Has].
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
@ -23,6 +25,9 @@ public class InAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
public class OutAttribute : Attribute { } public class OutAttribute : Attribute { }
// [AttributeUsage(AttributeTargets.Parameter)]
// public class OrAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
public class NotAttribute : Attribute { } public class NotAttribute : Attribute { }

@ -49,13 +49,14 @@ public class ModuleManager
$"Existing module {type} with name '{path}' not found"); $"Existing module {type} with name '{path}' not found");
// This implementation is pretty naive. It simply gets all nested // This implementation is pretty naive. It simply gets all nested
// types which are tagged with [Entity] attribute or a subtype // types which are tagged with an ICreateEntityAttribute base
// thereof and creates a lookup mapping. No sanity checking. // attribute and creates a lookup mapping. No sanity checking.
foreach (var nested in type.GetNestedTypes()) foreach (var nested in type.GetNestedTypes()) {
if (nested.Get<EntityAttribute>() is EntityAttribute nestedAttr) if (!nested.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().Any()) continue;
Universe.LookupOrThrow(entity, nestedAttr.Name ?? nested.Name) var name = nested.Get<EntityAttribute>()?.Name ?? nested.Name;
.CreateLookup(nested); Universe.LookupOrThrow(entity, name).CreateLookup(nested);
}
return entity; return entity;
@ -141,9 +142,10 @@ internal class ModuleInfo
var module = Universe.New(path).Add<Module>(); var module = Universe.New(path).Add<Module>();
// Add module dependencies from [DependsOn] attributes. // Add module dependencies from [DependsOn<>] attributes.
foreach (var dependsAttr in Type.GetMultiple<DependsOnAttribute>()) { foreach (var dependsAttr in Type.GetMultiple<AddRelationAttribute>().Where(attr =>
var dependsPath = ModuleManager.GetModulePath(dependsAttr.Type); attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) {
var dependsPath = ModuleManager.GetModulePath(dependsAttr.Target);
var dependency = Universe.Lookup(dependsPath) ?? var dependency = Universe.Lookup(dependsPath) ??
Universe.New(dependsPath).Add<Module>().Disable().Build(); Universe.New(dependsPath).Add<Module>().Disable().Build();
@ -169,35 +171,39 @@ internal class ModuleInfo
private void RegisterNestedTypes() private void RegisterNestedTypes()
{ {
foreach (var type in Type.GetNestedTypes()) { foreach (var type in Type.GetNestedTypes()) {
if (type.Get<EntityAttribute>() is not EntityAttribute attr) continue; if (!type.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().Any()) continue;
if (attr.Name != null) { // If proxied type is specified, use it instead of the marked type.
try { EntityPath.ValidateName(attr.Name); } // Attributes are still read from the original type.
catch (Exception ex) { throw new Exception( var proxyType = type.Get<ProxyAttribute>()?.Type ?? type;
$"{type} has invalid entity name '{attr.Name}: {ex.Message}'", ex); }
if (!type.Has<ComponentAttribute>() && (!proxyType.IsValueType || proxyType.GetFields().Length > 0)) {
var typeHint = (proxyType != type) ? $"{proxyType.Name} (proxied by {type})" : type.ToString();
throw new Exception($"Type {typeHint} must be an empty, used-defined struct.");
} }
var name = attr.Name ?? type.Name; var name = type.Get<EntityAttribute>()?.Name ?? proxyType.Name;
var entity = Entity.NewChild(name).Symbol(name); try { EntityPath.ValidateName(name); }
switch (attr) { catch (Exception ex) { throw new Exception(
$"{type} has invalid entity name '{name}: {ex.Message}'", ex); }
case TagAttribute: var builder = Entity.NewChild(name);
if (!type.IsValueType || type.GetFields().Length > 0) throw new Exception( if (!type.Has<PrivateAttribute>()) builder.Symbol(name);
$"Tag {type} must be an empty, used-defined struct.");
entity.Add<Tag>().Build().CreateLookup(type);
break;
case ComponentAttribute: foreach (var attr in type.GetMultiple<AddEntityAttribute>())
entity.Build().CreateComponent(type); builder.Add(Universe.LookupOrThrow(attr.Entity));
break; foreach (var attr in type.GetMultiple<AddRelationAttribute>())
builder.Add(Universe.LookupOrThrow(attr.Relation), Universe.LookupOrThrow(attr.Target));
default: var entity = builder.Build();
if (!type.IsValueType || type.GetFields().Length > 0) throw new Exception(
$"Entity {type} must be an empty, used-defined struct.");
entity.Build().CreateLookup(type);
break;
} if (type.Has<SingletonAttribute>())
entity.Add(entity);
if (type.Has<ComponentAttribute>())
entity.CreateComponent(proxyType);
else
entity.CreateLookup(proxyType);
} }
} }

@ -26,8 +26,7 @@ public unsafe partial class Universe
ChildOf = LookupOrThrow<Flecs.Core.ChildOf>(); ChildOf = LookupOrThrow<Flecs.Core.ChildOf>();
// Create "Game" singleton entity, which // Create "Game" entity to store global state.
// stores global state, configuration, ...
New("Game").Symbol("Game").Build() New("Game").Symbol("Game").Build()
.CreateLookup<Game>().Add<Game>(); .CreateLookup<Game>().Add<Game>();
} }

@ -1,24 +1,30 @@
using gaemstone.ECS; using gaemstone.ECS;
using static gaemstone.Flecs.Pipeline;
namespace gaemstone.Flecs; namespace gaemstone.Flecs;
[Module("flecs", "pipeline")] [Module("flecs", "pipeline")]
public static class SystemPhase public static class SystemPhase
{ {
[Entity] public struct PreFrame { } [Entity, Add<Phase>]
public struct PreFrame { }
/// <summary> /// <summary>
/// This phase contains all the systems that load data into your ECS. /// This phase contains all the systems that load data into your ECS.
/// This would be a good place to load keyboard and mouse inputs. /// This would be a good place to load keyboard and mouse inputs.
/// </summary> /// </summary>
[Entity] public struct OnLoad { } [Entity, Add<Phase>]
[DependsOn<PreFrame>]
public struct OnLoad { }
/// <summary> /// <summary>
/// Often the imported data needs to be processed. Maybe you want to associate /// Often the imported data needs to be processed. Maybe you want to associate
/// your keypresses with high level actions rather than comparing explicitly /// your keypresses with high level actions rather than comparing explicitly
/// in your game code if the user pressed the 'K' key. /// in your game code if the user pressed the 'K' key.
/// </summary> /// </summary>
[Entity] public struct PostLoad { } [Entity, Add<Phase>]
[DependsOn<OnLoad>]
public struct PostLoad { }
/// <summary> /// <summary>
/// Now that the input is loaded and processed, it's time to get ready to /// Now that the input is loaded and processed, it's time to get ready to
@ -27,13 +33,17 @@ public static class SystemPhase
/// This can be a good place to prepare the frame, maybe clean up some /// This can be a good place to prepare the frame, maybe clean up some
/// things from the previous frame, etcetera. /// things from the previous frame, etcetera.
/// </summary> /// </summary>
[Entity] public struct PreUpdate { } [Entity, Add<Phase>]
[DependsOn<PostLoad>]
public struct PreUpdate { }
/// <summary> /// <summary>
/// This is usually where the magic happens! This is where you put all of /// 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. /// your gameplay systems. By default systems are added to this phase.
/// </summary> /// </summary>
[Entity] public struct OnUpdate { } [Entity, Add<Phase>]
[DependsOn<PreUpdate>]
public struct OnUpdate { }
/// <summary> /// <summary>
/// This phase was introduced to deal with validating the state of the game /// This phase was introduced to deal with validating the state of the game
@ -42,7 +52,9 @@ public static class SystemPhase
/// This phase is for righting that wrong. A typical feature to implement /// This phase is for righting that wrong. A typical feature to implement
/// in this phase would be collision detection. /// in this phase would be collision detection.
/// </summary> /// </summary>
[Entity] public struct OnValidate { } [Entity, Add<Phase>]
[DependsOn<OnUpdate>]
public struct OnValidate { }
/// <summary> /// <summary>
/// When your game logic has been updated, and your validation pass has ran, /// When your game logic has been updated, and your validation pass has ran,
@ -50,7 +62,9 @@ public static class SystemPhase
/// detection system detected collisions in the <c>OnValidate</c> phase, /// detection system detected collisions in the <c>OnValidate</c> phase,
/// you may want to move the entities so that they no longer overlap. /// you may want to move the entities so that they no longer overlap.
/// </summary> /// </summary>
[Entity] public struct PostUpdate { } [Entity, Add<Phase>]
[DependsOn<OnValidate>]
public struct PostUpdate { }
/// <summary> /// <summary>
/// Now that all of the frame data is computed, validated and corrected for, /// Now that all of the frame data is computed, validated and corrected for,
@ -59,13 +73,19 @@ public static class SystemPhase
/// A good example would be a system that calculates transform matrices from /// A good example would be a system that calculates transform matrices from
/// a scene graph. /// a scene graph.
/// </summary> /// </summary>
[Entity] public struct PreStore { } [Entity, Add<Phase>]
[DependsOn<PostUpdate>]
public struct PreStore { }
/// <summary> /// <summary>
/// This is where it all comes together. Your frame is ready to be /// 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. /// rendered, and that is exactly what you would do in this phase.
/// </summary> /// </summary>
[Entity] public struct OnStore { } [Entity, Add<Phase>]
[DependsOn<PreStore>]
public struct OnStore { }
[Entity] public struct PostFrame { } [Entity, Add<Phase>]
[DependsOn<OnStore>]
public struct PostFrame { }
} }

@ -13,9 +13,10 @@ public unsafe static class CStringExtensions
public static unsafe byte[]? FlecsToBytes(this CString str) public static unsafe byte[]? FlecsToBytes(this CString str)
{ {
if (str.IsNull) return null; if (str.IsNull) return null;
var length = 0;
var pointer = (byte*)(nint)str; var pointer = (byte*)(nint)str;
while (true) if (pointer[length++] == 0) break; // Find length of the string by locating the NUL character.
var length = 0; while (true) if (pointer[length++] == 0) break;
// Create span over the region, NUL included, copy it into an array.
return new Span<byte>(pointer, length).ToArray(); return new Span<byte>(pointer, length).ToArray();
} }

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Reflection.Emit; using System.Reflection.Emit;
@ -9,6 +10,8 @@ using gaemstone.ECS;
namespace gaemstone.Utility.IL; namespace gaemstone.Utility.IL;
// TODO: Implement "or" operator.
// TODO: Support tuple syntax to match relationship pairs.
public unsafe class IterActionGenerator public unsafe class IterActionGenerator
{ {
private static readonly ConstructorInfo _entityRefCtor = typeof(EntityRef).GetConstructors().Single(); private static readonly ConstructorInfo _entityRefCtor = typeof(EntityRef).GetConstructors().Single();
@ -35,7 +38,7 @@ public unsafe class IterActionGenerator
public Universe Universe { get; } public Universe Universe { get; }
public MethodInfo Method { get; } public MethodInfo Method { get; }
public ParamInfo[] Parameters { get; } public IReadOnlyList<ParamInfo> Parameters { get; }
public IReadOnlyList<Term> Terms { get; } public IReadOnlyList<Term> Terms { get; }
public Action<object?, Iterator> GeneratedAction { get; } public Action<object?, Iterator> GeneratedAction { get; }
@ -44,12 +47,12 @@ public unsafe class IterActionGenerator
public void RunWithTryCatch(object? instance, Iterator iter) public void RunWithTryCatch(object? instance, Iterator iter)
{ {
try { GeneratedAction(instance, iter); } catch { try { GeneratedAction(instance, iter); } catch {
Console.WriteLine("Exception occured while running:"); Console.Error.WriteLine("Exception occured while running:");
Console.WriteLine(" " + Method); Console.Error.WriteLine(" " + Method);
Console.WriteLine(); Console.Error.WriteLine();
Console.WriteLine("Method's IL code:"); Console.Error.WriteLine("Method's IL code:");
Console.WriteLine(ReadableString); Console.Error.WriteLine(ReadableString);
Console.WriteLine(); Console.Error.WriteLine();
throw; throw;
} }
} }
@ -59,46 +62,24 @@ public unsafe class IterActionGenerator
Universe = universe; Universe = universe;
Method = method; Method = method;
Parameters = method.GetParameters().Select(ParamInfo.Build).ToArray(); var name = "<>Query_" + string.Join("_", method.Name);
// if (!Parameters.Any(c => c.IsRequired && (c.Kind != ParamKind.Unique)))
// throw new ArgumentException($"At least one parameter in {method} is required");
var name = "<>Query_" + string.Join("_", Parameters.Select(p => p.UnderlyingType.Name));
var genMethod = new DynamicMethod(name, null, new[] { typeof(object), typeof(Iterator) }); var genMethod = new DynamicMethod(name, null, new[] { typeof(object), typeof(Iterator) });
var IL = new ILGeneratorWrapper(genMethod); var IL = new ILGeneratorWrapper(genMethod);
var instanceArg = IL.Argument<object?>(0); var instanceArg = IL.Argument<object?>(0);
var iteratorArg = IL.Argument<Iterator>(1); var iteratorArg = IL.Argument<Iterator>(1);
// If parameters only contains global unique paremeters (such as var fieldIndex = 1;
// Universe or TimeSpan), or is empty, just run the system, since var parameters = new List<(ParamInfo Info, Term? Term, ILocal? FieldLocal, ILocal? TempLocal)>();
// without terms it won't match any entities anyway. foreach (var info in method.GetParameters()) {
if (Parameters.All(p => p.Kind == ParamKind.GlobalUnique)) { var p = ParamInfo.Build(info);
if (!Method.IsStatic) IL.Load(instanceArg);
foreach (var p in Parameters) {
IL.Comment($"Global unique parameter {p.ParameterType.GetFriendlyName()}");
_globalUniqueParameters[p.ParameterType](IL, iteratorArg);
}
IL.Call(Method);
IL.Return();
Terms = Array.Empty<Term>();
GeneratedAction = genMethod.CreateDelegate<Action<object?, Iterator>>();
ReadableString = IL.ToReadableString();
return;
}
var terms = new List<Term>();
var fieldLocals = new ILocal[Parameters.Length];
var tempLocals = new ILocal[Parameters.Length];
for (var i = 0; i < Parameters.Length; i++) { // If the parameter is unique, we don't create a term for it.
var p = Parameters[i]; if (p.Kind <= ParamKind.Unique)
if (p.Kind <= ParamKind.Unique) continue; { parameters.Add((p, null, null, null)); continue; }
// Add an entry to the terms to look for this type. // Create a term to add to the query.
terms.Add(new(universe.LookupOrThrow(p.UnderlyingType)) { var term = new Term(universe.LookupOrThrow(p.UnderlyingType)) {
Source = (p.Source != null) ? (TermID)Universe.LookupOrThrow(p.Source) : null, Source = (p.Source != null) ? (TermID)Universe.LookupOrThrow(p.Source) : null,
InOut = p.Kind switch { InOut = p.Kind switch {
ParamKind.In => TermInOutKind.In, ParamKind.In => TermInOutKind.In,
@ -108,116 +89,145 @@ public unsafe class IterActionGenerator
}, },
Oper = p.Kind switch { Oper = p.Kind switch {
ParamKind.Not => TermOperKind.Not, ParamKind.Not => TermOperKind.Not,
ParamKind.Or => TermOperKind.Or,
_ when !p.IsRequired => TermOperKind.Optional, _ when !p.IsRequired => TermOperKind.Optional,
_ => default, _ => default,
}, },
}); };
// Create a Span<T> local and initialize it to iterator.Field<T>(i).
var spanType = typeof(Span<>).MakeGenericType(p.FieldType); var spanType = typeof(Span<>).MakeGenericType(p.FieldType);
fieldLocals[i] = IL.Local(spanType, $"field_{i}"); var fieldLocal = IL.Local(spanType, $"{info.Name}Field");
if (p.Kind is ParamKind.Has or ParamKind.Not) { var tempLocal = (ILocal?)null;
switch (p.Kind) {
case ParamKind.Has or ParamKind.Not:
if (!p.ParameterType.IsValueType) break;
// If a "has" or "not" parameter is a struct, we require a temporary local that // If a "has" or "not" parameter is a struct, we require a temporary local that
// we can later load onto the stack when loading the arguments for the action. // we can later load onto the stack when loading the arguments for the action.
if (p.ParameterType.IsValueType) { IL.Comment($"{info.Name}Temp = default({p.ParameterType});");
IL.Comment($"temp_{i} = default({p.ParameterType});"); tempLocal = IL.Local(p.ParameterType);
tempLocals[i] = IL.Local(p.ParameterType); IL.LoadAddr(tempLocal);
IL.LoadAddr(tempLocals[i]); IL.Init(tempLocal.LocalType);
IL.Init(tempLocals[i].LocalType); break;
}
} else if (p.IsRequired) { case ParamKind.Nullable:
IL.Comment($"field_{i} = iterator.Field<{p.FieldType.Name}>({terms.Count})"); IL.Comment($"{info.Name}Field = iterator.MaybeField<{p.FieldType.Name}>({fieldIndex})");
IL.Load(iteratorArg);
IL.LoadConst(terms.Count);
IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocals[i]);
} else {
IL.Comment($"field_{i} = iterator.MaybeField<{p.FieldType.Name}>({terms.Count})");
IL.Load(iteratorArg); IL.Load(iteratorArg);
IL.LoadConst(terms.Count); IL.LoadConst(fieldIndex);
IL.Call(_iteratorMaybeFieldMethod.MakeGenericMethod(p.FieldType)); IL.Call(_iteratorMaybeFieldMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocals[i]); IL.Store(fieldLocal);
}
IL.Comment($"{info.Name}Temp = default({p.ParameterType});");
tempLocal = IL.Local(p.ParameterType);
IL.LoadAddr(tempLocal);
IL.Init(tempLocal.LocalType);
break;
if (p.Kind == ParamKind.Nullable) { default:
IL.Comment($"temp_{i} = default({p.ParameterType});"); IL.Comment($"{info.Name}Field = iterator.Field<{p.FieldType.Name}>({fieldIndex})");
tempLocals[i] = IL.Local(p.ParameterType); IL.Load(iteratorArg);
IL.LoadAddr(tempLocals[i]); IL.LoadConst(fieldIndex);
IL.Init(tempLocals[i].LocalType); IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocal);
break;
} }
parameters.Add((p, term, fieldLocal, tempLocal));
fieldIndex++;
} }
// If there's any reference type parameters, we need to define a GCHandle local. // If there's any reference type parameters, we need to define a GCHandle local.
var hasReferenceType = Parameters var hasReferenceType = parameters
.Where(p => p.Kind > ParamKind.Unique) .Where(p => p.Info.Kind > ParamKind.Unique)
.Any(p => !p.UnderlyingType.IsValueType); .Any(p => !p.Info.UnderlyingType.IsValueType);
var handleLocal = hasReferenceType ? IL.Local<GCHandle>() : null; var handleLocal = hasReferenceType ? IL.Local<GCHandle>() : null;
using (IL.For(() => IL.Load(iteratorArg, _iteratorCountProp), out var currentLocal)) { IDisposable? forLoopBlock = null;
if (!Method.IsStatic) IL.Load(instanceArg); ILocal<int>? forCurrentLocal = null;
for (var i = 0; i < Parameters.Length; i++) { // If all parameters are fixed, iterator count will be 0, but since
var p = Parameters[i]; // the query matched, we want to run the callback at least once.
if (p.Kind == ParamKind.GlobalUnique) { if (parameters.Any(p => !p.Info.IsFixed))
IL.Comment($"Global unique parameter {p.ParameterType.GetFriendlyName()}"); forLoopBlock = IL.For(() => IL.Load(iteratorArg, _iteratorCountProp), out forCurrentLocal);
_globalUniqueParameters[p.ParameterType](IL, iteratorArg);
} else if (p.Kind == ParamKind.Unique) { if (!Method.IsStatic)
IL.Comment($"Unique parameter {p.ParameterType.GetFriendlyName()}"); IL.Load(instanceArg);
_uniqueParameters[p.ParameterType](IL, iteratorArg, currentLocal);
} else if (p.Kind is ParamKind.Has or ParamKind.Not) { foreach (var (info, term, fieldLocal, tempLocal) in parameters) {
if (p.ParameterType.IsValueType) switch (info.Kind) {
IL.LoadObj(tempLocals[i]!);
case ParamKind.GlobalUnique:
IL.Comment($"Global unique parameter {info.ParameterType.GetFriendlyName()}");
_globalUniqueParameters[info.ParameterType](IL, iteratorArg);
break;
case ParamKind.Unique:
IL.Comment($"Unique parameter {info.ParameterType.GetFriendlyName()}");
_uniqueParameters[info.ParameterType](IL, iteratorArg, forCurrentLocal!);
break;
case ParamKind.Has or ParamKind.Not:
if (info.ParameterType.IsValueType)
IL.LoadObj(tempLocal!);
else IL.LoadNull(); else IL.LoadNull();
} else { break;
var spanType = typeof(Span<>).MakeGenericType(p.FieldType);
default:
var spanType = typeof(Span<>).MakeGenericType(info.FieldType);
var spanItemMethod = spanType.GetProperty("Item")!.GetMethod!; var spanItemMethod = spanType.GetProperty("Item")!.GetMethod!;
var spanLengthMethod = spanType.GetProperty("Length")!.GetMethod!; var spanLengthMethod = spanType.GetProperty("Length")!.GetMethod!;
IL.Comment($"Parameter {p.ParameterType.GetFriendlyName()}"); IL.Comment($"Parameter {info.ParameterType.GetFriendlyName()}");
if (p.IsByRef) { if (info.IsByRef) {
IL.LoadAddr(fieldLocals[i]!); IL.LoadAddr(fieldLocal!);
IL.Load(currentLocal); if (info.IsFixed) IL.LoadConst(0);
else IL.Load(forCurrentLocal!);
IL.Call(spanItemMethod); IL.Call(spanItemMethod);
} else if (p.IsRequired) { } else if (info.IsRequired) {
IL.LoadAddr(fieldLocals[i]!); IL.LoadAddr(fieldLocal!);
IL.Load(currentLocal); if (info.IsFixed) IL.LoadConst(0);
else IL.Load(forCurrentLocal!);
IL.Call(spanItemMethod); IL.Call(spanItemMethod);
IL.LoadObj(p.FieldType); IL.LoadObj(info.FieldType);
} else { } else {
var elseLabel = IL.DefineLabel(); var elseLabel = IL.DefineLabel();
var doneLabel = IL.DefineLabel(); var doneLabel = IL.DefineLabel();
IL.LoadAddr(fieldLocals[i]!); IL.LoadAddr(fieldLocal!);
IL.Call(spanLengthMethod); IL.Call(spanLengthMethod);
IL.GotoIfFalse(elseLabel); IL.GotoIfFalse(elseLabel);
IL.LoadAddr(fieldLocals[i]!); IL.LoadAddr(fieldLocal!);
IL.Load(currentLocal); if (info.IsFixed) IL.LoadConst(0);
else IL.Load(forCurrentLocal!);
IL.Call(spanItemMethod); IL.Call(spanItemMethod);
IL.LoadObj(p.FieldType); IL.LoadObj(info.FieldType);
if (p.Kind == ParamKind.Nullable) if (info.Kind == ParamKind.Nullable)
IL.New(p.ParameterType); IL.New(info.ParameterType);
IL.Goto(doneLabel); IL.Goto(doneLabel);
IL.MarkLabel(elseLabel); IL.MarkLabel(elseLabel);
if (p.Kind == ParamKind.Nullable) if (info.Kind == ParamKind.Nullable)
IL.LoadObj(tempLocals[i]!); IL.LoadObj(tempLocal!);
else IL.LoadNull(); else IL.LoadNull();
IL.MarkLabel(doneLabel); IL.MarkLabel(doneLabel);
} }
if (!p.UnderlyingType.IsValueType) { if (!info.UnderlyingType.IsValueType) {
IL.Comment($"Convert nint to {p.UnderlyingType.GetFriendlyName()}"); IL.Comment($"Convert nint to {info.UnderlyingType.GetFriendlyName()}");
IL.Call(_handleFromIntPtrMethod); IL.Call(_handleFromIntPtrMethod);
IL.Store(handleLocal!); IL.Store(handleLocal!);
IL.LoadAddr(handleLocal!); IL.LoadAddr(handleLocal!);
IL.Call(_handleTargetProp.GetMethod!); IL.Call(_handleTargetProp.GetMethod!);
IL.Cast(p.UnderlyingType); IL.Cast(info.UnderlyingType);
} }
break;
} }
} }
IL.Call(Method); IL.Call(Method);
}
forLoopBlock?.Dispose();
IL.Return(); IL.Return();
Terms = terms.AsReadOnly(); Parameters = parameters.Select(p => p.Info).ToImmutableList();
Terms = parameters.Where(p => p.Term != null).Select(p => p.Term!).ToImmutableList();
GeneratedAction = genMethod.CreateDelegate<Action<object?, Iterator>>(); GeneratedAction = genMethod.CreateDelegate<Action<object?, Iterator>>();
ReadableString = IL.ToReadableString(); ReadableString = IL.ToReadableString();
} }
@ -228,7 +238,6 @@ public unsafe class IterActionGenerator
public class ParamInfo public class ParamInfo
{ {
public ParameterInfo Info { get; } public ParameterInfo Info { get; }
public int Index { get; }
public ParamKind Kind { get; } public ParamKind Kind { get; }
public Type ParameterType { get; } public Type ParameterType { get; }
@ -239,36 +248,33 @@ public unsafe class IterActionGenerator
public bool IsRequired => (Kind < ParamKind.Nullable); public bool IsRequired => (Kind < ParamKind.Nullable);
public bool IsByRef => (Kind >= ParamKind.In) && (Kind <= ParamKind.Ref); public bool IsByRef => (Kind >= ParamKind.In) && (Kind <= ParamKind.Ref);
public bool IsFixed => (Kind == ParamKind.GlobalUnique) || (Source != null);
private ParamInfo( private ParamInfo(ParameterInfo info, ParamKind kind,
ParameterInfo info, int index, ParamKind kind,
Type paramType, Type underlyingType) Type paramType, Type underlyingType)
{ {
Info = info; Info = info;
Index = index;
Kind = kind; Kind = kind;
ParameterType = paramType; ParameterType = paramType;
UnderlyingType = underlyingType; UnderlyingType = underlyingType;
// Reference types have a backing type of nint - they're pointers. // Reference types have a backing type of nint - they're pointers.
FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint); FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint);
// FIXME: Reimplement singletons somehow. if (UnderlyingType.Has<SingletonAttribute>()) Source = underlyingType;
// if (UnderlyingType.Has<EntityAttribute>()) Source = underlyingType;
if (Info.Get<SourceAttribute>() is SourceAttribute attr) Source = attr.Type; if (Info.Get<SourceAttribute>() is SourceAttribute attr) Source = attr.Type;
// TODO: Needs support for the new attributes. // TODO: Needs support for the new attributes.
} }
public static ParamInfo Build(ParameterInfo info, int index) public static ParamInfo Build(ParameterInfo info)
{ {
if (info.IsOptional) throw new ArgumentException("Optional parameters are not supported\nParameter: " + 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.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.IsPointer) throw new ArgumentException("Pointers are not supported\nParameter: " + info);
if (_globalUniqueParameters.ContainsKey(info.ParameterType)) if (_globalUniqueParameters.ContainsKey(info.ParameterType))
return new(info, index, ParamKind.GlobalUnique, info.ParameterType, info.ParameterType); return new(info, ParamKind.GlobalUnique, info.ParameterType, info.ParameterType);
if (_uniqueParameters.ContainsKey(info.ParameterType)) if (_uniqueParameters.ContainsKey(info.ParameterType))
return new(info, index, ParamKind.Unique, info.ParameterType, info.ParameterType); return new(info, ParamKind.Unique, info.ParameterType, info.ParameterType);
var isByRef = info.ParameterType.IsByRef; var isByRef = info.ParameterType.IsByRef;
var isNullable = info.IsNullable(); var isNullable = info.IsNullable();
@ -276,13 +282,13 @@ public unsafe class IterActionGenerator
if (info.Has<NotAttribute>()) { if (info.Has<NotAttribute>()) {
if (isByRef || isNullable) throw new ArgumentException( if (isByRef || isNullable) throw new ArgumentException(
"Parameter with NotAttribute must not be ByRef or nullable\nParameter: " + info); "Parameter with NotAttribute must not be ByRef or nullable\nParameter: " + info);
return new(info, index, ParamKind.Not, info.ParameterType, info.ParameterType); return new(info, ParamKind.Not, info.ParameterType, info.ParameterType);
} }
if (info.Has<HasAttribute>() || info.ParameterType.Has<TagAttribute>()) { if (info.Has<HasAttribute>() || info.ParameterType.Has<TagAttribute>()) {
if (isByRef || isNullable) throw new ArgumentException( if (isByRef || isNullable) throw new ArgumentException(
"Parameter with HasAttribute / TagAttribute must not be ByRef or nullable\nParameter: " + info); "Parameter with HasAttribute / TagAttribute must not be ByRef or nullable\nParameter: " + info);
return new(info, index, ParamKind.Has, info.ParameterType, info.ParameterType); return new(info, ParamKind.Has, info.ParameterType, info.ParameterType);
} }
var kind = ParamKind.Normal; var kind = ParamKind.Normal;
@ -308,15 +314,23 @@ public unsafe class IterActionGenerator
if (underlyingType.IsPrimitive) throw new ArgumentException( if (underlyingType.IsPrimitive) throw new ArgumentException(
"Primitives are not supported\nParameter: " + info); "Primitives are not supported\nParameter: " + info);
return new(info, index, kind, info.ParameterType, underlyingType); return new(info, kind, info.ParameterType, underlyingType);
} }
} }
public enum ParamKind public enum ParamKind
{ {
/// <summary> Parameter is not part of terms, handled uniquely, such as Universe. </summary> /// <summary>
/// Not part of the resulting query's terms.
/// Same value across a single invocation of a callback.
/// For example <see cref="ECS.Universe"/> or <see cref="TimeSpan"/>.
/// </summary>
GlobalUnique, GlobalUnique,
/// <summary> Parameter is unique per matched entity, such as EntityRef. </summary> /// <summary>
/// Not part of the resulting query's terms.
/// Unique value for each iterated entity.
/// For example <see cref="EntityRef"/>.
/// </summary>
Unique, Unique,
/// <summary> Passed by value. </summary> /// <summary> Passed by value. </summary>
Normal, Normal,
@ -332,9 +346,15 @@ public unsafe class IterActionGenerator
/// Automatically applied for types with <see cref="TagAttribute"/>. /// Automatically applied for types with <see cref="TagAttribute"/>.
/// </summary> /// </summary>
Has, Has,
/// <summary> Struct passed as <c>Nullable&lt;T&gt;</c>. </summary> /// <summary> Struct or class passed as <see cref="T?"/>. </summary>
Nullable, Nullable,
/// <summary> /// <summary>
/// Matches any terms in a chain of "or" terms.
/// Applied with <see cref="OrAttribute"/>.
/// Implies <see cref="Nullable"/>.
/// </summary>
Or,
/// <summary>
/// Only checks for absence. /// Only checks for absence.
/// Applied with <see cref="NotAttribute"/>. /// Applied with <see cref="NotAttribute"/>.
/// </summary> /// </summary>

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<LangVersion>preview</LangVersion>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings> <ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>

Loading…
Cancel
Save