Full ECS wrapper refactor

wip/source-generators
copygirl 2 years ago
parent 12f3ff451c
commit b4a769e3a6
  1. 8
      src/Immersion/ObserverTest.cs
  2. 70
      src/Immersion/Program.cs
  3. 2
      src/flecs-cs
  4. 2
      src/gaemstone.Bloxel/ChunkPaletteStorage.cs
  5. 25
      src/gaemstone.Bloxel/ChunkPos.cs
  6. 34
      src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs
  7. 22
      src/gaemstone.Bloxel/Components/CoreComponents.cs
  8. 16
      src/gaemstone.Bloxel/Constants.cs
  9. 23
      src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs
  10. 0
      src/gaemstone.Bloxel/Systems/SurfaceGrassGenerator.cs.disabled
  11. 3
      src/gaemstone.Client/Components/CameraComponents.cs
  12. 62
      src/gaemstone.Client/Components/RenderingComponents.cs
  13. 14
      src/gaemstone.Client/Mesh.cs
  14. 8
      src/gaemstone.Client/MeshManager.cs
  15. 10
      src/gaemstone.Client/Systems/FreeCameraController.cs
  16. 6
      src/gaemstone.Client/Systems/Input.cs
  17. 16
      src/gaemstone.Client/Systems/Renderer.cs
  18. 4
      src/gaemstone.Client/Systems/Windowing.cs
  19. 14
      src/gaemstone.Client/Texture.cs
  20. 36
      src/gaemstone.Client/TextureCoords4.cs
  21. 6
      src/gaemstone.Client/TextureManager.cs
  22. 17
      src/gaemstone/Components/TransformComponents.cs
  23. 12
      src/gaemstone/ECS/Attributes.cs
  24. 44
      src/gaemstone/ECS/Component.cs
  25. 43
      src/gaemstone/ECS/Entity+AddRemove.cs
  26. 86
      src/gaemstone/ECS/Entity+GetSet.cs
  27. 234
      src/gaemstone/ECS/Entity.cs
  28. 81
      src/gaemstone/ECS/EntityBuilder.cs
  29. 20
      src/gaemstone/ECS/EntityDesc.cs.disabled
  30. 73
      src/gaemstone/ECS/Filter.cs
  31. 41
      src/gaemstone/ECS/Flecs.cs
  32. 24
      src/gaemstone/ECS/FlecsException.cs
  33. 15
      src/gaemstone/ECS/Game.cs
  34. 99
      src/gaemstone/ECS/Identifier.cs
  35. 16
      src/gaemstone/ECS/Iterator.cs
  36. 122
      src/gaemstone/ECS/Module.cs
  37. 80
      src/gaemstone/ECS/Observer.cs
  38. 37
      src/gaemstone/ECS/Query.cs
  39. 67
      src/gaemstone/ECS/Registerable.cs
  40. 14
      src/gaemstone/ECS/Relation.cs
  41. 35
      src/gaemstone/ECS/Rule.cs
  42. 146
      src/gaemstone/ECS/System.cs
  43. 69
      src/gaemstone/ECS/SystemPhase.cs
  44. 23
      src/gaemstone/ECS/Tag.cs
  45. 134
      src/gaemstone/ECS/Term.cs
  46. 62
      src/gaemstone/ECS/Universe+Lookup.cs
  47. 147
      src/gaemstone/ECS/Universe+Modules.cs
  48. 93
      src/gaemstone/ECS/Universe+Observers.cs
  49. 152
      src/gaemstone/ECS/Universe+Systems.cs
  50. 178
      src/gaemstone/ECS/Universe.cs
  51. 13
      src/gaemstone/GlobalTransform.cs
  52. 25
      src/gaemstone/Utility/CStringExtensions.cs
  53. 43
      src/gaemstone/Utility/CallbackContextHelper.cs
  54. 11
      src/gaemstone/Utility/IL/ILGeneratorWrapper.cs
  55. 85
      src/gaemstone/Utility/IL/IterActionGenerator.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!");
}

@ -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<Game>();
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<Windowing>();
universe.RegisterModule<gaemstone.Client.Systems.Windowing>();
game.Set(new Canvas(window.CreateOpenGL()));
game.Set(new GameWindow(window));
TextureManager.Initialize(universe);
universe.RegisterModule<gaemstone.Components.TransformComponents>();
universe.RegisterComponent<Mesh>();
universe.RegisterComponent<gaemstone.Client.Texture>();
universe.RegisterComponent<TextureCoords4>();
universe.RegisterModule<gaemstone.Client.Components.RenderingComponents>();
universe.RegisterModule<gaemstone.Client.Systems.Renderer>();
universe.RegisterModule<CameraComponents>();
universe.RegisterModule<FreeCameraController>();
universe.RegisterModule<Input>();
universe.RegisterModule<Renderer>();
TextureManager.Initialize(universe);
universe.RegisterModule<gaemstone.Client.Systems.Input>();
game.Set(new RawInput());
// TODO: Find a way to automatically register chunk storage.
universe.RegisterComponent<ChunkPaletteStorage<ecs_entity_t>>();
universe.RegisterAll(typeof(Chunk).Assembly);
universe.RegisterAll(typeof(Program).Assembly);
universe.RegisterModule<gaemstone.Client.Components.CameraComponents>();
universe.RegisterModule<gaemstone.Client.Systems.FreeCameraController>();
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<gaemstone.Bloxel.Components.CoreComponents>();
universe.RegisterModule<gaemstone.Bloxel.Systems.BasicWorldGenerator>();
universe.RegisterModule<gaemstone.Bloxel.Client.Systems.ChunkMeshGenerator>();
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<ecs_entity_t>(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<ObserverTest>();
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");
});

@ -1 +1 @@
Subproject commit 1e36559cffa5ab2fb755feef563c4294a6f32b0c
Subproject commit f5eea6704075601a674e3d759fdadc306b75573d

@ -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<T>
{
const int Size = 16 * 16 * 16;

@ -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<float> GetOrigin() => new(
X << Chunk.BIT_SHIFT, Y << Chunk.BIT_SHIFT, Z << Chunk.BIT_SHIFT);
X << ChunkBitShift, Y << ChunkBitShift, Z << ChunkBitShift);
public Vector3D<float> 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<float> 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));
}

@ -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<float>[] _uvs = new Vector2D<float>[StartingCapacity];
[System]
public void GenerateChunkMeshes(Universe universe, Entity entity,
in Chunk chunk, ChunkPaletteStorage<ecs_entity_t> 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<ecs_entity_t> centerStorage)
ChunkStoreBlocks centerBlocks)
{
// TODO: We'll need a way to get neighbors again.
// var storages = new ChunkPaletteStorage<ecs_entity_t>[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<ecs_entity_t>[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<float>(x, y, z);
var textureCell = block.Get<TextureCoords4>();
@ -104,7 +106,7 @@ public class ChunkMeshGenerator
}
static bool IsNeighborEmpty(
ChunkPaletteStorage<ecs_entity_t>[,,] 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;
}
}

@ -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<Entity>
{
public ChunkStoreBlocks()
: base(default) { }
}
}

@ -1,17 +1,11 @@
using gaemstone.ECS;
namespace gaemstone.Bloxel;
[Component]
public readonly struct Chunk
public static class Constants
{
// <summary> Length of the egde of a world chunk. </summary>
public const int LENGTH = 16;
// <summary> Amount of bit shifting to go from a BlockPos to a ChunkPos. </summary>
public const int BIT_SHIFT = 4;
public const int ChunkBitShift = 4;
// <summary> Amount of bit masking to go from a BlockPos to a chunk-relative BlockPos. </summary>
public const int BIT_MASK = 0b1111;
public ChunkPos Position { get; }
public Chunk(ChunkPos pos) => Position = pos;
public const int ChunkBitMask = ~(~0 << ChunkBitShift);
// <summary> Length of the egde of a world chunk. </summary>
public const int ChunkLength = 1 << ChunkBitShift;
}

@ -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<ecs_entity_t> 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<HasBasicWorldGeneration>();
}

@ -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]

@ -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<float> TopLeft { get; }
public Vector2D<float> TopRight { get; }
public Vector2D<float> BottomLeft { get; }
public Vector2D<float> 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);
}
}

@ -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; }
}

@ -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<Game>().Get<Windowing.Canvas>().GL;
var GL = universe.Lookup<Game>().Get<Canvas>().GL;
var vao = GL.GenVertexArray();
GL.BindVertexArray(vao);
@ -50,7 +52,7 @@ public static class MeshManager
ReadOnlySpan<ushort> indices, ReadOnlySpan<Vector3D<float>> vertices,
ReadOnlySpan<Vector3D<float>> normals, ReadOnlySpan<Vector2D<float>> uvs)
{
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL;
var GL = universe.Lookup<Game>().Get<Canvas>().GL;
var vao = GL.GenVertexArray();
GL.BindVertexArray(vao);
@ -78,7 +80,7 @@ public static class MeshManager
public static Mesh Create(Universe universe, ReadOnlySpan<Vector3D<float>> vertices,
ReadOnlySpan<Vector3D<float>> normals, ReadOnlySpan<Vector2D<float>> uvs)
{
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL;
var GL = universe.Lookup<Game>().Get<Canvas>().GL;
var vao = GL.GenVertexArray();
GL.BindVertexArray(vao);

@ -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;

@ -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();

@ -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]

@ -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;
}

@ -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);
}

@ -1,36 +0,0 @@
using System.Drawing;
using gaemstone.ECS;
using Silk.NET.Maths;
namespace gaemstone.Client;
[Component]
public readonly struct TextureCoords4
{
public Vector2D<float> TopLeft { get; }
public Vector2D<float> TopRight { get; }
public Vector2D<float> BottomLeft { get; }
public Vector2D<float> 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);
}

@ -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<Game>().Get<Windowing.Canvas>().GL;
var GL = universe.Lookup<Game>().Get<Canvas>().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<Game>().Get<Windowing.Canvas>().GL;
var GL = universe.Lookup<Game>().Get<Canvas>().GL;
var texture = new Texture(TextureTarget.Texture2D, GL.GenTexture());
GL.BindTexture(texture.Target, texture.Handle);

@ -0,0 +1,17 @@
using gaemstone.ECS;
using Silk.NET.Maths;
namespace gaemstone.Components;
[Module]
public class TransformComponents
{
[Component]
public struct GlobalTransform
{
public Matrix4X4<float> Value;
public GlobalTransform(Matrix4X4<float> value) => Value = value;
public static implicit operator GlobalTransform(in Matrix4X4<float> value) => new(value);
public static implicit operator Matrix4X4<float>(in GlobalTransform index) => index.Value;
}
}

@ -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 { }

@ -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<T>(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;
}
}

@ -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<T>()
=> Add(Universe.Lookup<T>());
public EntityRef Add<TRelation, TTarget>()
=> Add(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>());
public EntityRef Add<TRelation>(Entity target)
=> Add(Universe.Lookup<TRelation>(), target);
public EntityRef Remove<T>()
=> Remove(Universe.Lookup<T>());
public EntityRef Remove<TRelation, TTarget>()
=> Remove(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>());
public EntityRef Remove<TRelation>(Entity target)
=> Remove(Universe.Lookup<TRelation>(), target);
public bool Has<T>()
=> Has(Universe.Lookup<T>());
public bool Has<TRelation, TTarget>()
=> Has(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>());
public bool Has<TRelation>(Entity target)
=> Has(Universe.Lookup<TRelation>(), target);
// public EntityRef Override<T>()
// => Override(Universe.Lookup<T>());
// public EntityRef Override<TRelation, TTarget>()
// => Override(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>());
// public EntityRef Override<TRelation>(Entity target)
// => Override(Universe.Lookup<TRelation>(), target);
}

@ -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
{
/// <summary>
/// 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 <see cref="Modified"/>.
/// </summary>
public T Get<T>()
{
var comp = Universe.Lookup<T>();
var ptr = ecs_get_id(Universe, this, comp);
return (typeof(T).IsValueType) ? Unsafe.Read<T>(ptr)
: (T)((GCHandle)Unsafe.Read<nint>(ptr)).Target!;
}
/// <summary>
/// Gets a reference to a component value from this entity. Only works for
/// value types. When modifying, consider calling <see cref="Modified"/>.
/// </summary>
public ref T GetRef<T>()
where T : unmanaged
{
var comp = Universe.Lookup<T>();
var ptr = ecs_get_mut_id(Universe, this, comp);
return ref Unsafe.AsRef<T>(ptr);
}
/// <summary>
/// Marks a component as modified. Do this after getting a reference to
/// it with <see cref="Get"/> or <see cref="GetRef"/>, making sure change
/// detection will kick in.
/// </summary>
public void Modified<T>()
=> ecs_modified_id(Universe, this, Universe.Lookup<T>());
public EntityRef Set<T>(in T value)
where T : unmanaged
{
var comp = Universe.Lookup<T>();
var size = (ulong)Unsafe.SizeOf<T>();
fixed (T* ptr = &value)
ecs_set_id(Universe, this, comp, size, ptr);
return this;
}
// public Entity SetOverride<T>(in T value)
// where T : unmanaged
// {
// var comp = Universe.Lookup<T>();
// var size = (ulong)Unsafe.SizeOf<T>();
// 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>(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>(T obj)
// where T : class
// {
// var comp = Universe.Lookup<T>();
// 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;
// }
}

@ -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<Entity>
{
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<Entity> 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<T>()
=> Add(Universe.Lookup<T>());
public Entity Add<TRelation, TTarget>()
=> Add(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>());
public Entity Add<TRelation>(Entity target)
=> Add(Universe.Lookup<TRelation>(), 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<T>()
=> Override(Universe.Lookup<T>());
public Entity Override<TRelation, TTarget>()
=> Override(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>());
public Entity Override<TRelation>(Entity target)
=> Override(Universe.Lookup<TRelation>(), 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<T>() => Remove(Universe.Lookup<T>());
public unsafe readonly partial struct EntityRef
: IEquatable<EntityRef>
, 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<Entity> Children => ...
public bool Has<T>()
=> Has(Universe.Lookup<T>());
public bool Has<TRelation, TTarget>()
=> Has(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>());
public bool Has<TRelation>(Entity target)
=> Has(Universe.Lookup<TRelation>(), target);
public EntityRef(Universe universe, Entity entity)
{ Universe = universe; Entity = entity.ThrowIfInvalid(); }
void IDisposable.Dispose() => Delete();
public unsafe void Delete() => ecs_delete(Universe, this);
/// <summary>
/// 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 <see cref="Modified"/>.
/// </summary>
public T Get<T>()
{
var comp = Universe.Lookup<T>();
var ptr = ecs_get_id(Universe, this, comp);
if (typeof(T).IsValueType) {
return Unsafe.Read<T>(ptr);
} else {
var handle = (GCHandle)Unsafe.Read<nint>(ptr);
return (T)handle.Target!;
}
}
public EntityRef Disable() => Add<Flecs.Disabled>();
public EntityRef Enable() => Remove<Flecs.Disabled>();
/// <summary>
/// Gets a reference to a component value from this entity. Only works for
/// value types. When modifying, consider calling <see cref="Modified"/>.
/// </summary>
public ref T GetRef<T>()
where T : unmanaged
{
var comp = Universe.Lookup<T>();
var ptr = ecs_get_mut_id(Universe, this, comp);
return ref Unsafe.AsRef<T>(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()!;
/// <summary>
/// Marks a component as modified. Do this after getting a reference to
/// it with <see cref="Get"/> or <see cref="GetRef"/>, making sure change
/// detection will kick in.
/// </summary>
public void Modified<T>()
{
var comp = Universe.Lookup<T>();
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<T>(in T value)
where T : unmanaged
{
var comp = Universe.Lookup<T>();
var size = (ulong)Unsafe.SizeOf<T>();
fixed (T* ptr = &value) ecs_set_id(Universe, this, comp, size, ptr);
return this;
}
public Entity SetOverride<T>(in T value)
where T : unmanaged
{
var comp = Universe.Lookup<T>();
var size = (ulong)Unsafe.SizeOf<T>();
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>(T obj) where T : class
=> Set(typeof(T), obj);
public Entity SetOverride<T>(T obj)
where T : class
{
var comp = Universe.Lookup<T>();
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<Identifier>
: IReadOnlyList<IdentifierRef>
{
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<Identifier> 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<IdentifierRef> 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<T>(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;
}
}

@ -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<Identifier>
{
public Universe Universe { get; }
/// <summary> Set to modify existing entity (optional). </summary>
public Entity ID { get; set; }
/// <summary>
/// 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.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 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.
/// </summary>
public string? Symbol { get; set; }
/// <summary>
/// When set to true, a low id (typically reserved for components)
/// will be used to create the entity, if no id is specified.
/// </summary>
public bool UseLowID { get; set; }
/// <summary> IDs to add to the new or existing entity. </summary>
private readonly List<Identifier> _add = new();
/// <summary> String expression with components to add. </summary>
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<T>() => Add(Universe.Lookup<T>());
public EntityBuilder Add(Entity first, Entity second) => Add(first & second);
public EntityBuilder Add<TFirst>(Entity second) => Add(Universe.Lookup<TFirst>(), second);
public EntityBuilder Add<TFirst, TSecond>() => Add(Universe.Lookup<TFirst>(), Universe.Lookup<TSecond>());
// TODO: Add support for Set.
public EntityBuilder ChildOf(Entity parent) => Add(Universe.Lookup<Flecs.ChildOf>(), parent);
public EntityBuilder ChildOf<TParent>() => ChildOf(Universe.Lookup<TParent>());
public EntityBuilder Disabled() => Add(Universe.Lookup<Flecs.Disabled>());
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<Identifier> GetEnumerator() => _add.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

@ -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;
}

@ -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<Iterator>
, 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<Iterator> GetEnumerator() => Iter().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public static implicit operator ecs_filter_t*(Filter q) => q.Handle;
public class FilterDesc
{
public IReadOnlyList<Term> Terms { get; set; }
/// <summary>
/// Optional name of filter, used for debugging. If a filter is created
/// for a system, the provided name should match the system name.
/// </summary>
public string? Name { get; set; }
/// <summary> Filter expression. Should not be set at the same time as terms. </summary>
public string? Expression { get; set; }
/// <summary>
/// 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.
/// </summary>
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;
}
}

@ -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 { }
}

@ -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();
}

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

@ -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<Identifier>
{
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<IdentifierRef>
{
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,
}

@ -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<T> Field<T>(int index)
where T : unmanaged
@ -62,12 +65,11 @@ public unsafe class Iterator
}
public bool FieldIs<T>(int index)
where T : unmanaged
{
fixed (ecs_iter_t* ptr = &Value) {
var id = ecs_field_id(ptr, index);
var comp = Universe.Lookup<T>();
return id == comp.Value.Data;
return id == comp.Entity.Value.Data;
}
}

@ -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<T>(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<Module>();
var Disabled = universe.Lookup<Flecs.Disabled>();
var DependsOn = universe.Lookup<Flecs.DependsOn>();
if (!type.IsClass || type.IsAbstract) throw new Exception(
"Module must be a non-abstract and non-static class");
if (!type.Has<ModuleAttribute>()) 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<DependsOnAttribute>()
.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<Entity, Module>();
_disabledModulesQuery ??= new Query(universe, new(
"DisabledModulesQuery",
Module,
Disabled
));
foreach (var iter in _disabledModulesQuery) {
var modules = iter.FieldRef<Module>(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<Entity, Module>(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<MemberInfo>(type.GetMethods()))
member.GetRegisterableInfo(out var _)?
.Register(module.Universe, instance, member)
.Add<Flecs.ChildOf>(module);
module.Remove<Flecs.Disabled>();
return instance;
}
}

@ -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<Iterator> 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<ObserverAttribute>() ?? throw new ArgumentException(
"Observer must specify ObserverAttribute", nameof(method));
var filter = new FilterDesc();
Action<Iterator> 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<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), 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);
}
}

@ -5,7 +5,7 @@ using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe class Query
public unsafe sealed class Query
: IEnumerable<Iterator>
, 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<Iterator> 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;
}
}

@ -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<Type>(); }
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<Type, RegisterableInfo> _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<ModuleAttribute>() == 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;
}
}

@ -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<T>(this Universe universe)
=> universe.RegisterRelation(typeof(T));
public unsafe static EntityRef RegisterRelation(this Universe universe, Type type)
=> throw new NotImplementedException(); // TODO: Implement me.
}

@ -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<Iterator>
, 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<Iterator> GetEnumerator() => Iter().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

@ -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,
/// <summary>
/// This phase contains all the systems that load data into your ECS.
/// This would be a good place to load keyboard and mouse inputs.
/// </summary>
OnLoad = ECS_HI_COMPONENT_ID + 66,
/// <summary>
/// Often the imported data needs to be processed. Maybe you want to associate
/// your keypresses with high level actions rather than comparing explicitly
/// in your game code if the user pressed the 'K' key.
/// </summary>
PostLoad = ECS_HI_COMPONENT_ID + 67,
/// <summary>
/// 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.
/// </summary>
PreUpdate = ECS_HI_COMPONENT_ID + 68,
/// <summary>
/// This is usually where the magic happens! This is where you put all of
/// your gameplay systems. By default systems are added to this phase.
/// </summary>
OnUpdate = ECS_HI_COMPONENT_ID + 69,
/// <summary>
/// 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.
/// </summary>
OnValidate = ECS_HI_COMPONENT_ID + 70,
/// <summary>
/// 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 <c>OnValidate</c> phase,
/// you may want to move the entities so that they no longer overlap.
/// </summary>
PostUpdate = ECS_HI_COMPONENT_ID + 71,
/// <summary>
/// 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.
/// </summary>
PreStore = ECS_HI_COMPONENT_ID + 72,
/// <summary>
/// This is where it all comes together. Your frame is ready to be
/// rendered, and that is exactly what you would do in this phase.
/// </summary>
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<Iterator> callback)
{
query.Name = name;
var desc = new ecs_system_desc_t {
query = query.ToFlecs(),
entity = new EntityBuilder(universe, name)
.Add<Flecs.DependsOn>(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<SystemAttribute>();
var query = new QueryDesc();
if (action is Action<Iterator> 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<SystemAttribute>();
var query = new QueryDesc();
Action<Iterator> 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<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), 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);
}
}

@ -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 { }
/// <summary>
/// This phase contains all the systems that load data into your ECS.
/// This would be a good place to load keyboard and mouse inputs.
/// </summary>
[BuiltIn(256 + 66)] public struct OnLoad { }
/// <summary>
/// Often the imported data needs to be processed. Maybe you want to associate
/// your keypresses with high level actions rather than comparing explicitly
/// in your game code if the user pressed the 'K' key.
/// </summary>
[BuiltIn(256 + 67)] public struct PostLoad { }
/// <summary>
/// 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.
/// </summary>
[BuiltIn(256 + 68)] public struct PreUpdate { }
/// <summary>
/// This is usually where the magic happens! This is where you put all of
/// your gameplay systems. By default systems are added to this phase.
/// </summary>
[BuiltIn(256 + 69)] public struct OnUpdate { }
/// <summary>
/// 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.
/// </summary>
[BuiltIn(256 + 70)] public struct OnValidate { }
/// <summary>
/// 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 <c>OnValidate</c> phase,
/// you may want to move the entities so that they no longer overlap.
/// </summary>
[BuiltIn(256 + 71)] public struct PostUpdate { }
/// <summary>
/// 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.
/// </summary>
[BuiltIn(256 + 72)] public struct PreStore { }
/// <summary>
/// This is where it all comes together. Your frame is ready to be
/// rendered, and that is exactly what you would do in this phase.
/// </summary>
[BuiltIn(256 + 73)] public struct OnStore { }
[BuiltIn(256 + 74)] public struct PostFrame { }
}

@ -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<T>(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<Flecs.Tag>();
universe.RegisterLookup(type, entity);
return entity;
}
}

@ -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
{
/// <summary> Match on self. </summary>
Self = EcsSelf,
/// <summary> Match by traversing upwards. </summary>
Up = EcsUp,
/// <summary> Match by traversing downwards (derived, cannot be set). </summary>
Down = EcsDown,
/// <summary> Sort results breadth first. </summary>
Cascade = EcsCascade,
/// <summary> Short for up(ChildOf). </summary>
Parent = EcsParent,
/// <summary> Term id is a variable. </summary>
IsVariable = EcsIsVariable,
/// <summary> Term id is an entity. </summary>
IsEntity = EcsIsEntity,
/// <summary> Prevent observer from triggering on term. </summary>
Filter = EcsFilter,
}

@ -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<Type, Entity> _lookupByType = new();
private void RegisterBuiltInLookups()
{
foreach (var type in typeof(Universe).Assembly.GetTypes())
if (type.Get<BuiltInAttribute>() 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<T>()
=> 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<T>() => new(this, TryLookup<T>());
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));
}

@ -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<T>() 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<Type, ModuleInfo> _modules = new();
internal readonly Dictionary<Type, ModuleBuilder> _deferred = new();
}
public class ModuleInfo
{
public Universe Universe { get; }
public object Instance { get; }
public IReadOnlyList<Entity> Relations { get; }
public IReadOnlyList<Entity> Components { get; }
public IReadOnlyList<Entity> Tags { get; }
public IReadOnlyList<Entity> Entities { get; }
public IReadOnlyList<SystemInfo> Systems { get; }
public IReadOnlyList<ObserverInfo> 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<Type> DependsOn { get; }
public bool HasSimpleConstructor { get; }
public IReadOnlyList<Type> Relations { get; }
public IReadOnlyList<Type> Components { get; }
public IReadOnlyList<Type> Tags { get; }
public IReadOnlyList<Type> Entities { get; }
public IReadOnlyList<MethodInfo> Systems { get; }
public IReadOnlyList<MethodInfo> Observers { get; }
public HashSet<Type> 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<ModuleAttribute>()) throw new Exception(
"Module must be marked with ModuleAttribute");
Universe = universe;
Type = type;
DependsOn = type.GetMultiple<DependsOnAttribute>()
.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<Type>();
var components = new List<Type>();
var tags = new List<Type>();
var entities = new List<Type>();
var systems = new List<MethodInfo>();
var observers = new List<MethodInfo>();
foreach (var member in Type.GetNestedTypes().Concat<MemberInfo>(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();
}
}
}

@ -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<Iterator> callback, string expression,
ObserverEvent @event, string? name = null)
=> RegisterObserver(name ?? callback.Method.Name, expression, @event, new() { expr = expression }, callback);
public ObserverInfo RegisterObserver(Action<Iterator> 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<Iterator> 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<ObserverAttribute>() ?? 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<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), 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<ObserverInfo>
{
internal readonly List<ObserverInfo> _observers = new();
// IReadOnlyCollection implementation
public int Count => _observers.Count;
public IEnumerator<ObserverInfo> 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<Iterator> Callback { get; }
internal ObserverInfo(Universe universe, Entity entity, string name, string? expression,
ObserverEvent @event, ecs_filter_desc_t filter, Action<Iterator> callback)
{
Universe = universe;
Entity = entity;
Name = name;
Expression = expression;
Event = @event;
Filter = filter;
Callback = callback;
}
}
}

@ -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<Iterator> 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<Iterator> 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<Iterator> 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<SystemAttribute>();
var phase = attr?.Phase ?? SystemPhase.OnUpdate;
if (action is Action<Iterator> 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<SystemAttribute>();
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<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), 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<SystemInfo>
{
public readonly struct SystemCallbackContext
{
public Universe Universe { get; }
public Action<Iterator> Callback { get; }
public SystemCallbackContext(Universe universe, Action<Iterator> callback)
{ Universe = universe; Callback = callback; }
}
private static SystemCallbackContext[] _systemCallbackContexts = new SystemCallbackContext[64];
private static int _systemCallbackContextsCount = 0;
public static nint CreateSystemCallbackContext(Universe universe, Action<Iterator> 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<SystemInfo> _systems = new();
// IReadOnlyCollection implementation
public int Count => _systems.Count;
public IEnumerator<SystemInfo> 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<Iterator> Callback { get; }
internal SystemInfo(Universe universe, Entity entity, string name, string? expression,
SystemPhase phase, ecs_filter_desc_t filter, Action<Iterator> callback)
{
Universe = universe;
Entity = entity;
Name = name;
Expression = expression;
Phase = phase;
Filter = filter;
Callback = callback;
}
}
}

@ -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<Type, ecs_entity_t> _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<Game>();
this.RegisterComponent<Module>();
}
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<T>()
=> 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<T>()
=> TryLookup<T>().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<T>()
=> RegisterRelation(typeof(T));
public Entity RegisterRelation(Type type)
=> throw new NotImplementedException();
public Entity RegisterComponent<T>()
=> 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<EntityAttribute>()) {
if (type.IsValueType) entity.Add(entity);
else entity.Set(type, Activator.CreateInstance(type)!);
}
return entity;
}
public Entity RegisterTag<T>()
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<T>()
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<EntityAttribute>()?.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;
}

@ -1,13 +0,0 @@
using gaemstone.ECS;
using Silk.NET.Maths;
namespace gaemstone;
[Component]
public struct GlobalTransform
{
public Matrix4X4<float> Value;
public GlobalTransform(Matrix4X4<float> value) => Value = value;
public static implicit operator GlobalTransform(in Matrix4X4<float> value) => new(value);
public static implicit operator Matrix4X4<float>(in GlobalTransform index) => index.Value;
}

@ -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; }
}

@ -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<Iterator> Callback { get; }
public CallbackContext(Universe universe, Action<Iterator> 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<Iterator> 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));
}
}

@ -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> array) { Load(array); LoadLength(); }
public void LoadObj(Type type) => Emit(OpCodes.Ldobj, type);
public void LoadObj<T>() 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<int> local) { Load(local); LoadConst(1); Add(); Store(local); }
public void Init(Type type) => Emit(OpCodes.Initobj, type);
public void Init<T>() where T : struct => Emit(OpCodes.Initobj, typeof(T));
public void Init<T>() 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<T> : ArgumentImpl, IArgument<T>
{ 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<T> : LocalImpl, ILocal<T>
{ public LocalImpl(LocalBuilder builder, string? name) : base(builder, name) { } }

@ -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<MethodInfo, QueryActionGenerator> _cache = new();
private static readonly ConditionalWeakTable<MethodInfo, IterActionGenerator> _cache = new();
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>, ILocal<int>>> _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<Term> Terms { get; }
public Action<object?, Iterator> 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<Term>();
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<object?>(0);
var iteratorArg = IL.Argument<Iterator>(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<T> local and initialize it to iterator.Field<T>(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<Action<object?, Iterator>>();
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<EntityAttribute>()) Source = underlyingType;
if (Info.Get<SourceAttribute>() is SourceAttribute attr) Source = attr.Type;
// TODO: Needs support for the new attributes.
}
public static ParamInfo Build(ParameterInfo info, int index)
Loading…
Cancel
Save