Compare commits

...

18 Commits

Author SHA1 Message Date
copygirl 6a38f97830 Update to newer gaemstone.ECS and Flecs 1 year ago
copygirl c97c4a530f Improve built-in entities 1 year ago
copygirl 03b7e5ce42 Add [Set] attribute to set entity's component 1 year ago
copygirl fe54cc3637 Fix IModuleInitializer.Initialize not being called 1 year ago
copygirl 40cb0045df Make EntityInspector use Entity<T>,AliveOrNull 1 year ago
copygirl 931a86fcea Terms that are [Tag] have Kind = Has 1 year ago
copygirl 58989e949f Fix systems not implying [DependsOn<OnUpdate>] 1 year ago
copygirl d51c0331bb Get [Source] from Has<T> and Not<T> as well 1 year ago
copygirl ef64ba4e08 Run queries that only match singletons 1 year ago
copygirl 982741a364 Don't zero-init singleton components 1 year ago
copygirl c9a986f9bf Update how or terms are handled 1 year ago
copygirl 357e8b4bfa Re-add [Symbol] attribute 1 year ago
copygirl 64f7ece2a7 Update to newest flecs-cs 1 year ago
copygirl 8e9ff611d9 Source Generators, part 3: Another Refactor 1 year ago
copygirl 20d0cd2f0e Add ".NET Attach" launch config 1 year ago
copygirl 976d9fd326 Replace [Symbol] with [Public] and [Private] 1 year ago
copygirl 03d672febf Source Generators, step 2: Modules 1 year ago
copygirl 2b877e0c5c Source Generators, step 1: Components 1 year ago
  1. 5
      .vscode/launch.json
  2. 3
      .vscode/tasks.json
  3. 21
      gaemstone.sln
  4. 4
      src/Immersion/Immersion.csproj
  5. 25
      src/Immersion/ManagedComponentTest.cs
  6. 6
      src/Immersion/ObserverTest.cs
  7. 44
      src/Immersion/Program.cs
  8. 6
      src/gaemstone.Bloxel/ChunkPaletteStorage.cs
  9. 114
      src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs
  10. 4
      src/gaemstone.Bloxel/Components/CoreComponents.cs
  11. 26
      src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs
  12. 2
      src/gaemstone.Bloxel/Systems/SurfaceGrassGenerator.cs.disabled
  13. 3
      src/gaemstone.Bloxel/gaemstone.Bloxel.csproj
  14. 2
      src/gaemstone.Client/Components/CameraComponents.cs
  15. 25
      src/gaemstone.Client/Components/InputComponents.cs
  16. 8
      src/gaemstone.Client/Components/RenderingComponents.cs
  17. 8
      src/gaemstone.Client/Components/ResourceComponents.cs
  18. 258
      src/gaemstone.Client/Systems/EntityInspector.cs
  19. 37
      src/gaemstone.Client/Systems/FreeCameraController.cs
  20. 6
      src/gaemstone.Client/Systems/ImGuiDemoWindow.cs
  21. 38
      src/gaemstone.Client/Systems/ImGuiInputDebug.cs
  22. 36
      src/gaemstone.Client/Systems/ImGuiManager.cs
  23. 86
      src/gaemstone.Client/Systems/InputManager.cs
  24. 11
      src/gaemstone.Client/Systems/MeshManager.cs
  25. 51
      src/gaemstone.Client/Systems/Renderer.cs
  26. 13
      src/gaemstone.Client/Systems/TextureManager.cs
  27. 11
      src/gaemstone.Client/Systems/Windowing.cs
  28. 5
      src/gaemstone.Client/gaemstone.Client.csproj
  29. 2
      src/gaemstone.ECS
  30. 230
      src/gaemstone.SourceGen/Descriptors.cs
  31. 398
      src/gaemstone.SourceGen/ModuleGenerator.cs
  32. 82
      src/gaemstone.SourceGen/RelevantSymbolReceiver.cs
  33. 97
      src/gaemstone.SourceGen/Structure/BaseEntityInfo.cs
  34. 43
      src/gaemstone.SourceGen/Structure/BaseInfo.cs
  35. 98
      src/gaemstone.SourceGen/Structure/MethodEntityInfo.cs
  36. 62
      src/gaemstone.SourceGen/Structure/ModuleEntityInfo.cs
  37. 220
      src/gaemstone.SourceGen/Structure/ParameterInfo.cs
  38. 96
      src/gaemstone.SourceGen/Structure/TypeEntityInfo.cs
  39. 10
      src/gaemstone.SourceGen/Utility/CollectionExtensions.cs
  40. 8
      src/gaemstone.SourceGen/Utility/IsExternalInit.cs
  41. 118
      src/gaemstone.SourceGen/Utility/SymbolExtensions.cs
  42. 20
      src/gaemstone.SourceGen/gaemstone.SourceGen.csproj
  43. 4
      src/gaemstone/Components/TransformComponents.cs
  44. 2
      src/gaemstone/Doc.cs
  45. 121
      src/gaemstone/ECS/Attributes.cs
  46. 16
      src/gaemstone/ECS/FilterExtensions.cs
  47. 14
      src/gaemstone/ECS/Game.cs
  48. 69
      src/gaemstone/ECS/Module+Attributes.cs
  49. 46
      src/gaemstone/ECS/Module+Components.cs
  50. 24
      src/gaemstone/ECS/Module.cs
  51. 45
      src/gaemstone/ECS/Observer.cs
  52. 88
      src/gaemstone/ECS/System+Terms.cs
  53. 70
      src/gaemstone/ECS/System.cs
  54. 32
      src/gaemstone/ECS/TermAttributes.cs
  55. 77
      src/gaemstone/Flecs/Core.cs
  56. 18
      src/gaemstone/Flecs/DeletionEvent.cs
  57. 59
      src/gaemstone/Flecs/Doc.cs
  58. 14
      src/gaemstone/Flecs/ObserverEvent.cs
  59. 88
      src/gaemstone/Flecs/Pipeline.cs
  60. 91
      src/gaemstone/Flecs/SystemPhase.cs
  61. 13
      src/gaemstone/Flecs/Systems/Monitor.cs
  62. 13
      src/gaemstone/Flecs/Systems/Rest.cs
  63. 259
      src/gaemstone/Universe+Modules.cs
  64. 31
      src/gaemstone/Universe.cs
  65. 299
      src/gaemstone/Utility/IL/ILGeneratorWrapper.cs
  66. 404
      src/gaemstone/Utility/IL/IterActionGenerator.cs
  67. 37
      src/gaemstone/Utility/Union.cs
  68. 3
      src/gaemstone/gaemstone.csproj

@ -11,6 +11,11 @@
"cwd": "${workspaceFolder}/src/Immersion",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Attach",
"type": "coreclr",
"request": "attach"
}
]
}

@ -1,6 +1,7 @@
{
"version": "2.0.0",
"tasks": [{
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",

@ -1,17 +1,19 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{599B7E67-7F73-4301-A9C6-E8DF286A2625}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone", "src\gaemstone\gaemstone.csproj", "{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.Bloxel", "src\gaemstone.Bloxel\gaemstone.Bloxel.csproj", "{7A80D49C-6768-4803-9866-691C7AD80817}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.Client", "src\gaemstone.Client\gaemstone.Client.csproj", "{67B9B2D4-FCB7-4642-B584-A0186CAB2969}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.ECS", "src\gaemstone.ECS\gaemstone.ECS.csproj", "{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone", "src\gaemstone\gaemstone.csproj", "{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.SourceGen", "src\gaemstone.SourceGen\gaemstone.SourceGen.csproj", "{07963390-747C-4FBF-A727-A33E024F4E13}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Immersion", "src\Immersion\Immersion.csproj", "{4B9C20F6-0793-4E85-863A-2E14230A028F}"
EndProject
@ -24,6 +26,10 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.Build.0 = Release|Any CPU
{7A80D49C-6768-4803-9866-691C7AD80817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A80D49C-6768-4803-9866-691C7AD80817}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A80D49C-6768-4803-9866-691C7AD80817}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -36,20 +42,21 @@ Global
{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}.Release|Any CPU.Build.0 = Release|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.Build.0 = Release|Any CPU
{07963390-747C-4FBF-A727-A33E024F4E13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07963390-747C-4FBF-A727-A33E024F4E13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07963390-747C-4FBF-A727-A33E024F4E13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07963390-747C-4FBF-A727-A33E024F4E13}.Release|Any CPU.Build.0 = Release|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{7A80D49C-6768-4803-9866-691C7AD80817} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{67B9B2D4-FCB7-4642-B584-A0186CAB2969} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{07963390-747C-4FBF-A727-A33E024F4E13} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{4B9C20F6-0793-4E85-863A-2E14230A028F} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
EndGlobalSection
EndGlobal

@ -9,13 +9,15 @@
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/*" />
<EmbeddedResource Include="Resources/**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../gaemstone.Bloxel/gaemstone.Bloxel.csproj" />
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" />
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
<ProjectReference Include="../gaemstone.SourceGen/gaemstone.SourceGen.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1,25 @@
using System;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
namespace Immersion;
[Module]
public partial class ManagedComponentTest
{
[Component]
public class BigManagedData
{
public readonly byte[] BigArray = new byte[1024 * 1024];
}
[System]
public static void CreateLotsOfGarbageData<T>(World<T> world)
{
var game = world.LookupPathOrThrow("/Game");
game.Remove<BigManagedData>();
game.Set(new BigManagedData());
// This is to make sure the number of objects kept alive stays stable.
Console.WriteLine(ReferenceHandle.NumActiveHandles);
}
}

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

@ -5,7 +5,6 @@ using System.Numerics;
using System.Threading;
using gaemstone;
using gaemstone.Bloxel;
using gaemstone.ECS;
using gaemstone.Utility;
using Silk.NET.Windowing;
using static gaemstone.Bloxel.Components.CoreComponents;
@ -20,21 +19,23 @@ var culture = CultureInfo.InvariantCulture;
Thread.CurrentThread.CurrentCulture = culture;
CultureInfo.DefaultThreadCurrentCulture = culture;
var universe = new Universe();
var game = universe.LookupByTypeOrThrow<Game>();
var universe = new Universe<Program>();
var world = universe.World;
// TODO: Figure out a nice way to get rid of "compile errors" here.
// FIXME: universe.Modules.Register<gaemstone.Flecs.Systems.Monitor>();
universe.Modules.Register<gaemstone.Flecs.Systems.Rest>();
universe.Modules.Register<gaemstone.Flecs.Systems.Monitor>();
var window = Window.Create(WindowOptions.Default with {
Title = "gæmstone",
Size = new(1280, 720),
Size = new(1280, 720),
PreferredDepthBufferBits = 24,
});
window.Initialize();
window.Center();
// universe.Modules.Register<ObserverTest>();
// universe.Modules.Register<Immersion.ObserverTest>();
// universe.Modules.Register<Immersion.ManagedComponentTest>();
universe.Modules.Register<gaemstone.Client.Systems.Windowing>();
universe.Modules.Register<gaemstone.Components.TransformComponents>();
@ -56,19 +57,24 @@ universe.Modules.Register<gaemstone.Client.Systems.EntityInspector>();
universe.Modules.Register<gaemstone.Client.Components.CameraComponents>();
universe.Modules.Register<gaemstone.Client.Systems.FreeCameraController>();
game.Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window)));
game.Set(new GameWindow(window));
foreach (var module in universe.Modules)
if (!module.IsInitialized) throw new InvalidOperationException(
$"Module '{module.Entity.Path}' is not initialized");
// Initialize Canvas and GameWindow singletons with actual values.
world.Entity<Canvas>().Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window)));
world.Entity<GameWindow>().Set(new GameWindow(window));
universe.New("MainCamera")
world.New("MainCamera")
.Set(Camera.Default3D)
.Set((GlobalTransform)Matrix4x4.CreateTranslation(0.0F, 2.0F, 0.0F))
.Set(new CameraController { MouseSensitivity = 12.0F })
.Build();
var heartMesh = universe.New("/Immersion/Resources/heart.glb").Add<Mesh>().Build();
var swordMesh = universe.New("/Immersion/Resources/sword.glb").Add<Mesh>().Build();
var heartMesh = world.New("/Immersion/Resources/heart.glb").Add<Mesh>().Build();
var swordMesh = world.New("/Immersion/Resources/sword.glb").Add<Mesh>().Build();
var entities = universe.New("Entities").Build();
var entities = world.New("Entities").Build();
var rnd = new Random();
for (var x = -12; x <= 12; x++)
for (var z = -12; z <= 12; z++) {
@ -85,13 +91,13 @@ universe.Modules.Register<gaemstone.Bloxel.Components.CoreComponents>();
universe.Modules.Register<gaemstone.Bloxel.Systems.BasicWorldGenerator>();
universe.Modules.Register<gaemstone.Bloxel.Client.Systems.ChunkMeshGenerator>();
var texture = universe.New("/Immersion/Resources/terrain.png").Add<Texture>().Build();
var texture = world.New("/Immersion/Resources/terrain.png").Add<Texture>().Build();
var stone = universe.New("Stone").Set(TextureCoords4.FromGrid(4, 4, 1, 0)).Build();
var dirt = universe.New("Dirt" ).Set(TextureCoords4.FromGrid(4, 4, 2, 0)).Build();
var grass = universe.New("Grass").Set(TextureCoords4.FromGrid(4, 4, 3, 0)).Build();
var stone = world.New("Stone").Set(TextureCoords4.FromGrid(4, 4, 1, 0)).Build();
var dirt = world.New("Dirt" ).Set(TextureCoords4.FromGrid(4, 4, 2, 0)).Build();
var grass = world.New("Grass").Set(TextureCoords4.FromGrid(4, 4, 3, 0)).Build();
var chunks = universe.New("Chunks").Build();
var chunks = world.New("Chunks").Build();
var sizeH = 4; var sizeY = 2;
for (var cx = -sizeH; cx < sizeH; cx++)
for (var cy = -sizeY; cy < sizeY; cy++)
@ -109,12 +115,12 @@ for (var cz = -sizeH; cz < sizeH; cz++) {
// universe.Modules.Register<ObserverTest>();
var stopwatch = Stopwatch.StartNew();
var minFrameTime = TimeSpan.FromSeconds(1) / 30;
var minFrameTime = TimeSpan.FromSeconds(1) / 60;
window.Run(() => {
var delta = stopwatch.Elapsed;
stopwatch.Restart();
if (!universe.Progress(delta))
if (!world.Progress(delta))
window.Close();
var requiredTime = stopwatch.Elapsed;

@ -168,9 +168,9 @@ public class ChunkPaletteStorage<T>
}
private int GetIndex(int x, int y, int z)
=> (x |
y << Constants.ChunkBitShift |
z << (Constants.ChunkBitShift * 2)) * _indicesLength;
=> (x | y << Constants.ChunkBitShift
| z << (Constants.ChunkBitShift * 2))
* _indicesLength;
private struct PaletteEntry

@ -2,7 +2,9 @@ using System;
using System.Numerics;
using gaemstone.Client.Systems;
using gaemstone.ECS;
using Silk.NET.OpenGL;
using static gaemstone.Bloxel.Components.CoreComponents;
using static gaemstone.Bloxel.Systems.BasicWorldGenerator;
using static gaemstone.Client.Components.RenderingComponents;
using static gaemstone.Client.Components.ResourceComponents;
using static gaemstone.Client.Systems.Windowing;
@ -10,7 +12,12 @@ using static gaemstone.Client.Systems.Windowing;
namespace gaemstone.Bloxel.Client.Systems;
[Module]
public class ChunkMeshGenerator
[DependsOn<gaemstone.Bloxel.Components.CoreComponents>]
[DependsOn<gaemstone.Bloxel.Systems.BasicWorldGenerator>]
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
[DependsOn<gaemstone.Client.Components.ResourceComponents>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
public partial class ChunkMeshGenerator
{
private const int StartingCapacity = 1024;
@ -20,29 +27,32 @@ public class ChunkMeshGenerator
new Vector3[]{ new(1,1,0), new(0,1,0), new(0,1,1), new(1,1,1) }, // Up (+Y)
new Vector3[]{ new(1,0,1), new(0,0,1), new(0,0,0), new(1,0,0) }, // Down (-Y)
new Vector3[]{ new(0,1,1), new(0,0,1), new(1,0,1), new(1,1,1) }, // South (+Z)
new Vector3[]{ new(1,1,0), new(1,0,0), new(0,0,0), new(0,1,0) } // North (-Z)
new Vector3[]{ new(1,1,0), new(1,0,0), new(0,0,0), new(0,1,0) }, // North (-Z)
};
private static readonly int[] TriangleIndices
= { 0, 1, 3, 1, 2, 3 };
private ushort[] _indices = new ushort[StartingCapacity];
private Vector3[] _vertices = new Vector3[StartingCapacity];
private Vector3[] _normals = new Vector3[StartingCapacity];
private Vector2[] _uvs = new Vector2[StartingCapacity];
// TODO: Turn these into a component on the module.
private static ushort [] _indices = new ushort [StartingCapacity];
private static Vector3[] _vertices = new Vector3[StartingCapacity];
private static Vector3[] _normals = new Vector3[StartingCapacity];
private static Vector2[] _uvs = new Vector2[StartingCapacity];
[System]
[Expression("[in] Chunk, ChunkStoreBlocks, HasBasicWorldGeneration, !(Mesh, *)")]
public void GenerateChunkMeshes(Universe universe, EntityRef entity,
in Chunk chunk, ChunkStoreBlocks blocks)
public static void GenerateChunkMeshes<T>(
World<T> world, Canvas canvas,
Entity<T> entity, in Chunk chunk, ChunkStoreBlocks blocks,
Has<BasicWorldGenerationDone> _1, Not<Mesh, Flecs.Core.Wildcard> _2)
{
if (Generate(universe, chunk.Position, blocks) is MeshHandle handle)
entity.Add<Mesh>(entity.NewChild("Mesh").Set(handle).Build());
else entity.Delete();
var result = Generate(world, canvas.GL, chunk.Position, blocks);
if (result is MeshHandle handle) {
var mesh = entity.NewChild("Mesh").Set(handle).Build();
entity.Add<Mesh>(mesh);
} else entity.Delete();
}
public MeshHandle? Generate(Universe universe,
public static MeshHandle? Generate<T>(World<T> world, GL GL,
ChunkPos chunkPos, ChunkStoreBlocks centerBlocks)
{
// TODO: We'll need a way to get neighbors again.
@ -59,47 +69,45 @@ public class ChunkMeshGenerator
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 = universe.LookupAlive(centerBlocks[x, y, z]);
if (block == null) continue;
var blockVertex = new Vector3(x, y, z);
var textureCell = block.GetOrThrow<TextureCoords4>();
foreach (var facing in BlockFacings.All) {
if (!IsNeighborEmpty(storages, x, y, z, facing)) continue;
if (_indices.Length <= indexCount + 6)
Array.Resize(ref _indices, _indices.Length << 1);
if (_vertices.Length <= vertexCount + 4) {
Array.Resize(ref _vertices, _vertices.Length << 1);
Array.Resize(ref _normals , _vertices.Length << 1);
Array.Resize(ref _uvs , _vertices.Length << 1);
}
for (var i = 0; i < TriangleIndices.Length; i++)
_indices[indexCount++] = (ushort)(vertexCount + TriangleIndices[i]);
var normal = facing.ToVector3();
for (var i = 0; i < 4; i++) {
var offset = OffsetPerFacing[(int)facing][i];
_vertices[vertexCount] = blockVertex + offset;
_normals[vertexCount] = normal;
_uvs[vertexCount] = i switch {
0 => textureCell.TopLeft,
1 => textureCell.BottomLeft,
2 => textureCell.BottomRight,
3 => textureCell.TopRight,
_ => throw new InvalidOperationException()
};
vertexCount++;
for (var y = 0; y < 16; y++)
for (var z = 0; z < 16; z++) {
var maybeBlock = world.LookupAliveOrNull(centerBlocks[x, y, z]);
if (maybeBlock is not Entity<T> block) continue;
var blockVertex = new Vector3(x, y, z);
var textureCell = block.GetOrThrow<TextureCoords4>();
foreach (var facing in BlockFacings.All) {
if (!IsNeighborEmpty(storages, x, y, z, facing)) continue;
if (_indices.Length <= indexCount + 6)
Array.Resize(ref _indices, _indices.Length << 1);
if (_vertices.Length <= vertexCount + 4) {
Array.Resize(ref _vertices, _vertices.Length << 1);
Array.Resize(ref _normals , _vertices.Length << 1);
Array.Resize(ref _uvs , _vertices.Length << 1);
}
for (var i = 0; i < TriangleIndices.Length; i++)
_indices[indexCount++] = (ushort)(vertexCount + TriangleIndices[i]);
var normal = facing.ToVector3();
for (var i = 0; i < 4; i++) {
var offset = OffsetPerFacing[(int)facing][i];
_vertices[vertexCount] = blockVertex + offset;
_normals[vertexCount] = normal;
_uvs[vertexCount] = i switch {
0 => textureCell.TopLeft,
1 => textureCell.BottomLeft,
2 => textureCell.BottomRight,
3 => textureCell.TopRight,
_ => throw new InvalidOperationException()
};
vertexCount++;
}
}
}
}
}
// TODO: Should dynamically generating meshes require getting GL this way?
var GL = universe.LookupByTypeOrThrow<Game>().GetOrThrow<Canvas>().GL;
return (indexCount > 0)
? MeshManager.Create(GL,
_indices.AsSpan(0, indexCount), _vertices.AsSpan(0, vertexCount),

@ -5,14 +5,14 @@ namespace gaemstone.Bloxel.Components;
[Module]
public partial class CoreComponents
{
[Symbol, Component]
[Component, Symbol]
public readonly struct Chunk
{
public ChunkPos Position { get; }
public Chunk(ChunkPos pos) => Position = pos;
}
[Symbol, Component]
[Component, Symbol]
public class ChunkStoreBlocks
: ChunkPaletteStorage<Entity>
{

@ -8,12 +8,13 @@ namespace gaemstone.Bloxel.Systems;
[Module]
[DependsOn<gaemstone.Bloxel.Components.CoreComponents>]
public class BasicWorldGenerator
public partial class BasicWorldGenerator
{
private readonly FastNoiseLite _noise;
private readonly Random _rnd = new();
// TODO: Turn these into a component on the module.
private static readonly FastNoiseLite _noise;
private static readonly Random _rnd = new();
public BasicWorldGenerator()
static BasicWorldGenerator()
{
_noise = new(new Random().Next());
_noise.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
@ -22,17 +23,18 @@ public class BasicWorldGenerator
_noise.SetFractalGain(0.6f);
}
[Symbol, Tag]
public struct HasBasicWorldGeneration { }
[Tag]
public struct BasicWorldGenerationDone { }
[System]
public void Populate(World world, EntityRef entity,
public static void Populate<T>(
World<T> world, Entity<T> entity,
in Chunk chunk, ChunkStoreBlocks blocks,
[Not] HasBasicWorldGeneration _)
Not<BasicWorldGenerationDone> _)
{
var stone = world.LookupByPathOrThrow("Stone");
var dirt = world.LookupByPathOrThrow("Dirt");
var grass = world.LookupByPathOrThrow("Grass");
var stone = world.LookupPathOrThrow("Stone");
var dirt = world.LookupPathOrThrow("Dirt");
var grass = world.LookupPathOrThrow("Grass");
for (var localX = 0; localX < ChunkLength; localX++)
for (var localY = 0; localY < ChunkLength; localY++)
for (var localZ = 0; localZ < ChunkLength; localZ++) {
@ -43,6 +45,6 @@ public class BasicWorldGenerator
if (_noise.GetNoise(globalX, globalY, globalZ) > bias)
blocks[localX, localY, localZ] = _rnd.Pick(stone, dirt, grass);
}
entity.Add<HasBasicWorldGeneration>();
entity.Add<BasicWorldGenerationDone>();
}
}

@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace gaemstone.Bloxel.WorldGen;
// FIXME: There is an issue with this generator where it doesn't generate grass and dirt properly.
public class SurfaceGrassGenerator
public partial class SurfaceGrassGenerator
: IWorldGenerator
{
public static readonly string IDENTIFIER = nameof(SurfaceGrassGenerator);

@ -5,6 +5,7 @@
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <!-- -->
</PropertyGroup>
<ItemGroup>
@ -15,6 +16,8 @@
<ProjectReference Include="../gaemstone/gaemstone.csproj" />
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" />
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
<ProjectReference Include="../gaemstone.SourceGen/gaemstone.SourceGen.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>

@ -4,7 +4,7 @@ using gaemstone.ECS;
namespace gaemstone.Client.Components;
[Module]
public class CameraComponents
public partial class CameraComponents
{
[Component]
public struct Camera

@ -5,32 +5,35 @@ using gaemstone.ECS;
namespace gaemstone.Client.Components;
[Module]
public class InputComponents
public partial class InputComponents
{
[Symbol, Path("/Input")]
[Entity, Path("/Input"), Symbol]
[Add<Input>]
public struct Input { }
[Symbol, Path("/Input/Mouse")]
[Entity, Path("/Input/Mouse"), Symbol]
[Add<Mouse>]
public struct Mouse { }
[Symbol, Path("/Input/Keyboard")]
[Entity, Path("/Input/Keyboard"), Symbol]
[Add<Keyboard>]
public struct Keyboard { }
[Symbol, Tag]
[Tag, Symbol]
public struct Gamepad { }
/// <summary> Present on inputs / actions that are currently active. </summary>
[Symbol, Component] public struct Active { public TimeSpan Duration; }
[Component, Symbol]
public struct Active { public TimeSpan Duration; }
/// <summary> Present on inputs / actions were activated this frame. </summary>
[Symbol, Tag] public struct Activated { }
[Tag, Symbol]
public struct Activated { }
/// <summary> Present on inputs / actions were deactivated this frame. </summary>
[Symbol, Tag] public struct Deactivated { }
[Tag, Symbol]
public struct Deactivated { }
/// <summary>
@ -41,7 +44,7 @@ public class InputComponents
/// This is set if a UI element is focused that captures
/// navigational or text input.
/// </remarks>
[Symbol, Relation, Tag, Exclusive]
[Relation, Tag, Symbol, Exclusive]
public struct InputCapturedBy { }
/// <summary>
@ -52,7 +55,7 @@ public class InputComponents
/// This could for example include the mouse currently being over
/// a UI element, preventing the game from handling mouse input.
/// </remarks>
[Symbol, Relation, Tag, Exclusive]
[Relation, Tag, Symbol, Exclusive]
public struct MouseInputCapturedBy { }
/// <summary>
@ -62,7 +65,7 @@ public class InputComponents
/// <remarks>
/// This is set when a camera controller assumes control of the mouse.
/// </remarks>
[Symbol, Relation, Tag, Exclusive]
[Relation, Tag, Symbol, Exclusive]
[With<InputCapturedBy>]
[With<MouseInputCapturedBy>]
public struct CursorCapturedBy { }

@ -6,9 +6,9 @@ using Silk.NET.OpenGL;
namespace gaemstone.Client.Components;
[Module]
public class RenderingComponents
public partial class RenderingComponents
{
[Symbol, Component]
[Component, Symbol]
public readonly struct MeshHandle
{
public uint Handle { get; }
@ -19,7 +19,7 @@ public class RenderingComponents
{ Handle = handle; Count = count; IsIndexed = indexed; }
}
[Symbol, Component]
[Component, Symbol]
public readonly struct TextureHandle
{
public TextureTarget Target { get; }
@ -29,7 +29,7 @@ public class RenderingComponents
=> (Target, Handle) = (target, handle);
}
[Symbol, Component]
[Component, Symbol]
public readonly struct TextureCoords4
{
public Vector2 TopLeft { get; }

@ -3,9 +3,9 @@ using gaemstone.ECS;
namespace gaemstone.Client.Components;
[Module]
public class ResourceComponents
public partial class ResourceComponents
{
[Symbol, Tag]
[Tag, Symbol]
public struct Resource { }
// Entities can have for example Texture as a tag, in which case
@ -14,9 +14,9 @@ public class ResourceComponents
// Entities can also have a (Texture, $T) pair where $T is a resource,
// meaning the entity has that resource assigned as their texture.
[Symbol, Relation, Tag, IsA<Resource>]
[Relation, Tag, Symbol, IsA<Resource>]
public struct Texture { }
[Symbol, Relation, Tag, IsA<Resource>]
[Relation, Tag, Symbol, IsA<Resource>]
public struct Mesh { }
}

@ -7,7 +7,6 @@ using gaemstone.ECS;
using gaemstone.Flecs;
using ImGuiNET;
using static gaemstone.Client.Systems.ImGuiManager;
using static gaemstone.Flecs.Core;
using Icon = gaemstone.Client.Utility.ForkAwesome;
using ImGuiInternal = ImGuiNET.Internal.ImGui;
@ -15,14 +14,14 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
public class EntityInspector
public partial class EntityInspector
: IModuleInitializer
{
[Tag]
public struct InspectorWindow { }
[Relation, Exclusive]
[Add<DeletionEvent.OnDeleteTarget, DeletionBehavior.Delete>]
[Add<Core.OnDeleteTarget, Core.Delete>]
public struct Selected { }
[Tag]
@ -44,17 +43,16 @@ public class EntityInspector
public Entry? Prev { get; set; }
public Entry? Next { get; set; }
public Entry(EntityRef entity, Entry? prev, Entry? next)
public Entry(Entity entity, EntityPath path, Entry? prev, Entry? next)
{
Entity = entity;
Path = entity.GetFullPath();
Path = path;
if ((Prev = prev) != null) Prev.Next = this;
if ((Next = next) != null) Next.Prev = this;
}
}
}
[Component]
public struct DocPriority { public float Value; }
@ -64,10 +62,10 @@ public class EntityInspector
private const string DefaultWindowTitle = "Inspector Gadget";
public void Initialize(EntityRef module)
public static void Initialize<T>(Entity<T> module)
{
void SetDocInfo(string path, float priority, string icon, float r, float g, float b)
=> module.World.LookupByPathOrThrow(path)
=> module.World.LookupPathOrThrow(path)
.Add<Doc.DisplayType>()
.Set(new DocPriority { Value = priority })
.Set(new DocIcon { Value = icon[0] })
@ -78,17 +76,18 @@ public class EntityInspector
SetDocInfo("/flecs/core/Observer" , 2 , Icon.Eye , 1.0f, 0.8f, 0.8f);
SetDocInfo("/gaemstone/Doc/Relation" , 3 , Icon.ShareAlt , 0.7f, 1.0f, 0.8f);
SetDocInfo("/flecs/core/Component" , 4 , Icon.PencilSquare , 0.6f, 0.6f, 1.0f);
// TODO: Handle tags like Flecs does.
SetDocInfo("/flecs/core/Tag" , 5 , Icon.Tag , 0.7f, 0.8f, 1.0f);
SetDocInfo("/flecs/core/Prefab" , 6 , Icon.Cube , 0.9f, 0.8f, 1.0f);
}
[System]
public void ShowUIButton(World world, ImGuiData _)
public static void ShowUIButton<T>(World<T> world, ImGuiData _)
{
var hasAnyInspector = false;
var inspectorWindow = world.LookupByTypeOrThrow<InspectorWindow>();
foreach (var entity in Iterator.FromTerm(world, new(inspectorWindow)))
var inspectorWindow = world.Entity<InspectorWindow>();
foreach (var entity in Iterator<T>.FromTerm(world, new(inspectorWindow)))
{ hasAnyInspector = true; break; }
if (ImGuiUtility.UIButton(0, Icon.Search, DefaultWindowTitle, hasAnyInspector))
@ -96,7 +95,7 @@ public class EntityInspector
}
[System]
public void ShowInspectorWindow(EntityRef window, InspectorWindow _, History? history)
public static void ShowInspectorWindow<T>(Entity<T> window, InspectorWindow _, History? history)
{
var isOpen = true;
var fontSize = ImGui.GetFontSize();
@ -105,11 +104,11 @@ public class EntityInspector
ImGui.SetNextWindowSize(new(fontSize * 40, fontSize * 25), ImGuiCond.Appearing);
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]);
var title = window.GetDocName() ?? DefaultWindowTitle;
if (ImGui.Begin($"{Icon.Search} {title}###{window.Id}",
if (ImGui.Begin($"{Icon.Search} {title}###{window.NumericId}",
ref isOpen, ImGuiWindowFlags.NoScrollbar)) {
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[0]);
var selected = window.GetTargets<Selected>().FirstOrDefault();
var selected = window.GetTargets<Selected>().FirstOrDefault().AliveOrNull();
ActionBarAndPath(window, history, selected);
ImGui.BeginTable("Views", 2, ImGuiTableFlags.Resizable);
@ -121,7 +120,7 @@ public class EntityInspector
ExplorerView(window, history, selected);
ImGui.EndChild();
void Tab(string name, Action<EntityRef, History?, EntityRef?> contentMethod)
void Tab(string name, Action<Entity<T>, History?, Entity<T>?> contentMethod)
{
if (!ImGui.BeginTabItem(name)) return;
ImGui.BeginChild($"{name}Tab", new(-float.Epsilon, -float.Epsilon));
@ -150,16 +149,14 @@ public class EntityInspector
if (!isOpen) window.Delete();
}
[Observer<ObserverEvent.OnRemove>]
public void ClearStorageOnRemove(EntityRef _1, InspectorWindow _2)
[Observer<Core.OnRemove>]
public static void ClearStorageOnRemove<T>(Entity<T> _1, InspectorWindow _2)
{
// TODO: Clear out settings store for the window.
}
private void ActionBarAndPath(EntityRef window, History? history, EntityRef? selected)
private static void ActionBarAndPath<T>(Entity<T> window, History? history, Entity<T>? selected)
{
var world = window.World;
static bool IconButtonWithToolTip(string icon, string tooltip, bool enabled = true) {
if (!enabled) ImGui.BeginDisabled();
var clicked = ImGui.Button(icon);
@ -176,9 +173,9 @@ public class EntityInspector
ImGui.TableSetupColumn("Entity", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableNextColumn();
var hasExpanded = window.Has<Expanded, Wildcard>();
var hasExpanded = window.Has<Expanded, Core.Wildcard>();
if (IconButtonWithToolTip(Icon.Outdent, "Collapse all items in the Explorer View", hasExpanded))
window.Remove<Expanded, Wildcard>();
window.Remove<Expanded, Core.Wildcard>();
if (history != null) {
var hasPrev = ((selected != null) ? history.Current?.Prev : history.Current) != null;
@ -201,9 +198,7 @@ public class EntityInspector
ImGui.TableNextColumn();
if (IconButtonWithToolTip(Icon.PlusCircle, "Create a new child entity", (selected != null)))
// FIXME: Replace this once Flecs has been fixed.
SetSelected(window, history, world.New().Build().ChildOf(selected));
// SelectAndScrollTo(windowEntity, windowData, selected!.NewChild().Build(), selected);
SetSelected(window, history, selected?.NewChild().Build());
ImGui.SameLine();
if (IconButtonWithToolTip(Icon.Pencil, "Rename the current entity", false && (selected != null)))
@ -214,26 +209,26 @@ public class EntityInspector
var icon = !isDisabled ? Icon.BellSlash : Icon.Bell;
var tooltip = $"{(!isDisabled ? "Disable" : "Enable")} the current entity";
if (IconButtonWithToolTip(icon, tooltip, (selected != null)))
{ if (isDisabled) selected!.Enable(); else selected!.Disable(); }
{ if (isDisabled) selected?.Enable(); else selected?.Disable(); }
ImGui.SameLine();
if (IconButtonWithToolTip(Icon.Trash, "Delete the current entity", (selected != null))) {
// TODO: Delete history for deleted entity?
SetSelected(window, history, selected!.Parent);
selected.Delete(); // TODO: Confirmation dialog?
SetSelected(window, history, selected?.Parent);
selected?.Delete(); // TODO: Confirmation dialog?
}
ImGui.EndTable();
ImGui.PopStyleVar();
}
private void PathInput(EntityRef window, History? history, EntityRef? selected, float availableWidth)
private static void PathInput<T>(Entity<T> window, History? history, Entity<T>? selected, float availableWidth)
{
var style = ImGui.GetStyle();
ImGui.AlignTextToFramePadding();
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, style.ItemSpacing.Y));
var path = selected?.GetFullPath() ?? null;
var path = selected?.Path ?? null;
if (path != null) {
var visiblePath = path.GetParts().ToList();
@ -261,8 +256,10 @@ public class EntityInspector
ImGui.EndDisabled();
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip(path[1..(numHiddenItems+2)].ToString());
} else if (ImGui.Button(visiblePath[i]))
SetSelected(window, history, window.World.LookupByPath(path[..(actualIndex+1)]));
} else if (ImGui.Button(visiblePath[i])) {
var toSelect = window.World.LookupPathOrNull(path[..(actualIndex + 1)]);
SetSelected(window, history, toSelect);
}
ImGui.SameLine();
}
}
@ -275,12 +272,27 @@ public class EntityInspector
ImGui.PopStyleVar();
}
private class ExplorerEntry
: IComparable<ExplorerEntry>
private interface IExplorerEntry
: IComparable<IExplorerEntry>
{
private readonly EntityInspector _module;
public Entity Entity { get; }
public int NumChildren { get; }
public bool HasChildren { get; }
public bool IsExpanded { get; }
public bool IsDisabled { get; }
public string? Name { get; }
public string? DocName { get; }
public EntityRef Entity { get; }
public float Priority { get; }
public Entity? Type { get; }
}
private class ExplorerEntry<T>
: IExplorerEntry
{
public Entity<T> Entity { get; }
Entity IExplorerEntry.Entity => Entity;
public int NumChildren { get; }
public bool HasChildren => (NumChildren > 0);
public bool IsExpanded { get; }
@ -292,65 +304,67 @@ public class EntityInspector
public string? DocName => (_docNameCached++ > 0) ? _docName : _docName = Entity.GetDocName();
private float? _priority;
private EntityRef? _type;
public float Priority => EnsureTypeCached()._priority!.Value;
public EntityRef? Type => EnsureTypeCached()._type;
private Entity? _type;
public float Priority => EnsureTypeCached()._priority!.Value;
public Entity? Type => EnsureTypeCached()._type;
public ExplorerEntry(EntityInspector module, EntityRef entity,
int numChildren, bool isExpanded, bool isDisabled)
public ExplorerEntry(Entity<T> entity,
int numChildren, bool isExpanded, bool isDisabled)
{
_module = module;
Entity = entity;
NumChildren = numChildren;
IsExpanded = (isExpanded && HasChildren);
IsDisabled = isDisabled;
}
private ExplorerEntry EnsureTypeCached()
private ExplorerEntry<T> EnsureTypeCached()
{
if (_priority != null) return this;
(_type, _priority) = _module.FindDisplayType(Entity);
(_type, _priority) = FindDisplayType(Entity);
return this;
}
public int CompareTo(ExplorerEntry? other)
public int CompareTo(IExplorerEntry? other)
{
if (other == null) return -1;
return Compare(Priority, other.Priority)
?? Compare(Name, other.Name)
?? Compare(DocName, other.DocName)
?? Compare(Entity.Id, other.Entity.Id)
?? Compare(Entity.NumericId, other.Entity.NumericId)
?? 0;
static int? Compare<T>(T x, T y) {
static int? Compare<TCompare>(TCompare x, TCompare y)
{
if (x is null) { if (y is null) return null; else return 1; }
else if (y is null) return -1;
var result = Comparer<T>.Default.Compare(x, y);
var result = Comparer<TCompare>.Default.Compare(x, y);
return (result != 0) ? result : null;
}
}
}
private void ExplorerView(EntityRef window, History? history, EntityRef? selected)
private static void ExplorerView<T>(Entity<T> window, History? history, Entity<T>? selected)
{
// For some reason, the analyzer thinks world can be
// nullable, so let's be explicit about the type here.
World world = window.World;
var world = window.World;
var Wildcard = world.LookupByTypeOrThrow<Wildcard>().Entity;
var Any = world.LookupByTypeOrThrow<Any>().Entity;
var This = world.LookupByTypeOrThrow<This>().Entity;
var Var = world.LookupByTypeOrThrow<Variable>().Entity;
var Wildcard = world.Entity<Core.Wildcard>().Value;
var Any = world.Entity<Core.Any>().Value;
var This = world.Entity<Core.This>().Value;
var Variable = world.Entity<Core.Variable>().Value;
bool IsSpecialEntity(Entity entity)
=> (entity == Wildcard) || (entity == Any)
|| (entity == This) || (entity == Var);
var expId = world.LookupByTypeOrThrow<Expanded>().Id;
List<ExplorerEntry> GetEntries(Entity? parent) {
var result = new List<ExplorerEntry>();
using var rule = new Rule(world, new(
$"(ChildOf, {parent?.Id ?? 0})" // Must be child of parent, or root entity.
+ $",?{expId}({window.Id}, $This)" // Whether entity is expanded in explorer view.
+ $",?Disabled" // Don't filter out disabled entities.
|| (entity == This) || (entity == Variable);
var expId = world.Entity<Expanded>().NumericId;
List<IExplorerEntry> GetEntries(Entity? parent) {
var result = new List<IExplorerEntry>();
using var rule = new Rule<T>(world, new(
$"(ChildOf, {parent?.NumericId ?? 0})" // Must be child of parent, or root entity.
+ $",?{expId}({window.NumericId}, $This)" // Whether entity is expanded in explorer view.
+ $",?Disabled" // Don't filter out disabled entities.
));
foreach (var iter in rule.Iter()) {
var isExpanded = iter.FieldIsSet(2);
@ -358,8 +372,8 @@ public class EntityInspector
for (var i = 0; i < iter.Count; i++) {
var entity = iter.Entity(i);
var count = IsSpecialEntity(entity) ? 0
: IdRef.Pair<ChildOf>(entity).Count;
result.Add(new(this, entity, count, isExpanded, isDisabled));
: world.Pair<Core.ChildOf>(entity).Count;
result.Add(new ExplorerEntry<T>(entity, count, isExpanded, isDisabled));
}
}
return result;
@ -373,8 +387,8 @@ public class EntityInspector
float GetIndent(int depth) => depth * (nodeHeight + spacingX);
bool RenderNode(ExplorerEntry entry, int? depth) {
var entity = entry.Entity;
bool RenderNode(IExplorerEntry entry, int? depth) {
var entity = Entity<T>.GetOrThrow(world, entry.Entity);
var startY = ImGui.GetCursorPosY();
var isVisible = ImGui.IsRectVisible(new(nodeHeight, nodeHeight));
var isSelected = (entity == selected);
@ -384,7 +398,7 @@ public class EntityInspector
// Button for expanding the child entries.
if (entry.HasChildren && !entry.IsExpanded) {
ImGui.SetCursorPosX(GetIndent(depth!.Value));
if (ImGui.Button($"##Expand{entity.Id}", new(nodeHeight)))
if (ImGui.Button($"##Expand{entity.NumericId}", new(nodeHeight)))
window.Add<Expanded>(entity);
}
@ -427,7 +441,7 @@ public class EntityInspector
var fullHeight = ImGui.GetCursorPosY() - startY - spacingY;
ImGui.SetCursorPos(new(GetIndent(depth!.Value), startY));
if (ImGui.Button($"##Expand{entity.Id}", new(nodeHeight, fullHeight)))
if (ImGui.Button($"##Expand{entity.NumericId}", new(nodeHeight, fullHeight)))
window.Remove<Expanded>(entity);
return true;
@ -439,33 +453,33 @@ public class EntityInspector
RenderNode(child, 0);
}
private void ComponentsTab(EntityRef window, History? history, EntityRef? selected)
private static void ComponentsTab<T>(Entity<T> window, History? history, Entity<T>? sel)
{
if (selected == null) return;
var ChildOf = window.World.LookupByTypeOrThrow<ChildOf>();
if (sel is not Entity<T> selected) return;
var ChildOf = window.World.Entity<Core.ChildOf>();
foreach (var id in selected.Type) {
// Hide ChildOf relations, as they are visible in the explorer.
if (id.IsPair && (id.Id.RelationUnsafe == ChildOf)) continue;
if (id.IsPair && (id.Value.RelationUnsafe == ChildOf)) continue;
RenderIdentifier(window, history, id);
}
}
private void ReferencesTab(EntityRef window, History? history, EntityRef? selected)
private static void ReferencesTab<T>(Entity<T> window, History? history, Entity<T>? sel)
{
if (selected == null) return;
if (sel is not Entity<T> selected) return;
var world = window.World;
var ChildOf = world.LookupByTypeOrThrow<ChildOf>();
var Wildcard = world.LookupByTypeOrThrow<Wildcard>();
var ChildOf = world.Entity<Core.ChildOf>();
var Wildcard = world.Entity<Core.Wildcard>();
if (ImGui.CollapsingHeader($"As {Icon.Tag} Entity", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in Iterator.FromTerm(world, new(selected)))
if (ImGui.CollapsingHeader($"As {Icon.Tag} Component", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in Iterator<T>.FromTerm(world, new(selected)))
for (var i = 0; i < iter.Count; i++)
RenderEntity(window, history, iter.Entity(i));
if (ImGui.CollapsingHeader($"As {Icon.ShareAlt} Relation", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in Iterator.FromTerm(world, new(selected, Wildcard))) {
foreach (var iter in Iterator<T>.FromTerm(world, new(selected, Wildcard))) {
var id = iter.FieldId(1);
if (id.AsPair() is not (EntityRef relation, EntityRef target)) throw new InvalidOperationException();
if (id.AsPair() is not (Entity<T> relation, Entity<T> target)) throw new InvalidOperationException();
if (relation == ChildOf) continue; // Hide ChildOf relations.
for (var i = 0; i < iter.Count; i++) {
@ -476,9 +490,9 @@ public class EntityInspector
}
if (ImGui.CollapsingHeader($"As {Icon.Bullseye} Target", ImGuiTreeNodeFlags.DefaultOpen))
foreach (var iter in Iterator.FromTerm(world, new(Wildcard, selected))) {
foreach (var iter in Iterator<T>.FromTerm(world, new(Wildcard, selected))) {
var id = iter.FieldId(1);
if (id.AsPair() is not (EntityRef relation, EntityRef target)) throw new InvalidOperationException();
if (id.AsPair() is not (Entity<T> relation, Entity<T> target)) throw new InvalidOperationException();
if (relation == ChildOf) continue; // Hide ChildOf relations.
for (var i = 0; i < iter.Count; i++) {
@ -489,7 +503,7 @@ public class EntityInspector
}
}
private void DocumentationTab(EntityRef _1, History? _2, EntityRef? selected)
private static void DocumentationTab<T>(Entity<T> _1, History? _2, Entity<T>? selected)
{
var hasSelected = (selected != null);
@ -515,7 +529,7 @@ public class EntityInspector
if (!hasSelected) ImGui.BeginDisabled();
var name = selected?.GetDocName(false) ?? "";
if (ImGui.InputText("##Name", ref name, 256))
selected!.SetDocName((name.Length > 0) ? name : null);
selected?.SetDocName((name.Length > 0) ? name : null);
if (!hasSelected) ImGui.EndDisabled();
Column($"{Icon.Comment} Description",
@ -523,7 +537,7 @@ public class EntityInspector
if (!hasSelected) ImGui.BeginDisabled();
var brief = selected?.GetDocBrief() ?? "";
if (ImGui.InputText("##Brief", ref brief, 256))
selected!.SetDocBrief((brief.Length > 0) ? brief : null);
selected?.SetDocBrief((brief.Length > 0) ? brief : null);
if (!hasSelected) ImGui.EndDisabled();
Column($"{Icon.FileText} Documentation", """
@ -539,7 +553,7 @@ public class EntityInspector
// TODO: Needs wordwrap.
if (ImGui.InputTextMultiline("##Detail", ref detail, 2048,
new(-float.Epsilon, Math.Max(minHeight, availHeight))))
selected!.SetDocDetail((detail.Length > 0) ? detail : null);
selected?.SetDocDetail((detail.Length > 0) ? detail : null);
if (!hasSelected) ImGui.EndDisabled();
Column($"{Icon.Link} Link", """
@ -549,7 +563,7 @@ public class EntityInspector
if (!hasSelected) ImGui.BeginDisabled();
var link = selected?.GetDocLink() ?? "";
if (ImGui.InputText("##Link", ref link, 256))
selected!.SetDocLink((link.Length > 0) ? link : null);
selected?.SetDocLink((link.Length > 0) ? link : null);
if (!hasSelected) ImGui.EndDisabled();
Column($"{Icon.PaintBrush} Color", """
@ -561,15 +575,15 @@ public class EntityInspector
var hasColor = (maybeColor != null);
var color = maybeColor ?? Color.White;
if (ImGui.Checkbox("##HasColor", ref hasColor)) {
if (hasColor) selected!.SetDocColor(color.ToHexString());
else selected!.SetDocColor(null);
if (hasColor) selected?.SetDocColor(color.ToHexString());
else selected?.SetDocColor(null);
}
ImGui.SameLine();
if (!hasColor) ImGui.BeginDisabled();
ImGui.SetNextItemWidth(-float.Epsilon);
var colorVec = color.ToVector3();
if (ImGui.ColorEdit3("##Color", ref colorVec))
selected!.SetDocColor(Color.FromRGB(colorVec).ToHexString());
selected?.SetDocColor(Color.FromRGB(colorVec).ToHexString());
if (!hasColor) ImGui.EndDisabled();
if (!hasSelected) ImGui.EndDisabled();
@ -582,71 +596,71 @@ public class EntityInspector
// == Utility Functions ==
// =======================
private EntityRef NewEntityInspectorWindow(World world)
private static Entity<T> NewEntityInspectorWindow<T>(World<T> world)
=> world.New().Add<InspectorWindow>().Set(new History())
.Build().SetDocName(DefaultWindowTitle);
private void SetSelected(
EntityRef window, // The InspectorWindow entity.
private static void SetSelected<T>(
Entity<T> window, // The InspectorWindow entity.
History? history, // InspectorWindow's History component, null if it shouldn't be changed.
EntityRef? entity, // Entity to set as selected or null to unset.
Entity<T>? entity, // Entity to set as selected or null to unset.
bool scrollTo = true) // Should entity be scrolled to in the explorer view?
{
if (entity != null) window.Add<Selected>(entity);
else window.Remove<Selected, Wildcard>();
if (entity is Entity<T> e1) window.Add<Selected>(e1);
else window.Remove<Selected, Core.Wildcard>();
for (var parent = entity?.Parent; parent != null; parent = parent.Parent)
for (var p = entity?.Parent; p is Entity<T> parent; p = parent.Parent)
window.Add<Expanded>(parent);
if ((entity != null) && scrollTo)
window.Add<ScrollToSelected>();
if (history != null) {
if (entity != null) history.Current = new History.Entry(entity, history.Current, null);
if (entity is Entity<T> e2) history.Current = new(e2, e2.Path, history.Current, null);
else if (history.Current is History.Entry entry) entry.Next = null;
}
}
private void GoToPrevious(EntityRef window, History history, EntityRef? selected)
private static void GoToPrevious<T>(Entity<T> window, History history, Entity<T>? selected)
{
if (selected != null) {
if (history.Current?.Prev == null) return;
history.Current = history.Current.Prev;
} else if (history.Current == null) return;
var entity = EntityRef.CreateOrNull(window.World, history.Current.Entity);
var entity = Entity<T>.GetOrNull(window.World, history.Current.Entity);
SetSelected(window, null, entity);
// TODO: Set path if entity could not be found.
}
private void GoToNext(EntityRef window, History history)
private static void GoToNext<T>(Entity<T> window, History history)
{
if (history.Current?.Next == null) return;
history.Current = history.Current.Next;
var entity = EntityRef.CreateOrNull(window.World, history.Current.Entity);
var entity = Entity<T>.GetOrNull(window.World, history.Current.Entity);
SetSelected(window, null, entity);
// TODO: Set path if entity could not be found.
}
private Rule? _findDisplayTypeRule;
private (EntityRef? DisplayType, float Priority) FindDisplayType(EntityRef entity)
private static object? _findDisplayTypeRule;
private static (Entity<T>? DisplayType, float Priority) FindDisplayType<T>(Entity<T> entity)
{
var world = entity.World;
var component = world.LookupByTypeOrThrow<Component>();
var component = world.Entity<Core.Component>();
var rule = _findDisplayTypeRule ??= new Rule(world, new(
$"$Type, gaemstone.Doc.DisplayType($Type)"));
var rule = (Rule<T>)(_findDisplayTypeRule ??= new Rule<T>(world, new(
$"$Type, gaemstone.Doc.DisplayType($Type)")));
var typeVar = rule.Variables["Type"]!;
var curType = (EntityRef?)null;
var curType = (Entity<T>?)null;
var curPriority = float.MaxValue;
foreach (var iter in _findDisplayTypeRule.Iter().SetVar(rule.ThisVar!, entity))
foreach (var iter in rule.Iter().SetVar(rule.ThisVar!, entity))
for (var i = 0; i < iter.Count; i++) {
var type = iter.GetVar(typeVar);
if ((type == component) && (entity.GetOrNull<Component>(component)?.Size == 0))
type = world.LookupByTypeOrThrow<Tag>();
var priority = type.GetOrNull<DocPriority>()?.Value ?? float.MaxValue;
if ((type == component) && (entity.GetOrNull<Core.Component>(component)?.Size == 0))
type = world.Entity<Core.Tag>();
var priority = type?.GetOrNull<DocPriority>()?.Value ?? float.MaxValue;
if (priority <= curPriority) { curType = type; curPriority = priority; }
}
@ -658,24 +672,24 @@ public class EntityInspector
// == Utility ImGui Functions ==
// =============================
private void RenderIdentifier(EntityRef window, History? history, IdRef id)
private static void RenderIdentifier<T>(Entity<T> window, History? history, Id<T> id)
{
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, ImGui.GetStyle().ItemSpacing.Y));
if (id.AsPair() is (EntityRef relation, EntityRef target)) {
if (id.AsPair() is (Entity<T> relation, Entity<T> target)) {
ImGui.TextUnformatted("("); ImGui.SameLine();
RenderEntity(window, history, relation);
ImGui.SameLine(); ImGui.TextUnformatted(","); ImGui.SameLine();
RenderEntity(window, history, target);
ImGui.SameLine(); ImGui.TextUnformatted(")");
} else if (id.AsEntity() is EntityRef entity)
} else if (id.AsEntity() is Entity<T> entity)
RenderEntity(window, history, entity);
else
ImGui.TextUnformatted(id.ToString());
ImGui.PopStyleVar();
}
private void RenderEntity(
EntityRef window, History? history, EntityRef entity,
private static void RenderEntity<T>(
Entity<T> window, History? history, Entity<T> entity,
RenderEntityFlags flags = default)
{
var spanAvailWidth = (flags & RenderEntityFlags.SpanAvailWidth) != 0;
@ -696,7 +710,7 @@ public class EntityInspector
var docName = entity.GetDocName(false);
var isDisabled = entity.IsDisabled;
var displayName = (docName != null) ? $"\"{docName}\"" : entity.Name ?? entity.Id.ToString();
var displayName = (docName != null) ? $"\"{docName}\"" : entity.Name ?? entity.NumericId.ToString();
if (docIcon != null) displayName = $"{docIcon} {displayName}";
// Gotta push the font to calculate size properly.
@ -715,10 +729,10 @@ public class EntityInspector
: Color.Transparent.RGBA);
ImGui.PushStyleColor(ImGuiCol.ButtonActive , ImGui.GetColorU32(ImGuiCol.HeaderActive));
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.HeaderHovered));
ImGui.Button($"##{entity.Id}", size);
ImGui.Button($"##{entity.NumericId}", size);
ImGui.PopStyleColor(3);
ImGui.PopStyleVar();
} else ImGui.InvisibleButton($"##{entity.Id}", size);
} else ImGui.InvisibleButton($"##{entity.NumericId}", size);
var shift = ImGui.IsKeyDown(ImGuiKey.ModShift);
var hovered = ImGui.IsItemHovered();
@ -746,7 +760,7 @@ public class EntityInspector
if (hovered) {
ImGui.BeginTooltip();
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]);
ImGui.TextUnformatted(entity.GetFullPath().ToString());
ImGui.TextUnformatted(entity.Path.ToString());
ImGui.PopFont();
if (isDisabled) {
ImGui.SameLine();

@ -12,7 +12,7 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Components.CameraComponents>]
[DependsOn<gaemstone.Client.Components.InputComponents>]
[DependsOn<gaemstone.Components.TransformComponents>]
public class FreeCameraController
public partial class FreeCameraController
{
[Component]
public struct CameraController
@ -21,24 +21,25 @@ public class FreeCameraController
}
[System]
public static void UpdateCamera(
Universe universe, TimeSpan delta,
in Camera camera, ref GlobalTransform transform, ref CameraController controller)
public static void UpdateCamera<T>(
World<T> world, TimeSpan delta,
in Camera camera, ref GlobalTransform transform, in CameraController controller)
{
var input = universe.LookupByType<Input>();
var mouse = universe.LookupByType<Mouse>();
var keyboard = universe.LookupByType<Keyboard>();
if ((input == null) || (mouse == null) || (keyboard == null)) return;
var input = world.Entity<Input>();
var mouse = world.Entity<Mouse>();
var keyboard = world.Entity<Keyboard>();
// FIXME: Is it okay if this crashes?
// if ((input == null) || (mouse == null) || (keyboard == null)) return;
var module = universe.LookupByTypeOrThrow<FreeCameraController>();
var module = world.Entity<FreeCameraController>();
var capturedBy = input.GetTargets<CursorCapturedBy>().FirstOrDefault();
var inputCapturedBy = input.GetTargets<MouseInputCapturedBy>().FirstOrDefault();
var isCaptured = (capturedBy != null);
var isCaptured = capturedBy.IsSome;
// If another system has the mouse captured, don't do anything here.
if (isCaptured && (capturedBy != module)) return;
var isMouseDown = ((inputCapturedBy == null) || (inputCapturedBy == module))
&& mouse.LookupChild("Buttons/Right")?.Has<Active>() == true;
var isMouseDown = (inputCapturedBy.IsNone || (inputCapturedBy == module))
&& mouse.LookupChildOrNull("Buttons/Right")?.Has<Active>() == true;
if (isMouseDown != isCaptured) {
if (isMouseDown)
input.Add<CursorCapturedBy>(module);
@ -51,18 +52,18 @@ public class FreeCameraController
var mouseMovement = Vector2.Zero;
if (isCaptured) {
var raw = (Vector2?)mouse.LookupChild("Delta")?.GetOrThrow<RawValue2D>() ?? default;
var raw = (Vector2?)mouse.LookupChildOrNull("Delta")?.GetOrThrow<RawValue2D>() ?? default;
mouseMovement = raw * controller.MouseSensitivity * (float)delta.TotalSeconds;
}
if (camera.IsOrthographic) {
transform *= Matrix4x4.CreateTranslation(-mouseMovement.X, -mouseMovement.Y, 0);
} else {
var shift = keyboard.LookupChild("ShiftLeft")?.Has<Active>() == true;
var w = keyboard.LookupChild("W")?.Has<Active>() == true;
var a = keyboard.LookupChild("A")?.Has<Active>() == true;
var s = keyboard.LookupChild("S")?.Has<Active>() == true;
var d = keyboard.LookupChild("D")?.Has<Active>() == true;
var shift = keyboard.LookupChildOrNull("ShiftLeft")?.Has<Active>() == true;
var w = keyboard.LookupChildOrNull("W")?.Has<Active>() == true;
var a = keyboard.LookupChildOrNull("A")?.Has<Active>() == true;
var s = keyboard.LookupChildOrNull("S")?.Has<Active>() == true;
var d = keyboard.LookupChildOrNull("D")?.Has<Active>() == true;
var speed = (shift ? 12 : 4) * (float)delta.TotalSeconds;
var forwardMovement = ((w ? -1 : 0) + (s ? 1 : 0)) * speed;

@ -7,12 +7,12 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
public class ImGuiDemoWindow
public partial class ImGuiDemoWindow
{
private bool _isOpen = false;
private static bool _isOpen = false;
[System]
public void Show(ImGuiData _)
public static void Show(ImGuiData _)
{
if (ImGuiUtility.UIButtonToggle(2, ForkAwesome.WindowMaximize, "ImGui Demo Window", ref _isOpen))
ImGui.ShowDemoWindow(ref _isOpen);

@ -15,15 +15,14 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn<gaemstone.Client.Components.InputComponents>]
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
public class ImGuiInputDebug
public partial class ImGuiInputDebug
{
private bool _isOpen = false;
private static bool _isOpen = false;
[System]
public void ShowInputDebugWindow(Universe universe, ImGuiData _)
public static void ShowInputDebugWindow<T>(World<T> world, ImGuiData _)
{
var input = universe.LookupByType<Input>();
if (input == null) return;
var input = world.Entity<Input>();
if (!ImGuiUtility.UIButtonToggle(1, ForkAwesome.Gamepad, "Input Information", ref _isOpen)) return;
@ -34,19 +33,20 @@ public class ImGuiInputDebug
ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize)) {
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[0]);
if (universe.LookupByType<Keyboard>() is EntityRef keyboard)
DrawKeyboard(keyboard);
var keyboard = world.Entity<Keyboard>();
DrawKeyboard(keyboard);
if (universe.LookupByType<Mouse>() is EntityRef mouse) {
var mouse = world.Entity<Mouse>();
{
ImGui.BeginChild("Mouse Info", new(160, 180), true);
ImGui.Text("Position: " + (Vector2?)mouse.LookupChild("Position")?.GetOrNull<RawValue2D>());
ImGui.Text("Delta: " + (Vector2?)mouse.LookupChild("Delta" )?.GetOrNull<RawValue2D>());
ImGui.Text("Wheel: " + (float?) mouse.LookupChild("Wheel" )?.GetOrNull<RawValue1D>());
ImGui.Text("Position: " + (Vector2?)mouse.LookupChildOrNull("Position")?.GetOrNull<RawValue2D>());
ImGui.Text("Delta: " + (Vector2?)mouse.LookupChildOrNull("Delta" )?.GetOrNull<RawValue2D>());
ImGui.Text("Wheel: " + (float?) mouse.LookupChildOrNull("Wheel" )?.GetOrNull<RawValue1D>());
ImGui.Spacing();
var buttons = mouse.LookupChild("Buttons")?.GetChildren().ToArray() ?? Array.Empty<EntityRef>();
var buttons = mouse.LookupChildOrNull("Buttons")?.Children.ToArray() ?? Array.Empty<Entity<T>>();
ImGui.Text("Buttons: " + string.Join(" ", buttons
.Where (button => button.Has<Active>())
.Select(button => $"{button.Name} ({button.GetOrThrow<Active>().Duration.TotalSeconds:f2}s)")));
@ -60,11 +60,11 @@ public class ImGuiInputDebug
ImGui.EndChild();
}
for (var index = 0; input.LookupChild("Gamepad" + index) is EntityRef gamepad; index++) {
for (var index = 0; input.LookupChildOrNull("Gamepad" + index) is Entity<T> gamepad; index++) {
ImGui.SameLine();
ImGui.BeginChild($"{gamepad.Name} Info", new(160, 180), true);
var buttons = gamepad.LookupChild("Buttons")?.GetChildren().ToArray() ?? Array.Empty<EntityRef>();
var buttons = gamepad.LookupChildOrNull("Buttons")?.Children.ToArray() ?? Array.Empty<Entity<T>>();
ImGui.Text("Buttons: " + string.Join(" ", buttons.Where(b => b.Has<Active>())
.Select(b => $"{b.Name} ({b.GetOrThrow<Active>().Duration.TotalSeconds:f2}s)")));
ImGui.Text(" Pressed: " + string.Join(" ", buttons.Where(b => b.Has<Activated>()).Select(b => b.Name)));
@ -73,7 +73,7 @@ public class ImGuiInputDebug
ImGui.Spacing();
ImGui.Text("Triggers:");
for (var i = 0; gamepad.LookupChild("Trigger" + i) is EntityRef trigger; i++) {
for (var i = 0; gamepad.LookupChildOrNull("Trigger" + i) is Entity<T> trigger; i++) {
var text = $" {i}: {(float?)trigger.GetOrNull<RawValue1D>() ?? default:f2}";
if (trigger.Has<Activated>()) text += " pressed!";
else if (trigger.Has<Deactivated>()) text += " released!";
@ -83,7 +83,7 @@ public class ImGuiInputDebug
}
ImGui.Text("Thumbsticks:");
for (var i = 0; gamepad.LookupChild("Thumbstick" + i) is EntityRef thumbstick; i++)
for (var i = 0; gamepad.LookupChildOrNull("Thumbstick" + i) is Entity<T> thumbstick; i++)
ImGui.Text($" {i}: {(Vector2?)thumbstick.GetOrNull<RawValue2D>() ?? default:f2}");
ImGui.EndChild();
@ -139,7 +139,7 @@ public class ImGuiInputDebug
[Key.NumLock] = "Num\nLck", [Key.KeypadEnter] = "=",
};
public static void DrawKeyboard(EntityRef keyboard)
public static void DrawKeyboard<T>(Entity<T> keyboard)
{
const float UnitKeySize = 32.0f;
@ -150,10 +150,10 @@ public class ImGuiInputDebug
var current = Vector2.Zero;
foreach (var (widths, keys) in KeyboardLayout.Zip(KeyboardKeys)) {
foreach (var (width, key) in widths.Zip(keys)) {
var active = (key != null) && (keyboard.LookupChild(key.Value.ToString())?.Has<Active>() == true);
var active = (key != null) && (keyboard.LookupChildOrNull(key.Value.ToString())?.Has<Active>() == true);
var keySize = new Vector2(width, 1.0f);
if (width == T) keySize = new Vector2((-keySize.X - 10), 2.0f);
if (width == ImGuiInputDebug.T) keySize = new Vector2((-keySize.X - 10), 2.0f);
else if (width < -10) keySize = new Vector2((-keySize.X - 10), 1.0f);
else if (width < 0) { current += new Vector2(-keySize.X * UnitKeySize, 0); continue; }

@ -4,8 +4,8 @@ using System.Linq;
using System.Text;
using gaemstone.Client.Utility;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
using gaemstone.Flecs;
using gaemstone.Utility;
using ImGuiNET;
using Silk.NET.Input;
using Silk.NET.OpenGL.Extensions.ImGui;
@ -19,17 +19,17 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Components.InputComponents>]
[DependsOn<gaemstone.Client.Systems.InputManager>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
public class ImGuiManager
public partial class ImGuiManager
{
[Entity, Add<Pipeline.Phase>]
[DependsOn<SystemPhase.OnLoad>]
[DependsOn<Pipeline.OnLoad>]
public struct ImGuiUpdatePhase { }
[Entity, Add<Pipeline.Phase>]
[DependsOn<SystemPhase.OnStore>]
[DependsOn<Pipeline.OnStore>]
public struct ImGuiRenderPhase { }
[Component, Singleton(AutoAdd = false)]
[Singleton]
public class ImGuiData
{
public ImGuiController Controller { get; }
@ -84,10 +84,11 @@ public class ImGuiManager
return io.Fonts.AddFontFromMemoryTTF((IntPtr)dataPtr, size, size, cfg);
}
[System<SystemPhase.OnLoad>]
public unsafe void Initialize(Universe universe, GameWindow window, Canvas canvas,
[Source<Input>] InputContext inputContext, [Not] ImGuiData _)
=> universe.LookupByTypeOrThrow<ImGuiData>().Set(new ImGuiData(
[System]
[DependsOn<Pipeline.OnLoad>]
public static unsafe void Initialize<T>(World<T> world, GameWindow window, Canvas canvas,
[Source<Input>] InputContext inputContext, Not<ImGuiData> _)
=> world.Entity<ImGuiData>().Set(new ImGuiData(
new(canvas.GL, window.Handle, inputContext.Value, () => {
var io = ImGui.GetIO();
var style = ImGui.GetStyle();
@ -139,15 +140,16 @@ public class ImGuiManager
}
})));
[System<SystemPhase.OnLoad>]
public static void UpdateMouse(Universe universe,
[System]
[DependsOn<Pipeline.OnLoad>]
public static void UpdateMouse<T>(World<T> world,
[Source<Mouse>] MouseImpl impl, ImGuiData _)
{
var mouse = impl.Value;
var input = universe.LookupByTypeOrThrow<Input>();
var module = universe.LookupByTypeOrThrow<ImGuiManager>();
var input = world.Entity<Input>();
var module = world.Entity<ImGuiManager>();
var capturedBy = input.GetTargets<MouseInputCapturedBy>().FirstOrDefault();;
var isCaptured = (capturedBy != null);
var isCaptured = capturedBy.IsSome;
// If another system has the mouse captured, don't do anything here.
if (isCaptured && (capturedBy != module)) return;
@ -180,11 +182,13 @@ public class ImGuiManager
};
}
[System<ImGuiUpdatePhase>]
[System]
[DependsOn<ImGuiUpdatePhase>]
public static void Update(TimeSpan delta, ImGuiData imgui)
=> imgui.Controller.Update((float)delta.TotalSeconds);
[System<ImGuiRenderPhase>]
[System]
[DependsOn<ImGuiRenderPhase>]
public static void Render(ImGuiData imgui)
=> imgui.Controller.Render();
}

@ -13,7 +13,7 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn<gaemstone.Client.Components.InputComponents>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
public class InputManager
public partial class InputManager
{
[Component] public record class InputContext(IInputContext Value) { }
@ -21,11 +21,12 @@ public class InputManager
[Component] public record class KeyboardImpl(IKeyboard Value) { }
[Component] public record class GamepadImpl(IGamepad Value) { }
[System<SystemPhase.OnLoad>]
public static void Initialize(Universe universe,
[Game] GameWindow window, [Source<Input>, Not] InputContext _)
[System]
[DependsOn<Pipeline.OnLoad>]
public static void Initialize<T>(World<T> world,
GameWindow window, [Source<Input>] Not<InputContext> _)
{
var input = universe.LookupByTypeOrThrow<Input>();
var input = world.Entity<Input>();
var context = window.Handle.CreateInput();
input.Set(new InputContext(context));
@ -42,24 +43,25 @@ public class InputManager
}
[Observer<ObserverEvent.OnAdd>]
[Expression("CursorCapturedBy(Input, *)")]
public static void OnCursorCaptured(Universe universe)
=> universe.LookupByTypeOrThrow<Mouse>().GetOrThrow<MouseImpl>()
[Observer<Core.OnAdd>]
public static void OnCursorCaptured<T>(World<T> universe,
[Source<Input>] Has<CursorCapturedBy, Core.Wildcard> _)
=> universe.Entity<Mouse>().GetOrThrow<MouseImpl>()
.Value.Cursor.CursorMode = CursorMode.Raw;
[Observer<ObserverEvent.OnRemove>]
[Expression("CursorCapturedBy(Input, *)")]
public static void OnCursorReleased(Universe universe)
=> universe.LookupByTypeOrThrow<Mouse>().GetOrThrow<MouseImpl>()
[Observer<Core.OnRemove>]
public static void OnCursorReleased<T>(World<T> universe,
[Source<Input>] Has<CursorCapturedBy, Core.Wildcard> _)
=> universe.Entity<Mouse>().GetOrThrow<MouseImpl>()
.Value.Cursor.CursorMode = CursorMode.Normal;
[System<SystemPhase.OnLoad>]
public static void ProcessMouse(TimeSpan delta, EntityRef entity, MouseImpl impl)
[System]
[DependsOn<Pipeline.OnLoad>]
public static void ProcessMouse<T>(TimeSpan delta, Entity<T> entity, MouseImpl impl)
{
var mouse = impl.Value;
var isCaptured = entity.Parent!.Has<CursorCapturedBy, Core.Any>();
var isCaptured = entity.Parent?.Has<CursorCapturedBy, Core.Any>() ?? false;
ref var position = ref entity.NewChild("Position").Build().GetMut<RawValue2D>();
ref var posDelta = ref entity.NewChild("Delta" ).Build().GetMut<RawValue2D>();
posDelta = mouse.Position - position;
@ -74,8 +76,9 @@ public class InputManager
mouse.IsButtonPressed(button) ? 1 : 0);
}
[System<SystemPhase.OnLoad>]
public static void ProcessKeyboard(TimeSpan delta, EntityRef entity, KeyboardImpl impl)
[System]
[DependsOn<Pipeline.OnLoad>]
public static void ProcessKeyboard<T>(TimeSpan delta, Entity<T> entity, KeyboardImpl impl)
{
var keyboard = impl.Value;
foreach (var key in keyboard.SupportedKeys) {
@ -84,8 +87,9 @@ public class InputManager
}
}
[System<SystemPhase.OnLoad>]
public static void ProcessGamepad(TimeSpan delta, EntityRef entity, GamepadImpl impl)
[System]
[DependsOn<Pipeline.OnLoad>]
public static void ProcessGamepad<T>(TimeSpan delta, Entity<T> entity, GamepadImpl impl)
{
var gamepad = impl.Value;
var buttons = entity.NewChild("Buttons").Build();
@ -101,29 +105,42 @@ public class InputManager
private const float ActivationThreshold = 0.90f;
private const float DeactivationThreshold = 0.75f;
private static void Update1D(TimeSpan delta, EntityRef entity, float current)
private static void Update1D<T>(TimeSpan delta, Entity<T> entity, float current)
{
entity.GetMut<RawValue1D>() = current;
if (current >= ActivationThreshold) {
ref var active = ref entity.GetRefOrNull<Active>();
if (Unsafe.IsNullRef(ref active)) {
// TODO: Find out why using GetRefOrNull here crashes.
if (entity.Has<Active>()) {
entity.GetMut<Active>().Duration += delta;
} else {
entity.Set(new Active());
entity.Add<Activated>();
} else active.Duration += delta;
}
// ref var active = ref entity.GetRefOrNull<Active>();
// if (Unsafe.IsNullRef(ref active)) {
// entity.Set(new Active());
// entity.Add<Activated>();
// } else active.Duration += delta;
} else if (current <= DeactivationThreshold)
entity.Remove<Active>();
}
private static void Update2D(TimeSpan delta, EntityRef entity, Vector2 current)
private static void Update2D<T>(TimeSpan delta, Entity<T> entity, Vector2 current)
{
entity.GetMut<RawValue2D>() = current;
var magnitude = current.Length();
if (magnitude >= ActivationThreshold) {
ref var active = ref entity.GetRefOrNull<Active>();
if (Unsafe.IsNullRef(ref active)) {
if (entity.Has<Active>()) {
entity.GetMut<Active>().Duration += delta;
} else {
entity.Set(new Active());
entity.Add<Activated>();
} else active.Duration += delta;
}
// ref var active = ref entity.GetRefOrNull<Active>();
// if (Unsafe.IsNullRef(ref active)) {
// entity.Set(new Active());
// entity.Add<Activated>();
// } else active.Duration += delta;
} else if (magnitude <= DeactivationThreshold)
entity.Remove<Active>();
}
@ -134,11 +151,12 @@ public class InputManager
// public static void OnActiveAdded(EntityRef entity, Active _)
// => entity.Add<Activated>();
[Observer<ObserverEvent.OnRemove>]
public static void OnActiveRemoved(EntityRef entity, Active _)
=> entity.Add<Deactivated>();
// [Observer<Core.OnRemove>]
// public static void OnActiveRemoved<T>(Entity<T> entity, Active _)
// => entity.Add<Deactivated>();
[System<SystemPhase.PostFrame>]
public static void ClearDeActivated(EntityRef entity, [Or] Activated _1, [Or] Deactivated _2)
=> entity.Remove<Activated>().Remove<Deactivated>();
// [System]
// [DependsOn<Pipeline.PostFrame>]
// public static void ClearDeActivated<T>(Entity<T> entity, Has<Or<Activated, Deactivated>> _)
// => entity.Remove<Activated>().Remove<Deactivated>();
}

@ -14,19 +14,18 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
[DependsOn<gaemstone.Client.Components.ResourceComponents>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
public class MeshManager
public partial class MeshManager
{
private const uint PositionAttribIndex = 0;
private const uint NormalAttribIndex = 1;
private const uint UvAttribIndex = 2;
[System]
public static void LoadMeshWhenDefined(
[Game] Canvas canvas, EntityRef entity,
Mesh _1, [Not] MeshHandle _2)
public static void LoadMeshWhenDefined<T>(
Canvas canvas, Entity<T> entity,
Has<Mesh> _1, Not<MeshHandle> _2)
{
var path = entity.GetFullPath();
using var stream = Resources.GetStream(path);
using var stream = Resources.GetStream(entity.Path);
var handle = CreateFromStream(canvas.GL, stream);
entity.Set(handle);
}

@ -3,8 +3,8 @@ using System.Diagnostics;
using System.Numerics;
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
using gaemstone.Flecs;
using gaemstone.Utility;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
using static gaemstone.Client.Components.CameraComponents;
@ -19,15 +19,15 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
[DependsOn<gaemstone.Components.TransformComponents>]
public class Renderer
public partial class Renderer
{
private uint _program;
private int _cameraMatrixUniform;
private int _modelMatrixUniform;
private Rule? _renderEntityRule;
private static uint _program;
private static int _cameraMatrixUniform;
private static int _modelMatrixUniform;
private static object? _renderEntityRule;
[Observer<ObserverEvent.OnSet>]
public void OnCanvasSet(Canvas canvas)
[Observer<Core.OnSet>]
public static void OnCanvasSet(Canvas canvas)
{
var GL = canvas.GL;
@ -54,8 +54,9 @@ public class Renderer
_modelMatrixUniform = GL.GetUniformLocation(_program, "modelMatrix");
}
[System<SystemPhase.PreStore>]
public void Clear(Canvas canvas)
[System]
[DependsOn<Pipeline.PreStore>]
public static void Clear(Canvas canvas)
{
var GL = canvas.GL;
GL.UseProgram(_program);
@ -64,8 +65,9 @@ public class Renderer
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
}
[System<SystemPhase.OnStore>]
public void Render(Universe universe, [Game] Canvas canvas,
[System]
[DependsOn<Pipeline.OnStore>]
public static void Render<T>(World<T> world, Canvas canvas,
in GlobalTransform cameraTransform, in Camera camera, CameraViewport? viewport)
{
var color = viewport?.ClearColor ?? Color.FromRGB(0.3f, 0.0f, 0.5f);
@ -83,7 +85,7 @@ public class Renderer
bounds.Width, -bounds.Height,
camera.NearPlane, camera.FarPlane)
: Matrix4x4.CreatePerspectiveFieldOfView(
camera.FieldOfView * MathF.PI / 180, // Degrees => Radians
camera.FieldOfView * MathF.PI / 180, // Degrees => Radians
(float)bounds.Width / bounds.Height, // Aspect Ratio
camera.NearPlane, camera.FarPlane);
@ -93,28 +95,28 @@ public class Renderer
var cameraMatrix = invertedTransform * cameraProjection;
GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.M11);
_renderEntityRule ??= new(universe, new("""
GlobalTransform,
(Mesh, $mesh), MeshHandle($mesh),
?(Texture, $tex), ?TextureHandle($tex)
"""));
foreach (var iter in _renderEntityRule.Iter()) {
var rule = (Rule<T>)(_renderEntityRule ??= new Rule<T>(world, new("""
[in] GlobalTransform,
(Mesh, $mesh), [in] MeshHandle($mesh),
?(Texture, $tex), [in] ?TextureHandle($tex)
""")));
foreach (var iter in rule.Iter()) {
var transforms = iter.Field<GlobalTransform>(1);
var meshes = iter.Field<MeshHandle>(3);
// var texPairs = iter.FieldOrEmpty<Identifier>(4);
var textures = iter.FieldOrEmpty<TextureHandle>(5);
for (var i = 0; i < iter.Count; i++) {
var rTransform = transforms[i];
var mesh = meshes[i];
var transform = transforms[i];
var mesh = meshes[i];
// var hasTexture = (texPairs.Length > 0);
var texture = textures.GetOrNull(i);
var texture = textures.GetOrNull(i);
// If entity has Texture, bind it now.
if (texture.HasValue) GL.BindTexture(texture.Value.Target, texture.Value.Handle);
// Draw the mesh.
GL.UniformMatrix4(_modelMatrixUniform, 1, false, in rTransform.Value.M11);
GL.UniformMatrix4(_modelMatrixUniform, 1, false, in transform.Value.M11);
GL.BindVertexArray(mesh.Handle);
if (!mesh.IsIndexed) GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)mesh.Count);
else unsafe { GL.DrawElements(PrimitiveType.Triangles, (uint)mesh.Count, DrawElementsType.UnsignedShort, null); }
@ -125,7 +127,8 @@ public class Renderer
}
}
[System<SystemPhase.PostFrame>]
[System]
[DependsOn<Pipeline.PostFrame>]
public static void SwapBuffers(GameWindow window)
=> window.Handle.SwapBuffers();

@ -15,9 +15,9 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
[DependsOn<gaemstone.Client.Components.ResourceComponents>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
public class TextureManager
public partial class TextureManager
{
[Observer<ObserverEvent.OnSet>]
[Observer<Core.OnSet>]
public static void OnCanvasSet(Canvas canvas)
{
var GL = canvas.GL;
@ -31,12 +31,11 @@ public class TextureManager
}
[System]
public static void LoadTextureWhenDefined(
[Game] Canvas canvas, EntityRef entity,
Texture _1, [Not] TextureHandle _2)
public static void LoadTextureWhenDefined<T>(
Canvas canvas, Entity<T> entity,
Has<Texture> _1, Not<TextureHandle> _2)
{
var path = entity.GetFullPath();
using var stream = Resources.GetStream(path);
using var stream = Resources.GetStream(entity.Path);
var handle = CreateFromStream(canvas.GL, stream);
entity.Set(handle);
}

@ -7,9 +7,11 @@ using Silk.NET.Windowing;
namespace gaemstone.Client.Systems;
[Module]
public class Windowing
public partial class Windowing
{
[Component]
// TODO: Canvas and GameWindow should maybe not be Singleton?
[Singleton]
public class Canvas
{
public GL GL { get; }
@ -19,14 +21,15 @@ public class Windowing
public Color BackgroundColor { get; set; }
}
[Component]
[Singleton]
public class GameWindow
{
public IWindow Handle { get; }
public GameWindow(IWindow handle) => Handle = handle;
}
[System<SystemPhase.PreFrame>]
[System]
[DependsOn<Pipeline.PreFrame>]
public static void ProcessWindow(GameWindow window, Canvas canvas)
{
canvas.Size = new(window.Handle.Size.X, window.Handle.Size.Y);

@ -6,15 +6,18 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <!-- -->
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/*" />
<EmbeddedResource Include="Resources/**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../gaemstone/gaemstone.csproj" />
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
<ProjectReference Include="../gaemstone.SourceGen/gaemstone.SourceGen.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="../ImGui.NET/src/ImGui.NET/ImGui.NET.csproj" />
</ItemGroup>

@ -1 +1 @@
Subproject commit 46e171940eea723f2ad8376b5056afd9b9d8a340
Subproject commit eec968d6361930eebad180f6300d126c4bac70f3

@ -0,0 +1,230 @@
using Microsoft.CodeAnalysis;
namespace gaemstone.SourceGen;
public static class Descriptors
{
public const string DiagnosticCategory = "gaemstone.SourceGen";
// TODO: Replace this counter with proper hardcoded IDs.
private static int _idCounter = 1;
// Diagnostics relating to where the symbol occurs.
public static readonly DiagnosticDescriptor ModuleMustNotBeNested = new(
$"gSG{_idCounter++:00}", "Module must not be a nested class",
"A [Module] must be defined as a top-level class.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor EntityMustBeInModule = new(
$"gSG{_idCounter++:00}", "Entity must be part of a Module",
"Entity type must be defined within a [Module].",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor InstanceMethodOnlyValidInSingleton = new(
$"gSG{_idCounter++:00}", "Non-static method is only valid in Singleton Module",
"Non-static [System] or [Observer] is only valid in a [Module] which is also marked as a [Singleton].",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor ParamMustBeInMethod = new(
$"gSG{_idCounter++:00}", "Parameter must be part of a System or Observer",
"This parameter must be part of a method marked as [System] or [Observer].",
DiagnosticCategory, DiagnosticSeverity.Error, true);
// Diagnostics relating to the combined usage of attributes.
public static readonly DiagnosticDescriptor AttributesRequireTypeOrPath = new(
$"gSG{_idCounter++:00}", "Entity attributes require type or path",
"Entity attributes require a type (such as [Entity]) or [Path].",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor InvalidAttributeCombination = new(
$"gSG{_idCounter++:00}", "Invalid combination of entity attributes",
"The combination of entity attributes {0} is not valid.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor ValidModuleAttributesHint = new(
$"gSG{_idCounter++:00}", "Module may be a Singleton",
"A [Module] may be marked as a [Singleton].",
DiagnosticCategory, DiagnosticSeverity.Info, true);
public static readonly DiagnosticDescriptor ValidRelationAttributesHint = new(
$"gSG{_idCounter++:00}", "Relation may be a Tag or Component",
"A [Relation] may be marked as a [Tag] or [Component].",
DiagnosticCategory, DiagnosticSeverity.Info, true);
public static readonly DiagnosticDescriptor SingletonImpliesComponentHint = new(
$"gSG{_idCounter++:00}", "Singleton implies Component",
"A [Singleton] is already implied to be a [Component].",
DiagnosticCategory, DiagnosticSeverity.Info, true);
public static readonly DiagnosticDescriptor BuiltInModuleMustHavePath = new(
$"gSG{_idCounter++:00}", "BuiltIn Module must have Path",
"A [BuiltIn, Module] must also have a [Path] set.",
DiagnosticCategory, DiagnosticSeverity.Info, true);
public static readonly DiagnosticDescriptor BuiltInModuleMustNotHaveMethods = new(
$"gSG{_idCounter++:00}", "BuiltIn Module must not have System / Observer",
"A [BuiltIn, Module] must not have [System] or [Observer] methods.",
DiagnosticCategory, DiagnosticSeverity.Info, true);
// Diagnostics relating to keywords / modifiers used with the symbol.
public static readonly DiagnosticDescriptor ModuleMustBePartial = new(
$"gSG{_idCounter++:00}", "ModuleMustBePartial",
"A [Module] type must be marked as partial.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor TypeMustNotBeStatic = new(
$"gSG{_idCounter++:00}", "Entity type must not be static",
"Entity type must not be static.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor TypeMustNotBeAbstract = new(
$"gSG{_idCounter++:00}", "Entity type must not be abstract",
"Entity type must not be abstract.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor MethodMustNotBeAbstract = new(
$"gSG{_idCounter++:00}", "System / Observer must not be abstract",
"A [System] or [Observer] method must not be marked as abstract.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor MethodMustNotBeAsync = new(
$"gSG{_idCounter++:00}", "System / Observer must not be async",
"A [System] or [Observer] method must not be marked as async.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
// TODO: Check for any other weird modifiers we don't want.
// Diagnostics relating to (generic) parameters on the attributes themselves.
// TODO: Be more specific?
public static readonly DiagnosticDescriptor InvalidTypeArgument = new(
$"gSG{_idCounter++:00}", "Invalid type argument",
"The specified type argument is not valid here.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
// Diagnostics relating to system / observer generic parameters.
public static readonly DiagnosticDescriptor MethodMustNotBeExtension = new(
$"gSG{_idCounter++:00}", "System / Observer must not be extension method",
"A [System] or [Observer] method must not be an extension method.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor MethodMustHaveParameters = new(
$"gSG{_idCounter++:00}", "System / Observer must have parameters",
"A [System] or [Observer] must have parameters.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor MethodMustHaveExpression = new(
$"gSG{_idCounter++:00}", "Iterator-only System / Observer must have expression",
"An Iterator-only [System] or [Observer] must have an [Expression] set.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor MethodGenericParamAtMostOne = new(
$"gSG{_idCounter++:00}", "System / Observer must have at most one generic parameter",
"A [System] or [Observer] must at most one generic parameter.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
// TODO: See if we can change this wording, but try to use the correct one. (Does "open generic" work?)
public static readonly DiagnosticDescriptor MethodGenericParamMustNotBeSubstutited = new(
$"gSG{_idCounter++:00}", "System / Observer generic parameter must not be substituted",
"The generic parameter of a [System] or [Observer] must not be substituted.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor MethodGenericParamMustNotBeConstrained = new(
$"gSG{_idCounter++:00}", "System / Observer generic parameter must not be substituted",
"The generic parameter of a [System] or [Observer] must not be constrained.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
// Diagnostics relating to system / observer parameters.
public static readonly DiagnosticDescriptor ParamMustNotBeArray = new(
$"gSG{_idCounter++:00}", "Parameter must not be array",
"Parameter must not be an array type.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor ParamMustNotBePointer = new(
$"gSG{_idCounter++:00}", "Parameter must not be pointer",
"Parameter must not be a pointer type.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor ParamMustNotBeGenericType = new(
$"gSG{_idCounter++:00}", "Parameter must not be generic type",
"Parameter must not be a generic type parameter.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor ParamMustNotBePrimitive = new(
$"gSG{_idCounter++:00}", "Parameter must not be primitive",
"Parameter must not be a primitive type.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor ParamMustNotBeGeneric = new(
$"gSG{_idCounter++:00}", "Parameter must not be geric",
"Parameter must not be generic (except Iterator, World, Entity, Nullable, Has, Not and Or).",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor ParamDoesNotSupportOptional = new(
$"gSG{_idCounter++:00}", "Parameter does not support optional",
"Parameter does not support optional value syntax.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor ParamByRefMustBeValueType = new(
$"gSG{_idCounter++:00}", "ByRef parameter must be a value type",
"ByRef (in/out/ref) parameter must be a value type (struct).",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor ParamByRefMustNotBeNullable = new(
$"gSG{_idCounter++:00}", "ByRef parameter must not be nullable",
"ByRef (in/out/ref) parameter must not be nullable.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
// TODO: Make this more descriptive. (Special = Has<>, Not<> and [Tag]?)
public static readonly DiagnosticDescriptor SpecialMustNotBeByRef = new(
$"gSG{_idCounter++:00}", "Special parameter must not be ByRef",
"Special parameter must not be ByRef (in/out/ref).",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor SpecialArgMustNotBeNullable = new(
$"gSG{_idCounter++:00}", "Special type argument must not be nullable",
"Special type argument must not be nullable.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor SpecialArgMustNotBeGeneric = new(
$"gSG{_idCounter++:00}", "Special type argument must not be generic",
"Special type argument must not be generic.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor UniqueParamMustNotBeByRef = new(
$"gSG{_idCounter++:00}", "Unique parameter must not be ByRef",
"Unique parameter must not be ByRef (in/out/ref).",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor UniqueParamMustNotBeNullable = new(
$"gSG{_idCounter++:00}", "Unique parameter must not be nullable",
"Unique parameter must not be nullable.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor UniqueParamGenericMustMatch = new(
$"gSG{_idCounter++:00}", "Unique parameter generic type parameter must match method",
"Unique parameter's generic type parameter must match the method's type parameter.",
DiagnosticCategory, DiagnosticSeverity.Error, true);
public static readonly DiagnosticDescriptor UniqueParamNotSupported = new(
$"gSG{_idCounter++:00}", "Unique parameter does not support this attribute",
"Unique parameter does not support {0}",
DiagnosticCategory, DiagnosticSeverity.Error, true);
}

@ -0,0 +1,398 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using gaemstone.SourceGen.Structure;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace gaemstone.SourceGen;
[Generator]
public class ModuleGenerator
: ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
#if DEBUG
// while (!System.Diagnostics.Debugger.IsAttached)
// System.Threading.Thread.Sleep(500);
#endif
context.RegisterForSyntaxNotifications(
() => new RelevantSymbolReceiver());
}
private Dictionary<ISymbol, BaseInfo>? _symbolToInfoLookup;
public BaseInfo? Lookup(ISymbol symbol)
=> (_symbolToInfoLookup ?? throw new InvalidOperationException())
.TryGetValue(symbol, out var info) ? info : null;
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxContextReceiver is not RelevantSymbolReceiver receiver) return;
_symbolToInfoLookup = receiver.Symbols;
var infos = receiver.Symbols.Values;
// Go through all entity infos (types and methods), populate their
// Parent property and add them to their parent's Children property.
foreach (var info in infos.OfType<BaseEntityInfo>()) {
if ((info.Symbol.ContainingType is INamedTypeSymbol parentSymbol)
&& (Lookup(parentSymbol) is TypeEntityInfo parentInfo))
{
info.Parent = parentInfo;
parentInfo.Children.Add(info);
}
}
// Go through all the method infos and lookup / construct
// their parameter infos from the method's parameters.
foreach (var info in infos.OfType<MethodEntityInfo>()) {
foreach (var paramSymbol in info.Symbol.Parameters) {
if (Lookup(paramSymbol) is not ParameterInfo paramInfo)
paramInfo = new ParameterInfo(paramSymbol);
info.Parameters.Add(paramInfo);
paramInfo.Parent = info;
}
}
// Validate all instances of base info without a Parent / Method set.
// Nested infos are validated by their containing info.
foreach (var info in infos.Where(info => info.Parent == null))
info.Validate();
// Report all the diagnostics we found.
foreach (var info in infos)
foreach (var diag in info.Diagnostics)
context.ReportDiagnostic(diag);
foreach (var module in infos.OfType<ModuleEntityInfo>()) {
if (module.IsErrored) continue;
var sb = new StringBuilder();
AppendHeader(sb, module.Namespace);
AppendModuleType(sb, module);
context.AddSource($"{module.FullName}.g.cs", sb.ToString());
}
_symbolToInfoLookup = null;
}
private void AppendHeader(StringBuilder sb, string @namespace)
=> sb.AppendLine($$"""
// <auto-generated/>
using System.Collections.Generic;
using System.Collections.Immutable;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
namespace {{ @namespace }};
""");
private void AppendModuleType(StringBuilder sb, ModuleEntityInfo module)
{
var type = module.Symbol.IsValueType ? "struct" : "class";
sb.AppendLine($$"""
public partial {{ type }} {{ module.Name }}
: IModule
""");
var modulePath = module.GetModulePath().ToStringLiteral();
sb.AppendLine($$"""
{
static string IModule.Path { get; } = {{ modulePath }};
static bool IModule.IsBuiltIn { get; } = {{( module.IsBuiltIn ? "true" : "false" )}};
""");
// TODO: Built-in modules should not have dependencies.
var dependencies = module.GetDependencies().ToList();
sb.Append("\tstatic IReadOnlyList<string> IModule.Dependencies { get; } = ");
if (dependencies.Count > 0) {
sb.AppendLine("ImmutableList.Create(");
foreach (var dependency in dependencies)
sb.AppendLine($"\t\t{dependency.ToStringLiteral()},");
sb.Length -= 2; sb.AppendLine();
sb.AppendLine("\t);");
} else sb.AppendLine("ImmutableList.Create<string>();");
sb.AppendLine();
sb.AppendLine($$"""
static void IModule.Initialize<T>(Entity<T> module)
{
var world = module.World;
""");
// TODO: Might want to add things related to the module entity.
AppendEntityRegistration(sb, module);
AppendMethodRegistration(sb, module);
AppendEntityToAdd(sb, module);
AppendShimMethods(sb, module);
// TODO: Can BuiltIn modules have systems and such?
if (module.HasInitializer)
sb.AppendLine("\t\tInitialize(module);");
sb.AppendLine("\t}");
sb.AppendLine("}");
}
private void AppendEntityRegistration(
StringBuilder sb, ModuleEntityInfo module)
{
var entities = module.Children
.OfType<TypeEntityInfo>()
.Where(e => !e.IsErrored)
.ToList();
if (entities.Count == 0) return;
sb.AppendLine();
sb.AppendLine("\t\t// Register entities.");
foreach (var e in entities) {
var @var = $"_{e.Name}_Entity";
var path = (e.EntityPath ?? e.Name).ToStringLiteral();
// When looking for a BuiltIn entity, error if it doesn't exist.
var lookupFunc = (e.IsBuiltIn ? "LookupPathOrThrow" : "New");
sb.AppendLine($"\t\tvar {@var} = world.{lookupFunc}({path}, module)");
// Since this is a custom gaemstone tag, we want to add it even for [BuiltIn] modules.
if (e.IsRelation) sb.AppendLine("\t\t\t.Add<gaemstone.Doc.Relation>()");
// Other than [Relation], we don't add anything to entities defined in [BuiltIn] modules.
if (module.IsBuiltIn)
{
sb.AppendLine($"\t\t\t.CreateLookup<{e.FullName}>()");
}
else
{
if (e.EntitySymbol != null)
sb.AppendLine($"\t\t\t.Symbol({e.EntitySymbol.ToStringLiteral()})");
// Tags and relations in Flecs are marked as empty components.
if (e.IsTag || e.IsRelation) sb.AppendLine("\t\t\t.Add<gaemstone.Flecs.Core.Component>()");
if (e.IsTag && e.IsRelation) sb.AppendLine("\t\t\t.Add<gaemstone.Flecs.Core.Tag>()");
sb.Append( "\t\t\t");
if (!e.IsBuiltIn) sb.Append(".Build()");
if (e.IsComponent) sb.Append($".InitComponent<{e.FullName}>()");
else sb.Append($".CreateLookup<{e.FullName}>()");
sb.AppendLine();
// I don't think it makes sense to have singletons pre-initialized to zero.
// Especially for singletons that are reference types, which would default to null.
// if (e.IsSingleton) sb.AppendLine($"\t\t\t.Add<{e.FullName}>()");
// TODO: Look into if it would be possible to detect if we have field initializers.
}
sb.Insert(sb.Length - 1, ";");
}
}
private void AppendMethodRegistration(
StringBuilder sb, ModuleEntityInfo module)
{
var methods = module.Children
.OfType<MethodEntityInfo>()
.Where(e => !e.IsErrored)
.ToList();
if (methods.Count == 0) return;
sb.AppendLine();
sb.AppendLine("\t\t// Register systems / observers.");
foreach (var m in methods) {
var @var = $"_{m.Name}_Entity";
var path = (m.EntityPath ?? m.Name).ToStringLiteral();
sb.AppendLine($"\t\tvar {@var} = world.New({path}, module)");
sb.Append("\t\t\t.Build()");
if (m.IsSystem) sb.AppendLine(".InitSystem(");
if (m.IsObserver) sb.AppendLine(".InitObserver(");
if (m.IsIteratorOnly) {
var expression = m.Expression.ToStringLiteral();
sb.AppendLine($"\t\t\t\tnew({expression}), {m.Name},");
} else {
sb.AppendLine("\t\t\t\tnew(");
foreach (var p in m.Parameters)
if (p.HasTerm) {
for (var i = 0; i < p.TermTypes.Count; i++) {
var term = p.TermTypes[i];
var isLastTerm = (i == p.TermTypes.Count - 1);
sb.Append($"\t\t\t\t\tnew Term(");
switch (term) {
case ITypeSymbol type:
AppendTypeEntity(sb, module, type);
break;
case ParameterInfo.Pair pair:
AppendTypeEntity(sb, module, pair.Relation);
sb.Append(',');
AppendTypeEntity(sb, module, pair.Target);
break;
default: throw new InvalidOperationException(
$"Unexpected term type {term.GetType()}");
}
sb.Append(')');
if (p.Source != null) {
sb.Append("{ Source = ");
AppendTypeEntity(sb, module, p.Source);
sb.Append(" }");
}
// The last term in a group of OR terms must not have the TermOperKind.Or set.
if (p.IsOr && !isLastTerm) sb.Append(".Or");
sb.Append(p.Kind switch {
ParameterKind.Has => ".None",
ParameterKind.Not => ".Not",
ParameterKind.Ref => ".InOut",
ParameterKind.Out => ".Out",
_ when !p.IsValueType => ".InOut", // Reference types always imply writability.
_ => ".In",
});
if (p.IsNullable) sb.Append(".Optional");
sb.AppendLine(",");
}
}
if (m.Parameters.Any(p => p.HasTerm))
{ sb.Length -= 2; sb.AppendLine(); }
sb.AppendLine( "\t\t\t\t),");
sb.AppendLine($"\t\t\t\t_{m.Name}_Shim,");
}
if (m.IsObserver)
foreach (var ev in m.ObserverEvents!) {
sb.Append("\t\t\t\t");
AppendTypeEntity(sb, module, ev);
sb.AppendLine(",");
}
sb.Length -= 2;
sb.AppendLine(");");
}
}
private void AppendEntityToAdd(
StringBuilder sb, ModuleEntityInfo module)
{
var entities = module.Children
.Where(e => !e.IsErrored)
.Where(e => e.HasEntitiesToAdd)
.ToList();
if (entities.Count == 0) return;
sb.AppendLine();
sb.AppendLine("\t\t// Add things to entities.");
foreach (var e in entities) {
var @var = $"_{e.Name}_Entity";
foreach (var a in e.EntitiesToAdd)
sb.AppendLine($"\t\t{@var}.Add<{a.GetFullName()}>();");
foreach (var (r, t) in e.RelationsToAdd)
sb.AppendLine($"\t\t{@var}.Add<{r.GetFullName()}, {t.GetFullName()}>();");
foreach (var (c, args) in e.ComponentsToAdd)
sb.AppendLine($"\t\t{@var}.Set(new {c.GetFullName()}({string.Join(", ", args.Select(a => a.ToCSharpString()))}));");
// If system doesn't have an explicit phase set, default to OnUpdate.
if (e is MethodEntityInfo { IsSystem: true, HasPhaseSet: false })
sb.AppendLine($"\t\t{@var}.Add<gaemstone.Flecs.Core.DependsOn, gaemstone.Flecs.Pipeline.OnUpdate>();");
}
}
private void AppendShimMethods(
StringBuilder sb, ModuleEntityInfo module)
{
var methods = module.Children
.OfType<MethodEntityInfo>()
.Where(m => !m.IsErrored)
.Where(m => !m.IsIteratorOnly)
.ToList();
foreach (var method in methods)
AppendShimMethod(sb, module, method);
}
private void AppendShimMethod(
StringBuilder sb, ModuleEntityInfo module,
MethodEntityInfo method)
{
sb.AppendLine();
sb.AppendLine($$"""
void _{{ method.Name }}_Shim(Iterator<T> iter)
{
""");
foreach (var param in method.Parameters)
if (param.HasField)
sb.Append($"\t\t\tvar _{param.Name}_Field = ")
.Append(param.IsNullable ? "iter.FieldOrEmpty" : "iter.Field")
.AppendLine($"<{param.FieldType!.GetFullName()}>({param.TermIndex});");
// When the shim method is called, there's guaranteed to be at least one match.
// iter.Count may be 0 when there's no variable entities that can be matched.
// For example for a query that only matches singleton entities.
sb.AppendLine("\t\t\tfor (var i = 0; i == 0 || i < iter.Count; i++) {");
sb.Append($"\t\t\t\t{method.Name}");
if (method.IsGeneric) sb.Append("<T>");
sb.Append($"(");
foreach (var param in method.Parameters) {
// TODO: Support [Or<...>]
if (param.IsOr && (param.Kind != ParameterKind.Has))
throw new NotSupportedException($"Or<...> parameter not yet supported");
switch (param.Kind) {
case ParameterKind.Unique:
sb.Append(param.UniqueReplacement);
break;
case ParameterKind.In:
case ParameterKind.Out:
case ParameterKind.Ref:
var modifier = param.Kind.ToString().ToLowerInvariant();
sb.Append(modifier).Append(' ');
goto case ParameterKind.Normal;
case ParameterKind.Normal:
case ParameterKind.Nullable:
// FIXME: Handle pairs.
sb.Append($"_{param.Name}_Field")
.Append(param.IsNullable ? ".GetOrNull(i)" : "[i]");
break;
case ParameterKind.Has:
case ParameterKind.Not:
sb.Append("default");
break;
}
sb.Append(", ");
}
if (method.Parameters.Any())
sb.Length -= 2;
sb.AppendLine(");");
sb.AppendLine($$"""
}
}
""");
}
private void AppendTypeEntity(
StringBuilder sb, ModuleEntityInfo module,
ITypeSymbol type)
{
// TODO: Cache entity lookup.
var found = module.Children.Where(c => !c.IsErrored)
.Any(c => SymbolEqualityComparer.Default.Equals(c.Symbol, type));
sb.Append(found ? $"_{type.Name}_Entity"
: $"world.Entity<{type.GetFullName()}>()");
}
}

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using gaemstone.SourceGen.Structure;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace gaemstone.SourceGen;
public class RelevantSymbolReceiver
: ISyntaxContextReceiver
{
private static readonly HashSet<string> RelevantAttributeNames = new(){
// Base entity attributes
"Module", // Can also be [Singleton]
"Entity",
"Relation", // Can also be [Tag] or [Component]
"Tag",
"Component",
"Singleton", // Implies [Component]
"System",
"Observer",
// Entity properties that specify additional info / behavior
"Path", // TODO: When referring to a pre-existing entity, only [Path] should be necessary, right?
"Symbol",
"Add",
"Set",
"BuiltIn", // Valid on [Module]
"Expression", // Valid on [System] and [Observer]
// Term properties (on [System] and [Observer] parameters)
"Source",
"Pair",
"Has",
"Not",
"Or",
};
public Dictionary<ISymbol, BaseInfo> Symbols { get; } = new(SymbolEqualityComparer.Default);
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
var model = context.SemanticModel;
if (context.Node is not AttributeSyntax node) return;
if (node.Parent?.Parent is not SyntaxNode parent) return;
if (model.GetDeclaredSymbol(parent) is not ISymbol symbol) return;
if (model.GetTypeInfo(node).Type is not INamedTypeSymbol type) return;
// Go through the attribute's type hierarchy to see if it matches any
// of the attributes we care about. This is to make sure attributes
// based on [Add<...>] are picked up correctly, including custom ones.
for (var baseType = type; baseType != null; baseType = baseType.BaseType) {
if ((ToRelevantAttributeName(baseType) is string name)
&& RelevantAttributeNames.Contains(name) // Check if we found a relevant attribute.
&& !Symbols.ContainsKey(symbol)) // Check if this is already a known symbol.
{
Symbols.Add(symbol, symbol switch {
INamedTypeSymbol typeSymbol =>
typeSymbol.GetAttributes().Any(attr => attr.AttributeClass!
.GetFullName() == "gaemstone.ECS.ModuleAttribute")
? new ModuleEntityInfo(typeSymbol)
: new TypeEntityInfo(typeSymbol),
IMethodSymbol methodSymbol => new MethodEntityInfo(methodSymbol),
IParameterSymbol paramSymbol => new ParameterInfo(paramSymbol),
_ => throw new InvalidOperationException(
$"Unhandled symbol type {symbol.GetType()}"),
});
break;
}
}
}
public static string? ToRelevantAttributeName(INamedTypeSymbol symbol)
{
if (symbol.GetNamespace() != "gaemstone.ECS") return null;
var name = symbol.MetadataName.Split('`')[0];
return name.EndsWith("Attribute") ? name[..^"Attribute".Length] : null;
}
}

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace gaemstone.SourceGen.Structure;
public abstract class BaseEntityInfo : BaseInfo
{
public new TypeEntityInfo? Parent { get => (TypeEntityInfo?)base.Parent; set => base.Parent = value; }
public string? EntityPath { get; }
public string? EntitySymbol { get; }
public List<INamedTypeSymbol> EntitiesToAdd { get; } = new();
public List<(INamedTypeSymbol Relation, INamedTypeSymbol Target)> RelationsToAdd { get; } = new();
public List<(INamedTypeSymbol Component, ImmutableArray<TypedConstant> Arguments)> ComponentsToAdd { get; } = new();
public virtual bool HasEntitiesToAdd => (EntitiesToAdd.Count > 0)
|| (RelationsToAdd.Count > 0)
|| (ComponentsToAdd.Count > 0);
public BaseEntityInfo(ISymbol symbol)
: base(symbol)
{
// TODO: Validate that these only contain valid characters.
EntityPath = Get("Path")?.ConstructorArguments.FirstOrDefault().Value as string;
EntitySymbol = (Get("Symbol") is AttributeData symbolAttr)
// If [Symbol] is present, use the given custom symbol (if given), ..
? (symbolAttr.ConstructorArguments.FirstOrDefault().Value as string)
?? EntityPath?.Split('/')[^1] // .. otherwise default to the name in [Path], ..
?? Name // .. or just use the default: The symbol's name.
: null;
}
protected override IEnumerable<Diagnostic> ValidateSelf()
{
if (this is ModuleEntityInfo) {
// If this entity is a module, it must not be nested.
if (Symbol.ContainingType != null) yield return Diagnostic.Create(
Descriptors.ModuleMustNotBeNested, Location);
} else {
// Otherwise, it must occur within a module
if (Parent is not ModuleEntityInfo) yield return Diagnostic.Create(
Descriptors.EntityMustBeInModule, Location);
}
foreach (var attr in Symbol.GetAttributes()) {
// Add entities and relationships specified using [Add<...>] attributes.
for (var attrType = attr.AttributeClass; attrType != null; attrType = attrType.BaseType) {
var attrName = RelevantSymbolReceiver.ToRelevantAttributeName(attrType);
if (attrName is "Add" or "Set") {
var allTypeArgumentsValid = true;
for (var i = 0; i < attrType.TypeArguments.Length; i++) {
var arg = attrType.TypeArguments[i];
var param = attrType.TypeParameters[i];
if (arg is not INamedTypeSymbol) {
yield return Diagnostic.Create(
Descriptors.InvalidTypeArgument, param.Locations.Single());
allTypeArgumentsValid = false;
}
// TODO: Make sure entities being added have appropriate attributes as well.
}
if (!allTypeArgumentsValid) continue;
switch (attrName) {
case "Add":
switch (attrType.TypeArguments) {
case [ INamedTypeSymbol entity ]:
EntitiesToAdd.Add(entity);
break;
case [ INamedTypeSymbol relation, INamedTypeSymbol target ]:
RelationsToAdd.Add((relation, target));
break;
default: throw new InvalidOperationException(
"Type argument pattern matching failed");
}
break;
case "Set":
var component = (INamedTypeSymbol)attrType.TypeArguments.Single();
var arguments = attr.ConstructorArguments.Single().Values;
// TODO: Verify arguments actually match a constructor.
// Right now this will just error in the generated code. Probably good enough?
ComponentsToAdd.Add((component, arguments));
break;
default: throw new InvalidOperationException(
"Invalid relevant attribute name");
}
}
}
}
}
}

@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Linq;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
namespace gaemstone.SourceGen.Structure;
public abstract class BaseInfo
{
public ISymbol Symbol { get; }
public BaseEntityInfo? Parent { get; set; }
public string Name => Symbol.Name;
public string FullName => Symbol.GetFullName();
public string Namespace => Symbol.GetNamespace()!;
public Location Location => Symbol.Locations.First();
public List<Diagnostic> Diagnostics { get; } = new();
public bool IsErrored => Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error);
public BaseInfo(ISymbol symbol) => Symbol = symbol;
public void Validate()
{
// All of the children are validated before the parent is,
// in case their state affects the parent in some way.
foreach (var child in GetChildren())
child.Validate();
foreach (var diag in ValidateSelf())
Diagnostics.Add(diag);
}
protected virtual IEnumerable<BaseInfo> GetChildren() => Enumerable.Empty<BaseInfo>();
protected abstract IEnumerable<Diagnostic> ValidateSelf();
protected bool Has(string name)
=> Get(name) != null;
protected AttributeData? Get(string name)
=> Symbol.GetAttributes().FirstOrDefault(attr =>
RelevantSymbolReceiver.ToRelevantAttributeName(attr.AttributeClass!) == name);
}

@ -0,0 +1,98 @@
using System.Collections.Generic;
using System.Linq;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
namespace gaemstone.SourceGen.Structure;
public class MethodEntityInfo : BaseEntityInfo
{
public new IMethodSymbol Symbol => (IMethodSymbol)base.Symbol;
public List<ParameterInfo> Parameters { get; } = new();
protected override IEnumerable<BaseInfo> GetChildren() => Parameters;
public bool IsSystem { get; }
public bool IsObserver { get; }
public bool IsIteratorOnly { get; }
public string? Expression { get; }
public ITypeSymbol[]? ObserverEvents { get; }
public bool IsStatic => Symbol.IsStatic;
public bool IsGeneric => Symbol.IsGenericMethod;
public bool HasPhaseSet { get; private set; }
public override bool HasEntitiesToAdd => base.HasEntitiesToAdd || !HasPhaseSet;
// TODO: Support [Source].
public MethodEntityInfo(ISymbol symbol)
: base(symbol)
{
IsSystem = Has("System");
IsObserver = Has("Observer");
IsIteratorOnly = (Parameters.Count == 1) && (Parameters[0].Symbol.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.Iterator");
Expression = Get("Expression")?.ConstructorArguments.FirstOrDefault().Value as string;
ObserverEvents = Get("Observer")?.AttributeClass!.TypeArguments.ToArray();
}
protected override IEnumerable<Diagnostic> ValidateSelf()
{
foreach (var diag in base.ValidateSelf()) yield return diag;
if (IsSystem && IsObserver) yield return Diagnostic.Create(
Descriptors.InvalidAttributeCombination, Location, "[System, Observer]");
if (Parent is ModuleEntityInfo { IsBuiltIn: true }) yield return Diagnostic.Create(
Descriptors.BuiltInModuleMustNotHaveMethods, Location);
if (Symbol.IsAbstract) yield return Diagnostic.Create(
Descriptors.MethodMustNotBeAbstract, Location);
if (Symbol.IsAsync) yield return Diagnostic.Create(
Descriptors.MethodMustNotBeAsync, Location);
if (Symbol.Parameters.Length == 0) yield return Diagnostic.Create(
Descriptors.MethodMustHaveParameters, Location);
if (Symbol.IsExtensionMethod) yield return Diagnostic.Create(
Descriptors.MethodMustNotBeExtension, Location);
if (IsIteratorOnly && (Expression == null)) yield return Diagnostic.Create(
Descriptors.MethodMustHaveExpression, Location);
if (!IsStatic && Parent is ModuleEntityInfo { IsSingleton: false })
yield return Diagnostic.Create(
Descriptors.InstanceMethodOnlyValidInSingleton, Location);
if (IsGeneric) {
if (Symbol.TypeParameters.Length > 1) yield return Diagnostic.Create(
Descriptors.MethodGenericParamAtMostOne, Location);
else {
var param = Symbol.TypeParameters[0];
if (!SymbolEqualityComparer.Default.Equals(param, Symbol.TypeArguments[0]))
yield return Diagnostic.Create(
Descriptors.MethodGenericParamMustNotBeSubstutited, param.Locations.Single());
if (param.HasReferenceTypeConstraint || param.HasValueTypeConstraint
|| param.HasUnmanagedTypeConstraint || param.HasNotNullConstraint
|| param.HasConstructorConstraint || (param.ConstraintTypes.Length > 0))
yield return Diagnostic.Create(
Descriptors.MethodGenericParamMustNotBeConstrained, param.Locations.Single());
}
}
// Set TermIndex of parameters.
var termIndex = 1;
foreach (var param in Parameters)
if (param.HasTerm)
param.TermIndex = termIndex++;
// See if we have any [DependsOn<...>] attributes for this system.
// If not, ModuleGenerator will add [DependsOn<Flecs.Pipeline.OnUpdate>].
HasPhaseSet = IsSystem && RelationsToAdd.Any(entry => entry.Relation
.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.DependsOnAttribute");
// TODO: Handle systems with [Source].
// TODO: Validate ObserverEvents.
}
}

@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Linq;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace gaemstone.SourceGen.Structure;
public class ModuleEntityInfo : TypeEntityInfo
{
public bool IsPartial { get; }
public bool HasInitializer { get; }
public ModuleEntityInfo(ISymbol symbol)
: base(symbol)
{
var classDecl = (TypeDeclarationSyntax)Symbol.DeclaringSyntaxReferences.First().GetSyntax();
IsPartial = classDecl.Modifiers.Any(t => t.IsKind(SyntaxKind.PartialKeyword));
HasInitializer = Symbol.AllInterfaces.Any(i =>
i.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.IModuleInitializer");
IsBuiltIn = Has("BuiltIn");
}
protected override IEnumerable<Diagnostic> ValidateSelf()
{
foreach (var diag in base.ValidateSelf()) yield return diag;
if (!IsPartial) yield return Diagnostic.Create(
Descriptors.ModuleMustBePartial, Location);
if (IsBuiltIn && (EntityPath == null)) yield return Diagnostic.Create(
Descriptors.BuiltInModuleMustHavePath, Location);
}
public IEnumerable<string> GetDependencies()
{
foreach (var (relation, target) in RelationsToAdd)
if (relation.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.Flecs.Core.DependsOn")
yield return GetModulePath(target);
}
public string GetModulePath()
=> GetModulePath(Symbol);
private static string GetModulePath(ISymbol module)
{
var pathAttr = module.GetAttributes().FirstOrDefault(attr =>
attr.AttributeClass!.GetFullName() == "gaemstone.ECS.PathAttribute");
var path = pathAttr?.ConstructorArguments.FirstOrDefault().Value as string;
var isAbsolute = (path?.FirstOrDefault() == '/');
if (isAbsolute) return path!;
var fullPath = module.GetFullName().Replace('.', '/');
return (path != null)
? $"/{fullPath[..(fullPath.LastIndexOf('/'))]}/{path}"
: $"/{fullPath}";
}
}

@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
namespace gaemstone.SourceGen.Structure;
public class ParameterInfo : BaseInfo
{
public record Pair(ITypeSymbol Relation, ITypeSymbol Target);
private static readonly Dictionary<string, string> UniqueParameters = new() {
["gaemstone.ECS.Iterator`1"] = "iter",
["gaemstone.ECS.World"] = "iter.World",
["gaemstone.ECS.World`1"] = "iter.World",
["gaemstone.ECS.Entity"] = "iter.Entity(i)",
["gaemstone.ECS.Entity`1"] = "iter.Entity(i)",
["System.TimeSpan"] = "iter.DeltaTime",
};
public new IParameterSymbol Symbol => (IParameterSymbol)base.Symbol;
public new MethodEntityInfo? Parent { get => (MethodEntityInfo?)base.Parent; set => base.Parent = value; }
public bool IsGeneric => Symbol.Type is INamedTypeSymbol { IsGenericType: true };
public bool IsValueType => Symbol.Type.IsValueType;
public bool IsNullable => Symbol.Type.IsNullable();
public bool IsByRef => (Symbol.RefKind != RefKind.None);
public ITypeSymbol? Source { get; } // TODO: Support [Source] for each term?
public int? TermIndex { get; set; } // Assigned by MethodEntityInfo
public string? UniqueReplacement { get; }
public IReadOnlyList<object> TermTypes { get; } // Either ITypeSymbol or Pair for relationships.
public ITypeSymbol? FieldType { get; }
public ParameterKind Kind { get; }
public bool IsOr { get; }
public bool HasTerm => (Kind != ParameterKind.Unique);
public bool HasField => HasTerm && !(Kind is ParameterKind.Has or ParameterKind.Not);
public ParameterInfo(ISymbol symbol)
: base(symbol)
{
var typeFullName = Symbol.Type.GetFullName(FullNameStyle.Metadata);
if (UniqueParameters.TryGetValue(typeFullName, out var replacement))
{
Source = null;
UniqueReplacement = replacement;
TermTypes = Array.Empty<ITypeSymbol>();
Kind = ParameterKind.Unique;
IsOr = false;
}
else
{
IsOr = typeFullName.StartsWith("gaemstone.ECS.Or");
var isHas = typeFullName.StartsWith("gaemstone.ECS.Has");
var isNot = typeFullName.StartsWith("gaemstone.ECS.Not");
if (IsGeneric)
{
var type = (INamedTypeSymbol)Symbol.Type; // Checked by IsGeneric.
var args = type.TypeArguments;
// Has<...> usually doesn't support a generic type as its own type parameter.
// However, Has<Or<...>> is an exception to this rule, so we check for this here.
if (isHas && (args is [ INamedTypeSymbol { IsGenericType: true } argType ])
&& argType.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.Or")
{
TermTypes = argType.TypeArguments.ToImmutableList();
FieldType = null;
IsOr = true;
}
else if ((isHas || isNot) && (args is [ ITypeSymbol relation, ITypeSymbol target ]))
{
TermTypes = ImmutableList.Create(new Pair(relation, target));
FieldType = null;
}
else
{
TermTypes = args.ToImmutableList();
FieldType = (IsNullable || isHas || isNot) ? args[0] : type;
}
}
else
{
TermTypes = ImmutableList.Create(Symbol.Type);
FieldType = Symbol.Type;
// If the type of the parameter has the [Tag] attribute,
// the only way to sensibly use it is to check for its (non-)existance.
// (This would also apply to [Relation, Tag] but should be no issue.)
if (Symbol.Type.HasAttribute("gaemstone.ECS.TagAttribute")) isHas = true;
// TODO: Make sure [Tag] is used appropriately.
}
Source = Get("Source")?.AttributeClass!.TypeArguments[0]
// If the type of the parameter has the [Singleton] attribute, use it as the default Source.
?? ((FieldType?.HasAttribute("gaemstone.ECS.SingletonAttribute") == true) ? FieldType : null);
Kind = isHas ? ParameterKind.Has
: isNot ? ParameterKind.Not
: IsNullable ? ParameterKind.Nullable
: Symbol.RefKind switch {
RefKind.In => ParameterKind.In,
RefKind.Out => ParameterKind.Out,
RefKind.Ref => ParameterKind.Ref,
_ => ParameterKind.Normal,
};
}
}
protected override IEnumerable<Diagnostic> ValidateSelf()
{
if (Parent == null) { yield return Diagnostic.Create(
Descriptors.ParamMustBeInMethod, Location); yield break; }
// TODO: Support optionals.
if (Symbol.IsOptional) yield return Diagnostic.Create(
Descriptors.ParamDoesNotSupportOptional, Location);
if (IsByRef && !IsValueType) { yield return Diagnostic.Create(
Descriptors.ParamByRefMustBeValueType, Location); yield break; }
if (IsByRef && IsNullable) { yield return Diagnostic.Create(
Descriptors.ParamByRefMustNotBeNullable, Location); yield break; }
if (UniqueReplacement != null)
{
if (Source != null) yield return Diagnostic.Create(
Descriptors.UniqueParamNotSupported, Location, "[Source]");
if (IsByRef) { yield return Diagnostic.Create(
Descriptors.UniqueParamMustNotBeByRef, Location); yield break; }
if (IsNullable) { yield return Diagnostic.Create(
Descriptors.UniqueParamMustNotBeNullable, Location); yield break; }
if (IsGeneric) {
if (!Parent.IsGeneric) { yield return Diagnostic.Create(
Descriptors.UniqueParamGenericMustMatch, Location); yield break; }
var paramParam = ((INamedTypeSymbol)Symbol.Type).TypeArguments[0];
var methodParam = Parent.Symbol.TypeParameters[0];
if (!SymbolEqualityComparer.Default.Equals(paramParam, methodParam)) {
yield return Diagnostic.Create(Descriptors.UniqueParamGenericMustMatch, Location);
yield break;
}
}
}
else
{
var isSpecial = IsOr || (Kind is ParameterKind.Has or ParameterKind.Not);
if (isSpecial && IsByRef) yield return Diagnostic.Create(
Descriptors.SpecialMustNotBeByRef, Location);
if (isSpecial && IsNullable) yield return Diagnostic.Create(
Descriptors.SpecialArgMustNotBeNullable, Location);
foreach (var termTypeBase in TermTypes) {
var termTypes = termTypeBase switch {
ITypeSymbol type => new[]{ type },
Pair pair => new[]{ pair.Relation, pair.Target },
_ => throw new InvalidOperationException(
$"Unexpected term type {termTypeBase.GetType()}")
};
foreach (var termType in termTypes) {
var location = termType.Locations.Single();
switch (termType.TypeKind) {
case TypeKind.Array: yield return Diagnostic.Create(
Descriptors.ParamMustNotBeArray, location); continue;
case TypeKind.Pointer: yield return Diagnostic.Create(
Descriptors.ParamMustNotBePointer, location); continue;
case TypeKind.TypeParameter: yield return Diagnostic.Create(
Descriptors.ParamMustNotBeGenericType, location); continue;
}
if (termType.IsPrimitiveType()) yield return Diagnostic.Create(
Descriptors.ParamMustNotBePrimitive, location);
if (isSpecial) {
if (termType is INamedTypeSymbol { IsGenericType: true })
yield return Diagnostic.Create(
Descriptors.SpecialArgMustNotBeGeneric, location);
if (termType.IsNullable())
yield return Diagnostic.Create(
Descriptors.SpecialArgMustNotBeNullable, location);
}
}
}
if (IsGeneric && !isSpecial && !(IsValueType && IsNullable)) {
yield return Diagnostic.Create(
Descriptors.ParamMustNotBeGeneric, Location);
yield return Diagnostic.Create(
Descriptors.ParamMustNotBeGeneric, Location);
}
}
}
}
public enum ParameterKind
{
Unique, // for example "Iterator<T>", "Entity<T>", "Entity"
Normal, // Pass by value (copy)
Nullable, // Pass by value, but may be missing (null)
In, // Pass by reference (read-only)
Out, // Pass by reference (write-only)
Ref, // Pass by reference (read/write)
Has, // Required present (no read/write)
Not, // Required missing (no read/write)
}
// public enum TermKind
// {
// In, // Pass by reference (read-only)
// Out, // Pass by reference (write-only)
// Ref, // Pass by reference (read/write)
// Has, // Required present (no read/write)
// Not, // Required missing (no read/write)
// }

@ -0,0 +1,96 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
namespace gaemstone.SourceGen.Structure;
public class TypeEntityInfo : BaseEntityInfo
{
public new INamedTypeSymbol Symbol => (INamedTypeSymbol)base.Symbol;
public List<BaseEntityInfo> Children { get; } = new();
protected override IEnumerable<BaseInfo> GetChildren() => Children;
public bool IsModule { get; }
private bool IsEntity { get; }
public bool IsTag { get; }
public bool IsComponent { get; private set; }
public bool IsSingleton { get; }
public bool IsRelation { get; }
// Built-in entities error if they don't exist.
// When a module is built-in, most attributes become descriptive,
// and don't change or affect the entity they're attached to.
public bool IsBuiltIn { get; protected set; }
public bool IsValueType => Symbol.IsValueType;
public bool IsReferenceType => Symbol.IsReferenceType;
// TODO: Make sure that component has (instance) fields, non-component entity does not have fields.
public TypeEntityInfo(ISymbol symbol)
: base(symbol)
{
IsModule = Has("Module");
IsEntity = Has("Entity");
IsTag = Has("Tag");
IsComponent = Has("Component");
IsSingleton = Has("Singleton");
IsRelation = Has("Relation");
IsBuiltIn = !IsModule && !IsEntity && !IsTag
&& !IsComponent && !IsSingleton && !IsRelation
&& (EntityPath != null);
}
protected override IEnumerable<Diagnostic> ValidateSelf()
{
foreach (var diag in base.ValidateSelf()) yield return diag;
if (Symbol.IsStatic) yield return Diagnostic.Create(
Descriptors.TypeMustNotBeStatic, Location);
if (Symbol.IsAbstract) yield return Diagnostic.Create(
Descriptors.TypeMustNotBeAbstract, Location);
var attributeList = new List<string>();
if (IsModule) attributeList.Add("Module");
if (IsEntity) attributeList.Add("Entity");
if (IsRelation) attributeList.Add("Relation");
if (IsTag) attributeList.Add("Tag");
if (IsComponent) attributeList.Add("Component");
if (IsSingleton) attributeList.Add("Singleton");
switch (attributeList)
{
// No attributes are only valid if [Path] is set.
case [ ]:
if (!IsBuiltIn) yield return Diagnostic.Create(
Descriptors.AttributesRequireTypeOrPath, Location,
$"[{string.Join(", ", attributeList)}]");
break;
// A single attribute is valid.
case [ _ ]:
// The following are valid attribute combinations.
case [ "Module", "Singleton" ]:
case [ "Relation", "Tag" ]:
case [ "Relation", "Component" ]:
break;
default:
yield return Diagnostic.Create(
Descriptors.InvalidAttributeCombination, Location,
$"[{string.Join(", ", attributeList)}]");
if (IsModule) yield return Diagnostic.Create(
Descriptors.ValidModuleAttributesHint, Location);
if (IsRelation) yield return Diagnostic.Create(
Descriptors.ValidRelationAttributesHint, Location);
if (IsSingleton && IsComponent) yield return Diagnostic.Create(
Descriptors.SingletonImpliesComponentHint, Location);
break;
}
// In in a built-in module, this entity is built-in, too.
if (Parent is ModuleEntityInfo { IsBuiltIn: true })
IsBuiltIn = true;
// Singletons are special kinds of components.
if (IsSingleton) IsComponent = true;
}
}

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace gaemstone.SourceGen.Utility;
public static class CollectionExtensions
{
public static void Deconstruct<TKey, TValue>(
this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
{ key = kvp.Key; value = kvp.Value; }
}

@ -0,0 +1,8 @@
// Unfortunately, this is necessary for record types or init
// properties to work under netstandard2.0 target framework.
namespace System.Runtime.CompilerServices
{
public static class IsExternalInit
{
}
}

@ -0,0 +1,118 @@
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace gaemstone.SourceGen.Utility;
public static class SymbolExtensions
{
public static bool IsNullable(this ITypeSymbol type) => type.IsValueType
? (type.OriginalDefinition?.SpecialType == SpecialType.System_Nullable_T)
: (type.NullableAnnotation == NullableAnnotation.Annotated);
public static bool IsPrimitiveType(this ITypeSymbol symbol)
=> symbol.SpecialType switch {
SpecialType.System_Boolean or
SpecialType.System_SByte or
SpecialType.System_Int16 or
SpecialType.System_Int32 or
SpecialType.System_Int64 or
SpecialType.System_Byte or
SpecialType.System_UInt16 or
SpecialType.System_UInt32 or
SpecialType.System_UInt64 or
SpecialType.System_Single or
SpecialType.System_Double or
SpecialType.System_Char or
SpecialType.System_String or
SpecialType.System_Object => true,
_ => false,
};
public static string GetFullName(
this ISymbol symbol, FullNameStyle style = FullNameStyle.Full)
{
var builder = new StringBuilder();
AppendFullName(symbol, builder, style);
return builder.ToString();
}
public static void AppendFullName(
this ISymbol symbol, StringBuilder builder,
FullNameStyle style = FullNameStyle.Full)
{
var withGeneric = (style != FullNameStyle.NoGeneric);
var withMetadata = (style == FullNameStyle.Metadata);
if ((symbol.Kind != SymbolKind.TypeParameter)
&& (symbol.ContainingSymbol is ISymbol parent)
&& (parent is not INamespaceSymbol { IsGlobalNamespace: true }))
{
AppendFullName(parent, builder, style);
builder.Append((withMetadata && (parent is ITypeSymbol)) ? '+' : '.');
}
if ((symbol is INamedTypeSymbol { IsGenericType: true } typeSymbol)
&& !(withGeneric && withMetadata))
{
var length = symbol.MetadataName.IndexOf('`');
builder.Append(symbol.MetadataName, 0, length);
if (withGeneric) {
builder.Append('<');
foreach (var arg in typeSymbol.TypeArguments) {
AppendFullName(arg, builder, style);
builder.Append(',');
}
builder.Length--; // Remove the last ',' character.
builder.Append('>');
}
}
else builder.Append(symbol.MetadataName);
}
public static string? GetNamespace(this ISymbol symbol)
=> symbol.ContainingNamespace?.GetFullName();
public static bool HasAttribute(this ISymbol symbol, string name,
FullNameStyle matchStyle = FullNameStyle.Metadata)
=> symbol.GetAttributes().Any(attr =>
attr.AttributeClass!.GetFullName(matchStyle) == name);
public static AttributeData? FindAttribute(this ISymbol symbol, string name,
FullNameStyle matchStyle = FullNameStyle.Metadata)
=> symbol.GetAttributes().FirstOrDefault(attr =>
attr.AttributeClass!.GetFullName(matchStyle) == name);
public static string ToStringLiteral(this string? input)
=> (input != null) ? SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(input)).ToFullString() : "null";
}
public enum FullNameStyle
{
Full, // Namespace.Foo.Bar<Baz<Quux>>
Metadata, // Namespace.Foo+Bar`1
NoGeneric, // Namespace.Foo.Bar
}
public struct StringifyOptions
{
public static readonly StringifyOptions Default = new();
public static readonly StringifyOptions StripGeneric = new(){ Generic = GenericDisplayMode.None };
public static readonly StringifyOptions MetadataGeneric = new(){ Generic = GenericDisplayMode.Metadata };
public bool Namespace { get; set; } = true;
// TODO: public bool FriendlyNames { get; set; } = true;
public GenericDisplayMode Generic { get; set; } = GenericDisplayMode.Full;
public StringifyOptions() { }
public enum GenericDisplayMode
{
None, // Foo
Metadata, // Foo`1
Full, // Foo<Bar<Baz>>
}
}

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<LangVersion>preview</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IndexRange" Version="1.0.2" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</ItemGroup>
</Project>

@ -4,9 +4,9 @@ using gaemstone.ECS;
namespace gaemstone.Components;
[Module]
public class TransformComponents
public partial class TransformComponents
{
[Symbol, Component]
[Component, Symbol]
public struct GlobalTransform
{
public Matrix4x4 Value;

@ -5,7 +5,7 @@ using static gaemstone.Flecs.Core;
namespace gaemstone;
[Module]
public class Doc
public partial class Doc
{
[Tag]
public struct DisplayType { }

@ -1,121 +0,0 @@
using System;
using static gaemstone.Flecs.Core;
namespace gaemstone.ECS;
/// <summary>
/// When present on an attribute attached to a type that's part of a module
/// being registered automatically through <see cref="ModuleManager.Register"/>,
/// an entity is automatically created and <see cref="LookupExtensions.CreateLookup"/>
/// called on it, meaning it can be looked up using <see cref="World.LookupByType(Type)"/>.
/// </summary>
public interface ICreateEntityAttribute { }
[AttributeUsage(AttributeTargets.Struct)]
public class EntityAttribute : Attribute, ICreateEntityAttribute { }
/// <summary>
/// Use a custom name or path for this entity instead of the type's name.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class PathAttribute : Attribute, ICreateEntityAttribute
{
public string Value { get; }
public PathAttribute(string value) => Value = value;
}
/// <summary>
/// Register the entity under a globally unique symbol.
/// Uses the type's name by default.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class SymbolAttribute : Attribute, ICreateEntityAttribute
{
public string? Value { get; }
public SymbolAttribute() { }
public SymbolAttribute(string value) => Value = value;
}
/// <summary>
/// A singleton is a single instance of a tag or component that can be retrieved
/// without explicitly specifying an entity in a query, where it is equivalent
/// to <see cref="SourceAttribute{}"/> with itself as the generic type parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class SingletonAttribute : Attribute, ICreateEntityAttribute
{ public bool AutoAdd { get; init; } = true; }
/// <summary>
/// Marked entity automatically has the specified entity added to it when
/// automatically registered. Equivalent to <see cref="EntityBase.Add{T}"/>.
/// </summary>
public class AddAttribute<TEntity> : AddEntityAttribute
{ public AddAttribute() : base(typeof(TEntity)) { } }
/// <summary>
/// Marked entity automatically has the specified relationship pair added to it when
/// automatically registered, Equivalent to <see cref="EntityBase.Add{TRelation, TTarget}"/>.
/// </summary>
public class AddAttribute<TRelation, TTarget> : AddRelationAttribute
{ public AddAttribute() : base(typeof(TRelation), typeof(TTarget)) { } }
/// <summary>
/// Marked entity represents a relationship type.
/// It may be used as the "relation" in a pair.
/// </summary>
/// <remarks>
/// The relationship may have component data associated with
/// it when added to an entity under these circumstances:
/// <list type="bullet">
/// <item>If marked as a <see cref="TagAttribute"/>, does not carry data.</item>
/// <item>If marked as a <see cref="ComponentAttribute"/>, carries the relation's data.</item>
/// <item>If marked with neither, will carry the target's data, if it's a component.</item>
/// </list>
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class RelationAttribute : Attribute, ICreateEntityAttribute { }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class ComponentAttribute : Attribute, ICreateEntityAttribute { }
/// <seealso cref="Tag"/>
[AttributeUsage(AttributeTargets.Struct)]
public class TagAttribute : AddAttribute<Tag>, ICreateEntityAttribute { }
/// <seealso cref="IsA"/>
public class IsAAttribute<TTarget> : AddAttribute<IsA, TTarget> { }
/// <seealso cref="ChildOf"/>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class ChildOfAttribute<TTarget> : AddAttribute<ChildOf, TTarget> { }
/// <seealso cref="DependsOn"/>
public class DependsOnAttribute<TTarget> : AddAttribute<DependsOn, TTarget> { }
/// <seealso cref="Exclusive"/>
public class ExclusiveAttribute : AddAttribute<Exclusive> { }
/// <seealso cref="With"/>
public class WithAttribute<TTarget> : AddAttribute<With, TTarget> { }
// Base attributes for other attributes.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)]
public class AddEntityAttribute : Attribute
{
public Type Entity { get; }
internal AddEntityAttribute(Type entity) => Entity = entity;
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)]
public class AddRelationAttribute : Attribute
{
public Type Relation { get; }
public Type Target { get; }
internal AddRelationAttribute(Type relation, Type target)
{ Relation = relation; Target = target; }
}

@ -1,16 +0,0 @@
using System;
using System.Linq;
using gaemstone.Utility.IL;
namespace gaemstone.ECS;
public static class FilterExtensions
{
public static void RunOnce(World world, Delegate action)
{
var gen = IterActionGenerator.GetOrBuild(world, action.Method);
var desc = new FilterDesc(gen.Terms.ToArray());
using var filter = new Filter(world, desc);
foreach (var iter in filter.Iter()) gen.RunWithTryCatch(action.Target, iter);
}
}

@ -1,14 +0,0 @@
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>
[Singleton]
public struct Game { }
/// <summary> Equivalent to <see cref="SourceAttribute{Game}"/>. </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public class GameAttribute : SourceAttribute<Game> { }

@ -0,0 +1,69 @@
using System;
using static gaemstone.Flecs.Core;
namespace gaemstone.ECS;
/// <summary>
/// Entities marked with this attribute are automatically registered with a
/// <see cref="EntityRef.Symbol"/> specified in the attribute constructor,
/// defaulting to their <see cref="EntityRef.Name"/> if not given.
///
/// Symbols are unique string identifiers used to look up their entities.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct
| AttributeTargets.Enum | AttributeTargets.Method)]
public class SymbolAttribute : Attribute
{
public string? Value { get; }
public SymbolAttribute() { }
public SymbolAttribute(string value) => Value = value;
}
// TODO: Should this be renamed from [Add<...>] to something else?
/// <summary>
/// Marked entity automatically has the specified entity added to it when
/// automatically registered. Equivalent to <see cref="EntityBuilder.Add{T}"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct
| AttributeTargets.Enum | AttributeTargets.Method,
AllowMultiple = true)]
public class AddAttribute<TEntity> : Attribute { }
/// <summary>
/// Marked entity automatically has the specified relationship pair added to it when
/// automatically registered. Equivalent to <see cref="EntityBuilder.Add{TRelation, TTarget}"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct
| AttributeTargets.Enum | AttributeTargets.Method,
AllowMultiple = true)]
public class AddAttribute<TRelation, TTarget> : Attribute { }
/// <summary>
/// Marked entity automatically has the specified component added to it when
/// automatically registered. Equivalent to <see cref="EntityBuilder.Set{TComponent}"/>.
/// Arguments are passed to a valid constructor with matching arguments. Only
/// attribute-safe primitives are allowed for the specified arguments.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct
| AttributeTargets.Enum | AttributeTargets.Method,
AllowMultiple = true)]
public class SetAttribute<TComponent> : Attribute
{
public object[] Arguments { get; }
public SetAttribute(params object[] args) => Arguments = args;
}
/// <seealso cref="IsA"/>
public class IsAAttribute<TTarget> : AddAttribute<IsA, TTarget> { }
/// <seealso cref="ChildOf"/>
public class ChildOfAttribute<TTarget> : AddAttribute<ChildOf, TTarget> { }
/// <seealso cref="DependsOn"/>
public class DependsOnAttribute<TTarget> : AddAttribute<DependsOn, TTarget> { }
/// <seealso cref="Exclusive"/>
public class ExclusiveAttribute : AddAttribute<Exclusive> { }
/// <seealso cref="With"/>
public class WithAttribute<TTarget> : AddAttribute<With, TTarget> { }

@ -0,0 +1,46 @@
using System;
namespace gaemstone.ECS;
/// <summary> Use a custom name or path for this entity instead of the type's name. </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct
| AttributeTargets.Enum | AttributeTargets.Method)]
public class PathAttribute : Attribute
{
public string Value { get; }
public PathAttribute(string value) => Value = value;
}
[AttributeUsage(AttributeTargets.Struct)]
public class EntityAttribute : Attribute { }
// On types marked as [Relation], this has an effect on the relation's behavior.
// Otherwise this has the same behavior as [Entity], but informs people
[AttributeUsage(AttributeTargets.Struct)]
public class TagAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class ComponentAttribute : Attribute { }
/// <summary>
/// A singleton is a single instance of a [Component] that can be retrieved
/// without explicitly specifying an entity in a query, where it is equivalent
/// to <see cref="SourceAttribute{}"/> with itself as the generic type parameter.
/// </summary>
public class SingletonAttribute : ComponentAttribute { }
/// <summary>
/// Marked entity represents a relationship type.
/// It may be used as the "relation" in a pair.
/// </summary>
/// <remarks>
/// The relationship may have component data associated with
/// it when added to an entity under these circumstances:
/// <list type="bullet">
/// <item>If marked as a <see cref="TagAttribute"/>, does not carry data.</item>
/// <item>If marked as a <see cref="ComponentAttribute"/>, carries the relation's data.</item>
/// <item>If marked with neither, will carry the target's data, if it's a component.</item>
/// </list>
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class RelationAttribute : Attribute { }

@ -1,11 +1,31 @@
using System;
using System.Collections.Generic;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class ModuleAttribute : SingletonAttribute { }
[AttributeUsage(AttributeTargets.Class)]
public class ModuleAttribute : Attribute { }
public class BuiltInAttribute : Attribute { }
/// <summary>
/// A concrete implementation of this interface is generated by a source
/// generator for each type marked as <see cref="ModuleAttribute"/>.
/// </summary>
public interface IModule
{
static abstract string Path { get; }
static abstract bool IsBuiltIn { get; }
static abstract IReadOnlyList<string> Dependencies { get; }
static abstract void Initialize<TContext>(Entity<TContext> module);
}
public interface IModuleInitializer
{
void Initialize(EntityRef module);
static abstract void Initialize<TContext>(Entity<TContext> module);
}

@ -1,45 +0,0 @@
using System;
using System.Linq;
using System.Reflection;
using gaemstone.Utility;
using gaemstone.Utility.IL;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Method)]
public class ObserverAttribute : Attribute
{
public Type Event { get; }
internal ObserverAttribute(Type @event) => Event = @event; // Use generic type instead.
}
public class ObserverAttribute<TEvent> : ObserverAttribute
{ public ObserverAttribute() : base(typeof(TEvent)) { } }
public static class ObserverExtensions
{
public static EntityRef InitObserver(this World world,
object? instance, MethodInfo method)
{
var attr = method.Get<ObserverAttribute>() ?? throw new ArgumentException(
"Observer must specify ObserverAttribute", nameof(method));
var expr = method.Get<ExpressionAttribute>()?.Value;
FilterDesc filter;
Action<Iterator> iterAction;
var param = method.GetParameters();
if ((param.Length == 1) && (param[0].ParameterType == typeof(Iterator))) {
filter = new(expr ?? throw new Exception(
"Observer must specify ExpressionAttribute"));
if (method.IsStatic) instance = null;
iterAction = (Action<Iterator>)Delegate.CreateDelegate(
typeof(Action<Iterator>), instance, method);
} else {
var gen = IterActionGenerator.GetOrBuild(world, method);
filter = (expr != null) ? new(expr) : new(gen.Terms.ToArray());
iterAction = iter => gen.RunWithTryCatch(instance, iter);
}
var @event = world.LookupByTypeOrThrow(attr.Event);
return world.New(method.Name).Build().InitObserver(@event, filter, iterAction);
}
}

@ -0,0 +1,88 @@
using System;
using gaemstone.Utility;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)]
public class SourceAttribute<T> : Attribute { }
// TODO: Implement [Pair] somehow.
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.GenericParameter)]
public class PairAttribute<TRelation, TTarget> : Attribute { }
public static class Pair<TRelation, TTarget>
{
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.GenericParameter)]
public class RelationAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.GenericParameter)]
public class TargetAttribute : Attribute { }
}
public readonly ref struct Has<T> { }
public readonly ref struct Has<TRelation, TTarget> { }
public readonly ref struct Not<T> { }
public readonly ref struct Not<TRelation, TTarget> { }
public readonly struct Or<T1, T2>
{
private readonly Union<T1, T2> _union;
public int Case { get; }
public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; }
public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; }
public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException();
public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException();
}
public readonly struct Or<T1, T2, T3>
{
private readonly Union<T1, T2, T3> _union;
public int Case { get; }
public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; }
public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; }
public Or(T3 value) { Case = 3; _union = new() { Value3 = value }; }
public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException();
public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException();
public T3 Value3 => (Case == 3) ? _union.Value3 : throw new InvalidOperationException();
}
public readonly struct Or<T1, T2, T3, T4>
{
private readonly Union<T1, T2, T3, T4> _union;
public int Case { get; }
public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; }
public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; }
public Or(T3 value) { Case = 3; _union = new() { Value3 = value }; }
public Or(T4 value) { Case = 4; _union = new() { Value4 = value }; }
public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException();
public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException();
public T3 Value3 => (Case == 3) ? _union.Value3 : throw new InvalidOperationException();
public T4 Value4 => (Case == 4) ? _union.Value4 : throw new InvalidOperationException();
}
public readonly struct Or<T1, T2, T3, T4, T5>
{
private readonly Union<T1, T2, T3, T4, T5> _union;
public int Case { get; }
public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; }
public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; }
public Or(T3 value) { Case = 3; _union = new() { Value3 = value }; }
public Or(T4 value) { Case = 4; _union = new() { Value4 = value }; }
public Or(T5 value) { Case = 5; _union = new() { Value5 = value }; }
public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException();
public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException();
public T3 Value3 => (Case == 3) ? _union.Value3 : throw new InvalidOperationException();
public T4 Value4 => (Case == 4) ? _union.Value4 : throw new InvalidOperationException();
public T5 Value5 => (Case == 5) ? _union.Value5 : throw new InvalidOperationException();
}

@ -1,21 +1,20 @@
using System;
using System.Linq;
using System.Reflection;
using gaemstone.Flecs;
using gaemstone.Utility;
using gaemstone.Utility.IL;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Method)]
public class SystemAttribute : Attribute
{
public Type Phase { get; }
public SystemAttribute() : this(typeof(SystemPhase.OnUpdate)) { }
internal SystemAttribute(Type phase) => Phase = phase; // Use generic type instead.
}
public class SystemAttribute<TPhase> : SystemAttribute
{ public SystemAttribute() : base(typeof(TPhase)) { } }
public class SystemAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Method)]
public class ObserverAttribute<T> : Attribute { }
[AttributeUsage(AttributeTargets.Method)]
public class ObserverAttribute<T1, T2> : Attribute { }
[AttributeUsage(AttributeTargets.Method)]
public class ObserverAttribute<T1, T2, T3> : Attribute { }
[AttributeUsage(AttributeTargets.Method)]
public class ObserverAttribute<T1, T2, T3, T4> : Attribute { }
[AttributeUsage(AttributeTargets.Method)]
public class ObserverAttribute<T1, T2, T3, T4, T5> : Attribute { }
[AttributeUsage(AttributeTargets.Method)]
public class ExpressionAttribute : Attribute
@ -23,48 +22,3 @@ public class ExpressionAttribute : Attribute
public string Value { get; }
public ExpressionAttribute(string value) => Value = value;
}
public static class SystemExtensions
{
public static EntityRef InitSystem(this World world, Delegate action)
{
var attr = action.Method.Get<SystemAttribute>();
var expr = action.Method.Get<ExpressionAttribute>()?.Value;
QueryDesc query;
if (action is Action<Iterator> callback) {
query = new(expr ?? throw new ArgumentException(
"System must specify ExpressionAttribute", nameof(action)));
} else {
var gen = IterActionGenerator.GetOrBuild(world, action.Method);
query = (expr != null) ? new(expr) : new(gen.Terms.ToArray());
callback = iter => gen.RunWithTryCatch(action.Target, iter);
}
var phase = world.LookupByTypeOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate));
return world.New(action.Method.Name).Build().InitSystem(phase, query, callback);
}
public static EntityRef InitSystem(this World world,
object? instance, MethodInfo method)
{
var attr = method.Get<SystemAttribute>();
var expr = method.Get<ExpressionAttribute>()?.Value;
QueryDesc query;
Action<Iterator> callback;
var param = method.GetParameters();
if ((param.Length == 1) && (param[0].ParameterType == typeof(Iterator))) {
query = new(expr ?? throw new ArgumentException(
"System must specify ExpressionAttribute", nameof(method)));
callback = (Action<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), instance, method);
} else {
var gen = IterActionGenerator.GetOrBuild(world, method);
query = (expr != null) ? new(expr) : new(gen.Terms.ToArray());
callback = iter => gen.RunWithTryCatch(instance, iter);
}
var phase = world.LookupByTypeOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate));
return world.New(method.Name).Build().InitSystem(phase, query, callback);
}
}

@ -1,32 +0,0 @@
using System;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)]
public class SourceAttribute : Attribute
{
public Type Type { get; }
// TODO: Support path as source too.
internal SourceAttribute(Type type) => Type = type;
}
public class SourceAttribute<TEntity> : SourceAttribute
{ public SourceAttribute() : base(typeof(TEntity)) { } }
// Parameters types marked with [Tag] are equivalent to [Has].
[AttributeUsage(AttributeTargets.Parameter)]
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 OrAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Parameter)]
public class NotAttribute : Attribute { }

@ -2,44 +2,27 @@ using gaemstone.ECS;
namespace gaemstone.Flecs;
[Module, Path("/flecs/core")]
public static class Core
[BuiltIn, Module, Path("/flecs/core")]
public partial class Core
{
// Entity Tags
[Tag] public struct Name { }
[Tag] public struct Symbol { }
[Tag] public struct Alias { }
[Tag] public struct Module { }
[Tag] public struct Private { }
[Tag] public struct Prefab { }
[Tag] public struct SlotOf { }
[Tag] public struct Disabled { }
[Tag] public struct Empty { }
// Can't be in a module class with the same name.
// [Path("/flecs/system/System")]
// [Tag] public struct System { }
// Entities
// Conflicts with World class, and not required?
// [Entity] public struct World { }
[Path("*")] public struct Wildcard { }
[Path("_")] public struct Any { }
[Entity] public struct This { }
[Path("$")] public struct Variable { }
[Entity] public struct Flag { }
// Entity Relationships
[Tag] public struct SlotOf { }
[Tag] public struct Flag { }
[Relation, Tag] public struct IsA { }
[Relation, Tag] public struct ChildOf { }
[Relation, Tag] public struct DependsOn { }
// Component / Relationship Properties
[Entity, Path("*")] public struct Wildcard { }
[Entity, Path("_")] public struct Any { }
[Entity, Path("this")] public struct This { }
[Entity, Path("$")] public struct Variable { }
[Tag] public struct Transitive { }
[Tag] public struct Reflexive { }
[Tag] public struct Symmetric { }
@ -49,9 +32,48 @@ public static class Core
[Tag] public struct Union { }
[Tag] public struct Exclusive { }
[Tag] public struct Acyclic { }
[Tag] public struct Traversable { }
[Relation, Tag] public struct With { }
[Tag] public struct OneOf { }
// Entity Relationships
[Relation, Tag] public struct ChildOf { }
[Relation, Tag] public struct IsA { }
[Relation, Tag] public struct DependsOn { }
// Identifier Tags
[Tag] public struct Name { }
[Tag] public struct Symbol { }
[Tag] public struct Alias { }
// Observer Events
[Entity] public struct OnAdd { }
[Entity] public struct OnRemove { }
[Entity] public struct OnSet { }
[Entity] public struct UnSet { }
[Entity] public struct OnTableCreate { }
[Entity] public struct OnTableDelete { }
[Entity] public struct OnTableEmpty { }
[Entity] public struct OnTableFilled { }
// Related to Deletion Events
[Relation, Tag] public struct OnDelete { }
[Relation, Tag] public struct OnDeleteTarget { }
[Tag] public struct Remove { }
[Tag] public struct Delete { }
[Tag] public struct Panic { }
// Components
[Component]
@ -75,6 +97,7 @@ public static class Core
#pragma warning restore
public override string? ToString() { unsafe {
// TODO: What does Flecs do if you give it "" as an identifier?
if ((_value == null) || (_length == 0)) return null;
else return new UTF8View(new(_value, (int)_length)).ToString();
} }

@ -1,18 +0,0 @@
using gaemstone.ECS;
namespace gaemstone.Flecs;
[Module, Path("/flecs/core")]
public static class DeletionEvent
{
[Relation, Tag] public struct OnDelete { }
[Relation, Tag] public struct OnDeleteTarget { }
}
[Module, Path("/flecs/core")]
public static class DeletionBehavior
{
[Tag] public struct Remove { }
[Tag] public struct Delete { }
[Tag] public struct Panic { }
}

@ -1,13 +1,12 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.Utility;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[Module, Path("/flecs/doc")]
public static class Doc
[BuiltIn, Module, Path("/flecs/doc")]
public partial class Doc
{
[Tag] public struct Brief { }
[Tag] public struct Detail { }
@ -28,52 +27,44 @@ public static class Doc
public static unsafe class DocExtensions
{
private static EntityRef Set<T>(EntityRef entity, string? value)
private static Entity<TContext> Set<TContext, T>(Entity<TContext> entity, string? value)
{
var world = entity.World;
var id = IdRef.Pair<Doc.Description, T>(world);
var id = entity.World.Pair<Doc.Description, T>();
var hadId = entity.Has(id);
var alloc = GlobalHeapAllocator.Instance;
if (value != null) {
var ptr = ecs_get_mut_id(world, entity, id);
ref var desc = ref Unsafe.AsRef<Doc.Description>(ptr);
// FIXME: Why does freeing these cause crashes?
// if (has) alloc.Free((nint)desc.Value); // Free previous value.
desc.Value = (void*)(nint)alloc.AllocateCString(value);
} else if (hadId) {
// var @ref = ecs_ref_init_id(world, entity, id);
// var ptr = ecs_ref_get_id(world, &@ref, id);
// var desc = Unsafe.AsRef<Doc.Description>(ptr);
// alloc.Free((nint)desc.Value); // Free previous value.
var str = GlobalHeapAllocator.Instance.AllocateCString(value);
var desc = new Doc.Description { Value = (void*)(nint)str };
entity.Set(id, desc);
} else {
entity.Remove(id);
}
return entity;
}
public static string? GetDocName(this EntityRef entity, bool fallbackToEntityName = true)
public static string? GetDocName<TContext>(this Entity<TContext> entity, bool fallbackToEntityName = true)
=> fallbackToEntityName || entity.Has<Doc.Description, Core.Name>()
? ecs_doc_get_name(entity.World, entity).FlecsToString() : null;
public static EntityRef SetDocName(this EntityRef entity, string? value)
=> Set<Core.Name>(entity, value);
public static Entity<TContext> SetDocName<TContext>(this Entity<TContext> entity, string? value)
=> Set<TContext, Core.Name>(entity, value);
public static string? GetDocBrief(this EntityRef entity)
public static string? GetDocBrief<TContext>(this Entity<TContext> entity)
=> ecs_doc_get_brief(entity.World, entity).FlecsToString()!;
public static EntityRef SetDocBrief(this EntityRef entity, string? value)
=> Set<Doc.Brief>(entity, value);
public static Entity<TContext> SetDocBrief<TContext>(this Entity<TContext> entity, string? value)
=> Set<TContext, Doc.Brief>(entity, value);
public static string? GetDocDetail(this EntityRef entity)
public static string? GetDocDetail<TContext>(this Entity<TContext> entity)
=> ecs_doc_get_detail(entity.World, entity).FlecsToString()!;
public static EntityRef SetDocDetail(this EntityRef entity, string? value)
=> Set<Doc.Detail>(entity, value);
public static Entity<TContext> SetDocDetail<TContext>(this Entity<TContext> entity, string? value)
=> Set<TContext, Doc.Detail>(entity, value);
public static string? GetDocLink(this EntityRef entity)
public static string? GetDocLink<TContext>(this Entity<TContext> entity)
=> ecs_doc_get_link(entity.World, entity).FlecsToString()!;
public static EntityRef SetDocLink(this EntityRef entity, string? value)
=> Set<Doc.Link>(entity, value);
public static Entity<TContext> SetDocLink<TContext>(this Entity<TContext> entity, string? value)
=> Set<TContext, Doc.Link>(entity, value);
public static string? GetDocColor(this EntityRef entity)
public static string? GetDocColor<TContext>(this Entity<TContext> entity)
=> ecs_doc_get_color(entity.World, entity).FlecsToString()!;
public static EntityRef SetDocColor(this EntityRef entity, string? value)
=> Set<Doc.Color>(entity, value);
public static Entity<TContext> SetDocColor<TContext>(this Entity<TContext> entity, string? value)
=> Set<TContext, Doc.Color>(entity, value);
}

@ -1,14 +0,0 @@
using gaemstone.ECS;
namespace gaemstone.Flecs;
[Module, Path("/flecs/core")]
public static class ObserverEvent
{
[Entity] public struct OnAdd { }
[Entity] public struct OnRemove { }
[Entity] public struct OnSet { }
[Entity] public struct UnSet { }
[Entity] public struct OnTableEmpty { }
[Entity] public struct OnTableFilled { }
}

@ -2,8 +2,92 @@ using gaemstone.ECS;
namespace gaemstone.Flecs;
[Module, Path("/flecs/pipeline")]
public static class Pipeline
[BuiltIn, Module, Path("/flecs/pipeline")]
public partial class Pipeline
{
[Entity] public struct Phase { }
[Entity, Add<Phase>]
public struct PreFrame { }
/// <summary>
/// This phase contains all the systems that load data into your ECS.
/// This would be a good place to load keyboard and mouse inputs.
/// </summary>
[Entity, Add<Phase>]
[DependsOn<PreFrame>]
public struct OnLoad { }
/// <summary>
/// Often the imported data needs to be processed. Maybe you want to associate
/// your keypresses with high level actions rather than comparing explicitly
/// in your game code if the user pressed the 'K' key.
/// </summary>
[Entity, Add<Phase>]
[DependsOn<OnLoad>]
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>
[Entity, Add<Phase>]
[DependsOn<PostLoad>]
public struct PreUpdate { }
/// <summary>
/// This is usually where the magic happens! This is where you put all of
/// your gameplay systems. By default systems are added to this phase.
/// </summary>
[Entity, Add<Phase>]
[DependsOn<PreUpdate>]
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>
[Entity, Add<Phase>]
[DependsOn<OnUpdate>]
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>
[Entity, Add<Phase>]
[DependsOn<OnValidate>]
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>
[Entity, Add<Phase>]
[DependsOn<PostUpdate>]
public struct PreStore { }
/// <summary>
/// This is where it all comes together. Your frame is ready to be
/// rendered, and that is exactly what you would do in this phase.
/// </summary>
[Entity, Add<Phase>]
[DependsOn<PreStore>]
public struct OnStore { }
[Entity, Add<Phase>]
[DependsOn<OnStore>]
public struct PostFrame { }
}

@ -1,91 +0,0 @@
using gaemstone.ECS;
using static gaemstone.Flecs.Pipeline;
namespace gaemstone.Flecs;
[Module, Path("/flecs/pipeline")]
public static class SystemPhase
{
[Entity, Add<Phase>]
public struct PreFrame { }
/// <summary>
/// This phase contains all the systems that load data into your ECS.
/// This would be a good place to load keyboard and mouse inputs.
/// </summary>
[Entity, Add<Phase>]
[DependsOn<PreFrame>]
public struct OnLoad { }
/// <summary>
/// Often the imported data needs to be processed. Maybe you want to associate
/// your keypresses with high level actions rather than comparing explicitly
/// in your game code if the user pressed the 'K' key.
/// </summary>
[Entity, Add<Phase>]
[DependsOn<OnLoad>]
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>
[Entity, Add<Phase>]
[DependsOn<PostLoad>]
public struct PreUpdate { }
/// <summary>
/// This is usually where the magic happens! This is where you put all of
/// your gameplay systems. By default systems are added to this phase.
/// </summary>
[Entity, Add<Phase>]
[DependsOn<PreUpdate>]
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>
[Entity, Add<Phase>]
[DependsOn<OnUpdate>]
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>
[Entity, Add<Phase>]
[DependsOn<OnValidate>]
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>
[Entity, Add<Phase>]
[DependsOn<PostUpdate>]
public struct PreStore { }
/// <summary>
/// This is where it all comes together. Your frame is ready to be
/// rendered, and that is exactly what you would do in this phase.
/// </summary>
[Entity, Add<Phase>]
[DependsOn<PreStore>]
public struct OnStore { }
[Entity, Add<Phase>]
[DependsOn<OnStore>]
public struct PostFrame { }
}

@ -1,19 +1,20 @@
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.Utility;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs.Systems;
[Module, Path("/flecs/monitor")]
public unsafe class Monitor
[BuiltIn, Module, Path("/flecs/monitor")]
public unsafe partial class Monitor
: IModuleInitializer
{
public void Initialize(EntityRef module)
public static void Initialize<T>(Entity<T> module)
{
using var alloc = TempAllocator.Use();
ecs_import_c(module.World, new() { Data = new() {
Pointer = &MonitorImport } }, alloc.AllocateCString("FlecsMonitor"));
ecs_import_c(module.World,
new() { Data = new() { Pointer = &MonitorImport } },
alloc.AllocateCString("FlecsMonitor"));
}
[UnmanagedCallersOnly]

@ -1,19 +1,20 @@
using System.Runtime.InteropServices;
using gaemstone.ECS;
using gaemstone.Utility;
using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.Flecs.Systems;
[Module, Path("/flecs/rest")]
public unsafe class Rest
[BuiltIn, Module, Path("/flecs/rest")]
public unsafe partial class Rest
: IModuleInitializer
{
public void Initialize(EntityRef module)
public static void Initialize<T>(Entity<T> module)
{
using (var alloc = TempAllocator.Use())
ecs_import_c(module.World, new() { Data = new() {
Pointer = &RestImport } }, alloc.AllocateCString("FlecsRest"));
ecs_import_c(module.World,
new() { Data = new() { Pointer = &RestImport } },
alloc.AllocateCString("FlecsRest"));
module.NewChild("Rest").Build()
.CreateLookup<EcsRest>()

@ -1,224 +1,135 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using gaemstone.ECS;
using gaemstone.Utility;
using static gaemstone.Flecs.Core;
using BindingFlags = System.Reflection.BindingFlags;
using Module = gaemstone.Flecs.Core.Module;
namespace gaemstone;
public class ModuleManager
public class ModuleManager<TContext>
: IEnumerable<ModuleManager<TContext>.IModuleInfo>
{
private readonly Dictionary<Entity, ModuleInfo> _modules = new();
private readonly Dictionary<Entity<TContext>, IModuleInfo> _modules = new();
public Universe Universe { get; }
public ModuleManager(Universe universe)
public Universe<TContext> Universe { get; }
public ModuleManager(Universe<TContext> universe)
=> Universe = universe;
internal ModuleInfo? Lookup(Entity entity)
internal IModuleInfo? Lookup(Entity<TContext> entity)
=> _modules.GetValueOrDefault(entity);
public void RegisterAllFrom(System.Reflection.Assembly assembly)
=> RegisterAll(assembly.GetTypes());
public void RegisterAll(IEnumerable<Type> types)
public Entity<TContext> Register<T>()
where T : IModule
{
foreach (var type in types)
if (type.Has<ModuleAttribute>())
Register(type);
}
public EntityRef Register<T>()
where T : class => Register(typeof(T));
public EntityRef Register(Type moduleType)
{
if (!moduleType.IsClass || moduleType.IsGenericType || moduleType.IsGenericTypeDefinition) throw new Exception(
$"Module {moduleType} must be a non-generic class");
if (moduleType.Get<ModuleAttribute>() is not ModuleAttribute moduleAttr) throw new Exception(
$"Module {moduleType} must be marked with ModuleAttribute");
// Check if module type is static.
if (moduleType.IsAbstract && moduleType.IsSealed) {
// Static modules represent existing modules, as such they don't
// create entities, only look up existing ones to add type lookups
// for use with the Lookup(Type) method.
if (moduleType.Get<PathAttribute>()?.Value is not string modulePathStr) throw new Exception(
$"Existing module {moduleType} must have {nameof(PathAttribute)} set");
var modulePath = EntityPath.Parse(modulePathStr);
var moduleEntity = Universe.LookupByPath(modulePath) ?? throw new Exception(
$"Existing module {moduleType} with name '{modulePath}' not found");
// This implementation is pretty naive. It simply gets all nested
// types which are tagged with an ICreateEntityAttribute base
// attribute and creates a lookup mapping. No sanity checking.
foreach (var type in moduleType.GetNestedTypes()) {
if (!type.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().Any()) continue;
var attr = type.Get<PathAttribute>();
var path = EntityPath.Parse(attr?.Value ?? type.Name);
var entity = Universe.LookupByPathOrThrow(moduleEntity, path);
entity.CreateLookup(type);
if (type.Has<RelationAttribute>()) entity.Add<Doc.Relation>();
}
// if (!typeof(T).IsAssignableTo(typeof(IModule))) throw new ArgumentException(
// $"The specified type {typeof(T)} does not implement IModule", nameof(T));
return moduleEntity;
} else {
var path = GetModulePath(moduleType);
var module = new ModuleInfo(Universe, moduleType, path);
_modules.Add(module.Entity, module);
TryEnableModule(module);
return module.Entity;
}
var module = new ModuleInfo<T>(Universe);
_modules.Add(module.Entity, module);
TryEnableModule(module);
return module.Entity;
}
private void TryEnableModule(ModuleInfo module)
private void TryEnableModule(IModuleInfo module)
{
if (module.UnmetDependencies.Count > 0) return;
if (!module.Dependencies.All(dep => dep.IsDependencyMet)) return;
Console.WriteLine($"Enabling module {module.Entity.Path}");
Console.WriteLine($"Enabling module {module.Path} ...");
module.Enable();
// Find other modules that might be missing this module as a dependency.
foreach (var other in _modules.Values) {
if (other.IsActive) continue;
if (!other.UnmetDependencies.Contains(module.Entity)) continue;
if (other.IsInitialized) continue;
var dependency = other.Dependencies.FirstOrDefault(dep => dep.Entity == module.Entity);
if (dependency == null) continue;
// Move the just enabled module from unmet to met depedencies.
other.UnmetDependencies.Remove(module.Entity);
other.MetDependencies.Add(module);
dependency.Info = module;
dependency.IsDependencyMet = true;
TryEnableModule(other);
}
}
public static EntityPath GetModulePath(Type type)
public interface IModuleInfo
{
var attr = type.Get<ModuleAttribute>();
if (attr == null) throw new ArgumentException(
$"Module {type} must be marked with ModuleAttribute", nameof(type));
var path = EntityPath.Parse(
(type.Get<PathAttribute>() is PathAttribute pathAttr)
? pathAttr.Value : type.Name);
Entity<TContext> Entity { get; }
IReadOnlyCollection<ModuleDependency> Dependencies { get; }
bool IsInitialized { get; }
void Enable();
}
// If specified path is absolute, return it now.
if (path.IsAbsolute) return path;
// IEnumerable implementation
public IEnumerator<IModuleInfo> GetEnumerator() => _modules.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// Otherwise, create it based on the type's assembly, namespace and name.
var assemblyName = type.Assembly.GetName().Name!;
if (!type.FullName!.StartsWith(assemblyName + '.')) throw new InvalidOperationException(
$"Module {type} must be defined under namespace {assemblyName}");
var fullNameWithoutAssembly = type.FullName![(assemblyName.Length + 1)..];
public class ModuleDependency
{
public Entity<TContext> Entity { get; }
public IModuleInfo? Info { get; internal set; }
public bool IsDependencyMet { get; internal set; }
var parts = fullNameWithoutAssembly.Split('.')[..^1];
return new(true, parts.Prepend(assemblyName).Concat(path.GetParts()).ToArray());
public ModuleDependency(Entity<TContext> entity,
IModuleInfo? info = null, bool isDependencyMet = false)
{
Entity = entity;
Info = info;
IsDependencyMet = isDependencyMet;
}
}
internal class ModuleInfo
internal class ModuleInfo<T> : IModuleInfo
where T : IModule
{
public Universe Universe { get; }
public Type Type { get; }
public EntityPath Path { get; }
public EntityRef Entity { get; }
public object? Instance { get; internal set; }
public bool IsActive => Instance != null;
public Entity<TContext> Entity { get; }
public IReadOnlyCollection<ModuleDependency> Dependencies { get; }
public bool IsInitialized { get; private set; }
public HashSet<ModuleInfo> MetDependencies { get; } = new();
public HashSet<Entity> UnmetDependencies { get; } = new();
public ModuleInfo(Universe universe, Type type, EntityPath path)
public ModuleInfo(Universe<TContext> universe)
{
Universe = universe;
Type = type;
Path = path;
if (Type.IsAbstract || Type.IsSealed) throw new Exception(
$"Module {Type} must not be abstract, sealed or static");
if (Type.GetConstructor(Type.EmptyTypes) == null) throw new Exception(
$"Module {Type} must define public parameterless constructor");
var module = Universe.New(Path).Add<Module>();
var world = universe.World;
// Add module dependencies from [DependsOn<>] attributes.
foreach (var dependsAttr in Type.GetMultiple<AddRelationAttribute>().Where(attr =>
attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) {
var dependsPath = GetModulePath(dependsAttr.Target);
var dependency = Universe.LookupByPath(dependsPath) ??
Universe.New(dependsPath).Add<Module>().Disable().Build();
var depModule = Universe.Modules.Lookup(dependency);
if (depModule?.IsActive == true) MetDependencies.Add(depModule);
else { UnmetDependencies.Add(dependency); module.Disable(); }
module.Add<DependsOn>(dependency);
if (T.IsBuiltIn)
{
Entity = world.LookupPathOrThrow(T.Path);
Dependencies = Array.Empty<ModuleDependency>();
}
else
{
var builder = world.New(T.Path);
var deps = new List<ModuleDependency>();
builder.Add<Module>();
foreach (var dependsPath in T.Dependencies) {
var dependency = world.LookupPathOrNull(dependsPath) ??
world.New(dependsPath).Add<Module>().Add<Disabled>().Build();
var depModule = universe.Modules.Lookup(dependency);
var isDepInit = (depModule?.IsInitialized == true);
deps.Add(new(dependency, depModule, isDepInit));
if (!isDepInit) builder.Add<Disabled>();
builder.Add<DependsOn>(dependency);
}
Entity = builder.Build().CreateLookup<T>();
Dependencies = deps.AsReadOnly();
// Ensure all parent entities have the Module tag set.
for (var p = Entity.Parent; p is Entity<TContext> parent; p = parent.Parent)
parent.Add<Module>();
}
Entity = module.Build().CreateLookup(Type);
// Ensure all parent entities have Module set.
for (var p = Entity.Parent; p != null; p = p.Parent)
p.Add<Module>();
}
public void Enable()
{
Entity.Enable();
Instance = Activator.CreateInstance(Type)!;
foreach (var type in Type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic))
RegisterNestedType(type);
(Instance as IModuleInitializer)?.Initialize(Entity);
RegisterMethods(Instance);
}
private void RegisterNestedType(Type type)
{
if (!type.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().Any()) return;
if (!type.Has<ComponentAttribute>() && (!type.IsValueType || (type.GetFields().Length > 0)))
throw new Exception($"Type {type} must be an empty, used-defined struct.");
var path = EntityPath.Parse(type.Get<PathAttribute>()?.Value ?? type.Name);
var builder = path.IsAbsolute ? Universe.New(path) : Entity.NewChild(path);
if (type.Get<SymbolAttribute>() is SymbolAttribute symbolAttr)
builder.Symbol(symbolAttr.Value ?? path.Name);
var entity = builder.Build();
EntityRef Lookup(Type toLookup)
=> (type != toLookup) ? Universe.LookupByTypeOrThrow(toLookup) : entity;
foreach (var attr in type.GetMultiple<AddEntityAttribute>())
entity.Add(Lookup(attr.Entity));
foreach (var attr in type.GetMultiple<AddRelationAttribute>())
entity.Add(Lookup(attr.Relation), Lookup(attr.Target));
if (type.Get<SingletonAttribute>()?.AutoAdd == true) entity.Add(entity);
if (type.Has<ComponentAttribute>()) entity.InitComponent(type);
else entity.CreateLookup(type);
if (type.Has<RelationAttribute>()) entity.Add<Doc.Relation>();
if (type.Has<TagAttribute>()) entity.Add<Tag>();
}
private void RegisterMethods(object? instance)
{
foreach (var method in Type.GetMethods(
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance
)) {
if (method.Has<SystemAttribute>())
Universe.InitSystem(instance, method).ChildOf(Entity);
if (method.Has<ObserverAttribute>())
Universe.InitObserver(instance, method).ChildOf(Entity);
}
T.Initialize(Entity);
IsInitialized = true;
}
}
}

@ -1,35 +1,26 @@
using System.Linq;
using gaemstone.ECS;
namespace gaemstone;
public class Universe : World
public class Universe<TContext>
{
public ModuleManager Modules { get; }
public World<TContext> World { get; }
public ModuleManager<TContext> Modules { get; }
public Universe(params string[] args)
: base(args)
{
World = new(args);
Modules = new(this);
// Bootstrap some stuff, so we can register flecs properly.
New("/gaemstone/Doc/DisplayType")
.Add(LookupByPathOrThrow("/flecs/core/Exclusive"))
.Build().CreateLookup<Doc.DisplayType>();
New("/gaemstone/Doc/Relation").Build().CreateLookup<Doc.Relation>();
LookupByPathOrThrow("/flecs/core/Module" ).CreateLookup<Flecs.Core.Module>();
LookupByPathOrThrow("/flecs/core/Component").CreateLookup<Flecs.Core.Component>();
LookupByPathOrThrow("/flecs/core/Tag" ).CreateLookup<Flecs.Core.Tag>();
// Bootstrap [Relation] tag, since it will be added to some Flecs types.
World.New("/gaemstone/Doc/Relation").Build()
.CreateLookup<Doc.Relation>();
// Register built-in (static) modules, which
// are defined in the "gaemstone.Flecs" namespace.
Modules.RegisterAll(GetType().Assembly.GetTypes()
.Where(t => t.IsAbstract && t.IsSealed));
// Bootstrap built-in (static) modules from Flecs.
Modules.Register<Flecs.Core>();
Modules.Register<Flecs.Doc>();
Modules.Register<Flecs.Pipeline>();
Modules.Register<Doc>();
// Create "Game" entity to store global state.
New("/Game").Symbol("Game").Build()
.CreateLookup<Game>().Add<Game>();
}
}

@ -1,299 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
namespace gaemstone.Utility.IL;
public class ILGeneratorWrapper
{
private readonly DynamicMethod _method;
private readonly ILGenerator _il;
private readonly List<ILocal> _locals = new();
private readonly List<(int Offset, int Indent, OpCode Code, object? Arg)> _instructions = new();
private readonly Dictionary<Label, int> _labelToOffset = new();
private readonly Stack<BlockImpl> _indents = new();
public ILGeneratorWrapper(DynamicMethod method)
{
_method = method;
_il = method.GetILGenerator();
}
public string ToReadableString()
{
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.GetFriendlyName()})");
sb.AppendLine("Return:");
sb.AppendLine($" {_method.ReturnType.GetFriendlyName()}");
sb.AppendLine();
sb.AppendLine("Locals:");
foreach (var local in _locals)
sb.AppendLine($" {local}");
sb.AppendLine();
sb.AppendLine("Instructions:");
foreach (var (offset, indent, code, arg) in _instructions) {
sb.Append(" ");
// Append instruction offset.
if (offset < 0) sb.Append(" ");
else sb.Append($"0x{offset:X4} ");
// Append instruction opcode.
if (code == OpCodes.Nop) sb.Append(" ");
else sb.Append($"{code.Name,-12}");
// Append indents.
for (var i = 0; i < indent; i++)
sb.Append("| ");
// Append instruction argument.
if (code == OpCodes.Nop) sb.Append("// ");
switch (arg) {
case Label label: sb.Append($"Label(0x{_labelToOffset.GetValueOrDefault(label, -1):X4})"); break;
case not null: sb.Append(arg); break;
}
sb.AppendLine();
}
return sb.ToString();
}
public IArgument Argument(int index)
{
var type = _method.GetParameters()[index].ParameterType;
if (type.IsByRefLike) return new ArgumentImpl(index, type);
return (IArgument)Activator.CreateInstance(typeof(ArgumentImpl<>).MakeGenericType(type), index)!;
}
public IArgument<T> Argument<T>(int index) => (IArgument<T>)Argument(index);
public ILocal Local(Type type, string? name = null)
{
var builder = _il.DeclareLocal(type);
var local = type.IsByRefLike ? new LocalImpl(builder, name)
: (ILocal)Activator.CreateInstance(typeof(LocalImpl<>).MakeGenericType(type), builder, name)!;
_locals.Add(local);
return local;
}
public ILocal<T> Local<T>(string? name = null) => (ILocal<T>)Local(typeof(T), name);
public ILocal<Array> LocalArray(Type type, string? name = null) => (ILocal<Array>)Local(type.MakeArrayType(), name);
public ILocal<T[]> LocalArray<T>(string? name = null) => (ILocal<T[]>)Local(typeof(T).MakeArrayType(), name);
public Label DefineLabel() => _il.DefineLabel();
public void MarkLabel(Label label)
{
_instructions.Add((-1, _indents.Count, OpCodes.Nop, label));
_labelToOffset.Add(label, _il.ILOffset);
_il.MarkLabel(label);
}
private void AddInstr(OpCode code, object? arg = null) => _instructions.Add((_il.ILOffset, _indents.Count, code, arg));
public void Comment(string comment) => _instructions.Add((-1, _indents.Count, OpCodes.Nop, comment));
private static readonly MethodInfo _writeLine = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) })!;
public void Log(string str) { Load(str); Call(_writeLine); }
internal void Emit(OpCode code) { AddInstr(code, null); _il.Emit(code); }
internal void Emit(OpCode code, int arg) { AddInstr(code, arg); _il.Emit(code, arg); }
internal void Emit(OpCode code, string arg) { AddInstr(code, arg); _il.Emit(code, arg); }
internal void Emit(OpCode code, Type type) { AddInstr(code, type); _il.Emit(code, type); }
internal void Emit(OpCode code, Label label) { AddInstr(code, label); _il.Emit(code, label); }
internal void Emit(OpCode code, ILocal local) { AddInstr(code, local); _il.Emit(code, local.Builder); }
internal void Emit(OpCode code, IArgument arg) { AddInstr(code, arg); _il.Emit(code, arg.Index); }
internal void Emit(OpCode code, MethodInfo method) { AddInstr(code, method); _il.Emit(code, method); }
internal void Emit(OpCode code, ConstructorInfo constr) { AddInstr(code, constr); _il.Emit(code, constr); }
public void Dup() => Emit(OpCodes.Dup);
public void Pop() => Emit(OpCodes.Pop);
public void LoadNull() => Emit(OpCodes.Ldnull);
public void LoadConst(int value) => Emit(OpCodes.Ldc_I4, value);
public void Load(string value) => Emit(OpCodes.Ldstr, value);
private static readonly MethodInfo _typeFromHandleMethod = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!;
public void Typeof(Type value) { Emit(OpCodes.Ldtoken, value); Call(_typeFromHandleMethod); }
public void Load(IArgument arg) => Emit(OpCodes.Ldarg, arg);
public void LoadAddr(IArgument arg) => Emit(OpCodes.Ldarga, arg);
public void Load(ILocal local) => Emit(OpCodes.Ldloc, local);
public void LoadAddr(ILocal local) => Emit(OpCodes.Ldloca, local);
public void Store(ILocal local) => Emit(OpCodes.Stloc, local);
public void Set(ILocal<int> local, int value) { LoadConst(value); Store(local); }
public void LoadLength() { Emit(OpCodes.Ldlen); Emit(OpCodes.Conv_I4); }
public void LoadLength(IArgument<Array> array) { Load(array); LoadLength(); }
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); }
public void LoadElem(Type type) => Emit(OpCodes.Ldelem, type);
public void LoadElem(Type type, int index) { LoadConst(index); LoadElem(type); }
public void LoadElem(Type type, ILocal<int> index) { Load(index); LoadElem(type); }
public void LoadElem(Type type, IArgument<Array> array, int index) { Load(array); LoadElem(type, index); }
public void LoadElem(Type type, IArgument<Array> array, ILocal<int> index) { Load(array); LoadElem(type, index); }
public void LoadElem(Type type, ILocal<Array> array, int index) { Load(array); LoadElem(type, index); }
public void LoadElem(Type type, ILocal<Array> array, ILocal<int> index) { Load(array); LoadElem(type, index); }
public void LoadElemRef() => Emit(OpCodes.Ldelem_Ref);
public void LoadElemRef(int index) { LoadConst(index); LoadElemRef(); }
public void LoadElemRef(ILocal<int> index) { Load(index); LoadElemRef(); }
public void LoadElemRef(IArgument<Array> array, int index) { Load(array); LoadElemRef(index); }
public void LoadElemRef(IArgument<Array> array, ILocal<int> index) { Load(array); LoadElemRef(index); }
public void LoadElemRef(ILocal<Array> array, int index) { Load(array); LoadElemRef(index); }
public void LoadElemRef(ILocal<Array> array, ILocal<int> index) { Load(array); LoadElemRef(index); }
public void LoadElemEither(Type type) { if (type.IsValueType) LoadElem(type); else LoadElemRef(); }
public void LoadElemEither(Type type, int index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); }
public void LoadElemEither(Type type, ILocal<int> index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); }
public void LoadElemEither(Type type, IArgument<Array> array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); }
public void LoadElemEither(Type type, IArgument<Array> array, ILocal<int> index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); }
public void LoadElemEither(Type type, ILocal<Array> array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); }
public void LoadElemEither(Type type, ILocal<Array> array, ILocal<int> index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); }
public void LoadElemAddr(Type type) => Emit(OpCodes.Ldelema, type);
public void LoadElemAddr(Type type, int index) { LoadConst(index); LoadElemAddr(type); }
public void LoadElemAddr(Type type, ILocal<int> index) { Load(index); LoadElemAddr(type); }
public void LoadElemAddr(Type type, IArgument<Array> array, int index) { Load(array); LoadElemAddr(type, index); }
public void LoadElemAddr(Type type, IArgument<Array> array, ILocal<int> index) { Load(array); LoadElemAddr(type, index); }
public void LoadElemAddr(Type type, ILocal<Array> array, int index) { Load(array); LoadElemAddr(type, index); }
public void LoadElemAddr(Type type, ILocal<Array> array, ILocal<int> index) { Load(array); LoadElemAddr(type, index); }
public void Load(PropertyInfo info) => CallVirt(info.GetMethod!);
public void Load(ILocal obj, PropertyInfo info) { Load(obj); Load(info); }
public void Load(IArgument obj, PropertyInfo info) { Load(obj); Load(info); }
public void Add() => Emit(OpCodes.Add);
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 => Init(typeof(T));
public void New(ConstructorInfo constructor) => Emit(OpCodes.Newobj, constructor);
public void New(Type type) => New(type.GetConstructors().Single());
public void New(Type type, params Type[] paramTypes) => New(type.GetConstructor(paramTypes)!);
public void Cast(Type type) => Emit(OpCodes.Castclass, type);
public void Cast<T>() => Cast(typeof(T));
public void Box(Type type) => Emit(OpCodes.Box, type);
public void Box<T>() => Box(typeof(T));
public void Goto(Label label) => Emit(OpCodes.Br, label);
public void GotoIfTrue(Label label) => Emit(OpCodes.Brtrue, label);
public void GotoIfFalse(Label label) => Emit(OpCodes.Brfalse, label);
public void GotoIf(Label label, ILocal<int> a, Comparison op, ILocal<int> b)
{ Load(a); Load(b); Emit(op.Code, label); }
public void GotoIfNull(Label label, ILocal<object> local)
{ Load(local); Emit(OpCodes.Brfalse, label); }
public void GotoIfNotNull(Label label, ILocal<object> local)
{ Load(local); Emit(OpCodes.Brtrue, label); }
public void Call(MethodInfo method) => Emit(OpCodes.Call, method);
public void CallVirt(MethodInfo method) => Emit(OpCodes.Callvirt, method);
private static readonly MethodInfo _consoleWriteLineMethod = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) })!;
public void Print(string str) { Load(str); Call(_consoleWriteLineMethod); }
public void Return() => Emit(OpCodes.Ret);
public delegate void WhileBodyAction(Label continueLabel, Label breakLabel);
public delegate void WhileTestAction(Label continueLabel);
public void While(string name, WhileTestAction testAction, WhileBodyAction bodyAction) {
var bodyLabel = DefineLabel();
var testLabel = DefineLabel();
var breakLabel = DefineLabel();
Goto(testLabel);
Comment("BEGIN LOOP " + name);
MarkLabel(bodyLabel);
using (Indent()) bodyAction(testLabel, breakLabel);
Comment("TEST LOOP " + name);
MarkLabel(testLabel);
using (Indent()) testAction(bodyLabel);
Comment("END LOOP " + name);
MarkLabel(breakLabel);
}
public IDisposable Block(Action onClose)
=> new BlockImpl(onClose);
public IDisposable Indent()
{
BlockImpl indent = null!;
indent = new(() => { if (_indents.Pop() != indent) throw new InvalidOperationException(); });
_indents.Push(indent);
return indent;
}
internal class BlockImpl : IDisposable
{
public Action OnClose { get; }
public BlockImpl(Action onClose) => OnClose = onClose;
public void Dispose() => OnClose();
}
internal class ArgumentImpl : IArgument
{
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.GetFriendlyName()})";
}
internal class ArgumentImpl<T> : ArgumentImpl, IArgument<T>
{ public ArgumentImpl(int index) : base(index, typeof(T)) { } }
internal class LocalImpl : ILocal
{
public LocalBuilder Builder { get; }
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.GetFriendlyName()}){(Name != null ? $" // {Name}" : "")}";
}
internal class LocalImpl<T> : LocalImpl, ILocal<T>
{ public LocalImpl(LocalBuilder builder, string? name) : base(builder, name) { } }
}
public class Comparison
{
public static Comparison NotEqual { get; } = new(OpCodes.Bne_Un);
public static Comparison LessThan { get; } = new(OpCodes.Blt);
public static Comparison LessOrEq { get; } = new(OpCodes.Ble);
public static Comparison Equal { get; } = new(OpCodes.Beq);
public static Comparison GreaterOrEq { get; } = new(OpCodes.Bge);
public static Comparison GreaterThan { get; } = new(OpCodes.Bgt);
public OpCode Code { get; }
private Comparison(OpCode code) => Code = code;
}
public interface IArgument
{
int Index { get; }
Type ArgumentType { get; }
}
public interface IArgument<out T>
: IArgument { }
public interface ILocal
{
LocalBuilder Builder { get; }
Type LocalType { get; }
}
public interface ILocal<out T>
: ILocal { }

@ -1,404 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using gaemstone.ECS;
namespace gaemstone.Utility.IL;
// TODO: Support tuple syntax to match relationship pairs.
public unsafe class IterActionGenerator
{
private static readonly ConstructorInfo _entityRefCtor = typeof(EntityRef).GetConstructors().Single();
private static readonly PropertyInfo _iterWorldProp = typeof(Iterator).GetProperty(nameof(Iterator.World))!;
private static readonly PropertyInfo _iterDeltaTimeProp = typeof(Iterator).GetProperty(nameof(Iterator.DeltaTime))!;
private static readonly PropertyInfo _iterCountProp = typeof(Iterator).GetProperty(nameof(Iterator.Count))!;
private static readonly MethodInfo _iterEntityMethod = typeof(Iterator).GetMethod(nameof(Iterator.Entity))!;
private static readonly MethodInfo _iterFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field ), 1, new[] { typeof(int) })!;
private static readonly MethodInfo _iterFieldOrEmptyMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldOrEmpty), 1, new[] { typeof(int) })!;
private static readonly MethodInfo _iterFieldRefMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field ), 1, new[] { typeof(int), Type.MakeGenericMethodParameter(0) })!;
private static readonly MethodInfo _iterFieldOrEmptyRefMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldOrEmpty), 1, new[] { typeof(int), Type.MakeGenericMethodParameter(0) })!;
private static readonly ConditionalWeakTable<MethodInfo, IterActionGenerator> _cache = new();
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>>> _globalUniqueParameters = new() {
[typeof(World)] = (IL, iter) => { IL.Load(iter, _iterWorldProp); },
[typeof(Universe)] = (IL, iter) => { IL.Load(iter, _iterWorldProp); IL.Cast(typeof(Universe)); },
[typeof(TimeSpan)] = (IL, iter) => { IL.Load(iter, _iterDeltaTimeProp); },
};
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>, ILocal<int>>> _uniqueParameters = new() {
[typeof(Iterator)] = (IL, iter, i) => { IL.Load(iter); },
[typeof(EntityRef)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iterEntityMethod); },
};
public World World { get; }
public MethodInfo Method { get; }
public IReadOnlyList<ParamInfo> Parameters { get; }
public IReadOnlyList<Term> Terms { get; }
public Action<object?, Iterator> GeneratedAction { get; }
public string ReadableString { get; }
public void RunWithTryCatch(object? instance, Iterator iter)
{
try { GeneratedAction(instance, iter); }
catch { Console.Error.WriteLine(ReadableString); throw; }
}
public IterActionGenerator(World world, MethodInfo method)
{
World = world;
Method = method;
Parameters = method.GetParameters().Select(ParamInfo.Build).ToImmutableArray();
var name = "<>Query_" + string.Join("_", method.Name);
var genMethod = new DynamicMethod(name, null, new[] { typeof(object), typeof(Iterator) });
var IL = new ILGeneratorWrapper(genMethod);
var instanceArg = IL.Argument<object?>(0);
var iteratorArg = IL.Argument<Iterator>(1);
var fieldIndex = 0;
var paramData = new List<(ParamInfo Info, Term? Term, ILocal? FieldLocal, ILocal? TempLocal)>();
foreach (var p in Parameters) {
// If the parameter is unique, we don't create a term for it.
if (p.Kind <= ParamKind.Unique)
{ paramData.Add((p, null, null, null)); continue; }
// Create a term to add to the query.
var term = new Term(world.LookupByTypeOrThrow(p.UnderlyingType)) {
Source = (p.Source != null) ? (TermId)World.LookupByTypeOrThrow(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,
ParamKind.Or => TermOperKind.Or,
_ when !p.IsRequired => TermOperKind.Optional,
_ => default,
},
};
// If this and the previous parameter are marked with [Or], do not advance the field index.
if ((fieldIndex == 0) || (p.Kind != ParamKind.Or) || (paramData[^1].Info.Kind != ParamKind.Or))
fieldIndex++;
var spanType = typeof(Span<>).MakeGenericType(p.FieldType);
var fieldLocal = (ILocal?)null;
var tempLocal = (ILocal?)null;
switch (p.Kind) {
// FIXME: Currently would not work with [Or]'d components.
case ParamKind.Has or ParamKind.Not or ParamKind.Or:
if (!p.ParameterType.IsValueType) break;
// If parameter is a struct, we require a temporary local that we can
// later load onto the stack when loading the arguments for the action.
IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});");
tempLocal = IL.Local(p.ParameterType);
IL.LoadAddr(tempLocal);
IL.Init(tempLocal.LocalType);
break;
case ParamKind.Nullable or ParamKind.Or:
IL.Comment($"{p.Info.Name}Field = iterator.FieldOrEmpty<{p.FieldType.Name}>({fieldIndex})");
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field");
IL.Load(iteratorArg);
IL.LoadConst(fieldIndex);
IL.Call(_iterFieldOrEmptyMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocal);
if (p.UnderlyingType.IsValueType) {
IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});");
tempLocal = IL.Local(p.ParameterType);
IL.LoadAddr(tempLocal);
IL.Init(tempLocal.LocalType);
}
break;
default:
IL.Comment($"{p.Info.Name}Field = iterator.Field<{p.FieldType.Name}>({fieldIndex})");
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field");
IL.Load(iteratorArg);
IL.LoadConst(fieldIndex);
IL.Call(_iterFieldMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocal);
break;
}
paramData.Add((p, term, fieldLocal, tempLocal));
}
var indexLocal = IL.Local<int>("iter_index");
var countLocal = IL.Local<int>("iter_count");
IL.Set(indexLocal, 0);
IL.Load(iteratorArg, _iterCountProp);
IL.Store(countLocal);
// If all parameters are fixed, iterator count will be 0, but since
// the query matched, we want to run the callback at least once.
IL.Comment("if (iter_count == 0) iter_count = 1;");
var dontIncrementLabel = IL.DefineLabel();
IL.Load(countLocal);
IL.GotoIfTrue(dontIncrementLabel);
IL.LoadConst(1);
IL.Store(countLocal);
IL.MarkLabel(dontIncrementLabel);
IL.While("IteratorLoop", (@continue) => {
IL.GotoIf(@continue, indexLocal, Comparison.LessThan, countLocal);
}, (_, _) => {
if (!Method.IsStatic)
IL.Load(instanceArg);
foreach (var (info, term, fieldLocal, tempLocal) in paramData) {
var isValueType = info.UnderlyingType.IsValueType;
var paramName = info.ParameterType.GetFriendlyName();
switch (info.Kind) {
case ParamKind.GlobalUnique:
IL.Comment($"Global unique parameter {paramName}");
_globalUniqueParameters[info.ParameterType](IL, iteratorArg);
break;
case ParamKind.Unique:
IL.Comment($"Unique parameter {paramName}");
_uniqueParameters[info.ParameterType](IL, iteratorArg, indexLocal!);
break;
// FIXME: Currently would not work with [Or]'d components.
case ParamKind.Has or ParamKind.Not or ParamKind.Or:
IL.Comment($"Has parameter {paramName}");
if (isValueType) IL.LoadObj(tempLocal!);
else IL.LoadNull();
break;
default:
var spanType = isValueType ? typeof(Span<>) : typeof(Iterator.SpanToRef<>);
var concreteSpanType = spanType.MakeGenericType(info.UnderlyingType);
var spanItemMethod = concreteSpanType.GetProperty("Item")!.GetMethod!;
var spanLengthMethod = concreteSpanType.GetProperty("Length")!.GetMethod!;
IL.Comment($"Parameter {paramName}");
if (info.IsByRef) {
IL.LoadAddr(fieldLocal!);
if (info.IsFixed) IL.LoadConst(0);
else IL.Load(indexLocal!);
IL.Call(spanItemMethod);
} else if (info.IsRequired) {
IL.LoadAddr(fieldLocal!);
if (info.IsFixed) IL.LoadConst(0);
else IL.Load(indexLocal!);
IL.Call(spanItemMethod);
if (isValueType) IL.LoadObj(info.FieldType);
} else {
var elseLabel = IL.DefineLabel();
var doneLabel = IL.DefineLabel();
IL.LoadAddr(fieldLocal!);
IL.Call(spanLengthMethod);
IL.GotoIfFalse(elseLabel);
IL.LoadAddr(fieldLocal!);
if (info.IsFixed) IL.LoadConst(0);
else IL.Load(indexLocal!);
IL.Call(spanItemMethod);
if (isValueType) {
IL.LoadObj(info.FieldType);
IL.New(info.ParameterType);
}
IL.Goto(doneLabel);
IL.MarkLabel(elseLabel);
if (isValueType) IL.LoadObj(tempLocal!);
else IL.LoadNull();
IL.MarkLabel(doneLabel);
}
break;
}
}
IL.Call(Method);
IL.Increment(indexLocal);
});
IL.Return();
Terms = paramData.Where(p => p.Term != null).Select(p => p.Term!).ToImmutableList();
GeneratedAction = genMethod.CreateDelegate<Action<object?, Iterator>>();
ReadableString = IL.ToReadableString();
}
public static IterActionGenerator GetOrBuild(World world, MethodInfo method)
=>_cache.GetValue(method, m => new IterActionGenerator(world, m));
public class ParamInfo
{
public ParameterInfo Info { get; }
public ParamKind Kind { get; }
public Type ParameterType { get; }
public Type UnderlyingType { get; }
public Type FieldType { get; }
public Type? Source { get; }
public bool IsRequired => (Kind < ParamKind.Nullable);
public bool IsByRef => (Kind is ParamKind.In or ParamKind.Ref);
public bool IsFixed => (Kind == ParamKind.GlobalUnique) || (Source != null);
private ParamInfo(ParameterInfo info, ParamKind kind,
Type paramType, Type underlyingType)
{
Info = info;
Kind = kind;
ParameterType = paramType;
UnderlyingType = underlyingType;
// Reference types have a backing type of ReferenceHandle.
FieldType = underlyingType.IsValueType ? underlyingType : typeof(ReferenceHandle);
if (UnderlyingType.Has<SingletonAttribute>()) Source = UnderlyingType;
if (Info.Get<SourceAttribute>()?.Type is Type type) Source = type;
}
public static ParamInfo Build(ParameterInfo info)
{
if (info.IsOptional) throw new ArgumentException($"Optional parameters are not supported\nParameter: {info}");
if (info.ParameterType.IsArray) throw new ArgumentException($"Arrays are not supported\nParameter: {info}");
if (info.ParameterType.IsPointer) throw new ArgumentException($"Pointers are not supported\nParameter: {info}");
if (info.ParameterType.IsPrimitive) throw new ArgumentException($"Primitives are not supported\nParameter: {info}");
// Find out initial parameter kind from provided attribute.
var fromAttributes = new List<ParamKind>();
if (info.Has< InAttribute>()) fromAttributes.Add(ParamKind.In);
if (info.Has<OutAttribute>()) fromAttributes.Add(ParamKind.Out);
if (info.Has<HasAttribute>()) fromAttributes.Add(ParamKind.Has);
if (info.Has< OrAttribute>()) fromAttributes.Add(ParamKind.Or);
if (info.Has<NotAttribute>()) fromAttributes.Add(ParamKind.Not);
// Throw an error if multiple incompatible attributes were found.
if (fromAttributes.Count > 1) throw new ArgumentException(
"Parameter must not be marked with multiple attributes: "
+ string.Join(", ", fromAttributes.Select(a => $"[{a}]"))
+ $"\nParameter: {info}");
var kind = fromAttributes.FirstOrNull() ?? ParamKind.Normal;
// Handle unique parameters such as Universe, EntityRef, ...
var isGlobalUnique = _globalUniqueParameters.ContainsKey(info.ParameterType);
var isUnique = _uniqueParameters.ContainsKey(info.ParameterType);
if (isGlobalUnique || isUnique) {
if (kind != ParamKind.Normal) throw new ArgumentException(
$"Unique parameter {info.ParameterType.Name} does not support [{kind}]\nParameter: {info}");
kind = isGlobalUnique ? ParamKind.GlobalUnique : ParamKind.Unique;
return new(info, kind, info.ParameterType, info.ParameterType);
}
var isNullable = info.IsNullable();
var isByRef = info.ParameterType.IsByRef;
if (info.ParameterType.Has<TagAttribute>() && (kind is not (ParamKind.Has or ParamKind.Not or ParamKind.Or))) {
if (kind is not ParamKind.Normal) throw new ArgumentException($"Parameter does not support [{kind}]\nParameter: {info}");
kind = ParamKind.Has;
}
if (kind is ParamKind.Not or ParamKind.Has) {
if (isNullable) throw new ArgumentException($"Parameter does not support Nullable\nParameter: {info}");
if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}");
return new(info, kind, info.ParameterType, info.ParameterType);
}
var underlyingType = info.ParameterType;
if (isNullable) {
if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}");
if (info.ParameterType.IsValueType)
underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!;
kind = ParamKind.Nullable;
}
if (info.ParameterType.IsByRef) {
if (kind != ParamKind.Normal) throw new ArgumentException(
$"Parameter does not support [{kind}]\nParameter: {info}");
underlyingType = info.ParameterType.GetElementType()!;
if (!underlyingType.IsValueType) throw new ArgumentException(
$"Reference types can't also be ByRef\nParameter: {info}");
kind = info.IsIn ? ParamKind.In
: info.IsOut ? ParamKind.Out
: ParamKind.Ref;
}
if (underlyingType.IsPrimitive) throw new ArgumentException(
$"Primitives are not supported\nParameter: {info}");
return new(info, kind, info.ParameterType, underlyingType);
}
}
public enum ParamKind
{
/// <summary>
/// Not part of the resulting query's terms.
/// Same value across a single invocation of a callback.
/// For example <see cref="ECS.World"/> or <see cref="TimeSpan"/>.
/// </summary>
GlobalUnique,
/// <summary>
/// Not part of the resulting query's terms.
/// Unique value for each iterated entity.
/// For example <see cref="EntityRef"/>.
/// </summary>
Unique,
/// <summary> Passed by value. </summary>
Normal,
/// <summary>
/// Struct passed with the "in" modifier, allowing direct pointer access.
/// Manually applied with <see cref="InAttribute"/>.
/// Marks a component as being read from.
/// </summary>
In,
/// <summary>
/// Struct passed with the "out" modifier, allowing direct pointer access.
/// Manually applied with <see cref="HasAttribute"/>.
/// Marks a component as being written to.
/// </summary>
Out,
/// <summary>
/// Struct passed with the "ref" modifier, allowing direct pointer access.
/// Marks a component as being read from and written to.
/// </summary>
Ref,
/// <summary>
/// Only checks for presence.
/// Manually applied with <see cref="HasAttribute"/>.
/// Automatically applied for types with <see cref="TagAttribute"/>.
/// Marks a component as not being accessed.
/// </summary>
Has,
/// <summary>
/// Struct or class passed as <see cref="T?"/>.
/// </summary>
Nullable,
/// <summary>
/// Only checks for absence.
/// Applied with <see cref="NotAttribute"/>.
/// </summary>
Not,
/// <summary>
/// Matches any terms in a chain of "or" terms.
/// Applied with <see cref="OrAttribute"/>.
/// Implies <see cref="Nullable"/>.
/// </summary>
Or,
}
}

@ -0,0 +1,37 @@
using System.Runtime.InteropServices;
namespace gaemstone.Utility;
[StructLayout(LayoutKind.Explicit)]
internal struct Union<T1, T2>
{
[FieldOffset(0)] public T1 Value1;
[FieldOffset(0)] public T2 Value2;
}
[StructLayout(LayoutKind.Explicit)]
internal struct Union<T1, T2, T3>
{
[FieldOffset(0)] public T1 Value1;
[FieldOffset(0)] public T2 Value2;
[FieldOffset(0)] public T3 Value3;
}
[StructLayout(LayoutKind.Explicit)]
internal struct Union<T1, T2, T3, T4>
{
[FieldOffset(0)] public T1 Value1;
[FieldOffset(0)] public T2 Value2;
[FieldOffset(0)] public T3 Value3;
[FieldOffset(0)] public T4 Value4;
}
[StructLayout(LayoutKind.Explicit)]
internal struct Union<T1, T2, T3, T4, T5>
{
[FieldOffset(0)] public T1 Value1;
[FieldOffset(0)] public T2 Value2;
[FieldOffset(0)] public T3 Value3;
[FieldOffset(0)] public T4 Value4;
[FieldOffset(0)] public T5 Value5;
}

@ -6,10 +6,13 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <!-- -->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
<ProjectReference Include="../gaemstone.SourceGen/gaemstone.SourceGen.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>

Loading…
Cancel
Save