diff --git a/src/Immersion/ObserverTest.cs b/src/Immersion/ObserverTest.cs index db1abb4..ebaf740 100644 --- a/src/Immersion/ObserverTest.cs +++ b/src/Immersion/ObserverTest.cs @@ -1,14 +1,14 @@ using System; -using gaemstone.Bloxel; -using gaemstone.Client; using gaemstone.ECS; +using static gaemstone.Bloxel.Components.CoreComponents; +using static gaemstone.Client.Components.RenderingComponents; namespace Immersion; [Module] -public class ObserverModule +public class ObserverTest { - [Observer(ObserverEvent.OnSet)] + [Observer(typeof(ObserverEvent.OnSet))] public static void DoObserver(in Chunk chunk, in Mesh _) => Console.WriteLine($"Chunk at {chunk.Position} now has a Mesh!"); } diff --git a/src/Immersion/Program.cs b/src/Immersion/Program.cs index e05756b..63a928c 100644 --- a/src/Immersion/Program.cs +++ b/src/Immersion/Program.cs @@ -1,5 +1,6 @@ using System; -using gaemstone; +using System.Diagnostics; +using System.Threading; using gaemstone.Bloxel; using gaemstone.Client; using gaemstone.ECS; @@ -7,50 +8,44 @@ using gaemstone.Utility; using Silk.NET.Maths; using Silk.NET.OpenGL; using Silk.NET.Windowing; -using static flecs_hub.flecs; -using static gaemstone.Client.CameraComponents; -using static gaemstone.Client.FreeCameraController; -using static gaemstone.Client.Input; -using static gaemstone.Client.Windowing; +using static gaemstone.Bloxel.Components.CoreComponents; +using static gaemstone.Client.Components.CameraComponents; +using static gaemstone.Client.Components.RenderingComponents; +using static gaemstone.Client.Systems.FreeCameraController; +using static gaemstone.Client.Systems.Input; +using static gaemstone.Client.Systems.Windowing; +using static gaemstone.Components.TransformComponents; + +FlecsAbortException.SetupHook(); +Resources.ResourceAssembly = typeof(Program).Assembly; var universe = new Universe(); var game = universe.Lookup(); -Resources.ResourceAssembly = typeof(Program).Assembly; var window = Window.Create(WindowOptions.Default with { Title = "gæmstone", Size = new(1280, 720), - FramesPerSecond = 60.0, PreferredDepthBufferBits = 24, }); window.Initialize(); window.Center(); -universe.RegisterModule(); - +universe.RegisterModule(); game.Set(new Canvas(window.CreateOpenGL())); game.Set(new GameWindow(window)); -TextureManager.Initialize(universe); +universe.RegisterModule(); -universe.RegisterComponent(); -universe.RegisterComponent(); -universe.RegisterComponent(); +universe.RegisterModule(); +universe.RegisterModule(); -universe.RegisterModule(); -universe.RegisterModule(); - -universe.RegisterModule(); -universe.RegisterModule(); +TextureManager.Initialize(universe); +universe.RegisterModule(); game.Set(new RawInput()); -// TODO: Find a way to automatically register chunk storage. -universe.RegisterComponent>(); -universe.RegisterAll(typeof(Chunk).Assembly); - -universe.RegisterAll(typeof(Program).Assembly); - +universe.RegisterModule(); +universe.RegisterModule(); universe.Create("MainCamera") .Set(Camera.Default3D) .Set((GlobalTransform) Matrix4X4.CreateTranslation(0.0F, 2.0F, 0.0F)) @@ -69,6 +64,10 @@ for (var z = -12; z <= 12; z++) { .Set(rnd.Pick(heartMesh, swordMesh)); } +universe.RegisterModule(); +universe.RegisterModule(); +universe.RegisterModule(); + var texture = TextureManager.Load(universe, "terrain.png"); var stone = universe.Create("Stone").Set(TextureCoords4.FromGrid(4, 4, 1, 0)); @@ -81,7 +80,7 @@ for (var cx = -sizeH; cx < sizeH; cx++) 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 ChunkPaletteStorage(default); + var storage = new ChunkStoreBlocks(); universe.Create() .Set((GlobalTransform)Matrix4X4.CreateTranslation(pos.GetOrigin())) .Set(new Chunk(pos)) @@ -89,8 +88,19 @@ for (var cz = -sizeH; cz < sizeH; cz++) { .Set(texture); } -window.Render += (delta) => { - if (!universe.Progress(TimeSpan.FromSeconds(delta))) +// universe.RegisterModule(); + +var stopwatch = Stopwatch.StartNew(); +var minFrameTime = TimeSpan.FromSeconds(1) / 30; +window.Run(() => { + var delta = stopwatch.Elapsed; + stopwatch.Restart(); + + if (!universe.Progress(delta)) window.Close(); -}; -window.Run(); + + var requiredTime = stopwatch.Elapsed; + while (stopwatch.Elapsed < minFrameTime) Thread.Sleep(0); + var totalTime = stopwatch.Elapsed; + // Console.WriteLine($"Frame time: req={requiredTime.TotalMilliseconds:F2}ms, total={totalTime.TotalMilliseconds:F2}ms"); +}); diff --git a/src/flecs-cs b/src/flecs-cs index 1e36559..f5eea67 160000 --- a/src/flecs-cs +++ b/src/flecs-cs @@ -1 +1 @@ -Subproject commit 1e36559cffa5ab2fb755feef563c4294a6f32b0c +Subproject commit f5eea6704075601a674e3d759fdadc306b75573d diff --git a/src/gaemstone.Bloxel/ChunkPaletteStorage.cs b/src/gaemstone.Bloxel/ChunkPaletteStorage.cs index 48527e1..f11ab3e 100644 --- a/src/gaemstone.Bloxel/ChunkPaletteStorage.cs +++ b/src/gaemstone.Bloxel/ChunkPaletteStorage.cs @@ -2,13 +2,11 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using gaemstone.ECS; namespace gaemstone.Bloxel; // Based on "Palette-based compression for chunked discrete voxel data" by /u/Longor1996 // https://www.reddit.com/r/VoxelGameDev/comments/9yu8qy/palettebased_compression_for_chunked_discrete/ -[Component] public class ChunkPaletteStorage { const int Size = 16 * 16 * 16; diff --git a/src/gaemstone.Bloxel/ChunkPos.cs b/src/gaemstone.Bloxel/ChunkPos.cs index 4f1b193..1234afe 100644 --- a/src/gaemstone.Bloxel/ChunkPos.cs +++ b/src/gaemstone.Bloxel/ChunkPos.cs @@ -1,5 +1,6 @@ using System; using Silk.NET.Maths; +using static gaemstone.Bloxel.Constants; namespace gaemstone.Bloxel; @@ -16,11 +17,11 @@ public readonly struct ChunkPos public void Deconstruct(out int x, out int y, out int z) => (x, y, z) = (X, Y, Z); public Vector3D GetOrigin() => new( - X << Chunk.BIT_SHIFT, Y << Chunk.BIT_SHIFT, Z << Chunk.BIT_SHIFT); + X << ChunkBitShift, Y << ChunkBitShift, Z << ChunkBitShift); public Vector3D GetCenter() => new( - (X << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2, - (Y << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2, - (Z << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2); + (X << ChunkBitShift) + ChunkLength / 2, + (Y << ChunkBitShift) + ChunkLength / 2, + (Z << ChunkBitShift) + ChunkLength / 2); public ChunkPos Add(int x, int y, int z) @@ -66,16 +67,16 @@ public readonly struct ChunkPos public static class ChunkPosExtensions { public static ChunkPos ToChunkPos(this Vector3D pos) => new( - (int)MathF.Floor(pos.X) >> Chunk.BIT_SHIFT, - (int)MathF.Floor(pos.Y) >> Chunk.BIT_SHIFT, - (int)MathF.Floor(pos.Z) >> Chunk.BIT_SHIFT); + (int)MathF.Floor(pos.X) >> ChunkBitShift, + (int)MathF.Floor(pos.Y) >> ChunkBitShift, + (int)MathF.Floor(pos.Z) >> ChunkBitShift); public static ChunkPos ToChunkPos(this BlockPos self) => new( - self.X >> Chunk.BIT_SHIFT, self.Y >> Chunk.BIT_SHIFT, self.Z >> Chunk.BIT_SHIFT); + self.X >> ChunkBitShift, self.Y >> ChunkBitShift, self.Z >> ChunkBitShift); public static BlockPos ToChunkRelative(this BlockPos self) => new( - self.X & Chunk.BIT_MASK, self.Y & Chunk.BIT_MASK, self.Z & Chunk.BIT_MASK); + self.X & ChunkBitMask, self.Y & ChunkBitMask, self.Z & ChunkBitMask); public static BlockPos ToChunkRelative(this BlockPos self, ChunkPos chunk) => new( - self.X - (chunk.X << Chunk.BIT_SHIFT), - self.Y - (chunk.Y << Chunk.BIT_SHIFT), - self.Z - (chunk.Z << Chunk.BIT_SHIFT)); + self.X - (chunk.X << ChunkBitShift), + self.Y - (chunk.Y << ChunkBitShift), + self.Z - (chunk.Z << ChunkBitShift)); } diff --git a/src/gaemstone.Bloxel/Client/ChunkMeshGenerator.cs b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs similarity index 81% rename from src/gaemstone.Bloxel/Client/ChunkMeshGenerator.cs rename to src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs index f5b1f99..811703d 100644 --- a/src/gaemstone.Bloxel/Client/ChunkMeshGenerator.cs +++ b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs @@ -2,10 +2,11 @@ using System; using gaemstone.Client; using gaemstone.ECS; using Silk.NET.Maths; -using static flecs_hub.flecs; -using static gaemstone.Bloxel.WorldGen.BasicWorldGenerator; +using static gaemstone.Bloxel.Components.CoreComponents; +using static gaemstone.Bloxel.Systems.BasicWorldGenerator; +using static gaemstone.Client.Components.RenderingComponents; -namespace gaemstone.Bloxel.Client; +namespace gaemstone.Bloxel.Client.Systems; [Module] public class ChunkMeshGenerator @@ -31,36 +32,37 @@ public class ChunkMeshGenerator private Vector2D[] _uvs = new Vector2D[StartingCapacity]; [System] - public void GenerateChunkMeshes(Universe universe, Entity entity, - in Chunk chunk, ChunkPaletteStorage storage, + public void GenerateChunkMeshes(Universe universe, EntityRef entity, + in Chunk chunk, ChunkStoreBlocks blocks, HasBasicWorldGeneration _1, [Not] Mesh _2) { - var mesh = Generate(universe, chunk.Position, storage); - if (mesh is Mesh m) entity.Set(m); + var maybeMesh = Generate(universe, chunk.Position, blocks); + if (maybeMesh is Mesh mesh) entity.Set(mesh); else entity.Delete(); } public Mesh? Generate(Universe universe, ChunkPos chunkPos, - ChunkPaletteStorage centerStorage) + ChunkStoreBlocks centerBlocks) { // TODO: We'll need a way to get neighbors again. - // var storages = new ChunkPaletteStorage[3, 3, 3]; + // var storages = new ChunkStoreBlocks[3, 3, 3]; // foreach (var (x, y, z) in Neighbors.ALL.Prepend(Neighbor.None)) // if (_chunkStore.TryGetEntityID(chunkPos.Add(x, y, z), out var neighborID)) // if (_storageStore.TryGet(neighborID, out var storage)) // storages[x+1, y+1, z+1] = storage; // var centerStorage = storages[1, 1, 1]; - var storages = new ChunkPaletteStorage[3, 3, 3]; - storages[1, 1, 1] = centerStorage; + var storages = new ChunkStoreBlocks[3, 3, 3]; + storages[1, 1, 1] = centerBlocks; var indexCount = 0; var vertexCount = 0; for (var x = 0; x < 16; x++) for (var y = 0; y < 16; y++) for (var z = 0; z < 16; z++) { - var block = new Entity(universe, centerStorage[x, y, z]); - if (block.IsNone) continue; + var blockEntity = new Entity(centerBlocks[x, y, z]); + if (!blockEntity.IsValid) continue; + var block = new EntityRef(universe, blockEntity); var blockVertex = new Vector3D(x, y, z); var textureCell = block.Get(); @@ -104,7 +106,7 @@ public class ChunkMeshGenerator } static bool IsNeighborEmpty( - ChunkPaletteStorage[,,] storages, + ChunkStoreBlocks[,,] blocks, int x, int y, int z, BlockFacing facing) { var cx = 1; var cy = 1; var cz = 1; @@ -116,9 +118,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 = storages[cx, cy, cz]; + var neighborChunk = blocks[cx, cy, cz]; if (neighborChunk == null) return true; var neighborBlock = neighborChunk[x & 0b1111, y & 0b1111, z & 0b1111]; - return neighborBlock.Data.Data == 0; + return !neighborBlock.IsValid; } } diff --git a/src/gaemstone.Bloxel/Components/CoreComponents.cs b/src/gaemstone.Bloxel/Components/CoreComponents.cs new file mode 100644 index 0000000..7651739 --- /dev/null +++ b/src/gaemstone.Bloxel/Components/CoreComponents.cs @@ -0,0 +1,22 @@ +using gaemstone.ECS; + +namespace gaemstone.Bloxel.Components; + +[Module] +public partial class CoreComponents +{ + [Component] + public readonly struct Chunk + { + public ChunkPos Position { get; } + public Chunk(ChunkPos pos) => Position = pos; + } + + [Component] + public class ChunkStoreBlocks + : ChunkPaletteStorage + { + public ChunkStoreBlocks() + : base(default) { } + } +} diff --git a/src/gaemstone.Bloxel/Chunk.cs b/src/gaemstone.Bloxel/Constants.cs similarity index 53% rename from src/gaemstone.Bloxel/Chunk.cs rename to src/gaemstone.Bloxel/Constants.cs index b700df9..0a43cb5 100644 --- a/src/gaemstone.Bloxel/Chunk.cs +++ b/src/gaemstone.Bloxel/Constants.cs @@ -1,17 +1,11 @@ -using gaemstone.ECS; - namespace gaemstone.Bloxel; -[Component] -public readonly struct Chunk +public static class Constants { - // Length of the egde of a world chunk. - public const int LENGTH = 16; // Amount of bit shifting to go from a BlockPos to a ChunkPos. - public const int BIT_SHIFT = 4; + public const int ChunkBitShift = 4; // Amount of bit masking to go from a BlockPos to a chunk-relative BlockPos. - public const int BIT_MASK = 0b1111; - - public ChunkPos Position { get; } - public Chunk(ChunkPos pos) => Position = pos; + public const int ChunkBitMask = ~(~0 << ChunkBitShift); + // Length of the egde of a world chunk. + public const int ChunkLength = 1 << ChunkBitShift; } diff --git a/src/gaemstone.Bloxel/WorldGen/BasicWorldGenerator.cs b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs similarity index 54% rename from src/gaemstone.Bloxel/WorldGen/BasicWorldGenerator.cs rename to src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs index 354bb2b..34dd1b4 100644 --- a/src/gaemstone.Bloxel/WorldGen/BasicWorldGenerator.cs +++ b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs @@ -1,8 +1,9 @@ using System; using gaemstone.ECS; -using static flecs_hub.flecs; +using static gaemstone.Bloxel.Components.CoreComponents; +using static gaemstone.Bloxel.Constants; -namespace gaemstone.Bloxel.WorldGen; +namespace gaemstone.Bloxel.Systems; [Module] public class BasicWorldGenerator @@ -22,20 +23,20 @@ public class BasicWorldGenerator public struct HasBasicWorldGeneration { } [System] - public void Populate(Universe universe, Entity entity, - in Chunk chunk, ChunkPaletteStorage storage, + public void Populate(Universe universe, EntityRef entity, + in Chunk chunk, ChunkStoreBlocks blocks, [Not] HasBasicWorldGeneration _) { var stone = universe.Lookup("Stone"); - for (var lx = 0; lx < Chunk.LENGTH; lx++) - for (var ly = 0; ly < Chunk.LENGTH; ly++) - for (var lz = 0; lz < Chunk.LENGTH; lz++) { - var gx = chunk.Position.X << Chunk.BIT_SHIFT | lx; - var gy = chunk.Position.Y << Chunk.BIT_SHIFT | ly; - var gz = chunk.Position.Z << Chunk.BIT_SHIFT | lz; + for (var lx = 0; lx < ChunkLength; lx++) + for (var ly = 0; ly < ChunkLength; ly++) + for (var lz = 0; lz < ChunkLength; lz++) { + var gx = chunk.Position.X << ChunkBitShift | lx; + var gy = chunk.Position.Y << ChunkBitShift | ly; + var gz = chunk.Position.Z << ChunkBitShift | lz; var bias = Math.Clamp(gy / 32.0F + 1.0F, 0.0F, 1.0F); if (_noise.GetNoise(gx, gy, gz) > bias) - storage[lx, ly, lz] = stone; + blocks[lx, ly, lz] = stone; } entity.Add(); } diff --git a/src/gaemstone.Bloxel/WorldGen/SurfaceGrassGenerator.cs.disabled b/src/gaemstone.Bloxel/Systems/SurfaceGrassGenerator.cs.disabled similarity index 100% rename from src/gaemstone.Bloxel/WorldGen/SurfaceGrassGenerator.cs.disabled rename to src/gaemstone.Bloxel/Systems/SurfaceGrassGenerator.cs.disabled diff --git a/src/gaemstone.Client/Modules/CameraComponents.cs b/src/gaemstone.Client/Components/CameraComponents.cs similarity index 94% rename from src/gaemstone.Client/Modules/CameraComponents.cs rename to src/gaemstone.Client/Components/CameraComponents.cs index f564913..cc7d0b8 100644 --- a/src/gaemstone.Client/Modules/CameraComponents.cs +++ b/src/gaemstone.Client/Components/CameraComponents.cs @@ -1,10 +1,9 @@ using gaemstone.ECS; using Silk.NET.Maths; -namespace gaemstone.Client; +namespace gaemstone.Client.Components; [Module] -[DependsOn(typeof(Input))] public class CameraComponents { [Component] diff --git a/src/gaemstone.Client/Components/RenderingComponents.cs b/src/gaemstone.Client/Components/RenderingComponents.cs new file mode 100644 index 0000000..7266362 --- /dev/null +++ b/src/gaemstone.Client/Components/RenderingComponents.cs @@ -0,0 +1,62 @@ +using System.Drawing; +using gaemstone.ECS; +using Silk.NET.Maths; +using Silk.NET.OpenGL; + +namespace gaemstone.Client.Components; + +[Module] +public class RenderingComponents +{ + [Component] + public readonly struct Mesh + { + public uint Handle { get; } + public int Count { get; } + public bool IsIndexed { get; } + + public Mesh(uint handle, int count, bool indexed = true) + { Handle = handle; Count = count; IsIndexed = indexed; } + } + + [Component] + public readonly struct Texture + { + public TextureTarget Target { get; } + public uint Handle { get; } + + public Texture(TextureTarget target, uint handle) + => (Target, Handle) = (target, handle); + } + + [Component] + public readonly struct TextureCoords4 + { + public Vector2D TopLeft { get; } + public Vector2D TopRight { get; } + public Vector2D BottomLeft { get; } + public Vector2D BottomRight { get; } + + public TextureCoords4(float x1, float y1, float x2, float y2) + { + TopLeft = new(x1, y1); + TopRight = new(x2, y1); + BottomLeft = new(x1, y2); + BottomRight = new(x2, y2); + } + + public static TextureCoords4 FromIntCoords(Size textureSize, Point origin, Size size) + => FromIntCoords(textureSize, origin.X, origin.Y, size.Width, size.Height); + public static TextureCoords4 FromIntCoords(Size textureSize, int x, int y, int width, int height) => new( + x / (float)textureSize.Width + 0.001F, + y / (float)textureSize.Height + 0.001F, + (x + width) / (float)textureSize.Width - 0.001F, + (y + height) / (float)textureSize.Height - 0.001F); + + public static TextureCoords4 FromGrid(int numCellsX, int numCellsY, int cellX, int cellY) => new( + cellX / (float)numCellsX + 0.001F, + cellY / (float)numCellsY + 0.001F, + (cellX + 1) / (float)numCellsX - 0.001F, + (cellY + 1) / (float)numCellsY - 0.001F); + } +} diff --git a/src/gaemstone.Client/Mesh.cs b/src/gaemstone.Client/Mesh.cs deleted file mode 100644 index f0cacbf..0000000 --- a/src/gaemstone.Client/Mesh.cs +++ /dev/null @@ -1,14 +0,0 @@ -using gaemstone.ECS; - -namespace gaemstone.Client; - -[Component] -public readonly struct Mesh -{ - public uint Handle { get; } - public int Count { get; } - public bool IsIndexed { get; } - - public Mesh(uint handle, int count, bool indexed = true) - { Handle = handle; Count = count; IsIndexed = indexed; } -} diff --git a/src/gaemstone.Client/MeshManager.cs b/src/gaemstone.Client/MeshManager.cs index b2434fc..fe28719 100644 --- a/src/gaemstone.Client/MeshManager.cs +++ b/src/gaemstone.Client/MeshManager.cs @@ -2,6 +2,8 @@ using System; using gaemstone.ECS; using Silk.NET.Maths; using Silk.NET.OpenGL; +using static gaemstone.Client.Components.RenderingComponents; +using static gaemstone.Client.Systems.Windowing; using ModelRoot = SharpGLTF.Schema2.ModelRoot; namespace gaemstone.Client; @@ -23,7 +25,7 @@ public static class MeshManager var vertices = primitive.VertexAccessors["POSITION"]; var normals = primitive.VertexAccessors["NORMAL"]; - var GL = universe.Lookup().Get().GL; + var GL = universe.Lookup().Get().GL; var vao = GL.GenVertexArray(); GL.BindVertexArray(vao); @@ -50,7 +52,7 @@ public static class MeshManager ReadOnlySpan indices, ReadOnlySpan> vertices, ReadOnlySpan> normals, ReadOnlySpan> uvs) { - var GL = universe.Lookup().Get().GL; + var GL = universe.Lookup().Get().GL; var vao = GL.GenVertexArray(); GL.BindVertexArray(vao); @@ -78,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.Lookup().Get().GL; var vao = GL.GenVertexArray(); GL.BindVertexArray(vao); diff --git a/src/gaemstone.Client/Modules/CameraController.cs b/src/gaemstone.Client/Systems/FreeCameraController.cs similarity index 88% rename from src/gaemstone.Client/Modules/CameraController.cs rename to src/gaemstone.Client/Systems/FreeCameraController.cs index cad0629..72cbd98 100644 --- a/src/gaemstone.Client/Modules/CameraController.cs +++ b/src/gaemstone.Client/Systems/FreeCameraController.cs @@ -1,11 +1,13 @@ using System; +using gaemstone.Client.Components; using gaemstone.ECS; using Silk.NET.Input; using Silk.NET.Maths; -using static gaemstone.Client.CameraComponents; -using static gaemstone.Client.Input; +using static gaemstone.Client.Components.CameraComponents; +using static gaemstone.Client.Systems.Input; +using static gaemstone.Components.TransformComponents; -namespace gaemstone.Client; +namespace gaemstone.Client.Systems; [Module] [DependsOn(typeof(CameraComponents))] @@ -22,7 +24,7 @@ public class FreeCameraController [System] public static void UpdateCamera(TimeSpan delta, in Camera camera, ref GlobalTransform transform, ref CameraController controller, - [Source(typeof(Game))] RawInput input) + [Game] RawInput input) { var isMouseDown = input.IsDown(MouseButton.Right); var isMouseGrabbed = controller.MouseGrabbedAt != null; diff --git a/src/gaemstone.Client/Modules/Input.cs b/src/gaemstone.Client/Systems/Input.cs similarity index 94% rename from src/gaemstone.Client/Modules/Input.cs rename to src/gaemstone.Client/Systems/Input.cs index f8d3415..c5abe54 100644 --- a/src/gaemstone.Client/Modules/Input.cs +++ b/src/gaemstone.Client/Systems/Input.cs @@ -4,9 +4,9 @@ using System.Linq; using gaemstone.ECS; using Silk.NET.Input; using Silk.NET.Maths; -using static gaemstone.Client.Windowing; +using static gaemstone.Client.Systems.Windowing; -namespace gaemstone.Client; +namespace gaemstone.Client.Systems; [Module] [DependsOn(typeof(Windowing))] @@ -35,7 +35,7 @@ public class Input public bool Released; } - [System(SystemPhase.OnLoad)] + [System(typeof(SystemPhase.OnLoad))] public static void ProcessInput(GameWindow window, RawInput input, TimeSpan delta) { window.Handle.DoEvents(); diff --git a/src/gaemstone.Client/Modules/Renderer.cs b/src/gaemstone.Client/Systems/Renderer.cs similarity index 88% rename from src/gaemstone.Client/Modules/Renderer.cs rename to src/gaemstone.Client/Systems/Renderer.cs index 944641d..18eb6b3 100644 --- a/src/gaemstone.Client/Modules/Renderer.cs +++ b/src/gaemstone.Client/Systems/Renderer.cs @@ -4,10 +4,14 @@ using System.Runtime.InteropServices; using gaemstone.ECS; using Silk.NET.Maths; using Silk.NET.OpenGL; -using static gaemstone.Client.CameraComponents; -using static gaemstone.Client.Windowing; +using Silk.NET.Windowing; +using static gaemstone.Client.Components.CameraComponents; +using static gaemstone.Client.Components.RenderingComponents; +using static gaemstone.Client.Systems.Windowing; +using static gaemstone.Components.TransformComponents; +using Texture = gaemstone.Client.Components.RenderingComponents.Texture; -namespace gaemstone.Client; +namespace gaemstone.Client.Systems; [Module] [DependsOn(typeof(Windowing))] @@ -44,8 +48,8 @@ public class Renderer _modelMatrixUniform = GL.GetUniformLocation(_program, "modelMatrix"); } - [System] - public void Render(Universe universe, Canvas canvas) + [System(typeof(SystemPhase.OnStore))] + public void Render(Universe universe, GameWindow window, Canvas canvas) { var GL = canvas.GL; GL.UseProgram(_program); @@ -94,6 +98,8 @@ public class Renderer if (texture.HasValue) GL.BindTexture(texture.Value.Target, 0); }); }); + + window.Handle.SwapBuffers(); } [DebuggerStepThrough] diff --git a/src/gaemstone.Client/Modules/Windowing.cs b/src/gaemstone.Client/Systems/Windowing.cs similarity index 88% rename from src/gaemstone.Client/Modules/Windowing.cs rename to src/gaemstone.Client/Systems/Windowing.cs index d40b922..b5c8171 100644 --- a/src/gaemstone.Client/Modules/Windowing.cs +++ b/src/gaemstone.Client/Systems/Windowing.cs @@ -3,7 +3,7 @@ using Silk.NET.Maths; using Silk.NET.OpenGL; using Silk.NET.Windowing; -namespace gaemstone.Client; +namespace gaemstone.Client.Systems; [Module] public class Windowing @@ -25,7 +25,7 @@ public class Windowing public GameWindow(IWindow handle) => Handle = handle; } - [System(SystemPhase.PreFrame)] + [System(typeof(SystemPhase.PreFrame))] public static void ProcessWindow(GameWindow window, Canvas canvas) => canvas.Size = window.Handle.Size; } diff --git a/src/gaemstone.Client/Texture.cs b/src/gaemstone.Client/Texture.cs deleted file mode 100644 index 3a02fd0..0000000 --- a/src/gaemstone.Client/Texture.cs +++ /dev/null @@ -1,14 +0,0 @@ -using gaemstone.ECS; -using Silk.NET.OpenGL; - -namespace gaemstone.Client; - -[Component] -public readonly struct Texture -{ - public TextureTarget Target { get; } - public uint Handle { get; } - - public Texture(TextureTarget target, uint handle) - => (Target, Handle) = (target, handle); -} diff --git a/src/gaemstone.Client/TextureCoords4.cs b/src/gaemstone.Client/TextureCoords4.cs deleted file mode 100644 index 70cd3fe..0000000 --- a/src/gaemstone.Client/TextureCoords4.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Drawing; -using gaemstone.ECS; -using Silk.NET.Maths; - -namespace gaemstone.Client; - -[Component] -public readonly struct TextureCoords4 -{ - public Vector2D TopLeft { get; } - public Vector2D TopRight { get; } - public Vector2D BottomLeft { get; } - public Vector2D BottomRight { get; } - - public TextureCoords4(float x1, float y1, float x2, float y2) - { - TopLeft = new(x1, y1); - TopRight = new(x2, y1); - BottomLeft = new(x1, y2); - BottomRight = new(x2, y2); - } - - public static TextureCoords4 FromIntCoords(Size textureSize, Point origin, Size size) - => FromIntCoords(textureSize, origin.X, origin.Y, size.Width, size.Height); - public static TextureCoords4 FromIntCoords(Size textureSize, int x, int y, int width, int height) => new( - x / (float)textureSize.Width + 0.001F, - y / (float)textureSize.Height + 0.001F, - (x + width) / (float)textureSize.Width - 0.001F, - (y + height) / (float)textureSize.Height - 0.001F); - - public static TextureCoords4 FromGrid(int numCellsX, int numCellsY, int cellX, int cellY) => new( - cellX / (float)numCellsX + 0.001F, - cellY / (float)numCellsY + 0.001F, - (cellX + 1) / (float)numCellsX - 0.001F, - (cellY + 1) / (float)numCellsY - 0.001F); -} diff --git a/src/gaemstone.Client/TextureManager.cs b/src/gaemstone.Client/TextureManager.cs index e44e4d8..b55fd34 100644 --- a/src/gaemstone.Client/TextureManager.cs +++ b/src/gaemstone.Client/TextureManager.cs @@ -5,7 +5,9 @@ using gaemstone.ECS; using Silk.NET.OpenGL; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using static gaemstone.Client.Systems.Windowing; using Size = System.Drawing.Size; +using Texture = gaemstone.Client.Components.RenderingComponents.Texture; namespace gaemstone.Client; @@ -16,7 +18,7 @@ public static class TextureManager public static void Initialize(Universe universe) { - var GL = universe.Lookup().Get().GL; + var GL = universe.Lookup().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); @@ -36,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.Lookup().Get().GL; var texture = new Texture(TextureTarget.Texture2D, GL.GenTexture()); GL.BindTexture(texture.Target, texture.Handle); diff --git a/src/gaemstone/Components/TransformComponents.cs b/src/gaemstone/Components/TransformComponents.cs new file mode 100644 index 0000000..d536c64 --- /dev/null +++ b/src/gaemstone/Components/TransformComponents.cs @@ -0,0 +1,17 @@ +using gaemstone.ECS; +using Silk.NET.Maths; + +namespace gaemstone.Components; + +[Module] +public class TransformComponents +{ + [Component] + public struct GlobalTransform + { + public Matrix4X4 Value; + public GlobalTransform(Matrix4X4 value) => Value = value; + public static implicit operator GlobalTransform(in Matrix4X4 value) => new(value); + public static implicit operator Matrix4X4(in GlobalTransform index) => index.Value; + } +} diff --git a/src/gaemstone/ECS/Attributes.cs b/src/gaemstone/ECS/Attributes.cs deleted file mode 100644 index 593ba40..0000000 --- a/src/gaemstone/ECS/Attributes.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace gaemstone.ECS; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -public class ComponentAttribute : Attribute { } - -[AttributeUsage(AttributeTargets.Struct)] -public class TagAttribute : Attribute { } - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] -public class RelationAttribute : Attribute { } diff --git a/src/gaemstone/ECS/Component.cs b/src/gaemstone/ECS/Component.cs new file mode 100644 index 0000000..b497a69 --- /dev/null +++ b/src/gaemstone/ECS/Component.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.InteropServices; +using gaemstone.Utility; +using static flecs_hub.flecs; + +namespace gaemstone.ECS; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class ComponentAttribute : Attribute { } + +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) + { + var typeInfo = default(ecs_type_info_t); + if (type.IsValueType) { + var wrapper = TypeWrapper.For(type); + if (!wrapper.IsUnmanaged) throw new Exception( + "Struct component must satisfy the unmanaged constraint. " + + "Consider making it a class if you need to store references."); + var structLayout = type.StructLayoutAttribute; + if (structLayout == null || structLayout.Value == LayoutKind.Auto) throw new Exception( + "Struct component must have a sequential or explicit StructLayout. " + + "This is to ensure that the struct fields are not reorganized."); + typeInfo.size = wrapper.Size; + typeInfo.alignment = structLayout.Pack; + } else { + typeInfo.size = sizeof(nint); + 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; + } +} diff --git a/src/gaemstone/ECS/Entity+AddRemove.cs b/src/gaemstone/ECS/Entity+AddRemove.cs new file mode 100644 index 0000000..2e90ea1 --- /dev/null +++ b/src/gaemstone/ECS/Entity+AddRemove.cs @@ -0,0 +1,43 @@ +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 new file mode 100644 index 0000000..5723503 --- /dev/null +++ b/src/gaemstone/ECS/Entity+GetSet.cs @@ -0,0 +1,86 @@ +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 97cfab3..3a005b8 100644 --- a/src/gaemstone/ECS/Entity.cs +++ b/src/gaemstone/ECS/Entity.cs @@ -1,8 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Diagnostics.CodeAnalysis; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -11,184 +11,114 @@ namespace gaemstone.ECS; public class EntityAttribute : Attribute { public uint ID { get; set; } + public string? Name { get; set; } } -public unsafe readonly struct Entity +public readonly struct Entity + : IEquatable { - public Universe Universe { get; } - public ecs_entity_t Value { get; } - - public EntityType Type => new(Universe, ecs_get_type(Universe, Value)); - public string Name => ecs_get_name(Universe, Value).ToStringAndFree(); - public string FullPath => ecs_get_path_w_sep(Universe, default, Value, ".", default).ToStringAndFree(); - - public bool IsNone => Value.Data == 0; - public bool IsAlive => ecs_is_alive(Universe, Value); - - public IEnumerable Children { get { - var term = new ecs_term_t { id = Universe.EcsChildOf & this }; - foreach (var iter in Iterator.FromTerm(Universe, term)) - for (var i = 0; i < iter.Count; i++) - yield return iter.Entity(i); - } } - - public Entity(Universe universe, ecs_entity_t value) - { Universe = universe; Value = value; } - - public Entity ThrowIfNone() { if (IsNone) throw new InvalidOperationException("Entity is invalid"); return this; } - public Entity ThrowIfDead() { if (!IsAlive) throw new InvalidOperationException("Entity is dead"); return this; } + public readonly ecs_entity_t Value; - public void Delete() => ecs_delete(Universe, 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 Entity(ecs_entity_t value) => Value = value; - public Entity Add(ecs_id_t id) { ecs_add_id(Universe, this, id); return this; } - public Entity Add(Identifier id) { ecs_add_id(Universe, this, id); return this; } - public Entity Add(Entity relation, Entity target) => Add(relation & target); + 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 int GetHashCode() => Value.Data.GetHashCode(); + public override string? ToString() => $"Entity(0x{Value.Data.Data:X})"; - public Entity Add() - => Add(Universe.Lookup()); - public Entity Add() - => Add(Universe.Lookup(), Universe.Lookup()); - public Entity Add(Entity target) - => Add(Universe.Lookup(), target); + public static bool operator ==(Entity left, Entity right) => left.Equals(right); + public static bool operator !=(Entity left, Entity right) => !left.Equals(right); + public static Identifier operator &(Entity first, Entity second) => Identifier.Pair(first, second); - public Entity Override(ecs_id_t id) { ecs_override_id(Universe, this, id); return this; } - public Entity Override(Identifier id) { ecs_override_id(Universe, this, id); return this; } - public Entity Override(Entity relation, Entity target) => Override(relation & target); - - public Entity Override() - => Override(Universe.Lookup()); - public Entity Override() - => Override(Universe.Lookup(), Universe.Lookup()); - public Entity Override(Entity target) - => Override(Universe.Lookup(), target); - + public static implicit operator ecs_entity_t(Entity e) => e.Value; + public static implicit operator Identifier(Entity e) => new(e.Value.Data); + public static implicit operator ecs_id_t(Entity e) => e.Value.Data; +} - public void Remove(ecs_id_t id) => ecs_remove_id(Universe, this, id); - public void Remove(Identifier id) => ecs_remove_id(Universe, this, id); - public void Remove() => Remove(Universe.Lookup()); +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)); - public bool Has(ecs_id_t id) => ecs_has_id(Universe, this, id); - public bool Has(Identifier id) => ecs_has_id(Universe, this, id); - public bool Has(Entity relation, Entity target) => Has(relation & target); + // TODO: public IEnumerable Children => ... - 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(Universe universe, Entity entity) + { Universe = universe; Entity = entity.ThrowIfInvalid(); } + void IDisposable.Dispose() => Delete(); + public unsafe void Delete() => ecs_delete(Universe, this); - /// - /// 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); - if (typeof(T).IsValueType) { - return Unsafe.Read(ptr); - } else { - var handle = (GCHandle)Unsafe.Read(ptr); - return (T)handle.Target!; - } - } + public EntityRef Disable() => Add(); + public EntityRef Enable() => Remove(); - /// - /// 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); - } + 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()!; - /// - /// 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() - { - var comp = Universe.Lookup(); - ecs_modified_id(Universe, this, comp); - } + 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 Entity 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 static implicit operator Entity(EntityRef e) => new(e.Entity); + public static implicit operator ecs_entity_t(EntityRef e) => e.Entity.Value; - public Entity 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 Entity Set(T obj) where T : class - => Set(typeof(T), obj); - public Entity 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; - } - - - public static Identifier operator &(Entity first, Entity second) => Identifier.Pair(first, second); - public static Identifier operator &(ecs_entity_t first, Entity second) => Identifier.Pair(first, second); - - public static implicit operator ecs_id_t(Entity e) => e.Value.Data; - public static implicit operator ecs_entity_t(Entity e) => e.Value; - public static implicit operator Identifier(Entity e) => new(e.Universe, e); + 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 - : IEnumerable + : IReadOnlyList { public Universe Universe { get; } - public unsafe ecs_type_t* Handle { get; } - - public int Count => Handle->count; - public Identifier this[int index] => new(Universe, Handle->array[index]); + public ecs_type_t* Handle { get; } public EntityType(Universe universe, ecs_type_t* handle) { Universe = universe; Handle = handle; } - public IEnumerator GetEnumerator() - { for (var i = 0; i < Count; i++) yield return this[i]; } - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - public override string ToString() - => ecs_type_str(Universe, Handle).ToStringAndFree(); + => 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/EntityBuilder.cs b/src/gaemstone/ECS/EntityBuilder.cs new file mode 100644 index 0000000..440779d --- /dev/null +++ b/src/gaemstone/ECS/EntityBuilder.cs @@ -0,0 +1,81 @@ +using System.Collections; +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 +{ + public Universe Universe { get; } + + /// Set to modify existing entity (optional). + public Entity ID { get; set; } + + /// + /// Name of the entity. If no entity is provided, an entity with this name + /// will be looked up first. When an entity is provided, the name will be + /// verified with the existing entity. + /// + public string? Name { get; set; } + + /// + /// Optional entity symbol. A symbol is an unscoped identifier that can + /// be used to lookup an entity. The primary use case for this is to + /// associate the entity with a language identifier, such as a type or + /// function name, where these identifiers differ from the name they are + /// registered with in flecs. + /// + public string? Symbol { get; set; } + + /// + /// When set to true, a low id (typically reserved for components) + /// will be used to create the entity, if no id is specified. + /// + public bool UseLowID { get; set; } + + /// IDs to add to the new or existing entity. + private readonly List _add = 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; + + 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 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()); + + // TODO: Add support for Set. + + public EntityBuilder ChildOf(Entity parent) => Add(Universe.Lookup(), parent); + public EntityBuilder ChildOf() => ChildOf(Universe.Lookup()); + + public EntityBuilder Disabled() => Add(Universe.Lookup()); + + public unsafe EntityRef Build() + { + var desc = new ecs_entity_desc_t { + id = ID, + name = Name.FlecsToCString(), + symbol = Symbol.FlecsToCString(), + 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)); + } + + // IReadOnlyCollection implementation + public int Count => _add.Count; + public IEnumerator GetEnumerator() => _add.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/gaemstone/ECS/EntityDesc.cs.disabled b/src/gaemstone/ECS/EntityDesc.cs.disabled deleted file mode 100644 index 0a3e44b..0000000 --- a/src/gaemstone/ECS/EntityDesc.cs.disabled +++ /dev/null @@ -1,20 +0,0 @@ -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public struct EntityDesc -{ - public ecs_entity_desc_t Value; - - public string? Name { get => Value.name; set => Value.name.Set(value); } - public string? Symbol { get => Value.symbol; set => Value.symbol.Set(value); } - - public EntityDesc(params ecs_id_t[] ids) - { - Value = default; - for (var i = 0; i < ids.Length; i++) - Value.add[i] = ids[i]; - } - - public static explicit operator ecs_entity_desc_t(EntityDesc desc) => desc.Value; -} diff --git a/src/gaemstone/ECS/Filter.cs b/src/gaemstone/ECS/Filter.cs index c2d77d8..ad9df4f 100644 --- a/src/gaemstone/ECS/Filter.cs +++ b/src/gaemstone/ECS/Filter.cs @@ -1,12 +1,14 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; using gaemstone.Utility.IL; using static flecs_hub.flecs; namespace gaemstone.ECS; -public unsafe class Filter +public unsafe sealed class Filter : IEnumerable , IDisposable { @@ -15,25 +17,76 @@ public unsafe class Filter private Filter(Universe universe, ecs_filter_t* handle) { Universe = universe; Handle = handle; } - - public Filter(Universe universe, ecs_filter_desc_t desc) + private Filter(Universe universe, ecs_filter_desc_t desc) : this(universe, ecs_filter_init(universe, &desc)) { } - public Filter(Universe universe, string expression) - : this(universe, new ecs_filter_desc_t { expr = expression }) { } + + public Filter(Universe universe, FilterDesc desc) + : this(universe, desc.ToFlecs()) { } public static void RunOnce(Universe universe, Delegate action) { - var gen = QueryActionGenerator.GetOrBuild(universe, action.Method); - using var filter = new Filter(universe, gen.Filter); + var gen = IterActionGenerator.GetOrBuild(universe, action.Method); + var desc = new FilterDesc(action.Method.Name, gen.Terms.ToArray()); + using var filter = new Filter(universe, desc); foreach (var iter in filter) gen.RunWithTryCatch(action.Target, iter); } - ~Filter() => Dispose(); - public void Dispose() { ecs_filter_fini(Handle); GC.SuppressFinalize(this); } + public void Dispose() + => ecs_filter_fini(Handle); + + public override string ToString() + => ecs_filter_str(Universe, Handle).FlecsToStringAndFree()!; + + public static implicit operator ecs_filter_t*(Filter q) => q.Handle; + // IEnumerable implementation public Iterator Iter() => new(Universe, IteratorType.Filter, ecs_filter_iter(Universe, this)); public IEnumerator GetEnumerator() => Iter().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} - public static implicit operator ecs_filter_t*(Filter q) => q.Handle; +public class FilterDesc +{ + public IReadOnlyList Terms { get; set; } + + /// + /// Optional name of filter, used for debugging. If a filter is created + /// for a system, the provided name should match the system name. + /// + public string? Name { get; set; } + + /// Filter expression. Should not be set at the same time as terms. + public string? Expression { get; set; } + + /// + /// When true, terms returned by an iterator may either contain 1 or N + /// elements, where terms with N elements are owned, and terms with 1 + /// element are shared, for example from a parent or base entity. When + /// false, the iterator will at most return 1 element when the result + /// contains both owned and shared terms. + /// + public bool Instanced { get; set; } + + public FilterDesc(string? name = null, params Term[] terms) + { Name = name; Terms = terms; } + + public unsafe ecs_filter_desc_t ToFlecs() + { + var desc = new ecs_filter_desc_t { + name = Name.FlecsToCString(), + expr = Expression.FlecsToCString(), + instanced = Instanced, + }; + var span = desc.terms; + if (Terms.Count > desc.terms.Length) { + var byteCount = sizeof(ecs_term_t) * Terms.Count; + var ptr = (ecs_term_t*)Marshal.AllocHGlobal(byteCount); + desc.terms_buffer = ptr; + desc.terms_buffer_count = Terms.Count; + span = new(ptr, Terms.Count); + } + for (var i = 0; i < Terms.Count; i++) + span[i] = Terms[i].ToFlecs(); + return desc; + } } diff --git a/src/gaemstone/ECS/Flecs.cs b/src/gaemstone/ECS/Flecs.cs new file mode 100644 index 0000000..472ae76 --- /dev/null +++ b/src/gaemstone/ECS/Flecs.cs @@ -0,0 +1,41 @@ +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/FlecsException.cs b/src/gaemstone/ECS/FlecsException.cs index f4b8745..5c90a12 100644 --- a/src/gaemstone/ECS/FlecsException.cs +++ b/src/gaemstone/ECS/FlecsException.cs @@ -1,17 +1,35 @@ using System; using System.Diagnostics; +using System.Runtime.InteropServices; +using static flecs_hub.flecs; namespace gaemstone.ECS; -public class FlecsException : Exception +public class FlecsException + : Exception { public FlecsException() : base() { } public FlecsException(string message) : base(message) { } } -public class FlecsAbortException : FlecsException +public class FlecsAbortException + : FlecsException { private readonly string _stackTrace = new StackTrace(2, true).ToString(); - internal FlecsAbortException() : base("Abort was called by flecs") { } public override string? StackTrace => _stackTrace; + + private FlecsAbortException() + : base("Abort was called by flecs") { } + + // TODO: This might not be ideal if we ever want to set other OS API settings. + public unsafe static void SetupHook() + { + ecs_os_set_api_defaults(); + var api = ecs_os_get_api(); + api.abort_ = new FnPtr_Void { Pointer = &Abort }; + ecs_os_set_api(&api); + } + + [UnmanagedCallersOnly] + private static void Abort() => throw new FlecsAbortException(); } diff --git a/src/gaemstone/ECS/Game.cs b/src/gaemstone/ECS/Game.cs new file mode 100644 index 0000000..a3001d3 --- /dev/null +++ b/src/gaemstone/ECS/Game.cs @@ -0,0 +1,15 @@ +using System; + +namespace gaemstone.ECS; + +/// +/// Entity for storing global game state and configuration. +/// Parameters can use to source this entity. +/// +[Entity] +public struct Game { } + +/// Short for [Source(typeof(Game))]. +[AttributeUsage(AttributeTargets.Parameter)] +public class GameAttribute : SourceAttribute + { public GameAttribute() : base(typeof(Game)) { } } diff --git a/src/gaemstone/ECS/Identifier.cs b/src/gaemstone/ECS/Identifier.cs index 7fe35ea..273c448 100644 --- a/src/gaemstone/ECS/Identifier.cs +++ b/src/gaemstone/ECS/Identifier.cs @@ -1,55 +1,82 @@ using System; +using System.Diagnostics.CodeAnalysis; using static flecs_hub.flecs; namespace gaemstone.ECS; -[Flags] -public enum IdentifierFlags : ulong +public readonly struct Identifier + : IEquatable { - Pair = 1ul << 63, - Override = 1ul << 62, - Toggle = 1ul << 61, - Or = 1ul << 60, - And = 1ul << 59, - Not = 1ul << 58, + public readonly ecs_id_t Value; + + public IdentifierFlags Flags => (IdentifierFlags)(Value & ECS_ID_FLAGS_MASK); + public bool IsPair => ecs_id_is_pair(Value); + public bool IsWildcard => ecs_id_is_wildcard(Value); + + public Identifier(ecs_id_t value) => Value = value; + + 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 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 int GetHashCode() => Value.Data.GetHashCode(); + public override string? ToString() + => (Flags != default) ? $"Identifier(0x{Value.Data:X}, Flags={Flags})" + : $"Identifier(0x{Value.Data:X})"; + + public static bool operator ==(Identifier left, Identifier right) => left.Equals(right); + public static bool operator !=(Identifier left, Identifier right) => !left.Equals(right); + + public static implicit operator ecs_id_t(Identifier i) => i.Value; } -public unsafe readonly struct Identifier +public unsafe readonly struct IdentifierRef + : IEquatable { public Universe Universe { get; } - public ecs_id_t Value { get; } - - public IdentifierFlags Flags => (IdentifierFlags)(Value.Data & ECS_ID_FLAGS_MASK); - public bool IsPair => ecs_id_is_pair(Value); + public Identifier ID { get; } - public Identifier(Universe universe, ecs_id_t id) - { Universe = universe; Value = id; } - public Identifier(Universe universe, ecs_id_t id, IdentifierFlags flags) - : this(universe, Combine(flags, id)) { } + 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 static ecs_id_t Combine(IdentifierFlags flags, ecs_id_t id) - => (ulong)flags | id; - public static ecs_id_t Pair(ecs_id_t first, ecs_id_t second) - => Combine(IdentifierFlags.Pair, (first << 32) + (uint)second); + public IdentifierRef(Universe universe, Identifier id) + { Universe = universe; ID = id; } - public static Identifier Pair(Entity first, Entity second) - => new(first.Universe, Pair((ecs_id_t)first, (ecs_id_t)second)); - public static Identifier Pair(ecs_entity_t first, Entity second) - => new(second.Universe, Pair((ecs_id_t)first, (ecs_id_t)second)); + 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 (Entity, Entity) AsPair() - => (Universe.Lookup((ecs_id_t)((Value & ECS_COMPONENT_MASK) >> 32)), - Universe.Lookup((ecs_id_t) (Value & ECS_ENTITY_MASK))); + 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 Entity AsComponent() - // { - // var value = Value.Data & ECS_COMPONENT_MASK; - // return new Entity(Universe, new() { Data = value }); - // } + 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 override string ToString() - => ecs_id_str(Universe, Value).ToStringAndFree(); + 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; +} - public static implicit operator ecs_id_t(Identifier e) => e.Value; +[Flags] +public enum IdentifierFlags : ulong +{ + Pair = 1ul << 63, + Override = 1ul << 62, + Toggle = 1ul << 61, + Or = 1ul << 60, + And = 1ul << 59, + Not = 1ul << 58, } diff --git a/src/gaemstone/ECS/Iterator.cs b/src/gaemstone/ECS/Iterator.cs index 695061b..69cb619 100644 --- a/src/gaemstone/ECS/Iterator.cs +++ b/src/gaemstone/ECS/Iterator.cs @@ -21,10 +21,13 @@ public unsafe class Iterator public Iterator(Universe universe, IteratorType? type, ecs_iter_t value) { Universe = universe; Type = type; Value = value; } - public static Iterator FromTerm(Universe universe, in ecs_term_t term) + // TODO: ecs_iter_set_var(world, 0, ent) to run a query for a known entity. + + public static Iterator FromTerm(Universe universe, Term term) { - fixed (ecs_term_t* ptr = &term) - return new(universe, IteratorType.Term, ecs_term_iter(universe, ptr)); + var flecsTerm = term.ToFlecs(); + var flecsIter = ecs_term_iter(universe, &flecsTerm); + return new(universe, IteratorType.Term, flecsIter); } public bool Next() @@ -39,8 +42,8 @@ public unsafe class Iterator }; } - public Entity Entity(int index) - => new(Universe, Value.entities[index]); + public EntityRef Entity(int index) + => new(Universe, new(Value.entities[index])); public Span Field(int index) where T : unmanaged @@ -62,12 +65,11 @@ public unsafe class Iterator } public bool FieldIs(int index) - where T : unmanaged { fixed (ecs_iter_t* ptr = &Value) { var id = ecs_field_id(ptr, index); var comp = Universe.Lookup(); - return id == comp.Value.Data; + return id == comp.Entity.Value.Data; } } diff --git a/src/gaemstone/ECS/Module.cs b/src/gaemstone/ECS/Module.cs index 97513ac..37011d0 100644 --- a/src/gaemstone/ECS/Module.cs +++ b/src/gaemstone/ECS/Module.cs @@ -1,4 +1,9 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using gaemstone.Utility; namespace gaemstone.ECS; @@ -8,6 +13,121 @@ public class ModuleAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class DependsOnAttribute : Attribute { - public Type Target { get; } + public Type Target { get; } // TODO: Should probably move to string. public DependsOnAttribute(Type target) => Target = target; } + +[Component] +public class Module +{ + public Type Type { get; } + public object? Instance { get; internal set; } + public Module(Type type) => Type = type; +} + +public static class ModuleExtensions +{ + 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; + } +} diff --git a/src/gaemstone/ECS/Observer.cs b/src/gaemstone/ECS/Observer.cs index d5599a2..7f6b638 100644 --- a/src/gaemstone/ECS/Observer.cs +++ b/src/gaemstone/ECS/Observer.cs @@ -1,32 +1,76 @@ using System; +using System.Reflection; +using gaemstone.Utility; +using gaemstone.Utility.IL; using static flecs_hub.flecs; namespace gaemstone.ECS; -public enum ObserverEvent +public static class ObserverEvent { - OnAdd = ECS_HI_COMPONENT_ID + 33, - OnRemove = ECS_HI_COMPONENT_ID + 34, - OnSet = ECS_HI_COMPONENT_ID + 35, - UnSet = ECS_HI_COMPONENT_ID + 36, - OnDelete = ECS_HI_COMPONENT_ID + 37, - OnCreateTable = ECS_HI_COMPONENT_ID + 38, - OnDeleteTable = ECS_HI_COMPONENT_ID + 39, - OnTableEmpty = ECS_HI_COMPONENT_ID + 40, - OnTableFill = ECS_HI_COMPONENT_ID + 41, - OnCreateTrigger = ECS_HI_COMPONENT_ID + 42, - OnDeleteTrigger = ECS_HI_COMPONENT_ID + 43, - OnDeleteObservable = ECS_HI_COMPONENT_ID + 44, - OnComponentHooks = ECS_HI_COMPONENT_ID + 45, - OnDeleteTarget = ECS_HI_COMPONENT_ID + 46, + [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 ObserverEvent Event { get; } + public Type Event { get; } public string? Expression { get; } - public ObserverAttribute(ObserverEvent @event) - => Event = @event; + public ObserverAttribute(Type @event) => Event = @event; +} + +public static class ObserverExtensions +{ + public static unsafe EntityRef RegisterObserver(this Universe universe, + string name, Entity @event, FilterDesc filter, Action callback) + { + 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 } }, + }; + desc.events[0] = @event; + var entity = ecs_observer_init(universe, &desc); + return new(universe, new(entity)); + } + + public static EntityRef RegisterObserver(this Universe universe, + object? instance, MethodInfo method) + { + var attr = method.Get() ?? throw new ArgumentException( + "Observer must specify ObserverAttribute", nameof(method)); + var filter = new FilterDesc(); + Action iterAction; + + 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); + } else { + var gen = IterActionGenerator.GetOrBuild(universe, method); + if (attr.Expression == null) filter.Terms = gen.Terms; + else filter.Expression = attr.Expression; + iterAction = iter => gen.RunWithTryCatch(instance, iter); + } + + var @event = universe.Lookup(attr.Event); + return universe.RegisterObserver(method.Name, @event, filter, iterAction); + } } diff --git a/src/gaemstone/ECS/Query.cs b/src/gaemstone/ECS/Query.cs index 9677a17..550e430 100644 --- a/src/gaemstone/ECS/Query.cs +++ b/src/gaemstone/ECS/Query.cs @@ -5,7 +5,7 @@ using static flecs_hub.flecs; namespace gaemstone.ECS; -public unsafe class Query +public unsafe sealed class Query : IEnumerable , IDisposable { @@ -14,20 +14,37 @@ public unsafe class Query private Query(Universe universe, ecs_query_t* handle) { Universe = universe; Handle = handle; } - - public Query(Universe universe, ecs_query_desc_t desc) + private Query(Universe universe, ecs_query_desc_t desc) : this(universe, ecs_query_init(universe, &desc)) { } - public Query(Universe universe, ecs_filter_desc_t desc) - : this(universe, new ecs_query_desc_t { filter = desc }) { } - public Query(Universe universe, string expression) - : this(universe, new ecs_filter_desc_t { expr = expression }) { } - ~Query() => Dispose(); - public void Dispose() { ecs_query_fini(this); GC.SuppressFinalize(this); } + public Query(Universe universe, QueryDesc desc) + : this(universe, desc.ToFlecs()) { } + + public void Dispose() + => ecs_query_fini(this); + + public override string ToString() + => ecs_query_str(Handle).FlecsToStringAndFree()!; + + public static implicit operator ecs_query_t*(Query q) => q.Handle; + // IEnumerable implementation public Iterator Iter() => new(Universe, IteratorType.Query, ecs_query_iter(Universe, this)); public IEnumerator GetEnumerator() => Iter().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} - public static implicit operator ecs_query_t*(Query q) => q.Handle; +public class QueryDesc : FilterDesc +{ + public QueryDesc(string? name = null, params Term[] terms) + : base(name, terms) { } + + public new unsafe ecs_query_desc_t ToFlecs() + { + var desc = new ecs_query_desc_t { + filter = base.ToFlecs(), + // TODO: Implement more Query features. + }; + return desc; + } } diff --git a/src/gaemstone/ECS/Registerable.cs b/src/gaemstone/ECS/Registerable.cs index e959882..42c4b14 100644 --- a/src/gaemstone/ECS/Registerable.cs +++ b/src/gaemstone/ECS/Registerable.cs @@ -1,65 +1,56 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using gaemstone.Utility; namespace gaemstone.ECS; -public enum RegisterableKind -{ - Entity, - Tag, - Component, - Relation, - System, - Observer, - Module, -} +// TODO: Make this return an EntityBuilder instead. +public delegate EntityRef RegisterFunc(Universe universe, object? instance, MemberInfo member); public class RegisterableInfo { - public Type Type { get; } - public RegisterableKind Kind { get; } - public bool? PartOfModule { get; } - internal Type[] AllowedWith { get; } + public bool? PartOfModule { get; } // true/false = must (not) be part of module; null = doesn't matter + public RegisterFunc Register { get; } + public Type[] AllowedWith { get; } - internal RegisterableInfo(Type type, RegisterableKind kind, bool? partOfModule, Type[]? allowedWith = null) - { Type = type; Kind = kind; PartOfModule = partOfModule; AllowedWith = allowedWith ?? Array.Empty(); } + public RegisterableInfo(bool? partOfModule, RegisterFunc register, params Type[] allowedWith) + { PartOfModule = partOfModule; Register = register; AllowedWith = allowedWith; } } public static class RegisterableExtensions { - - // These are ordered by priority. For example a type marked with [Component, Relation] - // will result in RegisterableKind.Relation due to being first in the list. - private static readonly RegisterableInfo[] _knownAttributes = new RegisterableInfo[] { - new(typeof(RelationAttribute) , RegisterableKind.Relation , null, new[] { typeof(ComponentAttribute), typeof(TagAttribute) }), - new(typeof(ComponentAttribute) , RegisterableKind.Component , null, new[] { typeof(EntityAttribute) }), - new(typeof(TagAttribute) , RegisterableKind.Tag , null), - new(typeof(EntityAttribute) , RegisterableKind.Entity , null), - - new(typeof(ModuleAttribute) , RegisterableKind.Module , false), - new(typeof(SystemAttribute) , RegisterableKind.System , true), - new(typeof(ObserverAttribute) , RegisterableKind.Observer , true), + // 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(a => member.GetCustomAttribute(a.Type) != null).ToList(); - if (matched.Count == 0) return null; + var matched = _knownAttributes.Where(e => member.GetCustomAttribute(e.Key) != null).ToList(); - var attr = matched[0]; + if (matched.Count == 0) return null; + var (type, info) = matched[0]; - var disallowed = matched.Except(new[] { attr }).Select(a => a.Type).Except(attr.AllowedWith); + var disallowed = matched.Skip(1).Select(a => a.Key).Except(info.AllowedWith); if (disallowed.Any()) throw new InvalidOperationException( - $"{member} marked with {attr.Type} may not be used together with " + string.Join(", ", disallowed)); + $"{member} marked with {type} may not be used together with " + string.Join(", ", disallowed)); - if (attr.PartOfModule == true && !isPartOfModule) throw new InvalidOperationException( - $"{member} marked with {attr.Type} must be part of a module"); - if (attr.PartOfModule == false && isPartOfModule) throw new InvalidOperationException( - $"{member} marked with {attr.Type} must not be part of a module"); + 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 attr; + return info; } } diff --git a/src/gaemstone/ECS/Relation.cs b/src/gaemstone/ECS/Relation.cs new file mode 100644 index 0000000..c9be0a7 --- /dev/null +++ b/src/gaemstone/ECS/Relation.cs @@ -0,0 +1,14 @@ +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/Rule.cs b/src/gaemstone/ECS/Rule.cs new file mode 100644 index 0000000..cb4545f --- /dev/null +++ b/src/gaemstone/ECS/Rule.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using static flecs_hub.flecs; + +namespace gaemstone.ECS; + +public unsafe sealed class Rule + : IEnumerable + , IDisposable +{ + public Universe Universe { get; } + public ecs_rule_t* Handle { get; } + + private Rule(Universe universe, ecs_rule_t* handle) + { Universe = universe; Handle = handle; } + private Rule(Universe universe, ecs_filter_desc_t desc) + : this(universe, ecs_rule_init(universe, &desc)) { } + + public Rule(Universe universe, FilterDesc desc) + : this(universe, desc.ToFlecs()) { } + + public void Dispose() + => ecs_rule_fini(this); + + public override string ToString() + => ecs_rule_str(Handle).FlecsToStringAndFree()!; + + public static implicit operator ecs_rule_t*(Rule q) => q.Handle; + + // IEnumerable implementation + public Iterator Iter() => new(Universe, IteratorType.Rule, ecs_rule_iter(Universe, this)); + public IEnumerator GetEnumerator() => Iter().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/gaemstone/ECS/System.cs b/src/gaemstone/ECS/System.cs index eced974..b357fc2 100644 --- a/src/gaemstone/ECS/System.cs +++ b/src/gaemstone/ECS/System.cs @@ -1,4 +1,7 @@ using System; +using System.Reflection; +using gaemstone.Utility; +using gaemstone.Utility.IL; using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -6,89 +9,72 @@ namespace gaemstone.ECS; [AttributeUsage(AttributeTargets.Method)] public class SystemAttribute : Attribute { - public SystemPhase Phase { get; set; } + public Type Phase { get; set; } public string? Expression { get; set; } - public SystemAttribute() : this(SystemPhase.OnUpdate) { } - public SystemAttribute(SystemPhase phase) => Phase = phase; + public SystemAttribute() : this(typeof(SystemPhase.OnUpdate)) { } + public SystemAttribute(Type phase) => Phase = phase; } -[AttributeUsage(AttributeTargets.Parameter)] -public class SourceAttribute : Attribute +public static class SystemExtensions { - public Type Type { get; } - public SourceAttribute(Type type) => Type = type; -} - -[AttributeUsage(AttributeTargets.Parameter)] -public class HasAttribute : Attribute { } - -[AttributeUsage(AttributeTargets.Parameter)] -public class NotAttribute : Attribute { } - -public enum SystemPhase -{ - PreFrame = ECS_HI_COMPONENT_ID + 65, - - /// - /// This phase contains all the systems that load data into your ECS. - /// This would be a good place to load keyboard and mouse inputs. - /// - OnLoad = ECS_HI_COMPONENT_ID + 66, - - /// - /// 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. - /// - PostLoad = ECS_HI_COMPONENT_ID + 67, - - /// - /// Now that the input is loaded and processed, it's time to get ready to - /// start processing our game logic. Anything that needs to happen after - /// input processing but before processing the game logic can happen here. - /// This can be a good place to prepare the frame, maybe clean up some - /// things from the previous frame, etcetera. - /// - PreUpdate = ECS_HI_COMPONENT_ID + 68, - - /// - /// 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. - /// - OnUpdate = ECS_HI_COMPONENT_ID + 69, - - /// - /// This phase was introduced to deal with validating the state of the game - /// after processing the gameplay systems. Sometimes you entities too close - /// to each other, or the speed of an entity is increased too much. - /// This phase is for righting that wrong. A typical feature to implement - /// in this phase would be collision detection. - /// - OnValidate = ECS_HI_COMPONENT_ID + 70, - - /// - /// When your game logic has been updated, and your validation pass has ran, - /// you may want to apply some corrections. For example, if your collision - /// detection system detected collisions in the OnValidate phase, - /// you may want to move the entities so that they no longer overlap. - /// - PostUpdate = ECS_HI_COMPONENT_ID + 71, - - /// - /// Now that all of the frame data is computed, validated and corrected for, - /// it is time to prepare the frame for rendering. Any systems that need to - /// run before rendering, but after processing the game logic should go here. - /// A good example would be a system that calculates transform matrices from - /// a scene graph. - /// - PreStore = ECS_HI_COMPONENT_ID + 72, - - /// - /// 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. - /// - OnStore = ECS_HI_COMPONENT_ID + 73, - - PostFrame = ECS_HI_COMPONENT_ID + 74, + public static unsafe EntityRef RegisterSystem(this Universe universe, + string name, Entity phase, QueryDesc query, Action callback) + { + query.Name = name; + var desc = new ecs_system_desc_t { + query = query.ToFlecs(), + entity = new EntityBuilder(universe, name) + .Add(phase) + .Add(phase) + .Build(), + binding_ctx = (void*)CallbackContextHelper.Create(universe, callback), + callback = new() { Data = new() { Pointer = &CallbackContextHelper.Callback } }, + }; + var entity = ecs_system_init(universe, &desc); + return new(universe, new(entity)); + } + + public static EntityRef RegisterSystem(this Universe universe, Delegate action) + { + var attr = action.Method.Get(); + var query = new QueryDesc(); + + if (action is Action iterAction) { + query.Expression = attr?.Expression ?? throw new ArgumentException( + "System must specify expression in SystemAttribute", nameof(action)); + } else { + var method = action.GetType().GetMethod("Invoke")!; + var gen = IterActionGenerator.GetOrBuild(universe, method); + if (attr?.Expression != null) query.Expression = attr.Expression; + else query.Terms = gen.Terms; + iterAction = iter => gen.RunWithTryCatch(action.Target, iter); + } + + var phase = universe.Lookup(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); + return universe.RegisterSystem(action.Method.Name, phase, query, iterAction); + } + + public static EntityRef RegisterSystem(this Universe universe, + object? instance, MethodInfo method) + { + var attr = method.Get(); + var query = new QueryDesc(); + Action iterAction; + + 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)); + iterAction = (Action)Delegate.CreateDelegate(typeof(Action), instance, method); + } else { + var gen = IterActionGenerator.GetOrBuild(universe, method); + if (attr?.Expression == null) query.Terms = gen.Terms; + else query.Expression = attr.Expression; + iterAction = iter => gen.RunWithTryCatch(instance, iter); + } + + var phase = universe.Lookup(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); + return universe.RegisterSystem(method.Name, phase, query, iterAction); + } } diff --git a/src/gaemstone/ECS/SystemPhase.cs b/src/gaemstone/ECS/SystemPhase.cs new file mode 100644 index 0000000..ca9aff2 --- /dev/null +++ b/src/gaemstone/ECS/SystemPhase.cs @@ -0,0 +1,69 @@ +namespace gaemstone.ECS; + +// TODO: We *can* use path lookup here, but we have to look in "flecs.pipeline" instead of "flecs.core". +public static class SystemPhase +{ + [BuiltIn(256 + 65)] 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 { } + + /// + /// 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 { } + + /// + /// Now that the input is loaded and processed, it's time to get ready to + /// start processing our game logic. Anything that needs to happen after + /// input processing but before processing the game logic can happen here. + /// This can be a good place to prepare the frame, maybe clean up some + /// things from the previous frame, etcetera. + /// + [BuiltIn(256 + 68)] 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 { } + + /// + /// This phase was introduced to deal with validating the state of the game + /// after processing the gameplay systems. Sometimes you entities too close + /// to each other, or the speed of an entity is increased too much. + /// This phase is for righting that wrong. A typical feature to implement + /// in this phase would be collision detection. + /// + [BuiltIn(256 + 70)] public struct OnValidate { } + + /// + /// When your game logic has been updated, and your validation pass has ran, + /// you may want to apply some corrections. For example, if your collision + /// detection system detected collisions in the OnValidate phase, + /// you may want to move the entities so that they no longer overlap. + /// + [BuiltIn(256 + 71)] public struct PostUpdate { } + + /// + /// Now that all of the frame data is computed, validated and corrected for, + /// it is time to prepare the frame for rendering. Any systems that need to + /// run before rendering, but after processing the game logic should go here. + /// A good example would be a system that calculates transform matrices from + /// a scene graph. + /// + [BuiltIn(256 + 72)] 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 { } + + [BuiltIn(256 + 74)] public struct PostFrame { } +} diff --git a/src/gaemstone/ECS/Tag.cs b/src/gaemstone/ECS/Tag.cs new file mode 100644 index 0000000..0249cbb --- /dev/null +++ b/src/gaemstone/ECS/Tag.cs @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..8887279 --- /dev/null +++ b/src/gaemstone/ECS/Term.cs @@ -0,0 +1,134 @@ +using System; +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; } + public TermID? Source { get; set; } + public TermID? First { get; set; } + public TermID? Second { get; set; } + public TermInOutKind InOut { get; set; } + public TermOperKind Oper { get; set; } + public IdentifierFlags Flags { get; set; } + + public Term() { } + public Term(Identifier id) => ID = id; + 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 ecs_term_t ToFlecs() => new() { + id = ID, + src = Source?.ToFlecs() ?? default, + first = First?.ToFlecs() ?? default, + second = Second?.ToFlecs() ?? default, + inout = (ecs_inout_kind_t)InOut, + oper = (ecs_oper_kind_t)Oper, + id_flags = (ecs_id_t)(ulong)Flags, + }; +} + +public enum TermInOutKind +{ + Default = ecs_inout_kind_t.EcsInOutDefault, + None = ecs_inout_kind_t.EcsInOutNone, + InOut = ecs_inout_kind_t.EcsInOut, + In = ecs_inout_kind_t.EcsIn, + Out = ecs_inout_kind_t.EcsOut, +} + +public enum TermOperKind +{ + And = ecs_oper_kind_t.EcsAnd, + Or = ecs_oper_kind_t.EcsOr, + Not = ecs_oper_kind_t.EcsNot, + Optional = ecs_oper_kind_t.EcsOptional, + AndFrom = ecs_oper_kind_t.EcsAndFrom, + OrFrom = ecs_oper_kind_t.EcsOrFrom, + NotFrom = ecs_oper_kind_t.EcsNotFrom, +} + +public class TermID +{ + public static TermID This { get; } = new("$This"); + + public Entity ID { get; } + public string? Name { get; } + public Entity Traverse { get; set; } + public TermTraversalFlags Flags { get; set; } + + public TermID(Entity id) + => ID = id; + public TermID(string name) + { + if (name[0] == '$') { + Name = name[1..]; + Flags = TermTraversalFlags.IsVariable; + } else Name = name; + } + + public static implicit operator TermID(EntityRef entity) => new(entity); + public static implicit operator TermID(Entity entity) => new(entity); + public static implicit operator TermID(string name) => new(name); + + public ecs_term_id_t ToFlecs() => new() { + id = ID, + name = Name.FlecsToCString(), + trav = Traverse, + flags = (ecs_flags32_t)(uint)Flags + }; +} + +[Flags] +public enum TermTraversalFlags : uint +{ + /// Match on self. + Self = EcsSelf, + /// Match by traversing upwards. + Up = EcsUp, + /// Match by traversing downwards (derived, cannot be set). + Down = EcsDown, + /// Sort results breadth first. + Cascade = EcsCascade, + /// Short for up(ChildOf). + Parent = EcsParent, + /// Term id is a variable. + IsVariable = EcsIsVariable, + /// Term id is an entity. + IsEntity = EcsIsEntity, + /// Prevent observer from triggering on term. + Filter = EcsFilter, +} diff --git a/src/gaemstone/ECS/Universe+Lookup.cs b/src/gaemstone/ECS/Universe+Lookup.cs new file mode 100644 index 0000000..920182d --- /dev/null +++ b/src/gaemstone/ECS/Universe+Lookup.cs @@ -0,0 +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 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)); +} diff --git a/src/gaemstone/ECS/Universe+Modules.cs b/src/gaemstone/ECS/Universe+Modules.cs deleted file mode 100644 index 889b603..0000000 --- a/src/gaemstone/ECS/Universe+Modules.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Reflection; -using gaemstone.Utility; - -namespace gaemstone.ECS; - -public unsafe partial class Universe -{ - public void RegisterModule() where T : class - => RegisterModule(typeof(T)); - public void RegisterModule(Type type) - { - var builder = new ModuleBuilder(this, type); - - builder.UnmetDependencies.ExceptWith(Modules._modules.Keys); - if (builder.UnmetDependencies.Count > 0) { - // If builder has unmet dependencies, defer the registration. - Modules._deferred.Add(type, builder); - } else { - // Otherwise register it right away, .. - Modules._modules.Add(type, new ModuleInfo(builder)); - // .. and tell other deferred modules this one is now loaded. - RemoveDependency(builder.Type); - } - } - - private void RemoveDependency(Type type) - { - var resolved = Modules._deferred.Values - .Where(d => d.UnmetDependencies.Remove(type) - && d.UnmetDependencies.Count == 0) - .ToArray(); - foreach (var builder in resolved) { - Modules._deferred.Remove(builder.Type); - Modules._modules.Add(builder.Type, new ModuleInfo(builder)); - RemoveDependency(builder.Type); - } - } - - public class UniverseModules - { - internal readonly Dictionary _modules = new(); - internal readonly Dictionary _deferred = new(); - } - - public class ModuleInfo - { - public Universe Universe { get; } - public object Instance { get; } - - public IReadOnlyList Relations { get; } - public IReadOnlyList Components { get; } - public IReadOnlyList Tags { get; } - public IReadOnlyList Entities { get; } - public IReadOnlyList Systems { get; } - public IReadOnlyList Observers { get; } - - internal ModuleInfo(ModuleBuilder builder) - { - Universe = builder.Universe; - Instance = builder.HasSimpleConstructor - ? Activator.CreateInstance(builder.Type)! - : Activator.CreateInstance(builder.Type, Universe)!; - - Relations = builder.Relations .Select(Universe.RegisterRelation).ToImmutableList(); - Components = builder.Components.Select(Universe.RegisterComponent).ToImmutableList(); - Tags = builder.Tags .Select(Universe.RegisterTag).ToImmutableList(); - Entities = builder.Entities .Select(Universe.RegisterEntity).ToImmutableList(); - - Systems = builder.Systems .Select(s => Universe.RegisterSystem(Instance, s)).ToImmutableList(); - Observers = builder.Observers.Select(s => Universe.RegisterObserver(Instance, s)).ToImmutableList(); - } - } - - public class ModuleBuilder - { - public Universe Universe { get; } - public Type Type { get; } - public IReadOnlyList DependsOn { get; } - public bool HasSimpleConstructor { get; } - - public IReadOnlyList Relations { get; } - public IReadOnlyList Components { get; } - public IReadOnlyList Tags { get; } - public IReadOnlyList Entities { get; } - public IReadOnlyList Systems { get; } - public IReadOnlyList Observers { get; } - - public HashSet UnmetDependencies { get; } - - internal ModuleBuilder(Universe universe, Type type) - { - 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"); - - Universe = universe; - Type = type; - DependsOn = type.GetMultiple() - .Select(d => d.Target).ToImmutableList(); - - HasSimpleConstructor = type.GetConstructor(Type.EmptyTypes) != null; - var hasUniverseConstructor = type.GetConstructor(new[] { typeof(Universe) }) != null; - if (!HasSimpleConstructor && !hasUniverseConstructor) throw new Exception( - $"Module {Type} must define a public constructor with either no parameters, or a single {nameof(Universe)} parameter"); - - var relations = new List(); - var components = new List(); - var tags = new List(); - var entities = new List(); - var systems = new List(); - var observers = new List(); - - foreach (var member in Type.GetNestedTypes().Concat(Type.GetMethods())) { - var info = member.GetRegisterableInfo(out var _); - if (info == null) continue; - switch (info.Kind) { - case RegisterableKind.Entity: entities.Add((Type)member); break; - case RegisterableKind.Tag: tags.Add((Type)member); break; - case RegisterableKind.Component: components.Add((Type)member); break; - case RegisterableKind.Relation: relations.Add((Type)member); break; - case RegisterableKind.System: systems.Add((MethodInfo)member); break; - case RegisterableKind.Observer: observers.Add((MethodInfo)member); break; - default: throw new InvalidOperationException(); - } - } - - var elements = new IList[] { entities, tags, components, relations, systems, observers }; - if (elements.Sum(l => l.Count) == 0) throw new Exception( - "Module must define at least one ECS related type or method"); - - Relations = relations.AsReadOnly(); - Components = components.AsReadOnly(); - Tags = tags.AsReadOnly(); - Entities = entities.AsReadOnly(); - Systems = systems.AsReadOnly(); - Observers = observers.AsReadOnly(); - - UnmetDependencies = DependsOn.ToHashSet(); - } - } -} diff --git a/src/gaemstone/ECS/Universe+Observers.cs b/src/gaemstone/ECS/Universe+Observers.cs deleted file mode 100644 index a9a1d63..0000000 --- a/src/gaemstone/ECS/Universe+Observers.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Reflection; -using gaemstone.Utility; -using gaemstone.Utility.IL; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe partial class Universe -{ - public ObserverInfo RegisterObserver(Action callback, string expression, - ObserverEvent @event, string? name = null) - => RegisterObserver(name ?? callback.Method.Name, expression, @event, new() { expr = expression }, callback); - public ObserverInfo RegisterObserver(Action callback, ecs_filter_desc_t filter, - ObserverEvent @event, string? name = null) - => RegisterObserver(name ?? callback.Method.Name, null, @event, filter, callback); - - public ObserverInfo RegisterObserver(string name, string? expression, - ObserverEvent @event, ecs_filter_desc_t filter, Action callback) - { - var observerDesc = default(ecs_observer_desc_t); - observerDesc.filter = filter; - observerDesc.events[0] = (ecs_entity_t)(ecs_id_t)(uint)@event; - observerDesc.binding_ctx = (void*)UniverseSystems.CreateSystemCallbackContext(this, callback); - observerDesc.callback.Data.Pointer = &UniverseSystems.SystemCallback; - observerDesc.entity = Create(name); - - var entity = new Entity(this, ecs_observer_init(Handle, &observerDesc)); - var observer = new ObserverInfo(this, entity, name, expression, @event, filter, callback); - Observers._observers.Add(observer); - return observer; - } - - public ObserverInfo RegisterObserver(object? instance, MethodInfo method) - { - var attr = method.Get() ?? throw new ArgumentException( - "Observer must specify ObserverAttribute", nameof(method)); - - var param = method.GetParameters(); - if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) { - if (attr.Expression == null) throw new Exception( - "Observer must specify expression in ObserverAttribute"); - var action = (Action)Delegate.CreateDelegate(typeof(Action), instance, method); - return RegisterObserver(method.Name, attr.Expression, attr.Event, - new() { expr = attr.Expression }, action); - } else { - var gen = QueryActionGenerator.GetOrBuild(this, method); - var filter = (attr.Expression == null) ? gen.Filter : new() { expr = attr.Expression }; - return RegisterObserver(method.Name, attr.Expression, attr.Event, - filter, iter => gen.RunWithTryCatch(instance, iter)); - } - } - - public class UniverseObservers - : IReadOnlyCollection - { - internal readonly List _observers = new(); - - // IReadOnlyCollection implementation - public int Count => _observers.Count; - public IEnumerator GetEnumerator() => _observers.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - public class ObserverInfo - { - public Universe Universe { get; } - public Entity Entity { get; } - - public string Name { get; } - public string? Expression { get; } - - public ObserverEvent Event { get; } - public ecs_filter_desc_t Filter { get; } - public Action Callback { get; } - - internal ObserverInfo(Universe universe, Entity entity, string name, string? expression, - ObserverEvent @event, ecs_filter_desc_t filter, Action callback) - { - Universe = universe; - Entity = entity; - - Name = name; - Expression = expression; - - Event = @event; - Filter = filter; - Callback = callback; - } - } -} diff --git a/src/gaemstone/ECS/Universe+Systems.cs b/src/gaemstone/ECS/Universe+Systems.cs deleted file mode 100644 index 436e1f9..0000000 --- a/src/gaemstone/ECS/Universe+Systems.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Threading; -using gaemstone.Utility; -using gaemstone.Utility.IL; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe partial class Universe -{ - public SystemInfo RegisterSystem(Action callback, string expression, - SystemPhase? phase = null, string? name = null) - => RegisterSystem(name ?? callback.Method.Name, expression, phase ?? SystemPhase.OnUpdate, new() { expr = expression }, callback); - public SystemInfo RegisterSystem(Action callback, ecs_filter_desc_t filter, - SystemPhase? phase = null, string? name = null) - => RegisterSystem(name ?? callback.Method.Name, null, phase ?? SystemPhase.OnUpdate, filter, callback); - - public SystemInfo RegisterSystem(string name, string? expression, - SystemPhase phase, ecs_filter_desc_t filter, Action callback) - { - var _phase = (ecs_entity_t)(ecs_id_t)(uint)phase; - var entityDesc = default(ecs_entity_desc_t); - entityDesc.name = name; - entityDesc.add[0] = _phase.Data != 0 ? Identifier.Pair(EcsDependsOn, _phase) : default; - entityDesc.add[1] = _phase; - // TODO: Provide a nice way to create these entity descriptors. - - var systemDesc = default(ecs_system_desc_t); - systemDesc.entity = Create(entityDesc); - systemDesc.binding_ctx = (void*)UniverseSystems.CreateSystemCallbackContext(this, callback); - systemDesc.callback.Data.Pointer = &UniverseSystems.SystemCallback; - systemDesc.query.filter = filter; - - var entity = new Entity(this, ecs_system_init(Handle, &systemDesc)); - var system = new SystemInfo(this, entity, name, expression, phase, filter, callback); - Systems._systems.Add(system); - return system; - } - - public SystemInfo RegisterSystem(Delegate action) - { - var name = action.Method.Name; - var attr = action.Method.Get(); - var phase = attr?.Phase ?? SystemPhase.OnUpdate; - - if (action is Action iterAction) { - if (attr?.Expression == null) throw new Exception( - "System must specify expression in SystemAttribute"); - return RegisterSystem(name, attr.Expression, phase, new() { expr = attr.Expression }, iterAction); - } else { - var method = action.GetType().GetMethod("Invoke")!; - var gen = QueryActionGenerator.GetOrBuild(this, method); - var filter = (attr?.Expression == null) ? gen.Filter : new() { expr = attr.Expression }; - return RegisterSystem(name, attr?.Expression, phase, filter, - iter => gen.RunWithTryCatch(action.Target, iter)); - } - } - - public SystemInfo RegisterSystem(object? instance, MethodInfo method) - { - var attr = method.Get(); - var phase = attr?.Phase ?? SystemPhase.OnUpdate; - - var param = method.GetParameters(); - if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) { - if (attr?.Expression == null) throw new Exception( - "System must specify expression in SystemAttribute"); - var action = (Action)Delegate.CreateDelegate(typeof(Action), instance, method); - return RegisterSystem(method.Name, attr.Expression, phase, new() { expr = attr.Expression }, action); - } else { - var gen = QueryActionGenerator.GetOrBuild(this, method); - var filter = (attr?.Expression == null) ? gen.Filter : new() { expr = attr.Expression }; - return RegisterSystem(method.Name, attr?.Expression, phase, filter, - iter => gen.RunWithTryCatch(instance, iter)); - } - } - - public class UniverseSystems - : IReadOnlyCollection - { - public readonly struct SystemCallbackContext - { - public Universe Universe { get; } - public Action Callback { get; } - - public SystemCallbackContext(Universe universe, Action callback) - { Universe = universe; Callback = callback; } - } - - private static SystemCallbackContext[] _systemCallbackContexts = new SystemCallbackContext[64]; - private static int _systemCallbackContextsCount = 0; - - public static nint CreateSystemCallbackContext(Universe universe, Action callback) - { - var data = new SystemCallbackContext(universe, callback); - var count = Interlocked.Increment(ref _systemCallbackContextsCount); - if (count > _systemCallbackContexts.Length) - Array.Resize(ref _systemCallbackContexts, count * 2); - _systemCallbackContexts[count - 1] = data; - return count; - } - - public static SystemCallbackContext GetSystemCallbackContext(nint context) - => _systemCallbackContexts[(int)context - 1]; - - [UnmanagedCallersOnly] - internal static void SystemCallback(ecs_iter_t* iter) - { - var data = GetSystemCallbackContext((nint)iter->binding_ctx); - data.Callback(new Iterator(data.Universe, null, *iter)); - } - - - internal readonly List _systems = new(); - - // IReadOnlyCollection implementation - public int Count => _systems.Count; - public IEnumerator GetEnumerator() => _systems.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - public class SystemInfo - { - public Universe Universe { get; } - public Entity Entity { get; } - - public string Name { get; } - public string? Expression { get; } - - public SystemPhase Phase { get; } - public ecs_filter_desc_t Filter { get; } - public Action Callback { get; } - - internal SystemInfo(Universe universe, Entity entity, string name, string? expression, - SystemPhase phase, ecs_filter_desc_t filter, Action callback) - { - Universe = universe; - Entity = entity; - - Name = name; - Expression = expression; - - Phase = phase; - Filter = filter; - Callback = callback; - } - } -} diff --git a/src/gaemstone/ECS/Universe.cs b/src/gaemstone/ECS/Universe.cs index 8934f0a..23b9881 100644 --- a/src/gaemstone/ECS/Universe.cs +++ b/src/gaemstone/ECS/Universe.cs @@ -1,186 +1,26 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using gaemstone.Utility; using static flecs_hub.flecs; +using static flecs_hub.flecs.Runtime; namespace gaemstone.ECS; -[Entity] -public struct Game { } - -[Component] public unsafe partial class Universe { - // Relationships - public static ecs_entity_t EcsIsA { get; } = pinvoke_EcsIsA(); - public static ecs_entity_t EcsDependsOn { get; } = pinvoke_EcsDependsOn(); - public static ecs_entity_t EcsChildOf { get; } = pinvoke_EcsChildOf(); - public static ecs_entity_t EcsSlotOf { get; } = pinvoke_EcsSlotOf(); - - // Entity Tags - public static ecs_entity_t EcsPrefab { get; } = pinvoke_EcsPrefab(); - - - private readonly Dictionary _byType = new(); - public ecs_world_t* Handle { get; } - public UniverseModules Modules { get; } = new(); - public UniverseSystems Systems { get; } = new(); - public UniverseObservers Observers { get; } = new(); - - public Universe(string[]? args = null) + public Universe(params string[] args) { - [UnmanagedCallersOnly] - static void Abort() => throw new FlecsAbortException(); - - ecs_os_set_api_defaults(); - var api = ecs_os_get_api(); - api.abort_ = new FnPtr_Void { Pointer = &Abort }; - ecs_os_set_api(&api); + var argv = CStrings.CStringArray(args); + Handle = ecs_init_w_args(args.Length, argv); + CStrings.FreeCStrings(argv, args.Length); - if (args?.Length > 0) { - var argv = Runtime.CStrings.CStringArray(args); - Handle = ecs_init_w_args(args.Length, argv); - Runtime.CStrings.FreeCStrings(argv, args.Length); - } else { - Handle = ecs_init(); - } - - RegisterAll(typeof(Universe).Assembly); + RegisterBuiltInLookups(); + this.RegisterEntity(); + this.RegisterComponent(); } public bool Progress(TimeSpan delta) - { - if (Modules._deferred.Count > 0) throw new Exception( - "Modules with unmet dependencies: \n" + - string.Join(" \n", Modules._deferred.Values.Select( - m => m.Type + " is missing " + string.Join(", ", m.UnmetDependencies)))); - return ecs_progress(this, (float)delta.TotalSeconds); - } - - - public Entity TryLookup() - => TryLookup(typeof(T)); - public Entity TryLookup(Type type) - => _byType.TryGetValue(type, out var e) ? new(this, e) : default; - public Entity TryLookup(ecs_entity_t value) - => new(this, ecs_get_alive(this, value)); - public Entity TryLookup(string path) - => new(this, ecs_lookup_path_w_sep(this, default, path, ".", default, true)); - - public Entity Lookup() - => TryLookup().ThrowIfNone(); - public Entity Lookup(Type type) - => TryLookup(type).ThrowIfNone(); - public Entity Lookup(ecs_entity_t value) - => TryLookup(value).ThrowIfNone(); - public Entity Lookup(string path) - => TryLookup(path).ThrowIfNone(); - - - public void RegisterAll(Assembly? from = null) - { - from ??= Assembly.GetEntryAssembly()!; - foreach (var type in from.GetTypes()) { - var info = type.GetRegisterableInfo(out var isPartOfModule); - if (info == null || isPartOfModule) continue; - switch (info.Kind) { - case RegisterableKind.Entity: RegisterEntity(type); break; - case RegisterableKind.Tag: RegisterTag(type); break; - case RegisterableKind.Component: RegisterComponent(type); break; - case RegisterableKind.Relation: RegisterRelation(type); break; - case RegisterableKind.Module: RegisterModule(type); break; - default: throw new InvalidOperationException(); - } - } - } - - public Entity RegisterRelation() - => RegisterRelation(typeof(T)); - public Entity RegisterRelation(Type type) - => throw new NotImplementedException(); - - public Entity RegisterComponent() - => RegisterComponent(typeof(T)); - public Entity RegisterComponent(Type type) - { - var typeInfo = default(ecs_type_info_t); - if (type.IsValueType) { - var wrapper = TypeWrapper.For(type); - if (!wrapper.IsUnmanaged) throw new Exception( - "Component struct must satisfy the unmanaged constraint. " + - "(Must not contain any reference types or structs that contain references.)"); - var structLayout = type.StructLayoutAttribute; - if (structLayout == null || structLayout.Value == LayoutKind.Auto) throw new Exception( - "Component struct must have a StructLayout attribute with LayoutKind sequential or explicit. " + - "This is to ensure that the struct fields are not reorganized by the C# compiler."); - typeInfo.size = wrapper.Size; - typeInfo.alignment = structLayout.Pack; - } else { - typeInfo.size = sizeof(nint); - typeInfo.alignment = sizeof(nint); - } - - var name = type.GetFriendlyName(); - var entityDesc = new ecs_entity_desc_t { name = name, symbol = name }; - var componentDesc = new ecs_component_desc_t { entity = Create(entityDesc), type = typeInfo }; - - var id = ecs_component_init(Handle, &componentDesc); - _byType[type] = id; - // TODO: SetHooks(hooks, id); - var entity = new Entity(this, id); - - if (type.Has()) { - if (type.IsValueType) entity.Add(entity); - else entity.Set(type, Activator.CreateInstance(type)!); - } - - return entity; - } - - public Entity RegisterTag() - where T : unmanaged - => RegisterTag(typeof(T)); - public Entity RegisterTag(Type type) - { - if (!type.IsValueType || type.IsPrimitive || type.GetFields().Length > 0) - throw new Exception("Tag must be an empty, used-defined struct."); - var entity = Create(type.GetFriendlyName()); - _byType.Add(type, entity); - return entity; - } - - public Entity RegisterEntity() - where T : unmanaged - => RegisterEntity(typeof(T)); - public Entity RegisterEntity(Type type) - { - if (!type.IsValueType || type.IsPrimitive || type.GetFields().Length > 0) - throw new Exception("Entity must be an empty, used-defined struct."); - var id = type.Get()?.ID ?? 0; - var entity = Create(new ecs_entity_desc_t { - name = type.GetFriendlyName(), - id = new() { Data = id }, - }); - _byType.Add(type, entity); - return entity; - } - - - public Entity Create() - => Create(new ecs_entity_desc_t()); - public Entity Create(string name) - => Create(new ecs_entity_desc_t { name = name }); - public Entity Create(ecs_entity_desc_t desc) - { - var entity = ecs_entity_init(Handle, &desc); - return new Entity(this, entity).ThrowIfNone(); - } - + => ecs_progress(this, (float)delta.TotalSeconds); public static implicit operator ecs_world_t*(Universe w) => w.Handle; } diff --git a/src/gaemstone/GlobalTransform.cs b/src/gaemstone/GlobalTransform.cs deleted file mode 100644 index d4b9d83..0000000 --- a/src/gaemstone/GlobalTransform.cs +++ /dev/null @@ -1,13 +0,0 @@ -using gaemstone.ECS; -using Silk.NET.Maths; - -namespace gaemstone; - -[Component] -public struct GlobalTransform -{ - public Matrix4X4 Value; - public GlobalTransform(Matrix4X4 value) => Value = value; - public static implicit operator GlobalTransform(in Matrix4X4 value) => new(value); - public static implicit operator Matrix4X4(in GlobalTransform index) => index.Value; -} diff --git a/src/gaemstone/Utility/CStringExtensions.cs b/src/gaemstone/Utility/CStringExtensions.cs index 660e0d1..2d5b9b5 100644 --- a/src/gaemstone/Utility/CStringExtensions.cs +++ b/src/gaemstone/Utility/CStringExtensions.cs @@ -1,20 +1,21 @@ using System.Runtime.InteropServices; using static flecs_hub.flecs; +using static flecs_hub.flecs.Runtime; namespace gaemstone; -internal static class CStringExtensions +internal static unsafe class CStringExtensions { - public static string ToStringAndFree(this Runtime.CString str) - { - var result = Marshal.PtrToStringAnsi(str)!; - Marshal.FreeHGlobal(str); - return result; - } + public static CString FlecsToCString(this string? str) + => (str != null) ? new(Marshal.StringToHGlobalAnsi(str)) : default; - public static void Set(this ref Runtime.CString str, string? value) - { - if (!str.IsNull) Marshal.FreeHGlobal(str); - str = (value != null) ? new(Marshal.StringToHGlobalAnsi(value)) : default; - } + + public static void FlecsFree(this CString str) + { if (!str.IsNull) ecs_os_get_api().free_.Data.Pointer((void*)(nint)str); } + + public static string? FlecsToString(this CString str) + => !str.IsNull ? Marshal.PtrToStringAnsi((nint)str)! : null; + + public static string? FlecsToStringAndFree(this CString str) + { var result = str.FlecsToString(); str.FlecsFree(); return result; } } diff --git a/src/gaemstone/Utility/CallbackContextHelper.cs b/src/gaemstone/Utility/CallbackContextHelper.cs new file mode 100644 index 0000000..0140f9f --- /dev/null +++ b/src/gaemstone/Utility/CallbackContextHelper.cs @@ -0,0 +1,43 @@ +using System; +using System.Runtime.InteropServices; +using gaemstone.ECS; +using static flecs_hub.flecs; + +namespace gaemstone.Utility; + +public static class CallbackContextHelper +{ + public readonly struct CallbackContext + { + public Universe Universe { get; } + public Action Callback { get; } + + public CallbackContext(Universe universe, Action callback) + { Universe = universe; Callback = callback; } + } + + 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)); + } +} diff --git a/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs b/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs index 9a6c3ca..d673fa3 100644 --- a/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs +++ b/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs @@ -27,9 +27,9 @@ public class ILGeneratorWrapper var sb = new StringBuilder(); sb.AppendLine("Parameters:"); foreach (var (param, index) in _method.GetParameters().Select((p, i) => (p, i))) - sb.AppendLine($" Argument({index}, {param.ParameterType.Name})"); + sb.AppendLine($" Argument({index}, {param.ParameterType.GetFriendlyName()})"); sb.AppendLine("Return:"); - sb.AppendLine($" {_method.ReturnType.Name}"); + sb.AppendLine($" {_method.ReturnType.GetFriendlyName()}"); sb.AppendLine(); sb.AppendLine("Locals:"); @@ -123,6 +123,7 @@ public class ILGeneratorWrapper public void LoadLength(ILocal array) { Load(array); LoadLength(); } public void LoadObj(Type type) => Emit(OpCodes.Ldobj, type); + public void LoadObj() where T : struct => LoadObj(typeof(T)); public void LoadObj(ILocal local) { LoadAddr(local); LoadObj(local.LocalType); } public void LoadObj(IArgument arg) { LoadAddr(arg); LoadObj(arg.ArgumentType); } @@ -166,7 +167,7 @@ public class ILGeneratorWrapper public void Increment(ILocal local) { Load(local); LoadConst(1); Add(); Store(local); } public void Init(Type type) => Emit(OpCodes.Initobj, type); - public void Init() where T : struct => Emit(OpCodes.Initobj, typeof(T)); + public void Init() where T : struct => Init(typeof(T)); public void New(ConstructorInfo constructor) => Emit(OpCodes.Newobj, constructor); public void New(Type type) => New(type.GetConstructors().Single()); @@ -244,7 +245,7 @@ public class ILGeneratorWrapper public int Index { get; } public Type ArgumentType { get; } public ArgumentImpl(int index, Type type) { Index = index; ArgumentType = type; } - public override string ToString() => $"Argument({Index}, {ArgumentType.Name})"; + public override string ToString() => $"Argument({Index}, {ArgumentType.GetFriendlyName()})"; } internal class ArgumentImpl : ArgumentImpl, IArgument { public ArgumentImpl(int index) : base(index, typeof(T)) { } } @@ -255,7 +256,7 @@ public class ILGeneratorWrapper public string? Name { get; } public Type LocalType => Builder.LocalType; public LocalImpl(LocalBuilder builder, string? name) { Builder = builder; Name = name; } - public override string ToString() => $"Local({Builder.LocalIndex}, {LocalType.Name}){(Name != null ? $" // {Name}" : "")}"; + public override string ToString() => $"Local({Builder.LocalIndex}, {LocalType.GetFriendlyName()}){(Name != null ? $" // {Name}" : "")}"; } internal class LocalImpl : LocalImpl, ILocal { public LocalImpl(LocalBuilder builder, string? name) : base(builder, name) { } } diff --git a/src/gaemstone/Utility/IL/QueryActionGenerator.cs b/src/gaemstone/Utility/IL/IterActionGenerator.cs similarity index 81% rename from src/gaemstone/Utility/IL/QueryActionGenerator.cs rename to src/gaemstone/Utility/IL/IterActionGenerator.cs index c345db0..241f132 100644 --- a/src/gaemstone/Utility/IL/QueryActionGenerator.cs +++ b/src/gaemstone/Utility/IL/IterActionGenerator.cs @@ -6,12 +6,13 @@ using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using gaemstone.ECS; -using static flecs_hub.flecs; namespace gaemstone.Utility.IL; -public unsafe class QueryActionGenerator +public unsafe class IterActionGenerator { + private static readonly ConstructorInfo _entityRefCtor = typeof(EntityRef).GetConstructors().Single(); + private static readonly PropertyInfo _iteratorUniverseProp = typeof(Iterator).GetProperty(nameof(Iterator.Universe))!; private static readonly PropertyInfo _iteratorDeltaTimeProp = typeof(Iterator).GetProperty(nameof(Iterator.DeltaTime))!; private static readonly PropertyInfo _iteratorCountProp = typeof(Iterator).GetProperty(nameof(Iterator.Count))!; @@ -22,19 +23,19 @@ public unsafe class QueryActionGenerator private static readonly MethodInfo _handleFromIntPtrMethod = typeof(GCHandle).GetMethod(nameof(GCHandle.FromIntPtr))!; private static readonly PropertyInfo _handleTargetProp = typeof(GCHandle).GetProperty(nameof(GCHandle.Target))!; - private static readonly ConditionalWeakTable _cache = new(); + private static readonly ConditionalWeakTable _cache = new(); private static readonly Dictionary, ILocal>> _uniqueParameters = new() { - [typeof(Iterator)] = (IL, iter, i) => { IL.Load(iter); }, - [typeof(Universe)] = (IL, iter, i) => { IL.Load(iter, _iteratorUniverseProp); }, - [typeof(TimeSpan)] = (IL, iter, i) => { IL.Load(iter, _iteratorDeltaTimeProp); }, - [typeof(Entity)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iteratorEntityMethod); }, + [typeof(Iterator)] = (IL, iter, i) => { IL.Load(iter); }, + [typeof(Universe)] = (IL, iter, i) => { IL.Load(iter, _iteratorUniverseProp); }, + [typeof(TimeSpan)] = (IL, iter, i) => { IL.Load(iter, _iteratorDeltaTimeProp); }, + [typeof(EntityRef)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iteratorEntityMethod); }, }; public Universe Universe { get; } public MethodInfo Method { get; } public ParamInfo[] Parameters { get; } - public ecs_filter_desc_t Filter { get; } + public IReadOnlyList Terms { get; } public Action GeneratedAction { get; } public string ReadableString { get; } @@ -51,7 +52,7 @@ public unsafe class QueryActionGenerator } } - public QueryActionGenerator(Universe universe, MethodInfo method) + public IterActionGenerator(Universe universe, MethodInfo method) { Universe = universe; Method = method; @@ -60,15 +61,14 @@ public unsafe class QueryActionGenerator if (!Parameters.Any(c => c.IsRequired && (c.Kind != ParamKind.Unique))) throw new ArgumentException($"At least one parameter in {method} is required"); - var filter = default(ecs_filter_desc_t); - var name = "<>Query_" + string.Join("_", Parameters.Select(p => p.UnderlyingType.Name)); + var terms = new List(); + var name = "<>Query_" + string.Join("_", Parameters.Select(p => p.UnderlyingType.Name)); var genMethod = new DynamicMethod(name, null, new[] { typeof(object), typeof(Iterator) }); var IL = new ILGeneratorWrapper(genMethod); var instanceArg = IL.Argument(0); var iteratorArg = IL.Argument(1); - var counter = 0; // Counter for fields actually part of the filter terms. var fieldLocals = new ILocal[Parameters.Length]; var tempLocals = new ILocal[Parameters.Length]; @@ -76,51 +76,51 @@ public unsafe class QueryActionGenerator var p = Parameters[i]; if (p.Kind == ParamKind.Unique) continue; - // Update the flecs filter to look for this type. - // Term index is 0-based and field index (used below) is 1-based, so increasing counter here works out. - ref var term = ref filter.terms[counter++]; - term.id = Universe.Lookup(p.UnderlyingType); - term.inout = p.Kind switch { - ParamKind.In => ecs_inout_kind_t.EcsIn, - ParamKind.Out => ecs_inout_kind_t.EcsOut, - ParamKind.Has or ParamKind.Not => ecs_inout_kind_t.EcsInOutNone, - _ => ecs_inout_kind_t.EcsInOut, - }; - term.oper = p.Kind switch { - ParamKind.Not => ecs_oper_kind_t.EcsNot, - _ when !p.IsRequired => ecs_oper_kind_t.EcsOptional, - _ => ecs_oper_kind_t.EcsAnd, - }; - if (p.Source != null) term.src = new() { id = Universe.Lookup(p.Source) }; + // 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, + InOut = p.Kind switch { + ParamKind.In => TermInOutKind.In, + ParamKind.Out => TermInOutKind.Out, + ParamKind.Not or ParamKind.Not => TermInOutKind.None, + _ => default, + }, + Oper = p.Kind switch { + ParamKind.Not => TermOperKind.Not, + _ when !p.IsRequired => TermOperKind.Optional, + _ => default, + }, + }); // Create a Span local and initialize it to iterator.Field(i). var spanType = typeof(Span<>).MakeGenericType(p.FieldType); - fieldLocals[i] = IL.Local(spanType, $"field_{counter}"); + fieldLocals[i] = IL.Local(spanType, $"field_{i}"); if (p.Kind is ParamKind.Has or ParamKind.Not) { // If a "has" or "not" parameter is a struct, we require a temporary local that // we can later load onto the stack when loading the arguments for the action. if (p.ParameterType.IsValueType) { - IL.Comment($"temp_{counter} = default({p.ParameterType});"); + IL.Comment($"temp_{i} = default({p.ParameterType});"); tempLocals[i] = IL.Local(p.ParameterType); IL.LoadAddr(tempLocals[i]); IL.Init(tempLocals[i].LocalType); } } else if (p.IsRequired) { - IL.Comment($"field_{counter} = iterator.Field<{p.FieldType.Name}>({counter})"); + IL.Comment($"field_{i} = iterator.Field<{p.FieldType.Name}>({terms.Count})"); IL.Load(iteratorArg); - IL.LoadConst(counter); + IL.LoadConst(terms.Count); IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType)); IL.Store(fieldLocals[i]); } else { - IL.Comment($"field_{counter} = iterator.FieldIsSet({counter}) ? iterator.Field<{p.FieldType.Name}>({counter}) : default"); + IL.Comment($"field_{i} = iterator.FieldIsSet({terms.Count}) " + + "? iterator.Field<{p.FieldType.Name}>({terms.Count}) : default"); var elseLabel = IL.DefineLabel(); var doneLabel = IL.DefineLabel(); IL.Load(iteratorArg); - IL.LoadConst(counter); + IL.LoadConst(terms.Count); IL.Call(_iteratorFieldIsSetMethod); IL.GotoIfFalse(elseLabel); IL.Load(iteratorArg); - IL.LoadConst(counter); + IL.LoadConst(terms.Count); IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType)); IL.Store(fieldLocals[i]); IL.Goto(doneLabel); @@ -131,7 +131,7 @@ public unsafe class QueryActionGenerator } if (p.Kind == ParamKind.Nullable) { - IL.Comment($"temp_{counter} = default({p.ParameterType});"); + IL.Comment($"temp_{i} = default({p.ParameterType});"); tempLocals[i] = IL.Local(p.ParameterType); IL.LoadAddr(tempLocals[i]); IL.Init(tempLocals[i].LocalType); @@ -150,7 +150,7 @@ public unsafe class QueryActionGenerator for (var i = 0; i < Parameters.Length; i++) { var p = Parameters[i]; if (p.Kind == ParamKind.Unique) { - IL.Comment($"Unique parameter {p.ParameterType}"); + IL.Comment($"Unique parameter {p.ParameterType.GetFriendlyName()}"); _uniqueParameters[p.ParameterType](IL, iteratorArg, currentLocal); } else if (p.Kind is ParamKind.Has or ParamKind.Not) { if (p.ParameterType.IsValueType) @@ -161,7 +161,7 @@ public unsafe class QueryActionGenerator var spanItemMethod = spanType.GetProperty("Item")!.GetMethod!; var spanLengthMethod = spanType.GetProperty("Length")!.GetMethod!; - IL.Comment($"Parameter {p.ParameterType}"); + IL.Comment($"Parameter {p.ParameterType.GetFriendlyName()}"); if (p.IsByRef) { IL.LoadAddr(fieldLocals[i]!); IL.Load(currentLocal); @@ -192,7 +192,7 @@ public unsafe class QueryActionGenerator } if (!p.UnderlyingType.IsValueType) { - IL.Comment($"Convert nint to {p.UnderlyingType}"); + IL.Comment($"Convert nint to {p.UnderlyingType.GetFriendlyName()}"); IL.Call(_handleFromIntPtrMethod); IL.Store(handleLocal!); IL.LoadAddr(handleLocal!); @@ -206,13 +206,13 @@ public unsafe class QueryActionGenerator IL.Return(); - Filter = filter; + Terms = terms.AsReadOnly(); GeneratedAction = genMethod.CreateDelegate>(); ReadableString = IL.ToReadableString(); } - public static QueryActionGenerator GetOrBuild(Universe universe, MethodInfo method) - =>_cache.GetValue(method, m => new QueryActionGenerator(universe, m)); + public static IterActionGenerator GetOrBuild(Universe universe, MethodInfo method) + =>_cache.GetValue(method, m => new IterActionGenerator(universe, m)); public class ParamInfo { @@ -245,6 +245,7 @@ public unsafe class QueryActionGenerator // If the underlying type has EntityAttribute, it's a singleton. if (UnderlyingType.Has()) Source = underlyingType; if (Info.Get() is SourceAttribute attr) Source = attr.Type; + // TODO: Needs support for the new attributes. } public static ParamInfo Build(ParameterInfo info, int index)