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. 11
      src/gaemstone/ECS/Observer.cs
  20. 8
      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. 280
      src/gaemstone/Utility/IL/IterActionGenerator.cs
  27. 1
      src/gaemstone/gaemstone.csproj

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

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

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

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

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

@ -5,18 +5,20 @@ namespace gaemstone.Client.Components;
[Module]
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]
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,
// 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 { }
[Tag, Relation]
[Tag, Relation]//, IsA<Resource>]
public struct Mesh { }
}

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

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

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

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

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

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

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<LangVersion>preview</LangVersion>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<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;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class EntityAttribute : Attribute
{ public string? Name { get; set; } }
[AttributeUsage(AttributeTargets.Struct)]
public class TagAttribute : EntityAttribute { }
/// <summary> Unused, purely informational. </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class RelationAttribute : Attribute { }
public class EntityAttribute : Attribute, ICreateEntityAttribute
{ public string? Name { get; init; } }
/// <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>
public class SingletonAttribute : EntityAttribute { }
public readonly struct Entity
: IEquatable<Entity>

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

@ -11,9 +11,10 @@ public readonly struct Identifier
public bool IsPair => ecs_id_is_pair(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 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;

@ -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
{
void Initialize(EntityRef module);

@ -11,10 +11,11 @@ namespace gaemstone.ECS;
public class ObserverAttribute : Attribute
{
public Type Event { get; }
public string? Expression { get; set; }
public ObserverAttribute(Type @event) => Event = @event;
public string? Expression { get; init; }
internal ObserverAttribute(Type @event) => Event = @event; // Use generic type instead.
}
public class ObserverAttribute<TEvent> : ObserverAttribute
{ public ObserverAttribute() : base(typeof(TEvent)) { } }
public static class ObserverExtensions
{
@ -49,12 +50,12 @@ public static class ObserverExtensions
typeof(Action<Iterator>), instance, method);
} else {
var gen = IterActionGenerator.GetOrBuild(universe, method);
filter = (attr.Expression != null) ? new(attr.Expression) : new(gen.Terms.ToArray());
filter = (attr.Expression != null) ? new(attr.Expression) : new(gen.Terms.ToArray());
iterAction = iter => gen.RunWithTryCatch(instance, iter);
}
filter.Name = method.Name;
var @event = universe.LookupOrThrow(attr.Event);
var @event = universe.LookupOrThrow(attr.Event);
return universe.RegisterObserver(filter, @event, iterAction);
}
}

@ -17,8 +17,10 @@ public class SystemAttribute : Attribute
public string? Expression { get; set; }
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
{
@ -70,12 +72,12 @@ public static class SystemExtensions
iterAction = (Action<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), instance, method);
} else {
var gen = IterActionGenerator.GetOrBuild(universe, method);
query = (attr?.Expression != null) ? new(attr.Expression) : new(gen.Terms.ToArray());
query = (attr?.Expression != null) ? new(attr.Expression) : new(gen.Terms.ToArray());
iterAction = iter => gen.RunWithTryCatch(instance, iter);
}
query.Name = method.Name;
var phase = universe.LookupOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate));
var phase = universe.LookupOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate));
return universe.RegisterSystem(query, phase, iterAction);
}

@ -3,13 +3,15 @@ using System;
namespace gaemstone.ECS;
// TODO: Make it possible to use [Source] on systems.
[AttributeUsage(AttributeTargets.Parameter)]
public class SourceAttribute : Attribute
{
public Type Type { get; }
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].
[AttributeUsage(AttributeTargets.Parameter)]
@ -23,6 +25,9 @@ public class InAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Parameter)]
public class OutAttribute : Attribute { }
// [AttributeUsage(AttributeTargets.Parameter)]
// public class OrAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Parameter)]
public class NotAttribute : Attribute { }

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

@ -1,24 +1,30 @@
using gaemstone.ECS;
using static gaemstone.Flecs.Pipeline;
namespace gaemstone.Flecs;
[Module("flecs", "pipeline")]
public static class SystemPhase
{
[Entity] public struct PreFrame { }
[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] public struct OnLoad { }
[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] public struct PostLoad { }
[Entity, Add<Phase>]
[DependsOn<OnLoad>]
public struct PostLoad { }
/// <summary>
/// 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
/// things from the previous frame, etcetera.
/// </summary>
[Entity] public struct PreUpdate { }
[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] public struct OnUpdate { }
[Entity, Add<Phase>]
[DependsOn<PreUpdate>]
public struct OnUpdate { }
/// <summary>
/// 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
/// in this phase would be collision detection.
/// </summary>
[Entity] public struct OnValidate { }
[Entity, Add<Phase>]
[DependsOn<OnUpdate>]
public struct OnValidate { }
/// <summary>
/// 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,
/// you may want to move the entities so that they no longer overlap.
/// </summary>
[Entity] public struct PostUpdate { }
[Entity, Add<Phase>]
[DependsOn<OnValidate>]
public struct PostUpdate { }
/// <summary>
/// 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 scene graph.
/// </summary>
[Entity] public struct PreStore { }
[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] 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)
{
if (str.IsNull) return null;
var length = 0;
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();
}

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

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

Loading…
Cancel
Save