From c859bd37a1d1218df87dff38c8e1d5dfbe3e6a9b Mon Sep 17 00:00:00 2001 From: copygirl Date: Tue, 11 Oct 2022 18:31:57 +0200 Subject: [PATCH] Additional refactors --- src/Immersion/Program.cs | 48 +++-- src/gaemstone.Bloxel/BlockFacing.cs | 2 +- src/gaemstone.Bloxel/ChunkPaletteStorage.cs | 55 +++-- .../Client/Systems/ChunkMeshGenerator.cs | 13 +- src/gaemstone.Bloxel/Neighbor.cs | 2 +- .../Systems/BasicWorldGenerator.cs | 2 +- src/gaemstone.Bloxel/Utility/ZOrder.cs | 2 +- src/gaemstone.Client/MeshManager.cs | 14 +- src/gaemstone.Client/Systems/Renderer.cs | 14 +- src/gaemstone.Client/TextureManager.cs | 4 +- src/gaemstone/ECS/BuiltIn+ObserverEvent.cs | 22 ++ ...{SystemPhase.cs => BuiltIn+SystemPhase.cs} | 22 +- src/gaemstone/ECS/BuiltIn.cs | 30 +++ src/gaemstone/ECS/Component.cs | 22 +- src/gaemstone/ECS/ComponentHooks.cs | 90 +++++++++ src/gaemstone/ECS/Entity+AddRemove.cs | 43 ---- src/gaemstone/ECS/Entity+GetSet.cs | 86 -------- src/gaemstone/ECS/Entity.cs | 107 ++-------- src/gaemstone/ECS/EntityBase.cs | 44 ++++ src/gaemstone/ECS/EntityBuilder.cs | 51 +++-- src/gaemstone/ECS/EntityRef.cs | 105 ++++++++++ src/gaemstone/ECS/EntityType.cs | 24 +++ src/gaemstone/ECS/Filter.cs | 4 +- src/gaemstone/ECS/Flecs.cs | 41 ---- src/gaemstone/ECS/Game.cs | 5 +- src/gaemstone/ECS/Identifier.cs | 46 +---- src/gaemstone/ECS/IdentifierRef.cs | 41 ++++ src/gaemstone/ECS/Iterator.cs | 14 +- src/gaemstone/ECS/Module.cs | 130 +----------- src/gaemstone/ECS/Observer.cs | 34 +--- src/gaemstone/ECS/Registerable.cs | 56 ------ src/gaemstone/ECS/Relation.cs | 14 -- src/gaemstone/ECS/System.cs | 25 ++- src/gaemstone/ECS/Tag.cs | 23 --- src/gaemstone/ECS/Term.cs | 38 +--- src/gaemstone/ECS/TermAttributes.cs | 31 +++ src/gaemstone/ECS/Universe+Lookup.cs | 98 ++++----- src/gaemstone/ECS/Universe+Modules.cs | 188 ++++++++++++++++++ src/gaemstone/ECS/Universe.cs | 39 +++- src/gaemstone/Utility/CStringExtensions.cs | 23 +++ .../Utility/CallbackContextHelper.cs | 41 +--- .../Utility/IL/ILGeneratorWrapper.cs | 9 + .../Utility/IL/IterActionGenerator.cs | 14 +- src/gaemstone/Utility/SpanToRef.cs | 19 ++ src/gaemstone/Utility/TypeWrapper.cs | 23 +-- 45 files changed, 934 insertions(+), 824 deletions(-) create mode 100644 src/gaemstone/ECS/BuiltIn+ObserverEvent.cs rename src/gaemstone/ECS/{SystemPhase.cs => BuiltIn+SystemPhase.cs} (79%) create mode 100644 src/gaemstone/ECS/BuiltIn.cs create mode 100644 src/gaemstone/ECS/ComponentHooks.cs delete mode 100644 src/gaemstone/ECS/Entity+AddRemove.cs delete mode 100644 src/gaemstone/ECS/Entity+GetSet.cs create mode 100644 src/gaemstone/ECS/EntityBase.cs create mode 100644 src/gaemstone/ECS/EntityRef.cs create mode 100644 src/gaemstone/ECS/EntityType.cs delete mode 100644 src/gaemstone/ECS/Flecs.cs create mode 100644 src/gaemstone/ECS/IdentifierRef.cs delete mode 100644 src/gaemstone/ECS/Registerable.cs delete mode 100644 src/gaemstone/ECS/Relation.cs delete mode 100644 src/gaemstone/ECS/Tag.cs create mode 100644 src/gaemstone/ECS/TermAttributes.cs create mode 100644 src/gaemstone/ECS/Universe+Modules.cs create mode 100644 src/gaemstone/Utility/SpanToRef.cs diff --git a/src/Immersion/Program.cs b/src/Immersion/Program.cs index 63a928c..019a4b9 100644 --- a/src/Immersion/Program.cs +++ b/src/Immersion/Program.cs @@ -20,7 +20,9 @@ FlecsAbortException.SetupHook(); Resources.ResourceAssembly = typeof(Program).Assembly; var universe = new Universe(); -var game = universe.Lookup(); +var game = universe.LookupOrThrow(); +universe.EnableRest(); +// universe.EnableMonitor(); var window = Window.Create(WindowOptions.Default with { Title = "gæmstone", @@ -30,26 +32,27 @@ var window = Window.Create(WindowOptions.Default with { window.Initialize(); window.Center(); -universe.RegisterModule(); +universe.Modules.Register(); game.Set(new Canvas(window.CreateOpenGL())); game.Set(new GameWindow(window)); -universe.RegisterModule(); +universe.Modules.Register(); -universe.RegisterModule(); -universe.RegisterModule(); +universe.Modules.Register(); +universe.Modules.Register(); TextureManager.Initialize(universe); -universe.RegisterModule(); +universe.Modules.Register(); game.Set(new RawInput()); -universe.RegisterModule(); -universe.RegisterModule(); -universe.Create("MainCamera") +universe.Modules.Register(); +universe.Modules.Register(); +universe.New("MainCamera") .Set(Camera.Default3D) .Set((GlobalTransform) Matrix4X4.CreateTranslation(0.0F, 2.0F, 0.0F)) - .Set(new CameraController { MouseSensitivity = 12.0F }); + .Set(new CameraController { MouseSensitivity = 12.0F }) + .Build(); var heartMesh = MeshManager.Load(universe, "heart.glb"); var swordMesh = MeshManager.Load(universe, "sword.glb"); @@ -59,20 +62,22 @@ for (var x = -12; x <= 12; x++) for (var z = -12; z <= 12; z++) { var position = Matrix4X4.CreateTranslation(x * 2, 0.0F, z * 2); var rotation = Matrix4X4.CreateRotationY(rnd.NextFloat(MathF.PI * 2)); - universe.Create() + var (type, mesh) = rnd.Pick(("Heart", heartMesh), ("Sword", swordMesh)); + universe.New($"{type} {x}:{z}") .Set((GlobalTransform)(rotation * position)) - .Set(rnd.Pick(heartMesh, swordMesh)); + .Set(mesh) + .Build(); } -universe.RegisterModule(); -universe.RegisterModule(); -universe.RegisterModule(); +universe.Modules.Register(); +universe.Modules.Register(); +universe.Modules.Register(); var texture = TextureManager.Load(universe, "terrain.png"); -var stone = universe.Create("Stone").Set(TextureCoords4.FromGrid(4, 4, 1, 0)); -var dirt = universe.Create("Dirt" ).Set(TextureCoords4.FromGrid(4, 4, 2, 0)); -var grass = universe.Create("Grass").Set(TextureCoords4.FromGrid(4, 4, 3, 0)); +var stone = universe.New("Stone").Set(TextureCoords4.FromGrid(4, 4, 1, 0)).Build(); +var dirt = universe.New("Dirt" ).Set(TextureCoords4.FromGrid(4, 4, 2, 0)).Build(); +var grass = universe.New("Grass").Set(TextureCoords4.FromGrid(4, 4, 3, 0)).Build(); var sizeH = 4; var sizeY = 2; @@ -81,14 +86,15 @@ for (var cy = -sizeY; cy < sizeY; cy++) for (var cz = -sizeH; cz < sizeH; cz++) { var pos = new ChunkPos(cx, cy - 2, cz); var storage = new ChunkStoreBlocks(); - universe.Create() + universe.New($"Chunk {cx}:{cy}:{cz}") .Set((GlobalTransform)Matrix4X4.CreateTranslation(pos.GetOrigin())) .Set(new Chunk(pos)) .Set(storage) - .Set(texture); + .Set(texture) + .Build(); } -// universe.RegisterModule(); +// universe.Modules.Register(); var stopwatch = Stopwatch.StartNew(); var minFrameTime = TimeSpan.FromSeconds(1) / 30; diff --git a/src/gaemstone.Bloxel/BlockFacing.cs b/src/gaemstone.Bloxel/BlockFacing.cs index 6820400..3982b1b 100644 --- a/src/gaemstone.Bloxel/BlockFacing.cs +++ b/src/gaemstone.Bloxel/BlockFacing.cs @@ -18,7 +18,7 @@ public static class BlockFacings { public static readonly ImmutableHashSet Horizontals = ImmutableHashSet.Create(BlockFacing.East , BlockFacing.West , - BlockFacing.South, BlockFacing.North); + BlockFacing.South, BlockFacing.North); public static readonly ImmutableHashSet Verticals = ImmutableHashSet.Create(BlockFacing.Up, BlockFacing.Down); diff --git a/src/gaemstone.Bloxel/ChunkPaletteStorage.cs b/src/gaemstone.Bloxel/ChunkPaletteStorage.cs index f11ab3e..5e29d98 100644 --- a/src/gaemstone.Bloxel/ChunkPaletteStorage.cs +++ b/src/gaemstone.Bloxel/ChunkPaletteStorage.cs @@ -9,14 +9,14 @@ namespace gaemstone.Bloxel; // https://www.reddit.com/r/VoxelGameDev/comments/9yu8qy/palettebased_compression_for_chunked_discrete/ public class ChunkPaletteStorage { - const int Size = 16 * 16 * 16; - static readonly EqualityComparer COMPARER + private const int Size = Constants.ChunkLength * Constants.ChunkLength * Constants.ChunkLength; + private static readonly EqualityComparer COMPARER = EqualityComparer.Default; - BitArray? _data; - PaletteEntry[]? _palette; - int _usedPalettes; - int _indicesLength; + private BitArray? _data; + private PaletteEntry[]? _palette; + private int _usedPalettes; + private int _indicesLength; public T Default { get; } @@ -37,21 +37,18 @@ public class ChunkPaletteStorage => Default = @default; - T Get(int x, int y, int z) + private T Get(int x, int y, int z) { if (_palette == null) return Default; var entry = _palette[GetPaletteIndex(x, y, z)]; return !COMPARER.Equals(entry.Value, default!) ? entry.Value : Default; } - void Set(int x, int y, int z, T value) + private void Set(int x, int y, int z, T value) { - if (_palette == null) - { + if (_palette == null) { if (COMPARER.Equals(value, Default)) return; - } - else - { + } else { var index = GetIndex(x, y, z); ref var current = ref _palette[GetPaletteIndex(index)]; if (COMPARER.Equals(value, current.Value)) return; @@ -60,15 +57,13 @@ public class ChunkPaletteStorage _usedPalettes--; var replace = Array.FindIndex(_palette, entry => COMPARER.Equals(value, entry.Value)); - if (replace != -1) - { + if (replace != -1) { SetPaletteIndex(index, replace); _palette[replace].RefCount += 1; return; } - if (current.RefCount == 0) - { + if (current.RefCount == 0) { current.Value = value; current.RefCount = 1; _usedPalettes++; @@ -82,10 +77,9 @@ public class ChunkPaletteStorage _usedPalettes++; } - int NewPaletteEntry() + private int NewPaletteEntry() { - if (_palette != null) - { + if (_palette != null) { int firstFree = Array.FindIndex(_palette, entry => entry.Value == null || entry.RefCount == 0); if (firstFree != -1) return firstFree; @@ -95,10 +89,9 @@ public class ChunkPaletteStorage return NewPaletteEntry(); } - void GrowPalette() + private void GrowPalette() { - if (_palette == null) - { + if (_palette == null) { _data = new(Size); _palette = new PaletteEntry[2]; _usedPalettes = 1; @@ -156,9 +149,9 @@ public class ChunkPaletteStorage // } - int GetPaletteIndex(int x, int y, int z) + private int GetPaletteIndex(int x, int y, int z) => GetPaletteIndex(GetIndex(x, y, z)); - int GetPaletteIndex(int index) + private int GetPaletteIndex(int index) { var paletteIndex = 0; for (var i = 0; i < _indicesLength; i++) @@ -166,19 +159,21 @@ public class ChunkPaletteStorage return paletteIndex; } - void SetPaletteIndex(int x, int y, int z, int paletteIndex) + private void SetPaletteIndex(int x, int y, int z, int paletteIndex) => SetPaletteIndex(GetIndex(x, y, z), paletteIndex); - void SetPaletteIndex(int index, int paletteIndex) + private void SetPaletteIndex(int index, int paletteIndex) { for (var i = 0; i < _indicesLength; i++) _data!.Set(index + i, (paletteIndex >> i & 0b1) == 0b1); } - int GetIndex(int x, int y, int z) - => (x | y << 4 | z << 8) * _indicesLength; + private int GetIndex(int x, int y, int z) + => (x | + y << Constants.ChunkBitShift | + z << (Constants.ChunkBitShift * 2)) * _indicesLength; - struct PaletteEntry + private struct PaletteEntry { public T Value { get; set; } public int RefCount { get; set; } diff --git a/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs index 811703d..46d9547 100644 --- a/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs +++ b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs @@ -60,9 +60,8 @@ public class ChunkMeshGenerator for (var x = 0; x < 16; x++) for (var y = 0; y < 16; y++) for (var z = 0; z < 16; z++) { - var blockEntity = new Entity(centerBlocks[x, y, z]); - if (!blockEntity.IsValid) continue; - var block = new EntityRef(universe, blockEntity); + var block = universe.Lookup(centerBlocks[x, y, z]); + if (block == null) continue; var blockVertex = new Vector3D(x, y, z); var textureCell = block.Get(); @@ -118,9 +117,9 @@ public class ChunkMeshGenerator case BlockFacing.South : z += 1; if (z >= 16) cz += 1; break; case BlockFacing.North : z -= 1; if (z < 0) cz -= 1; break; } - var neighborChunk = blocks[cx, cy, cz]; - if (neighborChunk == null) return true; - var neighborBlock = neighborChunk[x & 0b1111, y & 0b1111, z & 0b1111]; - return !neighborBlock.IsValid; + var neighborChunk = blocks[cx, cy, cz]; + if (neighborChunk == null) return true; + var neighborBlock = neighborChunk[x & 0b1111, y & 0b1111, z & 0b1111]; + return neighborBlock.IsNone; } } diff --git a/src/gaemstone.Bloxel/Neighbor.cs b/src/gaemstone.Bloxel/Neighbor.cs index 6e7643b..c16b084 100644 --- a/src/gaemstone.Bloxel/Neighbor.cs +++ b/src/gaemstone.Bloxel/Neighbor.cs @@ -51,7 +51,7 @@ public static class Neighbors { public static readonly ImmutableHashSet Horizontals = ImmutableHashSet.Create(Neighbor.East , Neighbor.West , - Neighbor.South, Neighbor.North); + Neighbor.South, Neighbor.North); public static readonly ImmutableHashSet Verticals = ImmutableHashSet.Create(Neighbor.Up, Neighbor.Down); diff --git a/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs index 34dd1b4..4f83976 100644 --- a/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs +++ b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs @@ -27,7 +27,7 @@ public class BasicWorldGenerator in Chunk chunk, ChunkStoreBlocks blocks, [Not] HasBasicWorldGeneration _) { - var stone = universe.Lookup("Stone"); + var stone = universe.LookupOrThrow("Stone"); for (var lx = 0; lx < ChunkLength; lx++) for (var ly = 0; ly < ChunkLength; ly++) for (var lz = 0; lz < ChunkLength; lz++) { diff --git a/src/gaemstone.Bloxel/Utility/ZOrder.cs b/src/gaemstone.Bloxel/Utility/ZOrder.cs index 2047a9b..5c49622 100644 --- a/src/gaemstone.Bloxel/Utility/ZOrder.cs +++ b/src/gaemstone.Bloxel/Utility/ZOrder.cs @@ -39,7 +39,7 @@ public readonly struct ZOrder 0b_00010010_01001001_00100100_10010010_01001001_00100100_10010010_01001001, // 0x1249249249249249 }; - private static readonly long X_MASK = (long)MASKS[MASKS.Length - 1]; + private static readonly long X_MASK = (long)MASKS[^1]; private static readonly long Y_MASK = X_MASK << 1; private static readonly long Z_MASK = X_MASK << 2; private static readonly long XY_MASK = X_MASK | Y_MASK; diff --git a/src/gaemstone.Client/MeshManager.cs b/src/gaemstone.Client/MeshManager.cs index fe28719..1a7651b 100644 --- a/src/gaemstone.Client/MeshManager.cs +++ b/src/gaemstone.Client/MeshManager.cs @@ -10,9 +10,9 @@ namespace gaemstone.Client; public static class MeshManager { - const uint PositionAttribIndex = 0; - const uint NormalAttribIndex = 1; - const uint UvAttribIndex = 2; + private const uint PositionAttribIndex = 0; + private const uint NormalAttribIndex = 1; + private const uint UvAttribIndex = 2; public static Mesh Load(Universe universe, string name) { @@ -25,12 +25,12 @@ public static class MeshManager var vertices = primitive.VertexAccessors["POSITION"]; var normals = primitive.VertexAccessors["NORMAL"]; - var GL = universe.Lookup().Get().GL; + var GL = universe.LookupOrThrow().Get().GL; var vao = GL.GenVertexArray(); GL.BindVertexArray(vao); GL.CreateBufferFromData(indices.SourceBufferView.Content, - BufferTargetARB.ElementArrayBuffer); + BufferTargetARB.ElementArrayBuffer); GL.CreateBufferFromData(vertices.SourceBufferView.Content); GL.EnableVertexAttribArray(PositionAttribIndex); @@ -52,7 +52,7 @@ public static class MeshManager ReadOnlySpan indices, ReadOnlySpan> vertices, ReadOnlySpan> normals, ReadOnlySpan> uvs) { - var GL = universe.Lookup().Get().GL; + var GL = universe.LookupOrThrow().Get().GL; var vao = GL.GenVertexArray(); GL.BindVertexArray(vao); @@ -80,7 +80,7 @@ public static class MeshManager public static Mesh Create(Universe universe, ReadOnlySpan> vertices, ReadOnlySpan> normals, ReadOnlySpan> uvs) { - var GL = universe.Lookup().Get().GL; + var GL = universe.LookupOrThrow().Get().GL; var vao = GL.GenVertexArray(); GL.BindVertexArray(vao); diff --git a/src/gaemstone.Client/Systems/Renderer.cs b/src/gaemstone.Client/Systems/Renderer.cs index 18eb6b3..d80d62a 100644 --- a/src/gaemstone.Client/Systems/Renderer.cs +++ b/src/gaemstone.Client/Systems/Renderer.cs @@ -16,14 +16,15 @@ namespace gaemstone.Client.Systems; [Module] [DependsOn(typeof(Windowing))] public class Renderer + : IModuleInitializer { - private readonly uint _program; - private readonly int _cameraMatrixUniform; - private readonly int _modelMatrixUniform; + private uint _program; + private int _cameraMatrixUniform; + private int _modelMatrixUniform; - public Renderer(Universe universe) + public void Initialize(EntityRef entity) { - var GL = universe.Lookup().Get().GL; + var GL = entity.Universe.LookupOrThrow().Get().GL; GL.Enable(EnableCap.DebugOutputSynchronous); GL.DebugMessageCallback(DebugCallback, 0); @@ -83,8 +84,7 @@ public class Renderer var cameraMatrix = cameraTransform * cameraProjection; GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.Row1.X); - Filter.RunOnce(universe, (in GlobalTransform transform, in Mesh mesh, Texture? texture) => - { + Filter.RunOnce(universe, (in GlobalTransform transform, in Mesh mesh, Texture? texture) => { // If entity has Texture, bind it now. if (texture.HasValue) GL.BindTexture(texture.Value.Target, texture.Value.Handle); diff --git a/src/gaemstone.Client/TextureManager.cs b/src/gaemstone.Client/TextureManager.cs index b55fd34..af8688a 100644 --- a/src/gaemstone.Client/TextureManager.cs +++ b/src/gaemstone.Client/TextureManager.cs @@ -18,7 +18,7 @@ public static class TextureManager public static void Initialize(Universe universe) { - var GL = universe.Lookup().Get().GL; + var GL = universe.LookupOrThrow().Get().GL; // Upload single-pixel white texture into texture slot 0, so when // "no" texture is bound, we can still use the texture sampler. GL.BindTexture(TextureTarget.Texture2D, 0); @@ -38,7 +38,7 @@ public static class TextureManager public static Texture CreateFromStream(Universe universe, Stream stream, string? sourceFile = null) { - var GL = universe.Lookup().Get().GL; + var GL = universe.LookupOrThrow().Get().GL; var texture = new Texture(TextureTarget.Texture2D, GL.GenTexture()); GL.BindTexture(texture.Target, texture.Handle); diff --git a/src/gaemstone/ECS/BuiltIn+ObserverEvent.cs b/src/gaemstone/ECS/BuiltIn+ObserverEvent.cs new file mode 100644 index 0000000..fd99d79 --- /dev/null +++ b/src/gaemstone/ECS/BuiltIn+ObserverEvent.cs @@ -0,0 +1,22 @@ +using System.Reflection; + +namespace gaemstone.ECS; + +[Module(Name = "flecs.core")] +public static class ObserverEvent +{ + [Entity] public struct OnAdd { } + [Entity] public struct OnRemove { } + [Entity] public struct OnSet { } + [Entity] public struct UnSet { } + // [Entity] public struct OnDelete { } + // [Entity] public struct OnCreateTable { } + // [Entity] public struct OnDeleteTable { } + [Entity] public struct OnTableEmpty { } + [Entity] public struct OnTableFilled { } + // [Entity] public struct OnCreateTrigger { } + // [Entity] public struct OnDeleteTrigger { } + // [Entity] public struct OnDeleteObservable { } + // [Entity] public struct OnComponentHooks { } + // [Entity] public struct OnDeleteTarget { } +} diff --git a/src/gaemstone/ECS/SystemPhase.cs b/src/gaemstone/ECS/BuiltIn+SystemPhase.cs similarity index 79% rename from src/gaemstone/ECS/SystemPhase.cs rename to src/gaemstone/ECS/BuiltIn+SystemPhase.cs index ca9aff2..9c3b070 100644 --- a/src/gaemstone/ECS/SystemPhase.cs +++ b/src/gaemstone/ECS/BuiltIn+SystemPhase.cs @@ -1,22 +1,22 @@ namespace gaemstone.ECS; -// TODO: We *can* use path lookup here, but we have to look in "flecs.pipeline" instead of "flecs.core". +[Module(Name = "flecs.pipeline")] public static class SystemPhase { - [BuiltIn(256 + 65)] public struct PreFrame { } + [Entity] 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. /// - [BuiltIn(256 + 66)] public struct OnLoad { } + [Entity] 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. /// - [BuiltIn(256 + 67)] public struct PostLoad { } + [Entity] public struct PostLoad { } /// /// Now that the input is loaded and processed, it's time to get ready to @@ -25,13 +25,13 @@ public static class SystemPhase /// This can be a good place to prepare the frame, maybe clean up some /// things from the previous frame, etcetera. /// - [BuiltIn(256 + 68)] public struct PreUpdate { } + [Entity] 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. /// - [BuiltIn(256 + 69)] public struct OnUpdate { } + [Entity] public struct OnUpdate { } /// /// This phase was introduced to deal with validating the state of the game @@ -40,7 +40,7 @@ public static class SystemPhase /// This phase is for righting that wrong. A typical feature to implement /// in this phase would be collision detection. /// - [BuiltIn(256 + 70)] public struct OnValidate { } + [Entity] public struct OnValidate { } /// /// When your game logic has been updated, and your validation pass has ran, @@ -48,7 +48,7 @@ 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. /// - [BuiltIn(256 + 71)] public struct PostUpdate { } + [Entity] public struct PostUpdate { } /// /// Now that all of the frame data is computed, validated and corrected for, @@ -57,13 +57,13 @@ public static class SystemPhase /// A good example would be a system that calculates transform matrices from /// a scene graph. /// - [BuiltIn(256 + 72)] public struct PreStore { } + [Entity] 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. /// - [BuiltIn(256 + 73)] public struct OnStore { } + [Entity] public struct OnStore { } - [BuiltIn(256 + 74)] public struct PostFrame { } + [Entity] public struct PostFrame { } } diff --git a/src/gaemstone/ECS/BuiltIn.cs b/src/gaemstone/ECS/BuiltIn.cs new file mode 100644 index 0000000..18aa9b3 --- /dev/null +++ b/src/gaemstone/ECS/BuiltIn.cs @@ -0,0 +1,30 @@ +namespace gaemstone.ECS; + +[Module(Name = "flecs.core")] +public static class BuiltIn +{ + // Entity Tags + [Tag] public struct Module { } + [Tag] public struct Prefab { } + [Tag] public struct SlotOf { } + [Tag] public struct Disabled { } + [Tag] public struct Empty { } + + // Entity Relationships + [Tag, Relation] public struct IsA { } + [Tag, Relation] public struct ChildOf { } + [Tag, Relation] public struct DependsOn { } + + // Component / Relationship Properties + [Tag] public struct Transitive { } + [Tag] public struct Reflexive { } + [Tag] public struct Symmetric { } + [Tag] public struct Final { } + [Tag] public struct DontInherit { } + [Tag] public struct Tag { } + [Tag] public struct Union { } + [Tag] public struct Exclusive { } + [Tag] public struct Acyclic { } + [Tag, Relation] public struct With { } + [Tag] public struct OneOf { } +} diff --git a/src/gaemstone/ECS/Component.cs b/src/gaemstone/ECS/Component.cs index b497a69..9119d80 100644 --- a/src/gaemstone/ECS/Component.cs +++ b/src/gaemstone/ECS/Component.cs @@ -6,14 +6,16 @@ using static flecs_hub.flecs; namespace gaemstone.ECS; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -public class ComponentAttribute : Attribute { } +public class ComponentAttribute : EntityAttribute { } public static class ComponentExtensions { - public static EntityRef RegisterComponent(this Universe universe) - => universe.RegisterComponent(typeof(T)); - public unsafe static EntityRef RegisterComponent(this Universe universe, Type type) + public unsafe static EntityRef CreateComponent(this EntityRef entity) + => entity.CreateComponent(typeof(T)); + public unsafe static EntityRef CreateComponent(this EntityRef entity, Type type) { + // TODO: Do some additional sanity checking for this type. + var typeInfo = default(ecs_type_info_t); if (type.IsValueType) { var wrapper = TypeWrapper.For(type); @@ -31,14 +33,8 @@ public static class ComponentExtensions typeInfo.alignment = sizeof(nint); } - var name = type.GetFriendlyName(); - var entity = new EntityBuilder(universe, name) { Symbol = name } .Build(); - var desc = new ecs_component_desc_t { entity = entity, type = typeInfo }; - - entity = new(universe, new(ecs_component_init(universe, &desc))); - universe.RegisterLookup(type, entity); - // TODO: SetHooks(hooks, id); - - return entity; + var desc = new ecs_component_desc_t { entity = entity, type = typeInfo }; + ecs_component_init(entity.Universe, &desc); + return entity.CreateLookup(type); } } diff --git a/src/gaemstone/ECS/ComponentHooks.cs b/src/gaemstone/ECS/ComponentHooks.cs new file mode 100644 index 0000000..427add4 --- /dev/null +++ b/src/gaemstone/ECS/ComponentHooks.cs @@ -0,0 +1,90 @@ +using System; +using System.Runtime.InteropServices; +using gaemstone.Utility; +using static flecs_hub.flecs; + +namespace gaemstone.ECS; + +// FIXME: This API is flawed. +public unsafe class ComponentHooks +{ + public EntityRef Entity { get; } + public ecs_type_hooks_t* Handle { get; } + + private Action? _onAdd; + public event Action? OnAdd { + add { if ((_onAdd += value) != null) Handle->on_add.Data.Pointer = &CallbackOnAdd; } + remove { if ((_onAdd -= value) == null) Handle->on_add.Data.Pointer = null; } + } + private Action? _onSet; + public event Action? OnSet { + add { if ((_onSet += value) != null) Handle->on_set.Data.Pointer = &CallbackOnSet; } + remove { if ((_onSet -= value) == null) Handle->on_set.Data.Pointer = null; } + } + private Action? _onRemove; + public event Action? OnRemove { + add { if ((_onRemove += value) != null) Handle->on_remove.Data.Pointer = &CallbackOnRemove; } + remove { if ((_onRemove -= value) == null) Handle->on_remove.Data.Pointer = null; } + } + + private Action? _constructor; + public event Action? Construct { + add { if ((_constructor += value) != null) Handle->ctor.Data.Pointer = &CallbackConstructor; } + remove { if ((_constructor -= value) == null) Handle->ctor.Data.Pointer = null; } + } + private Action? _destructor; + public event Action? Destruct { + add { if ((_destructor += value) != null) Handle->dtor.Data.Pointer = &CallbackDestructor; } + remove { if ((_destructor -= value) == null) Handle->dtor.Data.Pointer = null; } + } + private Action? _copy; + public event Action? Copy { + add { if ((_copy += value) != null) Handle->copy.Data.Pointer = &CallbackCopy; } + remove { if ((_copy -= value) == null) Handle->copy.Data.Pointer = null; } + } + private Action? _move; + public event Action? Move { + add { if ((_move += value) != null) Handle->move.Data.Pointer = &CallbackMove; } + remove { if ((_move -= value) == null) Handle->move.Data.Pointer = null; } + } + + internal ComponentHooks(EntityRef entity, ecs_type_hooks_t* handle) + { Entity = entity; Handle = handle; } + + + private static ComponentHooks Get(void* ptr) + => CallbackContextHelper.Get((nint)ptr); + + [UnmanagedCallersOnly] private static void CallbackOnAdd(ecs_iter_t* it) + { var hooks = Get(it->binding_ctx); hooks._onAdd?.Invoke(new(hooks.Entity.Universe, null, *it)); } + [UnmanagedCallersOnly] private static void CallbackOnSet(ecs_iter_t* it) + { var hooks = Get(it->binding_ctx); hooks._onSet?.Invoke(new(hooks.Entity.Universe, null, *it)); } + [UnmanagedCallersOnly] private static void CallbackOnRemove(ecs_iter_t* it) + { var hooks = Get(it->binding_ctx); hooks._onRemove?.Invoke(new(hooks.Entity.Universe, null, *it)); } + + [UnmanagedCallersOnly] private static void CallbackConstructor(void* ptr, int count, ecs_type_info_t* type) + => Get(type->hooks.binding_ctx)._constructor?.Invoke((nint)ptr, count); + [UnmanagedCallersOnly] private static void CallbackDestructor(void* ptr, int count, ecs_type_info_t* type) + => Get(type->hooks.binding_ctx)._destructor?.Invoke((nint)ptr, count); + [UnmanagedCallersOnly] private static void CallbackCopy(void* dest, void* src, int count, ecs_type_info_t* type) + => Get(type->hooks.binding_ctx)._copy?.Invoke((nint)dest, (nint)src, count); + [UnmanagedCallersOnly] private static void CallbackMove(void* dest, void* src, int count, ecs_type_info_t* type) + => Get(type->hooks.binding_ctx)._move?.Invoke((nint)dest, (nint)src, count); +} + +public unsafe static class ComponentHooksExtensions +{ + public static ComponentHooks GetComponentHooks(this EntityRef entity) + { + var handle = ecs_get_hooks_id(entity.Universe, entity); + if (handle == null) throw new ArgumentException($"No type info found for {entity}"); + + if (handle->binding_ctx == null) { + var hooks = new ComponentHooks(entity, handle); + handle->binding_ctx = (void*)CallbackContextHelper.Create(hooks); + return hooks; + } else { + return CallbackContextHelper.Get((nint)handle->binding_ctx); + } + } +} diff --git a/src/gaemstone/ECS/Entity+AddRemove.cs b/src/gaemstone/ECS/Entity+AddRemove.cs deleted file mode 100644 index 2e90ea1..0000000 --- a/src/gaemstone/ECS/Entity+AddRemove.cs +++ /dev/null @@ -1,43 +0,0 @@ -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe readonly partial struct EntityRef -{ - public EntityRef Add(Identifier id) { ecs_add_id(Universe, this, id); return this; } - public EntityRef Add(Entity relation, Entity target) => Add(relation & target); - public EntityRef Remove(Identifier id) { ecs_remove_id(Universe, this, id); return this; } - public EntityRef Remove(Entity relation, Entity target) => Remove(relation & target); - public bool Has(Identifier id) => ecs_has_id(Universe, this, id); - public bool Has(Entity relation, Entity target) => Has(relation & target); - // public EntityRef Override(Identifier id) { ecs_override_id(Universe, this, id); return this; } - // public EntityRef Override(Entity relation, Entity target) => Override(relation & target); - - public EntityRef Add() - => Add(Universe.Lookup()); - public EntityRef Add() - => Add(Universe.Lookup(), Universe.Lookup()); - public EntityRef Add(Entity target) - => Add(Universe.Lookup(), target); - - public EntityRef Remove() - => Remove(Universe.Lookup()); - public EntityRef Remove() - => Remove(Universe.Lookup(), Universe.Lookup()); - public EntityRef Remove(Entity target) - => Remove(Universe.Lookup(), target); - - public bool Has() - => Has(Universe.Lookup()); - public bool Has() - => Has(Universe.Lookup(), Universe.Lookup()); - public bool Has(Entity target) - => Has(Universe.Lookup(), target); - - // public EntityRef Override() - // => Override(Universe.Lookup()); - // public EntityRef Override() - // => Override(Universe.Lookup(), Universe.Lookup()); - // public EntityRef Override(Entity target) - // => Override(Universe.Lookup(), target); -} diff --git a/src/gaemstone/ECS/Entity+GetSet.cs b/src/gaemstone/ECS/Entity+GetSet.cs deleted file mode 100644 index 5723503..0000000 --- a/src/gaemstone/ECS/Entity+GetSet.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe readonly partial struct EntityRef -{ - /// - /// Gets a component value from this entity. If the component is a value - /// type, this will return a copy. If the component is a reference type, - /// it will return the reference itself. - /// When modifying a reference, consider calling . - /// - public T Get() - { - var comp = Universe.Lookup(); - var ptr = ecs_get_id(Universe, this, comp); - return (typeof(T).IsValueType) ? Unsafe.Read(ptr) - : (T)((GCHandle)Unsafe.Read(ptr)).Target!; - } - - /// - /// Gets a reference to a component value from this entity. Only works for - /// value types. When modifying, consider calling . - /// - public ref T GetRef() - where T : unmanaged - { - var comp = Universe.Lookup(); - var ptr = ecs_get_mut_id(Universe, this, comp); - return ref Unsafe.AsRef(ptr); - } - - /// - /// Marks a component as modified. Do this after getting a reference to - /// it with or , making sure change - /// detection will kick in. - /// - public void Modified() - => ecs_modified_id(Universe, this, Universe.Lookup()); - - - public EntityRef Set(in T value) - where T : unmanaged - { - var comp = Universe.Lookup(); - var size = (ulong)Unsafe.SizeOf(); - fixed (T* ptr = &value) - ecs_set_id(Universe, this, comp, size, ptr); - return this; - } - - // public Entity SetOverride(in T value) - // where T : unmanaged - // { - // var comp = Universe.Lookup(); - // var size = (ulong)Unsafe.SizeOf(); - // ecs_add_id(Universe, this, Identifier.Combine(IdentifierFlags.Override, comp)); - // fixed (T* ptr = &value) ecs_set_id(Universe, this, comp, size, ptr); - // return this; - // } - - public EntityRef Set(T obj) where T : class - => Set(typeof(T), obj); - public EntityRef Set(Type type, object obj) - { - var comp = Universe.Lookup(type); - var handle = (nint)GCHandle.Alloc(obj); - ecs_set_id(Universe, this, comp, (ulong)sizeof(nint), &handle); - // FIXME: Handle needs to be freed when component is removed! - return this; - } - - // public EntityRef SetOverride(T obj) - // where T : class - // { - // var comp = Universe.Lookup(); - // var handle = (nint)GCHandle.Alloc(obj); - // ecs_add_id(Universe, this, Identifier.Combine(IdentifierFlags.Override, comp)); - // ecs_set_id(Universe, this, comp, (ulong)sizeof(nint), &handle); - // // FIXME: Handle needs to be freed when component is removed! - // return this; - // } -} diff --git a/src/gaemstone/ECS/Entity.cs b/src/gaemstone/ECS/Entity.cs index 3a005b8..6d3b9d8 100644 --- a/src/gaemstone/ECS/Entity.cs +++ b/src/gaemstone/ECS/Entity.cs @@ -1,32 +1,32 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] public class EntityAttribute : Attribute -{ - public uint ID { get; set; } - public string? Name { get; set; } -} + { 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 readonly struct Entity : IEquatable { public readonly ecs_entity_t Value; - // FIXME: IsValid is a function that should go on EntityRef, this should be IsNone instead. - public bool IsValid => Value.Data != 0; - public Entity ThrowIfInvalid() => IsValid ? this : throw new FlecsException(this + " is not valid"); + public bool IsNone => Value.Data == 0; + public Entity ThrowIfNone() => !IsNone ? this + : throw new FlecsException(this + " is none"); public Entity(ecs_entity_t value) => Value = value; public bool Equals(Entity other) => Value.Data == other.Value.Data; - public override bool Equals([NotNullWhen(true)] object? obj) => (obj is Entity other) && Equals(other); + public override bool Equals(object? obj) => (obj is Entity other) && Equals(other); public override int GetHashCode() => Value.Data.GetHashCode(); public override string? ToString() => $"Entity(0x{Value.Data.Data:X})"; @@ -39,86 +39,3 @@ public readonly struct Entity public static implicit operator Identifier(Entity e) => new(e.Value.Data); public static implicit operator ecs_id_t(Entity e) => e.Value.Data; } - -public unsafe readonly partial struct EntityRef - : IEquatable - , IDisposable -{ - public Universe Universe { get; } - public Entity Entity { get; } - - public bool IsAlive => ecs_is_alive(Universe, this); - public string Name => ecs_get_name(Universe, this).FlecsToString()!; - public string FullPath => ecs_get_path_w_sep(Universe, default, this, ".", default).FlecsToStringAndFree()!; - public EntityType Type => new(Universe, ecs_get_type(Universe, this)); - - // TODO: public IEnumerable Children => ... - - public EntityRef(Universe universe, Entity entity) - { Universe = universe; Entity = entity.ThrowIfInvalid(); } - - void IDisposable.Dispose() => Delete(); - public unsafe void Delete() => ecs_delete(Universe, this); - - public EntityRef Disable() => Add(); - public EntityRef Enable() => Remove(); - - public bool Equals(EntityRef other) => Universe == other.Universe && Entity == other.Entity; - public override bool Equals([NotNullWhen(true)] object? obj) => (obj is EntityRef other) && Equals(other); - public override int GetHashCode() => HashCode.Combine(Universe, Entity); - public override string? ToString() => ecs_entity_str(Universe, this).FlecsToStringAndFree()!; - - public static bool operator ==(EntityRef left, EntityRef right) => left.Equals(right); - public static bool operator !=(EntityRef left, EntityRef right) => !left.Equals(right); - - public static IdentifierRef operator &(EntityRef first, Entity second) => IdentifierRef.Pair(first, second); - public static IdentifierRef operator &(Entity first, EntityRef second) => IdentifierRef.Pair(first, second); - - public static implicit operator Entity(EntityRef e) => new(e.Entity); - public static implicit operator ecs_entity_t(EntityRef e) => e.Entity.Value; - - public static implicit operator Identifier(EntityRef e) => new(e.Entity.Value.Data); - public static implicit operator ecs_id_t(EntityRef e) => e.Entity.Value.Data; -} - -public unsafe readonly struct EntityType - : IReadOnlyList -{ - public Universe Universe { get; } - public ecs_type_t* Handle { get; } - - public EntityType(Universe universe, ecs_type_t* handle) - { Universe = universe; Handle = handle; } - - public override string ToString() - => ecs_type_str(Universe, Handle).FlecsToStringAndFree()!; - - // IReadOnlyList implementation - public int Count => Handle->count; - public IdentifierRef this[int index] => new(Universe, new(Handle->array[index])); - public IEnumerator GetEnumerator() { for (var i = 0; i < Count; i++) yield return this[i]; } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} - -public unsafe static class EntityExtensions -{ - public static EntityRef Create(this Universe universe) - => new EntityBuilder(universe).Build(); - - public static EntityRef Create(this Universe universe, string name) - => new EntityBuilder(universe, name).Build(); - - - public static EntityRef RegisterEntity(this Universe universe) - where T : unmanaged => universe.RegisterEntity(typeof(T)); - public static EntityRef RegisterEntity(this Universe universe, Type type) - { - if (!type.IsValueType || type.IsPrimitive || type.GetFields().Length > 0) - throw new Exception("Entity must be an empty, used-defined struct."); - var name = type.GetFriendlyName(); - var entity = new EntityBuilder(universe, name) { Symbol = name } .Build(); - // TODO: Automatically add fields as IDs of an entity? - universe.RegisterLookup(type, entity); - return entity; - } -} diff --git a/src/gaemstone/ECS/EntityBase.cs b/src/gaemstone/ECS/EntityBase.cs new file mode 100644 index 0000000..d4197fc --- /dev/null +++ b/src/gaemstone/ECS/EntityBase.cs @@ -0,0 +1,44 @@ +using static gaemstone.ECS.BuiltIn; + +namespace gaemstone.ECS; + +public abstract class EntityBase +{ + public abstract Universe Universe { get; } + + public abstract TReturn Add(Identifier id); + public abstract TReturn Remove(Identifier id); + public abstract bool Has(Identifier id); + + public abstract T Get(); + public abstract ref T GetRef() where T : unmanaged; + public abstract void Modified(); + + public abstract TReturn Set(in T value) where T : unmanaged; + public abstract TReturn Set(T obj) where T : class; + + public TReturn Add(string name) => Add(Universe.LookupOrThrow(name)); + public TReturn Add() => Add(Universe.LookupOrThrow(typeof(T))); + public TReturn Add(Entity relation, Entity target) => Add(relation & target); + public TReturn Add(Entity target) => Add(Universe.LookupOrThrow(), target); + public TReturn Add() => Add(Universe.LookupOrThrow(), Universe.LookupOrThrow()); + + public TReturn Remove(string name) => Remove(Universe.LookupOrThrow(name)); + public TReturn Remove() => Remove(Universe.LookupOrThrow(typeof(T))); + public TReturn Remove(Entity relation, Entity target) => Remove(relation & target); + public TReturn Remove(Entity target) => Remove(Universe.LookupOrThrow(), target); + public TReturn Remove() => Remove(Universe.LookupOrThrow(), Universe.LookupOrThrow()); + + public bool Has(string name) => Has(Universe.LookupOrThrow(name)); + public bool Has() => Has(Universe.LookupOrThrow(typeof(T))); + public bool Has(Entity relation, Entity target) => Has(relation & target); + public bool Has(Entity target) => Has(Universe.LookupOrThrow(), target); + public bool Has() => Has(Universe.LookupOrThrow(), Universe.LookupOrThrow()); + + public TReturn ChildOf(Entity parent) => Add(parent); + public TReturn ChildOf() => Add(); + + public TReturn Disable() => Add(); + public TReturn Enable() => Remove(); + public bool IsDisabled => Has(); +} diff --git a/src/gaemstone/ECS/EntityBuilder.cs b/src/gaemstone/ECS/EntityBuilder.cs index 440779d..503a86e 100644 --- a/src/gaemstone/ECS/EntityBuilder.cs +++ b/src/gaemstone/ECS/EntityBuilder.cs @@ -1,14 +1,13 @@ -using System.Collections; +using System; using System.Collections.Generic; using static flecs_hub.flecs; namespace gaemstone.ECS; -// TODO: Create an interface for EntityBuilder and EntityRef so common operations are available through it. public class EntityBuilder - : IReadOnlyCollection + : EntityBase { - public Universe Universe { get; } + public override Universe Universe { get; } /// Set to modify existing entity (optional). public Entity ID { get; set; } @@ -36,28 +35,31 @@ public class EntityBuilder public bool UseLowID { get; set; } /// IDs to add to the new or existing entity. - private readonly List _add = new(); + private readonly HashSet _toAdd = new(); /// String expression with components to add. public string? Expression { get; } - public EntityBuilder(Universe universe) => Universe = universe; - public EntityBuilder(Universe universe, string name) : this(universe) => Name = name; + /// Actions to run once the entity has been created. + private readonly List> _toSet = new(); - public EntityBuilder Add(Identifier id) { _add.Add(id); return this; } - public EntityBuilder Add(string name) => Add(Universe.Lookup(name)); - public EntityBuilder Add() => Add(Universe.Lookup()); + public EntityBuilder(Universe universe, string? name = null, string? symbol = null) + { Universe = universe; Name = name; Symbol = symbol; } - public EntityBuilder Add(Entity first, Entity second) => Add(first & second); - public EntityBuilder Add(Entity second) => Add(Universe.Lookup(), second); - public EntityBuilder Add() => Add(Universe.Lookup(), Universe.Lookup()); + public override EntityBuilder Add(Identifier id) { _toAdd.Add(id); return this; } + public override EntityBuilder Remove(Identifier id) => throw new NotSupportedException(); + public override bool Has(Identifier id) => !ecs_id_is_wildcard(id) ? _toAdd.Contains(id) : throw new NotSupportedException(); - // TODO: Add support for Set. + public override T Get() => throw new NotSupportedException(); + public override ref T GetRef() => throw new NotSupportedException(); + public override void Modified() => throw new NotImplementedException(); - public EntityBuilder ChildOf(Entity parent) => Add(Universe.Lookup(), parent); - public EntityBuilder ChildOf() => ChildOf(Universe.Lookup()); + public override EntityBuilder Set(in T value) + // "in" can't be used with lambdas, so we make a local copy. + { var copy = value; _toSet.Add(e => e.Set(copy)); return this; } - public EntityBuilder Disabled() => Add(Universe.Lookup()); + public override EntityBuilder Set(T obj) + { _toSet.Add(e => e.Set(obj)); return this; } public unsafe EntityRef Build() { @@ -68,14 +70,11 @@ public class EntityBuilder add_expr = Expression.FlecsToCString(), use_low_id = UseLowID, }; - var add = desc.add; - for (var i = 0; i < Count; i++) add[i] = _add[i]; - var entity = ecs_entity_init(Universe, &desc); - return new(Universe, new(entity)); + var add = desc.add; var index = 0; + foreach (var id in _toAdd) add[index++] = id; + var entityID = ecs_entity_init(Universe, &desc); + var entity = new EntityRef(Universe, new(entityID)); + foreach (var action in _toSet) action(entity); + return entity; } - - // IReadOnlyCollection implementation - public int Count => _add.Count; - public IEnumerator GetEnumerator() => _add.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/gaemstone/ECS/EntityRef.cs b/src/gaemstone/ECS/EntityRef.cs new file mode 100644 index 0000000..30b8d7e --- /dev/null +++ b/src/gaemstone/ECS/EntityRef.cs @@ -0,0 +1,105 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static flecs_hub.flecs; + +namespace gaemstone.ECS; + +public unsafe sealed class EntityRef + : EntityBase + , IEquatable +{ + public override Universe Universe { get; } + public Entity Entity { get; } + + public bool IsValid => ecs_is_valid(Universe, this); + public bool IsAlive => ecs_is_alive(Universe, this); + public EntityType Type => new(Universe, ecs_get_type(Universe, this)); + public string FullPath => ecs_get_path_w_sep(Universe, default, this, ".", default).FlecsToStringAndFree()!; + + public string? Name { + get => ecs_get_name(Universe, this).FlecsToString()!; + set => ecs_set_name(Universe, this, value.FlecsToCStringThenFree()); + } + public string? Symbol { + get => ecs_get_symbol(Universe, this).FlecsToString()!; + set => ecs_set_symbol(Universe, this, value.FlecsToCStringThenFree()); + } + + // TODO: public IEnumerable Children => ... + + public EntityRef(Universe universe, Entity entity, bool throwOnInvalid = true) + { + Universe = universe; + Entity = entity; + if (throwOnInvalid && !IsValid) throw new InvalidOperationException( + $"The entity {entity} is not valid"); + } + + public void Delete() => ecs_delete(Universe, this); + + public EntityBuilder NewChild(string? name = null, string? symbol = null) + => Universe.New(name, symbol).ChildOf(this); + + public override EntityRef Add(Identifier id) { ecs_add_id(Universe, this, id); return this; } + public override EntityRef Remove(Identifier id) { ecs_remove_id(Universe, this, id); return this; } + public override bool Has(Identifier id) => ecs_has_id(Universe, this, id); + + public override T Get() + { + var comp = Universe.LookupOrThrow(); + var ptr = ecs_get_id(Universe, this, comp); + if (ptr == null) throw new Exception($"Component {typeof(T)} not found on {this}"); + return (typeof(T).IsValueType) ? Unsafe.Read(ptr) + : (T)((GCHandle)Unsafe.Read(ptr)).Target!; + } + + public override ref T GetRef() + { + var comp = Universe.LookupOrThrow(); + var ptr = ecs_get_mut_id(Universe, this, comp); + if (ptr == null) throw new Exception($"Component {typeof(T)} not found on {this}"); + return ref Unsafe.AsRef(ptr); + } + + public override void Modified() + => ecs_modified_id(Universe, this, Universe.LookupOrThrow()); + + public override EntityRef Set(in T value) + { + var comp = Universe.LookupOrThrow(); + var size = (ulong)Unsafe.SizeOf(); + fixed (T* ptr = &value) + if (ecs_set_id(Universe, this, comp, size, ptr).Data == 0) + throw new InvalidOperationException(); + return this; + } + + public override EntityRef Set(T obj) where T : class + { + var comp = Universe.LookupOrThrow(); + var handle = (nint)GCHandle.Alloc(obj); + // FIXME: Previous handle needs to be freed. + if (ecs_set_id(Universe, this, comp, (ulong)sizeof(nint), &handle).Data == 0) + throw new InvalidOperationException(); + // FIXME: Handle needs to be freed when component is removed! + return this; + } + + public bool Equals(EntityRef? other) => (other is not null) && Universe == other.Universe && Entity == other.Entity; + public override bool Equals(object? obj) => Equals(obj as EntityRef); + public override int GetHashCode() => HashCode.Combine(Universe, Entity); + public override string? ToString() => ecs_entity_str(Universe, this).FlecsToStringAndFree()!; + + public static bool operator ==(EntityRef? left, EntityRef? right) => ReferenceEquals(left, right) || (left?.Equals(right) ?? false); + public static bool operator !=(EntityRef? left, EntityRef? right) => !(left == right); + + public static IdentifierRef operator &(EntityRef relation, Entity target) => IdentifierRef.Pair(relation, target); + public static IdentifierRef operator &(Entity relation, EntityRef target) => IdentifierRef.Pair(relation, target); + + public static implicit operator Entity(EntityRef e) => e.Entity; + public static implicit operator ecs_entity_t(EntityRef e) => e.Entity.Value; + + public static implicit operator Identifier(EntityRef e) => new(e.Entity.Value.Data); + public static implicit operator ecs_id_t(EntityRef e) => e.Entity.Value.Data; +} diff --git a/src/gaemstone/ECS/EntityType.cs b/src/gaemstone/ECS/EntityType.cs new file mode 100644 index 0000000..2644d11 --- /dev/null +++ b/src/gaemstone/ECS/EntityType.cs @@ -0,0 +1,24 @@ +using System.Collections; +using System.Collections.Generic; +using static flecs_hub.flecs; + +namespace gaemstone.ECS; + +public unsafe readonly struct EntityType + : IReadOnlyList +{ + public Universe Universe { get; } + public ecs_type_t* Handle { get; } + + public EntityType(Universe universe, ecs_type_t* handle) + { Universe = universe; Handle = handle; } + + public override string ToString() + => ecs_type_str(Universe, Handle).FlecsToStringAndFree()!; + + // IReadOnlyList implementation + public int Count => Handle->count; + public IdentifierRef this[int index] => new(Universe, new(Handle->array[index])); + public IEnumerator GetEnumerator() { for (var i = 0; i < Count; i++) yield return this[i]; } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/gaemstone/ECS/Filter.cs b/src/gaemstone/ECS/Filter.cs index ad9df4f..e5d8b6a 100644 --- a/src/gaemstone/ECS/Filter.cs +++ b/src/gaemstone/ECS/Filter.cs @@ -78,9 +78,9 @@ public class FilterDesc instanced = Instanced, }; var span = desc.terms; - if (Terms.Count > desc.terms.Length) { + if (Terms.Count > span.Length) { var byteCount = sizeof(ecs_term_t) * Terms.Count; - var ptr = (ecs_term_t*)Marshal.AllocHGlobal(byteCount); + var ptr = (ecs_term_t*)Marshal.AllocHGlobal(byteCount); // FIXME: Free this. desc.terms_buffer = ptr; desc.terms_buffer_count = Terms.Count; span = new(ptr, Terms.Count); diff --git a/src/gaemstone/ECS/Flecs.cs b/src/gaemstone/ECS/Flecs.cs deleted file mode 100644 index 472ae76..0000000 --- a/src/gaemstone/ECS/Flecs.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace gaemstone.ECS; - -public static class Flecs -{ - // Entities - - // [BuiltIn] public struct World { } - [BuiltIn] public struct Flag { } - - // Tags - - [BuiltIn] public struct Prefab { } - [BuiltIn] public struct SlotOf { } - [BuiltIn] public struct Disabled { } - [BuiltIn] public struct Empty { } - - // Component / relationship properties - - [BuiltIn(256 + 10)] public struct Wildcard { } - [BuiltIn(256 + 11)] public struct Any { } - [BuiltIn(256 + 12)] public struct This { } - [BuiltIn(256 + 13)] public struct Variable { } - - [BuiltIn] public struct Transitive { } - [BuiltIn] public struct Reflexive { } - [BuiltIn] public struct Symmetric { } - [BuiltIn] public struct Final { } - [BuiltIn] public struct DontInherit { } - [BuiltIn] public struct Tag { } - [BuiltIn] public struct Union { } - [BuiltIn] public struct Exclusive { } - [BuiltIn] public struct Acyclic { } - [BuiltIn] public struct With { } - [BuiltIn] public struct OneOf { } - - // Relationships - - [BuiltIn] public struct IsA { } - [BuiltIn] public struct ChildOf { } - [BuiltIn] public struct DependsOn { } -} diff --git a/src/gaemstone/ECS/Game.cs b/src/gaemstone/ECS/Game.cs index a3001d3..684f9c3 100644 --- a/src/gaemstone/ECS/Game.cs +++ b/src/gaemstone/ECS/Game.cs @@ -12,4 +12,7 @@ public struct Game { } /// Short for [Source(typeof(Game))]. [AttributeUsage(AttributeTargets.Parameter)] public class GameAttribute : SourceAttribute - { public GameAttribute() : base(typeof(Game)) { } } +{ + public GameAttribute() + : base(typeof(Game)) { } +} diff --git a/src/gaemstone/ECS/Identifier.cs b/src/gaemstone/ECS/Identifier.cs index 273c448..40cfbe3 100644 --- a/src/gaemstone/ECS/Identifier.cs +++ b/src/gaemstone/ECS/Identifier.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -17,11 +16,13 @@ public readonly struct Identifier public static Identifier Combine(IdentifierFlags flags, Identifier id) => new((ulong)flags | id.Value); - public static Identifier Pair(Entity first, Entity second) - => Combine(IdentifierFlags.Pair, new((first.Value.Data << 32) + (uint)second.Value.Data)); + public static Identifier Pair(Entity relation, Entity target) + => Combine(IdentifierFlags.Pair, new( + (relation.Value.Data << 32) | + (target.Value.Data & ECS_ENTITY_MASK))); public bool Equals(Identifier other) => Value.Data == other.Value.Data; - public override bool Equals([NotNullWhen(true)] object? obj) => (obj is Identifier other) && Equals(other); + public override bool Equals(object? obj) => (obj is Identifier other) && Equals(other); public override int GetHashCode() => Value.Data.GetHashCode(); public override string? ToString() => (Flags != default) ? $"Identifier(0x{Value.Data:X}, Flags={Flags})" @@ -33,43 +34,6 @@ public readonly struct Identifier public static implicit operator ecs_id_t(Identifier i) => i.Value; } -public unsafe readonly struct IdentifierRef - : IEquatable -{ - public Universe Universe { get; } - public Identifier ID { get; } - - public IdentifierFlags Flags => ID.Flags; - public bool IsPair => ID.IsPair; - public bool IsWildcard => ID.IsWildcard; - public bool IsValid => ecs_id_is_valid(Universe, ID); - - public IdentifierRef(Universe universe, Identifier id) - { Universe = universe; ID = id; } - - public static IdentifierRef Combine(IdentifierFlags flags, IdentifierRef id) - => new(id.Universe, Identifier.Combine(flags, id)); - public static IdentifierRef Pair(EntityRef first, Entity second) - => new(first.Universe, Identifier.Pair(first, second)); - public static IdentifierRef Pair(Entity first, EntityRef second) - => new(second.Universe, Identifier.Pair(first, second)); - - public (EntityRef, EntityRef) AsPair() - => (Universe.Lookup(new Entity(new() { Data = (ID.Value & ECS_COMPONENT_MASK) >> 32 })), - Universe.Lookup(new Entity(new() { Data = ID.Value & ECS_ENTITY_MASK }))); - - public bool Equals(IdentifierRef other) => Universe == other.Universe && ID == other.ID; - public override bool Equals([NotNullWhen(true)] object? obj) => (obj is IdentifierRef other) && Equals(other); - public override int GetHashCode() => HashCode.Combine(Universe, ID); - public override string? ToString() => ecs_id_str(Universe, this).FlecsToStringAndFree()!; - - public static bool operator ==(IdentifierRef left, IdentifierRef right) => left.Equals(right); - public static bool operator !=(IdentifierRef left, IdentifierRef right) => !left.Equals(right); - - public static implicit operator Identifier(IdentifierRef i) => i.ID; - public static implicit operator ecs_id_t(IdentifierRef i) => i.ID.Value; -} - [Flags] public enum IdentifierFlags : ulong { diff --git a/src/gaemstone/ECS/IdentifierRef.cs b/src/gaemstone/ECS/IdentifierRef.cs new file mode 100644 index 0000000..5bab82a --- /dev/null +++ b/src/gaemstone/ECS/IdentifierRef.cs @@ -0,0 +1,41 @@ +using System; +using static flecs_hub.flecs; + +namespace gaemstone.ECS; + +public unsafe class IdentifierRef + : IEquatable +{ + public Universe Universe { get; } + public Identifier ID { get; } + + public IdentifierFlags Flags => ID.Flags; + public bool IsPair => ID.IsPair; + public bool IsWildcard => ID.IsWildcard; + public bool IsValid => ecs_id_is_valid(Universe, ID); + + public IdentifierRef(Universe universe, Identifier id) + { Universe = universe; ID = id; } + + public static IdentifierRef Combine(IdentifierFlags flags, IdentifierRef id) + => new(id.Universe, Identifier.Combine(flags, id)); + public static IdentifierRef Pair(EntityRef relation, Entity target) + => new(relation.Universe, Identifier.Pair(relation, target)); + public static IdentifierRef Pair(Entity relation, EntityRef target) + => new(target.Universe, Identifier.Pair(relation, target)); + + public (EntityRef Relation, EntityRef Target) AsPair() + => (Universe.LookupOrThrow(new Entity(new() { Data = (ID.Value & ECS_COMPONENT_MASK) >> 32 })), + Universe.LookupOrThrow(new Entity(new() { Data = ID.Value & ECS_ENTITY_MASK }))); + + public bool Equals(IdentifierRef? other) => (other is not null) && Universe == other.Universe && ID == other.ID; + public override bool Equals(object? obj) => Equals(obj as IdentifierRef); + public override int GetHashCode() => HashCode.Combine(Universe, ID); + public override string? ToString() => ecs_id_str(Universe, this).FlecsToStringAndFree()!; + + public static bool operator ==(IdentifierRef? left, IdentifierRef? right) => ReferenceEquals(left, right) || (left?.Equals(right) ?? false); + public static bool operator !=(IdentifierRef? left, IdentifierRef? right) => !(left == right); + + public static implicit operator Identifier(IdentifierRef i) => i.ID; + public static implicit operator ecs_id_t(IdentifierRef i) => i.ID.Value; +} diff --git a/src/gaemstone/ECS/Iterator.cs b/src/gaemstone/ECS/Iterator.cs index 69cb619..fedb026 100644 --- a/src/gaemstone/ECS/Iterator.cs +++ b/src/gaemstone/ECS/Iterator.cs @@ -2,12 +2,12 @@ using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; -public unsafe class Iterator +public unsafe partial class Iterator : IEnumerable { public Universe Universe { get; } @@ -68,17 +68,15 @@ public unsafe class Iterator { fixed (ecs_iter_t* ptr = &Value) { var id = ecs_field_id(ptr, index); - var comp = Universe.Lookup(); + var comp = Universe.LookupOrThrow(); return id == comp.Entity.Value.Data; } } - public readonly ref struct SpanToRef + public override string ToString() { - private readonly Span _span; - internal SpanToRef(Span span) => _span = span; - public int Length => _span.Length; - public T this[int index] => (T)((GCHandle)_span[index]).Target!; + fixed (ecs_iter_t* ptr = &Value) + return ecs_iter_str(ptr).FlecsToStringAndFree()!; } // IEnumerable implementation diff --git a/src/gaemstone/ECS/Module.cs b/src/gaemstone/ECS/Module.cs index 37011d0..191e829 100644 --- a/src/gaemstone/ECS/Module.cs +++ b/src/gaemstone/ECS/Module.cs @@ -1,133 +1,25 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using gaemstone.Utility; namespace gaemstone.ECS; [AttributeUsage(AttributeTargets.Class)] -public class ModuleAttribute : Attribute { } +public class ModuleAttribute : Attribute +{ + public string? Name { get; set; } +} [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class DependsOnAttribute : Attribute { - public Type Target { get; } // TODO: Should probably move to string. - public DependsOnAttribute(Type target) => Target = target; -} + public string Name { get; } -[Component] -public class Module -{ - public Type Type { get; } - public object? Instance { get; internal set; } - public Module(Type type) => Type = type; + public DependsOnAttribute(string name) + => Name = name; + public DependsOnAttribute(Type type) + : this(ModuleManager.GetModuleName(type)) { } } -public static class ModuleExtensions +public interface IModuleInitializer { - private static readonly Regex _removeCommon = new("(Module|Components)$"); - // TODO: This wouldn't work with multiple universes. Find a different way to do it. - private static Query? _disabledModulesQuery; - private static Rule? _disabledModulesWithDisabledDepsRule; - - public static EntityRef RegisterModule(this Universe universe) - where T : class => universe.RegisterModule(typeof(T)); - public static EntityRef RegisterModule(this Universe universe, Type type) - { - // TODO: Would be nice to make this more straight-forward, especially for in-module code. - var Module = universe.Lookup(); - var Disabled = universe.Lookup(); - var DependsOn = universe.Lookup(); - - if (!type.IsClass || type.IsAbstract) throw new Exception( - "Module must be a non-abstract and non-static class"); - if (!type.Has()) throw new Exception( - "Module must be marked with ModuleAttribute"); - - var hasSimpleCtor = type.GetConstructor(Type.EmptyTypes) != null; - var hasUniverseCtor = type.GetConstructor(new[] { typeof(Universe) }) != null; - if (!hasSimpleCtor && !hasUniverseCtor) throw new Exception( - $"Module {type} must define public new() or new(Universe)"); - - static EntityRef LookupOrCreateModule(Universe universe, Type type) - { - var entity = universe.TryLookup(type); - if (entity.IsValid) return new(universe, entity); - - // Using startat: 1, regex shouldn't match strings whose entire name matches. - if (type.Namespace == null) throw new NotSupportedException("Module must have a namespace"); - var name = _removeCommon.Replace(type.GetFriendlyName(), "", 1, startat: 1); - var newEntity = new EntityBuilder(universe, $"{type.Namespace}.{name}") - // NOTE: Modules should not be accessed by symbol. - // (Could collide with component symbols.) - // { Symbol = symbol } - .Disabled().Build(); - universe.RegisterLookup(type, newEntity); - return newEntity; - } - - // Create an entity for the module that is being registered. - var entity = LookupOrCreateModule(universe, type); - var dependencies = type.GetMultiple() - .Select(attr => LookupOrCreateModule(universe, attr.Target)); - foreach (var dep in dependencies) entity.Add(DependsOn, dep); - entity.Set(new Module(type)); - - // Collect all currently disabled modules. - var disabledModules = new Dictionary(); - _disabledModulesQuery ??= new Query(universe, new( - "DisabledModulesQuery", - Module, - Disabled - )); - foreach (var iter in _disabledModulesQuery) { - var modules = iter.FieldRef(1); - for (var i = 0; i < iter.Count; i++) - disabledModules.Add(iter.Entity(i), modules[i]); - } - - while (true) { - - // Collect all modules that can now be enabled. - var modulesToEnable = new Dictionary(disabledModules); - _disabledModulesWithDisabledDepsRule ??= new Rule(universe, new( - "DisabledModulesWithDisabledDepsQuery", - Module, - Disabled, - new(DependsOn, "$Dependency"), - new(Disabled) { Source = "$Dependency" } - )); - foreach (var iter in _disabledModulesWithDisabledDepsRule) - for (var i = 0; i < iter.Count; i++) - modulesToEnable.Remove(iter.Entity(i)); - - if (modulesToEnable.Count == 0) break; - - foreach (var (e, c) in modulesToEnable) { - var module = new EntityRef(universe, e); - c.Instance = EnableModule(module, c.Type); - disabledModules.Remove(e); - } - - } - - return entity; - } - - private static object EnableModule(EntityRef module, Type type) - { - var instance = (type.GetConstructor(Type.EmptyTypes) != null) - ? Activator.CreateInstance(type)! - : Activator.CreateInstance(type, module.Universe)!; - - foreach (var member in type.GetNestedTypes().Concat(type.GetMethods())) - member.GetRegisterableInfo(out var _)? - .Register(module.Universe, instance, member) - .Add(module); - - module.Remove(); - return instance; - } + void Initialize(EntityRef entity); } diff --git a/src/gaemstone/ECS/Observer.cs b/src/gaemstone/ECS/Observer.cs index 7f6b638..f50bb87 100644 --- a/src/gaemstone/ECS/Observer.cs +++ b/src/gaemstone/ECS/Observer.cs @@ -6,29 +6,11 @@ using static flecs_hub.flecs; namespace gaemstone.ECS; -public static class ObserverEvent -{ - [BuiltIn] public struct OnAdd { } - [BuiltIn] public struct OnRemove { } - [BuiltIn] public struct OnSet { } - [BuiltIn] public struct UnSet { } - // [BuiltIn] public struct OnDelete { } - // [BuiltIn] public struct OnCreateTable { } - // [BuiltIn] public struct OnDeleteTable { } - [BuiltIn] public struct OnTableEmpty { } - [BuiltIn] public struct OnTableFilled { } - // [BuiltIn] public struct OnCreateTrigger { } - // [BuiltIn] public struct OnDeleteTrigger { } - // [BuiltIn] public struct OnDeleteObservable { } - // [BuiltIn] public struct OnComponentHooks { } - // [BuiltIn] public struct OnDeleteTarget { } -} - [AttributeUsage(AttributeTargets.Method)] public class ObserverAttribute : Attribute { public Type Event { get; } - public string? Expression { get; } + public string? Expression { get; set; } public ObserverAttribute(Type @event) => Event = @event; } @@ -41,9 +23,9 @@ public static class ObserverExtensions filter.Name = name; var desc = new ecs_observer_desc_t { filter = filter.ToFlecs(), - entity = universe.Create(name), - binding_ctx = (void*)CallbackContextHelper.Create(universe, callback), - callback = new() { Data = new() { Pointer = &CallbackContextHelper.Callback } }, + entity = universe.New(name).Build(), + binding_ctx = (void*)CallbackContextHelper.Create((universe, callback)), + callback = new() { Data = new() { Pointer = &SystemExtensions.Callback } }, }; desc.events[0] = @event; var entity = ecs_observer_init(universe, &desc); @@ -61,8 +43,10 @@ public static class ObserverExtensions var param = method.GetParameters(); if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) { filter.Expression = attr.Expression ?? throw new Exception( - "Observer must specify expression in ObserverAttribute"); - iterAction = (Action)Delegate.CreateDelegate(typeof(Action), instance, method); + "Observer must specify ObserverAttribute.Expression"); + if (method.IsStatic) instance = null; + iterAction = (Action)Delegate.CreateDelegate( + typeof(Action), instance, method); } else { var gen = IterActionGenerator.GetOrBuild(universe, method); if (attr.Expression == null) filter.Terms = gen.Terms; @@ -70,7 +54,7 @@ public static class ObserverExtensions iterAction = iter => gen.RunWithTryCatch(instance, iter); } - var @event = universe.Lookup(attr.Event); + var @event = universe.LookupOrThrow(attr.Event); return universe.RegisterObserver(method.Name, @event, filter, iterAction); } } diff --git a/src/gaemstone/ECS/Registerable.cs b/src/gaemstone/ECS/Registerable.cs deleted file mode 100644 index 42c4b14..0000000 --- a/src/gaemstone/ECS/Registerable.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using gaemstone.Utility; - -namespace gaemstone.ECS; - -// TODO: Make this return an EntityBuilder instead. -public delegate EntityRef RegisterFunc(Universe universe, object? instance, MemberInfo member); - -public class RegisterableInfo -{ - public bool? PartOfModule { get; } // true/false = must (not) be part of module; null = doesn't matter - public RegisterFunc Register { get; } - public Type[] AllowedWith { get; } - - public RegisterableInfo(bool? partOfModule, RegisterFunc register, params Type[] allowedWith) - { PartOfModule = partOfModule; Register = register; AllowedWith = allowedWith; } -} - -public static class RegisterableExtensions -{ - // Ordered by priority. For example a type with [Component, Relation] - // will result in a Relation due to being first in the list. - private static readonly Dictionary _knownAttributes = new() { - [typeof(RelationAttribute) ] = new(null, (u, i, m) => u.RegisterRelation ((Type)m), typeof(ComponentAttribute), typeof(TagAttribute)), - [typeof(ComponentAttribute)] = new(null, (u, i, m) => u.RegisterComponent((Type)m), typeof(EntityAttribute)), - [typeof(TagAttribute) ] = new(null, (u, i, m) => u.RegisterTag ((Type)m)), - [typeof(EntityAttribute) ] = new(null, (u, i, m) => u.RegisterEntity ((Type)m)), - - [typeof(ModuleAttribute) ] = new(false, (u, i, m) => u.RegisterModule((Type)m)), - [typeof(SystemAttribute) ] = new(true , (u, i, m) => u.RegisterSystem (i, (MethodInfo)m)), - [typeof(ObserverAttribute)] = new(true , (u, i, m) => u.RegisterObserver(i, (MethodInfo)m)), - }; - - public static RegisterableInfo? GetRegisterableInfo(this MemberInfo member, out bool isPartOfModule) - { - isPartOfModule = member.DeclaringType?.Has() == true; - var matched = _knownAttributes.Where(e => member.GetCustomAttribute(e.Key) != null).ToList(); - - if (matched.Count == 0) return null; - var (type, info) = matched[0]; - - var disallowed = matched.Skip(1).Select(a => a.Key).Except(info.AllowedWith); - if (disallowed.Any()) throw new InvalidOperationException( - $"{member} marked with {type} may not be used together with " + string.Join(", ", disallowed)); - - if (info.PartOfModule == true && !isPartOfModule) throw new InvalidOperationException( - $"{member} marked with {type} must be part of a module"); - if (info.PartOfModule == false && isPartOfModule) throw new InvalidOperationException( - $"{member} marked with {type} must not be part of a module"); - - return info; - } -} diff --git a/src/gaemstone/ECS/Relation.cs b/src/gaemstone/ECS/Relation.cs deleted file mode 100644 index c9be0a7..0000000 --- a/src/gaemstone/ECS/Relation.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace gaemstone.ECS; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -public class RelationAttribute : Attribute { } - -public static class RelationExtensions -{ - public static EntityRef RegisterRelation(this Universe universe) - => universe.RegisterRelation(typeof(T)); - public unsafe static EntityRef RegisterRelation(this Universe universe, Type type) - => throw new NotImplementedException(); // TODO: Implement me. -} diff --git a/src/gaemstone/ECS/System.cs b/src/gaemstone/ECS/System.cs index b357fc2..eaf4655 100644 --- a/src/gaemstone/ECS/System.cs +++ b/src/gaemstone/ECS/System.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using System.Runtime.InteropServices; using gaemstone.Utility; using gaemstone.Utility.IL; using static flecs_hub.flecs; @@ -24,12 +25,12 @@ public static class SystemExtensions query.Name = name; var desc = new ecs_system_desc_t { query = query.ToFlecs(), - entity = new EntityBuilder(universe, name) - .Add(phase) + entity = universe.New(name) + .Add(phase) .Add(phase) .Build(), - binding_ctx = (void*)CallbackContextHelper.Create(universe, callback), - callback = new() { Data = new() { Pointer = &CallbackContextHelper.Callback } }, + binding_ctx = (void*)CallbackContextHelper.Create((universe, callback)), + callback = new() { Data = new() { Pointer = &Callback } }, }; var entity = ecs_system_init(universe, &desc); return new(universe, new(entity)); @@ -42,7 +43,7 @@ public static class SystemExtensions if (action is Action iterAction) { query.Expression = attr?.Expression ?? throw new ArgumentException( - "System must specify expression in SystemAttribute", nameof(action)); + "System must specify SystemAttribute.Expression", nameof(action)); } else { var method = action.GetType().GetMethod("Invoke")!; var gen = IterActionGenerator.GetOrBuild(universe, method); @@ -51,7 +52,7 @@ public static class SystemExtensions iterAction = iter => gen.RunWithTryCatch(action.Target, iter); } - var phase = universe.Lookup(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); + var phase = universe.LookupOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); return universe.RegisterSystem(action.Method.Name, phase, query, iterAction); } @@ -65,7 +66,7 @@ public static class SystemExtensions var param = method.GetParameters(); if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) { query.Expression = attr?.Expression ?? throw new ArgumentException( - "System must specify expression in SystemAttribute", nameof(method)); + "System must specify SystemAttribute.Expression", nameof(method)); iterAction = (Action)Delegate.CreateDelegate(typeof(Action), instance, method); } else { var gen = IterActionGenerator.GetOrBuild(universe, method); @@ -74,7 +75,15 @@ public static class SystemExtensions iterAction = iter => gen.RunWithTryCatch(instance, iter); } - var phase = universe.Lookup(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); + var phase = universe.LookupOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); return universe.RegisterSystem(method.Name, phase, query, iterAction); } + + [UnmanagedCallersOnly] + internal static unsafe void Callback(ecs_iter_t* iter) + { + var (universe, callback) = CallbackContextHelper + .Get<(Universe, Action)>((nint)iter->binding_ctx); + callback(new Iterator(universe, null, *iter)); + } } diff --git a/src/gaemstone/ECS/Tag.cs b/src/gaemstone/ECS/Tag.cs deleted file mode 100644 index 0249cbb..0000000 --- a/src/gaemstone/ECS/Tag.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using gaemstone.Utility; - -namespace gaemstone.ECS; - -[AttributeUsage(AttributeTargets.Struct)] -public class TagAttribute : Attribute { } - -public static class TagExtensions -{ - public static EntityRef RegisterTag(this Universe universe) - where T : unmanaged => universe.RegisterTag(typeof(T)); - public static EntityRef RegisterTag(this Universe universe, Type type) - { - if (!type.IsValueType || type.IsPrimitive || type.GetFields().Length > 0) - throw new Exception("Tag must be an empty, used-defined struct."); - var name = type.GetFriendlyName(); - var entity = new EntityBuilder(universe, name) { Symbol = name } .Build(); - entity.Add(); - universe.RegisterLookup(type, entity); - return entity; - } -} diff --git a/src/gaemstone/ECS/Term.cs b/src/gaemstone/ECS/Term.cs index 8887279..5094244 100644 --- a/src/gaemstone/ECS/Term.cs +++ b/src/gaemstone/ECS/Term.cs @@ -3,34 +3,6 @@ using static flecs_hub.flecs; 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; -} - -[AttributeUsage(AttributeTargets.Parameter)] -public class HasAttribute : Attribute { } - -// Parameters with "in" modifier are equivalent to [In]. -[AttributeUsage(AttributeTargets.Parameter)] -public class InAttribute : Attribute { } - -// Parameters with "out" modifier are equivalent to [Out]. -[AttributeUsage(AttributeTargets.Parameter)] -public class OutAttribute : Attribute { } - -[AttributeUsage(AttributeTargets.Parameter)] -public class NotAttribute : Attribute { } - -// Parameters with nullable syntax are equivalent to [Optional]. -[AttributeUsage(AttributeTargets.Parameter)] -public class OptionalAttribute : Attribute { } - - public class Term { public Identifier ID { get; set; } @@ -43,12 +15,22 @@ public class Term public Term() { } public Term(Identifier id) => ID = id; + public Term(string name) => First = name; public Term(TermID first, TermID second) { First = first; Second = second; } public static implicit operator Term(EntityRef entity) => new(entity); public static implicit operator Term(Entity entity) => new(entity); public static implicit operator Term(IdentifierRef id) => new(id); public static implicit operator Term(Identifier id) => new(id); + public static implicit operator Term(string name) => new(name); + + public Term None { get { InOut = TermInOutKind.None; return this; } } + public Term In { get { InOut = TermInOutKind.In; return this; } } + public Term Out { get { InOut = TermInOutKind.Out; return this; } } + + public Term Or { get { Oper = TermOperKind.Or; return this; } } + public Term Not { get { Oper = TermOperKind.Not; return this; } } + public Term Optional { get { Oper = TermOperKind.Optional; return this; } } public ecs_term_t ToFlecs() => new() { id = ID, diff --git a/src/gaemstone/ECS/TermAttributes.cs b/src/gaemstone/ECS/TermAttributes.cs new file mode 100644 index 0000000..56f9ffc --- /dev/null +++ b/src/gaemstone/ECS/TermAttributes.cs @@ -0,0 +1,31 @@ +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; +} + +// Parameters types marked with [Tag] are equivalent to [Has]. +[AttributeUsage(AttributeTargets.Parameter)] +public class HasAttribute : Attribute { } + +// Parameters with "in" modifier are equivalent to [In]. +[AttributeUsage(AttributeTargets.Parameter)] +public class InAttribute : Attribute { } + +// Parameters with "out" modifier are equivalent to [Out]. +[AttributeUsage(AttributeTargets.Parameter)] +public class OutAttribute : Attribute { } + +[AttributeUsage(AttributeTargets.Parameter)] +public class NotAttribute : Attribute { } + +// Parameters with nullable syntax are equivalent to [Optional]. +[AttributeUsage(AttributeTargets.Parameter)] +public class OptionalAttribute : Attribute { } diff --git a/src/gaemstone/ECS/Universe+Lookup.cs b/src/gaemstone/ECS/Universe+Lookup.cs index 920182d..82f5e92 100644 --- a/src/gaemstone/ECS/Universe+Lookup.cs +++ b/src/gaemstone/ECS/Universe+Lookup.cs @@ -1,62 +1,62 @@ using System; using System.Collections.Generic; -using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; -[AttributeUsage(AttributeTargets.Struct)] -public class BuiltInAttribute : Attribute -{ - public string? Name { get; } - public uint ID { get; } - - internal BuiltInAttribute() { } // Defaults to type's name. - internal BuiltInAttribute(string name) => Name = name; - internal BuiltInAttribute(uint id) => ID = id; -} - public unsafe partial class Universe { private readonly Dictionary _lookupByType = new(); - private void RegisterBuiltInLookups() - { - foreach (var type in typeof(Universe).Assembly.GetTypes()) - if (type.Get() is BuiltInAttribute attr) - RegisterLookup(type, (attr.ID != 0) - ? TryLookup(new Entity(new() { Data = attr.ID })) - : TryLookup("flecs.core." + (attr.Name ?? type.Name))); - } - - public void RegisterLookup(Type type, Entity entity) - => _lookupByType.Add(type, entity.ThrowIfInvalid()); - public void RemoveLookup(Type type) - { if (!_lookupByType.Remove(type)) throw new InvalidOperationException( - $"Type {type} was not present in lookups"); } - - public Entity TryLookup() - => TryLookup(typeof(T)); - public Entity TryLookup(Type type) - => _lookupByType.TryGetValue(type, out var e) ? new(e) : default; - - public Entity TryLookup(Entity value) - => new(ecs_get_alive(this, value)); - public Entity TryLookup(string path) - => TryLookup(default, path); - public Entity TryLookup(Entity parent, string path) - => new(ecs_lookup_path_w_sep(this, parent, path.FlecsToCString(), ".", default, true)); - public Entity TryLookupSymbol(string symbol) - => new(ecs_lookup_symbol(this, symbol.FlecsToCString(), false)); - - - public EntityRef Lookup() => new(this, TryLookup()); - public EntityRef Lookup(Type type) => new(this, TryLookup(type)); - - public EntityRef Lookup(Entity entity) => new(this, TryLookup(entity)); + public void AddLookupByType(Type type, Entity entity) + => _lookupByType.Add(type, entity); + public void RemoveLookupByType(Type type) + { if (!_lookupByType.Remove(type)) throw new InvalidOperationException( + $"Lookup for {type} does not exist"); } + + private EntityRef? GetOrNull(Entity entity) + { var e = new EntityRef(this, entity, false); return e.IsValid ? e : null; } + + public EntityRef? Lookup() + => Lookup(typeof(T)); + public EntityRef? Lookup(Type type) + => Lookup(_lookupByType.GetValueOrDefault(type)); + + public EntityRef? Lookup(Entity value) + => GetOrNull(new(ecs_get_alive(this, value))); + + public EntityRef? Lookup(string path) + => Lookup(default, path); + public EntityRef? Lookup(Entity parent, string path) + => GetOrNull(new(ecs_lookup_path_w_sep( + this, parent, path.FlecsToCString(), ".", default, true))); + public EntityRef? LookupSymbol(string symbol) + => GetOrNull(new(ecs_lookup_symbol( + this, symbol.FlecsToCString(), false))); + + + public EntityRef LookupOrThrow() => LookupOrThrow(typeof(T)); + public EntityRef LookupOrThrow(Type type) => Lookup(type) + ?? throw new EntityNotFoundException($"Entity of type {type} not found"); + public EntityRef LookupOrThrow(Entity entity) => Lookup(entity) + ?? throw new EntityNotFoundException($"Entity {entity} not alive"); + public EntityRef LookupOrThrow(string path) => Lookup(default, path) + ?? throw new EntityNotFoundException($"Entity '{path}' not found"); + public EntityRef LookupOrThrow(Entity parent, string path) => Lookup(parent, path) + ?? throw new EntityNotFoundException($"Child entity of {parent} '{path}' not found"); + public EntityRef LookupSymbolOrThrow(string symbol) => LookupSymbol(symbol) + ?? throw new EntityNotFoundException($"Entity with symbol '{symbol}' not found"); + + + public class EntityNotFoundException : Exception + { public EntityNotFoundException(string message) : base(message) { } } +} - public EntityRef Lookup(string path) => new(this, TryLookup(path)); - public EntityRef Lookup(Entity parent, string path) => new(this, TryLookup(parent, path)); - public EntityRef LookupSymbol(string symbol) => new(this, TryLookupSymbol(symbol)); +public static class LookupExtensions +{ + public static EntityRef CreateLookup(this EntityRef entity) + => entity.CreateLookup(typeof(T)); + public static EntityRef CreateLookup(this EntityRef entity, Type type) + { entity.Universe.AddLookupByType(type, entity); return entity; } } diff --git a/src/gaemstone/ECS/Universe+Modules.cs b/src/gaemstone/ECS/Universe+Modules.cs new file mode 100644 index 0000000..39acdf7 --- /dev/null +++ b/src/gaemstone/ECS/Universe+Modules.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using gaemstone.Utility; +using static gaemstone.ECS.BuiltIn; + +namespace gaemstone.ECS; + +public class ModuleManager +{ + private readonly Dictionary _modules = new(); + + public Universe Universe { get; } + public ModuleManager(Universe universe) + => Universe = universe; + + internal ModuleInfo? Lookup(Entity entity) + => _modules.GetValueOrDefault(entity); + + public EntityRef Register() + where T : class => Register(typeof(T)); + public EntityRef Register(Type type) + { + if (!type.IsClass || type.IsGenericType || type.IsGenericTypeDefinition) throw new Exception( + $"Module {type} must be a non-generic class"); + if (type.Get() is not ModuleAttribute attr) throw new Exception( + $"Module {type} must be marked with ModuleAttribute"); + + var moduleName = type.Get()?.Name ?? type.FullName!; + + // Check if module type is static. + if (type.IsAbstract && type.IsSealed) { + + // Static modules represent existing modules, as such they don't + // create entities, only look up existing ones to add type lookups + // for use with the Lookup(Type) method. + + if (attr.Name == null) throw new Exception( + $"Existing module {type} must have ModuleAttribute.Name set"); + var entity = Universe.Lookup(attr.Name) ?? throw new Exception( + $"Existing module {type} with name '{attr.Name}' 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. + + foreach (var nested in type.GetNestedTypes()) { + if (nested.Get() is not EntityAttribute nestedAttr) continue; + + var name = nestedAttr.Name ?? nested.Name; + if (name.Contains('.')) throw new Exception( + $"EntityAttribute.Name for {type} must not contain a dot (path separator)"); + + Universe.LookupOrThrow($"{moduleName}.{name}") + .CreateLookup(nested); + } + + return entity; + + } else { + + var name = GetModuleName(type); + if (Universe.Lookup(name) != null) throw new Exception( + $"Can't register module {type}: '{type.FullName}' is already in use"); + + var module = new ModuleInfo(Universe, type, name); + _modules.Add(module.Entity, module); + TryEnableModule(module); + + return module.Entity; + + } + } + + private void TryEnableModule(ModuleInfo module) + { + if (module.UnmetDependencies.Count > 0) return; + module.Enable(); + + // Find other modules that might be missing this module as a dependency. + foreach (var other in _modules.Values) { + if (!other.IsActive) continue; + if (!other.UnmetDependencies.Contains(module.Entity)) continue; + + // Move the just enabled module from unmet to met depedencies. + other.UnmetDependencies.Remove(module.Entity); + other.MetDependencies.Add(module); + + TryEnableModule(other); + } + } + + public static string GetModuleName(Type type) + { + var attr = type.Get(); + if (attr == null) throw new ArgumentException( + $"Module {type} must be marked with ModuleAttribute", nameof(type)); + return attr.Name ?? type.FullName!; + } +} + +internal class ModuleInfo +{ + public Universe Universe { get; } + public Type ModuleType { get; } + public string ModuleName { get; } + + public EntityRef Entity { get; } + public object? Instance { get; internal set; } + public bool IsActive => Instance != null; + + public HashSet MetDependencies { get; } = new(); + public HashSet UnmetDependencies { get; } = new(); + + public ModuleInfo(Universe universe, Type type, string name) + { + Universe = universe; + ModuleType = type; + ModuleName = name; + + if (ModuleType.IsAbstract || ModuleType.IsSealed) throw new Exception( + $"Module {ModuleType} must not be abstract or sealed"); + if (ModuleType.GetConstructor(Type.EmptyTypes) == null) throw new Exception( + $"Module {ModuleType} must define public parameterless constructor"); + + var entity = Universe.New(name).Add(); + + // Add module dependencies from [DependsOn] attributes. + foreach (var dependsAttr in ModuleType.GetMultiple()) { + var dependency = Universe.Lookup(dependsAttr.Name) ?? + Universe.New(dependsAttr.Name).Add().Disable().Build(); + + var depModule = Universe.Modules.Lookup(dependency); + if (depModule?.IsActive == true) MetDependencies.Add(depModule); + else { UnmetDependencies.Add(dependency); entity.Disable(); } + + entity.Add(dependency); + } + + Entity = entity.Build().CreateLookup(type); + } + + public void Enable() + { + Entity.Enable(); + Instance = Activator.CreateInstance(ModuleType)!; + RegisterNestedTypes(); + (Instance as IModuleInitializer)?.Initialize(Entity); + RegisterMethods(Instance); + } + + private void RegisterNestedTypes() + { + foreach (var type in ModuleType.GetNestedTypes()) { + if (type.Get() is not EntityAttribute attr) continue; + var name = attr.Name ?? type.Name; + var entity = Universe.New($"{ModuleName}.{name}", name); + switch (attr) { + + 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; + + case ComponentAttribute: + entity.Build().CreateComponent(type); + break; + + 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; + + } + } + } + + private void RegisterMethods(object? instance) + { + foreach (var method in ModuleType.GetMethods()) { + if (method.Has()) + Universe.RegisterSystem(instance, method).ChildOf(Entity); + if (method.Has()) + Universe.RegisterObserver(instance, method).ChildOf(Entity); + } + } +} diff --git a/src/gaemstone/ECS/Universe.cs b/src/gaemstone/ECS/Universe.cs index 23b9881..eec9d93 100644 --- a/src/gaemstone/ECS/Universe.cs +++ b/src/gaemstone/ECS/Universe.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using static flecs_hub.flecs; using static flecs_hub.flecs.Runtime; @@ -7,6 +8,9 @@ namespace gaemstone.ECS; public unsafe partial class Universe { public ecs_world_t* Handle { get; } + public ModuleManager Modules { get; } + + public bool IsDeferred => ecs_is_deferred(this); public Universe(params string[] args) { @@ -14,9 +18,38 @@ public unsafe partial class Universe Handle = ecs_init_w_args(args.Length, argv); CStrings.FreeCStrings(argv, args.Length); - RegisterBuiltInLookups(); - this.RegisterEntity(); - this.RegisterComponent(); + Modules = new(this); + Modules.Register(typeof(BuiltIn)); + Modules.Register(typeof(ObserverEvent)); + Modules.Register(typeof(SystemPhase)); + + New("Game", "Game").Build().CreateLookup(); + } + + public EntityBuilder New(string? name = null, string? symbol = null) + => new(this, name, symbol); + + // TODO: Move to module. + public void EnableRest(ushort port = 27750) + { + [UnmanagedCallersOnly] + static void RestImport(ecs_world_t* world) + => FlecsRestImport(world); + + ecs_import(this, new() { Data = new() { Pointer = &RestImport } }, "FlecsRest"); + + var rest = LookupOrThrow("flecs.rest.Rest"); + _lookupByType.Add(typeof(EcsRest), rest); + rest.Set(new EcsRest { port = port }); + } + + public void EnableMonitor() + { + [UnmanagedCallersOnly] + static void MonitorImport(ecs_world_t* world) + => FlecsMonitorImport(world); + + ecs_import(this, new() { Data = new() { Pointer = &MonitorImport } }, "FlecsMonitor"); } public bool Progress(TimeSpan delta) diff --git a/src/gaemstone/Utility/CStringExtensions.cs b/src/gaemstone/Utility/CStringExtensions.cs index 2d5b9b5..f7735ef 100644 --- a/src/gaemstone/Utility/CStringExtensions.cs +++ b/src/gaemstone/Utility/CStringExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.InteropServices; using static flecs_hub.flecs; using static flecs_hub.flecs.Runtime; @@ -6,9 +7,13 @@ namespace gaemstone; internal static unsafe class CStringExtensions { + // FIXME: Most if not all strings passed to flecs probably need to be freed. public static CString FlecsToCString(this string? str) => (str != null) ? new(Marshal.StringToHGlobalAnsi(str)) : default; + public static AutoFreeCString FlecsToCStringThenFree(this string? str) + => new(str); + public static void FlecsFree(this CString str) { if (!str.IsNull) ecs_os_get_api().free_.Data.Pointer((void*)(nint)str); } @@ -18,4 +23,22 @@ internal static unsafe class CStringExtensions public static string? FlecsToStringAndFree(this CString str) { var result = str.FlecsToString(); str.FlecsFree(); return result; } + + + public class AutoFreeCString + : IDisposable + { + private readonly CString _cString; + public AutoFreeCString(string? str) + => _cString = str.FlecsToCString(); + + ~AutoFreeCString() => Dispose(); + public void Dispose() + { + if (!_cString.IsNull) Marshal.FreeHGlobal((nint)_cString); + GC.SuppressFinalize(this); + } + + public static implicit operator CString(AutoFreeCString str) => str._cString; + } } diff --git a/src/gaemstone/Utility/CallbackContextHelper.cs b/src/gaemstone/Utility/CallbackContextHelper.cs index 0140f9f..4bca09d 100644 --- a/src/gaemstone/Utility/CallbackContextHelper.cs +++ b/src/gaemstone/Utility/CallbackContextHelper.cs @@ -1,43 +1,14 @@ -using System; -using System.Runtime.InteropServices; -using gaemstone.ECS; -using static flecs_hub.flecs; +using System.Collections.Generic; namespace gaemstone.Utility; public static class CallbackContextHelper { - public readonly struct CallbackContext - { - public Universe Universe { get; } - public Action Callback { get; } + private static readonly List _contexts = new(); - public CallbackContext(Universe universe, Action callback) - { Universe = universe; Callback = callback; } - } + public static nint Create(T context) where T : notnull + { _contexts.Add(context); return _contexts.Count; } - private static readonly object _lock = new(); - private static CallbackContext[] _contexts = new CallbackContext[64]; - private static int _count = 0; - - public static nint Create(Universe universe, Action callback) - { - var data = new CallbackContext(universe, callback); - lock (_lock) { - if (++_count >= _contexts.Length) - Array.Resize(ref _contexts, _count * 2); - _contexts[_count - 1] = data; - return _count - 1; - } - } - - public static CallbackContext Get(nint context) - => _contexts[(int)context]; - - [UnmanagedCallersOnly] - internal static unsafe void Callback(ecs_iter_t* iter) - { - var data = Get((nint)iter->binding_ctx); - data.Callback(new Iterator(data.Universe, null, *iter)); - } + public static T Get(nint context) + => (T)_contexts[(int)context - 1]; } diff --git a/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs b/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs index d673fa3..5f35033 100644 --- a/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs +++ b/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs @@ -98,8 +98,12 @@ public class ILGeneratorWrapper private void AddInstr(OpCode code, object? arg = null) => _instructions.Add((_il.ILOffset, _indents.Count, code, arg)); public void Comment(string comment) => _instructions.Add((-1, _indents.Count, OpCodes.Nop, comment)); + private static readonly MethodInfo _writeLine = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) })!; + public void Log(string str) { Load(str); Call(_writeLine); } + internal void Emit(OpCode code) { AddInstr(code, null); _il.Emit(code); } internal void Emit(OpCode code, int arg) { AddInstr(code, arg); _il.Emit(code, arg); } + internal void Emit(OpCode code, string arg) { AddInstr(code, arg); _il.Emit(code, arg); } internal void Emit(OpCode code, Type type) { AddInstr(code, type); _il.Emit(code, type); } internal void Emit(OpCode code, Label label) { AddInstr(code, label); _il.Emit(code, label); } internal void Emit(OpCode code, ILocal local) { AddInstr(code, local); _il.Emit(code, local.Builder); } @@ -107,8 +111,11 @@ public class ILGeneratorWrapper internal void Emit(OpCode code, MethodInfo method) { AddInstr(code, method); _il.Emit(code, method); } internal void Emit(OpCode code, ConstructorInfo constr) { AddInstr(code, constr); _il.Emit(code, constr); } + public void Dup() => Emit(OpCodes.Dup); + public void LoadNull() => Emit(OpCodes.Ldnull); public void LoadConst(int value) => Emit(OpCodes.Ldc_I4, value); + public void Load(string value) => Emit(OpCodes.Ldstr, value); public void Load(IArgument arg) => Emit(OpCodes.Ldarg, arg); public void LoadAddr(IArgument arg) => Emit(OpCodes.Ldarga, arg); @@ -175,6 +182,8 @@ public class ILGeneratorWrapper public void Cast(Type type) => Emit(OpCodes.Castclass, type); public void Cast() => Cast(typeof(T)); + public void Box(Type type) => Emit(OpCodes.Box, type); + public void Box() => Box(typeof(T)); public void Goto(Label label) => Emit(OpCodes.Br, label); public void GotoIfTrue(Label label) => Emit(OpCodes.Brtrue, label); diff --git a/src/gaemstone/Utility/IL/IterActionGenerator.cs b/src/gaemstone/Utility/IL/IterActionGenerator.cs index 241f132..0f3120b 100644 --- a/src/gaemstone/Utility/IL/IterActionGenerator.cs +++ b/src/gaemstone/Utility/IL/IterActionGenerator.cs @@ -58,8 +58,8 @@ public unsafe class IterActionGenerator 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"); + // if (!Parameters.Any(c => c.IsRequired && (c.Kind != ParamKind.Unique))) + // throw new ArgumentException($"At least one parameter in {method} is required"); var terms = new List(); var name = "<>Query_" + string.Join("_", Parameters.Select(p => p.UnderlyingType.Name)); @@ -77,8 +77,8 @@ public unsafe class IterActionGenerator if (p.Kind == ParamKind.Unique) continue; // Add an entry to the terms to look for this type. - terms.Add(new(universe.Lookup(p.UnderlyingType)) { - Source = p.Source != null ? Universe.Lookup(p.Source) : null, + terms.Add(new(universe.LookupOrThrow(p.UnderlyingType)) { + Source = (p.Source != null) ? (TermID)Universe.LookupOrThrow(p.Source) : null, InOut = p.Kind switch { ParamKind.In => TermInOutKind.In, ParamKind.Out => TermInOutKind.Out, @@ -93,7 +93,7 @@ public unsafe class IterActionGenerator }); // Create a Span local and initialize it to iterator.Field(i). - var spanType = typeof(Span<>).MakeGenericType(p.FieldType); + 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 @@ -242,8 +242,8 @@ public unsafe class IterActionGenerator // Reference types have a backing type of nint - they're pointers. FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint); - // If the underlying type has EntityAttribute, it's a singleton. - if (UnderlyingType.Has()) Source = underlyingType; + // FIXME: Reimplement singletons somehow. + // if (UnderlyingType.Has()) Source = underlyingType; if (Info.Get() is SourceAttribute attr) Source = attr.Type; // TODO: Needs support for the new attributes. } diff --git a/src/gaemstone/Utility/SpanToRef.cs b/src/gaemstone/Utility/SpanToRef.cs new file mode 100644 index 0000000..b9f169b --- /dev/null +++ b/src/gaemstone/Utility/SpanToRef.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.InteropServices; + +namespace gaemstone.Utility; + +public readonly ref struct SpanToRef + where T : class +{ + private readonly Span _span; + + public int Length => _span.Length; + public T? this[int index] => (_span[index] != 0) + ? (T)((GCHandle)_span[index]).Target! : null; + + internal SpanToRef(Span span) => _span = span; + + public void Clear() => _span.Clear(); + public void CopyTo(SpanToRef dest) => _span.CopyTo(dest._span); +} diff --git a/src/gaemstone/Utility/TypeWrapper.cs b/src/gaemstone/Utility/TypeWrapper.cs index af44813..daade3e 100644 --- a/src/gaemstone/Utility/TypeWrapper.cs +++ b/src/gaemstone/Utility/TypeWrapper.cs @@ -31,7 +31,7 @@ public interface IFieldWrapper public static class TypeWrapper { - static readonly Dictionary _typeCache = new(); + private static readonly Dictionary _typeCache = new(); public static TypeWrapper For() => TypeWrapper.Instance; @@ -49,14 +49,13 @@ public class TypeWrapper : ITypeWrapper { internal static TypeWrapper Instance { get; } = new(); - readonly Dictionary _fieldCache = new(); + private readonly Dictionary _fieldCache = new(); public Type Type => typeof(TType); - public int Size { get; } = Unsafe.SizeOf(); public bool IsUnmanaged { get; } = !RuntimeHelpers.IsReferenceOrContainsReferences(); - TypeWrapper() { } + private TypeWrapper() { } IFieldWrapper ITypeWrapper.GetFieldForAutoProperty(string propertyName) => GetFieldForAutoProperty(propertyName); IFieldWrapper ITypeWrapper.GetFieldForAutoProperty(PropertyInfo property) => GetFieldForAutoProperty(property); @@ -125,7 +124,7 @@ public class TypeWrapper : ITypeWrapper } - IFieldWrapperForType GetField(FieldInfo field, PropertyInfo? property) + private IFieldWrapperForType GetField(FieldInfo field, PropertyInfo? property) { if (_fieldCache.TryGetValue(field, out var cached)) return cached; var type = typeof(FieldWrapper<>).MakeGenericType(typeof(TType), field.FieldType); @@ -136,7 +135,7 @@ public class TypeWrapper : ITypeWrapper return wrapper; } - FieldWrapper GetField(FieldInfo field, PropertyInfo? property) + private FieldWrapper GetField(FieldInfo field, PropertyInfo? property) { if (_fieldCache.TryGetValue(field, out var cached)) return (FieldWrapper)cached; if (field.FieldType != typeof(TField)) throw new ArgumentException( @@ -166,10 +165,10 @@ public class TypeWrapper : ITypeWrapper public delegate TField ValueGetterAction(in TType obj); public delegate void ValueSetterAction(ref TType obj, TField value); - Func? _classGetter; - Action? _classSetter; - ValueGetterAction? _byRefGetter; - ValueSetterAction? _byRefSetter; + private Func? _classGetter; + private Action? _classSetter; + private ValueGetterAction? _byRefGetter; + private ValueSetterAction? _byRefSetter; public ITypeWrapper DeclaringType { get; } public FieldInfo FieldInfo { get; } @@ -189,7 +188,7 @@ public class TypeWrapper : ITypeWrapper public ValueSetterAction ByRefSetter => _byRefSetter ??= BuildSetter(true); - TDelegate BuildGetter(bool byRef) + private TDelegate BuildGetter(bool byRef) where TDelegate : Delegate { if (DeclaringType.Type.IsValueType && !byRef) throw new InvalidOperationException( @@ -207,7 +206,7 @@ public class TypeWrapper : ITypeWrapper return method.CreateDelegate(); } - TDelegate BuildSetter(bool byRef) + private TDelegate BuildSetter(bool byRef) where TDelegate : Delegate { if (DeclaringType.Type.IsValueType && !byRef) throw new InvalidOperationException(