diff --git a/src/Immersion/Immersion.csproj b/src/Immersion/Immersion.csproj index 75cfde8..21832d4 100644 --- a/src/Immersion/Immersion.csproj +++ b/src/Immersion/Immersion.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Immersion/ManagedComponentTest.cs b/src/Immersion/ManagedComponentTest.cs index 931717d..cb9e6c4 100644 --- a/src/Immersion/ManagedComponentTest.cs +++ b/src/Immersion/ManagedComponentTest.cs @@ -1,5 +1,6 @@ using System; using gaemstone.ECS; +using gaemstone.ECS.Utility; namespace Immersion; @@ -13,9 +14,9 @@ public partial class ManagedComponentTest } [System] - public static void CreateLotsOfGarbageData(World world) + public static void CreateLotsOfGarbageData(World world) { - var game = world.LookupByPathOrThrow("/Game"); + var game = world.LookupPathOrThrow("/Game"); game.Remove(); game.Set(new BigManagedData()); // This is to make sure the number of objects kept alive stays stable. diff --git a/src/Immersion/ObserverTest.cs b/src/Immersion/ObserverTest.cs index 89ecdb5..3940632 100644 --- a/src/Immersion/ObserverTest.cs +++ b/src/Immersion/ObserverTest.cs @@ -1,7 +1,7 @@ using System; using gaemstone.ECS; -using gaemstone.Flecs; using static gaemstone.Bloxel.Components.CoreComponents; +using static gaemstone.Flecs.Core; namespace Immersion; @@ -10,7 +10,7 @@ namespace Immersion; [DependsOn] public partial class ObserverTest { - [Observer] + [Observer] [Expression("[in] Chunk, [none] (MeshHandle, *)")] public static void DoObserver(in Chunk chunk) => Console.WriteLine($"Chunk at {chunk.Position} now has a Mesh!"); diff --git a/src/Immersion/Program.cs b/src/Immersion/Program.cs index 4674b60..4a0109f 100644 --- a/src/Immersion/Program.cs +++ b/src/Immersion/Program.cs @@ -5,7 +5,6 @@ using System.Numerics; using System.Threading; using gaemstone; using gaemstone.Bloxel; -using gaemstone.ECS; using gaemstone.Utility; using Silk.NET.Windowing; using static gaemstone.Bloxel.Components.CoreComponents; @@ -20,15 +19,16 @@ var culture = CultureInfo.InvariantCulture; Thread.CurrentThread.CurrentCulture = culture; CultureInfo.DefaultThreadCurrentCulture = culture; -var universe = new Universe(); -var game = universe.LookupByTypeOrThrow(); +var universe = new Universe(); +var world = universe.World; +// TODO: Figure out a nice way to get rid of "compile errors" here. universe.Modules.Register(); universe.Modules.Register(); var window = Window.Create(WindowOptions.Default with { Title = "gæmstone", - Size = new(1280, 720), + Size = new(1280, 720), PreferredDepthBufferBits = 24, }); window.Initialize(); @@ -57,19 +57,24 @@ universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); -game.Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window))); -game.Set(new GameWindow(window)); +foreach (var module in universe.Modules) + if (!module.IsInitialized) throw new InvalidOperationException( + $"Module '{module.Entity.Path}' is not initialized"); -universe.New("MainCamera") +// Initialize Canvas and GameWindow singletons with actual values. +world.Entity().Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window))); +world.Entity().Set(new GameWindow(window)); + +world.New("MainCamera") .Set(Camera.Default3D) .Set((GlobalTransform)Matrix4x4.CreateTranslation(0.0F, 2.0F, 0.0F)) .Set(new CameraController { MouseSensitivity = 12.0F }) .Build(); -var heartMesh = universe.New("/Immersion/Resources/heart.glb").Add().Build(); -var swordMesh = universe.New("/Immersion/Resources/sword.glb").Add().Build(); +var heartMesh = world.New("/Immersion/Resources/heart.glb").Add().Build(); +var swordMesh = world.New("/Immersion/Resources/sword.glb").Add().Build(); -var entities = universe.New("Entities").Build(); +var entities = world.New("Entities").Build(); var rnd = new Random(); for (var x = -12; x <= 12; x++) for (var z = -12; z <= 12; z++) { @@ -86,13 +91,13 @@ universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); -var texture = universe.New("/Immersion/Resources/terrain.png").Add().Build(); +var texture = world.New("/Immersion/Resources/terrain.png").Add().Build(); -var stone = universe.New("Stone").Set(TextureCoords4.FromGrid(4, 4, 1, 0)).Build(); -var dirt = universe.New("Dirt" ).Set(TextureCoords4.FromGrid(4, 4, 2, 0)).Build(); -var grass = universe.New("Grass").Set(TextureCoords4.FromGrid(4, 4, 3, 0)).Build(); +var stone = world.New("Stone").Set(TextureCoords4.FromGrid(4, 4, 1, 0)).Build(); +var dirt = world.New("Dirt" ).Set(TextureCoords4.FromGrid(4, 4, 2, 0)).Build(); +var grass = world.New("Grass").Set(TextureCoords4.FromGrid(4, 4, 3, 0)).Build(); -var chunks = universe.New("Chunks").Build(); +var chunks = world.New("Chunks").Build(); var sizeH = 4; var sizeY = 2; for (var cx = -sizeH; cx < sizeH; cx++) for (var cy = -sizeY; cy < sizeY; cy++) @@ -110,12 +115,12 @@ for (var cz = -sizeH; cz < sizeH; cz++) { // universe.Modules.Register(); var stopwatch = Stopwatch.StartNew(); -var minFrameTime = TimeSpan.FromSeconds(1) / 30; +var minFrameTime = TimeSpan.FromSeconds(1) / 60; window.Run(() => { var delta = stopwatch.Elapsed; stopwatch.Restart(); - if (!universe.Progress(delta)) + if (!world.Progress(delta)) window.Close(); var requiredTime = stopwatch.Elapsed; diff --git a/src/gaemstone.Bloxel/ChunkPaletteStorage.cs b/src/gaemstone.Bloxel/ChunkPaletteStorage.cs index 5e29d98..0dda8ad 100644 --- a/src/gaemstone.Bloxel/ChunkPaletteStorage.cs +++ b/src/gaemstone.Bloxel/ChunkPaletteStorage.cs @@ -168,9 +168,9 @@ public class ChunkPaletteStorage } private int GetIndex(int x, int y, int z) - => (x | - y << Constants.ChunkBitShift | - z << (Constants.ChunkBitShift * 2)) * _indicesLength; + => (x | y << Constants.ChunkBitShift + | z << (Constants.ChunkBitShift * 2)) + * _indicesLength; private struct PaletteEntry diff --git a/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs index 7d9b9c3..5fffbcc 100644 --- a/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs +++ b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs @@ -2,7 +2,9 @@ using System; using System.Numerics; using gaemstone.Client.Systems; using gaemstone.ECS; +using Silk.NET.OpenGL; using static gaemstone.Bloxel.Components.CoreComponents; +using static gaemstone.Bloxel.Systems.BasicWorldGenerator; using static gaemstone.Client.Components.RenderingComponents; using static gaemstone.Client.Components.ResourceComponents; using static gaemstone.Client.Systems.Windowing; @@ -10,6 +12,11 @@ using static gaemstone.Client.Systems.Windowing; namespace gaemstone.Bloxel.Client.Systems; [Module] +[DependsOn] +[DependsOn] +[DependsOn] +[DependsOn] +[DependsOn] public partial class ChunkMeshGenerator { private const int StartingCapacity = 1024; @@ -20,29 +27,32 @@ public partial class ChunkMeshGenerator new Vector3[]{ new(1,1,0), new(0,1,0), new(0,1,1), new(1,1,1) }, // Up (+Y) new Vector3[]{ new(1,0,1), new(0,0,1), new(0,0,0), new(1,0,0) }, // Down (-Y) new Vector3[]{ new(0,1,1), new(0,0,1), new(1,0,1), new(1,1,1) }, // South (+Z) - new Vector3[]{ new(1,1,0), new(1,0,0), new(0,0,0), new(0,1,0) } // North (-Z) + new Vector3[]{ new(1,1,0), new(1,0,0), new(0,0,0), new(0,1,0) }, // North (-Z) }; private static readonly int[] TriangleIndices = { 0, 1, 3, 1, 2, 3 }; - - private ushort[] _indices = new ushort[StartingCapacity]; - private Vector3[] _vertices = new Vector3[StartingCapacity]; - private Vector3[] _normals = new Vector3[StartingCapacity]; - private Vector2[] _uvs = new Vector2[StartingCapacity]; + // TODO: Turn these into a component on the module. + private static ushort [] _indices = new ushort [StartingCapacity]; + private static Vector3[] _vertices = new Vector3[StartingCapacity]; + private static Vector3[] _normals = new Vector3[StartingCapacity]; + private static Vector2[] _uvs = new Vector2[StartingCapacity]; [System] - [Expression("[in] Chunk, ChunkStoreBlocks, HasBasicWorldGeneration, !(Mesh, *)")] - public void GenerateChunkMeshes(Universe universe, EntityRef entity, - in Chunk chunk, ChunkStoreBlocks blocks) + public static void GenerateChunkMeshes( + World world, Canvas canvas, + Entity entity, in Chunk chunk, ChunkStoreBlocks blocks, + Has _1, Not _2) { - if (Generate(universe, chunk.Position, blocks) is MeshHandle handle) - entity.Add(entity.NewChild("Mesh").Set(handle).Build()); - else entity.Delete(); + var result = Generate(world, canvas.GL, chunk.Position, blocks); + if (result is MeshHandle handle) { + var mesh = entity.NewChild("Mesh").Set(handle).Build(); + entity.Add(mesh); + } else entity.Delete(); } - public MeshHandle? Generate(Universe universe, + public static MeshHandle? Generate(World world, GL GL, ChunkPos chunkPos, ChunkStoreBlocks centerBlocks) { // TODO: We'll need a way to get neighbors again. @@ -59,47 +69,45 @@ public partial class ChunkMeshGenerator var indexCount = 0; var vertexCount = 0; for (var x = 0; x < 16; x++) - for (var y = 0; y < 16; y++) - for (var z = 0; z < 16; z++) { - var block = universe.LookupAlive(centerBlocks[x, y, z]); - if (block == null) continue; - - var blockVertex = new Vector3(x, y, z); - var textureCell = block.GetOrThrow(); - - foreach (var facing in BlockFacings.All) { - if (!IsNeighborEmpty(storages, x, y, z, facing)) continue; - - if (_indices.Length <= indexCount + 6) - Array.Resize(ref _indices, _indices.Length << 1); - if (_vertices.Length <= vertexCount + 4) { - Array.Resize(ref _vertices, _vertices.Length << 1); - Array.Resize(ref _normals , _vertices.Length << 1); - Array.Resize(ref _uvs , _vertices.Length << 1); - } - - for (var i = 0; i < TriangleIndices.Length; i++) - _indices[indexCount++] = (ushort)(vertexCount + TriangleIndices[i]); - - var normal = facing.ToVector3(); - for (var i = 0; i < 4; i++) { - var offset = OffsetPerFacing[(int)facing][i]; - _vertices[vertexCount] = blockVertex + offset; - _normals[vertexCount] = normal; - _uvs[vertexCount] = i switch { - 0 => textureCell.TopLeft, - 1 => textureCell.BottomLeft, - 2 => textureCell.BottomRight, - 3 => textureCell.TopRight, - _ => throw new InvalidOperationException() - }; - vertexCount++; + for (var y = 0; y < 16; y++) + for (var z = 0; z < 16; z++) { + var maybeBlock = world.LookupAliveOrNull(centerBlocks[x, y, z]); + if (maybeBlock is not Entity block) continue; + + var blockVertex = new Vector3(x, y, z); + var textureCell = block.GetOrThrow(); + + foreach (var facing in BlockFacings.All) { + if (!IsNeighborEmpty(storages, x, y, z, facing)) continue; + + if (_indices.Length <= indexCount + 6) + Array.Resize(ref _indices, _indices.Length << 1); + if (_vertices.Length <= vertexCount + 4) { + Array.Resize(ref _vertices, _vertices.Length << 1); + Array.Resize(ref _normals , _vertices.Length << 1); + Array.Resize(ref _uvs , _vertices.Length << 1); + } + + for (var i = 0; i < TriangleIndices.Length; i++) + _indices[indexCount++] = (ushort)(vertexCount + TriangleIndices[i]); + + var normal = facing.ToVector3(); + for (var i = 0; i < 4; i++) { + var offset = OffsetPerFacing[(int)facing][i]; + _vertices[vertexCount] = blockVertex + offset; + _normals[vertexCount] = normal; + _uvs[vertexCount] = i switch { + 0 => textureCell.TopLeft, + 1 => textureCell.BottomLeft, + 2 => textureCell.BottomRight, + 3 => textureCell.TopRight, + _ => throw new InvalidOperationException() + }; + vertexCount++; + } + } } - } - } - // TODO: Should dynamically generating meshes require getting GL this way? - var GL = universe.LookupByTypeOrThrow().GetOrThrow().GL; return (indexCount > 0) ? MeshManager.Create(GL, _indices.AsSpan(0, indexCount), _vertices.AsSpan(0, vertexCount), diff --git a/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs index 398032b..f7b7eeb 100644 --- a/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs +++ b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs @@ -6,14 +6,15 @@ using static gaemstone.Bloxel.Constants; namespace gaemstone.Bloxel.Systems; -[Public, Module] +[Module] [DependsOn] public partial class BasicWorldGenerator { - private readonly FastNoiseLite _noise; - private readonly Random _rnd = new(); + // TODO: Turn these into a component on the module. + private static readonly FastNoiseLite _noise; + private static readonly Random _rnd = new(); - public BasicWorldGenerator() + static BasicWorldGenerator() { _noise = new(new Random().Next()); _noise.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2); @@ -23,16 +24,17 @@ public partial class BasicWorldGenerator } [Tag] - public struct HasBasicWorldGeneration { } + public struct BasicWorldGenerationDone { } [System] - public void Populate(World world, EntityRef entity, + public static void Populate( + World world, Entity entity, in Chunk chunk, ChunkStoreBlocks blocks, - [Not] HasBasicWorldGeneration _) + Not _) { - var stone = world.LookupByPathOrThrow("Stone"); - var dirt = world.LookupByPathOrThrow("Dirt"); - var grass = world.LookupByPathOrThrow("Grass"); + var stone = world.LookupPathOrThrow("Stone"); + var dirt = world.LookupPathOrThrow("Dirt"); + var grass = world.LookupPathOrThrow("Grass"); for (var localX = 0; localX < ChunkLength; localX++) for (var localY = 0; localY < ChunkLength; localY++) for (var localZ = 0; localZ < ChunkLength; localZ++) { @@ -43,6 +45,6 @@ public partial class BasicWorldGenerator if (_noise.GetNoise(globalX, globalY, globalZ) > bias) blocks[localX, localY, localZ] = _rnd.Pick(stone, dirt, grass); } - entity.Add(); + entity.Add(); } } diff --git a/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj b/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj index 26ad828..184ab65 100644 --- a/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj +++ b/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj @@ -5,6 +5,7 @@ net7.0 disable enable + true diff --git a/src/gaemstone.Client/Systems/EntityInspector.cs b/src/gaemstone.Client/Systems/EntityInspector.cs index 622ec60..914c000 100644 --- a/src/gaemstone.Client/Systems/EntityInspector.cs +++ b/src/gaemstone.Client/Systems/EntityInspector.cs @@ -7,7 +7,6 @@ using gaemstone.ECS; using gaemstone.Flecs; using ImGuiNET; using static gaemstone.Client.Systems.ImGuiManager; -using static gaemstone.Flecs.Core; using Icon = gaemstone.Client.Utility.ForkAwesome; using ImGuiInternal = ImGuiNET.Internal.ImGui; @@ -22,7 +21,7 @@ public partial class EntityInspector public struct InspectorWindow { } [Relation, Exclusive] - [Add] + [Add] public struct Selected { } [Tag] @@ -44,17 +43,16 @@ public partial class EntityInspector public Entry? Prev { get; set; } public Entry? Next { get; set; } - public Entry(EntityRef entity, Entry? prev, Entry? next) + public Entry(Entity entity, EntityPath path, Entry? prev, Entry? next) { Entity = entity; - Path = entity.GetFullPath(); + Path = path; if ((Prev = prev) != null) Prev.Next = this; if ((Next = next) != null) Next.Prev = this; } } } - [Component] public struct DocPriority { public float Value; } @@ -64,10 +62,10 @@ public partial class EntityInspector private const string DefaultWindowTitle = "Inspector Gadget"; - public void Initialize(EntityRef module) + public static void Initialize(Entity module) { void SetDocInfo(string path, float priority, string icon, float r, float g, float b) - => module.World.LookupByPathOrThrow(path) + => module.World.LookupPathOrThrow(path) .Add() .Set(new DocPriority { Value = priority }) .Set(new DocIcon { Value = icon[0] }) @@ -78,17 +76,18 @@ public partial class EntityInspector SetDocInfo("/flecs/core/Observer" , 2 , Icon.Eye , 1.0f, 0.8f, 0.8f); SetDocInfo("/gaemstone/Doc/Relation" , 3 , Icon.ShareAlt , 0.7f, 1.0f, 0.8f); SetDocInfo("/flecs/core/Component" , 4 , Icon.PencilSquare , 0.6f, 0.6f, 1.0f); + // TODO: Handle tags like Flecs does. SetDocInfo("/flecs/core/Tag" , 5 , Icon.Tag , 0.7f, 0.8f, 1.0f); SetDocInfo("/flecs/core/Prefab" , 6 , Icon.Cube , 0.9f, 0.8f, 1.0f); } [System] - public void ShowUIButton(World world, ImGuiData _) + public static void ShowUIButton(World world, ImGuiData _) { var hasAnyInspector = false; - var inspectorWindow = world.LookupByTypeOrThrow(); - foreach (var entity in Iterator.FromTerm(world, new(inspectorWindow))) + var inspectorWindow = world.Entity(); + foreach (var entity in Iterator.FromTerm(world, new(inspectorWindow))) { hasAnyInspector = true; break; } if (ImGuiUtility.UIButton(0, Icon.Search, DefaultWindowTitle, hasAnyInspector)) @@ -96,7 +95,7 @@ public partial class EntityInspector } [System] - public void ShowInspectorWindow(EntityRef window, InspectorWindow _, History? history) + public static void ShowInspectorWindow(Entity window, InspectorWindow _, History? history) { var isOpen = true; var fontSize = ImGui.GetFontSize(); @@ -105,7 +104,7 @@ public partial class EntityInspector ImGui.SetNextWindowSize(new(fontSize * 40, fontSize * 25), ImGuiCond.Appearing); ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]); var title = window.GetDocName() ?? DefaultWindowTitle; - if (ImGui.Begin($"{Icon.Search} {title}###{window.Id}", + if (ImGui.Begin($"{Icon.Search} {title}###{window.NumericId}", ref isOpen, ImGuiWindowFlags.NoScrollbar)) { ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[0]); @@ -121,7 +120,7 @@ public partial class EntityInspector ExplorerView(window, history, selected); ImGui.EndChild(); - void Tab(string name, Action contentMethod) + void Tab(string name, Action, History?, Entity?> contentMethod) { if (!ImGui.BeginTabItem(name)) return; ImGui.BeginChild($"{name}Tab", new(-float.Epsilon, -float.Epsilon)); @@ -150,16 +149,14 @@ public partial class EntityInspector if (!isOpen) window.Delete(); } - [Observer] - public void ClearStorageOnRemove(EntityRef _1, InspectorWindow _2) + [Observer] + public static void ClearStorageOnRemove(Entity _1, InspectorWindow _2) { // TODO: Clear out settings store for the window. } - private void ActionBarAndPath(EntityRef window, History? history, EntityRef? selected) + private static void ActionBarAndPath(Entity window, History? history, Entity? selected) { - var world = window.World; - static bool IconButtonWithToolTip(string icon, string tooltip, bool enabled = true) { if (!enabled) ImGui.BeginDisabled(); var clicked = ImGui.Button(icon); @@ -176,9 +173,9 @@ public partial class EntityInspector ImGui.TableSetupColumn("Entity", ImGuiTableColumnFlags.WidthFixed); ImGui.TableNextColumn(); - var hasExpanded = window.Has(); + var hasExpanded = window.Has(); if (IconButtonWithToolTip(Icon.Outdent, "Collapse all items in the Explorer View", hasExpanded)) - window.Remove(); + window.Remove(); if (history != null) { var hasPrev = ((selected != null) ? history.Current?.Prev : history.Current) != null; @@ -201,9 +198,7 @@ public partial class EntityInspector ImGui.TableNextColumn(); if (IconButtonWithToolTip(Icon.PlusCircle, "Create a new child entity", (selected != null))) - // FIXME: Replace this once Flecs has been fixed. - SetSelected(window, history, world.New().Build().ChildOf(selected)); - // SelectAndScrollTo(windowEntity, windowData, selected!.NewChild().Build(), selected); + SetSelected(window, history, selected?.NewChild().Build()); ImGui.SameLine(); if (IconButtonWithToolTip(Icon.Pencil, "Rename the current entity", false && (selected != null))) @@ -214,26 +209,26 @@ public partial class EntityInspector var icon = !isDisabled ? Icon.BellSlash : Icon.Bell; var tooltip = $"{(!isDisabled ? "Disable" : "Enable")} the current entity"; if (IconButtonWithToolTip(icon, tooltip, (selected != null))) - { if (isDisabled) selected!.Enable(); else selected!.Disable(); } + { if (isDisabled) selected?.Enable(); else selected?.Disable(); } ImGui.SameLine(); if (IconButtonWithToolTip(Icon.Trash, "Delete the current entity", (selected != null))) { // TODO: Delete history for deleted entity? - SetSelected(window, history, selected!.Parent); - selected.Delete(); // TODO: Confirmation dialog? + SetSelected(window, history, selected?.Parent); + selected?.Delete(); // TODO: Confirmation dialog? } ImGui.EndTable(); ImGui.PopStyleVar(); } - private void PathInput(EntityRef window, History? history, EntityRef? selected, float availableWidth) + private static void PathInput(Entity window, History? history, Entity? selected, float availableWidth) { var style = ImGui.GetStyle(); ImGui.AlignTextToFramePadding(); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, style.ItemSpacing.Y)); - var path = selected?.GetFullPath() ?? null; + var path = selected?.Path ?? null; if (path != null) { var visiblePath = path.GetParts().ToList(); @@ -261,8 +256,10 @@ public partial class EntityInspector ImGui.EndDisabled(); if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip(path[1..(numHiddenItems+2)].ToString()); - } else if (ImGui.Button(visiblePath[i])) - SetSelected(window, history, window.World.LookupByPath(path[..(actualIndex+1)])); + } else if (ImGui.Button(visiblePath[i])) { + var toSelect = window.World.LookupPathOrNull(path[..(actualIndex + 1)]); + SetSelected(window, history, toSelect); + } ImGui.SameLine(); } } @@ -275,12 +272,27 @@ public partial class EntityInspector ImGui.PopStyleVar(); } - private class ExplorerEntry - : IComparable + private interface IExplorerEntry + : IComparable { - private readonly EntityInspector _module; + public Entity Entity { get; } + public int NumChildren { get; } + public bool HasChildren { get; } + public bool IsExpanded { get; } + public bool IsDisabled { get; } + + public string? Name { get; } + public string? DocName { get; } - public EntityRef Entity { get; } + public float Priority { get; } + public Entity? Type { get; } + } + + private class ExplorerEntry + : IExplorerEntry + { + public Entity Entity { get; } + Entity IExplorerEntry.Entity => Entity; public int NumChildren { get; } public bool HasChildren => (NumChildren > 0); public bool IsExpanded { get; } @@ -292,65 +304,67 @@ public partial class EntityInspector public string? DocName => (_docNameCached++ > 0) ? _docName : _docName = Entity.GetDocName(); private float? _priority; - private EntityRef? _type; - public float Priority => EnsureTypeCached()._priority!.Value; - public EntityRef? Type => EnsureTypeCached()._type; + private Entity? _type; + public float Priority => EnsureTypeCached()._priority!.Value; + public Entity? Type => EnsureTypeCached()._type; - public ExplorerEntry(EntityInspector module, EntityRef entity, - int numChildren, bool isExpanded, bool isDisabled) + public ExplorerEntry(Entity entity, + int numChildren, bool isExpanded, bool isDisabled) { - _module = module; Entity = entity; NumChildren = numChildren; IsExpanded = (isExpanded && HasChildren); IsDisabled = isDisabled; } - private ExplorerEntry EnsureTypeCached() + private ExplorerEntry EnsureTypeCached() { if (_priority != null) return this; - (_type, _priority) = _module.FindDisplayType(Entity); + (_type, _priority) = FindDisplayType(Entity); return this; } - public int CompareTo(ExplorerEntry? other) + public int CompareTo(IExplorerEntry? other) { if (other == null) return -1; + return Compare(Priority, other.Priority) ?? Compare(Name, other.Name) ?? Compare(DocName, other.DocName) - ?? Compare(Entity.Id, other.Entity.Id) + ?? Compare(Entity.NumericId, other.Entity.NumericId) ?? 0; - static int? Compare(T x, T y) { + + static int? Compare(TCompare x, TCompare y) + { if (x is null) { if (y is null) return null; else return 1; } else if (y is null) return -1; - var result = Comparer.Default.Compare(x, y); + var result = Comparer.Default.Compare(x, y); return (result != 0) ? result : null; } } } - private void ExplorerView(EntityRef window, History? history, EntityRef? selected) + private static void ExplorerView(Entity window, History? history, Entity? selected) { // For some reason, the analyzer thinks world can be // nullable, so let's be explicit about the type here. - World world = window.World; + var world = window.World; - var Wildcard = world.LookupByTypeOrThrow().Entity; - var Any = world.LookupByTypeOrThrow().Entity; - var This = world.LookupByTypeOrThrow().Entity; - var Var = world.LookupByTypeOrThrow().Entity; + var Wildcard = world.Entity().Value; + var Any = world.Entity().Value; + var This = world.Entity().Value; + var Variable = world.Entity().Value; bool IsSpecialEntity(Entity entity) => (entity == Wildcard) || (entity == Any) - || (entity == This) || (entity == Var); - - var expId = world.LookupByTypeOrThrow().Id; - List GetEntries(Entity? parent) { - var result = new List(); - using var rule = new Rule(world, new( - $"(ChildOf, {parent?.Id ?? 0})" // Must be child of parent, or root entity. - + $",?{expId}({window.Id}, $This)" // Whether entity is expanded in explorer view. - + $",?Disabled" // Don't filter out disabled entities. + || (entity == This) || (entity == Variable); + + var expId = world.Entity().NumericId; + List GetEntries(Entity? parent) { + var result = new List(); + using var rule = new Rule(world, new( + $"(ChildOf, {parent?.NumericId ?? 0})" // Must be child of parent, or root entity. + + $",?{expId}({window.NumericId}, $This)" // Whether entity is expanded in explorer view. + + $",?Disabled" // Don't filter out disabled entities. )); foreach (var iter in rule.Iter()) { var isExpanded = iter.FieldIsSet(2); @@ -358,8 +372,8 @@ public partial class EntityInspector for (var i = 0; i < iter.Count; i++) { var entity = iter.Entity(i); var count = IsSpecialEntity(entity) ? 0 - : IdRef.Pair(entity).Count; - result.Add(new(this, entity, count, isExpanded, isDisabled)); + : world.Pair(entity).Count; + result.Add(new ExplorerEntry(entity, count, isExpanded, isDisabled)); } } return result; @@ -373,8 +387,8 @@ public partial class EntityInspector float GetIndent(int depth) => depth * (nodeHeight + spacingX); - bool RenderNode(ExplorerEntry entry, int? depth) { - var entity = entry.Entity; + bool RenderNode(IExplorerEntry entry, int? depth) { + var entity = Entity.GetOrThrow(world, entry.Entity); var startY = ImGui.GetCursorPosY(); var isVisible = ImGui.IsRectVisible(new(nodeHeight, nodeHeight)); var isSelected = (entity == selected); @@ -384,7 +398,7 @@ public partial class EntityInspector // Button for expanding the child entries. if (entry.HasChildren && !entry.IsExpanded) { ImGui.SetCursorPosX(GetIndent(depth!.Value)); - if (ImGui.Button($"##Expand{entity.Id}", new(nodeHeight))) + if (ImGui.Button($"##Expand{entity.NumericId}", new(nodeHeight))) window.Add(entity); } @@ -427,7 +441,7 @@ public partial class EntityInspector var fullHeight = ImGui.GetCursorPosY() - startY - spacingY; ImGui.SetCursorPos(new(GetIndent(depth!.Value), startY)); - if (ImGui.Button($"##Expand{entity.Id}", new(nodeHeight, fullHeight))) + if (ImGui.Button($"##Expand{entity.NumericId}", new(nodeHeight, fullHeight))) window.Remove(entity); return true; @@ -439,33 +453,33 @@ public partial class EntityInspector RenderNode(child, 0); } - private void ComponentsTab(EntityRef window, History? history, EntityRef? selected) + private static void ComponentsTab(Entity window, History? history, Entity? sel) { - if (selected == null) return; - var ChildOf = window.World.LookupByTypeOrThrow(); + if (sel is not Entity selected) return; + var ChildOf = window.World.Entity(); foreach (var id in selected.Type) { // Hide ChildOf relations, as they are visible in the explorer. - if (id.IsPair && (id.Id.RelationUnsafe == ChildOf)) continue; + if (id.IsPair && (id.Value.RelationUnsafe == ChildOf)) continue; RenderIdentifier(window, history, id); } } - private void ReferencesTab(EntityRef window, History? history, EntityRef? selected) + private static void ReferencesTab(Entity window, History? history, Entity? sel) { - if (selected == null) return; + if (sel is not Entity selected) return; var world = window.World; - var ChildOf = world.LookupByTypeOrThrow(); - var Wildcard = world.LookupByTypeOrThrow(); + var ChildOf = world.Entity(); + var Wildcard = world.Entity(); - if (ImGui.CollapsingHeader($"As {Icon.Tag} Entity", ImGuiTreeNodeFlags.DefaultOpen)) - foreach (var iter in Iterator.FromTerm(world, new(selected))) + if (ImGui.CollapsingHeader($"As {Icon.Tag} Component", ImGuiTreeNodeFlags.DefaultOpen)) + foreach (var iter in Iterator.FromTerm(world, new(selected))) for (var i = 0; i < iter.Count; i++) RenderEntity(window, history, iter.Entity(i)); if (ImGui.CollapsingHeader($"As {Icon.ShareAlt} Relation", ImGuiTreeNodeFlags.DefaultOpen)) - foreach (var iter in Iterator.FromTerm(world, new(selected, Wildcard))) { + foreach (var iter in Iterator.FromTerm(world, new(selected, Wildcard))) { var id = iter.FieldId(1); - if (id.AsPair() is not (EntityRef relation, EntityRef target)) throw new InvalidOperationException(); + if (id.AsPair() is not (Entity relation, Entity target)) throw new InvalidOperationException(); if (relation == ChildOf) continue; // Hide ChildOf relations. for (var i = 0; i < iter.Count; i++) { @@ -476,9 +490,9 @@ public partial class EntityInspector } if (ImGui.CollapsingHeader($"As {Icon.Bullseye} Target", ImGuiTreeNodeFlags.DefaultOpen)) - foreach (var iter in Iterator.FromTerm(world, new(Wildcard, selected))) { + foreach (var iter in Iterator.FromTerm(world, new(Wildcard, selected))) { var id = iter.FieldId(1); - if (id.AsPair() is not (EntityRef relation, EntityRef target)) throw new InvalidOperationException(); + if (id.AsPair() is not (Entity relation, Entity target)) throw new InvalidOperationException(); if (relation == ChildOf) continue; // Hide ChildOf relations. for (var i = 0; i < iter.Count; i++) { @@ -489,7 +503,7 @@ public partial class EntityInspector } } - private void DocumentationTab(EntityRef _1, History? _2, EntityRef? selected) + private static void DocumentationTab(Entity _1, History? _2, Entity? selected) { var hasSelected = (selected != null); @@ -515,7 +529,7 @@ public partial class EntityInspector if (!hasSelected) ImGui.BeginDisabled(); var name = selected?.GetDocName(false) ?? ""; if (ImGui.InputText("##Name", ref name, 256)) - selected!.SetDocName((name.Length > 0) ? name : null); + selected?.SetDocName((name.Length > 0) ? name : null); if (!hasSelected) ImGui.EndDisabled(); Column($"{Icon.Comment} Description", @@ -523,7 +537,7 @@ public partial class EntityInspector if (!hasSelected) ImGui.BeginDisabled(); var brief = selected?.GetDocBrief() ?? ""; if (ImGui.InputText("##Brief", ref brief, 256)) - selected!.SetDocBrief((brief.Length > 0) ? brief : null); + selected?.SetDocBrief((brief.Length > 0) ? brief : null); if (!hasSelected) ImGui.EndDisabled(); Column($"{Icon.FileText} Documentation", """ @@ -539,7 +553,7 @@ public partial class EntityInspector // TODO: Needs wordwrap. if (ImGui.InputTextMultiline("##Detail", ref detail, 2048, new(-float.Epsilon, Math.Max(minHeight, availHeight)))) - selected!.SetDocDetail((detail.Length > 0) ? detail : null); + selected?.SetDocDetail((detail.Length > 0) ? detail : null); if (!hasSelected) ImGui.EndDisabled(); Column($"{Icon.Link} Link", """ @@ -549,7 +563,7 @@ public partial class EntityInspector if (!hasSelected) ImGui.BeginDisabled(); var link = selected?.GetDocLink() ?? ""; if (ImGui.InputText("##Link", ref link, 256)) - selected!.SetDocLink((link.Length > 0) ? link : null); + selected?.SetDocLink((link.Length > 0) ? link : null); if (!hasSelected) ImGui.EndDisabled(); Column($"{Icon.PaintBrush} Color", """ @@ -561,15 +575,15 @@ public partial class EntityInspector var hasColor = (maybeColor != null); var color = maybeColor ?? Color.White; if (ImGui.Checkbox("##HasColor", ref hasColor)) { - if (hasColor) selected!.SetDocColor(color.ToHexString()); - else selected!.SetDocColor(null); + if (hasColor) selected?.SetDocColor(color.ToHexString()); + else selected?.SetDocColor(null); } ImGui.SameLine(); if (!hasColor) ImGui.BeginDisabled(); ImGui.SetNextItemWidth(-float.Epsilon); var colorVec = color.ToVector3(); if (ImGui.ColorEdit3("##Color", ref colorVec)) - selected!.SetDocColor(Color.FromRGB(colorVec).ToHexString()); + selected?.SetDocColor(Color.FromRGB(colorVec).ToHexString()); if (!hasColor) ImGui.EndDisabled(); if (!hasSelected) ImGui.EndDisabled(); @@ -582,71 +596,71 @@ public partial class EntityInspector // == Utility Functions == // ======================= - private EntityRef NewEntityInspectorWindow(World world) + private static Entity NewEntityInspectorWindow(World world) => world.New().Add().Set(new History()) .Build().SetDocName(DefaultWindowTitle); - private void SetSelected( - EntityRef window, // The InspectorWindow entity. + private static void SetSelected( + Entity window, // The InspectorWindow entity. History? history, // InspectorWindow's History component, null if it shouldn't be changed. - EntityRef? entity, // Entity to set as selected or null to unset. + Entity? entity, // Entity to set as selected or null to unset. bool scrollTo = true) // Should entity be scrolled to in the explorer view? { - if (entity != null) window.Add(entity); - else window.Remove(); + if (entity is Entity e1) window.Add(e1); + else window.Remove(); - for (var parent = entity?.Parent; parent != null; parent = parent.Parent) + for (var p = entity?.Parent; p is Entity parent; p = parent.Parent) window.Add(parent); if ((entity != null) && scrollTo) window.Add(); if (history != null) { - if (entity != null) history.Current = new History.Entry(entity, history.Current, null); + if (entity is Entity e2) history.Current = new(e2, e2.Path, history.Current, null); else if (history.Current is History.Entry entry) entry.Next = null; } } - private void GoToPrevious(EntityRef window, History history, EntityRef? selected) + private static void GoToPrevious(Entity window, History history, Entity? selected) { if (selected != null) { if (history.Current?.Prev == null) return; history.Current = history.Current.Prev; } else if (history.Current == null) return; - var entity = EntityRef.CreateOrNull(window.World, history.Current.Entity); + var entity = Entity.GetOrNull(window.World, history.Current.Entity); SetSelected(window, null, entity); // TODO: Set path if entity could not be found. } - private void GoToNext(EntityRef window, History history) + private static void GoToNext(Entity window, History history) { if (history.Current?.Next == null) return; history.Current = history.Current.Next; - var entity = EntityRef.CreateOrNull(window.World, history.Current.Entity); + var entity = Entity.GetOrNull(window.World, history.Current.Entity); SetSelected(window, null, entity); // TODO: Set path if entity could not be found. } - private Rule? _findDisplayTypeRule; - private (EntityRef? DisplayType, float Priority) FindDisplayType(EntityRef entity) + private static object? _findDisplayTypeRule; + private static (Entity? DisplayType, float Priority) FindDisplayType(Entity entity) { var world = entity.World; - var component = world.LookupByTypeOrThrow(); + var component = world.Entity(); - var rule = _findDisplayTypeRule ??= new Rule(world, new( - $"$Type, gaemstone.Doc.DisplayType($Type)")); + var rule = (Rule)(_findDisplayTypeRule ??= new Rule(world, new( + $"$Type, gaemstone.Doc.DisplayType($Type)"))); var typeVar = rule.Variables["Type"]!; - var curType = (EntityRef?)null; + var curType = (Entity?)null; var curPriority = float.MaxValue; - foreach (var iter in _findDisplayTypeRule.Iter().SetVar(rule.ThisVar!, entity)) + foreach (var iter in rule.Iter().SetVar(rule.ThisVar!, entity)) for (var i = 0; i < iter.Count; i++) { var type = iter.GetVar(typeVar); - if ((type == component) && (entity.GetOrNull(component)?.Size == 0)) - type = world.LookupByTypeOrThrow(); - var priority = type.GetOrNull()?.Value ?? float.MaxValue; + if ((type == component) && (entity.GetOrNull(component)?.Size == 0)) + type = world.Entity(); + var priority = type?.GetOrNull()?.Value ?? float.MaxValue; if (priority <= curPriority) { curType = type; curPriority = priority; } } @@ -658,24 +672,24 @@ public partial class EntityInspector // == Utility ImGui Functions == // ============================= - private void RenderIdentifier(EntityRef window, History? history, IdRef id) + private static void RenderIdentifier(Entity window, History? history, Id id) { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, ImGui.GetStyle().ItemSpacing.Y)); - if (id.AsPair() is (EntityRef relation, EntityRef target)) { + if (id.AsPair() is (Entity relation, Entity target)) { ImGui.TextUnformatted("("); ImGui.SameLine(); RenderEntity(window, history, relation); ImGui.SameLine(); ImGui.TextUnformatted(","); ImGui.SameLine(); RenderEntity(window, history, target); ImGui.SameLine(); ImGui.TextUnformatted(")"); - } else if (id.AsEntity() is EntityRef entity) + } else if (id.AsEntity() is Entity entity) RenderEntity(window, history, entity); else ImGui.TextUnformatted(id.ToString()); ImGui.PopStyleVar(); } - private void RenderEntity( - EntityRef window, History? history, EntityRef entity, + private static void RenderEntity( + Entity window, History? history, Entity entity, RenderEntityFlags flags = default) { var spanAvailWidth = (flags & RenderEntityFlags.SpanAvailWidth) != 0; @@ -696,7 +710,7 @@ public partial class EntityInspector var docName = entity.GetDocName(false); var isDisabled = entity.IsDisabled; - var displayName = (docName != null) ? $"\"{docName}\"" : entity.Name ?? entity.Id.ToString(); + var displayName = (docName != null) ? $"\"{docName}\"" : entity.Name ?? entity.NumericId.ToString(); if (docIcon != null) displayName = $"{docIcon} {displayName}"; // Gotta push the font to calculate size properly. @@ -715,10 +729,10 @@ public partial class EntityInspector : Color.Transparent.RGBA); ImGui.PushStyleColor(ImGuiCol.ButtonActive , ImGui.GetColorU32(ImGuiCol.HeaderActive)); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.HeaderHovered)); - ImGui.Button($"##{entity.Id}", size); + ImGui.Button($"##{entity.NumericId}", size); ImGui.PopStyleColor(3); ImGui.PopStyleVar(); - } else ImGui.InvisibleButton($"##{entity.Id}", size); + } else ImGui.InvisibleButton($"##{entity.NumericId}", size); var shift = ImGui.IsKeyDown(ImGuiKey.ModShift); var hovered = ImGui.IsItemHovered(); @@ -746,7 +760,7 @@ public partial class EntityInspector if (hovered) { ImGui.BeginTooltip(); ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]); - ImGui.TextUnformatted(entity.GetFullPath().ToString()); + ImGui.TextUnformatted(entity.Path.ToString()); ImGui.PopFont(); if (isDisabled) { ImGui.SameLine(); diff --git a/src/gaemstone.Client/Systems/FreeCameraController.cs b/src/gaemstone.Client/Systems/FreeCameraController.cs index 5b4fb7c..bf3c1f5 100644 --- a/src/gaemstone.Client/Systems/FreeCameraController.cs +++ b/src/gaemstone.Client/Systems/FreeCameraController.cs @@ -21,24 +21,25 @@ public partial class FreeCameraController } [System] - public static void UpdateCamera( - Universe universe, TimeSpan delta, - in Camera camera, ref GlobalTransform transform, ref CameraController controller) + public static void UpdateCamera( + World world, TimeSpan delta, + in Camera camera, ref GlobalTransform transform, in CameraController controller) { - var input = universe.LookupByType(); - var mouse = universe.LookupByType(); - var keyboard = universe.LookupByType(); - if ((input == null) || (mouse == null) || (keyboard == null)) return; + var input = world.Entity(); + var mouse = world.Entity(); + var keyboard = world.Entity(); + // FIXME: Is it okay if this crashes? + // if ((input == null) || (mouse == null) || (keyboard == null)) return; - var module = universe.LookupByTypeOrThrow(); + var module = world.Entity(); var capturedBy = input.GetTargets().FirstOrDefault(); var inputCapturedBy = input.GetTargets().FirstOrDefault(); - var isCaptured = (capturedBy != null); + var isCaptured = capturedBy.IsSome; // If another system has the mouse captured, don't do anything here. if (isCaptured && (capturedBy != module)) return; - var isMouseDown = ((inputCapturedBy == null) || (inputCapturedBy == module)) - && mouse.LookupChild("Buttons/Right")?.Has() == true; + var isMouseDown = (inputCapturedBy.IsNone || (inputCapturedBy == module)) + && mouse.LookupChildOrNull("Buttons/Right")?.Has() == true; if (isMouseDown != isCaptured) { if (isMouseDown) input.Add(module); @@ -51,18 +52,18 @@ public partial class FreeCameraController var mouseMovement = Vector2.Zero; if (isCaptured) { - var raw = (Vector2?)mouse.LookupChild("Delta")?.GetOrThrow() ?? default; + var raw = (Vector2?)mouse.LookupChildOrNull("Delta")?.GetOrThrow() ?? default; mouseMovement = raw * controller.MouseSensitivity * (float)delta.TotalSeconds; } if (camera.IsOrthographic) { transform *= Matrix4x4.CreateTranslation(-mouseMovement.X, -mouseMovement.Y, 0); } else { - var shift = keyboard.LookupChild("ShiftLeft")?.Has() == true; - var w = keyboard.LookupChild("W")?.Has() == true; - var a = keyboard.LookupChild("A")?.Has() == true; - var s = keyboard.LookupChild("S")?.Has() == true; - var d = keyboard.LookupChild("D")?.Has() == true; + var shift = keyboard.LookupChildOrNull("ShiftLeft")?.Has() == true; + var w = keyboard.LookupChildOrNull("W")?.Has() == true; + var a = keyboard.LookupChildOrNull("A")?.Has() == true; + var s = keyboard.LookupChildOrNull("S")?.Has() == true; + var d = keyboard.LookupChildOrNull("D")?.Has() == true; var speed = (shift ? 12 : 4) * (float)delta.TotalSeconds; var forwardMovement = ((w ? -1 : 0) + (s ? 1 : 0)) * speed; diff --git a/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs b/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs index 0534b30..85ce160 100644 --- a/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs +++ b/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs @@ -9,10 +9,10 @@ namespace gaemstone.Client.Systems; [DependsOn] public partial class ImGuiDemoWindow { - private bool _isOpen = false; + private static bool _isOpen = false; [System] - public void Show(ImGuiData _) + public static void Show(ImGuiData _) { if (ImGuiUtility.UIButtonToggle(2, ForkAwesome.WindowMaximize, "ImGui Demo Window", ref _isOpen)) ImGui.ShowDemoWindow(ref _isOpen); diff --git a/src/gaemstone.Client/Systems/ImGuiInputDebug.cs b/src/gaemstone.Client/Systems/ImGuiInputDebug.cs index ee23dc7..36ce36c 100644 --- a/src/gaemstone.Client/Systems/ImGuiInputDebug.cs +++ b/src/gaemstone.Client/Systems/ImGuiInputDebug.cs @@ -17,13 +17,12 @@ namespace gaemstone.Client.Systems; [DependsOn] public partial class ImGuiInputDebug { - private bool _isOpen = false; + private static bool _isOpen = false; [System] - public void ShowInputDebugWindow(Universe universe, ImGuiData _) + public static void ShowInputDebugWindow(World world, ImGuiData _) { - var input = universe.LookupByType(); - if (input == null) return; + var input = world.Entity(); if (!ImGuiUtility.UIButtonToggle(1, ForkAwesome.Gamepad, "Input Information", ref _isOpen)) return; @@ -34,19 +33,20 @@ public partial class ImGuiInputDebug ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize)) { ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[0]); - if (universe.LookupByType() is EntityRef keyboard) - DrawKeyboard(keyboard); + var keyboard = world.Entity(); + DrawKeyboard(keyboard); - if (universe.LookupByType() is EntityRef mouse) { + var mouse = world.Entity(); + { ImGui.BeginChild("Mouse Info", new(160, 180), true); - ImGui.Text("Position: " + (Vector2?)mouse.LookupChild("Position")?.GetOrNull()); - ImGui.Text("Delta: " + (Vector2?)mouse.LookupChild("Delta" )?.GetOrNull()); - ImGui.Text("Wheel: " + (float?) mouse.LookupChild("Wheel" )?.GetOrNull()); + ImGui.Text("Position: " + (Vector2?)mouse.LookupChildOrNull("Position")?.GetOrNull()); + ImGui.Text("Delta: " + (Vector2?)mouse.LookupChildOrNull("Delta" )?.GetOrNull()); + ImGui.Text("Wheel: " + (float?) mouse.LookupChildOrNull("Wheel" )?.GetOrNull()); ImGui.Spacing(); - var buttons = mouse.LookupChild("Buttons")?.GetChildren().ToArray() ?? Array.Empty(); + var buttons = mouse.LookupChildOrNull("Buttons")?.Children.ToArray() ?? Array.Empty>(); ImGui.Text("Buttons: " + string.Join(" ", buttons .Where (button => button.Has()) .Select(button => $"{button.Name} ({button.GetOrThrow().Duration.TotalSeconds:f2}s)"))); @@ -60,11 +60,11 @@ public partial class ImGuiInputDebug ImGui.EndChild(); } - for (var index = 0; input.LookupChild("Gamepad" + index) is EntityRef gamepad; index++) { + for (var index = 0; input.LookupChildOrNull("Gamepad" + index) is Entity gamepad; index++) { ImGui.SameLine(); ImGui.BeginChild($"{gamepad.Name} Info", new(160, 180), true); - var buttons = gamepad.LookupChild("Buttons")?.GetChildren().ToArray() ?? Array.Empty(); + var buttons = gamepad.LookupChildOrNull("Buttons")?.Children.ToArray() ?? Array.Empty>(); ImGui.Text("Buttons: " + string.Join(" ", buttons.Where(b => b.Has()) .Select(b => $"{b.Name} ({b.GetOrThrow().Duration.TotalSeconds:f2}s)"))); ImGui.Text(" Pressed: " + string.Join(" ", buttons.Where(b => b.Has()).Select(b => b.Name))); @@ -73,7 +73,7 @@ public partial class ImGuiInputDebug ImGui.Spacing(); ImGui.Text("Triggers:"); - for (var i = 0; gamepad.LookupChild("Trigger" + i) is EntityRef trigger; i++) { + for (var i = 0; gamepad.LookupChildOrNull("Trigger" + i) is Entity trigger; i++) { var text = $" {i}: {(float?)trigger.GetOrNull() ?? default:f2}"; if (trigger.Has()) text += " pressed!"; else if (trigger.Has()) text += " released!"; @@ -83,7 +83,7 @@ public partial class ImGuiInputDebug } ImGui.Text("Thumbsticks:"); - for (var i = 0; gamepad.LookupChild("Thumbstick" + i) is EntityRef thumbstick; i++) + for (var i = 0; gamepad.LookupChildOrNull("Thumbstick" + i) is Entity thumbstick; i++) ImGui.Text($" {i}: {(Vector2?)thumbstick.GetOrNull() ?? default:f2}"); ImGui.EndChild(); @@ -139,7 +139,7 @@ public partial class ImGuiInputDebug [Key.NumLock] = "Num\nLck", [Key.KeypadEnter] = "=", }; - public static void DrawKeyboard(EntityRef keyboard) + public static void DrawKeyboard(Entity keyboard) { const float UnitKeySize = 32.0f; @@ -150,10 +150,10 @@ public partial class ImGuiInputDebug var current = Vector2.Zero; foreach (var (widths, keys) in KeyboardLayout.Zip(KeyboardKeys)) { foreach (var (width, key) in widths.Zip(keys)) { - var active = (key != null) && (keyboard.LookupChild(key.Value.ToString())?.Has() == true); + var active = (key != null) && (keyboard.LookupChildOrNull(key.Value.ToString())?.Has() == true); var keySize = new Vector2(width, 1.0f); - if (width == T) keySize = new Vector2((-keySize.X - 10), 2.0f); + if (width == ImGuiInputDebug.T) keySize = new Vector2((-keySize.X - 10), 2.0f); else if (width < -10) keySize = new Vector2((-keySize.X - 10), 1.0f); else if (width < 0) { current += new Vector2(-keySize.X * UnitKeySize, 0); continue; } diff --git a/src/gaemstone.Client/Systems/ImGuiManager.cs b/src/gaemstone.Client/Systems/ImGuiManager.cs index c9a4094..acd024d 100644 --- a/src/gaemstone.Client/Systems/ImGuiManager.cs +++ b/src/gaemstone.Client/Systems/ImGuiManager.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Text; using gaemstone.Client.Utility; using gaemstone.ECS; +using gaemstone.ECS.Utility; using gaemstone.Flecs; -using gaemstone.Utility; using ImGuiNET; using Silk.NET.Input; using Silk.NET.OpenGL.Extensions.ImGui; @@ -22,14 +22,17 @@ namespace gaemstone.Client.Systems; public partial class ImGuiManager { [Entity, Add] - [DependsOn] + [DependsOn] public struct ImGuiUpdatePhase { } [Entity, Add] - [DependsOn] + [DependsOn] public struct ImGuiRenderPhase { } - [Singleton(AutoAdd = false)] + // Start out disabled, because we first need to set it with a valid instance. + // TODO: Add a hint when a Singleton class is used without adding Disabled. + // TODO: Add a [Disabled] attribute as shortcut? + [Singleton, Add] public class ImGuiData { public ImGuiController Controller { get; } @@ -84,10 +87,11 @@ public partial class ImGuiManager return io.Fonts.AddFontFromMemoryTTF((IntPtr)dataPtr, size, size, cfg); } - [System] - public unsafe void Initialize(Universe universe, GameWindow window, Canvas canvas, - [Source] InputContext inputContext, [Not] ImGuiData _) - => universe.LookupByTypeOrThrow().Set(new ImGuiData( + [System] + [DependsOn] + public static unsafe void Initialize(World world, GameWindow window, Canvas canvas, + [Source] InputContext inputContext, Not _) + => world.Entity().Enable().Set(new ImGuiData( new(canvas.GL, window.Handle, inputContext.Value, () => { var io = ImGui.GetIO(); var style = ImGui.GetStyle(); @@ -139,15 +143,16 @@ public partial class ImGuiManager } }))); - [System] - public static void UpdateMouse(Universe universe, + [System] + [DependsOn] + public static void UpdateMouse(World world, [Source] MouseImpl impl, ImGuiData _) { var mouse = impl.Value; - var input = universe.LookupByTypeOrThrow(); - var module = universe.LookupByTypeOrThrow(); + var input = world.Entity(); + var module = world.Entity(); var capturedBy = input.GetTargets().FirstOrDefault();; - var isCaptured = (capturedBy != null); + var isCaptured = capturedBy.IsSome; // If another system has the mouse captured, don't do anything here. if (isCaptured && (capturedBy != module)) return; @@ -180,11 +185,13 @@ public partial class ImGuiManager }; } - [System] + [System] + [DependsOn] public static void Update(TimeSpan delta, ImGuiData imgui) => imgui.Controller.Update((float)delta.TotalSeconds); - [System] + [System] + [DependsOn] public static void Render(ImGuiData imgui) => imgui.Controller.Render(); } diff --git a/src/gaemstone.Client/Systems/InputManager.cs b/src/gaemstone.Client/Systems/InputManager.cs index 125210d..f35b5d1 100644 --- a/src/gaemstone.Client/Systems/InputManager.cs +++ b/src/gaemstone.Client/Systems/InputManager.cs @@ -21,11 +21,12 @@ public partial class InputManager [Component] public record class KeyboardImpl(IKeyboard Value) { } [Component] public record class GamepadImpl(IGamepad Value) { } - [System] - public static void Initialize(Universe universe, - [Game] GameWindow window, [Source, Not] InputContext _) + [System] + [DependsOn] + public static void Initialize(World world, + GameWindow window, [Source] Not _) { - var input = universe.LookupByTypeOrThrow(); + var input = world.Entity(); var context = window.Handle.CreateInput(); input.Set(new InputContext(context)); @@ -42,24 +43,25 @@ public partial class InputManager } - [Observer] - [Expression("CursorCapturedBy(Input, *)")] - public static void OnCursorCaptured(Universe universe) - => universe.LookupByTypeOrThrow().GetOrThrow() + [Observer] + public static void OnCursorCaptured(World universe, + [Source] Has _) + => universe.Entity().GetOrThrow() .Value.Cursor.CursorMode = CursorMode.Raw; - [Observer] - [Expression("CursorCapturedBy(Input, *)")] - public static void OnCursorReleased(Universe universe) - => universe.LookupByTypeOrThrow().GetOrThrow() + [Observer] + public static void OnCursorReleased(World universe, + [Source] Has _) + => universe.Entity().GetOrThrow() .Value.Cursor.CursorMode = CursorMode.Normal; - [System] - public static void ProcessMouse(TimeSpan delta, EntityRef entity, MouseImpl impl) + [System] + [DependsOn] + public static void ProcessMouse(TimeSpan delta, Entity entity, MouseImpl impl) { var mouse = impl.Value; - var isCaptured = entity.Parent!.Has(); + var isCaptured = entity.Parent?.Has() ?? false; ref var position = ref entity.NewChild("Position").Build().GetMut(); ref var posDelta = ref entity.NewChild("Delta" ).Build().GetMut(); posDelta = mouse.Position - position; @@ -74,8 +76,9 @@ public partial class InputManager mouse.IsButtonPressed(button) ? 1 : 0); } - [System] - public static void ProcessKeyboard(TimeSpan delta, EntityRef entity, KeyboardImpl impl) + [System] + [DependsOn] + public static void ProcessKeyboard(TimeSpan delta, Entity entity, KeyboardImpl impl) { var keyboard = impl.Value; foreach (var key in keyboard.SupportedKeys) { @@ -84,8 +87,9 @@ public partial class InputManager } } - [System] - public static void ProcessGamepad(TimeSpan delta, EntityRef entity, GamepadImpl impl) + [System] + [DependsOn] + public static void ProcessGamepad(TimeSpan delta, Entity entity, GamepadImpl impl) { var gamepad = impl.Value; var buttons = entity.NewChild("Buttons").Build(); @@ -101,7 +105,7 @@ public partial class InputManager private const float ActivationThreshold = 0.90f; private const float DeactivationThreshold = 0.75f; - private static void Update1D(TimeSpan delta, EntityRef entity, float current) + private static void Update1D(TimeSpan delta, Entity entity, float current) { entity.GetMut() = current; if (current >= ActivationThreshold) { @@ -114,7 +118,7 @@ public partial class InputManager entity.Remove(); } - private static void Update2D(TimeSpan delta, EntityRef entity, Vector2 current) + private static void Update2D(TimeSpan delta, Entity entity, Vector2 current) { entity.GetMut() = current; var magnitude = current.Length(); @@ -134,11 +138,12 @@ public partial class InputManager // public static void OnActiveAdded(EntityRef entity, Active _) // => entity.Add(); - [Observer] - public static void OnActiveRemoved(EntityRef entity, Active _) + [Observer] + public static void OnActiveRemoved(Entity entity, Active _) => entity.Add(); - [System] - public static void ClearDeActivated(EntityRef entity, [Or] Activated _1, [Or] Deactivated _2) + [System] + [DependsOn] + public static void ClearDeActivated(Entity entity, Has> _) => entity.Remove().Remove(); } diff --git a/src/gaemstone.Client/Systems/MeshManager.cs b/src/gaemstone.Client/Systems/MeshManager.cs index 3966d25..e28bfee 100644 --- a/src/gaemstone.Client/Systems/MeshManager.cs +++ b/src/gaemstone.Client/Systems/MeshManager.cs @@ -21,12 +21,11 @@ public partial class MeshManager private const uint UvAttribIndex = 2; [System] - public static void LoadMeshWhenDefined( - [Game] Canvas canvas, EntityRef entity, - Mesh _1, [Not] MeshHandle _2) + public static void LoadMeshWhenDefined( + Canvas canvas, Entity entity, + Has _1, Not _2) { - var path = entity.GetFullPath(); - using var stream = Resources.GetStream(path); + using var stream = Resources.GetStream(entity.Path); var handle = CreateFromStream(canvas.GL, stream); entity.Set(handle); } diff --git a/src/gaemstone.Client/Systems/Renderer.cs b/src/gaemstone.Client/Systems/Renderer.cs index e50499a..c7ba7ac 100644 --- a/src/gaemstone.Client/Systems/Renderer.cs +++ b/src/gaemstone.Client/Systems/Renderer.cs @@ -3,8 +3,8 @@ using System.Diagnostics; using System.Numerics; using System.Runtime.InteropServices; using gaemstone.ECS; +using gaemstone.ECS.Utility; using gaemstone.Flecs; -using gaemstone.Utility; using Silk.NET.OpenGL; using Silk.NET.Windowing; using static gaemstone.Client.Components.CameraComponents; @@ -21,13 +21,13 @@ namespace gaemstone.Client.Systems; [DependsOn] public partial class Renderer { - private uint _program; - private int _cameraMatrixUniform; - private int _modelMatrixUniform; - private Rule? _renderEntityRule; + private static uint _program; + private static int _cameraMatrixUniform; + private static int _modelMatrixUniform; + private static object? _renderEntityRule; - [Observer] - public void OnCanvasSet(Canvas canvas) + [Observer] + public static void OnCanvasSet(Canvas canvas) { var GL = canvas.GL; @@ -54,8 +54,9 @@ public partial class Renderer _modelMatrixUniform = GL.GetUniformLocation(_program, "modelMatrix"); } - [System] - public void Clear(Canvas canvas) + [System] + [DependsOn] + public static void Clear(Canvas canvas) { var GL = canvas.GL; GL.UseProgram(_program); @@ -64,8 +65,9 @@ public partial class Renderer GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); } - [System] - public void Render(Universe universe, [Game] Canvas canvas, + [System] + [DependsOn] + public static void Render(World world, Canvas canvas, in GlobalTransform cameraTransform, in Camera camera, CameraViewport? viewport) { var color = viewport?.ClearColor ?? Color.FromRGB(0.3f, 0.0f, 0.5f); @@ -93,12 +95,12 @@ public partial class Renderer var cameraMatrix = invertedTransform * cameraProjection; GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.M11); - _renderEntityRule ??= new(universe, new(""" + var rule = (Rule)(_renderEntityRule ??= new Rule(world, new(""" [in] GlobalTransform, (Mesh, $mesh), [in] MeshHandle($mesh), ?(Texture, $tex), [in] ?TextureHandle($tex) - """)); - foreach (var iter in _renderEntityRule.Iter()) { + """))); + foreach (var iter in rule.Iter()) { var transforms = iter.Field(1); var meshes = iter.Field(3); // var texPairs = iter.FieldOrEmpty(4); @@ -125,7 +127,8 @@ public partial class Renderer } } - [System] + [System] + [DependsOn] public static void SwapBuffers(GameWindow window) => window.Handle.SwapBuffers(); diff --git a/src/gaemstone.Client/Systems/TextureManager.cs b/src/gaemstone.Client/Systems/TextureManager.cs index ab2eabd..f4fc310 100644 --- a/src/gaemstone.Client/Systems/TextureManager.cs +++ b/src/gaemstone.Client/Systems/TextureManager.cs @@ -17,7 +17,7 @@ namespace gaemstone.Client.Systems; [DependsOn] public partial class TextureManager { - [Observer] + [Observer] public static void OnCanvasSet(Canvas canvas) { var GL = canvas.GL; @@ -31,12 +31,11 @@ public partial class TextureManager } [System] - public static void LoadTextureWhenDefined( - [Game] Canvas canvas, EntityRef entity, - Texture _1, [Not] TextureHandle _2) + public static void LoadTextureWhenDefined( + Canvas canvas, Entity entity, + Has _1, Not _2) { - var path = entity.GetFullPath(); - using var stream = Resources.GetStream(path); + using var stream = Resources.GetStream(entity.Path); var handle = CreateFromStream(canvas.GL, stream); entity.Set(handle); } diff --git a/src/gaemstone.Client/Systems/Windowing.cs b/src/gaemstone.Client/Systems/Windowing.cs index 19e3cd7..e3e46e8 100644 --- a/src/gaemstone.Client/Systems/Windowing.cs +++ b/src/gaemstone.Client/Systems/Windowing.cs @@ -9,7 +9,9 @@ namespace gaemstone.Client.Systems; [Module] public partial class Windowing { - [Component] + // TODO: Canvas and GameWindow should maybe not be Singleton? + + [Singleton] public class Canvas { public GL GL { get; } @@ -19,14 +21,15 @@ public partial class Windowing public Color BackgroundColor { get; set; } } - [Component] + [Singleton] public class GameWindow { public IWindow Handle { get; } public GameWindow(IWindow handle) => Handle = handle; } - [System] + [System] + [DependsOn] public static void ProcessWindow(GameWindow window, Canvas canvas) { canvas.Size = new(window.Handle.Size.X, window.Handle.Size.Y); diff --git a/src/gaemstone.Client/gaemstone.Client.csproj b/src/gaemstone.Client/gaemstone.Client.csproj index 57f8f39..b511224 100644 --- a/src/gaemstone.Client/gaemstone.Client.csproj +++ b/src/gaemstone.Client/gaemstone.Client.csproj @@ -6,10 +6,11 @@ true disable enable + true - + diff --git a/src/gaemstone.ECS b/src/gaemstone.ECS index 46e1719..f7d17d4 160000 --- a/src/gaemstone.ECS +++ b/src/gaemstone.ECS @@ -1 +1 @@ -Subproject commit 46e171940eea723f2ad8376b5056afd9b9d8a340 +Subproject commit f7d17d46ab68ae91487d5d6275c3a501e1ee0980 diff --git a/src/gaemstone.SourceGen/Descriptors.cs b/src/gaemstone.SourceGen/Descriptors.cs new file mode 100644 index 0000000..9e11d24 --- /dev/null +++ b/src/gaemstone.SourceGen/Descriptors.cs @@ -0,0 +1,220 @@ +using Microsoft.CodeAnalysis; + + +namespace gaemstone.SourceGen; + +public static class Descriptors +{ + public const string DiagnosticCategory = "gaemstone.SourceGen"; + + // TODO: Replace this counter with proper hardcoded IDs. + private static int _idCounter = 1; + + + // Diagnostics relating to where the symbol occurs. + + public static readonly DiagnosticDescriptor ModuleMustNotBeNested = new( + $"gSG{_idCounter++:00}", "Module must not be a nested class", + "A [Module] must be defined as a top-level class.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor EntityMustBeInModule = new( + $"gSG{_idCounter++:00}", "Entity must be part of a Module", + "Entity type must be defined within a [Module].", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor InstanceMethodOnlyValidInSingleton = new( + $"gSG{_idCounter++:00}", "Non-static method is only valid in Singleton Module", + "Non-static [System] or [Observer] is only valid in a [Module] which is also marked as a [Singleton].", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor ParamMustBeInMethod = new( + $"gSG{_idCounter++:00}", "Parameter must be part of a System or Observer", + "This parameter must be part of a method marked as [System] or [Observer].", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + + // Diagnostics relating to the combined usage of attributes. + + public static readonly DiagnosticDescriptor InvalidAttributeCombination = new( + $"gSG{_idCounter++:00}", "Invalid combination of entity attributes", + "The combination of entity attributes {0} is not valid.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor ValidModuleAttributesHint = new( + $"gSG{_idCounter++:00}", "Module may be a Singleton", + "A [Module] may be marked as a [Singleton].", + DiagnosticCategory, DiagnosticSeverity.Info, true); + + public static readonly DiagnosticDescriptor ValidRelationAttributesHint = new( + $"gSG{_idCounter++:00}", "Relation may be a Tag or Component", + "A [Relation] may be marked as a [Tag] or [Component].", + DiagnosticCategory, DiagnosticSeverity.Info, true); + + public static readonly DiagnosticDescriptor SingletonImpliesComponentHint = new( + $"gSG{_idCounter++:00}", "Singleton implies Component", + "A [Singleton] is already implied to be a [Component].", + DiagnosticCategory, DiagnosticSeverity.Info, true); + + public static readonly DiagnosticDescriptor BuiltInModuleMustHavePath = new( + $"gSG{_idCounter++:00}", "BuiltIn Module must have Path", + "A [BuiltIn, Module] must also have a [Path] set.", + DiagnosticCategory, DiagnosticSeverity.Info, true); + + + + + // Diagnostics relating to keywords / modifiers used with the symbol. + + public static readonly DiagnosticDescriptor ModuleMustBePartial = new( + $"gSG{_idCounter++:00}", "ModuleMustBePartial", + "A [Module] type must be marked as partial.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor TypeMustNotBeStatic = new( + $"gSG{_idCounter++:00}", "Entity type must not be static", + "Entity type must not be static.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor TypeMustNotBeAbstract = new( + $"gSG{_idCounter++:00}", "Entity type must not be abstract", + "Entity type must not be abstract.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + + public static readonly DiagnosticDescriptor MethodMustNotBeAbstract = new( + $"gSG{_idCounter++:00}", "System / Observer must not be abstract", + "A [System] or [Observer] method must not be marked as abstract.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor MethodMustNotBeAsync = new( + $"gSG{_idCounter++:00}", "System / Observer must not be async", + "A [System] or [Observer] method must not be marked as async.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + // TODO: Check for any other weird modifiers we don't want. + + + // Diagnostics relating to (generic) parameters on the attributes themselves. + + // TODO: Be more specific? + public static readonly DiagnosticDescriptor InvalidTypeArgument = new( + $"gSG{_idCounter++:00}", "Invalid type argument", + "The specified type argument is not valid here.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + + // Diagnostics relating to system / observer generic parameters. + + public static readonly DiagnosticDescriptor MethodMustNotBeExtension = new( + $"gSG{_idCounter++:00}", "System / Observer must not be extension method", + "A [System] or [Observer] method must not be an extension method.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor MethodMustHaveParameters = new( + $"gSG{_idCounter++:00}", "System / Observer must have parameters", + "A [System] or [Observer] must have parameters.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor MethodMustHaveExpression = new( + $"gSG{_idCounter++:00}", "Iterator-only System / Observer must have expression", + "An Iterator-only [System] or [Observer] must have an [Expression] set.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + + public static readonly DiagnosticDescriptor MethodGenericParamAtMostOne = new( + $"gSG{_idCounter++:00}", "System / Observer must have at most one generic parameter", + "A [System] or [Observer] must at most one generic parameter.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + // TODO: See if we can change this wording, but try to use the correct one. (Does "open generic" work?) + public static readonly DiagnosticDescriptor MethodGenericParamMustNotBeSubstutited = new( + $"gSG{_idCounter++:00}", "System / Observer generic parameter must not be substituted", + "The generic parameter of a [System] or [Observer] must not be substituted.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor MethodGenericParamMustNotBeConstrained = new( + $"gSG{_idCounter++:00}", "System / Observer generic parameter must not be substituted", + "The generic parameter of a [System] or [Observer] must not be constrained.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + + // Diagnostics relating to system / observer parameters. + + public static readonly DiagnosticDescriptor ParamMustNotBeArray = new( + $"gSG{_idCounter++:00}", "Parameter must not be array", + "Parameter must not be an array type.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor ParamMustNotBePointer = new( + $"gSG{_idCounter++:00}", "Parameter must not be pointer", + "Parameter must not be a pointer type.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor ParamMustNotBeGenericType = new( + $"gSG{_idCounter++:00}", "Parameter must not be generic type", + "Parameter must not be a generic type parameter.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor ParamMustNotBePrimitive = new( + $"gSG{_idCounter++:00}", "Parameter must not be primitive", + "Parameter must not be a primitive type.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor ParamMustNotBeGeneric = new( + $"gSG{_idCounter++:00}", "Parameter must not be geric", + "Parameter must not be generic (except Iterator, World, Entity, Nullable, Has, Not and Or).", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor ParamDoesNotSupportOptional = new( + $"gSG{_idCounter++:00}", "Parameter does not support optional", + "Parameter does not support optional value syntax.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor ParamByRefMustBeValueType = new( + $"gSG{_idCounter++:00}", "ByRef parameter must be a value type", + "ByRef (in/out/ref) parameter must be a value type (struct).", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor ParamByRefMustNotBeNullable = new( + $"gSG{_idCounter++:00}", "ByRef parameter must not be nullable", + "ByRef (in/out/ref) parameter must not be nullable.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + + public static readonly DiagnosticDescriptor SpecialMustNotBeByRef = new( + $"gSG{_idCounter++:00}", "Special parameter must not be ByRef", + "Special parameter must not be ByRef (in/out/ref).", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor SpecialArgMustNotBeNullable = new( + $"gSG{_idCounter++:00}", "Special type argument must not be nullable", + "Special type argument must not be nullable.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor SpecialArgMustNotBeGeneric = new( + $"gSG{_idCounter++:00}", "Special type argument must not be generic", + "Special type argument must not be generic.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + + public static readonly DiagnosticDescriptor UniqueParamMustNotBeByRef = new( + $"gSG{_idCounter++:00}", "Unique parameter must not be ByRef", + "Unique parameter must not be ByRef (in/out/ref).", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor UniqueParamMustNotBeNullable = new( + $"gSG{_idCounter++:00}", "Unique parameter must not be nullable", + "Unique parameter must not be nullable.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor UniqueParamGenericMustMatch = new( + $"gSG{_idCounter++:00}", "Unique parameter generic type parameter must match method", + "Unique parameter's generic type parameter must match the method's type parameter.", + DiagnosticCategory, DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor UniqueParamNotSupported = new( + $"gSG{_idCounter++:00}", "Unique parameter does not support this attribute", + "Unique parameter does not support {0}", + DiagnosticCategory, DiagnosticSeverity.Error, true); +} diff --git a/src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs b/src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs deleted file mode 100644 index 4fa5a5c..0000000 --- a/src/gaemstone.SourceGen/Generators/AutoRegisterComponentsGenerator.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using gaemstone.SourceGen.Utility; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace gaemstone.SourceGen.Generators; - -// TODO: "Jump to Definition" feature to open the file + line in which a module / component / system is defined. - -[Generator] -public partial class AutoRegisterComponentsGenerator - : ISourceGenerator -{ - private static readonly DiagnosticDescriptor ComponentNotPartOfModule = new( - "gaem0101", "Components must be part of a module", - "Type {0} isn't defined as part of a [Module]", - nameof(AutoRegisterComponentsGenerator), DiagnosticSeverity.Error, true); - - private static readonly DiagnosticDescriptor ComponentMultipleTypes = new( - "gaem0102", "Components may not have multiple component types", - "Type {0} is marked with multiple component types ({1})", - nameof(AutoRegisterComponentsGenerator), DiagnosticSeverity.Error, true); - - private static readonly DiagnosticDescriptor ComponentRelationInvalidType = new( - "gaem0103", "Relations may only be marked with [Component] or [Tag]", - "Type {0} marked as [Relation] may not be marked with [{1}], only [Component] or [Tag] are valid", - nameof(AutoRegisterComponentsGenerator), DiagnosticSeverity.Error, true); - - - public void Initialize(GeneratorInitializationContext context) - => context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - - private class SyntaxReceiver - : ISyntaxContextReceiver - { - public Dictionary> 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.IsPublic) 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 IsPublic { get; } - public bool IsStatic => Symbol.IsStatic; - - public ModuleInfo(INamedTypeSymbol symbol) - { - Symbol = symbol; - Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")? - .ConstructorArguments.FirstOrDefault().Value as string; - IsPublic = symbol.HasAttribute("gaemstone.ECS.PublicAttribute"); - } - } - - private class ComponentInfo - { - public INamedTypeSymbol Symbol { get; } - public string Name => Symbol.Name; - - public ModuleInfo Module { get; } - public RegisterType Type { get; } - public List EntitiesToAdd { get; } - public List<(ITypeSymbol, ITypeSymbol)> RelationsToAdd { get; } - - public string? Path { get; } - public bool IsPublic { get; } - public bool IsRelation { get; } - public bool IsTag => (Type is RegisterType.Tag); - public bool IsComponent => (Type is RegisterType.Component - or RegisterType.Singleton); - public bool IsSingleton => (Type is RegisterType.Singleton); - public bool SingletonAddSelf { get; } - - public ComponentInfo(INamedTypeSymbol symbol, ModuleInfo module, RegisterType type, - List entitiesToAdd, List<(ITypeSymbol, ITypeSymbol)> relationsToAdd) - { - Symbol = symbol; - Module = module; - Type = type; - - EntitiesToAdd = entitiesToAdd; - RelationsToAdd = relationsToAdd; - - Path = symbol.GetAttribute("gaemstone.ECS.PathAttribute")? - .ConstructorArguments.FirstOrDefault().Value as string; - IsPublic = symbol.HasAttribute("gaemstone.ECS.PublicAttribute") - || (Module.IsPublic && !symbol.HasAttribute("gaemstone.ECS.PrivateAttribute")); - IsRelation = symbol.HasAttribute("gaemstone.ECS.RelationAttribute"); - - SingletonAddSelf = IsSingleton - && (symbol.GetAttribute("gaemstone.ECS.SingletonAttribute")! - .NamedArguments.FirstOrDefault() is not ("AutoAdd", TypedConstant typedConst) - || (bool)typedConst.Value!); - } - } - - private enum RegisterType - { - Entity, - Singleton, - Relation, - Component, - Tag, - } -} diff --git a/src/gaemstone.SourceGen/Generators/ModuleGenerator.cs b/src/gaemstone.SourceGen/Generators/ModuleGenerator.cs deleted file mode 100644 index 880f4e8..0000000 --- a/src/gaemstone.SourceGen/Generators/ModuleGenerator.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; -using gaemstone.SourceGen.Utility; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace gaemstone.SourceGen.Generators; - -[Generator] -public class ModuleGenerator - : ISourceGenerator -{ - private static readonly DiagnosticDescriptor ModuleMayNotBeNested = new( - "gaem0001", "Module may not be nested", - "Type {0} marked with [Module] may not be a nested type", - nameof(ModuleGenerator), DiagnosticSeverity.Error, true); - - private static readonly DiagnosticDescriptor ModuleMustBePartial = new( - "gaem0002", "Module must be partial", - "Type {0} marked with [Module] must be a partial type", - nameof(ModuleGenerator), DiagnosticSeverity.Error, true); - - private static readonly DiagnosticDescriptor ModuleBuiltInMustHavePath = new( - "gaem0003", "Built-in module must have [Path]", - "Type {0} marked with [Module] is a built-in module (static), and therefore must have [Path] set", - nameof(ModuleGenerator), DiagnosticSeverity.Error, true); - - - public void Initialize(GeneratorInitializationContext context) - => context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - - private class SyntaxReceiver - : ISyntaxContextReceiver - { - public HashSet Symbols { get; } - = new(SymbolEqualityComparer.Default); - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - if (context.Node is not AttributeSyntax attrNode) return; - var model = context.SemanticModel; - - var attrType = model.GetTypeInfo(attrNode).Type!; - if (attrType.GetFullName(true) != "gaemstone.ECS.ModuleAttribute") return; - - var memberNode = attrNode.Parent?.Parent!; - var memberSymbol = model.GetDeclaredSymbol(memberNode) as INamedTypeSymbol; - Symbols.Add(memberSymbol!); - } - } - - public void Execute(GeneratorExecutionContext context) - { - if (context.SyntaxContextReceiver is not SyntaxReceiver receiver) return; - - foreach (var symbol in receiver.Symbols) { - var isNested = (symbol.ContainingType != null); - if (isNested) - context.ReportDiagnostic(Diagnostic.Create(ModuleMayNotBeNested, - symbol.Locations.FirstOrDefault(), symbol.GetFullName())); - - var isPartial = symbol.DeclaringSyntaxReferences - .Any(r => (r.GetSyntax() as ClassDeclarationSyntax)?.Modifiers - .Any(t => t.IsKind(SyntaxKind.PartialKeyword)) ?? false); - if (!isPartial) - context.ReportDiagnostic(Diagnostic.Create(ModuleMustBePartial, - symbol.Locations.FirstOrDefault(), symbol.GetFullName())); - - if (symbol.IsStatic && (symbol.GetAttribute("gaemstone.ECS.PathAttribute")? - .ConstructorArguments.FirstOrDefault().Value == null)) - context.ReportDiagnostic(Diagnostic.Create(ModuleBuiltInMustHavePath, - symbol.Locations.FirstOrDefault(), symbol.GetFullName())); - - // Static classes can't implement interfaces. - if (symbol.IsStatic) continue; - - var modulePath = GetModulePath(symbol).ToStringLiteral(); - var dependencies = new List(); - foreach (var attr in symbol.GetAttributes()) - for (var type = attr.AttributeClass; type != null; type = type.BaseType) - if ((type.GetFullName(true) == "gaemstone.ECS.AddAttribute`2") - && type.TypeArguments[0].GetFullName() == "gaemstone.Flecs.Core.DependsOn") - dependencies.Add(GetModulePath(type.TypeArguments[1])); - - var sb = new StringBuilder(); - sb.AppendLine($$""" - // - using System.Collections.Generic; - using System.Linq; - using gaemstone.ECS; - - namespace {{ symbol.GetNamespace() }}; - - public partial class {{ symbol.Name }} - : IModule - { - public static string ModulePath { get; } - = {{ modulePath }}; - - """); - - if (dependencies.Count > 0) { - sb.AppendLine($$""" - public static IEnumerable Dependencies { get; } = new[] { - """); - foreach (var dependency in dependencies) - sb.AppendLine($$""" {{ dependency.ToStringLiteral() }},"""); - sb.AppendLine($$""" - }; - """); - } else sb.AppendLine($$""" - public static IEnumerable Dependencies { get; } = Enumerable.Empty(); - """); - - - sb.AppendLine($$""" - } - - """); - - context.AddSource($"{symbol.GetFullName()}_Module.g.cs", sb.ToString()); - } - } - - private static string GetModulePath(ISymbol module) - { - var path = module.GetAttribute("gaemstone.ECS.PathAttribute")? - .ConstructorArguments.FirstOrDefault().Value as string; - var isAbsolute = (path?.FirstOrDefault() == '/'); - if (isAbsolute) return path!; - - var fullPath = module.GetFullName().Replace('.', '/'); - if (path != null) { - var index = fullPath.LastIndexOf('/'); - fullPath = $"{fullPath.Substring(0, index)}/{path}"; - } - return $"/{fullPath}"; - } -} diff --git a/src/gaemstone.SourceGen/ModuleGenerator.cs b/src/gaemstone.SourceGen/ModuleGenerator.cs new file mode 100644 index 0000000..0a83af3 --- /dev/null +++ b/src/gaemstone.SourceGen/ModuleGenerator.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using gaemstone.SourceGen.Structure; +using gaemstone.SourceGen.Utility; +using Microsoft.CodeAnalysis; + +namespace gaemstone.SourceGen; + +[Generator] +public class ModuleGenerator + : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { +#if DEBUG + // while (!System.Diagnostics.Debugger.IsAttached) + // System.Threading.Thread.Sleep(500); +#endif + context.RegisterForSyntaxNotifications( + () => new RelevantSymbolReceiver()); + } + + private Dictionary? _symbolToInfoLookup; + + public BaseInfo? Lookup(ISymbol symbol) + => (_symbolToInfoLookup ?? throw new InvalidOperationException()) + .TryGetValue(symbol, out var info) ? info : null; + + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxContextReceiver is not RelevantSymbolReceiver receiver) return; + _symbolToInfoLookup = receiver.Symbols; + var infos = receiver.Symbols.Values; + + // Go through all entity infos (types and methods), populate their + // Parent property and add them to their parent's Children property. + foreach (var info in infos.OfType()) { + if ((info.Symbol.ContainingType is INamedTypeSymbol parentSymbol) + && (Lookup(parentSymbol) is TypeEntityInfo parentInfo)) + { + info.Parent = parentInfo; + parentInfo.Children.Add(info); + } + } + + // Go through all the method infos and lookup / construct + // their parameter infos from the method's parameters. + foreach (var info in infos.OfType()) { + foreach (var paramSymbol in info.Symbol.Parameters) { + if (Lookup(paramSymbol) is not ParameterInfo paramInfo) + paramInfo = new ParameterInfo(paramSymbol); + info.Parameters.Add(paramInfo); + paramInfo.Parent = info; + } + } + + // Validate all instances of base info without a Parent / Method set. + // Nested infos are validated by their containing info. + foreach (var info in infos.Where(info => info.Parent == null)) + info.Validate(); + + // Report all the diagnostics we found. + foreach (var info in infos) + foreach (var diag in info.Diagnostics) + context.ReportDiagnostic(diag); + + foreach (var module in infos.OfType()) { + if (module.IsErrored) continue; + var sb = new StringBuilder(); + AppendHeader(sb, module.Namespace); + AppendModuleType(sb, module); + context.AddSource($"{module.FullName}.g.cs", sb.ToString()); + } + + _symbolToInfoLookup = null; + } + + private void AppendHeader(StringBuilder sb, string @namespace) + => sb.AppendLine($$""" + // + using System.Collections.Generic; + using System.Collections.Immutable; + using gaemstone.ECS; + using gaemstone.ECS.Utility; + + namespace {{ @namespace }}; + + """); + + private void AppendModuleType(StringBuilder sb, ModuleEntityInfo module) + { + var type = module.Symbol.IsValueType ? "struct" : "class"; + sb.AppendLine($$""" + public partial {{ type }} {{ module.Name }} + : IModule + """); + + var modulePath = module.GetModulePath().ToStringLiteral(); + sb.AppendLine($$""" + { + static string IModule.Path { get; } = {{ modulePath }}; + + static bool IModule.IsBuiltIn { get; } = {{( module.IsBuiltIn ? "true" : "false" )}}; + + """); + + var dependencies = module.GetDependencies().ToList(); + sb.Append("\tstatic IReadOnlyList IModule.Dependencies { get; } = "); + if (dependencies.Count > 0) { + sb.AppendLine("ImmutableList.Create("); + foreach (var dependency in dependencies) + sb.AppendLine($"\t\t{dependency.ToStringLiteral()},"); + sb.Length -= 2; sb.AppendLine(); + sb.AppendLine("\t);"); + } else sb.AppendLine("ImmutableList.Create();"); + sb.AppendLine(); + + sb.AppendLine($$""" + static void IModule.Initialize(Entity module) + { + var world = module.World; + """); + + // TODO: Might want to add things related to the module entity. + + AppendEntityRegistration(sb, module); + AppendMethodRegistration(sb, module); + AppendEntityToAdd(sb, module); + AppendShimMethods(sb, module); + + // TODO: Can BuiltIn modules have systems and such? + + sb.AppendLine("\t}"); + + sb.AppendLine("}"); + } + + private void AppendEntityRegistration( + StringBuilder sb, ModuleEntityInfo module) + { + var entities = module.Children + .OfType() + .Where(e => !e.IsErrored) + .ToList(); + if (entities.Count == 0) return; + + sb.AppendLine(); + sb.AppendLine("\t\t// Register entities."); + foreach (var e in entities) { + var @var = $"_{e.Name}_Entity"; + var path = (e.Path ?? e.Name).ToStringLiteral(); + sb.AppendLine($"\t\tvar {@var} = world.New({path}, module)"); + + // Since this is a custom gaemstone tag, we want to add it even for [BuiltIn] modules. + if (e.IsRelation) sb.AppendLine("\t\t\t.Add()"); + + if (module.IsBuiltIn) + { + sb.AppendLine($"\t\t\t.Build().CreateLookup<{e.FullName}>()"); + } + else + { + // TODO: if (e.IsPublic) sb.AppendLine($"\t\t\t.Symbol(\"{e.Name}\")"); + + // Tags and relations in Flecs are marked as empty components. + if (e.IsTag || e.IsRelation) sb.AppendLine("\t\t\t.Add()"); + if (e.IsTag && e.IsRelation) sb.AppendLine("\t\t\t.Add()"); + + sb.Append( "\t\t\t.Build()"); + if (e.IsComponent) sb.Append($".InitComponent<{e.FullName}>()"); + else sb.Append($".CreateLookup<{e.FullName}>()"); + sb.AppendLine(); + + if (e.IsSingleton) sb.AppendLine($"\t\t\t.Add<{e.FullName}>()"); + } + + sb.Insert(sb.Length - 1, ";"); + } + } + + private void AppendMethodRegistration( + StringBuilder sb, ModuleEntityInfo module) + { + var methods = module.Children + .OfType() + .Where(e => !e.IsErrored) + .ToList(); + if (methods.Count == 0) return; + + sb.AppendLine(); + sb.AppendLine("\t\t// Register systems / observers."); + foreach (var m in methods) { + var @var = $"_{m.Name}_Entity"; + var path = (m.Path ?? m.Name).ToStringLiteral(); + sb.AppendLine($"\t\tvar {@var} = world.New({path}, module)"); + + sb.Append("\t\t\t.Build()"); + if (m.IsSystem) sb.AppendLine(".InitSystem("); + if (m.IsObserver) sb.AppendLine(".InitObserver("); + + if (m.IsIteratorOnly) { + var expression = m.Expression.ToStringLiteral(); + sb.AppendLine($"\t\t\t\tnew({expression}), {m.Name},"); + } else { + sb.AppendLine("\t\t\t\tnew("); + foreach (var p in m.Parameters) + if (p.HasTerm) { + // TODO: Throw error if multiple Or terms appear next to each other. + foreach (var term in p.TermTypes) { + sb.Append($"\t\t\t\t\tnew Term("); + switch (term) { + case ITypeSymbol type: + AppendTypeEntity(sb, module, type); + break; + case ParameterInfo.Pair pair: + AppendTypeEntity(sb, module, pair.Relation); + sb.Append(','); + AppendTypeEntity(sb, module, pair.Target); + break; + default: throw new InvalidOperationException( + $"Unexpected term type {term.GetType()}"); + } + sb.Append(')'); + if (p.Source != null) { + sb.Append("{ Source = "); + AppendTypeEntity(sb, module, p.Source); + sb.Append(" }"); + } + sb.Append(p.Kind switch { + ParameterKind.Or => ".Or", + ParameterKind.HasOr => ".Or.None", + ParameterKind.Has => ".None", + ParameterKind.Not => ".None.Not", + ParameterKind.Ref => ".InOut", + ParameterKind.Out => ".Out", + _ when !p.IsValueType => ".InOut", // Reference types always imply writability. + _ => ".In", + }); + if (p.IsNullable) sb.Append(".Optional"); + sb.AppendLine(","); + } + } + if (m.Parameters.Any(p => p.HasTerm)) + { sb.Length -= 2; sb.AppendLine(); } + sb.AppendLine( "\t\t\t\t),"); + sb.AppendLine($"\t\t\t\t_{m.Name}_Shim,"); + } + + if (m.IsObserver) + foreach (var ev in m.ObserverEvents!) { + sb.Append("\t\t\t\t"); + AppendTypeEntity(sb, module, ev); + sb.AppendLine(","); + } + + sb.Length -= 2; + sb.AppendLine(");"); + } + } + + private void AppendEntityToAdd( + StringBuilder sb, ModuleEntityInfo module) + { + var entities = module.Children + .Where(e => !e.IsErrored) + .Where(e => e.HasEntitiesToAdd) + .ToList(); + if (entities.Count == 0) return; + + sb.AppendLine(); + sb.AppendLine("\t\t// Add things to entities."); + foreach (var e in entities) { + var @var = $"_{e.Name}_Entity"; + foreach (var a in e.EntitiesToAdd) + sb.AppendLine($"\t\t{@var}.Add<{a.GetFullName()}>();"); + foreach (var (r, t) in e.RelationsToAdd) + sb.AppendLine($"\t\t{@var}.Add<{r.GetFullName()}, {t.GetFullName()}>();"); + } + } + + private void AppendShimMethods( + StringBuilder sb, ModuleEntityInfo module) + { + var methods = module.Children + .OfType() + .Where(m => !m.IsErrored) + .Where(m => !m.IsIteratorOnly) + .ToList(); + + foreach (var method in methods) + AppendShimMethod(sb, module, method); + } + + private void AppendShimMethod( + StringBuilder sb, ModuleEntityInfo module, + MethodEntityInfo method) + { + sb.AppendLine(); + sb.AppendLine($$""" + void _{{ method.Name }}_Shim(Iterator iter) + { + """); + + foreach (var param in method.Parameters) + if (param.HasField) + sb.Append($"\t\t\tvar _{param.Name}_Field = ") + .Append(param.IsNullable ? "iter.FieldOrEmpty" : "iter.Field") + .AppendLine($"<{param.FieldType!.GetFullName()}>({param.TermIndex});"); + + sb.AppendLine("\t\t\tfor (var i = 0; i < iter.Count; i++) {"); + + sb.Append($"\t\t\t\t{method.Name}"); + if (method.IsGeneric) sb.Append(""); + sb.Append($"("); + foreach (var param in method.Parameters) { + switch (param.Kind) { + case ParameterKind.Unique: + sb.Append(param.UniqueReplacement); + break; + + case ParameterKind.In: + case ParameterKind.Out: + case ParameterKind.Ref: + var modifier = param.Kind.ToString().ToLowerInvariant(); + sb.Append(modifier).Append(' '); + goto case ParameterKind.Normal; + + case ParameterKind.Normal: + case ParameterKind.Nullable: + // FIXME: Handle pairs. + sb.Append($"_{param.Name}_Field") + .Append(param.IsNullable ? ".GetOrNull(i)" : "[i]"); + break; + + case ParameterKind.Has: + case ParameterKind.Not: + case ParameterKind.HasOr: + sb.Append("default"); + break; + + case ParameterKind.Or: + throw new NotSupportedException( + $"ParameterKind {param.Kind} not supported"); + } + sb.Append(", "); + } + if (method.Parameters.Any()) + sb.Length -= 2; + sb.AppendLine(");"); + + sb.AppendLine($$""" + } + } + """); + } + + + private void AppendTypeEntity( + StringBuilder sb, ModuleEntityInfo module, + ITypeSymbol type) + { + var found = module.Children.Where(c => !c.IsErrored) + .Any(c => SymbolEqualityComparer.Default.Equals(c.Symbol, type)); + sb.Append(found ? $"_{type.Name}_Entity" + : $"world.Entity<{type.GetFullName()}>()"); + } +} diff --git a/src/gaemstone.SourceGen/RelevantSymbolReceiver.cs b/src/gaemstone.SourceGen/RelevantSymbolReceiver.cs new file mode 100644 index 0000000..bca43a3 --- /dev/null +++ b/src/gaemstone.SourceGen/RelevantSymbolReceiver.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using gaemstone.SourceGen.Structure; +using gaemstone.SourceGen.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace gaemstone.SourceGen; + +public class RelevantSymbolReceiver + : ISyntaxContextReceiver +{ + private static readonly HashSet RelevantAttributeNames = new(){ + // Base entity attributes + "Module", // Can also be [Singleton] + "Entity", + "Relation", // Can also be [Tag] or [Component] + "Tag", + "Component", + "Singleton", // Implies [Component] + "System", + "Observer", + + // Entity properties that specify additional info / behavior + "Public", + "Private", + "Path", + "Add", + "BuiltIn", // Valid on [Module] + "Expression", // Valid on [System] and [Observer] + + // Term properties (on [System] and [Observer] parameters) + "Source", + "Pair", + "Has", + "Not", + "Or", + }; + + public Dictionary Symbols { get; } = new(SymbolEqualityComparer.Default); + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + var model = context.SemanticModel; + if (context.Node is not AttributeSyntax node) return; + if (node.Parent?.Parent is not SyntaxNode parent) return; + if (model.GetDeclaredSymbol(parent) is not ISymbol symbol) return; + if (model.GetTypeInfo(node).Type is not INamedTypeSymbol type) return; + + // Go through the attribute's type hierarchy to see if it matches any + // of the attributes we care about. This is to make sure attributes + // based on [Add<...>] are picked up correctly, including custom ones. + for (var baseType = type; baseType != null; baseType = baseType.BaseType) { + if ((ToRelevantAttributeName(baseType) is string name) + && RelevantAttributeNames.Contains(name) // Check if we found a relevant attribute. + && !Symbols.ContainsKey(symbol)) // Check if this is already a known symbol. + { + Symbols.Add(symbol, symbol switch { + INamedTypeSymbol typeSymbol => + typeSymbol.GetAttributes().Any(attr => attr.AttributeClass! + .GetFullName() == "gaemstone.ECS.ModuleAttribute") + ? new ModuleEntityInfo(typeSymbol) + : new TypeEntityInfo(typeSymbol), + IMethodSymbol methodSymbol => new MethodEntityInfo(methodSymbol), + IParameterSymbol paramSymbol => new ParameterInfo(paramSymbol), + _ => throw new InvalidOperationException( + $"Unhandled symbol type {symbol.GetType()}"), + }); + break; + } + } + } + + public static string? ToRelevantAttributeName(INamedTypeSymbol symbol) + { + if (symbol.GetNamespace() != "gaemstone.ECS") return null; + var name = symbol.MetadataName.Split('`')[0]; + return name.EndsWith("Attribute") ? name[..^"Attribute".Length] : null; + } +} diff --git a/src/gaemstone.SourceGen/Structure/BaseEntityInfo.cs b/src/gaemstone.SourceGen/Structure/BaseEntityInfo.cs new file mode 100644 index 0000000..db4e92e --- /dev/null +++ b/src/gaemstone.SourceGen/Structure/BaseEntityInfo.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace gaemstone.SourceGen.Structure; + +public abstract class BaseEntityInfo : BaseInfo +{ + public new TypeEntityInfo? Parent { get => (TypeEntityInfo?)base.Parent; set => base.Parent = value; } + + public string? Path { get; } + // TODO: Rename this to something sensible, like [Symbol]. + // public bool IsPublic { get; private set; } + // private bool IsPrivate { get; } + + public List EntitiesToAdd { get; } = new(); + public List<(INamedTypeSymbol, INamedTypeSymbol)> RelationsToAdd { get; } = new(); + public bool HasEntitiesToAdd => (EntitiesToAdd.Count > 0) || (RelationsToAdd.Count > 0); + + public BaseEntityInfo(ISymbol symbol) + : base(symbol) + { + Path = Get("Path")?.ConstructorArguments.FirstOrDefault().Value as string; + + // IsPublic = Symbol.HasAttribute("gaemstone.ECS.PublicAttribute"); + // IsPrivate = Symbol.HasAttribute("gaemstone.ECS.PrivateAttribute"); + } + + protected override IEnumerable ValidateSelf() + { + if (this is ModuleEntityInfo) { + // If this entity is a module, it must not be nested. + if (Symbol.ContainingType != null) yield return Diagnostic.Create( + Descriptors.ModuleMustNotBeNested, Location); + } else { + // Otherwise, it must occur within a module + if (Parent is not ModuleEntityInfo) yield return Diagnostic.Create( + Descriptors.EntityMustBeInModule, Location); + } + + // var moduleIsPublic = (Parent?.IsPublic == true); + // var inheritsPublic = (this is MethodEntityInfo); // Observers and systems don't inherit [Public] from their module. + // IsPublic = IsPublic || (moduleIsPublic && inheritsPublic && !IsPrivate); + + // Add entities and relationships specified using [Add<...>] attributes. + foreach (var attr in Symbol.GetAttributes()) { + for (var attrType = attr.AttributeClass; attrType != null; attrType = attrType.BaseType) { + if (RelevantSymbolReceiver.ToRelevantAttributeName(attrType) != "Add") continue; + + var allTypeArgumentsValid = true; + for (var i = 0; i < attrType.TypeArguments.Length; i++) { + var arg = attrType.TypeArguments[i]; + var param = attrType.TypeParameters[i]; + if (arg is not INamedTypeSymbol) { + yield return Diagnostic.Create( + Descriptors.InvalidTypeArgument, param.Locations.Single()); + allTypeArgumentsValid = false; + } + // TODO: Make sure entities being added have appropriate attributes as well. + } + + if (allTypeArgumentsValid) { + switch (attrType.TypeArguments) { + case [ INamedTypeSymbol entity ]: + EntitiesToAdd.Add(entity); + break; + case [ INamedTypeSymbol relation, INamedTypeSymbol target ]: + RelationsToAdd.Add((relation, target)); + break; + default: throw new InvalidOperationException( + "Type argument pattern matching failed"); + } + } + } + } + } +} diff --git a/src/gaemstone.SourceGen/Structure/BaseInfo.cs b/src/gaemstone.SourceGen/Structure/BaseInfo.cs new file mode 100644 index 0000000..6a43d46 --- /dev/null +++ b/src/gaemstone.SourceGen/Structure/BaseInfo.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using gaemstone.SourceGen.Utility; +using Microsoft.CodeAnalysis; + +namespace gaemstone.SourceGen.Structure; + +public abstract class BaseInfo +{ + public ISymbol Symbol { get; } + + public BaseEntityInfo? Parent { get; set; } + + public string Name => Symbol.Name; + public string FullName => Symbol.GetFullName(); + public string Namespace => Symbol.GetNamespace()!; + + public Location Location => Symbol.Locations.First(); + public List Diagnostics { get; } = new(); + public bool IsErrored => Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error); + + public BaseInfo(ISymbol symbol) => Symbol = symbol; + + public void Validate() + { + // All of the children are validated before the parent is, + // in case their state affects the parent in some way. + foreach (var child in GetChildren()) + child.Validate(); + foreach (var diag in ValidateSelf()) + Diagnostics.Add(diag); + } + + protected virtual IEnumerable GetChildren() => Enumerable.Empty(); + protected abstract IEnumerable ValidateSelf(); + + protected bool Has(string name) + => Get(name) != null; + + protected AttributeData? Get(string name) + => Symbol.GetAttributes().FirstOrDefault(attr => + RelevantSymbolReceiver.ToRelevantAttributeName(attr.AttributeClass!) == name); +} diff --git a/src/gaemstone.SourceGen/Structure/MethodEntityInfo.cs b/src/gaemstone.SourceGen/Structure/MethodEntityInfo.cs new file mode 100644 index 0000000..8728fc1 --- /dev/null +++ b/src/gaemstone.SourceGen/Structure/MethodEntityInfo.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; +using gaemstone.SourceGen.Utility; +using Microsoft.CodeAnalysis; + +namespace gaemstone.SourceGen.Structure; + +public class MethodEntityInfo : BaseEntityInfo +{ + public new IMethodSymbol Symbol => (IMethodSymbol)base.Symbol; + + public List Parameters { get; } = new(); + protected override IEnumerable GetChildren() => Parameters; + + public bool IsSystem { get; } + public bool IsObserver { get; } + + public bool IsIteratorOnly { get; } + public string? Expression { get; } + public ITypeSymbol[]? ObserverEvents { get; } + + public bool IsStatic => Symbol.IsStatic; + public bool IsGeneric => Symbol.IsGenericMethod; + + public MethodEntityInfo(ISymbol symbol) + : base(symbol) + { + IsSystem = Has("System"); + IsObserver = Has("Observer"); + + IsIteratorOnly = (Parameters.Count == 1) && (Parameters[0].Symbol.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.Iterator"); + Expression = Get("Expression")?.ConstructorArguments.FirstOrDefault().Value as string; + ObserverEvents = Get("Observer")?.AttributeClass!.TypeArguments.ToArray(); + } + + protected override IEnumerable ValidateSelf() + { + foreach (var diag in base.ValidateSelf()) yield return diag; + + if (IsSystem && IsObserver) yield return Diagnostic.Create( + Descriptors.InvalidAttributeCombination, Location, "[System, Observer]"); + + if (Symbol.IsAbstract) yield return Diagnostic.Create( + Descriptors.MethodMustNotBeAbstract, Location); + if (Symbol.IsAsync) yield return Diagnostic.Create( + Descriptors.MethodMustNotBeAsync, Location); + + if (Symbol.Parameters.Length == 0) yield return Diagnostic.Create( + Descriptors.MethodMustHaveParameters, Location); + if (Symbol.IsExtensionMethod) yield return Diagnostic.Create( + Descriptors.MethodMustNotBeExtension, Location); + + if (IsIteratorOnly && (Expression == null)) yield return Diagnostic.Create( + Descriptors.MethodMustHaveExpression, Location); + + if (!IsStatic && Parent is ModuleEntityInfo { IsSingleton: false }) + yield return Diagnostic.Create( + Descriptors.InstanceMethodOnlyValidInSingleton, Location); + + if (IsGeneric) { + if (Symbol.TypeParameters.Length > 1) yield return Diagnostic.Create( + Descriptors.MethodGenericParamAtMostOne, Location); + else { + var param = Symbol.TypeParameters[0]; + if (!SymbolEqualityComparer.Default.Equals(param, Symbol.TypeArguments[0])) + yield return Diagnostic.Create( + Descriptors.MethodGenericParamMustNotBeSubstutited, param.Locations.Single()); + if (param.HasReferenceTypeConstraint || param.HasValueTypeConstraint + || param.HasUnmanagedTypeConstraint || param.HasNotNullConstraint + || param.HasConstructorConstraint || (param.ConstraintTypes.Length > 0)) + yield return Diagnostic.Create( + Descriptors.MethodGenericParamMustNotBeConstrained, param.Locations.Single()); + } + } + + // Set TermIndex of parameters. + var termIndex = 1; + foreach (var param in Parameters) + if (param.HasTerm) + param.TermIndex = termIndex++; + + // TODO: Handle systems with [Source]. + // TODO: Validate ObserverEvents. + } +} diff --git a/src/gaemstone.SourceGen/Structure/ModuleEntityInfo.cs b/src/gaemstone.SourceGen/Structure/ModuleEntityInfo.cs new file mode 100644 index 0000000..58bdf44 --- /dev/null +++ b/src/gaemstone.SourceGen/Structure/ModuleEntityInfo.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using gaemstone.SourceGen.Utility; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace gaemstone.SourceGen.Structure; + +public class ModuleEntityInfo : TypeEntityInfo +{ + public bool IsPartial { get; } + public bool IsBuiltIn { get; } + + // TODO: Support [Source]. + + public ModuleEntityInfo(ISymbol symbol) + : base(symbol) + { + var classDecl = (TypeDeclarationSyntax)Symbol.DeclaringSyntaxReferences.First().GetSyntax(); + IsPartial = classDecl.Modifiers.Any(t => t.IsKind(SyntaxKind.PartialKeyword)); + IsBuiltIn = Has("BuiltIn"); + } + + protected override IEnumerable ValidateSelf() + { + foreach (var diag in base.ValidateSelf()) yield return diag; + + if (!IsPartial) yield return Diagnostic.Create( + Descriptors.ModuleMustBePartial, Location); + + if (IsBuiltIn && (Path == null)) yield return Diagnostic.Create( + Descriptors.BuiltInModuleMustHavePath, Location); + } + + public IEnumerable GetDependencies() + { + foreach (var (relation, target) in RelationsToAdd) + if (relation.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.Flecs.Core.DependsOn") + yield return GetModulePath(target); + } + + public string GetModulePath() + => GetModulePath(Symbol); + + private static string GetModulePath(ISymbol module) + { + var pathAttr = module.GetAttributes().FirstOrDefault(attr => + attr.AttributeClass!.GetFullName() == "gaemstone.ECS.PathAttribute"); + var path = pathAttr?.ConstructorArguments.FirstOrDefault().Value as string; + + var isAbsolute = (path?.FirstOrDefault() == '/'); + if (isAbsolute) return path!; + + var fullPath = module.GetFullName().Replace('.', '/'); + return (path != null) + ? $"/{fullPath[..(fullPath.LastIndexOf('/'))]}/{path}" + : $"/{fullPath}"; + } +} diff --git a/src/gaemstone.SourceGen/Structure/ParameterInfo.cs b/src/gaemstone.SourceGen/Structure/ParameterInfo.cs new file mode 100644 index 0000000..aa8177c --- /dev/null +++ b/src/gaemstone.SourceGen/Structure/ParameterInfo.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using gaemstone.SourceGen.Utility; +using Microsoft.CodeAnalysis; + +namespace gaemstone.SourceGen.Structure; + +public class ParameterInfo : BaseInfo +{ + public record Pair(ITypeSymbol Relation, ITypeSymbol Target); + + private static readonly Dictionary UniqueParameters = new() { + ["gaemstone.ECS.Iterator`1"] = "iter", + ["gaemstone.ECS.World"] = "iter.World", + ["gaemstone.ECS.World`1"] = "iter.World", + ["gaemstone.ECS.Entity"] = "iter.Entity(i)", + ["gaemstone.ECS.Entity`1"] = "iter.Entity(i)", + ["System.TimeSpan"] = "iter.DeltaTime", + }; + + public new IParameterSymbol Symbol => (IParameterSymbol)base.Symbol; + public new MethodEntityInfo? Parent { get => (MethodEntityInfo?)base.Parent; set => base.Parent = value; } + + public bool IsGeneric => Symbol.Type is INamedTypeSymbol { IsGenericType: true }; + public bool IsValueType => Symbol.Type.IsValueType; + public bool IsNullable => Symbol.Type.IsNullable(); + public bool IsByRef => (Symbol.RefKind != RefKind.None); + + public ITypeSymbol? Source { get; } // TODO: Support [Source] for each term? + public int? TermIndex { get; set; } // Assigned by MethodEntityInfo + + public string? UniqueReplacement { get; } + public IReadOnlyList TermTypes { get; } // Either ITypeSymbol or Pair for relationships. + public ITypeSymbol? FieldType { get; } + public ParameterKind Kind { get; } + + public bool HasTerm => (Kind != ParameterKind.Unique); + public bool HasField => HasTerm && !(Kind is ParameterKind.Has or ParameterKind.Not or ParameterKind.HasOr); + + public ParameterInfo(ISymbol symbol) + : base(symbol) + { + Source = Get("Source")?.AttributeClass!.TypeArguments[0] + // If the type of the parameter has the [Singleton] attribute, use it as the default Source. + ?? (Symbol.Type.HasAttribute("gaemstone.ECS.SingletonAttribute") ? Symbol.Type : null); + + var typeFullName = Symbol.Type.GetFullName(FullNameStyle.Metadata); + if (UniqueParameters.TryGetValue(typeFullName, out var replacement)) + { + Kind = ParameterKind.Unique; + TermTypes = Array.Empty(); + UniqueReplacement = replacement; + } + else + { + var isHas = typeFullName.StartsWith("gaemstone.ECS.Has"); + var isNot = typeFullName.StartsWith("gaemstone.ECS.Not"); + var isOr = typeFullName.StartsWith("gaemstone.ECS.Or"); + + if (IsGeneric) + { + var type = (INamedTypeSymbol)Symbol.Type; // Checked by IsGeneric. + var args = type.TypeArguments; + + // Has<...> usually doesn't support a generic type as its own type parameter. + // However, Has> is an exception to this rule, so we check for this here. + if (isHas && (args is [ INamedTypeSymbol { IsGenericType: true } argType ]) + && argType.GetFullName(FullNameStyle.NoGeneric) == "gaemstone.ECS.Or") + { + TermTypes = argType.TypeArguments.ToImmutableList(); + FieldType = null; + isOr = true; + } + else if ((isHas || isNot) && (args is [ INamedTypeSymbol relation, INamedTypeSymbol target ])) + { + TermTypes = ImmutableList.Create(new Pair(relation, target)); + FieldType = null; + } + else + { + TermTypes = args.ToImmutableList(); + FieldType = IsNullable ? args[0] : type; + } + } + else + { + TermTypes = ImmutableList.Create(Symbol.Type); + FieldType = Symbol.Type; + } + + Kind = (isHas && isOr) ? ParameterKind.HasOr + : isHas ? ParameterKind.Has + : isOr ? ParameterKind.Or + : isNot ? ParameterKind.Not + : IsNullable ? ParameterKind.Nullable + : Symbol.RefKind switch { + RefKind.In => ParameterKind.In, + RefKind.Out => ParameterKind.Out, + RefKind.Ref => ParameterKind.Ref, + _ => ParameterKind.Normal, + }; + } + } + + protected override IEnumerable ValidateSelf() + { + if (Parent == null) { yield return Diagnostic.Create( + Descriptors.ParamMustBeInMethod, Location); yield break; } + + // TODO: Support optionals. + if (Symbol.IsOptional) yield return Diagnostic.Create( + Descriptors.ParamDoesNotSupportOptional, Location); + + if (IsByRef && !IsValueType) { yield return Diagnostic.Create( + Descriptors.ParamByRefMustBeValueType, Location); yield break; } + if (IsByRef && IsNullable) { yield return Diagnostic.Create( + Descriptors.ParamByRefMustNotBeNullable, Location); yield break; } + + if (UniqueReplacement != null) + { + if (Source != null) yield return Diagnostic.Create( + Descriptors.UniqueParamNotSupported, Location, "[Source]"); + + if (IsByRef) { yield return Diagnostic.Create( + Descriptors.UniqueParamMustNotBeByRef, Location); yield break; } + if (IsNullable) { yield return Diagnostic.Create( + Descriptors.UniqueParamMustNotBeNullable, Location); yield break; } + + if (IsGeneric) { + if (!Parent.IsGeneric) { yield return Diagnostic.Create( + Descriptors.UniqueParamGenericMustMatch, Location); yield break; } + var paramParam = ((INamedTypeSymbol)Symbol.Type).TypeArguments[0]; + var methodParam = Parent.Symbol.TypeParameters[0]; + if (!SymbolEqualityComparer.Default.Equals(paramParam, methodParam)) { + yield return Diagnostic.Create(Descriptors.UniqueParamGenericMustMatch, Location); + yield break; + } + } + } + else + { + var isSpecial = Kind is ParameterKind.Has or ParameterKind.HasOr + or ParameterKind.Or or ParameterKind.Not; + if (isSpecial && IsByRef) yield return Diagnostic.Create( + Descriptors.SpecialMustNotBeByRef, Location); + if (isSpecial && IsNullable) yield return Diagnostic.Create( + Descriptors.SpecialArgMustNotBeNullable, Location); + + foreach (var termTypeBase in TermTypes) { + var termTypes = termTypeBase switch { + ITypeSymbol type => new[]{ type }, + Pair pair => new[]{ pair.Relation, pair.Target }, + _ => throw new InvalidOperationException( + $"Unexpected term type {termTypeBase.GetType()}") + }; + + foreach (var termType in termTypes) { + var location = termType.Locations.Single(); + + switch (termType.TypeKind) { + case TypeKind.Array: yield return Diagnostic.Create( + Descriptors.ParamMustNotBeArray, location); continue; + case TypeKind.Pointer: yield return Diagnostic.Create( + Descriptors.ParamMustNotBePointer, location); continue; + case TypeKind.TypeParameter: yield return Diagnostic.Create( + Descriptors.ParamMustNotBeGenericType, location); continue; + } + + if (termType.IsPrimitiveType()) yield return Diagnostic.Create( + Descriptors.ParamMustNotBePrimitive, location); + + if (isSpecial) { + if (termType is INamedTypeSymbol { IsGenericType: true }) + yield return Diagnostic.Create( + Descriptors.SpecialArgMustNotBeGeneric, location); + if (termType.IsNullable()) + yield return Diagnostic.Create( + Descriptors.SpecialArgMustNotBeNullable, location); + } + } + } + + if (IsGeneric && !isSpecial && !(IsValueType && IsNullable)) { + yield return Diagnostic.Create( + Descriptors.ParamMustNotBeGeneric, Location); + yield return Diagnostic.Create( + Descriptors.ParamMustNotBeGeneric, Location); + } + } + } +} + +public enum ParameterKind +{ + Unique, // for example "Iterator", "Entity", "Entity" + Normal, // Pass by value (copy) + Nullable, // Pass by value, but may be missing (null) + In, // Pass by reference (read-only) + Out, // Pass by reference (write-only) + Ref, // Pass by reference (read/write) + Has, // Required present (no read/write) + Not, // Required missing (no read/write) + Or, // Only one of multiple terms is required + HasOr, // Both Has and Or at the same time +} + +// public enum TermKind +// { +// In, // Pass by reference (read-only) +// Out, // Pass by reference (write-only) +// Ref, // Pass by reference (read/write) +// Has, // Required present (no read/write) +// Not, // Required missing (no read/write) +// } diff --git a/src/gaemstone.SourceGen/Structure/TypeEntityInfo.cs b/src/gaemstone.SourceGen/Structure/TypeEntityInfo.cs new file mode 100644 index 0000000..e28dc65 --- /dev/null +++ b/src/gaemstone.SourceGen/Structure/TypeEntityInfo.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace gaemstone.SourceGen.Structure; + +public class TypeEntityInfo : BaseEntityInfo +{ + public new INamedTypeSymbol Symbol => (INamedTypeSymbol)base.Symbol; + + public List Children { get; } = new(); + protected override IEnumerable GetChildren() => Children; + + public bool IsModule { get; } + private bool IsEntity { get; } + public bool IsTag { get; } + public bool IsComponent { get; private set; } + public bool IsSingleton { get; } + public bool IsRelation { get; } + + public bool IsValueType => Symbol.IsValueType; + public bool IsReferenceType => Symbol.IsReferenceType; + // TODO: Make sure that component has (instance) fields, non-component entity does not have fields. + + public TypeEntityInfo(ISymbol symbol) + : base(symbol) + { + IsModule = Has("Module"); + IsEntity = Has("Entity"); + IsTag = Has("Tag"); + IsComponent = Has("Component"); + IsSingleton = Has("Singleton"); + IsRelation = Has("Relation"); + } + + protected override IEnumerable ValidateSelf() + { + foreach (var diag in base.ValidateSelf()) yield return diag; + + if (Symbol.IsStatic) yield return Diagnostic.Create( + Descriptors.TypeMustNotBeStatic, Location); + if (Symbol.IsAbstract) yield return Diagnostic.Create( + Descriptors.TypeMustNotBeAbstract, Location); + + var attributeList = new List(); + if (IsModule) attributeList.Add("Module"); + if (IsEntity) attributeList.Add("Entity"); + if (IsRelation) attributeList.Add("Relation"); + if (IsTag) attributeList.Add("Tag"); + if (IsComponent) attributeList.Add("Component"); + if (IsSingleton) attributeList.Add("Singleton"); + + switch (attributeList) { + // A single attribute is valid. + case [ _ ]: + // The following are valid attribute combinations. + case [ "Module", "Singleton" ]: + case [ "Relation", "Tag" ]: + case [ "Relation", "Component" ]: + break; + default: + yield return Diagnostic.Create( + Descriptors.InvalidAttributeCombination, Location, + $"[{string.Join(", ", attributeList)}]"); + if (IsModule) yield return Diagnostic.Create( + Descriptors.ValidModuleAttributesHint, Location); + if (IsRelation) yield return Diagnostic.Create( + Descriptors.ValidRelationAttributesHint, Location); + if (IsSingleton && IsComponent) yield return Diagnostic.Create( + Descriptors.SingletonImpliesComponentHint, Location); + break; + } + + // Singletons are special kinds of components. + if (IsSingleton) IsComponent = true; + } +} diff --git a/src/gaemstone.SourceGen/Utility/IsExternalInit.cs b/src/gaemstone.SourceGen/Utility/IsExternalInit.cs new file mode 100644 index 0000000..41f195b --- /dev/null +++ b/src/gaemstone.SourceGen/Utility/IsExternalInit.cs @@ -0,0 +1,8 @@ +// Unfortunately, this is necessary for record types or init +// properties to work under netstandard2.0 target framework. +namespace System.Runtime.CompilerServices +{ + public static class IsExternalInit + { + } +} diff --git a/src/gaemstone.SourceGen/Utility/SymbolExtensions.cs b/src/gaemstone.SourceGen/Utility/SymbolExtensions.cs index 345fa99..0409433 100644 --- a/src/gaemstone.SourceGen/Utility/SymbolExtensions.cs +++ b/src/gaemstone.SourceGen/Utility/SymbolExtensions.cs @@ -7,45 +7,112 @@ namespace gaemstone.SourceGen.Utility; public static class SymbolExtensions { - public static string GetFullName(this ISymbol symbol, bool metadata = false) + public static bool IsNullable(this ITypeSymbol type) => type.IsValueType + ? (type.OriginalDefinition?.SpecialType == SpecialType.System_Nullable_T) + : (type.NullableAnnotation == NullableAnnotation.Annotated); + + public static bool IsPrimitiveType(this ITypeSymbol symbol) + => symbol.SpecialType switch { + SpecialType.System_Boolean or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_Int32 or + SpecialType.System_Int64 or + SpecialType.System_Byte or + SpecialType.System_UInt16 or + SpecialType.System_UInt32 or + SpecialType.System_UInt64 or + SpecialType.System_Single or + SpecialType.System_Double or + SpecialType.System_Char or + SpecialType.System_String or + SpecialType.System_Object => true, + _ => false, + }; + + public static string GetFullName( + this ISymbol symbol, FullNameStyle style = FullNameStyle.Full) { var builder = new StringBuilder(); - AppendFullName(symbol, builder, metadata); + AppendFullName(symbol, builder, style); return builder.ToString(); } - public static void AppendFullName(this ISymbol symbol, StringBuilder builder, bool metadata = false) + public static void AppendFullName( + this ISymbol symbol, StringBuilder builder, + FullNameStyle style = FullNameStyle.Full) { - if ((symbol.ContainingSymbol is ISymbol parent) - && ((parent as INamespaceSymbol)?.IsGlobalNamespace != true)) + var withGeneric = (style != FullNameStyle.NoGeneric); + var withMetadata = (style == FullNameStyle.Metadata); + + if ((symbol.Kind != SymbolKind.TypeParameter) + && (symbol.ContainingSymbol is ISymbol parent) + && (parent is not INamespaceSymbol { IsGlobalNamespace: true })) { - AppendFullName(parent, builder, metadata); - builder.Append(((parent is ITypeSymbol) && metadata) ? '+' : '.'); + AppendFullName(parent, builder, style); + builder.Append((withMetadata && (parent is ITypeSymbol)) ? '+' : '.'); } - if (!metadata && (symbol is INamedTypeSymbol typeSymbol) && typeSymbol.IsGenericType) { + if ((symbol is INamedTypeSymbol { IsGenericType: true } typeSymbol) + && !(withGeneric && withMetadata)) + { var length = symbol.MetadataName.IndexOf('`'); builder.Append(symbol.MetadataName, 0, length); - builder.Append('<'); - foreach (var genericType in typeSymbol.TypeArguments) { - AppendFullName(genericType, builder, true); - builder.Append(','); + + if (withGeneric) { + builder.Append('<'); + foreach (var arg in typeSymbol.TypeArguments) { + AppendFullName(arg, builder, style); + builder.Append(','); + } + builder.Length--; // Remove the last ',' character. + builder.Append('>'); } - builder.Length--; // Remove the last ',' character. - builder.Append('>'); - } else builder.Append(symbol.MetadataName); + } + else builder.Append(symbol.MetadataName); } public static string? GetNamespace(this ISymbol symbol) => symbol.ContainingNamespace?.GetFullName(); - public static bool HasAttribute(this ISymbol symbol, string attrMetadataName) - => symbol.GetAttributes().Any(attr => attr.AttributeClass!.GetFullName(true) == attrMetadataName); + public static bool HasAttribute(this ISymbol symbol, string name, + FullNameStyle matchStyle = FullNameStyle.Metadata) + => symbol.GetAttributes().Any(attr => + attr.AttributeClass!.GetFullName(matchStyle) == name); - public static AttributeData? GetAttribute(this ISymbol symbol, string attrMetadataName) - => symbol.GetAttributes().FirstOrDefault(attr => attr.AttributeClass!.GetFullName(true) == attrMetadataName); + public static AttributeData? FindAttribute(this ISymbol symbol, string name, + FullNameStyle matchStyle = FullNameStyle.Metadata) + => symbol.GetAttributes().FirstOrDefault(attr => + attr.AttributeClass!.GetFullName(matchStyle) == name); public static string ToStringLiteral(this string? input) => (input != null) ? SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(input)).ToFullString() : "null"; } + +public enum FullNameStyle +{ + Full, // Namespace.Foo.Bar> + Metadata, // Namespace.Foo+Bar`1 + NoGeneric, // Namespace.Foo.Bar +} + +public struct StringifyOptions +{ + public static readonly StringifyOptions Default = new(); + public static readonly StringifyOptions StripGeneric = new(){ Generic = GenericDisplayMode.None }; + public static readonly StringifyOptions MetadataGeneric = new(){ Generic = GenericDisplayMode.Metadata }; + + public bool Namespace { get; set; } = true; + // TODO: public bool FriendlyNames { get; set; } = true; + public GenericDisplayMode Generic { get; set; } = GenericDisplayMode.Full; + + public StringifyOptions() { } + + public enum GenericDisplayMode + { + None, // Foo + Metadata, // Foo`1 + Full, // Foo> + } +} diff --git a/src/gaemstone.SourceGen/gaemstone.SourceGen.csproj b/src/gaemstone.SourceGen/gaemstone.SourceGen.csproj index a02c609..c07af53 100644 --- a/src/gaemstone.SourceGen/gaemstone.SourceGen.csproj +++ b/src/gaemstone.SourceGen/gaemstone.SourceGen.csproj @@ -9,6 +9,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/gaemstone/ECS/FilterExtensions.cs b/src/gaemstone/ECS/FilterExtensions.cs deleted file mode 100644 index 48f69f8..0000000 --- a/src/gaemstone/ECS/FilterExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Linq; -using gaemstone.Utility.IL; - -namespace gaemstone.ECS; - -public static class FilterExtensions -{ - public static void RunOnce(World world, Delegate action) - { - var gen = IterActionGenerator.GetOrBuild(world, action.Method); - var desc = new FilterDesc(gen.Terms.ToArray()); - using var filter = new Filter(world, desc); - foreach (var iter in filter.Iter()) gen.RunWithTryCatch(action.Target, iter); - } -} diff --git a/src/gaemstone/ECS/Game.cs b/src/gaemstone/ECS/Game.cs deleted file mode 100644 index b86dfcc..0000000 --- a/src/gaemstone/ECS/Game.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace gaemstone.ECS; - -/// -/// Entity for storing global game state and configuration. -/// Parameters can use to source this entity. -/// -public struct Game { } - -/// Equivalent to . -[AttributeUsage(AttributeTargets.Parameter)] -public class GameAttribute : SourceAttribute { } diff --git a/src/gaemstone/ECS/Module+Attributes.cs b/src/gaemstone/ECS/Module+Attributes.cs index ca0603a..8241dc0 100644 --- a/src/gaemstone/ECS/Module+Attributes.cs +++ b/src/gaemstone/ECS/Module+Attributes.cs @@ -3,16 +3,6 @@ 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; -} - /// ///

/// Components marked with this attribute are automatically registered with @@ -24,40 +14,44 @@ public class PathAttribute : Attribute /// components, except ones marked with . ///

///
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct + | AttributeTargets.Enum | AttributeTargets.Method)] public class PublicAttribute : Attribute { } /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct + | AttributeTargets.Enum | AttributeTargets.Method)] public class PrivateAttribute : Attribute { } +// TODO: Should this be renamed from [Add<...>] to something else? /// /// Marked entity automatically has the specified entity added to it when -/// automatically registered. Equivalent to . +/// automatically registered. Equivalent to . /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct + | AttributeTargets.Enum | AttributeTargets.Method, + AllowMultiple = true)] public class AddAttribute : Attribute { } /// /// Marked entity automatically has the specified relationship pair added to it when -/// automatically registered. Equivalent to . +/// automatically registered. Equivalent to . /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct + | AttributeTargets.Enum | AttributeTargets.Method, + 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 index 7cc707e..ab9c46c 100644 --- a/src/gaemstone/ECS/Module+Components.cs +++ b/src/gaemstone/ECS/Module+Components.cs @@ -1,27 +1,33 @@ 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 | AttributeTargets.Method)] +public class PathAttribute : Attribute +{ + public string Value { get; } + public PathAttribute(string value) => Value = value; +} + [AttributeUsage(AttributeTargets.Struct)] public class EntityAttribute : Attribute { } -/// +// On types marked as [Relation], this has an effect on the relation's behavior. +// Otherwise this has the same behavior as [Entity], but informs people [AttributeUsage(AttributeTargets.Struct)] public class TagAttribute : Attribute { } -/// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] public class ComponentAttribute : Attribute { } /// -/// A singleton is a single instance of a tag or component that can be retrieved +/// A singleton is a single instance of a [Component] that can be retrieved /// without explicitly specifying an entity in a query, where it is equivalent /// to with itself as the generic type parameter. /// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] -public class SingletonAttribute : Attribute - { public bool AutoAdd { get; init; } = true; } +public class SingletonAttribute : ComponentAttribute { } /// /// Marked entity represents a relationship type. diff --git a/src/gaemstone/ECS/Module.cs b/src/gaemstone/ECS/Module.cs index 3e01260..f2dd52a 100644 --- a/src/gaemstone/ECS/Module.cs +++ b/src/gaemstone/ECS/Module.cs @@ -3,25 +3,29 @@ using System.Collections.Generic; namespace gaemstone.ECS; -[AttributeUsage(AttributeTargets.Class)] -public class ModuleAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class ModuleAttribute : SingletonAttribute { } -public interface IModuleInitializer -{ - void Initialize(EntityRef module); -} +[AttributeUsage(AttributeTargets.Class)] +public class BuiltInAttribute : Attribute { } -// The following will be implemented on [Module] -// types automatically for you by source generators. +/// +/// A concrete implementation of this interface is generated by a source +/// generator for each type marked as . +/// public interface IModule { - static abstract string ModulePath { get; } + static abstract string Path { get; } + + static abstract bool IsBuiltIn { get; } - static abstract IEnumerable Dependencies { get; } + static abstract IReadOnlyList Dependencies { get; } + + static abstract void Initialize(Entity module); } -public interface IModuleAutoRegisterComponents +public interface IModuleInitializer { - void RegisterComponents(EntityRef module); + static abstract void Initialize(Entity module); } diff --git a/src/gaemstone/ECS/Observer.cs b/src/gaemstone/ECS/Observer.cs deleted file mode 100644 index 245025d..0000000 --- a/src/gaemstone/ECS/Observer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using gaemstone.Utility; -using gaemstone.Utility.IL; - -namespace gaemstone.ECS; - -[AttributeUsage(AttributeTargets.Method)] -public class ObserverAttribute : Attribute -{ - public Type Event { get; } - internal ObserverAttribute(Type @event) => Event = @event; // Use generic type instead. -} -public class ObserverAttribute : ObserverAttribute - { public ObserverAttribute() : base(typeof(TEvent)) { } } - -public static class ObserverExtensions -{ - public static EntityRef InitObserver(this World world, - object? instance, MethodInfo method) - { - var attr = method.Get() ?? throw new ArgumentException( - "Observer must specify ObserverAttribute", nameof(method)); - var expr = method.Get()?.Value; - FilterDesc filter; - Action iterAction; - - var param = method.GetParameters(); - if ((param.Length == 1) && (param[0].ParameterType == typeof(Iterator))) { - filter = new(expr ?? throw new Exception( - "Observer must specify ExpressionAttribute")); - if (method.IsStatic) instance = null; - iterAction = (Action)Delegate.CreateDelegate( - typeof(Action), instance, method); - } else { - var gen = IterActionGenerator.GetOrBuild(world, method); - filter = (expr != null) ? new(expr) : new(gen.Terms.ToArray()); - iterAction = iter => gen.RunWithTryCatch(instance, iter); - } - - var @event = world.LookupByTypeOrThrow(attr.Event); - return world.New(method.Name).Build().InitObserver(@event, filter, iterAction); - } -} diff --git a/src/gaemstone/ECS/System+Terms.cs b/src/gaemstone/ECS/System+Terms.cs new file mode 100644 index 0000000..46d4b43 --- /dev/null +++ b/src/gaemstone/ECS/System+Terms.cs @@ -0,0 +1,88 @@ +using System; +using gaemstone.Utility; + +namespace gaemstone.ECS; + +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] +public class SourceAttribute : Attribute { } + + +// TODO: Implement [Pair] somehow. +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.GenericParameter)] +public class PairAttribute : Attribute { } + +public static class Pair +{ + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.GenericParameter)] + public class RelationAttribute : Attribute { } + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.GenericParameter)] + public class TargetAttribute : Attribute { } +} + + +public readonly ref struct Has { } +public readonly ref struct Has { } + +public readonly ref struct Not { } +public readonly ref struct Not { } + + +public readonly struct Or +{ + private readonly Union _union; + public int Case { get; } + + public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; } + public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; } + + public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException(); + public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException(); +} + +public readonly struct Or +{ + private readonly Union _union; + public int Case { get; } + + public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; } + public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; } + public Or(T3 value) { Case = 3; _union = new() { Value3 = value }; } + + public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException(); + public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException(); + public T3 Value3 => (Case == 3) ? _union.Value3 : throw new InvalidOperationException(); +} + +public readonly struct Or +{ + private readonly Union _union; + public int Case { get; } + + public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; } + public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; } + public Or(T3 value) { Case = 3; _union = new() { Value3 = value }; } + public Or(T4 value) { Case = 4; _union = new() { Value4 = value }; } + + public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException(); + public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException(); + public T3 Value3 => (Case == 3) ? _union.Value3 : throw new InvalidOperationException(); + public T4 Value4 => (Case == 4) ? _union.Value4 : throw new InvalidOperationException(); +} + +public readonly struct Or +{ + private readonly Union _union; + public int Case { get; } + + public Or(T1 value) { Case = 1; _union = new() { Value1 = value }; } + public Or(T2 value) { Case = 2; _union = new() { Value2 = value }; } + public Or(T3 value) { Case = 3; _union = new() { Value3 = value }; } + public Or(T4 value) { Case = 4; _union = new() { Value4 = value }; } + public Or(T5 value) { Case = 5; _union = new() { Value5 = value }; } + + public T1 Value1 => (Case == 1) ? _union.Value1 : throw new InvalidOperationException(); + public T2 Value2 => (Case == 2) ? _union.Value2 : throw new InvalidOperationException(); + public T3 Value3 => (Case == 3) ? _union.Value3 : throw new InvalidOperationException(); + public T4 Value4 => (Case == 4) ? _union.Value4 : throw new InvalidOperationException(); + public T5 Value5 => (Case == 5) ? _union.Value5 : throw new InvalidOperationException(); +} diff --git a/src/gaemstone/ECS/System.cs b/src/gaemstone/ECS/System.cs index 9195a6b..5665f95 100644 --- a/src/gaemstone/ECS/System.cs +++ b/src/gaemstone/ECS/System.cs @@ -1,21 +1,20 @@ using System; -using System.Linq; -using System.Reflection; -using gaemstone.Flecs; -using gaemstone.Utility; -using gaemstone.Utility.IL; namespace gaemstone.ECS; [AttributeUsage(AttributeTargets.Method)] -public class SystemAttribute : Attribute -{ - public Type Phase { get; } - public SystemAttribute() : this(typeof(SystemPhase.OnUpdate)) { } - internal SystemAttribute(Type phase) => Phase = phase; // Use generic type instead. -} -public class SystemAttribute : SystemAttribute - { public SystemAttribute() : base(typeof(TPhase)) { } } +public class SystemAttribute : Attribute { } + +[AttributeUsage(AttributeTargets.Method)] +public class ObserverAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Method)] +public class ObserverAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Method)] +public class ObserverAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Method)] +public class ObserverAttribute : Attribute { } +[AttributeUsage(AttributeTargets.Method)] +public class ObserverAttribute : Attribute { } [AttributeUsage(AttributeTargets.Method)] public class ExpressionAttribute : Attribute @@ -23,48 +22,3 @@ public class ExpressionAttribute : Attribute public string Value { get; } public ExpressionAttribute(string value) => Value = value; } - -public static class SystemExtensions -{ - public static EntityRef InitSystem(this World world, Delegate action) - { - var attr = action.Method.Get(); - var expr = action.Method.Get()?.Value; - - QueryDesc query; - if (action is Action callback) { - query = new(expr ?? throw new ArgumentException( - "System must specify ExpressionAttribute", nameof(action))); - } else { - var gen = IterActionGenerator.GetOrBuild(world, action.Method); - query = (expr != null) ? new(expr) : new(gen.Terms.ToArray()); - callback = iter => gen.RunWithTryCatch(action.Target, iter); - } - - var phase = world.LookupByTypeOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); - return world.New(action.Method.Name).Build().InitSystem(phase, query, callback); - } - - public static EntityRef InitSystem(this World world, - object? instance, MethodInfo method) - { - var attr = method.Get(); - var expr = method.Get()?.Value; - - QueryDesc query; - Action callback; - var param = method.GetParameters(); - if ((param.Length == 1) && (param[0].ParameterType == typeof(Iterator))) { - query = new(expr ?? throw new ArgumentException( - "System must specify ExpressionAttribute", nameof(method))); - callback = (Action)Delegate.CreateDelegate(typeof(Action), instance, method); - } else { - var gen = IterActionGenerator.GetOrBuild(world, method); - query = (expr != null) ? new(expr) : new(gen.Terms.ToArray()); - callback = iter => gen.RunWithTryCatch(instance, iter); - } - - var phase = world.LookupByTypeOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); - return world.New(method.Name).Build().InitSystem(phase, query, callback); - } -} diff --git a/src/gaemstone/ECS/TermAttributes.cs b/src/gaemstone/ECS/TermAttributes.cs deleted file mode 100644 index 6a88d68..0000000 --- a/src/gaemstone/ECS/TermAttributes.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -namespace gaemstone.ECS; - -[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] -public class SourceAttribute : Attribute -{ - public Type Type { get; } - // TODO: Support path as source too. - internal SourceAttribute(Type type) => Type = type; -} -public class SourceAttribute : SourceAttribute - { public SourceAttribute() : base(typeof(TEntity)) { } } - - -// Parameters types marked with [Tag] are equivalent to [Has]. -[AttributeUsage(AttributeTargets.Parameter)] -public class HasAttribute : Attribute { } - -// Parameters with "in" modifier are equivalent to [In]. -[AttributeUsage(AttributeTargets.Parameter)] -public class InAttribute : Attribute { } - -// Parameters with "out" modifier are equivalent to [Out]. -[AttributeUsage(AttributeTargets.Parameter)] -public class OutAttribute : Attribute { } - -[AttributeUsage(AttributeTargets.Parameter)] -public class OrAttribute : Attribute { } - -[AttributeUsage(AttributeTargets.Parameter)] -public class NotAttribute : Attribute { } diff --git a/src/gaemstone/Flecs/Core.cs b/src/gaemstone/Flecs/Core.cs index 7449e55..8cc95ed 100644 --- a/src/gaemstone/Flecs/Core.cs +++ b/src/gaemstone/Flecs/Core.cs @@ -2,8 +2,8 @@ using gaemstone.ECS; namespace gaemstone.Flecs; -[Module, Path("/flecs/core")] -public static partial class Core +[BuiltIn, Module, Path("/flecs/core")] +public partial class Core { // Entity Tags @@ -17,27 +17,23 @@ public static partial class Core [Tag] public struct Disabled { } [Tag] public struct Empty { } - // Can't be in a module class with the same name. - // [Path("/flecs/system/System")] - // [Tag] public struct System { } // Entities - // Conflicts with World class, and not required? - // [Entity] public struct World { } - [Entity, Path("*")] public struct Wildcard { } [Entity, Path("_")] public struct Any { } [Entity] public struct This { } [Entity, Path("$")] public struct Variable { } [Entity] public struct Flag { } + // Entity Relationships [Relation, Tag] public struct IsA { } [Relation, Tag] public struct ChildOf { } [Relation, Tag] public struct DependsOn { } + // Component / Relationship Properties [Tag] public struct Transitive { } @@ -52,6 +48,27 @@ public static partial class Core [Relation, Tag] public struct With { } [Tag] public struct OneOf { } + + // Observer Events + + [Entity] public struct OnAdd { } + [Entity] public struct OnRemove { } + [Entity] public struct OnSet { } + [Entity] public struct UnSet { } + [Entity] public struct OnTableEmpty { } + [Entity] public struct OnTableFilled { } + + + // Related to Deletion Events + + [Relation, Tag] public struct OnDelete { } + [Relation, Tag] public struct OnDeleteTarget { } + + [Tag] public struct Remove { } + [Tag] public struct Delete { } + [Tag] public struct Panic { } + + // Components [Component] @@ -75,6 +92,7 @@ public static partial class Core #pragma warning restore public override string? ToString() { unsafe { + // TODO: What does Flecs do if you give it "" as an identifier? if ((_value == null) || (_length == 0)) return null; else return new UTF8View(new(_value, (int)_length)).ToString(); } } diff --git a/src/gaemstone/Flecs/DeletionEvent.cs b/src/gaemstone/Flecs/DeletionEvent.cs deleted file mode 100644 index 050bc6f..0000000 --- a/src/gaemstone/Flecs/DeletionEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using gaemstone.ECS; - -namespace gaemstone.Flecs; - -[Module, Path("/flecs/core")] -public static partial class DeletionEvent -{ - [Relation, Tag] public struct OnDelete { } - [Relation, Tag] public struct OnDeleteTarget { } -} - -[Module, Path("/flecs/core")] -public static partial class DeletionBehavior -{ - [Tag] public struct Remove { } - [Tag] public struct Delete { } - [Tag] public struct Panic { } -} diff --git a/src/gaemstone/Flecs/Doc.cs b/src/gaemstone/Flecs/Doc.cs index 1fe5d92..20b7464 100644 --- a/src/gaemstone/Flecs/Doc.cs +++ b/src/gaemstone/Flecs/Doc.cs @@ -1,13 +1,12 @@ -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using gaemstone.ECS; -using gaemstone.Utility; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.Flecs; -[Module, Path("/flecs/doc")] -public static partial class Doc +[BuiltIn, Module, Path("/flecs/doc")] +public partial class Doc { [Tag] public struct Brief { } [Tag] public struct Detail { } @@ -28,52 +27,44 @@ public static partial class Doc public static unsafe class DocExtensions { - private static EntityRef Set(EntityRef entity, string? value) + private static Entity Set(Entity entity, string? value) { - var world = entity.World; - var id = IdRef.Pair(world); + var id = entity.World.Pair(); - var hadId = entity.Has(id); - var alloc = GlobalHeapAllocator.Instance; if (value != null) { - var ptr = ecs_get_mut_id(world, entity, id); - ref var desc = ref Unsafe.AsRef(ptr); - // FIXME: Why does freeing these cause crashes? - // if (has) alloc.Free((nint)desc.Value); // Free previous value. - desc.Value = (void*)(nint)alloc.AllocateCString(value); - } else if (hadId) { - // var @ref = ecs_ref_init_id(world, entity, id); - // var ptr = ecs_ref_get_id(world, &@ref, id); - // var desc = Unsafe.AsRef(ptr); - // alloc.Free((nint)desc.Value); // Free previous value. + var str = GlobalHeapAllocator.Instance.AllocateCString(value); + var desc = new Doc.Description { Value = (void*)(nint)str }; + entity.Set(id, desc); + } else { entity.Remove(id); } + return entity; } - public static string? GetDocName(this EntityRef entity, bool fallbackToEntityName = true) + public static string? GetDocName(this Entity entity, bool fallbackToEntityName = true) => fallbackToEntityName || entity.Has() ? ecs_doc_get_name(entity.World, entity).FlecsToString() : null; - public static EntityRef SetDocName(this EntityRef entity, string? value) - => Set(entity, value); + public static Entity SetDocName(this Entity entity, string? value) + => Set(entity, value); - public static string? GetDocBrief(this EntityRef entity) + public static string? GetDocBrief(this Entity entity) => ecs_doc_get_brief(entity.World, entity).FlecsToString()!; - public static EntityRef SetDocBrief(this EntityRef entity, string? value) - => Set(entity, value); + public static Entity SetDocBrief(this Entity entity, string? value) + => Set(entity, value); - public static string? GetDocDetail(this EntityRef entity) + public static string? GetDocDetail(this Entity entity) => ecs_doc_get_detail(entity.World, entity).FlecsToString()!; - public static EntityRef SetDocDetail(this EntityRef entity, string? value) - => Set(entity, value); + public static Entity SetDocDetail(this Entity entity, string? value) + => Set(entity, value); - public static string? GetDocLink(this EntityRef entity) + public static string? GetDocLink(this Entity entity) => ecs_doc_get_link(entity.World, entity).FlecsToString()!; - public static EntityRef SetDocLink(this EntityRef entity, string? value) - => Set(entity, value); + public static Entity SetDocLink(this Entity entity, string? value) + => Set(entity, value); - public static string? GetDocColor(this EntityRef entity) + public static string? GetDocColor(this Entity entity) => ecs_doc_get_color(entity.World, entity).FlecsToString()!; - public static EntityRef SetDocColor(this EntityRef entity, string? value) - => Set(entity, value); + public static Entity SetDocColor(this Entity entity, string? value) + => Set(entity, value); } diff --git a/src/gaemstone/Flecs/ObserverEvent.cs b/src/gaemstone/Flecs/ObserverEvent.cs deleted file mode 100644 index 21eb266..0000000 --- a/src/gaemstone/Flecs/ObserverEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using gaemstone.ECS; - -namespace gaemstone.Flecs; - -[Module, Path("/flecs/core")] -public static partial class ObserverEvent -{ - [Entity] public struct OnAdd { } - [Entity] public struct OnRemove { } - [Entity] public struct OnSet { } - [Entity] public struct UnSet { } - [Entity] public struct OnTableEmpty { } - [Entity] public struct OnTableFilled { } -} diff --git a/src/gaemstone/Flecs/Pipeline.cs b/src/gaemstone/Flecs/Pipeline.cs index b250583..b4800ff 100644 --- a/src/gaemstone/Flecs/Pipeline.cs +++ b/src/gaemstone/Flecs/Pipeline.cs @@ -2,8 +2,92 @@ using gaemstone.ECS; namespace gaemstone.Flecs; -[Module, Path("/flecs/pipeline")] -public static partial class Pipeline +[BuiltIn, Module, Path("/flecs/pipeline")] +public partial class Pipeline { [Entity] public struct Phase { } + + + [Entity, Add] + public struct PreFrame { } + + /// + /// This phase contains all the systems that load data into your ECS. + /// This would be a good place to load keyboard and mouse inputs. + /// + [Entity, Add] + [DependsOn] + public struct OnLoad { } + + /// + /// Often the imported data needs to be processed. Maybe you want to associate + /// your keypresses with high level actions rather than comparing explicitly + /// in your game code if the user pressed the 'K' key. + /// + [Entity, Add] + [DependsOn] + public struct PostLoad { } + + /// + /// Now that the input is loaded and processed, it's time to get ready to + /// start processing our game logic. Anything that needs to happen after + /// input processing but before processing the game logic can happen here. + /// This can be a good place to prepare the frame, maybe clean up some + /// things from the previous frame, etcetera. + /// + [Entity, Add] + [DependsOn] + public struct PreUpdate { } + + /// + /// This is usually where the magic happens! This is where you put all of + /// your gameplay systems. By default systems are added to this phase. + /// + [Entity, Add] + [DependsOn] + public struct OnUpdate { } + + /// + /// This phase was introduced to deal with validating the state of the game + /// after processing the gameplay systems. Sometimes you entities too close + /// to each other, or the speed of an entity is increased too much. + /// This phase is for righting that wrong. A typical feature to implement + /// in this phase would be collision detection. + /// + [Entity, Add] + [DependsOn] + public struct OnValidate { } + + /// + /// When your game logic has been updated, and your validation pass has ran, + /// you may want to apply some corrections. For example, if your collision + /// detection system detected collisions in the OnValidate phase, + /// you may want to move the entities so that they no longer overlap. + /// + [Entity, Add] + [DependsOn] + public struct PostUpdate { } + + /// + /// Now that all of the frame data is computed, validated and corrected for, + /// it is time to prepare the frame for rendering. Any systems that need to + /// run before rendering, but after processing the game logic should go here. + /// A good example would be a system that calculates transform matrices from + /// a scene graph. + /// + [Entity, Add] + [DependsOn] + public struct PreStore { } + + /// + /// This is where it all comes together. Your frame is ready to be + /// rendered, and that is exactly what you would do in this phase. + /// + [Entity, Add] + [DependsOn] + public struct OnStore { } + + [Entity, Add] + [DependsOn] + public struct PostFrame { } } diff --git a/src/gaemstone/Flecs/SystemPhase.cs b/src/gaemstone/Flecs/SystemPhase.cs deleted file mode 100644 index 827d165..0000000 --- a/src/gaemstone/Flecs/SystemPhase.cs +++ /dev/null @@ -1,91 +0,0 @@ -using gaemstone.ECS; -using static gaemstone.Flecs.Pipeline; - -namespace gaemstone.Flecs; - -[Module, Path("/flecs/pipeline")] -public static partial class SystemPhase -{ - [Entity, Add] - public struct PreFrame { } - - /// - /// This phase contains all the systems that load data into your ECS. - /// This would be a good place to load keyboard and mouse inputs. - /// - [Entity, Add] - [DependsOn] - public struct OnLoad { } - - /// - /// Often the imported data needs to be processed. Maybe you want to associate - /// your keypresses with high level actions rather than comparing explicitly - /// in your game code if the user pressed the 'K' key. - /// - [Entity, Add] - [DependsOn] - public struct PostLoad { } - - /// - /// Now that the input is loaded and processed, it's time to get ready to - /// start processing our game logic. Anything that needs to happen after - /// input processing but before processing the game logic can happen here. - /// This can be a good place to prepare the frame, maybe clean up some - /// things from the previous frame, etcetera. - /// - [Entity, Add] - [DependsOn] - public struct PreUpdate { } - - /// - /// This is usually where the magic happens! This is where you put all of - /// your gameplay systems. By default systems are added to this phase. - /// - [Entity, Add] - [DependsOn] - public struct OnUpdate { } - - /// - /// This phase was introduced to deal with validating the state of the game - /// after processing the gameplay systems. Sometimes you entities too close - /// to each other, or the speed of an entity is increased too much. - /// This phase is for righting that wrong. A typical feature to implement - /// in this phase would be collision detection. - /// - [Entity, Add] - [DependsOn] - public struct OnValidate { } - - /// - /// When your game logic has been updated, and your validation pass has ran, - /// you may want to apply some corrections. For example, if your collision - /// detection system detected collisions in the OnValidate phase, - /// you may want to move the entities so that they no longer overlap. - /// - [Entity, Add] - [DependsOn] - public struct PostUpdate { } - - /// - /// Now that all of the frame data is computed, validated and corrected for, - /// it is time to prepare the frame for rendering. Any systems that need to - /// run before rendering, but after processing the game logic should go here. - /// A good example would be a system that calculates transform matrices from - /// a scene graph. - /// - [Entity, Add] - [DependsOn] - public struct PreStore { } - - /// - /// This is where it all comes together. Your frame is ready to be - /// rendered, and that is exactly what you would do in this phase. - /// - [Entity, Add] - [DependsOn] - public struct OnStore { } - - [Entity, Add] - [DependsOn] - public struct PostFrame { } -} diff --git a/src/gaemstone/Flecs/Systems/Monitor.cs b/src/gaemstone/Flecs/Systems/Monitor.cs index 938a0ba..15dd014 100644 --- a/src/gaemstone/Flecs/Systems/Monitor.cs +++ b/src/gaemstone/Flecs/Systems/Monitor.cs @@ -1,19 +1,20 @@ using System.Runtime.InteropServices; using gaemstone.ECS; -using gaemstone.Utility; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.Flecs.Systems; -[Module, Path("/flecs/monitor")] +[BuiltIn, Module, Path("/flecs/monitor")] public unsafe partial class Monitor : IModuleInitializer { - public void Initialize(EntityRef module) + public static void Initialize(Entity module) { using var alloc = TempAllocator.Use(); - ecs_import_c(module.World, new() { Data = new() { - Pointer = &MonitorImport } }, alloc.AllocateCString("FlecsMonitor")); + ecs_import_c(module.World, + new() { Data = new() { Pointer = &MonitorImport } }, + alloc.AllocateCString("FlecsMonitor")); } [UnmanagedCallersOnly] diff --git a/src/gaemstone/Flecs/Systems/Rest.cs b/src/gaemstone/Flecs/Systems/Rest.cs index 8235fd1..d20cd8a 100644 --- a/src/gaemstone/Flecs/Systems/Rest.cs +++ b/src/gaemstone/Flecs/Systems/Rest.cs @@ -1,19 +1,20 @@ using System.Runtime.InteropServices; using gaemstone.ECS; -using gaemstone.Utility; +using gaemstone.ECS.Utility; using static flecs_hub.flecs; namespace gaemstone.Flecs.Systems; -[Module, Path("/flecs/rest")] +[BuiltIn, Module, Path("/flecs/rest")] public unsafe partial class Rest : IModuleInitializer { - public void Initialize(EntityRef module) + public static void Initialize(Entity module) { using (var alloc = TempAllocator.Use()) - ecs_import_c(module.World, new() { Data = new() { - Pointer = &RestImport } }, alloc.AllocateCString("FlecsRest")); + ecs_import_c(module.World, + new() { Data = new() { Pointer = &RestImport } }, + alloc.AllocateCString("FlecsRest")); module.NewChild("Rest").Build() .CreateLookup() diff --git a/src/gaemstone/Universe+Modules.cs b/src/gaemstone/Universe+Modules.cs index b0ea253..799f33c 100644 --- a/src/gaemstone/Universe+Modules.cs +++ b/src/gaemstone/Universe+Modules.cs @@ -1,30 +1,31 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Reflection; +using System.Collections.Immutable; +using System.Linq; using gaemstone.ECS; -using gaemstone.Utility; using static gaemstone.Flecs.Core; using Module = gaemstone.Flecs.Core.Module; namespace gaemstone; -public class ModuleManager +public class ModuleManager + : IEnumerable.IModuleInfo> { - private readonly Dictionary _modules = new(); + private readonly Dictionary, IModuleInfo> _modules = new(); - public Universe Universe { get; } - public ModuleManager(Universe universe) + public Universe Universe { get; } + public ModuleManager(Universe universe) => Universe = universe; - internal ModuleInfo? Lookup(Entity entity) + internal IModuleInfo? Lookup(Entity entity) => _modules.GetValueOrDefault(entity); - public EntityRef Register() - where T : class, IModule, new() + public Entity Register() + where T : IModule { - var moduleType = typeof(T); - if (moduleType.IsGenericType) throw new Exception( - $"Module {moduleType} must be a non-generic class"); + // if (!typeof(T).IsAssignableTo(typeof(IModule))) throw new ArgumentException( + // $"The specified type {typeof(T)} does not implement IModule", nameof(T)); var module = new ModuleInfo(Universe); _modules.Add(module.Entity, module); @@ -32,87 +33,97 @@ public class ModuleManager return module.Entity; } - private void TryEnableModule(ModuleInfo module) + private void TryEnableModule(IModuleInfo module) { - if (module.UnmetDependencies.Count > 0) return; + if (!module.Dependencies.All(dep => dep.IsDependencyMet)) return; + + Console.WriteLine($"Enabling module {module.Entity.Path}"); - Console.WriteLine($"Enabling module {module.Entity.GetFullPath()} ..."); module.Enable(); // Find other modules that might be missing this module as a dependency. foreach (var other in _modules.Values) { - if (other.IsActive) continue; - if (!other.UnmetDependencies.Contains(module.Entity)) continue; + if (other.IsInitialized) continue; + var dependency = other.Dependencies.FirstOrDefault(dep => dep.Entity == module.Entity); + if (dependency == null) continue; - // Move the just enabled module from unmet to met depedencies. - other.UnmetDependencies.Remove(module.Entity); - other.MetDependencies.Add(module); + dependency.Info = module; + dependency.IsDependencyMet = true; TryEnableModule(other); } } - internal abstract class ModuleInfo + public interface IModuleInfo { - public abstract EntityRef Entity { get; } - public abstract bool IsActive { get; } + Entity Entity { get; } + IReadOnlyCollection Dependencies { get; } + bool IsInitialized { get; } + void Enable(); + } - public HashSet MetDependencies { get; } = new(); - public HashSet UnmetDependencies { get; } = new(); + // IEnumerable implementation + public IEnumerator GetEnumerator() => _modules.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public abstract void Enable(); - } - internal class ModuleInfo : ModuleInfo - where T : IModule, new() + public class ModuleDependency { - public override EntityRef Entity { get; } - public override bool IsActive => (Instance != null); - public T? Instance { get; internal set; } + public Entity Entity { get; } + public IModuleInfo? Info { get; internal set; } + public bool IsDependencyMet { get; internal set; } - public ModuleInfo(Universe universe) + public ModuleDependency(Entity entity, + IModuleInfo? info = null, bool isDependencyMet = false) { - var builder = universe.New(T.ModulePath).Add(); - - foreach (var dependsPath in T.Dependencies) { - var dependency = universe.LookupByPath(dependsPath) ?? - universe.New(dependsPath).Add().Disable().Build(); + Entity = entity; + Info = info; + IsDependencyMet = isDependencyMet; + } + } - var depModule = universe.Modules.Lookup(dependency); - if (depModule?.IsActive == true) MetDependencies.Add(depModule); - else { UnmetDependencies.Add(dependency); builder.Disable(); } + internal class ModuleInfo : IModuleInfo + where T : IModule + { + public Entity Entity { get; } + public IReadOnlyCollection Dependencies { get; } + public bool IsInitialized { get; private set; } - builder.Add(dependency); + public ModuleInfo(Universe universe) + { + var world = universe.World; + var builder = world.New(T.Path); + var deps = new List(); + + if (!T.IsBuiltIn) { + builder.Add(); + foreach (var dependsPath in T.Dependencies) { + var dependency = world.LookupPathOrNull(dependsPath) ?? + world.New(dependsPath).Add().Add().Build(); + + var depModule = universe.Modules.Lookup(dependency); + var isDepInit = (depModule?.IsInitialized == true); + + deps.Add(new(dependency, depModule, isDepInit)); + if (!isDepInit) builder.Add(); + builder.Add(dependency); + } } Entity = builder.Build().CreateLookup(); + Dependencies = deps.AsReadOnly(); - // Ensure all parent entities have Module set. - for (var p = Entity.Parent; p != null; p = p.Parent) - p.Add(); + if (!T.IsBuiltIn) + // Ensure all parent entities have the Module tag set. + for (var p = Entity.Parent; p is Entity parent; p = parent.Parent) + parent.Add(); } - public override void Enable() + public void Enable() { Entity.Enable(); - Instance = new T(); - (Instance as IModuleAutoRegisterComponents)?.RegisterComponents(Entity); - (Instance as IModuleInitializer)?.Initialize(Entity); - RegisterMethods(Instance); - } - - private void RegisterMethods(object? instance) - { - var world = Entity.World; - foreach (var method in typeof(T).GetMethods( - BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static | BindingFlags.Instance - )) { - if (method.Has()) - world.InitSystem(instance, method).ChildOf(Entity); - if (method.Has()) - world.InitObserver(instance, method).ChildOf(Entity); - } + T.Initialize(Entity); + IsInitialized = true; } } } diff --git a/src/gaemstone/Universe.cs b/src/gaemstone/Universe.cs index 3fb235e..8364254 100644 --- a/src/gaemstone/Universe.cs +++ b/src/gaemstone/Universe.cs @@ -2,32 +2,25 @@ using gaemstone.ECS; namespace gaemstone; -public class Universe : World +public class Universe { - public ModuleManager Modules { get; } + public World World { get; } + public ModuleManager Modules { get; } public Universe(params string[] args) - : base(args) { + World = new(args); Modules = new(this); // Bootstrap [Relation] tag, since it will be added to some Flecs types. - New("/gaemstone/Doc/Relation").Build().CreateLookup(); + World.New("/gaemstone/Doc/Relation").Build() + .CreateLookup(); // 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(); + Modules.Register(); + Modules.Register(); Modules.Register(); - - // Create "Game" entity to store global state. - New("/Game").Symbol("Game").Build() - .CreateLookup().Add(); } } diff --git a/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs b/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs deleted file mode 100644 index 1317c28..0000000 --- a/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs +++ /dev/null @@ -1,299 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using System.Text; - -namespace gaemstone.Utility.IL; - -public class ILGeneratorWrapper -{ - private readonly DynamicMethod _method; - private readonly ILGenerator _il; - private readonly List _locals = new(); - private readonly List<(int Offset, int Indent, OpCode Code, object? Arg)> _instructions = new(); - private readonly Dictionary _labelToOffset = new(); - private readonly Stack _indents = new(); - - public ILGeneratorWrapper(DynamicMethod method) - { - _method = method; - _il = method.GetILGenerator(); - } - - public string ToReadableString() - { - var sb = new StringBuilder(); - sb.AppendLine("Parameters:"); - foreach (var (param, index) in _method.GetParameters().Select((p, i) => (p, i))) - sb.AppendLine($" Argument({index}, {param.ParameterType.GetFriendlyName()})"); - sb.AppendLine("Return:"); - sb.AppendLine($" {_method.ReturnType.GetFriendlyName()}"); - sb.AppendLine(); - - sb.AppendLine("Locals:"); - foreach (var local in _locals) - sb.AppendLine($" {local}"); - sb.AppendLine(); - - sb.AppendLine("Instructions:"); - foreach (var (offset, indent, code, arg) in _instructions) { - sb.Append(" "); - - // Append instruction offset. - if (offset < 0) sb.Append(" "); - else sb.Append($"0x{offset:X4} "); - - // Append instruction opcode. - if (code == OpCodes.Nop) sb.Append(" "); - else sb.Append($"{code.Name,-12}"); - - // Append indents. - for (var i = 0; i < indent; i++) - sb.Append("| "); - - // Append instruction argument. - if (code == OpCodes.Nop) sb.Append("// "); - switch (arg) { - case Label label: sb.Append($"Label(0x{_labelToOffset.GetValueOrDefault(label, -1):X4})"); break; - case not null: sb.Append(arg); break; - } - - sb.AppendLine(); - } - return sb.ToString(); - } - - - public IArgument Argument(int index) - { - var type = _method.GetParameters()[index].ParameterType; - if (type.IsByRefLike) return new ArgumentImpl(index, type); - return (IArgument)Activator.CreateInstance(typeof(ArgumentImpl<>).MakeGenericType(type), index)!; - } - public IArgument Argument(int index) => (IArgument)Argument(index); - - public ILocal Local(Type type, string? name = null) - { - var builder = _il.DeclareLocal(type); - var local = type.IsByRefLike ? new LocalImpl(builder, name) - : (ILocal)Activator.CreateInstance(typeof(LocalImpl<>).MakeGenericType(type), builder, name)!; - _locals.Add(local); - return local; - } - public ILocal Local(string? name = null) => (ILocal)Local(typeof(T), name); - public ILocal LocalArray(Type type, string? name = null) => (ILocal)Local(type.MakeArrayType(), name); - public ILocal LocalArray(string? name = null) => (ILocal)Local(typeof(T).MakeArrayType(), name); - - public Label DefineLabel() => _il.DefineLabel(); - public void MarkLabel(Label label) - { - _instructions.Add((-1, _indents.Count, OpCodes.Nop, label)); - _labelToOffset.Add(label, _il.ILOffset); - _il.MarkLabel(label); - } - - - private void AddInstr(OpCode code, object? arg = null) => _instructions.Add((_il.ILOffset, _indents.Count, code, arg)); - public void Comment(string comment) => _instructions.Add((-1, _indents.Count, OpCodes.Nop, comment)); - - private static readonly MethodInfo _writeLine = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) })!; - public void Log(string str) { Load(str); Call(_writeLine); } - - internal void Emit(OpCode code) { AddInstr(code, null); _il.Emit(code); } - internal void Emit(OpCode code, int arg) { AddInstr(code, arg); _il.Emit(code, arg); } - internal void Emit(OpCode code, string arg) { AddInstr(code, arg); _il.Emit(code, arg); } - internal void Emit(OpCode code, Type type) { AddInstr(code, type); _il.Emit(code, type); } - internal void Emit(OpCode code, Label label) { AddInstr(code, label); _il.Emit(code, label); } - internal void Emit(OpCode code, ILocal local) { AddInstr(code, local); _il.Emit(code, local.Builder); } - internal void Emit(OpCode code, IArgument arg) { AddInstr(code, arg); _il.Emit(code, arg.Index); } - internal void Emit(OpCode code, MethodInfo method) { AddInstr(code, method); _il.Emit(code, method); } - internal void Emit(OpCode code, ConstructorInfo constr) { AddInstr(code, constr); _il.Emit(code, constr); } - - public void Dup() => Emit(OpCodes.Dup); - public void Pop() => Emit(OpCodes.Pop); - - public void LoadNull() => Emit(OpCodes.Ldnull); - public void LoadConst(int value) => Emit(OpCodes.Ldc_I4, value); - public void Load(string value) => Emit(OpCodes.Ldstr, value); - - private static readonly MethodInfo _typeFromHandleMethod = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!; - public void Typeof(Type value) { Emit(OpCodes.Ldtoken, value); Call(_typeFromHandleMethod); } - - public void Load(IArgument arg) => Emit(OpCodes.Ldarg, arg); - public void LoadAddr(IArgument arg) => Emit(OpCodes.Ldarga, arg); - - public void Load(ILocal local) => Emit(OpCodes.Ldloc, local); - public void LoadAddr(ILocal local) => Emit(OpCodes.Ldloca, local); - public void Store(ILocal local) => Emit(OpCodes.Stloc, local); - public void Set(ILocal local, int value) { LoadConst(value); Store(local); } - - public void LoadLength() { Emit(OpCodes.Ldlen); Emit(OpCodes.Conv_I4); } - public void LoadLength(IArgument array) { Load(array); LoadLength(); } - public void LoadLength(ILocal array) { Load(array); LoadLength(); } - - public void LoadObj(Type type) => Emit(OpCodes.Ldobj, type); - public void LoadObj() where T : struct => LoadObj(typeof(T)); - public void LoadObj(ILocal local) { LoadAddr(local); LoadObj(local.LocalType); } - public void LoadObj(IArgument arg) { LoadAddr(arg); LoadObj(arg.ArgumentType); } - - public void LoadElem(Type type) => Emit(OpCodes.Ldelem, type); - public void LoadElem(Type type, int index) { LoadConst(index); LoadElem(type); } - public void LoadElem(Type type, ILocal index) { Load(index); LoadElem(type); } - public void LoadElem(Type type, IArgument array, int index) { Load(array); LoadElem(type, index); } - public void LoadElem(Type type, IArgument array, ILocal index) { Load(array); LoadElem(type, index); } - public void LoadElem(Type type, ILocal array, int index) { Load(array); LoadElem(type, index); } - public void LoadElem(Type type, ILocal array, ILocal index) { Load(array); LoadElem(type, index); } - - public void LoadElemRef() => Emit(OpCodes.Ldelem_Ref); - public void LoadElemRef(int index) { LoadConst(index); LoadElemRef(); } - public void LoadElemRef(ILocal index) { Load(index); LoadElemRef(); } - public void LoadElemRef(IArgument array, int index) { Load(array); LoadElemRef(index); } - public void LoadElemRef(IArgument array, ILocal index) { Load(array); LoadElemRef(index); } - public void LoadElemRef(ILocal array, int index) { Load(array); LoadElemRef(index); } - public void LoadElemRef(ILocal array, ILocal index) { Load(array); LoadElemRef(index); } - - public void LoadElemEither(Type type) { if (type.IsValueType) LoadElem(type); else LoadElemRef(); } - public void LoadElemEither(Type type, int index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); } - public void LoadElemEither(Type type, ILocal index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); } - public void LoadElemEither(Type type, IArgument array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } - public void LoadElemEither(Type type, IArgument array, ILocal index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } - public void LoadElemEither(Type type, ILocal array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } - public void LoadElemEither(Type type, ILocal array, ILocal index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } - - public void LoadElemAddr(Type type) => Emit(OpCodes.Ldelema, type); - public void LoadElemAddr(Type type, int index) { LoadConst(index); LoadElemAddr(type); } - public void LoadElemAddr(Type type, ILocal index) { Load(index); LoadElemAddr(type); } - public void LoadElemAddr(Type type, IArgument array, int index) { Load(array); LoadElemAddr(type, index); } - public void LoadElemAddr(Type type, IArgument array, ILocal index) { Load(array); LoadElemAddr(type, index); } - public void LoadElemAddr(Type type, ILocal array, int index) { Load(array); LoadElemAddr(type, index); } - public void LoadElemAddr(Type type, ILocal array, ILocal index) { Load(array); LoadElemAddr(type, index); } - - public void Load(PropertyInfo info) => CallVirt(info.GetMethod!); - public void Load(ILocal obj, PropertyInfo info) { Load(obj); Load(info); } - public void Load(IArgument obj, PropertyInfo info) { Load(obj); Load(info); } - - public void Add() => Emit(OpCodes.Add); - public void Increment(ILocal local) { Load(local); LoadConst(1); Add(); Store(local); } - - public void Init(Type type) => Emit(OpCodes.Initobj, type); - public void Init() where T : struct => Init(typeof(T)); - - public void New(ConstructorInfo constructor) => Emit(OpCodes.Newobj, constructor); - public void New(Type type) => New(type.GetConstructors().Single()); - public void New(Type type, params Type[] paramTypes) => New(type.GetConstructor(paramTypes)!); - - public void Cast(Type type) => Emit(OpCodes.Castclass, type); - public void Cast() => Cast(typeof(T)); - public void Box(Type type) => Emit(OpCodes.Box, type); - public void Box() => Box(typeof(T)); - - public void Goto(Label label) => Emit(OpCodes.Br, label); - public void GotoIfTrue(Label label) => Emit(OpCodes.Brtrue, label); - public void GotoIfFalse(Label label) => Emit(OpCodes.Brfalse, label); - - public void GotoIf(Label label, ILocal a, Comparison op, ILocal b) - { Load(a); Load(b); Emit(op.Code, label); } - public void GotoIfNull(Label label, ILocal local) - { Load(local); Emit(OpCodes.Brfalse, label); } - public void GotoIfNotNull(Label label, ILocal local) - { Load(local); Emit(OpCodes.Brtrue, label); } - - public void Call(MethodInfo method) => Emit(OpCodes.Call, method); - public void CallVirt(MethodInfo method) => Emit(OpCodes.Callvirt, method); - - private static readonly MethodInfo _consoleWriteLineMethod = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) })!; - public void Print(string str) { Load(str); Call(_consoleWriteLineMethod); } - - public void Return() => Emit(OpCodes.Ret); - - - public delegate void WhileBodyAction(Label continueLabel, Label breakLabel); - public delegate void WhileTestAction(Label continueLabel); - - public void While(string name, WhileTestAction testAction, WhileBodyAction bodyAction) { - var bodyLabel = DefineLabel(); - var testLabel = DefineLabel(); - var breakLabel = DefineLabel(); - - Goto(testLabel); - Comment("BEGIN LOOP " + name); - MarkLabel(bodyLabel); - using (Indent()) bodyAction(testLabel, breakLabel); - Comment("TEST LOOP " + name); - MarkLabel(testLabel); - using (Indent()) testAction(bodyLabel); - Comment("END LOOP " + name); - MarkLabel(breakLabel); - } - - - public IDisposable Block(Action onClose) - => new BlockImpl(onClose); - public IDisposable Indent() - { - BlockImpl indent = null!; - indent = new(() => { if (_indents.Pop() != indent) throw new InvalidOperationException(); }); - _indents.Push(indent); - return indent; - } - - internal class BlockImpl : IDisposable - { - public Action OnClose { get; } - public BlockImpl(Action onClose) => OnClose = onClose; - public void Dispose() => OnClose(); - } - - - internal class ArgumentImpl : IArgument - { - public int Index { get; } - public Type ArgumentType { get; } - public ArgumentImpl(int index, Type type) { Index = index; ArgumentType = type; } - public override string ToString() => $"Argument({Index}, {ArgumentType.GetFriendlyName()})"; - } - internal class ArgumentImpl : ArgumentImpl, IArgument - { public ArgumentImpl(int index) : base(index, typeof(T)) { } } - - internal class LocalImpl : ILocal - { - public LocalBuilder Builder { get; } - public string? Name { get; } - public Type LocalType => Builder.LocalType; - public LocalImpl(LocalBuilder builder, string? name) { Builder = builder; Name = name; } - public override string ToString() => $"Local({Builder.LocalIndex}, {LocalType.GetFriendlyName()}){(Name != null ? $" // {Name}" : "")}"; - } - internal class LocalImpl : LocalImpl, ILocal - { public LocalImpl(LocalBuilder builder, string? name) : base(builder, name) { } } -} - -public class Comparison -{ - public static Comparison NotEqual { get; } = new(OpCodes.Bne_Un); - public static Comparison LessThan { get; } = new(OpCodes.Blt); - public static Comparison LessOrEq { get; } = new(OpCodes.Ble); - public static Comparison Equal { get; } = new(OpCodes.Beq); - public static Comparison GreaterOrEq { get; } = new(OpCodes.Bge); - public static Comparison GreaterThan { get; } = new(OpCodes.Bgt); - - public OpCode Code { get; } - private Comparison(OpCode code) => Code = code; -} - -public interface IArgument -{ - int Index { get; } - Type ArgumentType { get; } -} -public interface IArgument - : IArgument { } - -public interface ILocal -{ - LocalBuilder Builder { get; } - Type LocalType { get; } -} -public interface ILocal - : ILocal { } diff --git a/src/gaemstone/Utility/IL/IterActionGenerator.cs b/src/gaemstone/Utility/IL/IterActionGenerator.cs deleted file mode 100644 index 2815ab6..0000000 --- a/src/gaemstone/Utility/IL/IterActionGenerator.cs +++ /dev/null @@ -1,404 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using System.Runtime.CompilerServices; -using gaemstone.ECS; - -namespace gaemstone.Utility.IL; - -// TODO: Support tuple syntax to match relationship pairs. -public unsafe class IterActionGenerator -{ - private static readonly ConstructorInfo _entityRefCtor = typeof(EntityRef).GetConstructors().Single(); - - private static readonly PropertyInfo _iterWorldProp = typeof(Iterator).GetProperty(nameof(Iterator.World))!; - private static readonly PropertyInfo _iterDeltaTimeProp = typeof(Iterator).GetProperty(nameof(Iterator.DeltaTime))!; - private static readonly PropertyInfo _iterCountProp = typeof(Iterator).GetProperty(nameof(Iterator.Count))!; - private static readonly MethodInfo _iterEntityMethod = typeof(Iterator).GetMethod(nameof(Iterator.Entity))!; - private static readonly MethodInfo _iterFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field ), 1, new[] { typeof(int) })!; - private static readonly MethodInfo _iterFieldOrEmptyMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldOrEmpty), 1, new[] { typeof(int) })!; - private static readonly MethodInfo _iterFieldRefMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field ), 1, new[] { typeof(int), Type.MakeGenericMethodParameter(0) })!; - private static readonly MethodInfo _iterFieldOrEmptyRefMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldOrEmpty), 1, new[] { typeof(int), Type.MakeGenericMethodParameter(0) })!; - - private static readonly ConditionalWeakTable _cache = new(); - private static readonly Dictionary>> _globalUniqueParameters = new() { - [typeof(World)] = (IL, iter) => { IL.Load(iter, _iterWorldProp); }, - [typeof(Universe)] = (IL, iter) => { IL.Load(iter, _iterWorldProp); IL.Cast(typeof(Universe)); }, - [typeof(TimeSpan)] = (IL, iter) => { IL.Load(iter, _iterDeltaTimeProp); }, - }; - private static readonly Dictionary, ILocal>> _uniqueParameters = new() { - [typeof(Iterator)] = (IL, iter, i) => { IL.Load(iter); }, - [typeof(EntityRef)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iterEntityMethod); }, - }; - - public World World { get; } - public MethodInfo Method { get; } - public IReadOnlyList Parameters { get; } - - public IReadOnlyList Terms { get; } - public Action GeneratedAction { get; } - public string ReadableString { get; } - - public void RunWithTryCatch(object? instance, Iterator iter) - { - try { GeneratedAction(instance, iter); } - catch { Console.Error.WriteLine(ReadableString); throw; } - } - - public IterActionGenerator(World world, MethodInfo method) - { - World = world; - Method = method; - Parameters = method.GetParameters().Select(ParamInfo.Build).ToImmutableArray(); - - var name = "<>Query_" + string.Join("_", method.Name); - var genMethod = new DynamicMethod(name, null, new[] { typeof(object), typeof(Iterator) }); - var IL = new ILGeneratorWrapper(genMethod); - - var instanceArg = IL.Argument(0); - var iteratorArg = IL.Argument(1); - - var fieldIndex = 0; - var paramData = new List<(ParamInfo Info, Term? Term, ILocal? FieldLocal, ILocal? TempLocal)>(); - foreach (var p in Parameters) { - // If the parameter is unique, we don't create a term for it. - if (p.Kind <= ParamKind.Unique) - { paramData.Add((p, null, null, null)); continue; } - - // Create a term to add to the query. - var term = new Term(world.LookupByTypeOrThrow(p.UnderlyingType)) { - Source = (p.Source != null) ? (TermId)World.LookupByTypeOrThrow(p.Source) : null, - InOut = p.Kind switch { - ParamKind.In => TermInOutKind.In, - ParamKind.Out => TermInOutKind.Out, - ParamKind.Not or ParamKind.Not => TermInOutKind.None, - _ => default, - }, - Oper = p.Kind switch { - ParamKind.Not => TermOperKind.Not, - ParamKind.Or => TermOperKind.Or, - _ when !p.IsRequired => TermOperKind.Optional, - _ => default, - }, - }; - - // If this and the previous parameter are marked with [Or], do not advance the field index. - if ((fieldIndex == 0) || (p.Kind != ParamKind.Or) || (paramData[^1].Info.Kind != ParamKind.Or)) - fieldIndex++; - - var spanType = typeof(Span<>).MakeGenericType(p.FieldType); - var fieldLocal = (ILocal?)null; - var tempLocal = (ILocal?)null; - - switch (p.Kind) { - // FIXME: Currently would not work with [Or]'d components. - case ParamKind.Has or ParamKind.Not or ParamKind.Or: - if (!p.ParameterType.IsValueType) break; - // If parameter is a struct, we require a temporary local that we can - // later load onto the stack when loading the arguments for the action. - IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});"); - tempLocal = IL.Local(p.ParameterType); - IL.LoadAddr(tempLocal); - IL.Init(tempLocal.LocalType); - break; - - case ParamKind.Nullable or ParamKind.Or: - IL.Comment($"{p.Info.Name}Field = iterator.FieldOrEmpty<{p.FieldType.Name}>({fieldIndex})"); - fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field"); - IL.Load(iteratorArg); - IL.LoadConst(fieldIndex); - IL.Call(_iterFieldOrEmptyMethod.MakeGenericMethod(p.FieldType)); - IL.Store(fieldLocal); - - if (p.UnderlyingType.IsValueType) { - IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});"); - tempLocal = IL.Local(p.ParameterType); - IL.LoadAddr(tempLocal); - IL.Init(tempLocal.LocalType); - } - break; - - default: - IL.Comment($"{p.Info.Name}Field = iterator.Field<{p.FieldType.Name}>({fieldIndex})"); - fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field"); - IL.Load(iteratorArg); - IL.LoadConst(fieldIndex); - IL.Call(_iterFieldMethod.MakeGenericMethod(p.FieldType)); - IL.Store(fieldLocal); - break; - } - - paramData.Add((p, term, fieldLocal, tempLocal)); - } - - var indexLocal = IL.Local("iter_index"); - var countLocal = IL.Local("iter_count"); - - IL.Set(indexLocal, 0); - IL.Load(iteratorArg, _iterCountProp); - IL.Store(countLocal); - - // If all parameters are fixed, iterator count will be 0, but since - // the query matched, we want to run the callback at least once. - IL.Comment("if (iter_count == 0) iter_count = 1;"); - var dontIncrementLabel = IL.DefineLabel(); - IL.Load(countLocal); - IL.GotoIfTrue(dontIncrementLabel); - IL.LoadConst(1); - IL.Store(countLocal); - IL.MarkLabel(dontIncrementLabel); - - IL.While("IteratorLoop", (@continue) => { - IL.GotoIf(@continue, indexLocal, Comparison.LessThan, countLocal); - }, (_, _) => { - if (!Method.IsStatic) - IL.Load(instanceArg); - - foreach (var (info, term, fieldLocal, tempLocal) in paramData) { - var isValueType = info.UnderlyingType.IsValueType; - var paramName = info.ParameterType.GetFriendlyName(); - switch (info.Kind) { - - case ParamKind.GlobalUnique: - IL.Comment($"Global unique parameter {paramName}"); - _globalUniqueParameters[info.ParameterType](IL, iteratorArg); - break; - - case ParamKind.Unique: - IL.Comment($"Unique parameter {paramName}"); - _uniqueParameters[info.ParameterType](IL, iteratorArg, indexLocal!); - break; - - // FIXME: Currently would not work with [Or]'d components. - case ParamKind.Has or ParamKind.Not or ParamKind.Or: - IL.Comment($"Has parameter {paramName}"); - if (isValueType) IL.LoadObj(tempLocal!); - else IL.LoadNull(); - break; - - default: - var spanType = isValueType ? typeof(Span<>) : typeof(Iterator.SpanToRef<>); - var concreteSpanType = spanType.MakeGenericType(info.UnderlyingType); - var spanItemMethod = concreteSpanType.GetProperty("Item")!.GetMethod!; - var spanLengthMethod = concreteSpanType.GetProperty("Length")!.GetMethod!; - - IL.Comment($"Parameter {paramName}"); - if (info.IsByRef) { - IL.LoadAddr(fieldLocal!); - if (info.IsFixed) IL.LoadConst(0); - else IL.Load(indexLocal!); - IL.Call(spanItemMethod); - } else if (info.IsRequired) { - IL.LoadAddr(fieldLocal!); - if (info.IsFixed) IL.LoadConst(0); - else IL.Load(indexLocal!); - IL.Call(spanItemMethod); - if (isValueType) IL.LoadObj(info.FieldType); - } else { - var elseLabel = IL.DefineLabel(); - var doneLabel = IL.DefineLabel(); - IL.LoadAddr(fieldLocal!); - IL.Call(spanLengthMethod); - IL.GotoIfFalse(elseLabel); - IL.LoadAddr(fieldLocal!); - if (info.IsFixed) IL.LoadConst(0); - else IL.Load(indexLocal!); - IL.Call(spanItemMethod); - if (isValueType) { - IL.LoadObj(info.FieldType); - IL.New(info.ParameterType); - } - IL.Goto(doneLabel); - IL.MarkLabel(elseLabel); - if (isValueType) IL.LoadObj(tempLocal!); - else IL.LoadNull(); - IL.MarkLabel(doneLabel); - } - break; - } - } - IL.Call(Method); - - IL.Increment(indexLocal); - }); - - IL.Return(); - - Terms = paramData.Where(p => p.Term != null).Select(p => p.Term!).ToImmutableList(); - GeneratedAction = genMethod.CreateDelegate>(); - ReadableString = IL.ToReadableString(); - } - - public static IterActionGenerator GetOrBuild(World world, MethodInfo method) - =>_cache.GetValue(method, m => new IterActionGenerator(world, m)); - - public class ParamInfo - { - public ParameterInfo Info { get; } - - public ParamKind Kind { get; } - public Type ParameterType { get; } - public Type UnderlyingType { get; } - public Type FieldType { get; } - - public Type? Source { get; } - - public bool IsRequired => (Kind < ParamKind.Nullable); - public bool IsByRef => (Kind is ParamKind.In or ParamKind.Ref); - public bool IsFixed => (Kind == ParamKind.GlobalUnique) || (Source != null); - - private ParamInfo(ParameterInfo info, ParamKind kind, - Type paramType, Type underlyingType) - { - Info = info; - Kind = kind; - ParameterType = paramType; - UnderlyingType = underlyingType; - // Reference types have a backing type of ReferenceHandle. - FieldType = underlyingType.IsValueType ? underlyingType : typeof(ReferenceHandle); - - if (UnderlyingType.Has()) Source = UnderlyingType; - if (Info.Get()?.Type is Type type) Source = type; - } - - public static ParamInfo Build(ParameterInfo info) - { - if (info.IsOptional) throw new ArgumentException($"Optional parameters are not supported\nParameter: {info}"); - if (info.ParameterType.IsArray) throw new ArgumentException($"Arrays are not supported\nParameter: {info}"); - if (info.ParameterType.IsPointer) throw new ArgumentException($"Pointers are not supported\nParameter: {info}"); - if (info.ParameterType.IsPrimitive) throw new ArgumentException($"Primitives are not supported\nParameter: {info}"); - - // Find out initial parameter kind from provided attribute. - var fromAttributes = new List(); - if (info.Has< InAttribute>()) fromAttributes.Add(ParamKind.In); - if (info.Has()) fromAttributes.Add(ParamKind.Out); - if (info.Has()) fromAttributes.Add(ParamKind.Has); - if (info.Has< OrAttribute>()) fromAttributes.Add(ParamKind.Or); - if (info.Has()) fromAttributes.Add(ParamKind.Not); - // Throw an error if multiple incompatible attributes were found. - if (fromAttributes.Count > 1) throw new ArgumentException( - "Parameter must not be marked with multiple attributes: " - + string.Join(", ", fromAttributes.Select(a => $"[{a}]")) - + $"\nParameter: {info}"); - var kind = fromAttributes.FirstOrNull() ?? ParamKind.Normal; - - // Handle unique parameters such as Universe, EntityRef, ... - var isGlobalUnique = _globalUniqueParameters.ContainsKey(info.ParameterType); - var isUnique = _uniqueParameters.ContainsKey(info.ParameterType); - if (isGlobalUnique || isUnique) { - if (kind != ParamKind.Normal) throw new ArgumentException( - $"Unique parameter {info.ParameterType.Name} does not support [{kind}]\nParameter: {info}"); - kind = isGlobalUnique ? ParamKind.GlobalUnique : ParamKind.Unique; - return new(info, kind, info.ParameterType, info.ParameterType); - } - - var isNullable = info.IsNullable(); - var isByRef = info.ParameterType.IsByRef; - - if (info.ParameterType.Has() && (kind is not (ParamKind.Has or ParamKind.Not or ParamKind.Or))) { - if (kind is not ParamKind.Normal) throw new ArgumentException($"Parameter does not support [{kind}]\nParameter: {info}"); - kind = ParamKind.Has; - } - - if (kind is ParamKind.Not or ParamKind.Has) { - if (isNullable) throw new ArgumentException($"Parameter does not support Nullable\nParameter: {info}"); - if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}"); - return new(info, kind, info.ParameterType, info.ParameterType); - } - - var underlyingType = info.ParameterType; - - if (isNullable) { - if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}"); - if (info.ParameterType.IsValueType) - underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!; - kind = ParamKind.Nullable; - } - - if (info.ParameterType.IsByRef) { - if (kind != ParamKind.Normal) throw new ArgumentException( - $"Parameter does not support [{kind}]\nParameter: {info}"); - underlyingType = info.ParameterType.GetElementType()!; - if (!underlyingType.IsValueType) throw new ArgumentException( - $"Reference types can't also be ByRef\nParameter: {info}"); - kind = info.IsIn ? ParamKind.In - : info.IsOut ? ParamKind.Out - : ParamKind.Ref; - } - - if (underlyingType.IsPrimitive) throw new ArgumentException( - $"Primitives are not supported\nParameter: {info}"); - - return new(info, kind, info.ParameterType, underlyingType); - } - } - - public enum ParamKind - { - /// - /// Not part of the resulting query's terms. - /// Same value across a single invocation of a callback. - /// For example or . - /// - GlobalUnique, - - /// - /// Not part of the resulting query's terms. - /// Unique value for each iterated entity. - /// For example . - /// - Unique, - - /// Passed by value. - Normal, - - /// - /// Struct passed with the "in" modifier, allowing direct pointer access. - /// Manually applied with . - /// Marks a component as being read from. - /// - In, - - /// - /// Struct passed with the "out" modifier, allowing direct pointer access. - /// Manually applied with . - /// Marks a component as being written to. - /// - Out, - - /// - /// Struct passed with the "ref" modifier, allowing direct pointer access. - /// Marks a component as being read from and written to. - /// - Ref, - - /// - /// Only checks for presence. - /// Manually applied with . - /// Automatically applied for types with . - /// Marks a component as not being accessed. - /// - Has, - - /// - /// Struct or class passed as . - /// - Nullable, - - /// - /// Only checks for absence. - /// Applied with . - /// - Not, - - /// - /// Matches any terms in a chain of "or" terms. - /// Applied with . - /// Implies . - /// - Or, - } -} diff --git a/src/gaemstone/Utility/Union.cs b/src/gaemstone/Utility/Union.cs new file mode 100644 index 0000000..5fe29a5 --- /dev/null +++ b/src/gaemstone/Utility/Union.cs @@ -0,0 +1,37 @@ +using System.Runtime.InteropServices; + +namespace gaemstone.Utility; + +[StructLayout(LayoutKind.Explicit)] +internal struct Union +{ + [FieldOffset(0)] public T1 Value1; + [FieldOffset(0)] public T2 Value2; +} + +[StructLayout(LayoutKind.Explicit)] +internal struct Union +{ + [FieldOffset(0)] public T1 Value1; + [FieldOffset(0)] public T2 Value2; + [FieldOffset(0)] public T3 Value3; +} + +[StructLayout(LayoutKind.Explicit)] +internal struct Union +{ + [FieldOffset(0)] public T1 Value1; + [FieldOffset(0)] public T2 Value2; + [FieldOffset(0)] public T3 Value3; + [FieldOffset(0)] public T4 Value4; +} + +[StructLayout(LayoutKind.Explicit)] +internal struct Union +{ + [FieldOffset(0)] public T1 Value1; + [FieldOffset(0)] public T2 Value2; + [FieldOffset(0)] public T3 Value3; + [FieldOffset(0)] public T4 Value4; + [FieldOffset(0)] public T5 Value5; +} diff --git a/src/gaemstone/gaemstone.csproj b/src/gaemstone/gaemstone.csproj index 6d3e4b0..88a8f14 100644 --- a/src/gaemstone/gaemstone.csproj +++ b/src/gaemstone/gaemstone.csproj @@ -6,6 +6,7 @@ true disable enable + true