diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 5adaf07..f7a0c72 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,6 +1,7 @@
{
"version": "2.0.0",
- "tasks": [{
+ "tasks": [
+ {
"label": "build",
"command": "dotnet",
"type": "process",
diff --git a/gaemstone.sln b/gaemstone.sln
index 45da22d..98916b7 100644
--- a/gaemstone.sln
+++ b/gaemstone.sln
@@ -1,17 +1,19 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{599B7E67-7F73-4301-A9C6-E8DF286A2625}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone", "src\gaemstone\gaemstone.csproj", "{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.Bloxel", "src\gaemstone.Bloxel\gaemstone.Bloxel.csproj", "{7A80D49C-6768-4803-9866-691C7AD80817}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.Client", "src\gaemstone.Client\gaemstone.Client.csproj", "{67B9B2D4-FCB7-4642-B584-A0186CAB2969}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.ECS", "src\gaemstone.ECS\gaemstone.ECS.csproj", "{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone", "src\gaemstone\gaemstone.csproj", "{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.SourceGen", "src\gaemstone.SourceGen\gaemstone.SourceGen.csproj", "{07963390-747C-4FBF-A727-A33E024F4E13}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Immersion", "src\Immersion\Immersion.csproj", "{4B9C20F6-0793-4E85-863A-2E14230A028F}"
EndProject
@@ -24,6 +26,10 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.Build.0 = Release|Any CPU
{7A80D49C-6768-4803-9866-691C7AD80817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A80D49C-6768-4803-9866-691C7AD80817}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A80D49C-6768-4803-9866-691C7AD80817}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -36,20 +42,21 @@ Global
{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}.Release|Any CPU.Build.0 = Release|Any CPU
- {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {07963390-747C-4FBF-A727-A33E024F4E13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {07963390-747C-4FBF-A727-A33E024F4E13}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {07963390-747C-4FBF-A727-A33E024F4E13}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {07963390-747C-4FBF-A727-A33E024F4E13}.Release|Any CPU.Build.0 = Release|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4B9C20F6-0793-4E85-863A-2E14230A028F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
+ {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{7A80D49C-6768-4803-9866-691C7AD80817} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{67B9B2D4-FCB7-4642-B584-A0186CAB2969} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
- {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
+ {07963390-747C-4FBF-A727-A33E024F4E13} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
{4B9C20F6-0793-4E85-863A-2E14230A028F} = {599B7E67-7F73-4301-A9C6-E8DF286A2625}
EndGlobalSection
EndGlobal
diff --git a/src/Immersion/Immersion.csproj b/src/Immersion/Immersion.csproj
index 732fd8b..75cfde8 100644
--- a/src/Immersion/Immersion.csproj
+++ b/src/Immersion/Immersion.csproj
@@ -16,6 +16,8 @@
+
diff --git a/src/Immersion/ManagedComponentTest.cs b/src/Immersion/ManagedComponentTest.cs
new file mode 100644
index 0000000..931717d
--- /dev/null
+++ b/src/Immersion/ManagedComponentTest.cs
@@ -0,0 +1,24 @@
+using System;
+using gaemstone.ECS;
+
+namespace Immersion;
+
+[Module]
+public partial class ManagedComponentTest
+{
+ [Component]
+ public class BigManagedData
+ {
+ public readonly byte[] BigArray = new byte[1024 * 1024];
+ }
+
+ [System]
+ public static void CreateLotsOfGarbageData(World world)
+ {
+ var game = world.LookupByPathOrThrow("/Game");
+ game.Remove();
+ game.Set(new BigManagedData());
+ // This is to make sure the number of objects kept alive stays stable.
+ Console.WriteLine(ReferenceHandle.NumActiveHandles);
+ }
+}
diff --git a/src/Immersion/ObserverTest.cs b/src/Immersion/ObserverTest.cs
index 48d1582..89ecdb5 100644
--- a/src/Immersion/ObserverTest.cs
+++ b/src/Immersion/ObserverTest.cs
@@ -8,7 +8,7 @@ namespace Immersion;
[Module]
[DependsOn]
[DependsOn]
-public class ObserverTest
+public partial class ObserverTest
{
[Observer]
[Expression("[in] Chunk, [none] (MeshHandle, *)")]
diff --git a/src/Immersion/Program.cs b/src/Immersion/Program.cs
index 3a234ea..4674b60 100644
--- a/src/Immersion/Program.cs
+++ b/src/Immersion/Program.cs
@@ -35,6 +35,7 @@ window.Initialize();
window.Center();
// universe.Modules.Register();
+// universe.Modules.Register();
universe.Modules.Register();
universe.Modules.Register();
diff --git a/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs
index be81e2d..7d9b9c3 100644
--- a/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs
+++ b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs
@@ -10,7 +10,7 @@ using static gaemstone.Client.Systems.Windowing;
namespace gaemstone.Bloxel.Client.Systems;
[Module]
-public class ChunkMeshGenerator
+public partial class ChunkMeshGenerator
{
private const int StartingCapacity = 1024;
diff --git a/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs
index b3ea9d0..6b2a9da 100644
--- a/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs
+++ b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs
@@ -8,7 +8,7 @@ namespace gaemstone.Bloxel.Systems;
[Module]
[DependsOn]
-public class BasicWorldGenerator
+public partial class BasicWorldGenerator
{
private readonly FastNoiseLite _noise;
private readonly Random _rnd = new();
diff --git a/src/gaemstone.Bloxel/Systems/SurfaceGrassGenerator.cs.disabled b/src/gaemstone.Bloxel/Systems/SurfaceGrassGenerator.cs.disabled
index a615398..7c7835f 100644
--- a/src/gaemstone.Bloxel/Systems/SurfaceGrassGenerator.cs.disabled
+++ b/src/gaemstone.Bloxel/Systems/SurfaceGrassGenerator.cs.disabled
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace gaemstone.Bloxel.WorldGen;
// FIXME: There is an issue with this generator where it doesn't generate grass and dirt properly.
-public class SurfaceGrassGenerator
+public partial class SurfaceGrassGenerator
: IWorldGenerator
{
public static readonly string IDENTIFIER = nameof(SurfaceGrassGenerator);
diff --git a/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj b/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj
index 36abede..26ad828 100644
--- a/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj
+++ b/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj
@@ -15,6 +15,8 @@
+
diff --git a/src/gaemstone.Client/Components/CameraComponents.cs b/src/gaemstone.Client/Components/CameraComponents.cs
index 0518c51..d9d5797 100644
--- a/src/gaemstone.Client/Components/CameraComponents.cs
+++ b/src/gaemstone.Client/Components/CameraComponents.cs
@@ -4,7 +4,7 @@ using gaemstone.ECS;
namespace gaemstone.Client.Components;
[Module]
-public class CameraComponents
+public partial class CameraComponents
{
[Component]
public struct Camera
diff --git a/src/gaemstone.Client/Components/InputComponents.cs b/src/gaemstone.Client/Components/InputComponents.cs
index d0b137f..1f4a0a1 100644
--- a/src/gaemstone.Client/Components/InputComponents.cs
+++ b/src/gaemstone.Client/Components/InputComponents.cs
@@ -5,17 +5,17 @@ using gaemstone.ECS;
namespace gaemstone.Client.Components;
[Module]
-public class InputComponents
+public partial class InputComponents
{
- [Symbol, Path("/Input")]
+ [Symbol, Entity, Path("/Input")]
[Add]
public struct Input { }
- [Symbol, Path("/Input/Mouse")]
+ [Symbol, Entity, Path("/Input/Mouse")]
[Add]
public struct Mouse { }
- [Symbol, Path("/Input/Keyboard")]
+ [Symbol, Entity, Path("/Input/Keyboard")]
[Add]
public struct Keyboard { }
diff --git a/src/gaemstone.Client/Components/RenderingComponents.cs b/src/gaemstone.Client/Components/RenderingComponents.cs
index bd605e1..c6c0808 100644
--- a/src/gaemstone.Client/Components/RenderingComponents.cs
+++ b/src/gaemstone.Client/Components/RenderingComponents.cs
@@ -6,7 +6,7 @@ using Silk.NET.OpenGL;
namespace gaemstone.Client.Components;
[Module]
-public class RenderingComponents
+public partial class RenderingComponents
{
[Symbol, Component]
public readonly struct MeshHandle
diff --git a/src/gaemstone.Client/Components/ResourceComponents.cs b/src/gaemstone.Client/Components/ResourceComponents.cs
index 148a3e7..5157ded 100644
--- a/src/gaemstone.Client/Components/ResourceComponents.cs
+++ b/src/gaemstone.Client/Components/ResourceComponents.cs
@@ -3,7 +3,7 @@ using gaemstone.ECS;
namespace gaemstone.Client.Components;
[Module]
-public class ResourceComponents
+public partial class ResourceComponents
{
[Symbol, Tag]
public struct Resource { }
diff --git a/src/gaemstone.Client/Systems/EntityInspector.cs b/src/gaemstone.Client/Systems/EntityInspector.cs
index e65bdec..622ec60 100644
--- a/src/gaemstone.Client/Systems/EntityInspector.cs
+++ b/src/gaemstone.Client/Systems/EntityInspector.cs
@@ -15,7 +15,7 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn]
-public class EntityInspector
+public partial class EntityInspector
: IModuleInitializer
{
[Tag]
diff --git a/src/gaemstone.Client/Systems/FreeCameraController.cs b/src/gaemstone.Client/Systems/FreeCameraController.cs
index e988a76..5b4fb7c 100644
--- a/src/gaemstone.Client/Systems/FreeCameraController.cs
+++ b/src/gaemstone.Client/Systems/FreeCameraController.cs
@@ -12,7 +12,7 @@ namespace gaemstone.Client.Systems;
[DependsOn]
[DependsOn]
[DependsOn]
-public class FreeCameraController
+public partial class FreeCameraController
{
[Component]
public struct CameraController
diff --git a/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs b/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs
index 6e8ca98..0534b30 100644
--- a/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs
+++ b/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs
@@ -7,7 +7,7 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn]
-public class ImGuiDemoWindow
+public partial class ImGuiDemoWindow
{
private bool _isOpen = false;
diff --git a/src/gaemstone.Client/Systems/ImGuiInputDebug.cs b/src/gaemstone.Client/Systems/ImGuiInputDebug.cs
index a8cf9d3..ee23dc7 100644
--- a/src/gaemstone.Client/Systems/ImGuiInputDebug.cs
+++ b/src/gaemstone.Client/Systems/ImGuiInputDebug.cs
@@ -15,7 +15,7 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn]
[DependsOn]
-public class ImGuiInputDebug
+public partial class ImGuiInputDebug
{
private bool _isOpen = false;
diff --git a/src/gaemstone.Client/Systems/ImGuiManager.cs b/src/gaemstone.Client/Systems/ImGuiManager.cs
index 7d45a8c..c9a4094 100644
--- a/src/gaemstone.Client/Systems/ImGuiManager.cs
+++ b/src/gaemstone.Client/Systems/ImGuiManager.cs
@@ -19,7 +19,7 @@ namespace gaemstone.Client.Systems;
[DependsOn]
[DependsOn]
[DependsOn]
-public class ImGuiManager
+public partial class ImGuiManager
{
[Entity, Add]
[DependsOn]
@@ -29,7 +29,7 @@ public class ImGuiManager
[DependsOn]
public struct ImGuiRenderPhase { }
- [Component, Singleton(AutoAdd = false)]
+ [Singleton(AutoAdd = false)]
public class ImGuiData
{
public ImGuiController Controller { get; }
diff --git a/src/gaemstone.Client/Systems/InputManager.cs b/src/gaemstone.Client/Systems/InputManager.cs
index edce533..125210d 100644
--- a/src/gaemstone.Client/Systems/InputManager.cs
+++ b/src/gaemstone.Client/Systems/InputManager.cs
@@ -13,7 +13,7 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn]
[DependsOn]
-public class InputManager
+public partial class InputManager
{
[Component] public record class InputContext(IInputContext Value) { }
diff --git a/src/gaemstone.Client/Systems/MeshManager.cs b/src/gaemstone.Client/Systems/MeshManager.cs
index 6eda563..3966d25 100644
--- a/src/gaemstone.Client/Systems/MeshManager.cs
+++ b/src/gaemstone.Client/Systems/MeshManager.cs
@@ -14,7 +14,7 @@ namespace gaemstone.Client.Systems;
[DependsOn]
[DependsOn]
[DependsOn]
-public class MeshManager
+public partial class MeshManager
{
private const uint PositionAttribIndex = 0;
private const uint NormalAttribIndex = 1;
diff --git a/src/gaemstone.Client/Systems/Renderer.cs b/src/gaemstone.Client/Systems/Renderer.cs
index 7259c6d..e50499a 100644
--- a/src/gaemstone.Client/Systems/Renderer.cs
+++ b/src/gaemstone.Client/Systems/Renderer.cs
@@ -19,7 +19,7 @@ namespace gaemstone.Client.Systems;
[DependsOn]
[DependsOn]
[DependsOn]
-public class Renderer
+public partial class Renderer
{
private uint _program;
private int _cameraMatrixUniform;
@@ -83,7 +83,7 @@ public class Renderer
bounds.Width, -bounds.Height,
camera.NearPlane, camera.FarPlane)
: Matrix4x4.CreatePerspectiveFieldOfView(
- camera.FieldOfView * MathF.PI / 180, // Degrees => Radians
+ camera.FieldOfView * MathF.PI / 180, // Degrees => Radians
(float)bounds.Width / bounds.Height, // Aspect Ratio
camera.NearPlane, camera.FarPlane);
@@ -94,9 +94,9 @@ public class Renderer
GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.M11);
_renderEntityRule ??= new(universe, new("""
- GlobalTransform,
- (Mesh, $mesh), MeshHandle($mesh),
- ?(Texture, $tex), ?TextureHandle($tex)
+ [in] GlobalTransform,
+ (Mesh, $mesh), [in] MeshHandle($mesh),
+ ?(Texture, $tex), [in] ?TextureHandle($tex)
"""));
foreach (var iter in _renderEntityRule.Iter()) {
var transforms = iter.Field(1);
@@ -105,16 +105,16 @@ public class Renderer
var textures = iter.FieldOrEmpty(5);
for (var i = 0; i < iter.Count; i++) {
- var rTransform = transforms[i];
- var mesh = meshes[i];
+ var transform = transforms[i];
+ var mesh = meshes[i];
// var hasTexture = (texPairs.Length > 0);
- var texture = textures.GetOrNull(i);
+ var texture = textures.GetOrNull(i);
// If entity has Texture, bind it now.
if (texture.HasValue) GL.BindTexture(texture.Value.Target, texture.Value.Handle);
// Draw the mesh.
- GL.UniformMatrix4(_modelMatrixUniform, 1, false, in rTransform.Value.M11);
+ GL.UniformMatrix4(_modelMatrixUniform, 1, false, in transform.Value.M11);
GL.BindVertexArray(mesh.Handle);
if (!mesh.IsIndexed) GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)mesh.Count);
else unsafe { GL.DrawElements(PrimitiveType.Triangles, (uint)mesh.Count, DrawElementsType.UnsignedShort, null); }
diff --git a/src/gaemstone.Client/Systems/TextureManager.cs b/src/gaemstone.Client/Systems/TextureManager.cs
index 7a059aa..ab2eabd 100644
--- a/src/gaemstone.Client/Systems/TextureManager.cs
+++ b/src/gaemstone.Client/Systems/TextureManager.cs
@@ -15,7 +15,7 @@ namespace gaemstone.Client.Systems;
[DependsOn]
[DependsOn]
[DependsOn]
-public class TextureManager
+public partial class TextureManager
{
[Observer]
public static void OnCanvasSet(Canvas canvas)
diff --git a/src/gaemstone.Client/Systems/Windowing.cs b/src/gaemstone.Client/Systems/Windowing.cs
index 24de8c9..19e3cd7 100644
--- a/src/gaemstone.Client/Systems/Windowing.cs
+++ b/src/gaemstone.Client/Systems/Windowing.cs
@@ -7,7 +7,7 @@ using Silk.NET.Windowing;
namespace gaemstone.Client.Systems;
[Module]
-public class Windowing
+public partial class Windowing
{
[Component]
public class Canvas
diff --git a/src/gaemstone.Client/gaemstone.Client.csproj b/src/gaemstone.Client/gaemstone.Client.csproj
index 9965755..57f8f39 100644
--- a/src/gaemstone.Client/gaemstone.Client.csproj
+++ b/src/gaemstone.Client/gaemstone.Client.csproj
@@ -15,6 +15,8 @@
+
diff --git a/src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs b/src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs
new file mode 100644
index 0000000..e744a70
--- /dev/null
+++ b/src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs
@@ -0,0 +1,256 @@
+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;
+
+[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> Modules { get; }
+ = new(SymbolEqualityComparer.Default);
+ public HashSet 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(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(
+ 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(name, out var type)
+ ? new[] { type } : Enumerable.Empty())
+ .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();
+ 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($$"""
+ //
+ 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()""");
+ 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.IsSymbol) sb.AppendLine($$""" .Symbol("{{ c.Name }}")""");
+ if (c.IsRelation) sb.AppendLine($$""" .Add()""");
+ if (c.IsTag) sb.AppendLine($$""" .Add()""");
+ 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 Components { get; } = new();
+ public string? Path { get; }
+ public bool IsStatic => Symbol.IsStatic;
+
+ public ModuleInfo(INamedTypeSymbol symbol)
+ {
+ Symbol = symbol;
+ Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")?
+ .ConstructorArguments.FirstOrDefault().Value as string;
+ }
+ }
+
+ private class ComponentInfo
+ {
+ public INamedTypeSymbol Symbol { get; }
+ public string Name => Symbol.Name;
+
+ public ModuleInfo Module { get; }
+ public RegisterType Type { get; }
+ public List EntitiesToAdd { get; }
+ public List<(ITypeSymbol, ITypeSymbol)> RelationsToAdd { get; }
+
+ public string? Path { get; }
+ public bool IsSymbol { get; } // TODO: Get rid of this.
+ 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 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;
+ IsSymbol = symbol.HasAttribute("gaemstone.ECS.SymbolAttribute");
+ 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,
+ }
+}
diff --git a/src/gaemstone.SourceGen/Generators/ModuleGenerator.cs b/src/gaemstone.SourceGen/Generators/ModuleGenerator.cs
new file mode 100644
index 0000000..f3b8f30
--- /dev/null
+++ b/src/gaemstone.SourceGen/Generators/ModuleGenerator.cs
@@ -0,0 +1,76 @@
+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.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 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()));
+ }
+ }
+}
diff --git a/src/gaemstone.SourceGen/Utility/CollectionExtensions.cs b/src/gaemstone.SourceGen/Utility/CollectionExtensions.cs
new file mode 100644
index 0000000..768fca3
--- /dev/null
+++ b/src/gaemstone.SourceGen/Utility/CollectionExtensions.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace gaemstone.SourceGen.Utility;
+
+public static class CollectionExtensions
+{
+ public static void Deconstruct(
+ this KeyValuePair kvp, out TKey key, out TValue value)
+ { key = kvp.Key; value = kvp.Value; }
+}
diff --git a/src/gaemstone.SourceGen/Utility/SymbolExtensions.cs b/src/gaemstone.SourceGen/Utility/SymbolExtensions.cs
new file mode 100644
index 0000000..345fa99
--- /dev/null
+++ b/src/gaemstone.SourceGen/Utility/SymbolExtensions.cs
@@ -0,0 +1,51 @@
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace gaemstone.SourceGen.Utility;
+
+public static class SymbolExtensions
+{
+ public static string GetFullName(this ISymbol symbol, bool metadata = false)
+ {
+ var builder = new StringBuilder();
+ AppendFullName(symbol, builder, metadata);
+ return builder.ToString();
+ }
+
+ public static void AppendFullName(this ISymbol symbol, StringBuilder builder, bool metadata = false)
+ {
+ if ((symbol.ContainingSymbol is ISymbol parent)
+ && ((parent as INamespaceSymbol)?.IsGlobalNamespace != true))
+ {
+ AppendFullName(parent, builder, metadata);
+ builder.Append(((parent is ITypeSymbol) && metadata) ? '+' : '.');
+ }
+
+ if (!metadata && (symbol is INamedTypeSymbol typeSymbol) && typeSymbol.IsGenericType) {
+ 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(',');
+ }
+ builder.Length--; // Remove the last ',' character.
+ builder.Append('>');
+ } else builder.Append(symbol.MetadataName);
+ }
+
+ public static string? GetNamespace(this ISymbol symbol)
+ => symbol.ContainingNamespace?.GetFullName();
+
+ public static bool HasAttribute(this ISymbol symbol, string attrMetadataName)
+ => symbol.GetAttributes().Any(attr => attr.AttributeClass!.GetFullName(true) == attrMetadataName);
+
+ public static AttributeData? GetAttribute(this ISymbol symbol, string attrMetadataName)
+ => symbol.GetAttributes().FirstOrDefault(attr => attr.AttributeClass!.GetFullName(true) == attrMetadataName);
+
+ public static string ToStringLiteral(this string? input)
+ => (input != null) ? SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression,
+ SyntaxFactory.Literal(input)).ToFullString() : "null";
+}
diff --git a/src/gaemstone.SourceGen/gaemstone.SourceGen.csproj b/src/gaemstone.SourceGen/gaemstone.SourceGen.csproj
new file mode 100644
index 0000000..a02c609
--- /dev/null
+++ b/src/gaemstone.SourceGen/gaemstone.SourceGen.csproj
@@ -0,0 +1,19 @@
+
+
+
+ preview
+ netstandard2.0
+ true
+ disable
+ enable
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
diff --git a/src/gaemstone/Components/TransformComponents.cs b/src/gaemstone/Components/TransformComponents.cs
index 5ef5736..b0d030c 100644
--- a/src/gaemstone/Components/TransformComponents.cs
+++ b/src/gaemstone/Components/TransformComponents.cs
@@ -4,7 +4,7 @@ using gaemstone.ECS;
namespace gaemstone.Components;
[Module]
-public class TransformComponents
+public partial class TransformComponents
{
[Symbol, Component]
public struct GlobalTransform
diff --git a/src/gaemstone/Doc.cs b/src/gaemstone/Doc.cs
index 6534099..3e66ea2 100644
--- a/src/gaemstone/Doc.cs
+++ b/src/gaemstone/Doc.cs
@@ -5,7 +5,7 @@ using static gaemstone.Flecs.Core;
namespace gaemstone;
[Module]
-public class Doc
+public partial class Doc
{
[Tag]
public struct DisplayType { }
diff --git a/src/gaemstone/ECS/Attributes.cs b/src/gaemstone/ECS/Attributes.cs
deleted file mode 100644
index abeb0fa..0000000
--- a/src/gaemstone/ECS/Attributes.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-using System;
-using static gaemstone.Flecs.Core;
-
-namespace gaemstone.ECS;
-
-///
-/// When present on an attribute attached to a type that's part of a module
-/// being registered automatically through ,
-/// an entity is automatically created and
-/// called on it, meaning it can be looked up using .
-///
-public interface ICreateEntityAttribute { }
-
-[AttributeUsage(AttributeTargets.Struct)]
-public class EntityAttribute : Attribute, ICreateEntityAttribute { }
-
-///
-/// Use a custom name or path for this entity instead of the type's name.
-///
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
-public class PathAttribute : Attribute, ICreateEntityAttribute
-{
- public string Value { get; }
- public PathAttribute(string value) => Value = value;
-}
-
-///
-/// Register the entity under a globally unique symbol.
-/// Uses the type's name by default.
-///
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
-public class SymbolAttribute : Attribute, ICreateEntityAttribute
-{
- public string? Value { get; }
- public SymbolAttribute() { }
- public SymbolAttribute(string value) => Value = value;
-}
-
-///
-/// A singleton is a single instance of a tag or component that can be retrieved
-/// without explicitly specifying an entity in a query, where it is equivalent
-/// to with itself as the generic type parameter.
-///
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
-public class SingletonAttribute : Attribute, ICreateEntityAttribute
- { public bool AutoAdd { get; init; } = true; }
-
-
-///
-/// Marked entity automatically has the specified entity added to it when
-/// automatically registered. Equivalent to .
-///
-public class AddAttribute : AddEntityAttribute
- { public AddAttribute() : base(typeof(TEntity)) { } }
-
-///
-/// Marked entity automatically has the specified relationship pair added to it when
-/// automatically registered, Equivalent to .
-///
-public class AddAttribute : AddRelationAttribute
- { public AddAttribute() : base(typeof(TRelation), typeof(TTarget)) { } }
-
-///
-/// Marked entity represents a relationship type.
-/// It may be used as the "relation" in a pair.
-///
-///
-/// The relationship may have component data associated with
-/// it when added to an entity under these circumstances:
-///
-/// - If marked as a , does not carry data.
-/// - If marked as a , carries the relation's data.
-/// - If marked with neither, will carry the target's data, if it's a component.
-///
-///
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
-public class RelationAttribute : Attribute, ICreateEntityAttribute { }
-
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
-public class ComponentAttribute : Attribute, ICreateEntityAttribute { }
-
-///
-[AttributeUsage(AttributeTargets.Struct)]
-public class TagAttribute : AddAttribute, ICreateEntityAttribute { }
-
-
-///
-public class IsAAttribute : AddAttribute { }
-
-///
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
-public class ChildOfAttribute : AddAttribute { }
-
-///
-public class DependsOnAttribute : AddAttribute { }
-
-
-///
-public class ExclusiveAttribute : AddAttribute { }
-
-///
-public class WithAttribute : AddAttribute { }
-
-
-// Base attributes for other attributes.
-
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)]
-public class AddEntityAttribute : Attribute
-{
- public Type Entity { get; }
- internal AddEntityAttribute(Type entity) => Entity = entity;
-}
-
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)]
-public class AddRelationAttribute : Attribute
-{
- public Type Relation { get; }
- public Type Target { get; }
- internal AddRelationAttribute(Type relation, Type target)
- { Relation = relation; Target = target; }
-}
diff --git a/src/gaemstone/ECS/Game.cs b/src/gaemstone/ECS/Game.cs
index 1b4d2b1..b86dfcc 100644
--- a/src/gaemstone/ECS/Game.cs
+++ b/src/gaemstone/ECS/Game.cs
@@ -6,7 +6,6 @@ namespace gaemstone.ECS;
/// Entity for storing global game state and configuration.
/// Parameters can use to source this entity.
///
-[Singleton]
public struct Game { }
/// Equivalent to .
diff --git a/src/gaemstone/ECS/Module+Attributes.cs b/src/gaemstone/ECS/Module+Attributes.cs
new file mode 100644
index 0000000..b10d904
--- /dev/null
+++ b/src/gaemstone/ECS/Module+Attributes.cs
@@ -0,0 +1,53 @@
+using System;
+using static gaemstone.Flecs.Core;
+
+namespace gaemstone.ECS;
+
+///
+/// Use a custom name or path for this entity instead of the type's name.
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
+public class PathAttribute : Attribute
+{
+ public string Value { get; }
+ public PathAttribute(string value) => Value = value;
+}
+
+///
+/// Register the entity under a globally unique symbol.
+/// Uses the type's name by default.
+///
+// TODO: Remove [Symbol], introduce [Public] for modules and [Private] or so.
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
+public class SymbolAttribute : Attribute { }
+
+
+///
+/// Marked entity automatically has the specified entity added to it when
+/// automatically registered. Equivalent to .
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)]
+public class AddAttribute : Attribute { }
+
+///
+/// Marked entity automatically has the specified relationship pair added to it when
+/// automatically registered. Equivalent to .
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)]
+public class AddAttribute : Attribute { }
+
+
+///
+public class IsAAttribute : AddAttribute { }
+
+///
+public class ChildOfAttribute : AddAttribute { }
+
+///
+public class DependsOnAttribute : AddAttribute { }
+
+///
+public class ExclusiveAttribute : AddAttribute { }
+
+///
+public class WithAttribute : AddAttribute { }
diff --git a/src/gaemstone/ECS/Module+Components.cs b/src/gaemstone/ECS/Module+Components.cs
new file mode 100644
index 0000000..7cc707e
--- /dev/null
+++ b/src/gaemstone/ECS/Module+Components.cs
@@ -0,0 +1,40 @@
+using System;
+using static gaemstone.Flecs.Core;
+
+namespace gaemstone.ECS;
+
+[AttributeUsage(AttributeTargets.Struct)]
+public class EntityAttribute : Attribute { }
+
+///
+[AttributeUsage(AttributeTargets.Struct)]
+public class TagAttribute : Attribute { }
+
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
+public class ComponentAttribute : Attribute { }
+
+///
+/// A singleton is a single instance of a tag or component that can be retrieved
+/// without explicitly specifying an entity in a query, where it is equivalent
+/// to with itself as the generic type parameter.
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
+public class SingletonAttribute : Attribute
+ { public bool AutoAdd { get; init; } = true; }
+
+///
+/// Marked entity represents a relationship type.
+/// It may be used as the "relation" in a pair.
+///
+///
+/// The relationship may have component data associated with
+/// it when added to an entity under these circumstances:
+///
+/// - If marked as a , does not carry data.
+/// - If marked as a , carries the relation's data.
+/// - If marked with neither, will carry the target's data, if it's a component.
+///
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
+public class RelationAttribute : Attribute { }
diff --git a/src/gaemstone/ECS/Module.cs b/src/gaemstone/ECS/Module.cs
index 6522474..77671be 100644
--- a/src/gaemstone/ECS/Module.cs
+++ b/src/gaemstone/ECS/Module.cs
@@ -5,6 +5,11 @@ namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Class)]
public class ModuleAttribute : Attribute { }
+public interface IModuleAutoRegisterComponents
+{
+ void RegisterComponents(EntityRef module);
+}
+
public interface IModuleInitializer
{
void Initialize(EntityRef module);
diff --git a/src/gaemstone/Flecs/Core.cs b/src/gaemstone/Flecs/Core.cs
index 91d3b69..7449e55 100644
--- a/src/gaemstone/Flecs/Core.cs
+++ b/src/gaemstone/Flecs/Core.cs
@@ -3,7 +3,7 @@ using gaemstone.ECS;
namespace gaemstone.Flecs;
[Module, Path("/flecs/core")]
-public static class Core
+public static partial class Core
{
// Entity Tags
@@ -26,11 +26,11 @@ public static class Core
// Conflicts with World class, and not required?
// [Entity] public struct World { }
- [Path("*")] public struct Wildcard { }
- [Path("_")] public struct Any { }
- [Entity] public struct This { }
- [Path("$")] public struct Variable { }
- [Entity] public struct Flag { }
+ [Entity, Path("*")] public struct Wildcard { }
+ [Entity, Path("_")] public struct Any { }
+ [Entity] public struct This { }
+ [Entity, Path("$")] public struct Variable { }
+ [Entity] public struct Flag { }
// Entity Relationships
diff --git a/src/gaemstone/Flecs/DeletionEvent.cs b/src/gaemstone/Flecs/DeletionEvent.cs
index 69003da..050bc6f 100644
--- a/src/gaemstone/Flecs/DeletionEvent.cs
+++ b/src/gaemstone/Flecs/DeletionEvent.cs
@@ -3,14 +3,14 @@ using gaemstone.ECS;
namespace gaemstone.Flecs;
[Module, Path("/flecs/core")]
-public static class DeletionEvent
+public static partial class DeletionEvent
{
[Relation, Tag] public struct OnDelete { }
[Relation, Tag] public struct OnDeleteTarget { }
}
[Module, Path("/flecs/core")]
-public static class DeletionBehavior
+public static partial class DeletionBehavior
{
[Tag] public struct Remove { }
[Tag] public struct Delete { }
diff --git a/src/gaemstone/Flecs/Doc.cs b/src/gaemstone/Flecs/Doc.cs
index 46c2a59..1fe5d92 100644
--- a/src/gaemstone/Flecs/Doc.cs
+++ b/src/gaemstone/Flecs/Doc.cs
@@ -7,7 +7,7 @@ using static flecs_hub.flecs;
namespace gaemstone.Flecs;
[Module, Path("/flecs/doc")]
-public static class Doc
+public static partial class Doc
{
[Tag] public struct Brief { }
[Tag] public struct Detail { }
diff --git a/src/gaemstone/Flecs/ObserverEvent.cs b/src/gaemstone/Flecs/ObserverEvent.cs
index 698ca75..21eb266 100644
--- a/src/gaemstone/Flecs/ObserverEvent.cs
+++ b/src/gaemstone/Flecs/ObserverEvent.cs
@@ -3,7 +3,7 @@ using gaemstone.ECS;
namespace gaemstone.Flecs;
[Module, Path("/flecs/core")]
-public static class ObserverEvent
+public static partial class ObserverEvent
{
[Entity] public struct OnAdd { }
[Entity] public struct OnRemove { }
diff --git a/src/gaemstone/Flecs/Pipeline.cs b/src/gaemstone/Flecs/Pipeline.cs
index 939a676..b250583 100644
--- a/src/gaemstone/Flecs/Pipeline.cs
+++ b/src/gaemstone/Flecs/Pipeline.cs
@@ -3,7 +3,7 @@ using gaemstone.ECS;
namespace gaemstone.Flecs;
[Module, Path("/flecs/pipeline")]
-public static class Pipeline
+public static partial class Pipeline
{
[Entity] public struct Phase { }
}
diff --git a/src/gaemstone/Flecs/SystemPhase.cs b/src/gaemstone/Flecs/SystemPhase.cs
index 891b0de..827d165 100644
--- a/src/gaemstone/Flecs/SystemPhase.cs
+++ b/src/gaemstone/Flecs/SystemPhase.cs
@@ -4,7 +4,7 @@ using static gaemstone.Flecs.Pipeline;
namespace gaemstone.Flecs;
[Module, Path("/flecs/pipeline")]
-public static class SystemPhase
+public static partial class SystemPhase
{
[Entity, Add]
public struct PreFrame { }
diff --git a/src/gaemstone/Flecs/Systems/Monitor.cs b/src/gaemstone/Flecs/Systems/Monitor.cs
index e9d1c51..938a0ba 100644
--- a/src/gaemstone/Flecs/Systems/Monitor.cs
+++ b/src/gaemstone/Flecs/Systems/Monitor.cs
@@ -6,7 +6,7 @@ using static flecs_hub.flecs;
namespace gaemstone.Flecs.Systems;
[Module, Path("/flecs/monitor")]
-public unsafe class Monitor
+public unsafe partial class Monitor
: IModuleInitializer
{
public void Initialize(EntityRef module)
diff --git a/src/gaemstone/Flecs/Systems/Rest.cs b/src/gaemstone/Flecs/Systems/Rest.cs
index a057002..8235fd1 100644
--- a/src/gaemstone/Flecs/Systems/Rest.cs
+++ b/src/gaemstone/Flecs/Systems/Rest.cs
@@ -6,7 +6,7 @@ using static flecs_hub.flecs;
namespace gaemstone.Flecs.Systems;
[Module, Path("/flecs/rest")]
-public unsafe class Rest
+public unsafe partial class Rest
: IModuleInitializer
{
public void Initialize(EntityRef module)
diff --git a/src/gaemstone/Universe+Modules.cs b/src/gaemstone/Universe+Modules.cs
index bb64a1b..f247866 100644
--- a/src/gaemstone/Universe+Modules.cs
+++ b/src/gaemstone/Universe+Modules.cs
@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using gaemstone.ECS;
using gaemstone.Utility;
using static gaemstone.Flecs.Core;
-using BindingFlags = System.Reflection.BindingFlags;
+using Module = gaemstone.Flecs.Core.Module;
namespace gaemstone;
@@ -19,63 +20,20 @@ public class ModuleManager
internal ModuleInfo? Lookup(Entity entity)
=> _modules.GetValueOrDefault(entity);
- public void RegisterAllFrom(System.Reflection.Assembly assembly)
- => RegisterAll(assembly.GetTypes());
- public void RegisterAll(IEnumerable types)
- {
- foreach (var type in types)
- if (type.Has())
- Register(type);
- }
-
public EntityRef Register()
- where T : class => Register(typeof(T));
- public EntityRef Register(Type moduleType)
+ where T : class, new()
{
- if (!moduleType.IsClass || moduleType.IsGenericType || moduleType.IsGenericTypeDefinition) throw new Exception(
+ var moduleType = typeof(T);
+ if (moduleType.IsGenericType) throw new Exception(
$"Module {moduleType} must be a non-generic class");
- if (moduleType.Get() is not ModuleAttribute moduleAttr) throw new Exception(
+ if (!moduleType.Has()) throw new Exception(
$"Module {moduleType} must be marked with ModuleAttribute");
- // Check if module type is static.
- if (moduleType.IsAbstract && moduleType.IsSealed) {
-
- // Static modules represent existing modules, as such they don't
- // create entities, only look up existing ones to add type lookups
- // for use with the Lookup(Type) method.
-
- if (moduleType.Get()?.Value is not string modulePathStr) throw new Exception(
- $"Existing module {moduleType} must have {nameof(PathAttribute)} set");
- var modulePath = EntityPath.Parse(modulePathStr);
- var moduleEntity = Universe.LookupByPath(modulePath) ?? throw new Exception(
- $"Existing module {moduleType} with name '{modulePath}' not found");
-
- // This implementation is pretty naive. It simply gets all nested
- // types which are tagged with an ICreateEntityAttribute base
- // attribute and creates a lookup mapping. No sanity checking.
-
- foreach (var type in moduleType.GetNestedTypes()) {
- if (!type.GetCustomAttributes(true).OfType().Any()) continue;
-
- var attr = type.Get();
- var path = EntityPath.Parse(attr?.Value ?? type.Name);
- var entity = Universe.LookupByPathOrThrow(moduleEntity, path);
- entity.CreateLookup(type);
-
- if (type.Has()) entity.Add();
- }
-
- return moduleEntity;
-
- } else {
-
- var path = GetModulePath(moduleType);
- var module = new ModuleInfo(Universe, moduleType, path);
- _modules.Add(module.Entity, module);
- TryEnableModule(module);
- return module.Entity;
-
- }
+ var path = GetModulePath(moduleType);
+ var module = new ModuleInfo(Universe, moduleType, path);
+ _modules.Add(module.Entity, module);
+ TryEnableModule(module);
+ return module.Entity;
}
private void TryEnableModule(ModuleInfo module)
@@ -149,10 +107,15 @@ public class ModuleManager
var module = Universe.New(Path).Add();
// Add module dependencies from [DependsOn<>] attributes.
- foreach (var dependsAttr in Type.GetMultiple().Where(attr =>
- attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) {
- var dependsPath = GetModulePath(dependsAttr.Target);
- var dependency = Universe.LookupByPath(dependsPath) ??
+ foreach (var attr in Type.GetCustomAttributes()) {
+ // TODO: Do this without reflection.
+ var attrType = attr.GetType();
+ if (!attrType.IsGenericType) continue;
+ if (attrType.GetGenericTypeDefinition() != typeof(DependsOnAttribute<>)) continue;
+
+ var dependsTarget = attrType.GenericTypeArguments[0];
+ var dependsPath = GetModulePath(dependsTarget);
+ var dependency = Universe.LookupByPath(dependsPath) ??
Universe.New(dependsPath).Add().Disable().Build();
var depModule = Universe.Modules.Lookup(dependency);
@@ -172,42 +135,13 @@ public class ModuleManager
public void Enable()
{
Entity.Enable();
- Instance = Activator.CreateInstance(Type)!;
- foreach (var type in Type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic))
- RegisterNestedType(type);
+ Instance = Activator.CreateInstance(Type)!; // TODO: Replace with generic new() somehow.
+ if (Instance is IModuleAutoRegisterComponents generatedComponents)
+ generatedComponents.RegisterComponents(Entity);
(Instance as IModuleInitializer)?.Initialize(Entity);
RegisterMethods(Instance);
}
- private void RegisterNestedType(Type type)
- {
- if (!type.GetCustomAttributes(true).OfType().Any()) return;
-
- if (!type.Has() && (!type.IsValueType || (type.GetFields().Length > 0)))
- throw new Exception($"Type {type} must be an empty, used-defined struct.");
-
- var path = EntityPath.Parse(type.Get()?.Value ?? type.Name);
- var builder = path.IsAbsolute ? Universe.New(path) : Entity.NewChild(path);
-
- if (type.Get() is SymbolAttribute symbolAttr)
- builder.Symbol(symbolAttr.Value ?? path.Name);
-
- var entity = builder.Build();
-
- EntityRef Lookup(Type toLookup)
- => (type != toLookup) ? Universe.LookupByTypeOrThrow(toLookup) : entity;
- foreach (var attr in type.GetMultiple())
- entity.Add(Lookup(attr.Entity));
- foreach (var attr in type.GetMultiple())
- entity.Add(Lookup(attr.Relation), Lookup(attr.Target));
-
- if (type.Get()?.AutoAdd == true) entity.Add(entity);
- if (type.Has()) entity.InitComponent(type);
- else entity.CreateLookup(type);
- if (type.Has()) entity.Add();
- if (type.Has()) entity.Add();
- }
-
private void RegisterMethods(object? instance)
{
foreach (var method in Type.GetMethods(
diff --git a/src/gaemstone/Universe.cs b/src/gaemstone/Universe.cs
index 2bb676d..3fb235e 100644
--- a/src/gaemstone/Universe.cs
+++ b/src/gaemstone/Universe.cs
@@ -1,4 +1,3 @@
-using System.Linq;
using gaemstone.ECS;
namespace gaemstone;
@@ -12,19 +11,18 @@ public class Universe : World
{
Modules = new(this);
- // Bootstrap some stuff, so we can register flecs properly.
- New("/gaemstone/Doc/DisplayType")
- .Add(LookupByPathOrThrow("/flecs/core/Exclusive"))
- .Build().CreateLookup();
+ // Bootstrap [Relation] tag, since it will be added to some Flecs types.
New("/gaemstone/Doc/Relation").Build().CreateLookup();
- LookupByPathOrThrow("/flecs/core/Module" ).CreateLookup();
- LookupByPathOrThrow("/flecs/core/Component").CreateLookup();
- LookupByPathOrThrow("/flecs/core/Tag" ).CreateLookup();
- // Register built-in (static) modules, which
- // are defined in the "gaemstone.Flecs" namespace.
- Modules.RegisterAll(GetType().Assembly.GetTypes()
- .Where(t => t.IsAbstract && t.IsSealed));
+ // Bootstrap built-in (static) modules from Flecs.
+ // 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();
diff --git a/src/gaemstone/gaemstone.csproj b/src/gaemstone/gaemstone.csproj
index aeb1b19..6d3e4b0 100644
--- a/src/gaemstone/gaemstone.csproj
+++ b/src/gaemstone/gaemstone.csproj
@@ -10,6 +10,8 @@
+