From 2b877e0c5c6c52319c58d1638d888e7bf3782d03 Mon Sep 17 00:00:00 2001 From: copygirl Date: Tue, 3 Jan 2023 12:43:41 +0100 Subject: [PATCH] Source Generators, step 1: Components - Add IModuleAutoRegisterComponents interface - Add gaemstone.SourceGen project which (currently) generates a method to automatically register a module's compiles. - Get rid of RegisterNestedType (which has been replaced) and code to handle static (Flecs built-in) modules. - Split Attributes into Module+Attributes and +Components "Components" is for attributes that define components "Attributes" are for other attributes that can be added --- .vscode/tasks.json | 3 +- gaemstone.sln | 21 +- src/Immersion/Immersion.csproj | 2 + src/Immersion/ManagedComponentTest.cs | 24 ++ src/Immersion/ObserverTest.cs | 2 +- src/Immersion/Program.cs | 1 + .../Client/Systems/ChunkMeshGenerator.cs | 2 +- .../Systems/BasicWorldGenerator.cs | 2 +- .../Systems/SurfaceGrassGenerator.cs.disabled | 2 +- src/gaemstone.Bloxel/gaemstone.Bloxel.csproj | 2 + .../Components/CameraComponents.cs | 2 +- .../Components/InputComponents.cs | 8 +- .../Components/RenderingComponents.cs | 2 +- .../Components/ResourceComponents.cs | 2 +- .../Systems/EntityInspector.cs | 2 +- .../Systems/FreeCameraController.cs | 2 +- .../Systems/ImGuiDemoWindow.cs | 2 +- .../Systems/ImGuiInputDebug.cs | 2 +- src/gaemstone.Client/Systems/ImGuiManager.cs | 4 +- src/gaemstone.Client/Systems/InputManager.cs | 2 +- src/gaemstone.Client/Systems/MeshManager.cs | 2 +- src/gaemstone.Client/Systems/Renderer.cs | 18 +- .../Systems/TextureManager.cs | 2 +- src/gaemstone.Client/Systems/Windowing.cs | 2 +- src/gaemstone.Client/gaemstone.Client.csproj | 2 + .../AutoRegisterComponentsGenerator.cs | 256 ++++++++++++++++++ .../Generators/ModuleGenerator.cs | 76 ++++++ .../Utility/CollectionExtensions.cs | 10 + .../Utility/SymbolExtensions.cs | 51 ++++ .../gaemstone.SourceGen.csproj | 19 ++ .../Components/TransformComponents.cs | 2 +- src/gaemstone/Doc.cs | 2 +- src/gaemstone/ECS/Attributes.cs | 121 --------- src/gaemstone/ECS/Game.cs | 1 - src/gaemstone/ECS/Module+Attributes.cs | 53 ++++ src/gaemstone/ECS/Module+Components.cs | 40 +++ src/gaemstone/ECS/Module.cs | 5 + src/gaemstone/Flecs/Core.cs | 12 +- src/gaemstone/Flecs/DeletionEvent.cs | 4 +- src/gaemstone/Flecs/Doc.cs | 2 +- src/gaemstone/Flecs/ObserverEvent.cs | 2 +- src/gaemstone/Flecs/Pipeline.cs | 2 +- src/gaemstone/Flecs/SystemPhase.cs | 2 +- src/gaemstone/Flecs/Systems/Monitor.cs | 2 +- src/gaemstone/Flecs/Systems/Rest.cs | 2 +- src/gaemstone/Universe+Modules.cs | 112 ++------ src/gaemstone/Universe.cs | 22 +- src/gaemstone/gaemstone.csproj | 2 + 48 files changed, 638 insertions(+), 277 deletions(-) create mode 100644 src/Immersion/ManagedComponentTest.cs create mode 100644 src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs create mode 100644 src/gaemstone.SourceGen/Generators/ModuleGenerator.cs create mode 100644 src/gaemstone.SourceGen/Utility/CollectionExtensions.cs create mode 100644 src/gaemstone.SourceGen/Utility/SymbolExtensions.cs create mode 100644 src/gaemstone.SourceGen/gaemstone.SourceGen.csproj delete mode 100644 src/gaemstone/ECS/Attributes.cs create mode 100644 src/gaemstone/ECS/Module+Attributes.cs create mode 100644 src/gaemstone/ECS/Module+Components.cs 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 @@ +