From 4a5494bfa3288a53c6534d2cff982b88567669c2 Mon Sep 17 00:00:00 2001 From: copygirl Date: Fri, 11 Nov 2022 01:45:03 +0100 Subject: [PATCH] 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] registers type not owned by the module - [Add] and [Add] 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 --- src/Immersion/Immersion.csproj | 1 + src/Immersion/ObserverTest.cs | 7 +- src/Immersion/Program.cs | 4 +- .../Systems/BasicWorldGenerator.cs | 1 + src/gaemstone.Bloxel/gaemstone.Bloxel.csproj | 1 + .../Components/ResourceComponents.cs | 12 +- .../Systems/FreeCameraController.cs | 6 +- src/gaemstone.Client/Systems/Input.cs | 7 +- src/gaemstone.Client/Systems/MeshManager.cs | 3 + src/gaemstone.Client/Systems/Renderer.cs | 14 +- .../Systems/TextureManager.cs | 7 +- src/gaemstone.Client/Systems/Windowing.cs | 2 +- src/gaemstone.Client/gaemstone.Client.csproj | 1 + src/gaemstone/ECS/Attributes.cs | 107 +++++++ src/gaemstone/ECS/Entity.cs | 18 +- src/gaemstone/ECS/Game.cs | 10 +- src/gaemstone/ECS/Identifier.cs | 3 +- src/gaemstone/ECS/Module.cs | 9 - src/gaemstone/ECS/Observer.cs | 11 +- src/gaemstone/ECS/System.cs | 8 +- src/gaemstone/ECS/TermAttributes.cs | 7 +- src/gaemstone/ECS/Universe+Modules.cs | 68 +++-- src/gaemstone/ECS/Universe.cs | 3 +- src/gaemstone/Flecs/SystemPhase.cs | 40 ++- src/gaemstone/Utility/CStringExtensions.cs | 5 +- .../Utility/IL/IterActionGenerator.cs | 280 ++++++++++-------- src/gaemstone/gaemstone.csproj | 1 + 27 files changed, 399 insertions(+), 237 deletions(-) create mode 100644 src/gaemstone/ECS/Attributes.cs diff --git a/src/Immersion/Immersion.csproj b/src/Immersion/Immersion.csproj index 95950e3..3cf7fed 100644 --- a/src/Immersion/Immersion.csproj +++ b/src/Immersion/Immersion.csproj @@ -2,6 +2,7 @@ Exe + preview net6.0 disable enable diff --git a/src/Immersion/ObserverTest.cs b/src/Immersion/ObserverTest.cs index 8ae1c61..937806a 100644 --- a/src/Immersion/ObserverTest.cs +++ b/src/Immersion/ObserverTest.cs @@ -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] +[DependsOn] public class ObserverTest { - [Observer(typeof(ObserverEvent.OnSet))] - public static void DoObserver(in Chunk chunk, in MeshHandle _) + [Observer(Expression = "[in] Chunk, [none] (Mesh, *)")] + public static void DoObserver(in Chunk chunk) => Console.WriteLine($"Chunk at {chunk.Position} now has a Mesh!"); } diff --git a/src/Immersion/Program.cs b/src/Immersion/Program.cs index 4d94830..1691766 100644 --- a/src/Immersion/Program.cs +++ b/src/Immersion/Program.cs @@ -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(); + universe.Modules.Register(); game.Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window))); game.Set(new GameWindow(window)); diff --git a/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs index 4f83976..6b8c157 100644 --- a/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs +++ b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs @@ -6,6 +6,7 @@ using static gaemstone.Bloxel.Constants; namespace gaemstone.Bloxel.Systems; [Module] +[DependsOn] public class BasicWorldGenerator { private readonly FastNoiseLite _noise; diff --git a/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj b/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj index d017c6b..208c375 100644 --- a/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj +++ b/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj @@ -1,6 +1,7 @@ + preview net6.0 disable enable diff --git a/src/gaemstone.Client/Components/ResourceComponents.cs b/src/gaemstone.Client/Components/ResourceComponents.cs index 0c21ad0..3bbd74d 100644 --- a/src/gaemstone.Client/Components/ResourceComponents.cs +++ b/src/gaemstone.Client/Components/ResourceComponents.cs @@ -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] public struct Texture { } - [Tag, Relation] + [Tag, Relation]//, IsA] public struct Mesh { } } diff --git a/src/gaemstone.Client/Systems/FreeCameraController.cs b/src/gaemstone.Client/Systems/FreeCameraController.cs index 72cbd98..ea2a68d 100644 --- a/src/gaemstone.Client/Systems/FreeCameraController.cs +++ b/src/gaemstone.Client/Systems/FreeCameraController.cs @@ -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] +[DependsOn] +[DependsOn] public class FreeCameraController { [Component] diff --git a/src/gaemstone.Client/Systems/Input.cs b/src/gaemstone.Client/Systems/Input.cs index 9f954be..d2e418c 100644 --- a/src/gaemstone.Client/Systems/Input.cs +++ b/src/gaemstone.Client/Systems/Input.cs @@ -10,7 +10,7 @@ using static gaemstone.Client.Systems.Windowing; namespace gaemstone.Client.Systems; [Module] -[DependsOn(typeof(Windowing))] +[DependsOn] 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] + public static void ProcessInput(TimeSpan delta, + GameWindow window, RawInput input) { window.Handle.DoEvents(); diff --git a/src/gaemstone.Client/Systems/MeshManager.cs b/src/gaemstone.Client/Systems/MeshManager.cs index 9f845ae..139183e 100644 --- a/src/gaemstone.Client/Systems/MeshManager.cs +++ b/src/gaemstone.Client/Systems/MeshManager.cs @@ -11,6 +11,9 @@ using ModelRoot = SharpGLTF.Schema2.ModelRoot; namespace gaemstone.Client.Systems; [Module] +[DependsOn] +[DependsOn] +[DependsOn] public class MeshManager { private const uint PositionAttribIndex = 0; diff --git a/src/gaemstone.Client/Systems/Renderer.cs b/src/gaemstone.Client/Systems/Renderer.cs index 3ccbbb8..8d22363 100644 --- a/src/gaemstone.Client/Systems/Renderer.cs +++ b/src/gaemstone.Client/Systems/Renderer.cs @@ -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] +[DependsOn] +[DependsOn] +[DependsOn] public class Renderer : IModuleInitializer { @@ -54,7 +54,7 @@ public class Renderer _modelMatrixUniform = GL.GetUniformLocation(_program, "modelMatrix"); } - [System(typeof(SystemPhase.PreStore))] + [System] 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] 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] public static void SwapBuffers(GameWindow window) => window.Handle.SwapBuffers(); diff --git a/src/gaemstone.Client/Systems/TextureManager.cs b/src/gaemstone.Client/Systems/TextureManager.cs index 8a94742..c2b1bff 100644 --- a/src/gaemstone.Client/Systems/TextureManager.cs +++ b/src/gaemstone.Client/Systems/TextureManager.cs @@ -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] +[DependsOn] +[DependsOn] public class TextureManager : IModuleInitializer { diff --git a/src/gaemstone.Client/Systems/Windowing.cs b/src/gaemstone.Client/Systems/Windowing.cs index 6205ba5..03793ea 100644 --- a/src/gaemstone.Client/Systems/Windowing.cs +++ b/src/gaemstone.Client/Systems/Windowing.cs @@ -26,7 +26,7 @@ public class Windowing public GameWindow(IWindow handle) => Handle = handle; } - [System(typeof(SystemPhase.PreFrame))] + [System] public static void ProcessWindow(GameWindow window, Canvas canvas) => canvas.Size = window.Handle.Size; } diff --git a/src/gaemstone.Client/gaemstone.Client.csproj b/src/gaemstone.Client/gaemstone.Client.csproj index 3e4b245..1403d60 100644 --- a/src/gaemstone.Client/gaemstone.Client.csproj +++ b/src/gaemstone.Client/gaemstone.Client.csproj @@ -1,6 +1,7 @@ + preview net6.0 disable enable diff --git a/src/gaemstone/ECS/Attributes.cs b/src/gaemstone/ECS/Attributes.cs new file mode 100644 index 0000000..a2114d0 --- /dev/null +++ b/src/gaemstone/ECS/Attributes.cs @@ -0,0 +1,107 @@ +using System; +using static gaemstone.Flecs.Core; + +namespace gaemstone.ECS; + +/// +/// When present on an attribute attached to a type that's part of a module +/// being registered automatically through , +/// an entity is automatically created and +/// called on it, meaning it can be looked up using . +/// +public interface ICreateEntityAttribute { } + +/// +/// 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. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class PrivateAttribute : Attribute { } + +/// +/// 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. +/// +public class ProxyAttribute : ProxyAttribute + { public ProxyAttribute() : base(typeof(T)) { } } + + +/// +/// Marked entity automatically has the specified entity added to it when +/// automatically registered. Equivalent to . +/// +public class AddAttribute : AddEntityAttribute + { public AddAttribute() : base(typeof(TEntity)) { } } + +/// +/// Marked entity automatically has the specified relationship pair added to it when +/// automatically registered, Equivalent to . +/// +public class AddAttribute : AddRelationAttribute + { public AddAttribute() : base(typeof(TRelation), typeof(TTarget)) { } } + +/// +[AttributeUsage(AttributeTargets.Struct)] +public class TagAttribute : AddAttribute, ICreateEntityAttribute { } + +/// +/// Marked entity represents a relationship type, meaning it may be used as +/// the "relation" in a pair. However, this attribute is purely informational. +/// +/// +/// The relationship may have component data associated with +/// it when added to an entity under these circumstances: +/// +/// If marked as a , does not carry data. +/// If marked as a , carries the relation's data. +/// If marked with neither, will carry the target's data, if it's a component. +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class RelationAttribute : Attribute, ICreateEntityAttribute { } + + +/// +public class IsAAttribute : AddAttribute { } + +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class ChildOfAttribute : AddAttribute { } + +/// +public class DependsOnAttribute : AddAttribute { } + + +/// +public class ExclusiveAttribute : AddAttribute { } + +/// +public class WithAttribute : AddAttribute { } + + +// 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; } +} diff --git a/src/gaemstone/ECS/Entity.cs b/src/gaemstone/ECS/Entity.cs index c2d39c8..c602785 100644 --- a/src/gaemstone/ECS/Entity.cs +++ b/src/gaemstone/ECS/Entity.cs @@ -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 { } - -/// Unused, purely informational. -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -public class RelationAttribute : Attribute { } +public class EntityAttribute : Attribute, ICreateEntityAttribute + { public string? Name { get; init; } } + +/// +/// 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 with itself as the generic type parameter. +/// +public class SingletonAttribute : EntityAttribute { } public readonly struct Entity : IEquatable diff --git a/src/gaemstone/ECS/Game.cs b/src/gaemstone/ECS/Game.cs index e3cfd3b..608458b 100644 --- a/src/gaemstone/ECS/Game.cs +++ b/src/gaemstone/ECS/Game.cs @@ -6,13 +6,9 @@ namespace gaemstone.ECS; /// Entity for storing global game state and configuration. /// Parameters can use to source this entity. /// -[Entity, Tag] +[Entity] public struct Game { } -/// Short for [Source(typeof(Game))]. +/// Equivalent to . [AttributeUsage(AttributeTargets.Parameter)] -public class GameAttribute : SourceAttribute -{ - public GameAttribute() - : base(typeof(Game)) { } -} +public class GameAttribute : SourceAttribute { } diff --git a/src/gaemstone/ECS/Identifier.cs b/src/gaemstone/ECS/Identifier.cs index 70e5aa1..fc7e8dc 100644 --- a/src/gaemstone/ECS/Identifier.cs +++ b/src/gaemstone/ECS/Identifier.cs @@ -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; diff --git a/src/gaemstone/ECS/Module.cs b/src/gaemstone/ECS/Module.cs index 1215175..c2aabf4 100644 --- a/src/gaemstone/ECS/Module.cs +++ b/src/gaemstone/ECS/Module.cs @@ -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); diff --git a/src/gaemstone/ECS/Observer.cs b/src/gaemstone/ECS/Observer.cs index 3bf94b0..5d57d66 100644 --- a/src/gaemstone/ECS/Observer.cs +++ b/src/gaemstone/ECS/Observer.cs @@ -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 : ObserverAttribute + { public ObserverAttribute() : base(typeof(TEvent)) { } } public static class ObserverExtensions { @@ -49,12 +50,12 @@ public static class ObserverExtensions typeof(Action), 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); } } diff --git a/src/gaemstone/ECS/System.cs b/src/gaemstone/ECS/System.cs index bd888ec..1166e6c 100644 --- a/src/gaemstone/ECS/System.cs +++ b/src/gaemstone/ECS/System.cs @@ -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 : SystemAttribute + { public SystemAttribute() : base(typeof(TPhase)) { } } public static class SystemExtensions { @@ -70,12 +72,12 @@ public static class SystemExtensions iterAction = (Action)Delegate.CreateDelegate(typeof(Action), 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); } diff --git a/src/gaemstone/ECS/TermAttributes.cs b/src/gaemstone/ECS/TermAttributes.cs index 56f9ffc..dc361a2 100644 --- a/src/gaemstone/ECS/TermAttributes.cs +++ b/src/gaemstone/ECS/TermAttributes.cs @@ -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 : 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 { } diff --git a/src/gaemstone/ECS/Universe+Modules.cs b/src/gaemstone/ECS/Universe+Modules.cs index 70b6f42..3186ffa 100644 --- a/src/gaemstone/ECS/Universe+Modules.cs +++ b/src/gaemstone/ECS/Universe+Modules.cs @@ -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() is EntityAttribute nestedAttr) - Universe.LookupOrThrow(entity, nestedAttr.Name ?? nested.Name) - .CreateLookup(nested); + foreach (var nested in type.GetNestedTypes()) { + if (!nested.GetCustomAttributes(true).OfType().Any()) continue; + var name = nested.Get()?.Name ?? nested.Name; + Universe.LookupOrThrow(entity, name).CreateLookup(nested); + } return entity; @@ -141,9 +142,10 @@ internal class ModuleInfo var module = Universe.New(path).Add(); - // Add module dependencies from [DependsOn] attributes. - foreach (var dependsAttr in Type.GetMultiple()) { - var dependsPath = ModuleManager.GetModulePath(dependsAttr.Type); + // Add module dependencies from [DependsOn<>] attributes. + foreach (var dependsAttr in Type.GetMultiple().Where(attr => + attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) { + var dependsPath = ModuleManager.GetModulePath(dependsAttr.Target); var dependency = Universe.Lookup(dependsPath) ?? Universe.New(dependsPath).Add().Disable().Build(); @@ -169,35 +171,39 @@ internal class ModuleInfo private void RegisterNestedTypes() { foreach (var type in Type.GetNestedTypes()) { - if (type.Get() is not EntityAttribute attr) continue; + if (!type.GetCustomAttributes(true).OfType().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()?.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() && (!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()?.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().Build().CreateLookup(type); - break; + var builder = Entity.NewChild(name); + if (!type.Has()) builder.Symbol(name); - case ComponentAttribute: - entity.Build().CreateComponent(type); - break; + foreach (var attr in type.GetMultiple()) + builder.Add(Universe.LookupOrThrow(attr.Entity)); + foreach (var attr in type.GetMultiple()) + 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()) + entity.Add(entity); + + if (type.Has()) + entity.CreateComponent(proxyType); + else + entity.CreateLookup(proxyType); } } diff --git a/src/gaemstone/ECS/Universe.cs b/src/gaemstone/ECS/Universe.cs index afdcaba..eb647af 100644 --- a/src/gaemstone/ECS/Universe.cs +++ b/src/gaemstone/ECS/Universe.cs @@ -26,8 +26,7 @@ public unsafe partial class Universe ChildOf = LookupOrThrow(); - // Create "Game" singleton entity, which - // stores global state, configuration, ... + // Create "Game" entity to store global state. New("Game").Symbol("Game").Build() .CreateLookup().Add(); } diff --git a/src/gaemstone/Flecs/SystemPhase.cs b/src/gaemstone/Flecs/SystemPhase.cs index 808e1f0..1f2cd85 100644 --- a/src/gaemstone/Flecs/SystemPhase.cs +++ b/src/gaemstone/Flecs/SystemPhase.cs @@ -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] + public struct PreFrame { } /// /// This phase contains all the systems that load data into your ECS. /// This would be a good place to load keyboard and mouse inputs. /// - [Entity] public struct OnLoad { } + [Entity, Add] + [DependsOn] + public struct OnLoad { } /// /// 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. /// - [Entity] public struct PostLoad { } + [Entity, Add] + [DependsOn] + public struct PostLoad { } /// /// 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. /// - [Entity] public struct PreUpdate { } + [Entity, Add] + [DependsOn] + public struct PreUpdate { } /// /// 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. /// - [Entity] public struct OnUpdate { } + [Entity, Add] + [DependsOn] + public struct OnUpdate { } /// /// 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. /// - [Entity] public struct OnValidate { } + [Entity, Add] + [DependsOn] + public struct OnValidate { } /// /// 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 OnValidate phase, /// you may want to move the entities so that they no longer overlap. /// - [Entity] public struct PostUpdate { } + [Entity, Add] + [DependsOn] + public struct PostUpdate { } /// /// 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. /// - [Entity] public struct PreStore { } + [Entity, Add] + [DependsOn] + public struct PreStore { } /// /// 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. /// - [Entity] public struct OnStore { } + [Entity, Add] + [DependsOn] + public struct OnStore { } - [Entity] public struct PostFrame { } + [Entity, Add] + [DependsOn] + public struct PostFrame { } } diff --git a/src/gaemstone/Utility/CStringExtensions.cs b/src/gaemstone/Utility/CStringExtensions.cs index 46577ab..0cc756b 100644 --- a/src/gaemstone/Utility/CStringExtensions.cs +++ b/src/gaemstone/Utility/CStringExtensions.cs @@ -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(pointer, length).ToArray(); } diff --git a/src/gaemstone/Utility/IL/IterActionGenerator.cs b/src/gaemstone/Utility/IL/IterActionGenerator.cs index d68c324..4af588b 100644 --- a/src/gaemstone/Utility/IL/IterActionGenerator.cs +++ b/src/gaemstone/Utility/IL/IterActionGenerator.cs @@ -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 Parameters { get; } public IReadOnlyList Terms { get; } public Action 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(0); var iteratorArg = IL.Argument(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(); - GeneratedAction = genMethod.CreateDelegate>(); - ReadableString = IL.ToReadableString(); - - return; - } - - var terms = new List(); - 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 local and initialize it to iterator.Field(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() : 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? 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>(); 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()) Source = underlyingType; + if (UnderlyingType.Has()) Source = underlyingType; if (Info.Get() 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()) { 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() || info.ParameterType.Has()) { 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 { - /// Parameter is not part of terms, handled uniquely, such as Universe. + /// + /// Not part of the resulting query's terms. + /// Same value across a single invocation of a callback. + /// For example or . + /// GlobalUnique, - /// Parameter is unique per matched entity, such as EntityRef. + /// + /// Not part of the resulting query's terms. + /// Unique value for each iterated entity. + /// For example . + /// Unique, /// Passed by value. Normal, @@ -332,9 +346,15 @@ public unsafe class IterActionGenerator /// Automatically applied for types with . /// Has, - /// Struct passed as Nullable<T>. + /// Struct or class passed as . Nullable, /// + /// Matches any terms in a chain of "or" terms. + /// Applied with . + /// Implies . + /// + Or, + /// /// Only checks for absence. /// Applied with . /// diff --git a/src/gaemstone/gaemstone.csproj b/src/gaemstone/gaemstone.csproj index 92bd2a3..af362dc 100644 --- a/src/gaemstone/gaemstone.csproj +++ b/src/gaemstone/gaemstone.csproj @@ -1,6 +1,7 @@ + preview net6.0 disable enable