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
wip/source-generators
copygirl 1 year ago
parent 13f69a0856
commit 2b877e0c5c
  1. 3
      .vscode/tasks.json
  2. 21
      gaemstone.sln
  3. 2
      src/Immersion/Immersion.csproj
  4. 24
      src/Immersion/ManagedComponentTest.cs
  5. 2
      src/Immersion/ObserverTest.cs
  6. 1
      src/Immersion/Program.cs
  7. 2
      src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs
  8. 2
      src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs
  9. 2
      src/gaemstone.Bloxel/Systems/SurfaceGrassGenerator.cs.disabled
  10. 2
      src/gaemstone.Bloxel/gaemstone.Bloxel.csproj
  11. 2
      src/gaemstone.Client/Components/CameraComponents.cs
  12. 8
      src/gaemstone.Client/Components/InputComponents.cs
  13. 2
      src/gaemstone.Client/Components/RenderingComponents.cs
  14. 2
      src/gaemstone.Client/Components/ResourceComponents.cs
  15. 2
      src/gaemstone.Client/Systems/EntityInspector.cs
  16. 2
      src/gaemstone.Client/Systems/FreeCameraController.cs
  17. 2
      src/gaemstone.Client/Systems/ImGuiDemoWindow.cs
  18. 2
      src/gaemstone.Client/Systems/ImGuiInputDebug.cs
  19. 4
      src/gaemstone.Client/Systems/ImGuiManager.cs
  20. 2
      src/gaemstone.Client/Systems/InputManager.cs
  21. 2
      src/gaemstone.Client/Systems/MeshManager.cs
  22. 18
      src/gaemstone.Client/Systems/Renderer.cs
  23. 2
      src/gaemstone.Client/Systems/TextureManager.cs
  24. 2
      src/gaemstone.Client/Systems/Windowing.cs
  25. 2
      src/gaemstone.Client/gaemstone.Client.csproj
  26. 256
      src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs
  27. 76
      src/gaemstone.SourceGen/Generators/ModuleGenerator.cs
  28. 10
      src/gaemstone.SourceGen/Utility/CollectionExtensions.cs
  29. 51
      src/gaemstone.SourceGen/Utility/SymbolExtensions.cs
  30. 19
      src/gaemstone.SourceGen/gaemstone.SourceGen.csproj
  31. 2
      src/gaemstone/Components/TransformComponents.cs
  32. 2
      src/gaemstone/Doc.cs
  33. 121
      src/gaemstone/ECS/Attributes.cs
  34. 1
      src/gaemstone/ECS/Game.cs
  35. 53
      src/gaemstone/ECS/Module+Attributes.cs
  36. 40
      src/gaemstone/ECS/Module+Components.cs
  37. 5
      src/gaemstone/ECS/Module.cs
  38. 12
      src/gaemstone/Flecs/Core.cs
  39. 4
      src/gaemstone/Flecs/DeletionEvent.cs
  40. 2
      src/gaemstone/Flecs/Doc.cs
  41. 2
      src/gaemstone/Flecs/ObserverEvent.cs
  42. 2
      src/gaemstone/Flecs/Pipeline.cs
  43. 2
      src/gaemstone/Flecs/SystemPhase.cs
  44. 2
      src/gaemstone/Flecs/Systems/Monitor.cs
  45. 2
      src/gaemstone/Flecs/Systems/Rest.cs
  46. 112
      src/gaemstone/Universe+Modules.cs
  47. 22
      src/gaemstone/Universe.cs
  48. 2
      src/gaemstone/gaemstone.csproj

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

@ -1,17 +1,19 @@

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

@ -16,6 +16,8 @@
<ProjectReference Include="../gaemstone.Bloxel/gaemstone.Bloxel.csproj" />
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" />
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
<ProjectReference Include="../gaemstone.SourceGen/gaemstone.SourceGen.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1,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<BigManagedData>();
game.Set(new BigManagedData());
// This is to make sure the number of objects kept alive stays stable.
Console.WriteLine(ReferenceHandle.NumActiveHandles);
}
}

@ -8,7 +8,7 @@ namespace Immersion;
[Module]
[DependsOn<gaemstone.Bloxel.Components.CoreComponents>]
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
public class ObserverTest
public partial class ObserverTest
{
[Observer<ObserverEvent.OnSet>]
[Expression("[in] Chunk, [none] (MeshHandle, *)")]

@ -35,6 +35,7 @@ window.Initialize();
window.Center();
// universe.Modules.Register<ObserverTest>();
// universe.Modules.Register<ManagedComponentTest>();
universe.Modules.Register<gaemstone.Client.Systems.Windowing>();
universe.Modules.Register<gaemstone.Components.TransformComponents>();

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

@ -8,7 +8,7 @@ namespace gaemstone.Bloxel.Systems;
[Module]
[DependsOn<gaemstone.Bloxel.Components.CoreComponents>]
public class BasicWorldGenerator
public partial class BasicWorldGenerator
{
private readonly FastNoiseLite _noise;
private readonly Random _rnd = new();

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

@ -15,6 +15,8 @@
<ProjectReference Include="../gaemstone/gaemstone.csproj" />
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" />
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
<ProjectReference Include="../gaemstone.SourceGen/gaemstone.SourceGen.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>

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

@ -5,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<Input>]
public struct Input { }
[Symbol, Path("/Input/Mouse")]
[Symbol, Entity, Path("/Input/Mouse")]
[Add<Mouse>]
public struct Mouse { }
[Symbol, Path("/Input/Keyboard")]
[Symbol, Entity, Path("/Input/Keyboard")]
[Add<Keyboard>]
public struct Keyboard { }

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

@ -3,7 +3,7 @@ using gaemstone.ECS;
namespace gaemstone.Client.Components;
[Module]
public class ResourceComponents
public partial class ResourceComponents
{
[Symbol, Tag]
public struct Resource { }

@ -15,7 +15,7 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
public class EntityInspector
public partial class EntityInspector
: IModuleInitializer
{
[Tag]

@ -12,7 +12,7 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Components.CameraComponents>]
[DependsOn<gaemstone.Client.Components.InputComponents>]
[DependsOn<gaemstone.Components.TransformComponents>]
public class FreeCameraController
public partial class FreeCameraController
{
[Component]
public struct CameraController

@ -7,7 +7,7 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
public class ImGuiDemoWindow
public partial class ImGuiDemoWindow
{
private bool _isOpen = false;

@ -15,7 +15,7 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn<gaemstone.Client.Components.InputComponents>]
[DependsOn<gaemstone.Client.Systems.ImGuiManager>]
public class ImGuiInputDebug
public partial class ImGuiInputDebug
{
private bool _isOpen = false;

@ -19,7 +19,7 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Components.InputComponents>]
[DependsOn<gaemstone.Client.Systems.InputManager>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
public class ImGuiManager
public partial class ImGuiManager
{
[Entity, Add<Pipeline.Phase>]
[DependsOn<SystemPhase.OnLoad>]
@ -29,7 +29,7 @@ public class ImGuiManager
[DependsOn<SystemPhase.OnStore>]
public struct ImGuiRenderPhase { }
[Component, Singleton(AutoAdd = false)]
[Singleton(AutoAdd = false)]
public class ImGuiData
{
public ImGuiController Controller { get; }

@ -13,7 +13,7 @@ namespace gaemstone.Client.Systems;
[Module]
[DependsOn<gaemstone.Client.Components.InputComponents>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
public class InputManager
public partial class InputManager
{
[Component] public record class InputContext(IInputContext Value) { }

@ -14,7 +14,7 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
[DependsOn<gaemstone.Client.Components.ResourceComponents>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
public class MeshManager
public partial class MeshManager
{
private const uint PositionAttribIndex = 0;
private const uint NormalAttribIndex = 1;

@ -19,7 +19,7 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
[DependsOn<gaemstone.Components.TransformComponents>]
public class Renderer
public partial class Renderer
{
private uint _program;
private int _cameraMatrixUniform;
@ -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<GlobalTransform>(1);
@ -105,16 +105,16 @@ public class Renderer
var textures = iter.FieldOrEmpty<TextureHandle>(5);
for (var i = 0; i < iter.Count; i++) {
var rTransform = transforms[i];
var mesh = meshes[i];
var transform = transforms[i];
var mesh = meshes[i];
// var hasTexture = (texPairs.Length > 0);
var texture = textures.GetOrNull(i);
var texture = textures.GetOrNull(i);
// If entity has Texture, bind it now.
if (texture.HasValue) GL.BindTexture(texture.Value.Target, texture.Value.Handle);
// Draw the mesh.
GL.UniformMatrix4(_modelMatrixUniform, 1, false, in rTransform.Value.M11);
GL.UniformMatrix4(_modelMatrixUniform, 1, false, in transform.Value.M11);
GL.BindVertexArray(mesh.Handle);
if (!mesh.IsIndexed) GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)mesh.Count);
else unsafe { GL.DrawElements(PrimitiveType.Triangles, (uint)mesh.Count, DrawElementsType.UnsignedShort, null); }

@ -15,7 +15,7 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Components.RenderingComponents>]
[DependsOn<gaemstone.Client.Components.ResourceComponents>]
[DependsOn<gaemstone.Client.Systems.Windowing>]
public class TextureManager
public partial class TextureManager
{
[Observer<ObserverEvent.OnSet>]
public static void OnCanvasSet(Canvas canvas)

@ -7,7 +7,7 @@ using Silk.NET.Windowing;
namespace gaemstone.Client.Systems;
[Module]
public class Windowing
public partial class Windowing
{
[Component]
public class Canvas

@ -15,6 +15,8 @@
<ItemGroup>
<ProjectReference Include="../gaemstone/gaemstone.csproj" />
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
<ProjectReference Include="../gaemstone.SourceGen/gaemstone.SourceGen.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="../ImGui.NET/src/ImGui.NET/ImGui.NET.csproj" />
</ItemGroup>

@ -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<INamedTypeSymbol, HashSet<INamedTypeSymbol>> Modules { get; }
= new(SymbolEqualityComparer.Default);
public HashSet<INamedTypeSymbol> ComponentsNotInModule { get; }
= new(SymbolEqualityComparer.Default);
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is not AttributeSyntax attrNode) return;
var model = context.SemanticModel;
var attrType = model.GetTypeInfo(attrNode).Type!;
if ((attrType.GetNamespace() != "gaemstone.ECS") || !attrType.Name.EndsWith("Attribute")) return;
var attrName = attrType.Name.Substring(0, attrType.Name.Length - "Attribute".Length);
if (!Enum.TryParse<RegisterType>(attrName, out _)) return;
var symbol = (model.GetDeclaredSymbol(attrNode.Parent?.Parent!) as INamedTypeSymbol)!;
if ((symbol.ContainingSymbol is INamedTypeSymbol module)
&& module.HasAttribute("gaemstone.ECS.ModuleAttribute"))
{
if (!Modules.TryGetValue(module, out var components))
Modules.Add(module, components = new(SymbolEqualityComparer.Default));
components.Add(symbol);
}
else ComponentsNotInModule.Add(symbol);
}
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxContextReceiver is not SyntaxReceiver receiver) return;
foreach (var symbol in receiver.ComponentsNotInModule)
context.ReportDiagnostic(Diagnostic.Create(ComponentNotPartOfModule,
symbol.Locations.FirstOrDefault(), symbol.GetFullName()));
var modules = new Dictionary<INamedTypeSymbol, ModuleInfo>(
SymbolEqualityComparer.Default);
foreach (var pair in receiver.Modules) {
var moduleSymbol = pair.Key;
if (!modules.TryGetValue(moduleSymbol, out var module))
modules.Add(moduleSymbol, module = new(moduleSymbol));
foreach (var symbol in pair.Value) {
var componentTypes = symbol.GetAttributes()
.Where (attr => (attr.AttributeClass?.GetNamespace() == "gaemstone.ECS"))
.Select(attr => attr.AttributeClass!.Name)
.Where (name => name.EndsWith("Attribute"))
.Select(name => name.Substring(0, name.Length - "Attribute".Length))
.SelectMany(name => Enum.TryParse<RegisterType>(name, out var type)
? new[] { type } : Enumerable.Empty<RegisterType>())
.ToList();
if (componentTypes.Count == 0) continue;
var isRelation = componentTypes.Contains(RegisterType.Relation);
if (isRelation && (componentTypes.Count == 2)) {
var other = componentTypes.Where(type => (type != RegisterType.Relation)).Single();
if (other is RegisterType.Component or RegisterType.Tag)
componentTypes.Remove(RegisterType.Relation);
else context.ReportDiagnostic(Diagnostic.Create(ComponentRelationInvalidType,
symbol.Locations.FirstOrDefault(), symbol.GetFullName(), other.ToString()));
}
if (componentTypes.Count >= 2)
context.ReportDiagnostic(Diagnostic.Create(ComponentMultipleTypes,
symbol.Locations.FirstOrDefault(), symbol.GetFullName(),
string.Join(", ", componentTypes.Select(s => $"[{s}]"))));
var componentType = componentTypes[0];
var addedEntities = new List<ITypeSymbol>();
var addedRelations = new List<(ITypeSymbol, ITypeSymbol)>();
foreach (var attr in symbol.GetAttributes())
for (var type = attr.AttributeClass; type != null; type = type.BaseType)
switch (type.GetFullName(true)) {
case "gaemstone.ECS.AddAttribute`1": addedEntities.Add(type.TypeArguments[0]); break;
case "gaemstone.ECS.AddAttribute`2": addedRelations.Add((type.TypeArguments[0], type.TypeArguments[1])); break;
}
module.Components.Add(new(symbol, module, componentType,
addedEntities, addedRelations));
}
}
foreach (var module in modules.Values) {
var sb = new StringBuilder();
sb.AppendLine($$"""
// <auto-generated/>
using gaemstone.ECS;
namespace {{module.Namespace}};
""");
if (module.IsStatic) {
sb.AppendLine($$"""
public static partial class {{ module.Name }}
{
public static void RegisterComponents(World world)
{
var module = world.LookupByPathOrThrow({{ module.Path.ToStringLiteral() }});
""");
foreach (var c in module.Components) {
var @var = $"entity{c.Name}";
var path = (c.Path ?? c.Name).ToStringLiteral();
sb.AppendLine($$""" var {{ @var }} = world.LookupByPathOrThrow(module, {{ path }})""");
if (c.IsRelation) sb.AppendLine($$""" .Add<gaemstone.Doc.Relation>()""");
sb.AppendLine($$""" .CreateLookup<{{ c.Name }}>()""");
sb.Insert(sb.Length - 1, ";");
}
} else {
sb.AppendLine($$"""
public partial class {{module.Name}}
: IModuleAutoRegisterComponents
{
public void RegisterComponents(EntityRef module)
{
var world = module.World;
""");
foreach (var c in module.Components) {
var @var = $"entity{c.Name}";
var path = (c.Path ?? c.Name).ToStringLiteral();
sb.AppendLine($$""" var {{ @var }} = world.New(module, {{ path }})""");
if (c.IsSymbol) sb.AppendLine($$""" .Symbol("{{ c.Name }}")""");
if (c.IsRelation) sb.AppendLine($$""" .Add<gaemstone.Doc.Relation>()""");
if (c.IsTag) sb.AppendLine($$""" .Add<gaemstone.Flecs.Core.Tag>()""");
if (c.IsComponent) sb.AppendLine($$""" .Build().InitComponent<{{ c.Name }}>()""");
else sb.AppendLine($$""" .Build().CreateLookup<{{ c.Name }}>()""");
if (c.SingletonAddSelf) sb.AppendLine($$""" .Add<{{ c.Name }}>()""");
sb.Insert(sb.Length - 1, ";");
}
foreach (var c in module.Components) {
var @var = $"entity{c.Name}";
foreach (var e in c.EntitiesToAdd)
sb.AppendLine($$""" {{ @var }}.Add<{{ e.GetFullName() }}>();""");
foreach (var (r, t) in c.RelationsToAdd)
sb.AppendLine($$""" {{ @var }}.Add<{{ r.GetFullName() }}, {{ t.GetFullName() }}>();""");
}
}
sb.AppendLine($$"""
}
}
""");
context.AddSource($"{module.FullName}_Components.g.cs", sb.ToString());
}
}
private class ModuleInfo
{
public INamedTypeSymbol Symbol { get; }
public string Name => Symbol.Name;
public string FullName => Symbol.GetFullName();
public string Namespace => Symbol.GetNamespace()!;
public List<ComponentInfo> Components { get; } = new();
public string? Path { get; }
public bool 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<ITypeSymbol> 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<ITypeSymbol> entitiesToAdd, List<(ITypeSymbol, ITypeSymbol)> relationsToAdd)
{
Symbol = symbol;
Module = module;
Type = type;
EntitiesToAdd = entitiesToAdd;
RelationsToAdd = relationsToAdd;
Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value as string;
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,
}
}

@ -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<INamedTypeSymbol> Symbols { get; }
= new(SymbolEqualityComparer.Default);
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is not AttributeSyntax attrNode) return;
var model = context.SemanticModel;
var attrType = model.GetTypeInfo(attrNode).Type!;
if (attrType.GetFullName(true) != "gaemstone.ECS.ModuleAttribute") return;
var memberNode = attrNode.Parent?.Parent!;
var memberSymbol = model.GetDeclaredSymbol(memberNode) as INamedTypeSymbol;
Symbols.Add(memberSymbol!);
}
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxContextReceiver is not SyntaxReceiver receiver) return;
foreach (var symbol in receiver.Symbols) {
var isNested = (symbol.ContainingType != null);
if (isNested)
context.ReportDiagnostic(Diagnostic.Create(ModuleMayNotBeNested,
symbol.Locations.FirstOrDefault(), symbol.GetFullName()));
var isPartial = symbol.DeclaringSyntaxReferences
.Any(r => (r.GetSyntax() as ClassDeclarationSyntax)?.Modifiers
.Any(t => t.IsKind(SyntaxKind.PartialKeyword)) ?? false);
if (!isPartial)
context.ReportDiagnostic(Diagnostic.Create(ModuleMustBePartial,
symbol.Locations.FirstOrDefault(), symbol.GetFullName()));
if (symbol.IsStatic && (symbol.GetAttribute("gaemstone.ECS.PathAttribute")?
.ConstructorArguments.FirstOrDefault().Value == null))
context.ReportDiagnostic(Diagnostic.Create(ModuleBuiltInMustHavePath,
symbol.Locations.FirstOrDefault(), symbol.GetFullName()));
}
}
}

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

@ -0,0 +1,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";
}

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

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

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

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

@ -6,7 +6,6 @@ namespace gaemstone.ECS;
/// Entity for storing global game state and configuration.
/// Parameters can use <see cref="GameAttribute"/> to source this entity.
/// </summary>
[Singleton]
public struct Game { }
/// <summary> Equivalent to <see cref="SourceAttribute{Game}"/>. </summary>

@ -0,0 +1,53 @@
using System;
using static gaemstone.Flecs.Core;
namespace gaemstone.ECS;
/// <summary>
/// Use a custom name or path for this entity instead of the type's name.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class PathAttribute : Attribute
{
public string Value { get; }
public PathAttribute(string value) => Value = value;
}
/// <summary>
/// Register the entity under a globally unique symbol.
/// Uses the type's name by default.
/// </summary>
// TODO: Remove [Symbol], introduce [Public] for modules and [Private] or so.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class SymbolAttribute : Attribute { }
/// <summary>
/// Marked entity automatically has the specified entity added to it when
/// automatically registered. Equivalent to <see cref="EntityBase.Add{T}"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)]
public class AddAttribute<TEntity> : Attribute { }
/// <summary>
/// Marked entity automatically has the specified relationship pair added to it when
/// automatically registered. Equivalent to <see cref="EntityBase.Add{TRelation, TTarget}"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)]
public class AddAttribute<TRelation, TTarget> : Attribute { }
/// <seealso cref="IsA"/>
public class IsAAttribute<TTarget> : AddAttribute<IsA, TTarget> { }
/// <seealso cref="ChildOf"/>
public class ChildOfAttribute<TTarget> : AddAttribute<ChildOf, TTarget> { }
/// <seealso cref="DependsOn"/>
public class DependsOnAttribute<TTarget> : AddAttribute<DependsOn, TTarget> { }
/// <seealso cref="Exclusive"/>
public class ExclusiveAttribute : AddAttribute<Exclusive> { }
/// <seealso cref="With"/>
public class WithAttribute<TTarget> : AddAttribute<With, TTarget> { }

@ -0,0 +1,40 @@
using System;
using static gaemstone.Flecs.Core;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Struct)]
public class EntityAttribute : Attribute { }
/// <seealso cref="Tag"/>
[AttributeUsage(AttributeTargets.Struct)]
public class TagAttribute : Attribute { }
/// <seealso cref="Component"/>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class ComponentAttribute : Attribute { }
/// <summary>
/// A singleton is a single instance of a tag or component that can be retrieved
/// without explicitly specifying an entity in a query, where it is equivalent
/// to <see cref="SourceAttribute{}"/> with itself as the generic type parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class SingletonAttribute : Attribute
{ public bool AutoAdd { get; init; } = true; }
/// <summary>
/// Marked entity represents a relationship type.
/// It may be used as the "relation" in a pair.
/// </summary>
/// <remarks>
/// The relationship may have component data associated with
/// it when added to an entity under these circumstances:
/// <list type="bullet">
/// <item>If marked as a <see cref="TagAttribute"/>, does not carry data.</item>
/// <item>If marked as a <see cref="ComponentAttribute"/>, carries the relation's data.</item>
/// <item>If marked with neither, will carry the target's data, if it's a component.</item>
/// </list>
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class RelationAttribute : Attribute { }

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

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

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

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

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

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

@ -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<Phase>]
public struct PreFrame { }

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

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

@ -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<Type> types)
{
foreach (var type in types)
if (type.Has<ModuleAttribute>())
Register(type);
}
public EntityRef Register<T>()
where T : class => Register(typeof(T));
public EntityRef Register(Type moduleType)
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<ModuleAttribute>() is not ModuleAttribute moduleAttr) throw new Exception(
if (!moduleType.Has<ModuleAttribute>()) throw new Exception(
$"Module {moduleType} must be marked with ModuleAttribute");
// Check if module type is static.
if (moduleType.IsAbstract && moduleType.IsSealed) {
// Static modules represent existing modules, as such they don't
// create entities, only look up existing ones to add type lookups
// for use with the Lookup(Type) method.
if (moduleType.Get<PathAttribute>()?.Value is not string modulePathStr) throw new Exception(
$"Existing module {moduleType} must have {nameof(PathAttribute)} set");
var modulePath = EntityPath.Parse(modulePathStr);
var moduleEntity = Universe.LookupByPath(modulePath) ?? throw new Exception(
$"Existing module {moduleType} with name '{modulePath}' not found");
// This implementation is pretty naive. It simply gets all nested
// types which are tagged with an ICreateEntityAttribute base
// attribute and creates a lookup mapping. No sanity checking.
foreach (var type in moduleType.GetNestedTypes()) {
if (!type.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().Any()) continue;
var attr = type.Get<PathAttribute>();
var path = EntityPath.Parse(attr?.Value ?? type.Name);
var entity = Universe.LookupByPathOrThrow(moduleEntity, path);
entity.CreateLookup(type);
if (type.Has<RelationAttribute>()) entity.Add<Doc.Relation>();
}
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<Module>();
// Add module dependencies from [DependsOn<>] attributes.
foreach (var dependsAttr in Type.GetMultiple<AddRelationAttribute>().Where(attr =>
attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) {
var dependsPath = GetModulePath(dependsAttr.Target);
var dependency = Universe.LookupByPath(dependsPath) ??
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<Module>().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<ICreateEntityAttribute>().Any()) return;
if (!type.Has<ComponentAttribute>() && (!type.IsValueType || (type.GetFields().Length > 0)))
throw new Exception($"Type {type} must be an empty, used-defined struct.");
var path = EntityPath.Parse(type.Get<PathAttribute>()?.Value ?? type.Name);
var builder = path.IsAbsolute ? Universe.New(path) : Entity.NewChild(path);
if (type.Get<SymbolAttribute>() is SymbolAttribute symbolAttr)
builder.Symbol(symbolAttr.Value ?? path.Name);
var entity = builder.Build();
EntityRef Lookup(Type toLookup)
=> (type != toLookup) ? Universe.LookupByTypeOrThrow(toLookup) : entity;
foreach (var attr in type.GetMultiple<AddEntityAttribute>())
entity.Add(Lookup(attr.Entity));
foreach (var attr in type.GetMultiple<AddRelationAttribute>())
entity.Add(Lookup(attr.Relation), Lookup(attr.Target));
if (type.Get<SingletonAttribute>()?.AutoAdd == true) entity.Add(entity);
if (type.Has<ComponentAttribute>()) entity.InitComponent(type);
else entity.CreateLookup(type);
if (type.Has<RelationAttribute>()) entity.Add<Doc.Relation>();
if (type.Has<TagAttribute>()) entity.Add<Tag>();
}
private void RegisterMethods(object? instance)
{
foreach (var method in Type.GetMethods(

@ -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<Doc.DisplayType>();
// Bootstrap [Relation] tag, since it will be added to some Flecs types.
New("/gaemstone/Doc/Relation").Build().CreateLookup<Doc.Relation>();
LookupByPathOrThrow("/flecs/core/Module" ).CreateLookup<Flecs.Core.Module>();
LookupByPathOrThrow("/flecs/core/Component").CreateLookup<Flecs.Core.Component>();
LookupByPathOrThrow("/flecs/core/Tag" ).CreateLookup<Flecs.Core.Tag>();
// 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<Doc>();

@ -10,6 +10,8 @@
<ItemGroup>
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
<ProjectReference Include="../gaemstone.SourceGen/gaemstone.SourceGen.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>

Loading…
Cancel
Save