Source Generators, part 3: Another Refactor

- Update to new gaemstone.ECS' new handling of
  generic context with World<T>, Entity<T>, etc
- Completely rewrite the source generator
- Remove AutoAdd from [Singleton]
  [Add<Flecs.Core.Disabled>] can be used instead
- Replace some term attributes with generics
  For example, "[Has] T foo" is now "Has<T> foo"
- Move some Flecs types into Flecs.Core
- Remove IL generator related code
- Remove [Game] attribute and entity
- Turn Canvas and GameWindow into singletons
- Probably some stuff I forgot
wip/source-generators
copygirl 12 months ago
parent 20d0cd2f0e
commit 8e9ff611d9
  1. 2
      src/Immersion/Immersion.csproj
  2. 5
      src/Immersion/ManagedComponentTest.cs
  3. 4
      src/Immersion/ObserverTest.cs
  4. 39
      src/Immersion/Program.cs
  5. 6
      src/gaemstone.Bloxel/ChunkPaletteStorage.cs
  6. 112
      src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs
  7. 24
      src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs
  8. 1
      src/gaemstone.Bloxel/gaemstone.Bloxel.csproj
  9. 254
      src/gaemstone.Client/Systems/EntityInspector.cs
  10. 35
      src/gaemstone.Client/Systems/FreeCameraController.cs
  11. 4
      src/gaemstone.Client/Systems/ImGuiDemoWindow.cs
  12. 36
      src/gaemstone.Client/Systems/ImGuiInputDebug.cs
  13. 37
      src/gaemstone.Client/Systems/ImGuiManager.cs
  14. 55
      src/gaemstone.Client/Systems/InputManager.cs
  15. 9
      src/gaemstone.Client/Systems/MeshManager.cs
  16. 33
      src/gaemstone.Client/Systems/Renderer.cs
  17. 11
      src/gaemstone.Client/Systems/TextureManager.cs
  18. 9
      src/gaemstone.Client/Systems/Windowing.cs
  19. 3
      src/gaemstone.Client/gaemstone.Client.csproj
  20. 2
      src/gaemstone.ECS
  21. 220
      src/gaemstone.SourceGen/Descriptors.cs
  22. 261
      src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs
  23. 141
      src/gaemstone.SourceGen/Generators/ModuleGenerator.cs
  24. 369
      src/gaemstone.SourceGen/ModuleGenerator.cs
  25. 82
      src/gaemstone.SourceGen/RelevantSymbolReceiver.cs
  26. 78
      src/gaemstone.SourceGen/Structure/BaseEntityInfo.cs
  27. 43
      src/gaemstone.SourceGen/Structure/BaseInfo.cs
  28. 85
      src/gaemstone.SourceGen/Structure/MethodEntityInfo.cs
  29. 60
      src/gaemstone.SourceGen/Structure/ModuleEntityInfo.cs
  30. 216
      src/gaemstone.SourceGen/Structure/ParameterInfo.cs
  31. 76
      src/gaemstone.SourceGen/Structure/TypeEntityInfo.cs
  32. 8
      src/gaemstone.SourceGen/Utility/IsExternalInit.cs
  33. 105
      src/gaemstone.SourceGen/Utility/SymbolExtensions.cs
  34. 1
      src/gaemstone.SourceGen/gaemstone.SourceGen.csproj
  35. 16
      src/gaemstone/ECS/FilterExtensions.cs
  36. 13
      src/gaemstone/ECS/Game.cs
  37. 32
      src/gaemstone/ECS/Module+Attributes.cs
  38. 20
      src/gaemstone/ECS/Module+Components.cs
  39. 28
      src/gaemstone/ECS/Module.cs
  40. 45
      src/gaemstone/ECS/Observer.cs
  41. 88
      src/gaemstone/ECS/System+Terms.cs
  42. 70
      src/gaemstone/ECS/System.cs
  43. 32
      src/gaemstone/ECS/TermAttributes.cs
  44. 34
      src/gaemstone/Flecs/Core.cs
  45. 18
      src/gaemstone/Flecs/DeletionEvent.cs
  46. 59
      src/gaemstone/Flecs/Doc.cs
  47. 14
      src/gaemstone/Flecs/ObserverEvent.cs
  48. 88
      src/gaemstone/Flecs/Pipeline.cs
  49. 91
      src/gaemstone/Flecs/SystemPhase.cs
  50. 11
      src/gaemstone/Flecs/Systems/Monitor.cs
  51. 11
      src/gaemstone/Flecs/Systems/Rest.cs
  52. 139
      src/gaemstone/Universe+Modules.cs
  53. 25
      src/gaemstone/Universe.cs
  54. 299
      src/gaemstone/Utility/IL/ILGeneratorWrapper.cs
  55. 404
      src/gaemstone/Utility/IL/IterActionGenerator.cs
  56. 37
      src/gaemstone/Utility/Union.cs
  57. 1
      src/gaemstone/gaemstone.csproj

@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/*" />
<EmbeddedResource Include="Resources/**" />
</ItemGroup>
<ItemGroup>

@ -1,5 +1,6 @@
using System;
using gaemstone.ECS;
using gaemstone.ECS.Utility;
namespace Immersion;
@ -13,9 +14,9 @@ public partial class ManagedComponentTest
}
[System]
public static void CreateLotsOfGarbageData(World world)
public static void CreateLotsOfGarbageData<T>(World<T> world)
{
var game = world.LookupByPathOrThrow("/Game");
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.

@ -1,7 +1,7 @@
using System;
using gaemstone.ECS;
using gaemstone.Flecs;
using static gaemstone.Bloxel.Components.CoreComponents;
using static gaemstone.Flecs.Core;
namespace Immersion;
@ -10,7 +10,7 @@ namespace Immersion;
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
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,15 +19,16 @@ 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.
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();
@ -57,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");
universe.New("MainCamera")
// 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));
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++) {
@ -86,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++)
@ -110,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,6 +12,11 @@ using static gaemstone.Client.Systems.Windowing;
namespace gaemstone.Bloxel.Client.Systems;
[Module]
[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 partial 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 partial 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),

@ -6,14 +6,15 @@ using static gaemstone.Bloxel.Constants;
namespace gaemstone.Bloxel.Systems;
[Public, Module]
[Module]
[DependsOn<gaemstone.Bloxel.Components.CoreComponents>]
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);
@ -23,16 +24,17 @@ public partial class BasicWorldGenerator
}
[Tag]
public struct HasBasicWorldGeneration { }
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 partial class BasicWorldGenerator
if (_noise.GetNoise(globalX, globalY, globalZ) > bias)
blocks[localX, localY, localZ] = _rnd.Pick(stone, dirt, grass);
}
entity.Add<HasBasicWorldGeneration>();
entity.Add<BasicWorldGenerationDone>();
}
}

@ -5,6 +5,7 @@
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <!-- -->
</PropertyGroup>
<ItemGroup>

@ -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;
@ -22,7 +21,7 @@ public partial class EntityInspector
public struct InspectorWindow { }
[Relation, Exclusive]
[Add<DeletionEvent.OnDeleteTarget, DeletionBehavior.Delete>]
[Add<Core.OnDeleteTarget, Core.Delete>]
public struct Selected { }
[Tag]
@ -44,17 +43,16 @@ public partial 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 partial 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 partial 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 partial 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,7 +104,7 @@ public partial 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]);
@ -121,7 +120,7 @@ public partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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();

@ -21,24 +21,25 @@ public partial 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 partial 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;

@ -9,10 +9,10 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
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);

@ -17,13 +17,12 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
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 partial 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 partial 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 partial 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 partial 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 partial 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 partial 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;
@ -22,14 +22,17 @@ namespace gaemstone.Client.Systems;
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 { }
[Singleton(AutoAdd = false)]
// Start out disabled, because we first need to set it with a valid instance.
// TODO: Add a hint when a Singleton class is used without adding Disabled.
// TODO: Add a [Disabled] attribute as shortcut?
[Singleton, Add<Core.Disabled>]
public class ImGuiData
{
public ImGuiController Controller { get; }
@ -84,10 +87,11 @@ public partial 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>().Enable().Set(new ImGuiData(
new(canvas.GL, window.Handle, inputContext.Value, () => {
var io = ImGui.GetIO();
var style = ImGui.GetStyle();
@ -139,15 +143,16 @@ public partial 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 +185,13 @@ public partial 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();
}

@ -21,11 +21,12 @@ public partial 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 partial 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 partial 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 partial 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,7 +105,7 @@ public partial 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) {
@ -114,7 +118,7 @@ public partial class InputManager
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();
@ -134,11 +138,12 @@ public partial class InputManager
// public static void OnActiveAdded(EntityRef entity, Active _)
// => entity.Add<Activated>();
[Observer<ObserverEvent.OnRemove>]
public static void OnActiveRemoved(EntityRef entity, Active _)
[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)
[System]
[DependsOn<Pipeline.PostFrame>]
public static void ClearDeActivated<T>(Entity<T> entity, Has<Or<Activated, Deactivated>> _)
=> entity.Remove<Activated>().Remove<Deactivated>();
}

@ -21,12 +21,11 @@ public partial class MeshManager
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;
@ -21,13 +21,13 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Components.TransformComponents>]
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 partial 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 partial 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);
@ -93,12 +95,12 @@ public partial class Renderer
var cameraMatrix = invertedTransform * cameraProjection;
GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.M11);
_renderEntityRule ??= new(universe, new("""
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 _renderEntityRule.Iter()) {
""")));
foreach (var iter in rule.Iter()) {
var transforms = iter.Field<GlobalTransform>(1);
var meshes = iter.Field<MeshHandle>(3);
// var texPairs = iter.FieldOrEmpty<Identifier>(4);
@ -125,7 +127,8 @@ public partial class Renderer
}
}
[System<SystemPhase.PostFrame>]
[System]
[DependsOn<Pipeline.PostFrame>]
public static void SwapBuffers(GameWindow window)
=> window.Handle.SwapBuffers();

@ -17,7 +17,7 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Systems.Windowing>]
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 partial 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);
}

@ -9,7 +9,9 @@ namespace gaemstone.Client.Systems;
[Module]
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 partial 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,10 +6,11 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <!-- -->
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/*" />
<EmbeddedResource Include="Resources/**" />
</ItemGroup>
<ItemGroup>

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

@ -0,0 +1,220 @@
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 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);
// 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);
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);
}

@ -1,261 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace gaemstone.SourceGen.Generators;
// TODO: "Jump to Definition" feature to open the file + line in which a module / component / system is defined.
[Generator]
public partial class AutoRegisterComponentsGenerator
: ISourceGenerator
{
private static readonly DiagnosticDescriptor ComponentNotPartOfModule = new(
"gaem0101", "Components must be part of a module",
"Type {0} isn't defined as part of a [Module]",
nameof(AutoRegisterComponentsGenerator), DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor ComponentMultipleTypes = new(
"gaem0102", "Components may not have multiple component types",
"Type {0} is marked with multiple component types ({1})",
nameof(AutoRegisterComponentsGenerator), DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor ComponentRelationInvalidType = new(
"gaem0103", "Relations may only be marked with [Component] or [Tag]",
"Type {0} marked as [Relation] may not be marked with [{1}], only [Component] or [Tag] are valid",
nameof(AutoRegisterComponentsGenerator), DiagnosticSeverity.Error, true);
public void Initialize(GeneratorInitializationContext context)
=> context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
private class SyntaxReceiver
: ISyntaxContextReceiver
{
public Dictionary<INamedTypeSymbol, HashSet<INamedTypeSymbol>> Modules { get; }
= new(SymbolEqualityComparer.Default);
public HashSet<INamedTypeSymbol> ComponentsNotInModule { get; }
= new(SymbolEqualityComparer.Default);
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is not AttributeSyntax attrNode) return;
var model = context.SemanticModel;
var attrType = model.GetTypeInfo(attrNode).Type!;
if ((attrType.GetNamespace() != "gaemstone.ECS") || !attrType.Name.EndsWith("Attribute")) return;
var attrName = attrType.Name.Substring(0, attrType.Name.Length - "Attribute".Length);
if (!Enum.TryParse<RegisterType>(attrName, out _)) return;
var symbol = (model.GetDeclaredSymbol(attrNode.Parent?.Parent!) as INamedTypeSymbol)!;
if ((symbol.ContainingSymbol is INamedTypeSymbol module)
&& module.HasAttribute("gaemstone.ECS.ModuleAttribute"))
{
if (!Modules.TryGetValue(module, out var components))
Modules.Add(module, components = new(SymbolEqualityComparer.Default));
components.Add(symbol);
}
else ComponentsNotInModule.Add(symbol);
}
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxContextReceiver is not SyntaxReceiver receiver) return;
foreach (var symbol in receiver.ComponentsNotInModule)
context.ReportDiagnostic(Diagnostic.Create(ComponentNotPartOfModule,
symbol.Locations.FirstOrDefault(), symbol.GetFullName()));
var modules = new Dictionary<INamedTypeSymbol, ModuleInfo>(
SymbolEqualityComparer.Default);
foreach (var pair in receiver.Modules) {
var moduleSymbol = pair.Key;
if (!modules.TryGetValue(moduleSymbol, out var module))
modules.Add(moduleSymbol, module = new(moduleSymbol));
foreach (var symbol in pair.Value) {
var componentTypes = symbol.GetAttributes()
.Where (attr => (attr.AttributeClass?.GetNamespace() == "gaemstone.ECS"))
.Select(attr => attr.AttributeClass!.Name)
.Where (name => name.EndsWith("Attribute"))
.Select(name => name.Substring(0, name.Length - "Attribute".Length))
.SelectMany(name => Enum.TryParse<RegisterType>(name, out var type)
? new[] { type } : Enumerable.Empty<RegisterType>())
.ToList();
if (componentTypes.Count == 0) continue;
var isRelation = componentTypes.Contains(RegisterType.Relation);
if (isRelation && (componentTypes.Count == 2)) {
var other = componentTypes.Where(type => (type != RegisterType.Relation)).Single();
if (other is RegisterType.Component or RegisterType.Tag)
componentTypes.Remove(RegisterType.Relation);
else context.ReportDiagnostic(Diagnostic.Create(ComponentRelationInvalidType,
symbol.Locations.FirstOrDefault(), symbol.GetFullName(), other.ToString()));
}
if (componentTypes.Count >= 2)
context.ReportDiagnostic(Diagnostic.Create(ComponentMultipleTypes,
symbol.Locations.FirstOrDefault(), symbol.GetFullName(),
string.Join(", ", componentTypes.Select(s => $"[{s}]"))));
var componentType = componentTypes[0];
var addedEntities = new List<ITypeSymbol>();
var addedRelations = new List<(ITypeSymbol, ITypeSymbol)>();
foreach (var attr in symbol.GetAttributes())
for (var type = attr.AttributeClass; type != null; type = type.BaseType)
switch (type.GetFullName(true)) {
case "gaemstone.ECS.AddAttribute`1": addedEntities.Add(type.TypeArguments[0]); break;
case "gaemstone.ECS.AddAttribute`2": addedRelations.Add((type.TypeArguments[0], type.TypeArguments[1])); break;
}
module.Components.Add(new(symbol, module, componentType,
addedEntities, addedRelations));
}
}
foreach (var module in modules.Values) {
var sb = new StringBuilder();
sb.AppendLine($$"""
// <auto-generated/>
using gaemstone.ECS;
namespace {{ module.Namespace }};
""");
if (module.IsStatic) {
sb.AppendLine($$"""
public static partial class {{ module.Name }}
{
public static void RegisterComponents(World world)
{
var module = world.LookupByPathOrThrow({{ module.Path.ToStringLiteral() }});
""");
foreach (var c in module.Components) {
var @var = $"entity{c.Name}";
var path = (c.Path ?? c.Name).ToStringLiteral();
sb.AppendLine($$""" var {{ @var }} = world.LookupByPathOrThrow(module, {{ path }})""");
if (c.IsRelation) sb.AppendLine($$""" .Add<gaemstone.Doc.Relation>()""");
sb.AppendLine($$""" .CreateLookup<{{ c.Name }}>()""");
sb.Insert(sb.Length - 1, ";");
}
} else {
sb.AppendLine($$"""
public partial class {{ module.Name }}
: IModuleAutoRegisterComponents
{
public void RegisterComponents(EntityRef module)
{
var world = module.World;
""");
foreach (var c in module.Components) {
var @var = $"entity{c.Name}";
var path = (c.Path ?? c.Name).ToStringLiteral();
sb.AppendLine($$""" var {{ @var }} = world.New(module, {{ path }})""");
if (c.IsPublic) sb.AppendLine($$""" .Symbol("{{ c.Name }}")""");
if (c.IsRelation) sb.AppendLine($$""" .Add<gaemstone.Doc.Relation>()""");
if (c.IsTag) sb.AppendLine($$""" .Add<gaemstone.Flecs.Core.Tag>()""");
if (c.IsComponent) sb.AppendLine($$""" .Build().InitComponent<{{ c.Name }}>()""");
else sb.AppendLine($$""" .Build().CreateLookup<{{ c.Name }}>()""");
if (c.SingletonAddSelf) sb.AppendLine($$""" .Add<{{ c.Name }}>()""");
sb.Insert(sb.Length - 1, ";");
}
foreach (var c in module.Components) {
var @var = $"entity{c.Name}";
foreach (var e in c.EntitiesToAdd)
sb.AppendLine($$""" {{ @var }}.Add<{{ e.GetFullName() }}>();""");
foreach (var (r, t) in c.RelationsToAdd)
sb.AppendLine($$""" {{ @var }}.Add<{{ r.GetFullName() }}, {{ t.GetFullName() }}>();""");
}
}
sb.AppendLine($$"""
}
}
""");
context.AddSource($"{module.FullName}_Components.g.cs", sb.ToString());
}
}
private class ModuleInfo
{
public INamedTypeSymbol Symbol { get; }
public string Name => Symbol.Name;
public string FullName => Symbol.GetFullName();
public string Namespace => Symbol.GetNamespace()!;
public List<ComponentInfo> Components { get; } = new();
public string? Path { get; }
public bool IsPublic { get; }
public bool IsStatic => Symbol.IsStatic;
public ModuleInfo(INamedTypeSymbol symbol)
{
Symbol = symbol;
Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value as string;
IsPublic = symbol.HasAttribute("gaemstone.ECS.PublicAttribute");
}
}
private class ComponentInfo
{
public INamedTypeSymbol Symbol { get; }
public string Name => Symbol.Name;
public ModuleInfo Module { get; }
public RegisterType Type { get; }
public List<ITypeSymbol> EntitiesToAdd { get; }
public List<(ITypeSymbol, ITypeSymbol)> RelationsToAdd { get; }
public string? Path { get; }
public bool IsPublic { get; }
public bool IsRelation { get; }
public bool IsTag => (Type is RegisterType.Tag);
public bool IsComponent => (Type is RegisterType.Component
or RegisterType.Singleton);
public bool IsSingleton => (Type is RegisterType.Singleton);
public bool SingletonAddSelf { get; }
public ComponentInfo(INamedTypeSymbol symbol, ModuleInfo module, RegisterType type,
List<ITypeSymbol> entitiesToAdd, List<(ITypeSymbol, ITypeSymbol)> relationsToAdd)
{
Symbol = symbol;
Module = module;
Type = type;
EntitiesToAdd = entitiesToAdd;
RelationsToAdd = relationsToAdd;
Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value as string;
IsPublic = symbol.HasAttribute("gaemstone.ECS.PublicAttribute")
|| (Module.IsPublic && !symbol.HasAttribute("gaemstone.ECS.PrivateAttribute"));
IsRelation = symbol.HasAttribute("gaemstone.ECS.RelationAttribute");
SingletonAddSelf = IsSingleton
&& (symbol.GetAttribute("gaemstone.ECS.SingletonAttribute")!
.NamedArguments.FirstOrDefault() is not ("AutoAdd", TypedConstant typedConst)
|| (bool)typedConst.Value!);
}
}
private enum RegisterType
{
Entity,
Singleton,
Relation,
Component,
Tag,
}
}

@ -1,141 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace gaemstone.SourceGen.Generators;
[Generator]
public class ModuleGenerator
: ISourceGenerator
{
private static readonly DiagnosticDescriptor ModuleMayNotBeNested = new(
"gaem0001", "Module may not be nested",
"Type {0} marked with [Module] may not be a nested type",
nameof(ModuleGenerator), DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor ModuleMustBePartial = new(
"gaem0002", "Module must be partial",
"Type {0} marked with [Module] must be a partial type",
nameof(ModuleGenerator), DiagnosticSeverity.Error, true);
private static readonly DiagnosticDescriptor ModuleBuiltInMustHavePath = new(
"gaem0003", "Built-in module must have [Path]",
"Type {0} marked with [Module] is a built-in module (static), and therefore must have [Path] set",
nameof(ModuleGenerator), DiagnosticSeverity.Error, true);
public void Initialize(GeneratorInitializationContext context)
=> context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
private class SyntaxReceiver
: ISyntaxContextReceiver
{
public HashSet<INamedTypeSymbol> Symbols { get; }
= new(SymbolEqualityComparer.Default);
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is not AttributeSyntax attrNode) return;
var model = context.SemanticModel;
var attrType = model.GetTypeInfo(attrNode).Type!;
if (attrType.GetFullName(true) != "gaemstone.ECS.ModuleAttribute") return;
var memberNode = attrNode.Parent?.Parent!;
var memberSymbol = model.GetDeclaredSymbol(memberNode) as INamedTypeSymbol;
Symbols.Add(memberSymbol!);
}
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxContextReceiver is not SyntaxReceiver receiver) return;
foreach (var symbol in receiver.Symbols) {
var isNested = (symbol.ContainingType != null);
if (isNested)
context.ReportDiagnostic(Diagnostic.Create(ModuleMayNotBeNested,
symbol.Locations.FirstOrDefault(), symbol.GetFullName()));
var isPartial = symbol.DeclaringSyntaxReferences
.Any(r => (r.GetSyntax() as ClassDeclarationSyntax)?.Modifiers
.Any(t => t.IsKind(SyntaxKind.PartialKeyword)) ?? false);
if (!isPartial)
context.ReportDiagnostic(Diagnostic.Create(ModuleMustBePartial,
symbol.Locations.FirstOrDefault(), symbol.GetFullName()));
if (symbol.IsStatic && (symbol.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value == null))
context.ReportDiagnostic(Diagnostic.Create(ModuleBuiltInMustHavePath,
symbol.Locations.FirstOrDefault(), symbol.GetFullName()));
// Static classes can't implement interfaces.
if (symbol.IsStatic) continue;
var modulePath = GetModulePath(symbol).ToStringLiteral();
var dependencies = new List<string>();
foreach (var attr in symbol.GetAttributes())
for (var type = attr.AttributeClass; type != null; type = type.BaseType)
if ((type.GetFullName(true) == "gaemstone.ECS.AddAttribute`2")
&& type.TypeArguments[0].GetFullName() == "gaemstone.Flecs.Core.DependsOn")
dependencies.Add(GetModulePath(type.TypeArguments[1]));
var sb = new StringBuilder();
sb.AppendLine($$"""
// <auto-generated/>
using System.Collections.Generic;
using System.Linq;
using gaemstone.ECS;
namespace {{ symbol.GetNamespace() }};
public partial class {{ symbol.Name }}
: IModule
{
public static string ModulePath { get; }
= {{ modulePath }};
""");
if (dependencies.Count > 0) {
sb.AppendLine($$"""
public static IEnumerable<string> Dependencies { get; } = new[] {
""");
foreach (var dependency in dependencies)
sb.AppendLine($$""" {{ dependency.ToStringLiteral() }},""");
sb.AppendLine($$"""
};
""");
} else sb.AppendLine($$"""
public static IEnumerable<string> Dependencies { get; } = Enumerable.Empty<string>();
""");
sb.AppendLine($$"""
}
""");
context.AddSource($"{symbol.GetFullName()}_Module.g.cs", sb.ToString());
}
}
private static string GetModulePath(ISymbol module)
{
var path = module.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value as string;
var isAbsolute = (path?.FirstOrDefault() == '/');
if (isAbsolute) return path!;
var fullPath = module.GetFullName().Replace('.', '/');
if (path != null) {
var index = fullPath.LastIndexOf('/');
fullPath = $"{fullPath.Substring(0, index)}/{path}";
}
return $"/{fullPath}";
}
}

@ -0,0 +1,369 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using gaemstone.SourceGen.Structure;
using gaemstone.SourceGen.Utility;
using Microsoft.CodeAnalysis;
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" )}};
""");
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?
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.Path ?? e.Name).ToStringLiteral();
sb.AppendLine($"\t\tvar {@var} = world.New({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>()");
if (module.IsBuiltIn)
{
sb.AppendLine($"\t\t\t.Build().CreateLookup<{e.FullName}>()");
}
else
{
// TODO: if (e.IsPublic) sb.AppendLine($"\t\t\t.Symbol(\"{e.Name}\")");
// 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.Build()");
if (e.IsComponent) sb.Append($".InitComponent<{e.FullName}>()");
else sb.Append($".CreateLookup<{e.FullName}>()");
sb.AppendLine();
if (e.IsSingleton) sb.AppendLine($"\t\t\t.Add<{e.FullName}>()");
}
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.Path ?? 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) {
// TODO: Throw error if multiple Or terms appear next to each other.
foreach (var term in p.TermTypes) {
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(" }");
}
sb.Append(p.Kind switch {
ParameterKind.Or => ".Or",
ParameterKind.HasOr => ".Or.None",
ParameterKind.Has => ".None",
ParameterKind.Not => ".None.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()}>();");
}
}
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});");
sb.AppendLine("\t\t\tfor (var 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) {
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:
case ParameterKind.HasOr:
sb.Append("default");
break;
case ParameterKind.Or:
throw new NotSupportedException(
$"ParameterKind {param.Kind} not supported");
}
sb.Append(", ");
}
if (method.Parameters.Any())
sb.Length -= 2;
sb.AppendLine(");");
sb.AppendLine($$"""
}
}
""");
}
private void AppendTypeEntity(
StringBuilder sb, ModuleEntityInfo module,
ITypeSymbol type)
{
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
"Public",
"Private",
"Path",
"Add",
"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,78 @@
using System;
using System.Collections.Generic;
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? Path { get; }
// TODO: Rename this to something sensible, like [Symbol].
// public bool IsPublic { get; private set; }
// private bool IsPrivate { get; }
public List<INamedTypeSymbol> EntitiesToAdd { get; } = new();
public List<(INamedTypeSymbol, INamedTypeSymbol)> RelationsToAdd { get; } = new();
public bool HasEntitiesToAdd => (EntitiesToAdd.Count > 0) || (RelationsToAdd.Count > 0);
public BaseEntityInfo(ISymbol symbol)
: base(symbol)
{
Path = Get("Path")?.ConstructorArguments.FirstOrDefault().Value as string;
// IsPublic = Symbol.HasAttribute("gaemstone.ECS.PublicAttribute");
// IsPrivate = Symbol.HasAttribute("gaemstone.ECS.PrivateAttribute");
}
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);
}
// var moduleIsPublic = (Parent?.IsPublic == true);
// var inheritsPublic = (this is MethodEntityInfo); // Observers and systems don't inherit [Public] from their module.
// IsPublic = IsPublic || (moduleIsPublic && inheritsPublic && !IsPrivate);
// Add entities and relationships specified using [Add<...>] attributes.
foreach (var attr in Symbol.GetAttributes()) {
for (var attrType = attr.AttributeClass; attrType != null; attrType = attrType.BaseType) {
if (RelevantSymbolReceiver.ToRelevantAttributeName(attrType) != "Add") continue;
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) {
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");
}
}
}
}
}
}

@ -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,85 @@
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 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 (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++;
// TODO: Handle systems with [Source].
// TODO: Validate ObserverEvents.
}
}

@ -0,0 +1,60 @@
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 IsBuiltIn { get; }
// TODO: Support [Source].
public ModuleEntityInfo(ISymbol symbol)
: base(symbol)
{
var classDecl = (TypeDeclarationSyntax)Symbol.DeclaringSyntaxReferences.First().GetSyntax();
IsPartial = classDecl.Modifiers.Any(t => t.IsKind(SyntaxKind.PartialKeyword));
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 && (Path == 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,216 @@
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 HasTerm => (Kind != ParameterKind.Unique);
public bool HasField => HasTerm && !(Kind is ParameterKind.Has or ParameterKind.Not or ParameterKind.HasOr);
public ParameterInfo(ISymbol symbol)
: base(symbol)
{
Source = Get("Source")?.AttributeClass!.TypeArguments[0]
// If the type of the parameter has the [Singleton] attribute, use it as the default Source.
?? (Symbol.Type.HasAttribute("gaemstone.ECS.SingletonAttribute") ? Symbol.Type : null);
var typeFullName = Symbol.Type.GetFullName(FullNameStyle.Metadata);
if (UniqueParameters.TryGetValue(typeFullName, out var replacement))
{
Kind = ParameterKind.Unique;
TermTypes = Array.Empty<ITypeSymbol>();
UniqueReplacement = replacement;
}
else
{
var isHas = typeFullName.StartsWith("gaemstone.ECS.Has");
var isNot = typeFullName.StartsWith("gaemstone.ECS.Not");
var isOr = typeFullName.StartsWith("gaemstone.ECS.Or");
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 [ INamedTypeSymbol relation, INamedTypeSymbol target ]))
{
TermTypes = ImmutableList.Create(new Pair(relation, target));
FieldType = null;
}
else
{
TermTypes = args.ToImmutableList();
FieldType = IsNullable ? args[0] : type;
}
}
else
{
TermTypes = ImmutableList.Create(Symbol.Type);
FieldType = Symbol.Type;
}
Kind = (isHas && isOr) ? ParameterKind.HasOr
: isHas ? ParameterKind.Has
: isOr ? ParameterKind.Or
: 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 = Kind is ParameterKind.Has or ParameterKind.HasOr
or ParameterKind.Or 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)
Or, // Only one of multiple terms is required
HasOr, // Both Has and Or at the same time
}
// 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,76 @@
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; }
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");
}
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) {
// 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;
}
// Singletons are special kinds of components.
if (IsSingleton) IsComponent = true;
}
}

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

@ -7,45 +7,112 @@ namespace gaemstone.SourceGen.Utility;
public static class SymbolExtensions
{
public static string GetFullName(this ISymbol symbol, bool metadata = false)
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, metadata);
AppendFullName(symbol, builder, style);
return builder.ToString();
}
public static void AppendFullName(this ISymbol symbol, StringBuilder builder, bool metadata = false)
public static void AppendFullName(
this ISymbol symbol, StringBuilder builder,
FullNameStyle style = FullNameStyle.Full)
{
if ((symbol.ContainingSymbol is ISymbol parent)
&& ((parent as INamespaceSymbol)?.IsGlobalNamespace != true))
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, metadata);
builder.Append(((parent is ITypeSymbol) && metadata) ? '+' : '.');
AppendFullName(parent, builder, style);
builder.Append((withMetadata && (parent is ITypeSymbol)) ? '+' : '.');
}
if (!metadata && (symbol is INamedTypeSymbol typeSymbol) && typeSymbol.IsGenericType) {
if ((symbol is INamedTypeSymbol { IsGenericType: true } typeSymbol)
&& !(withGeneric && withMetadata))
{
var length = symbol.MetadataName.IndexOf('`');
builder.Append(symbol.MetadataName, 0, length);
builder.Append('<');
foreach (var genericType in typeSymbol.TypeArguments) {
AppendFullName(genericType, builder, true);
builder.Append(',');
if (withGeneric) {
builder.Append('<');
foreach (var arg in typeSymbol.TypeArguments) {
AppendFullName(arg, builder, style);
builder.Append(',');
}
builder.Length--; // Remove the last ',' character.
builder.Append('>');
}
builder.Length--; // Remove the last ',' character.
builder.Append('>');
} else builder.Append(symbol.MetadataName);
}
else builder.Append(symbol.MetadataName);
}
public static string? GetNamespace(this ISymbol symbol)
=> symbol.ContainingNamespace?.GetFullName();
public static bool HasAttribute(this ISymbol symbol, string attrMetadataName)
=> symbol.GetAttributes().Any(attr => attr.AttributeClass!.GetFullName(true) == attrMetadataName);
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? GetAttribute(this ISymbol symbol, string attrMetadataName)
=> symbol.GetAttributes().FirstOrDefault(attr => attr.AttributeClass!.GetFullName(true) == attrMetadataName);
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>>
}
}

@ -9,6 +9,7 @@
</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>

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

@ -3,16 +3,6 @@ using static gaemstone.Flecs.Core;
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)]
public class PathAttribute : Attribute
{
public string Value { get; }
public PathAttribute(string value) => Value = value;
}
/// <summary>
/// <p>
/// Components marked with this attribute are automatically registered with
@ -24,40 +14,44 @@ public class PathAttribute : Attribute
/// components, except ones marked with <see cref="PrivateAttribute"/>.
/// </p>
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct
| AttributeTargets.Enum | AttributeTargets.Method)]
public class PublicAttribute : Attribute { }
/// <seealso cref="PublicAttribute"/>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct
| AttributeTargets.Enum | AttributeTargets.Method)]
public class PrivateAttribute : Attribute { }
// 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="EntityBase.Add{T}"/>.
/// automatically registered. Equivalent to <see cref="EntityBuilder.Add{T}"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)]
[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="EntityBase.Add{TRelation, TTarget}"/>.
/// automatically registered. Equivalent to <see cref="EntityBuilder.Add{TRelation, TTarget}"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct
| AttributeTargets.Enum | AttributeTargets.Method,
AllowMultiple = true)]
public class AddAttribute<TRelation, TTarget> : Attribute { }
/// <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> { }

@ -1,27 +1,33 @@
using System;
using static gaemstone.Flecs.Core;
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 { }
/// <seealso cref="Tag"/>
// 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 { }
/// <seealso cref="Component"/>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class ComponentAttribute : Attribute { }
/// <summary>
/// A singleton is a single instance of a tag or component that can be retrieved
/// 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>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class SingletonAttribute : Attribute
{ public bool AutoAdd { get; init; } = true; }
public class SingletonAttribute : ComponentAttribute { }
/// <summary>
/// Marked entity represents a relationship type.

@ -3,25 +3,29 @@ using System.Collections.Generic;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Class)]
public class ModuleAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class ModuleAttribute : SingletonAttribute { }
public interface IModuleInitializer
{
void Initialize(EntityRef module);
}
[AttributeUsage(AttributeTargets.Class)]
public class BuiltInAttribute : Attribute { }
// The following will be implemented on [Module]
// types automatically for you by source generators.
/// <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 ModulePath { get; }
static abstract string Path { get; }
static abstract bool IsBuiltIn { get; }
static abstract IEnumerable<string> Dependencies { get; }
static abstract IReadOnlyList<string> Dependencies { get; }
static abstract void Initialize<TContext>(Entity<TContext> module);
}
public interface IModuleAutoRegisterComponents
public interface IModuleInitializer
{
void RegisterComponents(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,8 +2,8 @@ using gaemstone.ECS;
namespace gaemstone.Flecs;
[Module, Path("/flecs/core")]
public static partial class Core
[BuiltIn, Module, Path("/flecs/core")]
public partial class Core
{
// Entity Tags
@ -17,27 +17,23 @@ public static partial class Core
[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 { }
[Entity, Path("*")] public struct Wildcard { }
[Entity, Path("_")] public struct Any { }
[Entity] public struct This { }
[Entity, Path("$")] public struct Variable { }
[Entity] public struct Flag { }
// Entity Relationships
[Relation, Tag] public struct IsA { }
[Relation, Tag] public struct ChildOf { }
[Relation, Tag] public struct DependsOn { }
// Component / Relationship Properties
[Tag] public struct Transitive { }
@ -52,6 +48,27 @@ public static partial class Core
[Relation, Tag] public struct With { }
[Tag] public struct OneOf { }
// Observer Events
[Entity] public struct OnAdd { }
[Entity] public struct OnRemove { }
[Entity] public struct OnSet { }
[Entity] public struct UnSet { }
[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 +92,7 @@ public static partial 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 partial class DeletionEvent
{
[Relation, Tag] public struct OnDelete { }
[Relation, Tag] public struct OnDeleteTarget { }
}
[Module, Path("/flecs/core")]
public static partial 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 partial 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 partial 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 partial 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 partial 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 partial 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")]
[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")]
[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,30 +1,31 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Collections.Immutable;
using System.Linq;
using gaemstone.ECS;
using gaemstone.Utility;
using static gaemstone.Flecs.Core;
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 EntityRef Register<T>()
where T : class, IModule, new()
public Entity<TContext> Register<T>()
where T : IModule
{
var moduleType = typeof(T);
if (moduleType.IsGenericType) throw new Exception(
$"Module {moduleType} must be a non-generic class");
// if (!typeof(T).IsAssignableTo(typeof(IModule))) throw new ArgumentException(
// $"The specified type {typeof(T)} does not implement IModule", nameof(T));
var module = new ModuleInfo<T>(Universe);
_modules.Add(module.Entity, module);
@ -32,87 +33,97 @@ public class ModuleManager
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.Entity.GetFullPath()} ...");
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);
}
}
internal abstract class ModuleInfo
public interface IModuleInfo
{
public abstract EntityRef Entity { get; }
public abstract bool IsActive { get; }
Entity<TContext> Entity { get; }
IReadOnlyCollection<ModuleDependency> Dependencies { get; }
bool IsInitialized { get; }
void Enable();
}
public HashSet<ModuleInfo> MetDependencies { get; } = new();
public HashSet<Entity> UnmetDependencies { get; } = new();
// IEnumerable implementation
public IEnumerator<IModuleInfo> GetEnumerator() => _modules.Values.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public abstract void Enable();
}
internal class ModuleInfo<T> : ModuleInfo
where T : IModule, new()
public class ModuleDependency
{
public override EntityRef Entity { get; }
public override bool IsActive => (Instance != null);
public T? Instance { get; internal set; }
public Entity<TContext> Entity { get; }
public IModuleInfo? Info { get; internal set; }
public bool IsDependencyMet { get; internal set; }
public ModuleInfo(Universe universe)
public ModuleDependency(Entity<TContext> entity,
IModuleInfo? info = null, bool isDependencyMet = false)
{
var builder = universe.New(T.ModulePath).Add<Module>();
foreach (var dependsPath in T.Dependencies) {
var dependency = universe.LookupByPath(dependsPath) ??
universe.New(dependsPath).Add<Module>().Disable().Build();
Entity = entity;
Info = info;
IsDependencyMet = isDependencyMet;
}
}
var depModule = universe.Modules.Lookup(dependency);
if (depModule?.IsActive == true) MetDependencies.Add(depModule);
else { UnmetDependencies.Add(dependency); builder.Disable(); }
internal class ModuleInfo<T> : IModuleInfo
where T : IModule
{
public Entity<TContext> Entity { get; }
public IReadOnlyCollection<ModuleDependency> Dependencies { get; }
public bool IsInitialized { get; private set; }
builder.Add<DependsOn>(dependency);
public ModuleInfo(Universe<TContext> universe)
{
var world = universe.World;
var builder = world.New(T.Path);
var deps = new List<ModuleDependency>();
if (!T.IsBuiltIn) {
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 Module set.
for (var p = Entity.Parent; p != null; p = p.Parent)
p.Add<Module>();
if (!T.IsBuiltIn)
// 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>();
}
public override void Enable()
public void Enable()
{
Entity.Enable();
Instance = new T();
(Instance as IModuleAutoRegisterComponents)?.RegisterComponents(Entity);
(Instance as IModuleInitializer)?.Initialize(Entity);
RegisterMethods(Instance);
}
private void RegisterMethods(object? instance)
{
var world = Entity.World;
foreach (var method in typeof(T).GetMethods(
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance
)) {
if (method.Has<SystemAttribute>())
world.InitSystem(instance, method).ChildOf(Entity);
if (method.Has<ObserverAttribute>())
world.InitObserver(instance, method).ChildOf(Entity);
}
T.Initialize(Entity);
IsInitialized = true;
}
}
}

@ -2,32 +2,25 @@ 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 [Relation] tag, since it will be added to some Flecs types.
New("/gaemstone/Doc/Relation").Build().CreateLookup<Doc.Relation>();
World.New("/gaemstone/Doc/Relation").Build()
.CreateLookup<Doc.Relation>();
// Bootstrap built-in (static) modules from Flecs.
// These methods are generated by gaemstone.SourceGen.
Flecs.Core .RegisterComponents(this);
Flecs.DeletionEvent .RegisterComponents(this);
Flecs.DeletionBehavior.RegisterComponents(this);
Flecs.Doc .RegisterComponents(this);
Flecs.ObserverEvent .RegisterComponents(this);
Flecs.Pipeline .RegisterComponents(this);
Flecs.SystemPhase .RegisterComponents(this);
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,6 +6,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <!-- -->
</PropertyGroup>
<ItemGroup>

Loading…
Cancel
Save