diff --git a/.editorconfig b/.editorconfig index 5e91147..c9e2b95 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,8 +14,10 @@ indent_size = 4 dotnet_diagnostic.IDE0005.severity = suggestion # IDE0047: Parentheses can be removed dotnet_diagnostic.IDE0047.severity = none +# IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity = none -[src/{FastNoiseLite,flecs-cs}/**] +[src/{FastNoiseLite,ImGui.NET}/**] # Suppress compiler and analyer warnings in dependencies. dotnet_analyzer_diagnostic.severity = none dotnet_diagnostic.IDE0005.severity = none diff --git a/.gitmodules b/.gitmodules index 8a9c26f..5d071b1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "src/flecs-cs"] - path = src/flecs-cs - url = https://github.com/flecs-hub/flecs-cs [submodule "src/FastNoiseLite"] path = src/FastNoiseLite url = https://github.com/Auburn/FastNoiseLite.git +[submodule "src/gaemstone.ECS"] + path = src/gaemstone.ECS + url = https://git.mcft.net/copygirl/gaemstone.ECS.git diff --git a/gaemstone.sln b/gaemstone.sln index ff837b3..45da22d 100644 --- a/gaemstone.sln +++ b/gaemstone.sln @@ -1,48 +1,55 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{599B7E67-7F73-4301-A9C6-E8DF286A2625}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.Bloxel", "src\gaemstone.Bloxel\gaemstone.Bloxel.csproj", "{7A80D49C-6768-4803-9866-691C7AD80817}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.Client", "src\gaemstone.Client\gaemstone.Client.csproj", "{67B9B2D4-FCB7-4642-B584-A0186CAB2969}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone", "src\gaemstone\gaemstone.csproj", "{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Immersion", "src\Immersion\Immersion.csproj", "{4B9C20F6-0793-4E85-863A-2E14230A028F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7A80D49C-6768-4803-9866-691C7AD80817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A80D49C-6768-4803-9866-691C7AD80817}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7A80D49C-6768-4803-9866-691C7AD80817}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A80D49C-6768-4803-9866-691C7AD80817}.Release|Any CPU.Build.0 = Release|Any CPU - {67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Debug|Any CPU.Build.0 = Debug|Any CPU - {67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Release|Any CPU.ActiveCfg = Release|Any CPU - {67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Release|Any CPU.Build.0 = Release|Any CPU - {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.Build.0 = Release|Any CPU - {4B9C20F6-0793-4E85-863A-2E14230A028F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4B9C20F6-0793-4E85-863A-2E14230A028F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4B9C20F6-0793-4E85-863A-2E14230A028F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4B9C20F6-0793-4E85-863A-2E14230A028F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {7A80D49C-6768-4803-9866-691C7AD80817} = {599B7E67-7F73-4301-A9C6-E8DF286A2625} - {67B9B2D4-FCB7-4642-B584-A0186CAB2969} = {599B7E67-7F73-4301-A9C6-E8DF286A2625} - {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0} = {599B7E67-7F73-4301-A9C6-E8DF286A2625} - {4B9C20F6-0793-4E85-863A-2E14230A028F} = {599B7E67-7F73-4301-A9C6-E8DF286A2625} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{599B7E67-7F73-4301-A9C6-E8DF286A2625}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.Bloxel", "src\gaemstone.Bloxel\gaemstone.Bloxel.csproj", "{7A80D49C-6768-4803-9866-691C7AD80817}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.Client", "src\gaemstone.Client\gaemstone.Client.csproj", "{67B9B2D4-FCB7-4642-B584-A0186CAB2969}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone.ECS", "src\gaemstone.ECS\gaemstone.ECS.csproj", "{EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaemstone", "src\gaemstone\gaemstone.csproj", "{7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Immersion", "src\Immersion\Immersion.csproj", "{4B9C20F6-0793-4E85-863A-2E14230A028F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7A80D49C-6768-4803-9866-691C7AD80817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A80D49C-6768-4803-9866-691C7AD80817}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A80D49C-6768-4803-9866-691C7AD80817}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A80D49C-6768-4803-9866-691C7AD80817}.Release|Any CPU.Build.0 = Release|Any CPU + {67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67B9B2D4-FCB7-4642-B584-A0186CAB2969}.Release|Any CPU.Build.0 = Release|Any CPU + {EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67}.Release|Any CPU.Build.0 = Release|Any CPU + {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0}.Release|Any CPU.Build.0 = Release|Any CPU + {4B9C20F6-0793-4E85-863A-2E14230A028F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B9C20F6-0793-4E85-863A-2E14230A028F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B9C20F6-0793-4E85-863A-2E14230A028F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B9C20F6-0793-4E85-863A-2E14230A028F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {7A80D49C-6768-4803-9866-691C7AD80817} = {599B7E67-7F73-4301-A9C6-E8DF286A2625} + {67B9B2D4-FCB7-4642-B584-A0186CAB2969} = {599B7E67-7F73-4301-A9C6-E8DF286A2625} + {EB4F82C0-1BDF-4404-84FB-F0A4E1E4DA67} = {599B7E67-7F73-4301-A9C6-E8DF286A2625} + {7744A8A5-7D9A-474C-BC24-1CF0A8CB7EC0} = {599B7E67-7F73-4301-A9C6-E8DF286A2625} + {4B9C20F6-0793-4E85-863A-2E14230A028F} = {599B7E67-7F73-4301-A9C6-E8DF286A2625} + EndGlobalSection +EndGlobal diff --git a/src/Immersion/Immersion.csproj b/src/Immersion/Immersion.csproj index f96793f..732fd8b 100644 --- a/src/Immersion/Immersion.csproj +++ b/src/Immersion/Immersion.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Immersion/ObserverTest.cs b/src/Immersion/ObserverTest.cs index b2fceb0..48d1582 100644 --- a/src/Immersion/ObserverTest.cs +++ b/src/Immersion/ObserverTest.cs @@ -11,7 +11,7 @@ namespace Immersion; public class ObserverTest { [Observer] - [Expression("[in] Chunk, [none] (Mesh, *)")] + [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 d522861..3a234ea 100644 --- a/src/Immersion/Program.cs +++ b/src/Immersion/Program.cs @@ -1,12 +1,12 @@ using System; using System.Diagnostics; using System.Globalization; +using System.Numerics; using System.Threading; +using gaemstone; using gaemstone.Bloxel; using gaemstone.ECS; -using gaemstone.Flecs; using gaemstone.Utility; -using Silk.NET.Maths; using Silk.NET.Windowing; using static gaemstone.Bloxel.Components.CoreComponents; using static gaemstone.Client.Components.CameraComponents; @@ -20,9 +20,8 @@ var culture = CultureInfo.InvariantCulture; Thread.CurrentThread.CurrentCulture = culture; CultureInfo.DefaultThreadCurrentCulture = culture; -FlecsAbortException.SetupHook(); var universe = new Universe(); -var game = universe.LookupOrThrow(); +var game = universe.LookupByTypeOrThrow(); universe.Modules.Register(); universe.Modules.Register(); @@ -43,6 +42,7 @@ universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); +universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); @@ -51,6 +51,7 @@ universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); +universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); @@ -60,7 +61,7 @@ game.Set(new GameWindow(window)); universe.New("MainCamera") .Set(Camera.Default3D) - .Set((GlobalTransform) Matrix4X4.CreateTranslation(0.0F, 2.0F, 0.0F)) + .Set((GlobalTransform)Matrix4x4.CreateTranslation(0.0F, 2.0F, 0.0F)) .Set(new CameraController { MouseSensitivity = 12.0F }) .Build(); @@ -71,8 +72,8 @@ var entities = universe.New("Entities").Build(); var rnd = new Random(); for (var x = -12; x <= 12; x++) for (var z = -12; z <= 12; z++) { - var position = Matrix4X4.CreateTranslation(x * 2, 0.0F, z * 2); - var rotation = Matrix4X4.CreateRotationY(rnd.NextFloat(MathF.PI * 2)); + var position = Matrix4x4.CreateTranslation(x * 2, 0.0F, z * 2); + var rotation = Matrix4x4.CreateRotationY(rnd.NextFloat(MathF.PI * 2)); var (type, mesh) = rnd.Pick(("Heart", heartMesh), ("Sword", swordMesh)); entities.NewChild() .Set((GlobalTransform)(rotation * position)) @@ -98,7 +99,7 @@ for (var cz = -sizeH; cz < sizeH; cz++) { var pos = new ChunkPos(cx, cy - 2, cz); var storage = new ChunkStoreBlocks(); chunks.NewChild() - .Set((GlobalTransform)Matrix4X4.CreateTranslation(pos.GetOrigin())) + .Set((GlobalTransform)Matrix4x4.CreateTranslation(pos.GetOrigin())) .Set(new Chunk(pos)) .Set(storage) .Add(texture) diff --git a/src/flecs-cs b/src/flecs-cs deleted file mode 160000 index a204798..0000000 --- a/src/flecs-cs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a2047983917aa462a8c2f34d5315aea48502f4d8 diff --git a/src/gaemstone.Bloxel/BlockFacing.cs b/src/gaemstone.Bloxel/BlockFacing.cs index 3982b1b..8957106 100644 --- a/src/gaemstone.Bloxel/BlockFacing.cs +++ b/src/gaemstone.Bloxel/BlockFacing.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Immutable; -using Silk.NET.Maths; +using System.Numerics; namespace gaemstone.Bloxel; @@ -47,14 +47,14 @@ public static class BlockFacingExtensions public static BlockFacing GetOpposite(this BlockFacing self) => (BlockFacing)((int)self ^ 0b1); - public static Vector3D ToVector3(this BlockFacing self) + public static Vector3 ToVector3(this BlockFacing self) => self switch { - BlockFacing.East => Vector3D.UnitX, - BlockFacing.West => -Vector3D.UnitX, - BlockFacing.Up => Vector3D.UnitY, - BlockFacing.Down => -Vector3D.UnitY, - BlockFacing.South => Vector3D.UnitZ, - BlockFacing.North => -Vector3D.UnitZ, + BlockFacing.East => Vector3.UnitX, + BlockFacing.West => -Vector3.UnitX, + BlockFacing.Up => Vector3.UnitY, + BlockFacing.Down => -Vector3.UnitY, + BlockFacing.South => Vector3.UnitZ, + BlockFacing.North => -Vector3.UnitZ, _ => throw new ArgumentException( $"'{self}' is not a valid BlockFacing", nameof(self)) }; diff --git a/src/gaemstone.Bloxel/BlockPos.cs b/src/gaemstone.Bloxel/BlockPos.cs index cfb093e..ef42540 100644 --- a/src/gaemstone.Bloxel/BlockPos.cs +++ b/src/gaemstone.Bloxel/BlockPos.cs @@ -1,5 +1,5 @@ using System; -using Silk.NET.Maths; +using System.Numerics; namespace gaemstone.Bloxel; @@ -15,8 +15,8 @@ public readonly struct BlockPos public BlockPos(int x, int y, int z) => (X, Y, Z) = (x, y, z); public void Deconstruct(out int x, out int y, out int z) => (x, y, z) = (X, Y, Z); - public Vector3D GetOrigin() => new(X, Y, Z); - public Vector3D GetCenter() => new(X + 0.5f, Y + 0.5f, Z + 0.5f); + public Vector3 GetOrigin() => new(X, Y, Z); + public Vector3 GetCenter() => new(X + 0.5f, Y + 0.5f, Z + 0.5f); public BlockPos Add(int x, int y, int z) => new(X + x, Y + y, Z + z); @@ -70,6 +70,6 @@ public readonly struct BlockPos public static class BlockPosExtensions { - public static BlockPos ToBlockPos(this Vector3D self) + public static BlockPos ToBlockPos(this Vector3 self) => new((int)MathF.Floor(self.X), (int)MathF.Floor(self.Y), (int)MathF.Floor(self.Z)); } diff --git a/src/gaemstone.Bloxel/ChunkPos.cs b/src/gaemstone.Bloxel/ChunkPos.cs index 1234afe..5e3e6b5 100644 --- a/src/gaemstone.Bloxel/ChunkPos.cs +++ b/src/gaemstone.Bloxel/ChunkPos.cs @@ -1,5 +1,5 @@ using System; -using Silk.NET.Maths; +using System.Numerics; using static gaemstone.Bloxel.Constants; namespace gaemstone.Bloxel; @@ -16,9 +16,9 @@ public readonly struct ChunkPos public ChunkPos(int x, int y, int z) => (X, Y, Z) = (x, y, z); public void Deconstruct(out int x, out int y, out int z) => (x, y, z) = (X, Y, Z); - public Vector3D GetOrigin() => new( + public Vector3 GetOrigin() => new( X << ChunkBitShift, Y << ChunkBitShift, Z << ChunkBitShift); - public Vector3D GetCenter() => new( + public Vector3 GetCenter() => new( (X << ChunkBitShift) + ChunkLength / 2, (Y << ChunkBitShift) + ChunkLength / 2, (Z << ChunkBitShift) + ChunkLength / 2); @@ -66,7 +66,7 @@ public readonly struct ChunkPos public static class ChunkPosExtensions { - public static ChunkPos ToChunkPos(this Vector3D pos) => new( + public static ChunkPos ToChunkPos(this Vector3 pos) => new( (int)MathF.Floor(pos.X) >> ChunkBitShift, (int)MathF.Floor(pos.Y) >> ChunkBitShift, (int)MathF.Floor(pos.Z) >> ChunkBitShift); diff --git a/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs index 45350a3..be81e2d 100644 --- a/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs +++ b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs @@ -1,7 +1,7 @@ using System; +using System.Numerics; using gaemstone.Client.Systems; using gaemstone.ECS; -using Silk.NET.Maths; using static gaemstone.Bloxel.Components.CoreComponents; using static gaemstone.Client.Components.RenderingComponents; using static gaemstone.Client.Components.ResourceComponents; @@ -14,13 +14,13 @@ public class ChunkMeshGenerator { private const int StartingCapacity = 1024; - private static readonly Vector3D[][] OffsetPerFacing = { - new Vector3D[]{ new(1,1,1), new(1,0,1), new(1,0,0), new(1,1,0) }, // East (+X) - new Vector3D[]{ new(0,1,0), new(0,0,0), new(0,0,1), new(0,1,1) }, // West (-X) - new Vector3D[]{ new(1,1,0), new(0,1,0), new(0,1,1), new(1,1,1) }, // Up (+Y) - new Vector3D[]{ new(1,0,1), new(0,0,1), new(0,0,0), new(1,0,0) }, // Down (-Y) - new Vector3D[]{ new(0,1,1), new(0,0,1), new(1,0,1), new(1,1,1) }, // South (+Z) - new Vector3D[]{ new(1,1,0), new(1,0,0), new(0,0,0), new(0,1,0) } // North (-Z) + private static readonly Vector3[][] OffsetPerFacing = { + new Vector3[]{ new(1,1,1), new(1,0,1), new(1,0,0), new(1,1,0) }, // East (+X) + new Vector3[]{ new(0,1,0), new(0,0,0), new(0,0,1), new(0,1,1) }, // West (-X) + 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) }; private static readonly int[] TriangleIndices @@ -28,9 +28,9 @@ public class ChunkMeshGenerator private ushort[] _indices = new ushort[StartingCapacity]; - private Vector3D[] _vertices = new Vector3D[StartingCapacity]; - private Vector3D[] _normals = new Vector3D[StartingCapacity]; - private Vector2D[] _uvs = new Vector2D[StartingCapacity]; + private Vector3[] _vertices = new Vector3[StartingCapacity]; + private Vector3[] _normals = new Vector3[StartingCapacity]; + private Vector2[] _uvs = new Vector2[StartingCapacity]; [System] [Expression("[in] Chunk, ChunkStoreBlocks, HasBasicWorldGeneration, !(Mesh, *)")] @@ -48,8 +48,8 @@ public class ChunkMeshGenerator // TODO: We'll need a way to get neighbors again. // var storages = new ChunkStoreBlocks[3, 3, 3]; // foreach (var (x, y, z) in Neighbors.ALL.Prepend(Neighbor.None)) - // if (_chunkStore.TryGetEntityID(chunkPos.Add(x, y, z), out var neighborID)) - // if (_storageStore.TryGet(neighborID, out var storage)) + // if (_chunkStore.TryGetEntityId(chunkPos.Add(x, y, z), out var neighborId)) + // if (_storageStore.TryGet(neighborId, out var storage)) // storages[x+1, y+1, z+1] = storage; // var centerStorage = storages[1, 1, 1]; @@ -61,11 +61,11 @@ public class ChunkMeshGenerator for (var x = 0; x < 16; x++) for (var y = 0; y < 16; y++) for (var z = 0; z < 16; z++) { - var block = universe.Lookup(centerBlocks[x, y, z]); + var block = universe.LookupAlive(centerBlocks[x, y, z]); if (block == null) continue; - var blockVertex = new Vector3D(x, y, z); - var textureCell = block.Get(); + 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; @@ -99,7 +99,7 @@ public class ChunkMeshGenerator } // TODO: Should dynamically generating meshes require getting GL this way? - var GL = universe.LookupOrThrow().Get().GL; + 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/Components/CoreComponents.cs b/src/gaemstone.Bloxel/Components/CoreComponents.cs index d1ea141..2fd60ef 100644 --- a/src/gaemstone.Bloxel/Components/CoreComponents.cs +++ b/src/gaemstone.Bloxel/Components/CoreComponents.cs @@ -5,14 +5,14 @@ namespace gaemstone.Bloxel.Components; [Module] public partial class CoreComponents { - [Component] + [Symbol, Component] public readonly struct Chunk { public ChunkPos Position { get; } public Chunk(ChunkPos pos) => Position = pos; } - [Component] + [Symbol, Component] public class ChunkStoreBlocks : ChunkPaletteStorage { diff --git a/src/gaemstone.Bloxel/Neighbor.cs b/src/gaemstone.Bloxel/Neighbor.cs index c16b084..a71bc4c 100644 --- a/src/gaemstone.Bloxel/Neighbor.cs +++ b/src/gaemstone.Bloxel/Neighbor.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Immutable; +using System.Numerics; using System.Text; -using Silk.NET.Maths; namespace gaemstone.Bloxel; @@ -174,7 +174,7 @@ public static class NeighborExtensions public static BlockPos ToProperPos(this Neighbor self) { var (x, y, z) = self; return new(x, y, z); } - public static Vector3D ToVector3(this Neighbor self) + public static Vector3 ToVector3(this Neighbor self) { var (x, y, z) = self; return new(x, y, z); } diff --git a/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs index 25bad8d..b3ea9d0 100644 --- a/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs +++ b/src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs @@ -1,5 +1,6 @@ using System; using gaemstone.ECS; +using gaemstone.Utility; using static gaemstone.Bloxel.Components.CoreComponents; using static gaemstone.Bloxel.Constants; @@ -10,6 +11,7 @@ namespace gaemstone.Bloxel.Systems; public class BasicWorldGenerator { private readonly FastNoiseLite _noise; + private readonly Random _rnd = new(); public BasicWorldGenerator() { @@ -20,24 +22,26 @@ public class BasicWorldGenerator _noise.SetFractalGain(0.6f); } - [Tag] + [Symbol, Tag] public struct HasBasicWorldGeneration { } [System] - public void Populate(Universe universe, EntityRef entity, + public void Populate(World world, EntityRef entity, in Chunk chunk, ChunkStoreBlocks blocks, [Not] HasBasicWorldGeneration _) { - var stone = universe.LookupOrThrow("Stone"); - for (var lx = 0; lx < ChunkLength; lx++) - for (var ly = 0; ly < ChunkLength; ly++) - for (var lz = 0; lz < ChunkLength; lz++) { - var gx = chunk.Position.X << ChunkBitShift | lx; - var gy = chunk.Position.Y << ChunkBitShift | ly; - var gz = chunk.Position.Z << ChunkBitShift | lz; - var bias = Math.Clamp(gy / 32.0f + 1.0f, 0.0f, 1.0f); - if (_noise.GetNoise(gx, gy, gz) > bias) - blocks[lx, ly, lz] = stone; + var stone = world.LookupByPathOrThrow("Stone"); + var dirt = world.LookupByPathOrThrow("Dirt"); + var grass = world.LookupByPathOrThrow("Grass"); + for (var localX = 0; localX < ChunkLength; localX++) + for (var localY = 0; localY < ChunkLength; localY++) + for (var localZ = 0; localZ < ChunkLength; localZ++) { + var globalX = chunk.Position.X << ChunkBitShift | localX; + var globalY = chunk.Position.Y << ChunkBitShift | localY; + var globalZ = chunk.Position.Z << ChunkBitShift | localZ; + var bias = Math.Clamp((globalY / 32.0f + 1.0f), 0.0f, 1.0f); + if (_noise.GetNoise(globalX, globalY, globalZ) > bias) + blocks[localX, localY, localZ] = _rnd.Pick(stone, dirt, grass); } entity.Add(); } diff --git a/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs b/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs index 103c455..8e61c50 100644 --- a/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs +++ b/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs @@ -72,7 +72,7 @@ public class ChunkedOctree while (enumerator.MoveNext()) yield return enumerator.Current; } - public sealed class Enumerator + public class Enumerator : IEnumerator<(ChunkPos ChunkPos, T Value, float Weight)> { private readonly ChunkedOctree _octree; diff --git a/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj b/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj index 6f48946..36abede 100644 --- a/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj +++ b/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj @@ -14,6 +14,11 @@ + + + + + diff --git a/src/gaemstone.Client/Color.cs b/src/gaemstone.Client/Color.cs index 721c857..4b96c27 100644 --- a/src/gaemstone.Client/Color.cs +++ b/src/gaemstone.Client/Color.cs @@ -1,8 +1,9 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; +using System.Globalization; +using System.Numerics; using System.Runtime.InteropServices; -using Silk.NET.Maths; +using System.Text.RegularExpressions; namespace gaemstone.Client; @@ -11,8 +12,8 @@ public readonly struct Color : IEquatable { public static readonly Color Transparent = default; - public static readonly Color Black = FromRGB(0x000000); - public static readonly Color White = FromRGB(0xFFFFFF); + public static readonly Color Black = FromRGB(0.0f, 0.0f, 0.0f); + public static readonly Color White = FromRGB(1.0f, 1.0f, 1.0f); [FieldOffset(0)] public readonly uint RGBA; @@ -26,19 +27,62 @@ public readonly struct Color [FieldOffset(3)] public readonly byte A; - private Color(uint rgba) - { Unsafe.SkipInit(out this); RGBA = rgba; } + private Color(uint rgba) => RGBA = rgba; private Color(byte r, byte g, byte b, byte a) - { Unsafe.SkipInit(out this); R = r; G = g; B = b; A = a; } + { R = r; G = g; B = b; A = a; } - public static Color FromRGBA(uint rgba) => new(rgba); - public static Color FromRGBA(byte r, byte g, byte b, byte a) => new(r, g, b, a); + public static Color FromRGBA(uint value) => new((byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)value); + public static Color FromRGBA(float r, float g, float b, float a = 1.0f) => new(F2B(r), F2B(g), F2B(b), F2B(a)); + public static Color FromRGBA(Vector4 vec) => FromRGBA(vec.X, vec.Y, vec.Z, vec.W); - public static Color FromRGB(uint rgb) => new(rgb | 0xFF000000); - public static Color FromRGB(byte r, byte g, byte b) => new(r, g, b, 0xFF); + public static Color FromRGB(uint value) => new((byte)(value >> 16), (byte)(value >> 8), (byte)value, 0xFF); + public static Color FromRGB(float r, float g, float b) => new(F2B(r), F2B(g), F2B(b), 0xFF); + public static Color FromRGB(Vector3 vec) => FromRGB(vec.X, vec.Y, vec.Z); - public static Color FromGrayscale(byte gray) => new(gray, gray, gray, 0xFF); - public static Color FromGrayscale(byte gray, byte alpha) => new(gray, gray, gray, alpha); + public static Color FromGrayscale(float value, float alpha = 1.0f) => FromRGBA(value, value, value, alpha); + + public static Color FromHSV(float hue, float saturation, float value, float alpha = 1.0f) + { + if (saturation <= 0.0f) return FromGrayscale(value, alpha); + + var h = (((hue % 360f) + 360f) % 360f) / 60f; + var s = Math.Clamp(saturation, 0.0f, 1.0f); + var v = Math.Clamp(value , 0.0f, 1.0f); + var f = h % 1f; + var p = v * (1.0f - s); + var q = v * (1.0f - s * f); + var t = v * (1.0f - s * (1.0f - f)); + + return (int)h switch { + 0 => FromRGBA(v, t, p, alpha), + 1 => FromRGBA(q, v, p, alpha), + 2 => FromRGBA(p, v, t, alpha), + 3 => FromRGBA(p, q, v, alpha), + 4 => FromRGBA(t, p, v, alpha), + 5 => FromRGBA(v, p, q, alpha), + _ => throw new InvalidOperationException() + }; + } + + private static readonly Regex _hexRegex = new("^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"); + public static Color? TryParseHex(string? str) + { + if (str == null) return null; + var match = _hexRegex.Match(str); + if (!match.Success) return null; + var hex = match.Value.AsSpan()[1..]; + var value = uint.Parse(hex, NumberStyles.HexNumber); + return (hex.Length == 8) ? FromRGBA(value) : FromRGB(value); + } + + public Color WithAlpha(float alpha) + => new(R, G, B, F2B(alpha)); + + public static Color Mix(Color a, Color b, float ratio) + => FromRGBA((B2F(a.R) * (1 - ratio) + B2F(b.R) * ratio) / 2, + (B2F(a.G) * (1 - ratio) + B2F(b.G) * ratio) / 2, + (B2F(a.B) * (1 - ratio) + B2F(b.B) * ratio) / 2, + (B2F(a.A) * (1 - ratio) + B2F(b.A) * ratio) / 2); public bool Equals(Color other) => RGBA == other.RGBA; @@ -46,13 +90,23 @@ public readonly struct Color => (obj is Color color) && Equals(color); public override int GetHashCode() => RGBA.GetHashCode(); - public override string? ToString() + public override string ToString() => $"Color(0x{RGBA:X8})"; + public Vector4 ToVector4() => new(B2F(R), B2F(G), B2F(B), B2F(A)); + public Vector3 ToVector3() => new(B2F(R), B2F(G), B2F(B)); + public string ToHexString() => (A < byte.MaxValue) ? $"#{RGBA:X8}" : $"#{R:X2}{G:X2}{B:X2}"; + public static bool operator ==(Color left, Color right) => left.Equals(right); public static bool operator !=(Color left, Color right) => !left.Equals(right); - public static implicit operator System.Drawing.Color(Color color) => System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B); - public static implicit operator Vector4D(Color color) => new(color.R / 255F, color.G / 255F, color.B / 255F, color.A / 255F); - public static implicit operator Vector4D(Color color) => new(color.R, color.G, color.B, color.A); + public static implicit operator System.Drawing.Color(Color color) + => System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B); + + + /// Converts a float clamped to range [0.0, 1.0] into a byte in range [0, 255]. + private static byte F2B(float f) => (byte)(Math.Clamp(f, 0.0f, 1.0f) * 255); + + /// Converts a byte in range [0, 255] into a float in range [0.0, 1.0]. + private static float B2F(byte b) => b / 255f; } diff --git a/src/gaemstone.Client/Components/CameraComponents.cs b/src/gaemstone.Client/Components/CameraComponents.cs index ba553db..0518c51 100644 --- a/src/gaemstone.Client/Components/CameraComponents.cs +++ b/src/gaemstone.Client/Components/CameraComponents.cs @@ -1,5 +1,5 @@ +using System.Drawing; using gaemstone.ECS; -using Silk.NET.Maths; namespace gaemstone.Client.Components; @@ -27,7 +27,7 @@ public class CameraComponents [Component] public struct CameraViewport { - public Vector4D ClearColor { get; set; } - public Rectangle Viewport { get; set; } + public Color ClearColor { get; set; } + public Rectangle Viewport { get; set; } } } diff --git a/src/gaemstone.Client/Components/InputComponents.cs b/src/gaemstone.Client/Components/InputComponents.cs index 72261a5..d0b137f 100644 --- a/src/gaemstone.Client/Components/InputComponents.cs +++ b/src/gaemstone.Client/Components/InputComponents.cs @@ -7,30 +7,30 @@ namespace gaemstone.Client.Components; [Module] public class InputComponents { - [Entity(Global = true)] + [Symbol, Path("/Input")] [Add] public struct Input { } - [Entity("Input", "Mouse", Global = true)] + [Symbol, Path("/Input/Mouse")] [Add] public struct Mouse { } - [Entity("Input", "Keyboard", Global = true)] + [Symbol, Path("/Input/Keyboard")] [Add] public struct Keyboard { } - [Tag] + [Symbol, Tag] public struct Gamepad { } /// Present on inputs / actions that are currently active. - [Component] public struct Active { public TimeSpan Duration; } + [Symbol, Component] public struct Active { public TimeSpan Duration; } /// Present on inputs / actions were activated this frame. - [Tag] public struct Activated { } + [Symbol, Tag] public struct Activated { } /// Present on inputs / actions were deactivated this frame. - [Tag] public struct Deactivated { } + [Symbol, Tag] public struct Deactivated { } /// @@ -41,7 +41,7 @@ public class InputComponents /// This is set if a UI element is focused that captures /// navigational or text input. /// - [Tag, Relation, Exclusive] + [Symbol, Relation, Tag, Exclusive] public struct InputCapturedBy { } /// @@ -52,7 +52,7 @@ public class InputComponents /// This could for example include the mouse currently being over /// a UI element, preventing the game from handling mouse input. /// - [Tag, Relation, Exclusive] + [Symbol, Relation, Tag, Exclusive] public struct MouseInputCapturedBy { } /// @@ -62,7 +62,7 @@ public class InputComponents /// /// This is set when a camera controller assumes control of the mouse. /// - [Tag, Relation, Exclusive] + [Symbol, Relation, Tag, Exclusive] [With] [With] public struct CursorCapturedBy { } diff --git a/src/gaemstone.Client/Components/RenderingComponents.cs b/src/gaemstone.Client/Components/RenderingComponents.cs index 1ab7603..bd605e1 100644 --- a/src/gaemstone.Client/Components/RenderingComponents.cs +++ b/src/gaemstone.Client/Components/RenderingComponents.cs @@ -1,6 +1,6 @@ using System.Drawing; +using System.Numerics; using gaemstone.ECS; -using Silk.NET.Maths; using Silk.NET.OpenGL; namespace gaemstone.Client.Components; @@ -8,7 +8,7 @@ namespace gaemstone.Client.Components; [Module] public class RenderingComponents { - [Component] + [Symbol, Component] public readonly struct MeshHandle { public uint Handle { get; } @@ -19,7 +19,7 @@ public class RenderingComponents { Handle = handle; Count = count; IsIndexed = indexed; } } - [Component] + [Symbol, Component] public readonly struct TextureHandle { public TextureTarget Target { get; } @@ -29,13 +29,13 @@ public class RenderingComponents => (Target, Handle) = (target, handle); } - [Component] + [Symbol, Component] public readonly struct TextureCoords4 { - public Vector2D TopLeft { get; } - public Vector2D TopRight { get; } - public Vector2D BottomLeft { get; } - public Vector2D BottomRight { get; } + public Vector2 TopLeft { get; } + public Vector2 TopRight { get; } + public Vector2 BottomLeft { get; } + public Vector2 BottomRight { get; } public TextureCoords4(float x1, float y1, float x2, float y2) { diff --git a/src/gaemstone.Client/Components/ResourceComponents.cs b/src/gaemstone.Client/Components/ResourceComponents.cs index dcb566a..148a3e7 100644 --- a/src/gaemstone.Client/Components/ResourceComponents.cs +++ b/src/gaemstone.Client/Components/ResourceComponents.cs @@ -5,7 +5,7 @@ namespace gaemstone.Client.Components; [Module] public class ResourceComponents { - [Tag] + [Symbol, Tag] public struct Resource { } // Entities can have for example Texture as a tag, in which case @@ -14,9 +14,9 @@ public class ResourceComponents // Entities can also have a (Texture, $T) pair where $T is a resource, // meaning the entity has that resource assigned as their texture. - [Relation, Tag, IsA] + [Symbol, Relation, Tag, IsA] public struct Texture { } - [Relation, Tag, IsA] + [Symbol, Relation, Tag, IsA] public struct Mesh { } } diff --git a/src/gaemstone.Client/Resources/ForkAwesome.ttf b/src/gaemstone.Client/Resources/ForkAwesome.ttf new file mode 100644 index 0000000..1f1d8f3 Binary files /dev/null and b/src/gaemstone.Client/Resources/ForkAwesome.ttf differ diff --git a/src/gaemstone.Client/Resources/LICENSE_ForkAwesome.txt b/src/gaemstone.Client/Resources/LICENSE_ForkAwesome.txt new file mode 100644 index 0000000..ba28031 --- /dev/null +++ b/src/gaemstone.Client/Resources/LICENSE_ForkAwesome.txt @@ -0,0 +1,95 @@ +Copyright (c) 2018, Fork Awesome (https://forkawesome.github.io), +with Reserved Font Name Fork Awesome. + + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/gaemstone.Client/Resources/LICENSE_OpenSans.txt b/src/gaemstone.Client/Resources/LICENSE_OpenSans.txt new file mode 100644 index 0000000..9b448d4 --- /dev/null +++ b/src/gaemstone.Client/Resources/LICENSE_OpenSans.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/gaemstone.Client/Resources/OpenSans.Bold.ttf b/src/gaemstone.Client/Resources/OpenSans.Bold.ttf new file mode 100644 index 0000000..a1398b3 Binary files /dev/null and b/src/gaemstone.Client/Resources/OpenSans.Bold.ttf differ diff --git a/src/gaemstone.Client/Resources/OpenSans.Italic.ttf b/src/gaemstone.Client/Resources/OpenSans.Italic.ttf new file mode 100644 index 0000000..a105616 Binary files /dev/null and b/src/gaemstone.Client/Resources/OpenSans.Italic.ttf differ diff --git a/src/gaemstone.Client/Resources/OpenSans.ttf b/src/gaemstone.Client/Resources/OpenSans.ttf new file mode 100644 index 0000000..1dc226d Binary files /dev/null and b/src/gaemstone.Client/Resources/OpenSans.ttf differ diff --git a/src/gaemstone.Client/Systems/EntityInspector.cs b/src/gaemstone.Client/Systems/EntityInspector.cs new file mode 100644 index 0000000..cb0e0a2 --- /dev/null +++ b/src/gaemstone.Client/Systems/EntityInspector.cs @@ -0,0 +1,763 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using gaemstone.Client.Utility; +using gaemstone.ECS; +using gaemstone.Flecs; +using gaemstone.Utility; +using ImGuiNET; +using static gaemstone.Client.Systems.ImGuiManager; +using Icon = gaemstone.Client.Utility.ForkAwesome; +using ImGuiInternal = ImGuiNET.Internal.ImGui; + +namespace gaemstone.Client.Systems; + +[Module] +[DependsOn] +public class EntityInspector + : IModuleInitializer +{ + [Tag] + public struct InspectorWindow { } + + [Relation, Exclusive] + [Add] + public struct Selected { } + + [Tag] + public struct ScrollToSelected { } + + [Relation] + public struct Expanded { } + + [Component] + public class History + { + public Entry? Current { get; set; } = null; + + public class Entry + { + public Entity Entity { get; } + public EntityPath? Path { get; } + + public Entry? Prev { get; set; } + public Entry? Next { get; set; } + + public Entry(EntityRef entity, Entry? prev, Entry? next) + { + Entity = entity; + Path = entity.GetFullPath(); + if ((Prev = prev) != null) Prev.Next = this; + if ((Next = next) != null) Next.Prev = this; + } + } + } + + + [Component] + public struct DocPriority { public float Value; } + + [Component] + public struct DocIcon { public char Value; } + + + public void Initialize(EntityRef module) + { + void SetDocInfo(string path, float priority, string icon, float r, float g, float b) + => module.World.LookupByPathOrThrow(path) + .Add() + .Set(new DocPriority { Value = priority }) + .Set(new DocIcon { Value = icon[0] }) + .SetDocColor(Color.FromRGB(r, g, b).ToHexString()); + + SetDocInfo("/flecs/core/Module" , 0 , Icon.Archive , 1.0f, 0.9f, 0.7f); + SetDocInfo("/flecs/system/System" , 1 , Icon.Cog , 1.0f, 0.7f, 0.7f); + 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/Tag" , 4 , Icon.Tag , 0.7f, 0.8f, 1.0f); + SetDocInfo("/flecs/core/Component" , 5 , Icon.PencilSquare , 0.6f, 0.6f, 1.0f); + SetDocInfo("/flecs/core/Prefab" , 6 , Icon.Cube , 0.9f, 0.8f, 1.0f); + } + + + [System] + public void ShowUIButton(World world, ImGuiData _) + { + var hasAnyInspector = false; + var inspectorWindow = world.LookupByTypeOrThrow(); + foreach (var entity in Iterator.FromTerm(world, new(inspectorWindow))) + { hasAnyInspector = true; break; } + + if (ImGuiUtility.UIButton(0, Icon.Search, "Entity Inspector", hasAnyInspector)) + NewEntityInspectorWindow(world); + } + + [System] + public void ShowExplorerWindow(EntityRef window, InspectorWindow _, History? history) + { + var isOpen = true; + var fontSize = ImGui.GetFontSize(); + var viewCenter = ImGui.GetMainViewport().GetCenter(); + ImGui.SetNextWindowPos(viewCenter, ImGuiCond.Appearing, new(0.5f, 0.5f)); + ImGui.SetNextWindowSize(new(fontSize * 40, fontSize * 25), ImGuiCond.Appearing); + ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]); + if (ImGui.Begin($"{Icon.Search} Entity Inspector##{window.Id}", + ref isOpen, ImGuiWindowFlags.NoScrollbar)) { + ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[0]); + + var selected = window.GetTargets().FirstOrDefault(); + ActionBarAndPath(window, history, selected); + + ImGui.BeginTable("Views", 2, ImGuiTableFlags.Resizable); + ImGui.TableSetupColumn("Explorer", ImGuiTableColumnFlags.WidthFixed, fontSize * 12); + ImGui.TableSetupColumn("Entity", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + ImGui.BeginChild("ExplorerView", new(-float.Epsilon, -float.Epsilon)); + ExplorerView(window, history, selected); + ImGui.EndChild(); + + void Tab(string name, Action contentMethod) + { + if (!ImGui.BeginTabItem(name)) return; + ImGui.BeginChild($"{name}Tab", new(-float.Epsilon, -float.Epsilon)); + contentMethod(window, history, selected); + ImGui.EndChild(); + ImGui.EndTabItem(); + } + + ImGui.TableNextColumn(); + ImGui.BeginChild("EntityView", new(-float.Epsilon, -float.Epsilon)); + if (!ImGui.BeginTabBar("Tabs")) return; + Tab($"{Icon.PencilSquare} Components", ComponentsTab); + Tab($"{Icon.ShareAlt} References", ReferencesTab); + Tab($"{Icon.InfoCircle} Documentation", DocumentationTab); + ImGui.EndTabBar(); + ImGui.EndChild(); + + ImGui.EndTable(); + + ImGui.PopFont(); + } + ImGui.PopFont(); + ImGui.End(); + + // If window is closed, delete the entity. + if (!isOpen) window.Delete(); + } + + [Observer] + public void ClearStorageOnRemove(EntityRef _1, InspectorWindow _2) + { + // TODO: Clear out settings store for the window. + } + + private void ActionBarAndPath(EntityRef window, History? history, EntityRef? selected) + { + var world = window.World; + + static bool IconButtonWithToolTip(string icon, string tooltip, bool enabled = true) { + if (!enabled) ImGui.BeginDisabled(); + var clicked = ImGui.Button(icon); + if (!enabled) ImGui.EndDisabled(); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip(tooltip); + return clicked; + } + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, ImGui.GetStyle().ItemSpacing.Y)); + ImGui.BeginTable("ActionBar", 3); + ImGui.TableSetupColumn("Explorer", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Entity", ImGuiTableColumnFlags.WidthFixed); + + ImGui.TableNextColumn(); + var hasExpanded = window.Has(); + if (IconButtonWithToolTip(Icon.Outdent, "Collapse all items in the Explorer View", hasExpanded)) + window.Remove(); + + if (history != null) { + var hasPrev = ((selected != null) ? history.Current?.Prev : history.Current) != null; + var hasNext = history.Current?.Next != null; + ImGui.SameLine(); + if (IconButtonWithToolTip(Icon.ArrowLeft, "Go to the previously viewed entity", hasPrev)) + GoToPrevious(window, history, selected); + ImGui.SameLine(); + if (IconButtonWithToolTip(Icon.ArrowRight, "Go to the next viewed entity", hasNext)) + GoToNext(window, history); + } + + ImGui.SameLine(); + if (IconButtonWithToolTip(Icon.Crosshairs, "Scroll to the current entity in the Explorer View", (selected != null))) + window.Add(); + + ImGui.TableNextColumn(); + var availableWidth = ImGui.GetColumnWidth() - ImGui.GetStyle().CellPadding.X * 2; + PathInput(window, history, selected, availableWidth); + + 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); + + ImGui.SameLine(); + if (IconButtonWithToolTip(Icon.Pencil, "Rename the current entity", false && (selected != null))) + { } // TODO: Implement this! + + ImGui.SameLine(); + var isDisabled = (selected?.IsDisabled == true); + 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(); } + + ImGui.SameLine(); + if (IconButtonWithToolTip(Icon.Trash, "Delete the current entity", (selected != null))) { + 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) + { + var style = ImGui.GetStyle(); + ImGui.AlignTextToFramePadding(); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, style.ItemSpacing.Y)); + + var path = selected?.GetFullPath() ?? null; + + if (path != null) { + var visiblePath = path.GetParts().ToList(); + var visiblePathTextSize = ImGui.CalcTextSize(path.ToString()).X + + style.ItemSpacing.X * 2 * (path.Count - 0.5f) + + style.FramePadding.X * 2 * path.Count; + while ((visiblePath.Count > 3) && (visiblePathTextSize > availableWidth)) { + if (visiblePath[1] != "...") { + visiblePathTextSize -= ImGui.CalcTextSize(visiblePath[1]).X - ImGui.CalcTextSize("...").X; + visiblePath[1] = "..."; + } else { + visiblePathTextSize -= ImGui.CalcTextSize(visiblePath[2] + "/").X + + (style.ItemSpacing.X + style.FramePadding.X) * 2; + visiblePath.RemoveAt(2); + } + } + + var numHiddenItems = path.Count - visiblePath.Count; + for (var i = 0; i < visiblePath.Count - 1; i++) { + var actualIndex = (i == 0) ? 0 : i + numHiddenItems; + ImGui.Text("/"); + ImGui.SameLine(); + if (visiblePath[i] == "...") { + ImGui.BeginDisabled(); + ImGui.Button("..."); + 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)])); + ImGui.SameLine(); + } + } + + ImGui.Text("/"); ImGui.SameLine(); + var name = path?.Name.ToString() ?? ""; + ImGui.SetNextItemWidth(-float.Epsilon); + ImGui.InputText("##Path", ref name, 256); + + ImGui.PopStyleVar(); + } + + private struct EntitySummary + : IComparable + { + public Entity Entity { get; init; } + public SpecialType? Type { get; init; } + public string? Name { get; init; } + public string? DocName { get; init; } + public Color? DocColor { get; init; } + public bool HasChildren { get; init; } + public bool IsExpanded { get; init; } + public bool IsDisabled { get; init; } + + public int CompareTo(EntitySummary other) + { + static int? Compare(T x, T 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); + return (result != 0) ? result : null; + } + + return Compare(Type, other.Type) + ?? Compare(Name, other.Name) + ?? Compare(DocName, other.DocName) + ?? Compare(Entity.Id, other.Entity.Id) + ?? 0; + } + + public string DisplayName { get { + var name = (DocName != null) ? $"\"{DocName}\"" : Name ?? Entity.Id.ToString(); + if (Type != null) name = $"{DisplayIcon} {name}"; + return name; + } } + + public string? DisplayIcon => Type switch { + SpecialType.Module => Icon.Archive, + SpecialType.System => Icon.Cog, + SpecialType.Relation => Icon.ShareAlt, + SpecialType.Component => Icon.PencilSquare, + SpecialType.Tag => Icon.Tag, + SpecialType.Prefab => Icon.Cube, + _ => null, + }; + + public Color? DisplayColor => DocColor ?? Type switch { + SpecialType.Module => Color.FromRGB(1.0f, 0.9f, 0.7f), + SpecialType.System => Color.FromRGB(1.0f, 0.7f, 0.7f), + SpecialType.Relation => Color.FromRGB(0.7f, 1.0f, 0.8f), + SpecialType.Component => Color.FromRGB(0.6f, 0.6f, 1.0f), + SpecialType.Tag => Color.FromRGB(0.7f, 0.8f, 1.0f), + SpecialType.Prefab => Color.FromRGB(0.9f, 0.8f, 1.0f), + _ => null, + }; + } + + public enum SpecialType + { + Module, + System, + Relation, + Component, + Tag, + Prefab, + } + + private const int MAX_CHILDREN = 64; + private void ExplorerView(EntityRef window, History? history, EntityRef? selected) + { + var Expanded = window.World.LookupByTypeOrThrow(); + + List GetSummaries(Entity? parent) { + var result = new List(); + var expression = $"(ChildOf, {parent?.Id ?? 0})" // Must be child of parent, or root entity. + + ",?(Identifier, Name)" // Name (in hierarchy) + + ",?(flecs.doc.Description, Name)" // DocName (human-readable) + + ",?(flecs.doc.Description, flecs.doc.Color)" // DocColor + + ",[none] ?ChildOf(_, $This)" // HasChildren + + $",?{Expanded.Id}({window.Id}, $This)" // IsExpanded + + ",?Disabled" // IsDisabled + + ",?Module,?flecs.system.System,?gaemstone.Doc.Relation,?Component,?Tag,?Prefab"; // Type + + using (var rule = new Rule(window.World, new(expression))) { + foreach (var iter in rule.Iter()) { + var names = iter.FieldOrEmpty(2); + var docNames = iter.FieldOrEmpty(3); + var docColors = iter.FieldOrEmpty(4); + + var hasChildren = iter.FieldIsSet(5); + var isExpanded = iter.FieldIsSet(6); + var isDisabled = iter.FieldIsSet(7); + + var isModule = iter.FieldIsSet(8); + var isSystem = iter.FieldIsSet(9); + var isRelation = iter.FieldIsSet(10); + var components = iter.FieldOrEmpty(11); + var isTag = iter.FieldIsSet(12); + var isPrefab = iter.FieldIsSet(13); + + for (var i = 0; i < iter.Count; i++) { + // Certain built-in components in Flecs actually have a size of 0, + // thus don't actually hold any value and behave more like tags. + // We pretend they are just tags and mark them as such. + var component = components.GetOrNull(i); + var isComponent = (component?.Size > 0); + var isTagEquiv = (component?.Size == 0) || isTag; + + var type = isModule ? SpecialType.Module + : isSystem ? SpecialType.System + : isRelation ? SpecialType.Relation + : isComponent ? SpecialType.Component + : isTagEquiv ? SpecialType.Tag + : isPrefab ? SpecialType.Prefab + : (SpecialType?)null; + result.Add(new() { + Entity = iter.Entity(i), + Type = type, + Name = names.GetOrNull(i)?.ToString(), + DocName = docNames.GetOrNull(i)?.ToString(), + DocColor = Color.TryParseHex(docColors.GetOrNull(i)?.ToString()), + HasChildren = hasChildren, + IsExpanded = isExpanded, + IsDisabled = isDisabled, + }); + if (result.Count > MAX_CHILDREN) + return result; + } + } + } + + result.Sort(); + return result; + } + + void EntryNode(EntitySummary summary) { + var entity = new EntityRef(window.World, summary.Entity); + var isExpanded = summary.IsExpanded; + var isSelected = (selected == entity); + + var flags = ImGuiTreeNodeFlags.OpenOnArrow + | ImGuiTreeNodeFlags.OpenOnDoubleClick + | ImGuiTreeNodeFlags.SpanAvailWidth; + if (!summary.HasChildren) flags |= ImGuiTreeNodeFlags.Leaf + | ImGuiTreeNodeFlags.Bullet + | ImGuiTreeNodeFlags.NoTreePushOnOpen; + if (isSelected) flags |= ImGuiTreeNodeFlags.Selected; + + var hasColor = false; + if (summary.DisplayColor is Color color) { ImGui.PushStyleColor(ImGuiCol.Text, color.RGBA); hasColor = true; } + if (summary.DocName != null) ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[2]); + if (summary.IsDisabled) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().DisabledAlpha); + ImGui.SetNextItemOpen(isExpanded); + ImGui.TreeNodeEx(summary.DisplayName, flags); + if (summary.IsDisabled) ImGui.PopStyleVar(); + if (summary.DocName != null) ImGui.PopFont(); + if (hasColor) ImGui.PopStyleColor(); + + // When hovering over the node, display brief description (if available). + if (ImGui.IsItemHovered() && entity.GetDocBrief() is string brief) + ImGui.SetTooltip(brief); + + // When node is clicked (but not on the arrow), select this entity. + if (ImGui.IsItemClicked() && !ImGui.IsItemToggledOpen()) + SetSelected(window, history, entity, scrollTo: false); + + // When node is toggled, toggle (Expanded, entity) + // relation on the inspector window entity. + if (ImGui.IsItemToggledOpen()) { + if (isExpanded) { + isExpanded = false; + window.Remove(Expanded, entity); + } else if (summary.HasChildren) { + isExpanded = true; + window.Add(Expanded, entity); + } + } + + if (window.Has() && isSelected) { + ImGui.SetScrollHereY(); + window.Remove(); + } + + if (isExpanded && summary.HasChildren) { + var children = GetSummaries(entity); + if (children.Count > MAX_CHILDREN) { + ImGui.TreePush(); + ImGui.TextWrapped($"{Icon.ExclamationTriangle} Too many children. " + + "If an entity's full path is known, it can be entered in the path input."); + ImGui.TreePop(); + } else foreach (var child in children) + EntryNode(child); + ImGui.TreePop(); + } + } + + foreach (var summary in GetSummaries(Entity.None)) + EntryNode(summary); + } + + private void ComponentsTab(EntityRef window, History? history, EntityRef? selected) + { + if (selected == null) return; + var ChildOf = window.World.LookupByTypeOrThrow(); + foreach (var id in selected.Type) { + // Hide ChildOf relations, as they are visible in the explorer. + if (id.IsPair && (id.Id.RelationUnsafe == ChildOf)) continue; + RenderIdentifier(window, history, id); + } + } + + private void ReferencesTab(EntityRef window, History? history, EntityRef? selected) + { + if (selected == null) return; + var world = window.World; + var ChildOf = world.LookupByTypeOrThrow(); + var Wildcard = world.LookupByTypeOrThrow(); + + if (ImGui.CollapsingHeader($"As {Icon.Tag} Entity", 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))) { + var id = iter.FieldId(1); + if (id.AsPair() is not (EntityRef relation, EntityRef target)) throw new InvalidOperationException(); + if (relation == ChildOf) continue; // Hide ChildOf relations. + + for (var i = 0; i < iter.Count; i++) { + RenderEntity(window, history, iter.Entity(i)); + ImGui.SameLine(); ImGui.TextUnformatted("has"); ImGui.SameLine(); + RenderIdentifier(window, history, id); + } + } + + if (ImGui.CollapsingHeader($"As {Icon.Bullseye} Target", ImGuiTreeNodeFlags.DefaultOpen)) + 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 (relation == ChildOf) continue; // Hide ChildOf relations. + + for (var i = 0; i < iter.Count; i++) { + RenderEntity(window, history, iter.Entity(i)); + ImGui.SameLine(); ImGui.TextUnformatted("has"); ImGui.SameLine(); + RenderIdentifier(window, history, id); + } + } + } + + private void DocumentationTab(EntityRef _1, History? _2, EntityRef? selected) + { + var hasSelected = (selected != null); + + ImGui.BeginTable("Documentation", 2); + ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.WidthStretch); + + static void Column(string label, string? tooltip, bool fill = true) { + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(label); + if (ImGui.IsItemHovered() && (tooltip != null)) + ImGui.SetTooltip(tooltip); + ImGui.TableNextColumn(); + if (fill) ImGui.SetNextItemWidth(-float.Epsilon); + } + + Column($"{Icon.Tag} Display Name", """ + A display name for this entity. + Names in the entity hierarchy must be unique within the parent entity, + This doesn't apply to display names - they are mostly informational. + """); + if (!hasSelected) ImGui.BeginDisabled(); + var name = selected?.GetDocName(false) ?? ""; + if (ImGui.InputText("##Name", ref name, 256)) + selected!.SetDocName((name.Length > 0) ? name : null); + if (!hasSelected) ImGui.EndDisabled(); + + Column($"{Icon.Comment} Description", + "A brief description of this entity."); + if (!hasSelected) ImGui.BeginDisabled(); + var brief = selected?.GetDocBrief() ?? ""; + if (ImGui.InputText("##Brief", ref brief, 256)) + selected!.SetDocBrief((brief.Length > 0) ? brief : null); + if (!hasSelected) ImGui.EndDisabled(); + + Column($"{Icon.FileText} Documentation", """ + A detailed description, or full documentation, of this entity's purpose and behaviors. + It's encouraged to use multiple paragraphs and markdown formatting if necessary. + """); + var cellPadding = ImGui.GetStyle().CellPadding.Y; + var minHeight = ImGui.GetTextLineHeightWithSpacing() * 4; + var availHeight = ImGui.GetContentRegionAvail().Y + - (ImGui.GetFrameHeight() + cellPadding * 2) * 2 - cellPadding; + if (!hasSelected) ImGui.BeginDisabled(); + var detail = selected?.GetDocDetail() ?? ""; + // TODO: Needs wordwrap. + if (ImGui.InputTextMultiline("##Detail", ref detail, 2048, + new(-float.Epsilon, Math.Max(minHeight, availHeight)))) + selected!.SetDocDetail((detail.Length > 0) ? detail : null); + if (!hasSelected) ImGui.EndDisabled(); + + Column($"{Icon.Link} Link", """ + A link to a website relating to this entity, such as + a module's repository, or further documentation. + """); + if (!hasSelected) ImGui.BeginDisabled(); + var link = selected?.GetDocLink() ?? ""; + if (ImGui.InputText("##Link", ref link, 256)) + selected!.SetDocLink((link.Length > 0) ? link : null); + if (!hasSelected) ImGui.EndDisabled(); + + Column($"{Icon.PaintBrush} Color", """ + A custom color to represent this entity. + Used in the entity inspector's explorer view. + """, false); + if (!hasSelected) ImGui.BeginDisabled(); + var maybeColor = Color.TryParseHex(selected?.GetDocColor()); + 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); + } + 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()); + if (!hasColor) ImGui.EndDisabled(); + if (!hasSelected) ImGui.EndDisabled(); + + ImGui.EndTable(); + ImGui.EndTabItem(); + } + + + // ======================= + // == Utility Functions == + // ======================= + + private EntityRef NewEntityInspectorWindow(World world) + => world.New().Add().Set(new History()) + .Build().SetDocName("Entity Inspector"); + + private void SetSelected( + EntityRef 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. + bool scrollTo = true) // Should entity be scrolled to in the explorer view? + { + if (entity != null) window.Add(entity); + else window.Remove(); + + for (var parent = entity?.Parent; parent != null; parent = 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); + else if (history.Current is History.Entry entry) entry.Next = null; + } + } + + private void GoToPrevious(EntityRef window, History history, EntityRef? 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); + SetSelected(window, null, entity); + // TODO: Set path if entity could not be found. + } + + private void GoToNext(EntityRef window, History history) + { + if (history.Current?.Next == null) return; + history.Current = history.Current.Next; + + var entity = EntityRef.CreateOrNull(window.World, history.Current.Entity); + SetSelected(window, null, entity); + // TODO: Set path if entity could not be found. + } + + private Rule? _findDisplayTypeRule; + private EntityRef? FindDisplayType(EntityRef entity) + { + var world = entity.World; + var component = world.LookupByTypeOrThrow(); + + var rule = _findDisplayTypeRule ??= new Rule(world, new( + $"$Type, gaemstone.Doc.DisplayType($Type)")); + var typeVar = rule.Variables["Type"]!; + + var curType = (EntityRef?)null; + var curPriority = float.MaxValue; + foreach (var iter in _findDisplayTypeRule.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 (priority <= curPriority) { curType = type; curPriority = priority; } + } + + return curType; + } + + + // ============================= + // == Utility ImGui Functions == + // ============================= + + private void RenderIdentifier(EntityRef window, History? history, IdentifierRef id) + { + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(2, ImGui.GetStyle().ItemSpacing.Y)); + if (id.AsPair() is (EntityRef relation, EntityRef 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) + RenderEntity(window, history, entity); + else + ImGui.TextUnformatted(id.ToString()); + ImGui.PopStyleVar(); + } + + private void RenderEntity(EntityRef window, History? history, EntityRef entity) + { + var pos = ImGui.GetCursorScreenPos(); + // Adjust based on AlignTextToFramePadding() or similar. + pos.Y += ImGuiInternal.GetCurrentWindow().DC.CurrLineTextBaseOffset; + + // TODO: Calculate the size properly. + var dummySize = new Vector2(20, ImGui.GetTextLineHeight()); + if (!ImGui.IsRectVisible(pos, pos + dummySize)) { ImGui.Dummy(dummySize); return; } + + var displayType = FindDisplayType(entity); + var docColor = Color.TryParseHex(entity.GetDocColor()) ?? Color.TryParseHex(displayType?.GetDocColor()); + var docIcon = entity.GetOrNull()?.Value.ToString() ?? displayType?.GetOrNull()?.Value.ToString(); + var docName = entity.GetDocName(false); + + var isDisabled = entity.IsDisabled; + var displayName = (docName != null) ? $"\"{docName}\"" : entity.Name ?? entity.Id.ToString(); + if (docIcon is string icon) displayName = $"{icon} {displayName}"; + + var font = ImGui.GetIO().Fonts.Fonts[(docName != null) ? 2 : 0]; + ImGui.PushFont(font); var size = ImGui.CalcTextSize(displayName); ImGui.PopFont(); + var color = docColor ?? Color.FromRGBA(ImGui.GetColorU32(ImGuiCol.Text)); + if (isDisabled) color = color.WithAlpha(ImGui.GetStyle().DisabledAlpha); + + var ctrl = ImGui.IsKeyDown(ImGuiKey.ModCtrl); + var shift = ImGui.IsKeyDown(ImGuiKey.ModShift); + if (ImGui.InvisibleButton(entity.Id.ToString(), size) && (ctrl || shift)) { + if (shift) window = NewEntityInspectorWindow(window.World); + SetSelected(window, history, entity); + } + + var drawList = ImGui.GetWindowDrawList(); + drawList.AddText(font, ImGui.GetFontSize(), pos, color.RGBA, displayName); + // Draw an underscore (like a hyperlink) if hovered and Ctrl key is held. + if (ImGui.IsItemHovered() && (ctrl || shift)) { + pos.Y -= 1.75f; + drawList.AddLine(pos + new Vector2(0, size.Y), pos + size, color.RGBA); + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + } + + if (ImGui.IsItemHovered()) { + ImGui.BeginTooltip(); + ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]); + ImGui.TextUnformatted(entity.GetFullPath().ToString()); + ImGui.PopFont(); + if (isDisabled) { + ImGui.SameLine(); + ImGui.BeginDisabled(); + ImGui.TextUnformatted(" (disabled)"); + ImGui.EndDisabled(); + } + if (entity.GetDocBrief() is string brief) ImGui.Text(brief); + ImGui.EndTooltip(); + } + } +} diff --git a/src/gaemstone.Client/Systems/FreeCameraController.cs b/src/gaemstone.Client/Systems/FreeCameraController.cs index 1296e4b..e988a76 100644 --- a/src/gaemstone.Client/Systems/FreeCameraController.cs +++ b/src/gaemstone.Client/Systems/FreeCameraController.cs @@ -1,7 +1,7 @@ using System; +using System.Linq; using System.Numerics; using gaemstone.ECS; -using Silk.NET.Maths; using static gaemstone.Client.Components.CameraComponents; using static gaemstone.Client.Components.InputComponents; using static gaemstone.Components.TransformComponents; @@ -25,20 +25,20 @@ public class FreeCameraController Universe universe, TimeSpan delta, in Camera camera, ref GlobalTransform transform, ref CameraController controller) { - var input = universe.Lookup(); - var mouse = universe.Lookup(); - var keyboard = universe.Lookup(); + var input = universe.LookupByType(); + var mouse = universe.LookupByType(); + var keyboard = universe.LookupByType(); if ((input == null) || (mouse == null) || (keyboard == null)) return; - var module = universe.LookupOrThrow(); - var capturedBy = input.GetTarget(); - var inputCapturedBy = input.GetTarget(); + var module = universe.LookupByTypeOrThrow(); + var capturedBy = input.GetTargets().FirstOrDefault(); + var inputCapturedBy = input.GetTargets().FirstOrDefault(); var isCaptured = (capturedBy != null); // If another system has the mouse captured, don't do anything here. if (isCaptured && (capturedBy != module)) return; var isMouseDown = ((inputCapturedBy == null) || (inputCapturedBy == module)) - && mouse.Lookup("Buttons/Right")?.Has() == true; + && mouse.LookupChild("Buttons/Right")?.Has() == true; if (isMouseDown != isCaptured) { if (isMouseDown) input.Add(module); @@ -51,27 +51,27 @@ public class FreeCameraController var mouseMovement = Vector2.Zero; if (isCaptured) { - var raw = (Vector2?)mouse.Lookup("Delta")?.Get() ?? default; + var raw = (Vector2?)mouse.LookupChild("Delta")?.GetOrThrow() ?? default; mouseMovement = raw * controller.MouseSensitivity * (float)delta.TotalSeconds; } if (camera.IsOrthographic) { - transform *= Matrix4X4.CreateTranslation(-mouseMovement.X, -mouseMovement.Y, 0); + transform *= Matrix4x4.CreateTranslation(-mouseMovement.X, -mouseMovement.Y, 0); } else { - var shift = keyboard.Lookup("ShiftLeft")?.Has() == true; - var w = keyboard.Lookup("W")?.Has() == true; - var a = keyboard.Lookup("A")?.Has() == true; - var s = keyboard.Lookup("S")?.Has() == true; - var d = keyboard.Lookup("D")?.Has() == true; + 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 speed = (shift ? 12 : 4) * (float)delta.TotalSeconds; var forwardMovement = ((w ? -1 : 0) + (s ? 1 : 0)) * speed; var sideMovement = ((a ? -1 : 0) + (d ? 1 : 0)) * speed; - var curTranslation = new Vector3D(transform.Value.M41, transform.Value.M42, transform.Value.M43); - var yawRotation = Matrix4X4.CreateRotationY(-mouseMovement.X / 100, curTranslation); - var pitchRotation = Matrix4X4.CreateRotationX(-mouseMovement.Y / 100); - var translation = Matrix4X4.CreateTranslation(sideMovement, 0, forwardMovement); + var curTranslation = new Vector3(transform.Value.M41, transform.Value.M42, transform.Value.M43); + var yawRotation = Matrix4x4.CreateRotationY(-mouseMovement.X / 100, curTranslation); + var pitchRotation = Matrix4x4.CreateRotationX(-mouseMovement.Y / 100); + var translation = Matrix4x4.CreateTranslation(sideMovement, 0, forwardMovement); transform = translation * pitchRotation * transform * yawRotation; } diff --git a/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs b/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs new file mode 100644 index 0000000..6e8ca98 --- /dev/null +++ b/src/gaemstone.Client/Systems/ImGuiDemoWindow.cs @@ -0,0 +1,20 @@ +using gaemstone.Client.Utility; +using gaemstone.ECS; +using ImGuiNET; +using static gaemstone.Client.Systems.ImGuiManager; + +namespace gaemstone.Client.Systems; + +[Module] +[DependsOn] +public class ImGuiDemoWindow +{ + private bool _isOpen = false; + + [System] + public 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 dbb02df..a8cf9d3 100644 --- a/src/gaemstone.Client/Systems/ImGuiInputDebug.cs +++ b/src/gaemstone.Client/Systems/ImGuiInputDebug.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using gaemstone.Client.Utility; using gaemstone.ECS; using ImGuiNET; using Silk.NET.GLFW; @@ -16,88 +17,101 @@ namespace gaemstone.Client.Systems; [DependsOn] public class ImGuiInputDebug { + private bool _isOpen = false; + [System] - public static void ShowInputDebugWindow(Universe universe, ImGuiData _) + public void ShowInputDebugWindow(Universe universe, ImGuiData _) { - var input = universe.Lookup(); + var input = universe.LookupByType(); if (input == null) return; - ImGui.Begin("Input Information", ImGuiWindowFlags.NoResize); + if (!ImGuiUtility.UIButtonToggle(1, ForkAwesome.Gamepad, "Input Information", ref _isOpen)) return; - if (universe.Lookup() is EntityRef keyboard) - DrawKeyboard(keyboard); + var center = ImGui.GetMainViewport().GetCenter(); + ImGui.SetNextWindowPos(center, ImGuiCond.Appearing, new(0.5f, 0.5f)); + ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]); + if (ImGui.Begin($"{ForkAwesome.Gamepad} Input Information", ref _isOpen, + ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize)) { + ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[0]); - if (universe.Lookup() is EntityRef mouse) { - ImGui.BeginChild("Mouse Info", new(160, 180), true); + if (universe.LookupByType() is EntityRef keyboard) + DrawKeyboard(keyboard); - ImGui.Text("Position: " + (Vector2?)mouse.Lookup("Position")?.MaybeGet()); - ImGui.Text("Delta: " + (Vector2?)mouse.Lookup("Delta" )?.MaybeGet()); - ImGui.Text("Wheel: " + (float?) mouse.Lookup("Wheel" )?.MaybeGet()); + if (universe.LookupByType() is EntityRef mouse) { + ImGui.BeginChild("Mouse Info", new(160, 180), true); - ImGui.Spacing(); + ImGui.Text("Position: " + (Vector2?)mouse.LookupChild("Position")?.GetOrNull()); + ImGui.Text("Delta: " + (Vector2?)mouse.LookupChild("Delta" )?.GetOrNull()); + ImGui.Text("Wheel: " + (float?) mouse.LookupChild("Wheel" )?.GetOrNull()); - var buttons = mouse.Lookup("Buttons")?.GetChildren().ToArray() ?? Array.Empty(); - ImGui.Text("Buttons: " + string.Join(" ", buttons - .Where (button => button.Has()) - .Select(button => $"{button.Name} ({button.Get().Duration.TotalSeconds:f2}s)"))); - ImGui.Text(" Pressed: " + string.Join(" ", buttons - .Where (button => button.Has()) - .Select(button => button.Name))); - ImGui.Text(" Released: " + string.Join(" ", buttons - .Where (button => button.Has()) - .Select(button => button.Name))); + ImGui.Spacing(); - ImGui.EndChild(); - } + var buttons = mouse.LookupChild("Buttons")?.GetChildren().ToArray() ?? Array.Empty(); + ImGui.Text("Buttons: " + string.Join(" ", buttons + .Where (button => button.Has()) + .Select(button => $"{button.Name} ({button.GetOrThrow().Duration.TotalSeconds:f2}s)"))); + ImGui.Text(" Pressed: " + string.Join(" ", buttons + .Where (button => button.Has()) + .Select(button => button.Name))); + ImGui.Text(" Released: " + string.Join(" ", buttons + .Where (button => button.Has()) + .Select(button => button.Name))); - for (var index = 0; input.Lookup("Gamepad" + index) is EntityRef gamepad; index++) { - ImGui.SameLine(); - ImGui.BeginChild($"{gamepad.Name} Info", new(160, 180), true); - - var buttons = gamepad.Lookup("Buttons")?.GetChildren().ToArray() ?? Array.Empty(); - ImGui.Text("Buttons: " + string.Join(" ", buttons.Where(b => b.Has()) - .Select(b => $"{b.Name} ({b.Get().Duration.TotalSeconds:f2}s)"))); - ImGui.Text(" Pressed: " + string.Join(" ", buttons.Where(b => b.Has()).Select(b => b.Name))); - ImGui.Text(" Released: " + string.Join(" ", buttons.Where(b => b.Has()).Select(b => b.Name))); - - ImGui.Spacing(); - - ImGui.Text("Triggers:"); - for (var i = 0; gamepad.Lookup("Trigger" + i) is EntityRef trigger; i++) { - var text = $" {i}: {(float?)trigger.MaybeGet() ?? default:f2}"; - if (trigger.Has()) text += " pressed!"; - else if (trigger.Has()) text += " released!"; - else if (trigger.MaybeGet() is Active active) - text += $" ({active.Duration.TotalSeconds:f2}s)"; - ImGui.Text(text); + ImGui.EndChild(); } - ImGui.Text("Thumbsticks:"); - for (var i = 0; gamepad.Lookup("Thumbstick" + i) is EntityRef thumbstick; i++) - ImGui.Text($" {i}: {(Vector2?)thumbstick.MaybeGet() ?? default:f2}"); + for (var index = 0; input.LookupChild("Gamepad" + index) is EntityRef gamepad; index++) { + ImGui.SameLine(); + ImGui.BeginChild($"{gamepad.Name} Info", new(160, 180), true); + + var buttons = gamepad.LookupChild("Buttons")?.GetChildren().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))); + ImGui.Text(" Released: " + string.Join(" ", buttons.Where(b => b.Has()).Select(b => b.Name))); + + ImGui.Spacing(); + + ImGui.Text("Triggers:"); + for (var i = 0; gamepad.LookupChild("Trigger" + i) is EntityRef trigger; i++) { + var text = $" {i}: {(float?)trigger.GetOrNull() ?? default:f2}"; + if (trigger.Has()) text += " pressed!"; + else if (trigger.Has()) text += " released!"; + else if (trigger.GetOrNull() is Active active) + text += $" ({active.Duration.TotalSeconds:f2}s)"; + ImGui.Text(text); + } + + ImGui.Text("Thumbsticks:"); + for (var i = 0; gamepad.LookupChild("Thumbstick" + i) is EntityRef thumbstick; i++) + ImGui.Text($" {i}: {(Vector2?)thumbstick.GetOrNull() ?? default:f2}"); - ImGui.EndChild(); - } + ImGui.EndChild(); + } + + ImGui.PopFont(); + } + ImGui.PopFont(); ImGui.End(); } - private const float U = 1.00F; - private const float SM = 1.25F; + private const float U = 1.00f; + private const float SM = 1.25f; // Spacing (invisible) - private const float I = -0.50F; - private const float _ = -1.00F; - private const float ER = -1.25F; + private const float I = -0.50f; + private const float _ = -1.00f; + private const float ER = -1.25f; // Special - private const float T = -11.00F; - private const float ENT = -11.50F; + private const float T = -11.00f; + private const float ENT = -11.50f; private static readonly float[][] KeyboardLayout = { new[] { U, _, U, U, U, U,I,U, U, U, U,I, U, U, U, U, I,U, U, U }, - new[] { U, U, U, U, U, U, U, U, U, U, U, U, U, 2.0F, I,U, U, U,I, U, U, U, U }, - new[] { 1.5F, U, U, U, U, U, U, U, U, U, U, U, U, ENT, I,U, U, U,I, U, U, U, T }, - new[] { 1.75F, U, U, U, U, U, U, U, U, U, U, U, U, ER, I,_, _, _,I, U, U, U, _ }, - new[] { SM, U, U, U, U, U, U, U, U, U, U, U, 2.75F, I,_, U, _,I, U, U, U, T }, - new[] { SM, SM, SM, 6.25F, SM, SM, SM, SM, I,U, U, U,I, 2.0F, U, _ }, + new[] { U, U, U, U, U, U, U, U, U, U, U, U, U, 2.0f, I,U, U, U,I, U, U, U, U }, + new[] { 1.5f, U, U, U, U, U, U, U, U, U, U, U, U, ENT, I,U, U, U,I, U, U, U, T }, + new[] { 1.75f, U, U, U, U, U, U, U, U, U, U, U, U, ER, I,_, _, _,I, U, U, U, _ }, + new[] { SM, U, U, U, U, U, U, U, U, U, U, U, 2.75f, I,_, U, _,I, U, U, U, T }, + new[] { SM, SM, SM, 6.25f, SM, SM, SM, SM, I,U, U, U,I, 2.0f, U, _ }, }; private static readonly Key?[][] KeyboardKeys = { new Key?[] { Key.Escape, null, Key.F1, Key.F2, Key.F3, Key.F4, null, Key.F5, Key.F6, Key.F7, Key.F8, null, Key.F9, Key.F10, Key.F11, Key.F12, null, Key.PrintScreen, Key.ScrollLock, Key.Pause }, @@ -108,7 +122,7 @@ public class ImGuiInputDebug new Key?[] { Key.ControlLeft, Key.SuperLeft, Key.AltLeft, Key.Space, Key.AltRight, Key.SuperRight, Key.Menu, Key.ControlRight, null, Key.Left, Key.Down, Key.Right, null, Key.Keypad0, Key.KeypadDecimal, null }, }; private static readonly Dictionary KeyToNameMapping = new() { - [Key.Escape] = "Ecs", [Key.PrintScreen] = "Prn\nScr", [Key.ScrollLock] = "Scr\nLck", [Key.Pause] = "Pause", + [Key.Escape] = "Esc", [Key.PrintScreen] = "Prn\nScr", [Key.ScrollLock] = "Scr\nLck", [Key.Pause] = "Pause", [Key.F1] = "F1", [Key.F2] = "F2", [Key.F3] = "F3", [Key.F4] = "F4", [Key.F5] = "F5", [Key.F6] = "F6", [Key.F7] = "F7", [Key.F8] = "F8", [Key.F9] = "F9", [Key.F10] = "F10", [Key.F11] = "F11", [Key.F12] = "F12", [Key.Tab] = "Tab", [Key.CapsLock] = "Caps\nLock", [Key.Menu] = "Menu", @@ -122,72 +136,77 @@ public class ImGuiInputDebug [Key.Insert] = "Ins", [Key.Delete] = "Del", [Key.Home] = "Home", [Key.End] = "End", [Key.PageUp] = "PgUp", [Key.PageDown] = "PgDn", - [Key.NumLock] = "Num\nLck", [Key.KeypadEnter] = "Enter", + [Key.NumLock] = "Num\nLck", [Key.KeypadEnter] = "=", }; public static void DrawKeyboard(EntityRef keyboard) { - var GLFW = Glfw.GetApi(); - const float UnitKeySize = 32.0F; - Vector2 Size = new Vector2(23, 6.5F) * UnitKeySize; + const float UnitKeySize = 32.0f; - Vector2 Border = new(1, 1); - Vector2 LabelOffset = new(7, 3); - Vector2 FaceStartOffset = new(5, 3); - Vector2 FaceEndOffset = new(5, 6); - - uint BorderColor = Color.FromGrayscale(24).RGBA; - uint LabelColor = Color.FromGrayscale(64).RGBA; + var GLFW = Glfw.GetApi(); + var draw = ImGui.GetWindowDrawList(); - var draw = ImGui.GetWindowDrawList(); var offset = ImGui.GetCursorScreenPos(); var current = Vector2.Zero; foreach (var (widths, keys) in KeyboardLayout.Zip(KeyboardKeys)) { foreach (var (width, key) in widths.Zip(keys)) { - uint KeyColor(byte lightness) - => (key != null) && (keyboard.Lookup(key.Value.ToString())?.Has() == true) - ? Color.FromRGB(lightness, (byte)(lightness / 2), (byte)(lightness / 2)).RGBA - : Color.FromGrayscale(lightness).RGBA; + var active = (key != null) && (keyboard.LookupChild(key.Value.ToString())?.Has() == true); - var start = offset + current; - var keySize = new Vector2(width, 1.0F); - - if (width == T) keySize = new Vector2((-keySize.X - 10), 2.0F); - else if (width < -10) keySize = new Vector2((-keySize.X - 10), 1.0F); + var keySize = new Vector2(width, 1.0f); + if (width == 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; } var label = (key != null) ? KeyToNameMapping.GetValueOrDefault(key.Value) ?? GLFW.GetKeyName((int)key.Value, 0)?.ToUpper() : null; - - var end = start + keySize * UnitKeySize; if (width == ENT) { - var start2 = start + new Vector2(0.25F * UnitKeySize, 0); - var end2 = end + new Vector2(0, 1 * UnitKeySize); - draw.AddRectFilled(start , end , BorderColor, 3); - draw.AddRectFilled(start2 , end2 , BorderColor, 3); - draw.AddRectFilled(start + Border, end - Border, KeyColor(204), 3); - draw.AddRectFilled(start2 + Border, end2 - Border, KeyColor(204), 3); - var faceStart = start + FaceStartOffset; - var faceEnd = end - FaceEndOffset; - draw.AddRectFilled(faceStart , faceEnd , KeyColor(252), 2); - var faceStart2 = start2 + FaceStartOffset; - var faceEnd2 = end2 - FaceEndOffset; - draw.AddRectFilled(faceStart2, faceEnd2, KeyColor(252), 2); - if (label != null) draw.AddText(start + LabelOffset, LabelColor, label); - } else { - draw.AddRectFilled(start , end , BorderColor, 3); - draw.AddRectFilled(start + Border, end - Border, KeyColor(204), 3); - var faceStart = start + FaceStartOffset; - var faceEnd = end - FaceEndOffset; - draw.AddRectFilled(faceStart, faceEnd, KeyColor(252), 2); - if (label != null) draw.AddText(start + LabelOffset, LabelColor, label); - } - current += new Vector2(keySize.X * UnitKeySize, 0); + DrawButton(draw, label, active, + (offset + current, keySize * UnitKeySize), + (offset + current + new Vector2(0.25f * UnitKeySize, 0), (keySize + new Vector2(-0.25f, 1)) * UnitKeySize)); + } else DrawButton(draw, label, active, + (offset + current, keySize * UnitKeySize)); + current += new Vector2(keySize.X * UnitKeySize, 0); } current = new Vector2(0, current.Y + UnitKeySize); if (widths == KeyboardLayout[0]) current += new Vector2(0, UnitKeySize / 2); } - ImGui.Dummy(Size); + + ImGui.Dummy(new Vector2(23, 6.5f) * UnitKeySize); + } + + private static void DrawButton(ImDrawListPtr draw, string? label, + bool active, params (Vector2 Pos, Vector2 Size)[] parts) + { + const int Rounding = 3; + var Border = new Vector2(1, 1); + var LabelOffset = new Vector2(7, 3); + var FaceStartOffset = new Vector2(5, 3); + var FaceEndOffset = new Vector2(5, 6); + + var BorderColor = Color.FromGrayscale(0.10f); + var LabelColor = Color.FromGrayscale(0.25f); + var OuterKeyColorActive = Color.FromRGB(0.8f, 0.4f, 0.4f); + var OuterKeyColorInactive = Color.FromGrayscale(0.8f); + var InnerKeyColorActive = Color.FromRGB(1.0f, 0.0f, 0.0f); + var InnerKeyColorInactive = Color.FromGrayscale(1.0f); + + var outerKeyColor = active ? OuterKeyColorActive : OuterKeyColorInactive; + var innerKeyColor = active ? InnerKeyColorActive : InnerKeyColorInactive; + + foreach (var (pos, size) in parts) + draw.AddRectFilled(pos, pos + size, BorderColor.RGBA, Rounding); + foreach (var (pos, size) in parts) + draw.AddRectFilled(pos + Border, pos + size - Border, outerKeyColor.RGBA, Rounding); + foreach (var (pos, size) in parts) + draw.AddRectFilled(pos + FaceStartOffset, pos + size - FaceEndOffset, innerKeyColor.RGBA, Rounding - 1); + if (label != null) { + var pos = parts[0].Pos + LabelOffset; + var height = ImGui.GetTextLineHeight() - 4; + foreach (var line in label.Split('\n')) { + draw.AddText(pos, LabelColor.RGBA, line); + pos += new Vector2(0, height); + } + } } } diff --git a/src/gaemstone.Client/Systems/ImGuiManager.cs b/src/gaemstone.Client/Systems/ImGuiManager.cs index bd12c7d..2ca6ec9 100644 --- a/src/gaemstone.Client/Systems/ImGuiManager.cs +++ b/src/gaemstone.Client/Systems/ImGuiManager.cs @@ -1,6 +1,11 @@ using System; +using System.IO; +using System.Linq; +using System.Text; +using gaemstone.Client.Utility; using gaemstone.ECS; using gaemstone.Flecs; +using gaemstone.Utility; using ImGuiNET; using Silk.NET.Input; using Silk.NET.OpenGL.Extensions.ImGui; @@ -23,29 +28,92 @@ public class ImGuiManager [DependsOn] public struct ImGuiRenderPhase { } - [Component, Singleton] + [Component, Singleton(AutoAdd = false)] public class ImGuiData { public ImGuiController Controller { get; } internal ImGuiData(ImGuiController controller) => Controller = controller; } + private unsafe static ImFontPtr AddFontFromResources( + ImGuiIOPtr io, string name, int size, Action? cfgAction = null, + float offset = 0, float minAdvance = 0, + (int Min, int Max)[]? ranges = null, bool merge = false) + { + // TODO: FontConfig can be freed at the end of this method. + // Unfortunately, data has to stick around until font atlas is built. + var cfg = new ImFontConfigPtr(ImGuiNative.ImFontConfig_ImFontConfig()) { + GlyphOffset = new(0, offset), + GlyphMinAdvanceX = minAdvance, + MergeMode = merge, + }; + // Set cfg.Name so the font has a nice display name in ImGui. + var fullName = $"{name.Replace('.', ' ')} {size}px"; + Encoding.UTF8.GetBytes(fullName, new Span(cfg.Name.Data, cfg.Name.Count)); + // If glyph ranges are specified, allocate unmanaged heap memory for them. + if (ranges != null) { + var rangesSpan = GlobalHeapAllocator.Instance.Allocate(ranges.Length * 2 + 1); + for (var i = 0; i < ranges.Length; i++) { + rangesSpan[i * 2 ] = (char)ranges[i].Min; + rangesSpan[i * 2 + 1] = (char)ranges[i].Max; + } + rangesSpan[^1] = default; + fixed (void* rangesPtr = rangesSpan) + cfg.GlyphRanges = (IntPtr)rangesPtr; + } + // Use cfgAction to allow changing other font configs. + cfgAction?.Invoke(cfg); + + // Grab the stream for this font from the assembly's resources. + var file = $"gaemstone.Client.Resources.{name}.ttf"; + using var stream = typeof(Resources).Assembly.GetManifestResourceStream(file) + ?? throw new InvalidOperationException($"Resource '{file}' was not found"); + + using var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); // Write font file from resources to memory stream. + memoryStream.WriteByte(0); // Add a NUL termination character. + + // Copy the data into unmanaged memory and pass it to ImGui. + var fontData = memoryStream.ToArray(); + var fontDataSpan = GlobalHeapAllocator.Instance.AllocateCopy(fontData); + fixed (byte* dataPtr = fontDataSpan) + return io.Fonts.AddFontFromMemoryTTF((IntPtr)dataPtr, size, size, cfg); + } + [System] public unsafe void Initialize(Universe universe, GameWindow window, Canvas canvas, [Source] IInputContext inputContext, [Not] ImGuiData _) - => universe.LookupOrThrow().Set(new ImGuiData( + => universe.LookupByTypeOrThrow().Set(new ImGuiData( new(canvas.GL, window.Handle, inputContext, () => { - var IO = ImGui.GetIO(); + var io = ImGui.GetIO(); + var style = ImGui.GetStyle(); + + var fontSize = 16; + void MergeIcons() => AddFontFromResources(io, "ForkAwesome", fontSize, + minAdvance: 18, ranges: new[]{ (ForkAwesome.Min, ForkAwesome.Max) }, merge: true); + AddFontFromResources(io, "OpenSans" , fontSize, offset: -1); MergeIcons(); + AddFontFromResources(io, "OpenSans.Bold" , fontSize, offset: -1); MergeIcons(); + AddFontFromResources(io, "OpenSans.Italic", fontSize, offset: -1); MergeIcons(); // Do not save a settings or log file. - IO.NativePtr->IniFilename = null; - IO.NativePtr->LogFilename = null; + io.NativePtr->IniFilename = null; + io.NativePtr->LogFilename = null; - IO.BackendFlags |= ImGuiBackendFlags.HasMouseCursors + io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors | ImGuiBackendFlags.HasSetMousePos; - IO.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard + io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard | ImGuiConfigFlags.NavEnableSetMousePos; - IO.ConfigWindowsResizeFromEdges = true; + // | ImGuiConfigFlags.DockingEnable; + io.ConfigWindowsResizeFromEdges = true; + io.ConfigWindowsMoveFromTitleBarOnly = true; + + style.WindowRounding = style.ChildRounding + = style.PopupRounding = style.TabRounding + = style.FrameRounding = style.ScrollbarRounding = 6; + + style.WindowTitleAlign = new(0.5f, 0.5f); + style.ColorButtonPosition = ImGuiDir.Left; + style.IndentSpacing = 8; // Set up key mappings. foreach (var imguiKey in Enum.GetValues()) { @@ -62,7 +130,7 @@ public class ImGuiManager } if (Enum.TryParse(name, true, out var silkKey)) - IO.KeyMap[(int)imguiKey] = (int)silkKey; + io.KeyMap[(int)imguiKey] = (int)silkKey; } }))); @@ -70,9 +138,9 @@ public class ImGuiManager public static void UpdateMouse(Universe universe, [Source] IMouse impl, ImGuiData _) { - var input = universe.LookupOrThrow(); - var module = universe.LookupOrThrow(); - var capturedBy = input.GetTarget(); + var input = universe.LookupByTypeOrThrow(); + var module = universe.LookupByTypeOrThrow(); + var capturedBy = input.GetTargets().FirstOrDefault();; var isCaptured = (capturedBy != null); // If another system has the mouse captured, don't do anything here. if (isCaptured && (capturedBy != module)) return; @@ -110,10 +178,6 @@ public class ImGuiManager public static void Update(TimeSpan delta, ImGuiData imgui) => imgui.Controller.Update((float)delta.TotalSeconds); - [System] - public static void ShowDemoWindow(ImGuiData _) - => ImGui.ShowDemoWindow(); - [System] 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 9ea0337..904c104 100644 --- a/src/gaemstone.Client/Systems/InputManager.cs +++ b/src/gaemstone.Client/Systems/InputManager.cs @@ -15,28 +15,29 @@ namespace gaemstone.Client.Systems; [DependsOn] public class InputManager { - [Component("InputContext"), Proxy] public struct ContextProxy { } + [Component, Path("InputContext"), Proxy] public struct ContextProxy { } - [Component("MouseImpl" ), Proxy] public struct MouseProxy { } - [Component("KeyboardImpl"), Proxy] public struct KeyboardProxy { } - [Component("GamepadImpl" ), Proxy] public struct GamepadProxy { } + [Component, Path("MouseImpl" ), Proxy] public struct MouseProxy { } + [Component, Path("KeyboardImpl"), Proxy] public struct KeyboardProxy { } + [Component, Path("GamepadImpl" ), Proxy] public struct GamepadProxy { } [System] public static void Initialize(Universe universe, [Game] GameWindow window, [Source, Not] IInputContext _) { - var input = universe.LookupOrThrow(); + var input = universe.LookupByTypeOrThrow(); var context = window.Handle.CreateInput(); input.Set(context); // TODO: Add device names as documentation names to these entities. foreach (var impl in context.Mice.Take(1)) - input.LookupOrThrow("Mouse").Set(impl); + input.LookupChildOrThrow("Mouse").Set(impl); foreach (var impl in context.Keyboards.Take(1)) - input.LookupOrThrow("Keyboard").Set(impl); + input.LookupChildOrThrow("Keyboard").Set(impl); foreach (var impl in context.Gamepads) input.NewChild("Gamepad" + impl.Index).Add().Set(impl).Build(); + // TODO: Should we even support joysticks? } @@ -44,13 +45,13 @@ public class InputManager [Observer] [Expression("CursorCapturedBy(Input, *)")] public static void OnCursorCaptured(Universe universe) - => universe.LookupOrThrow().Get() + => universe.LookupByTypeOrThrow().GetOrThrow() .Cursor.CursorMode = CursorMode.Raw; [Observer] [Expression("CursorCapturedBy(Input, *)")] public static void OnCursorReleased(Universe universe) - => universe.LookupOrThrow().Get() + => universe.LookupByTypeOrThrow().GetOrThrow() .Cursor.CursorMode = CursorMode.Normal; @@ -94,8 +95,8 @@ public class InputManager } - private const float ActivationThreshold = 0.90F; - private const float DeactivationThreshold = 0.75F; + private const float ActivationThreshold = 0.90f; + private const float DeactivationThreshold = 0.75f; private static void Update1D(TimeSpan delta, EntityRef entity, float current) { @@ -135,7 +136,6 @@ public class InputManager => entity.Add(); [System] - // [Expression("Activated || Deactivated")] public static void ClearDeActivated(EntityRef entity, [Or] Activated _1, [Or] Deactivated _2) => entity.Remove().Remove(); } diff --git a/src/gaemstone.Client/Systems/MeshManager.cs b/src/gaemstone.Client/Systems/MeshManager.cs index 139183e..6eda563 100644 --- a/src/gaemstone.Client/Systems/MeshManager.cs +++ b/src/gaemstone.Client/Systems/MeshManager.cs @@ -1,7 +1,7 @@ using System; using System.IO; +using System.Numerics; using gaemstone.ECS; -using Silk.NET.Maths; using Silk.NET.OpenGL; using static gaemstone.Client.Components.RenderingComponents; using static gaemstone.Client.Components.ResourceComponents; @@ -63,8 +63,8 @@ public class MeshManager } public static MeshHandle Create(GL GL, - ReadOnlySpan indices, ReadOnlySpan> vertices, - ReadOnlySpan> normals, ReadOnlySpan> uvs) + ReadOnlySpan indices, ReadOnlySpan vertices, + ReadOnlySpan normals, ReadOnlySpan uvs) { var vao = GL.GenVertexArray(); GL.BindVertexArray(vao); @@ -90,8 +90,8 @@ public class MeshManager return new(vao, indices.Length); } - public static MeshHandle Create(GL GL, ReadOnlySpan> vertices, - ReadOnlySpan> normals, ReadOnlySpan> uvs) + public static MeshHandle Create(GL GL, ReadOnlySpan vertices, + ReadOnlySpan normals, ReadOnlySpan uvs) { var vao = GL.GenVertexArray(); GL.BindVertexArray(vao); diff --git a/src/gaemstone.Client/Systems/Renderer.cs b/src/gaemstone.Client/Systems/Renderer.cs index 455d6c9..7259c6d 100644 --- a/src/gaemstone.Client/Systems/Renderer.cs +++ b/src/gaemstone.Client/Systems/Renderer.cs @@ -1,10 +1,10 @@ using System; using System.Diagnostics; +using System.Numerics; using System.Runtime.InteropServices; using gaemstone.ECS; using gaemstone.Flecs; using gaemstone.Utility; -using Silk.NET.Maths; using Silk.NET.OpenGL; using Silk.NET.Windowing; using static gaemstone.Client.Components.CameraComponents; @@ -60,7 +60,7 @@ public class Renderer var GL = canvas.GL; GL.UseProgram(_program); GL.Viewport(canvas.Size); - GL.ClearColor(Color.Black); + GL.ClearColor(canvas.BackgroundColor); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); } @@ -68,53 +68,53 @@ public class Renderer public void Render(Universe universe, [Game] Canvas canvas, in GlobalTransform cameraTransform, in Camera camera, CameraViewport? viewport) { - var color = viewport?.ClearColor ?? new(0x4B, 0x00, 0x82, 255); + var color = viewport?.ClearColor ?? Color.FromRGB(0.3f, 0.0f, 0.5f); var bounds = viewport?.Viewport ?? new(default, canvas.Size); var GL = canvas.GL; GL.Enable(EnableCap.ScissorTest); - GL.Viewport(bounds); GL.Scissor(bounds.Origin.X, bounds.Origin.Y, (uint)bounds.Size.X, (uint)bounds.Size.Y); + GL.Viewport(bounds); GL.Scissor(bounds.Left, bounds.Top, (uint)bounds.Width, (uint)bounds.Height); GL.ClearColor(color); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); GL.Disable(EnableCap.ScissorTest); // Create the camera's projection matrix. var cameraProjection = camera.IsOrthographic - ? Matrix4X4.CreateOrthographic( - bounds.Size.X, -bounds.Size.Y, + ? Matrix4x4.CreateOrthographic( + bounds.Width, -bounds.Height, camera.NearPlane, camera.FarPlane) - : Matrix4X4.CreatePerspectiveFieldOfView( + : Matrix4x4.CreatePerspectiveFieldOfView( camera.FieldOfView * MathF.PI / 180, // Degrees => Radians - (float)bounds.Size.X / bounds.Size.Y, // Aspect Ratio + (float)bounds.Width / bounds.Height, // Aspect Ratio camera.NearPlane, camera.FarPlane); // Get the camera's transform matrix and invert it. - Matrix4X4.Invert(cameraTransform, out var invertedTransform); + Matrix4x4.Invert(cameraTransform, out var invertedTransform); // Set the uniform to the combined transform and projection. var cameraMatrix = invertedTransform * cameraProjection; - GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.Row1.X); + GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.M11); - _renderEntityRule ??= new(universe, new(@" + _renderEntityRule ??= new(universe, new(""" GlobalTransform, (Mesh, $mesh), MeshHandle($mesh), ?(Texture, $tex), ?TextureHandle($tex) - ")); + """)); foreach (var iter in _renderEntityRule.Iter()) { var transforms = iter.Field(1); var meshes = iter.Field(3); - // var texPairs = iter.MaybeField(4); - var textures = iter.MaybeField(5); + // var texPairs = iter.FieldOrEmpty(4); + var textures = iter.FieldOrEmpty(5); for (var i = 0; i < iter.Count; i++) { var rTransform = transforms[i]; var mesh = meshes[i]; // var hasTexture = (texPairs.Length > 0); - var texture = textures.MaybeGet(i); + var texture = textures.GetOrNull(i); // If entity has Texture, bind it now. if (texture.HasValue) GL.BindTexture(texture.Value.Target, texture.Value.Handle); // Draw the mesh. - GL.UniformMatrix4(_modelMatrixUniform, 1, false, in rTransform.Value.Row1.X); + GL.UniformMatrix4(_modelMatrixUniform, 1, false, in rTransform.Value.M11); GL.BindVertexArray(mesh.Handle); if (!mesh.IsIndexed) GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)mesh.Count); else unsafe { GL.DrawElements(PrimitiveType.Triangles, (uint)mesh.Count, DrawElementsType.UnsignedShort, null); } diff --git a/src/gaemstone.Client/Systems/Windowing.cs b/src/gaemstone.Client/Systems/Windowing.cs index 2782bb0..24de8c9 100644 --- a/src/gaemstone.Client/Systems/Windowing.cs +++ b/src/gaemstone.Client/Systems/Windowing.cs @@ -1,6 +1,6 @@ +using System.Drawing; using gaemstone.ECS; using gaemstone.Flecs; -using Silk.NET.Maths; using Silk.NET.OpenGL; using Silk.NET.Windowing; @@ -15,7 +15,7 @@ public class Windowing public GL GL { get; } public Canvas(GL gl) => GL = gl; - public Vector2D Size { get; set; } + public Size Size { get; set; } public Color BackgroundColor { get; set; } } @@ -29,7 +29,7 @@ public class Windowing [System] public static void ProcessWindow(GameWindow window, Canvas canvas) { - canvas.Size = window.Handle.Size; + canvas.Size = new(window.Handle.Size.X, window.Handle.Size.Y); window.Handle.DoEvents(); } } diff --git a/src/gaemstone.Client/Utility/ForkAwesome.cs b/src/gaemstone.Client/Utility/ForkAwesome.cs new file mode 100644 index 0000000..f917f77 --- /dev/null +++ b/src/gaemstone.Client/Utility/ForkAwesome.cs @@ -0,0 +1,819 @@ +namespace gaemstone.Client.Utility; + +public class ForkAwesome +{ + public const int Min = 0xf000; + public const int Max = 0xf372; + + public const string Glass = "\uF000"; + public const string Music = "\uF001"; + public const string Search = "\uF002"; + public const string EnvelopeO = "\uF003"; + public const string Heart = "\uF004"; + public const string Star = "\uF005"; + public const string StarO = "\uF006"; + public const string User = "\uF007"; + public const string Film = "\uF008"; + public const string ThLarge = "\uF009"; + public const string Th = "\uF00A"; + public const string ThList = "\uF00B"; + public const string Check = "\uF00C"; + public const string Times = "\uF00D"; + public const string SearchPlus = "\uF00E"; + public const string SearchMinus = "\uF010"; + public const string PowerOff = "\uF011"; + public const string Signal = "\uF012"; + public const string Cog = "\uF013"; + public const string TrashO = "\uF014"; + public const string Home = "\uF015"; + public const string FileO = "\uF016"; + public const string ClockO = "\uF017"; + public const string Road = "\uF018"; + public const string Download = "\uF019"; + public const string ArrowCircleODown = "\uF01A"; + public const string ArrowCircleOUp = "\uF01B"; + public const string Inbox = "\uF01C"; + public const string PlayCircleO = "\uF01D"; + public const string Repeat = "\uF01E"; + public const string Refresh = "\uF021"; + public const string ListAlt = "\uF022"; + public const string Lock = "\uF023"; + public const string Flag = "\uF024"; + public const string Headphones = "\uF025"; + public const string VolumeOff = "\uF026"; + public const string VolumeDown = "\uF027"; + public const string VolumeUp = "\uF028"; + public const string Qrcode = "\uF029"; + public const string Barcode = "\uF02A"; + public const string Tag = "\uF02B"; + public const string Tags = "\uF02C"; + public const string Book = "\uF02D"; + public const string Bookmark = "\uF02E"; + public const string Print = "\uF02F"; + public const string Camera = "\uF030"; + public const string Font = "\uF031"; + public const string Bold = "\uF032"; + public const string Italic = "\uF033"; + public const string TextHeight = "\uF034"; + public const string TextWidth = "\uF035"; + public const string AlignLeft = "\uF036"; + public const string AlignCenter = "\uF037"; + public const string AlignRight = "\uF038"; + public const string AlignJustify = "\uF039"; + public const string List = "\uF03A"; + public const string Outdent = "\uF03B"; + public const string Indent = "\uF03C"; + public const string VideoCamera = "\uF03D"; + public const string PictureO = "\uF03E"; + public const string Pencil = "\uF040"; + public const string MapMarker = "\uF041"; + public const string Adjust = "\uF042"; + public const string Tint = "\uF043"; + public const string PencilSquareO = "\uF044"; + public const string ShareSquareO = "\uF045"; + public const string CheckSquareO = "\uF046"; + public const string Arrows = "\uF047"; + public const string StepBackward = "\uF048"; + public const string FastBackward = "\uF049"; + public const string Backward = "\uF04A"; + public const string Play = "\uF04B"; + public const string Pause = "\uF04C"; + public const string Stop = "\uF04D"; + public const string Forward = "\uF04E"; + public const string FastForward = "\uF050"; + public const string StepForward = "\uF051"; + public const string Eject = "\uF052"; + public const string ChevronLeft = "\uF053"; + public const string ChevronRight = "\uF054"; + public const string PlusCircle = "\uF055"; + public const string MinusCircle = "\uF056"; + public const string TimesCircle = "\uF057"; + public const string CheckCircle = "\uF058"; + public const string QuestionCircle = "\uF059"; + public const string InfoCircle = "\uF05A"; + public const string Crosshairs = "\uF05B"; + public const string TimesCircleO = "\uF05C"; + public const string CheckCircleO = "\uF05D"; + public const string Ban = "\uF05E"; + public const string ArrowLeft = "\uF060"; + public const string ArrowRight = "\uF061"; + public const string ArrowUp = "\uF062"; + public const string ArrowDown = "\uF063"; + public const string Share = "\uF064"; + public const string Expand = "\uF065"; + public const string Compress = "\uF066"; + public const string Plus = "\uF067"; + public const string Minus = "\uF068"; + public const string Asterisk = "\uF069"; + public const string ExclamationCircle = "\uF06A"; + public const string Gift = "\uF06B"; + public const string Leaf = "\uF06C"; + public const string Fire = "\uF06D"; + public const string Eye = "\uF06E"; + public const string EyeSlash = "\uF070"; + public const string ExclamationTriangle = "\uF071"; + public const string Plane = "\uF072"; + public const string Calendar = "\uF073"; + public const string Random = "\uF074"; + public const string Comment = "\uF075"; + public const string Magnet = "\uF076"; + public const string ChevronUp = "\uF077"; + public const string ChevronDown = "\uF078"; + public const string Retweet = "\uF079"; + public const string ShoppingCart = "\uF07A"; + public const string Folder = "\uF07B"; + public const string FolderOpen = "\uF07C"; + public const string ArrowsV = "\uF07D"; + public const string ArrowsH = "\uF07E"; + public const string BarChart = "\uF080"; + public const string TwitterSquare = "\uF081"; + public const string FacebookSquare = "\uF082"; + public const string CameraRetro = "\uF083"; + public const string Key = "\uF084"; + public const string Cogs = "\uF085"; + public const string Comments = "\uF086"; + public const string ThumbsOUp = "\uF087"; + public const string ThumbsODown = "\uF088"; + public const string StarHalf = "\uF089"; + public const string HeartO = "\uF08A"; + public const string SignOut = "\uF08B"; + public const string LinkedinSquare = "\uF08C"; + public const string ThumbTack = "\uF08D"; + public const string ExternalLink = "\uF08E"; + public const string SignIn = "\uF090"; + public const string Trophy = "\uF091"; + public const string GithubSquare = "\uF092"; + public const string Upload = "\uF093"; + public const string LemonO = "\uF094"; + public const string Phone = "\uF095"; + public const string SquareO = "\uF096"; + public const string BookmarkO = "\uF097"; + public const string PhoneSquare = "\uF098"; + public const string Twitter = "\uF099"; + public const string Facebook = "\uF09A"; + public const string Github = "\uF09B"; + public const string Unlock = "\uF09C"; + public const string CreditCard = "\uF09D"; + public const string Rss = "\uF09E"; + public const string HddO = "\uF0A0"; + public const string Bullhorn = "\uF0A1"; + public const string BellO = "\uF0F3"; + public const string Certificate = "\uF0A3"; + public const string HandORight = "\uF0A4"; + public const string HandOLeft = "\uF0A5"; + public const string HandOUp = "\uF0A6"; + public const string HandODown = "\uF0A7"; + public const string ArrowCircleLeft = "\uF0A8"; + public const string ArrowCircleRight = "\uF0A9"; + public const string ArrowCircleUp = "\uF0AA"; + public const string ArrowCircleDown = "\uF0AB"; + public const string Globe = "\uF0AC"; + public const string GlobeE = "\uF304"; + public const string GlobeW = "\uF305"; + public const string Wrench = "\uF0AD"; + public const string Tasks = "\uF0AE"; + public const string Filter = "\uF0B0"; + public const string Briefcase = "\uF0B1"; + public const string ArrowsAlt = "\uF0B2"; + public const string Users = "\uF0C0"; + public const string Link = "\uF0C1"; + public const string Cloud = "\uF0C2"; + public const string Flask = "\uF0C3"; + public const string Scissors = "\uF0C4"; + public const string FilesO = "\uF0C5"; + public const string Paperclip = "\uF0C6"; + public const string FloppyO = "\uF0C7"; + public const string Square = "\uF0C8"; + public const string Bars = "\uF0C9"; + public const string ListUl = "\uF0CA"; + public const string ListOl = "\uF0CB"; + public const string Strikethrough = "\uF0CC"; + public const string Underline = "\uF0CD"; + public const string Table = "\uF0CE"; + public const string Magic = "\uF0D0"; + public const string Truck = "\uF0D1"; + public const string Pinterest = "\uF0D2"; + public const string PinterestSquare = "\uF0D3"; + public const string GooglePlusSquare = "\uF0D4"; + public const string GooglePlus = "\uF0D5"; + public const string Money = "\uF0D6"; + public const string CaretDown = "\uF0D7"; + public const string CaretUp = "\uF0D8"; + public const string CaretLeft = "\uF0D9"; + public const string CaretRight = "\uF0DA"; + public const string Columns = "\uF0DB"; + public const string Sort = "\uF0DC"; + public const string SortDesc = "\uF0DD"; + public const string SortAsc = "\uF0DE"; + public const string Envelope = "\uF0E0"; + public const string Linkedin = "\uF0E1"; + public const string Undo = "\uF0E2"; + public const string Gavel = "\uF0E3"; + public const string Tachometer = "\uF0E4"; + public const string CommentO = "\uF0E5"; + public const string CommentsO = "\uF0E6"; + public const string Bolt = "\uF0E7"; + public const string Sitemap = "\uF0E8"; + public const string Umbrella = "\uF0E9"; + public const string Clipboard = "\uF0EA"; + public const string LightbulbO = "\uF0EB"; + public const string Exchange = "\uF0EC"; + public const string CloudDownload = "\uF0ED"; + public const string CloudUpload = "\uF0EE"; + public const string UserMd = "\uF0F0"; + public const string Stethoscope = "\uF0F1"; + public const string Suitcase = "\uF0F2"; + public const string Bell = "\uF0A2"; + public const string Coffee = "\uF0F4"; + public const string Cutlery = "\uF0F5"; + public const string FileTextO = "\uF0F6"; + public const string BuildingO = "\uF0F7"; + public const string HospitalO = "\uF0F8"; + public const string Ambulance = "\uF0F9"; + public const string Medkit = "\uF0FA"; + public const string FighterJet = "\uF0FB"; + public const string Beer = "\uF0FC"; + public const string HSquare = "\uF0FD"; + public const string PlusSquare = "\uF0FE"; + public const string AngleDoubleLeft = "\uF100"; + public const string AngleDoubleRight = "\uF101"; + public const string AngleDoubleUp = "\uF102"; + public const string AngleDoubleDown = "\uF103"; + public const string AngleLeft = "\uF104"; + public const string AngleRight = "\uF105"; + public const string AngleUp = "\uF106"; + public const string AngleDown = "\uF107"; + public const string Desktop = "\uF108"; + public const string Laptop = "\uF109"; + public const string Tablet = "\uF10A"; + public const string Mobile = "\uF10B"; + public const string CircleO = "\uF10C"; + public const string QuoteLeft = "\uF10D"; + public const string QuoteRight = "\uF10E"; + public const string Spinner = "\uF110"; + public const string Circle = "\uF111"; + public const string Reply = "\uF112"; + public const string GithubAlt = "\uF113"; + public const string FolderO = "\uF114"; + public const string FolderOpenO = "\uF115"; + public const string SmileO = "\uF118"; + public const string FrownO = "\uF119"; + public const string MehO = "\uF11A"; + public const string Gamepad = "\uF11B"; + public const string KeyboardO = "\uF11C"; + public const string FlagO = "\uF11D"; + public const string FlagCheckered = "\uF11E"; + public const string Terminal = "\uF120"; + public const string Code = "\uF121"; + public const string ReplyAll = "\uF122"; + public const string StarHalfO = "\uF123"; + public const string LocationArrow = "\uF124"; + public const string Crop = "\uF125"; + public const string CodeFork = "\uF126"; + public const string ChainBroken = "\uF127"; + public const string Question = "\uF128"; + public const string Info = "\uF129"; + public const string Exclamation = "\uF12A"; + public const string Superscript = "\uF12B"; + public const string Subscript = "\uF12C"; + public const string Eraser = "\uF12D"; + public const string PuzzlePiece = "\uF12E"; + public const string Microphone = "\uF130"; + public const string MicrophoneSlash = "\uF131"; + public const string Shield = "\uF132"; + public const string CalendarO = "\uF133"; + public const string FireExtinguisher = "\uF134"; + public const string Rocket = "\uF135"; + public const string Maxcdn = "\uF136"; + public const string ChevronCircleLeft = "\uF137"; + public const string ChevronCircleRight = "\uF138"; + public const string ChevronCircleUp = "\uF139"; + public const string ChevronCircleDown = "\uF13A"; + public const string Html5 = "\uF13B"; + public const string Css3 = "\uF13C"; + public const string Anchor = "\uF13D"; + public const string UnlockAlt = "\uF13E"; + public const string Bullseye = "\uF140"; + public const string EllipsisH = "\uF141"; + public const string EllipsisV = "\uF142"; + public const string RssSquare = "\uF143"; + public const string PlayCircle = "\uF144"; + public const string Ticket = "\uF145"; + public const string MinusSquare = "\uF146"; + public const string MinusSquareO = "\uF147"; + public const string LevelUp = "\uF148"; + public const string LevelDown = "\uF149"; + public const string CheckSquare = "\uF14A"; + public const string PencilSquare = "\uF14B"; + public const string ExternalLinkSquare = "\uF14C"; + public const string ShareSquare = "\uF14D"; + public const string Compass = "\uF14E"; + public const string CaretSquareODown = "\uF150"; + public const string CaretSquareOUp = "\uF151"; + public const string CaretSquareORight = "\uF152"; + public const string Eur = "\uF153"; + public const string Gbp = "\uF154"; + public const string Usd = "\uF155"; + public const string Inr = "\uF156"; + public const string Jpy = "\uF157"; + public const string Rub = "\uF158"; + public const string Krw = "\uF159"; + public const string Btc = "\uF15A"; + public const string File = "\uF15B"; + public const string FileText = "\uF15C"; + public const string SortAlphaAsc = "\uF15D"; + public const string SortAlphaDesc = "\uF15E"; + public const string SortAmountAsc = "\uF160"; + public const string SortAmountDesc = "\uF161"; + public const string SortNumericAsc = "\uF162"; + public const string SortNumericDesc = "\uF163"; + public const string ThumbsUp = "\uF164"; + public const string ThumbsDown = "\uF165"; + public const string YoutubeSquare = "\uF166"; + public const string Youtube = "\uF167"; + public const string Xing = "\uF168"; + public const string XingSquare = "\uF169"; + public const string YoutubePlay = "\uF16A"; + public const string Dropbox = "\uF16B"; + public const string StackOverflow = "\uF16C"; + public const string Instagram = "\uF16D"; + public const string Flickr = "\uF16E"; + public const string Adn = "\uF170"; + public const string Bitbucket = "\uF171"; + public const string BitbucketSquare = "\uF172"; + public const string Tumblr = "\uF173"; + public const string TumblrSquare = "\uF174"; + public const string LongArrowDown = "\uF175"; + public const string LongArrowUp = "\uF176"; + public const string LongArrowLeft = "\uF177"; + public const string LongArrowRight = "\uF178"; + public const string Apple = "\uF179"; + public const string Windows = "\uF17A"; + public const string Android = "\uF17B"; + public const string Linux = "\uF17C"; + public const string Dribbble = "\uF17D"; + public const string Skype = "\uF17E"; + public const string Foursquare = "\uF180"; + public const string Trello = "\uF181"; + public const string Female = "\uF182"; + public const string Male = "\uF183"; + public const string Gratipay = "\uF184"; + public const string SunO = "\uF185"; + public const string MoonO = "\uF186"; + public const string Archive = "\uF187"; + public const string Bug = "\uF188"; + public const string Vk = "\uF189"; + public const string Weibo = "\uF18A"; + public const string Renren = "\uF18B"; + public const string Pagelines = "\uF18C"; + public const string StackExchange = "\uF18D"; + public const string ArrowCircleORight = "\uF18E"; + public const string ArrowCircleOLeft = "\uF190"; + public const string CaretSquareOLeft = "\uF191"; + public const string DotCircleO = "\uF192"; + public const string Wheelchair = "\uF193"; + public const string VimeoSquare = "\uF194"; + public const string Try = "\uF195"; + public const string PlusSquareO = "\uF196"; + public const string SpaceShuttle = "\uF197"; + public const string Slack = "\uF198"; + public const string EnvelopeSquare = "\uF199"; + public const string Wordpress = "\uF19A"; + public const string Openid = "\uF19B"; + public const string University = "\uF19C"; + public const string GraduationCap = "\uF19D"; + public const string Yahoo = "\uF19E"; + public const string Google = "\uF1A0"; + public const string Reddit = "\uF1A1"; + public const string RedditSquare = "\uF1A2"; + public const string StumbleuponCircle = "\uF1A3"; + public const string Stumbleupon = "\uF1A4"; + public const string Delicious = "\uF1A5"; + public const string Digg = "\uF1A6"; + public const string Drupal = "\uF1A9"; + public const string Joomla = "\uF1AA"; + public const string Language = "\uF1AB"; + public const string Fax = "\uF1AC"; + public const string Building = "\uF1AD"; + public const string Child = "\uF1AE"; + public const string Paw = "\uF1B0"; + public const string Spoon = "\uF1B1"; + public const string Cube = "\uF1B2"; + public const string Cubes = "\uF1B3"; + public const string Behance = "\uF1B4"; + public const string BehanceSquare = "\uF1B5"; + public const string Steam = "\uF1B6"; + public const string SteamSquare = "\uF1B7"; + public const string Recycle = "\uF1B8"; + public const string Car = "\uF1B9"; + public const string Taxi = "\uF1BA"; + public const string Tree = "\uF1BB"; + public const string Spotify = "\uF1BC"; + public const string Deviantart = "\uF1BD"; + public const string Soundcloud = "\uF1BE"; + public const string Database = "\uF1C0"; + public const string FilePdfO = "\uF1C1"; + public const string FileWordO = "\uF1C2"; + public const string FileExcelO = "\uF1C3"; + public const string FilePowerpointO = "\uF1C4"; + public const string FileImageO = "\uF1C5"; + public const string FileArchiveO = "\uF1C6"; + public const string FileAudioO = "\uF1C7"; + public const string FileVideoO = "\uF1C8"; + public const string FileCodeO = "\uF1C9"; + public const string Vine = "\uF1CA"; + public const string Codepen = "\uF1CB"; + public const string Jsfiddle = "\uF1CC"; + public const string LifeRing = "\uF1CD"; + public const string CircleONotch = "\uF1CE"; + public const string Rebel = "\uF1D0"; + public const string Empire = "\uF1D1"; + public const string GitSquare = "\uF1D2"; + public const string Git = "\uF1D3"; + public const string HackerNews = "\uF1D4"; + public const string TencentWeibo = "\uF1D5"; + public const string Qq = "\uF1D6"; + public const string Weixin = "\uF1D7"; + public const string PaperPlane = "\uF1D8"; + public const string PaperPlaneO = "\uF1D9"; + public const string History = "\uF1DA"; + public const string CircleThin = "\uF1DB"; + public const string Header = "\uF1DC"; + public const string Paragraph = "\uF1DD"; + public const string Sliders = "\uF1DE"; + public const string ShareAlt = "\uF1E0"; + public const string ShareAltSquare = "\uF1E1"; + public const string Bomb = "\uF1E2"; + public const string FutbolO = "\uF1E3"; + public const string Tty = "\uF1E4"; + public const string Binoculars = "\uF1E5"; + public const string Plug = "\uF1E6"; + public const string Slideshare = "\uF1E7"; + public const string Twitch = "\uF1E8"; + public const string Yelp = "\uF1E9"; + public const string NewspaperO = "\uF1EA"; + public const string Wifi = "\uF1EB"; + public const string Calculator = "\uF1EC"; + public const string Paypal = "\uF1ED"; + public const string GoogleWallet = "\uF1EE"; + public const string CcVisa = "\uF1F0"; + public const string CcMastercard = "\uF1F1"; + public const string CcDiscover = "\uF1F2"; + public const string CcAmex = "\uF1F3"; + public const string CcPaypal = "\uF1F4"; + public const string CcStripe = "\uF1F5"; + public const string BellSlash = "\uF1F6"; + public const string BellSlashO = "\uF1F7"; + public const string Trash = "\uF1F8"; + public const string Copyright = "\uF1F9"; + public const string At = "\uF1FA"; + public const string Eyedropper = "\uF1FB"; + public const string PaintBrush = "\uF1FC"; + public const string BirthdayCake = "\uF1FD"; + public const string AreaChart = "\uF1FE"; + public const string PieChart = "\uF200"; + public const string LineChart = "\uF201"; + public const string Lastfm = "\uF202"; + public const string LastfmSquare = "\uF203"; + public const string ToggleOff = "\uF204"; + public const string ToggleOn = "\uF205"; + public const string Bicycle = "\uF206"; + public const string Bus = "\uF207"; + public const string Ioxhost = "\uF208"; + public const string Angellist = "\uF209"; + public const string Cc = "\uF20A"; + public const string Ils = "\uF20B"; + public const string Meanpath = "\uF20C"; + public const string Buysellads = "\uF20D"; + public const string Connectdevelop = "\uF20E"; + public const string Dashcube = "\uF210"; + public const string Forumbee = "\uF211"; + public const string Leanpub = "\uF212"; + public const string Sellsy = "\uF213"; + public const string Shirtsinbulk = "\uF214"; + public const string Simplybuilt = "\uF215"; + public const string Skyatlas = "\uF216"; + public const string CartPlus = "\uF217"; + public const string CartArrowDown = "\uF218"; + public const string Diamond = "\uF219"; + public const string Ship = "\uF21A"; + public const string UserSecret = "\uF21B"; + public const string Motorcycle = "\uF21C"; + public const string StreetView = "\uF21D"; + public const string Heartbeat = "\uF21E"; + public const string Venus = "\uF221"; + public const string Mars = "\uF222"; + public const string Mercury = "\uF223"; + public const string Transgender = "\uF224"; + public const string TransgenderAlt = "\uF225"; + public const string VenusDouble = "\uF226"; + public const string MarsDouble = "\uF227"; + public const string VenusMars = "\uF228"; + public const string MarsStroke = "\uF229"; + public const string MarsStrokeV = "\uF22A"; + public const string MarsStrokeH = "\uF22B"; + public const string Neuter = "\uF22C"; + public const string Genderless = "\uF22D"; + public const string FacebookOfficial = "\uF230"; + public const string PinterestP = "\uF231"; + public const string Whatsapp = "\uF232"; + public const string Server = "\uF233"; + public const string UserPlus = "\uF234"; + public const string UserTimes = "\uF235"; + public const string Bed = "\uF236"; + public const string Viacoin = "\uF237"; + public const string Train = "\uF238"; + public const string Subway = "\uF239"; + public const string Medium = "\uF23A"; + public const string MediumSquare = "\uF2F8"; + public const string YCombinator = "\uF23B"; + public const string OptinMonster = "\uF23C"; + public const string Opencart = "\uF23D"; + public const string Expeditedssl = "\uF23E"; + public const string BatteryFull = "\uF240"; + public const string BatteryThreeQuarters = "\uF241"; + public const string BatteryHalf = "\uF242"; + public const string BatteryQuarter = "\uF243"; + public const string BatteryEmpty = "\uF244"; + public const string MousePointer = "\uF245"; + public const string ICursor = "\uF246"; + public const string ObjectGroup = "\uF247"; + public const string ObjectUngroup = "\uF248"; + public const string StickyNote = "\uF249"; + public const string StickyNoteO = "\uF24A"; + public const string CcJcb = "\uF24B"; + public const string CcDinersClub = "\uF24C"; + public const string Clone = "\uF24D"; + public const string BalanceScale = "\uF24E"; + public const string HourglassO = "\uF250"; + public const string HourglassStart = "\uF251"; + public const string HourglassHalf = "\uF252"; + public const string HourglassEnd = "\uF253"; + public const string Hourglass = "\uF254"; + public const string HandRockO = "\uF255"; + public const string HandPaperO = "\uF256"; + public const string HandScissorsO = "\uF257"; + public const string HandLizardO = "\uF258"; + public const string HandSpockO = "\uF259"; + public const string HandPointerO = "\uF25A"; + public const string HandPeaceO = "\uF25B"; + public const string Trademark = "\uF25C"; + public const string Registered = "\uF25D"; + public const string CreativeCommons = "\uF25E"; + public const string Gg = "\uF260"; + public const string GgCircle = "\uF261"; + public const string Tripadvisor = "\uF262"; + public const string Odnoklassniki = "\uF263"; + public const string OdnoklassnikiSquare = "\uF264"; + public const string GetPocket = "\uF265"; + public const string WikipediaW = "\uF266"; + public const string Safari = "\uF267"; + public const string Chrome = "\uF268"; + public const string Firefox = "\uF269"; + public const string Opera = "\uF26A"; + public const string InternetExplorer = "\uF26B"; + public const string Television = "\uF26C"; + public const string Contao = "\uF26D"; + public const string Num500px = "\uF26E"; + public const string Amazon = "\uF270"; + public const string CalendarPlusO = "\uF271"; + public const string CalendarMinusO = "\uF272"; + public const string CalendarTimesO = "\uF273"; + public const string CalendarCheckO = "\uF274"; + public const string Industry = "\uF275"; + public const string MapPin = "\uF276"; + public const string MapSigns = "\uF277"; + public const string MapO = "\uF278"; + public const string Map = "\uF279"; + public const string Commenting = "\uF27A"; + public const string CommentingO = "\uF27B"; + public const string Houzz = "\uF27C"; + public const string Vimeo = "\uF27D"; + public const string BlackTie = "\uF27E"; + public const string Fonticons = "\uF280"; + public const string RedditAlien = "\uF281"; + public const string Edge = "\uF282"; + public const string CreditCardAlt = "\uF283"; + public const string Codiepie = "\uF284"; + public const string Modx = "\uF285"; + public const string FortAwesome = "\uF286"; + public const string Usb = "\uF287"; + public const string ProductHunt = "\uF288"; + public const string Mixcloud = "\uF289"; + public const string Scribd = "\uF28A"; + public const string PauseCircle = "\uF28B"; + public const string PauseCircleO = "\uF28C"; + public const string StopCircle = "\uF28D"; + public const string StopCircleO = "\uF28E"; + public const string ShoppingBag = "\uF290"; + public const string ShoppingBasket = "\uF291"; + public const string Hashtag = "\uF292"; + public const string Bluetooth = "\uF293"; + public const string BluetoothB = "\uF294"; + public const string Percent = "\uF295"; + public const string Gitlab = "\uF296"; + public const string Wpbeginner = "\uF297"; + public const string Wpforms = "\uF298"; + public const string Envira = "\uF299"; + public const string UniversalAccess = "\uF29A"; + public const string WheelchairAlt = "\uF29B"; + public const string QuestionCircleO = "\uF29C"; + public const string Blind = "\uF29D"; + public const string AudioDescription = "\uF29E"; + public const string VolumeControlPhone = "\uF2A0"; + public const string Braille = "\uF2A1"; + public const string AssistiveListeningSystems = "\uF2A2"; + public const string AmericanSignLanguageInterpreting = "\uF2A3"; + public const string Deaf = "\uF2A4"; + public const string Glide = "\uF2A5"; + public const string GlideG = "\uF2A6"; + public const string SignLanguage = "\uF2A7"; + public const string LowVision = "\uF2A8"; + public const string Viadeo = "\uF2A9"; + public const string ViadeoSquare = "\uF2AA"; + public const string Snapchat = "\uF2AB"; + public const string SnapchatGhost = "\uF2AC"; + public const string SnapchatSquare = "\uF2AD"; + public const string FirstOrder = "\uF2B0"; + public const string Yoast = "\uF2B1"; + public const string Themeisle = "\uF2B2"; + public const string GooglePlusOfficial = "\uF2B3"; + public const string FontAwesome = "\uF2B4"; + public const string HandshakeO = "\uF2B5"; + public const string EnvelopeOpen = "\uF2B6"; + public const string EnvelopeOpenO = "\uF2B7"; + public const string Linode = "\uF2B8"; + public const string AddressBook = "\uF2B9"; + public const string AddressBookO = "\uF2BA"; + public const string AddressCard = "\uF2BB"; + public const string AddressCardO = "\uF2BC"; + public const string UserCircle = "\uF2BD"; + public const string UserCircleO = "\uF2BE"; + public const string UserO = "\uF2C0"; + public const string IdBadge = "\uF2C1"; + public const string IdCard = "\uF2C2"; + public const string IdCardO = "\uF2C3"; + public const string Quora = "\uF2C4"; + public const string FreeCodeCamp = "\uF2C5"; + public const string Telegram = "\uF2C6"; + public const string ThermometerFull = "\uF2C7"; + public const string ThermometerThreeQuarters = "\uF2C8"; + public const string ThermometerHalf = "\uF2C9"; + public const string ThermometerQuarter = "\uF2CA"; + public const string ThermometerEmpty = "\uF2CB"; + public const string Shower = "\uF2CC"; + public const string Bath = "\uF2CD"; + public const string Podcast = "\uF2CE"; + public const string WindowMaximize = "\uF2D0"; + public const string WindowMinimize = "\uF2D1"; + public const string WindowRestore = "\uF2D2"; + public const string WindowClose = "\uF2D3"; + public const string WindowCloseO = "\uF2D4"; + public const string Bandcamp = "\uF2D5"; + public const string Grav = "\uF2D6"; + public const string Etsy = "\uF2D7"; + public const string Imdb = "\uF2D8"; + public const string Ravelry = "\uF2D9"; + public const string Eercast = "\uF2DA"; + public const string Microchip = "\uF2DB"; + public const string SnowflakeO = "\uF2DC"; + public const string Superpowers = "\uF2DD"; + public const string Wpexplorer = "\uF2DE"; + public const string Meetup = "\uF2E0"; + public const string Mastodon = "\uF2E1"; + public const string MastodonAlt = "\uF2E2"; + public const string ForkAwesomeIcon = "\uF2E3"; + public const string Peertube = "\uF2E4"; + public const string Diaspora = "\uF2E5"; + public const string Friendica = "\uF2E6"; + public const string GnuSocial = "\uF2E7"; + public const string LiberapaySquare = "\uF2E8"; + public const string Liberapay = "\uF2E9"; + public const string Scuttlebutt = "\uF2EA"; + public const string Hubzilla = "\uF2EB"; + public const string SocialHome = "\uF2EC"; + public const string Artstation = "\uF2ED"; + public const string Discord = "\uF2EE"; + public const string DiscordAlt = "\uF2EF"; + public const string Patreon = "\uF2F0"; + public const string Snowdrift = "\uF2F1"; + public const string Activitypub = "\uF2F2"; + public const string Ethereum = "\uF2F3"; + public const string Keybase = "\uF2F4"; + public const string Shaarli = "\uF2F5"; + public const string ShaarliO = "\uF2F6"; + public const string KeyModern = "\uF2F7"; + public const string Xmpp = "\uF2F9"; + public const string ArchiveOrg = "\uF2FC"; + public const string Freedombox = "\uF2FD"; + public const string FacebookMessenger = "\uF2FE"; + public const string Debian = "\uF2FF"; + public const string MastodonSquare = "\uF300"; + public const string Tipeee = "\uF301"; + public const string React = "\uF302"; + public const string Dogmazic = "\uF303"; + public const string Zotero = "\uF309"; + public const string Nodejs = "\uF308"; + public const string Nextcloud = "\uF306"; + public const string NextcloudSquare = "\uF307"; + public const string Hackaday = "\uF30A"; + public const string Laravel = "\uF30B"; + public const string Signalapp = "\uF30C"; + public const string Gnupg = "\uF30D"; + public const string Php = "\uF30E"; + public const string Ffmpeg = "\uF30F"; + public const string Joplin = "\uF310"; + public const string Syncthing = "\uF311"; + public const string Inkscape = "\uF312"; + public const string MatrixOrg = "\uF313"; + public const string Pixelfed = "\uF314"; + public const string Bootstrap = "\uF315"; + public const string DevTo = "\uF316"; + public const string Hashnode = "\uF317"; + public const string Jirafeau = "\uF318"; + public const string Emby = "\uF319"; + public const string Wikidata = "\uF31A"; + public const string Gimp = "\uF31B"; + public const string C = "\uF31C"; + public const string Digitalocean = "\uF31D"; + public const string Att = "\uF31E"; + public const string Gitea = "\uF31F"; + public const string FileEpub = "\uF321"; + public const string Python = "\uF322"; + public const string Archlinux = "\uF323"; + public const string Pleroma = "\uF324"; + public const string Unsplash = "\uF325"; + public const string Hackster = "\uF326"; + public const string SpellCheck = "\uF327"; + public const string Moon = "\uF328"; + public const string Sun = "\uF329"; + public const string FDroid = "\uF32A"; + public const string Biometric = "\uF32B"; + public const string Wire = "\uF32C"; + public const string TorOnion = "\uF32E"; + public const string VolumeMute = "\uF32F"; + public const string BellRinging = "\uF32D"; + public const string BellRingingO = "\uF330"; + public const string Hal = "\uF333"; + public const string Jupyter = "\uF335"; + public const string Julia = "\uF334"; + public const string Classicpress = "\uF331"; + public const string ClassicpressCircle = "\uF332"; + public const string OpenCollective = "\uF336"; + public const string Orcid = "\uF337"; + public const string Researchgate = "\uF338"; + public const string Funkwhale = "\uF339"; + public const string Askfm = "\uF33A"; + public const string Blockstack = "\uF33B"; + public const string Boardgamegeek = "\uF33C"; + public const string Bunny = "\uF35F"; + public const string Buymeacoffee = "\uF33D"; + public const string CcBy = "\uF33E"; + public const string CcCc = "\uF33F"; + public const string CcNcEu = "\uF341"; + public const string CcNcJp = "\uF342"; + public const string CcNc = "\uF340"; + public const string CcNd = "\uF343"; + public const string CcPd = "\uF344"; + public const string CcRemix = "\uF345"; + public const string CcSa = "\uF346"; + public const string CcShare = "\uF347"; + public const string CcZero = "\uF348"; + public const string ConwayGlider = "\uF349"; + public const string Csharp = "\uF34A"; + public const string EmailBulk = "\uF34B"; + public const string EmailBulkO = "\uF34C"; + public const string Gnu = "\uF34D"; + public const string GooglePlay = "\uF34E"; + public const string Heroku = "\uF34F"; + public const string HomeAssistant = "\uF350"; + public const string Java = "\uF351"; + public const string Mariadb = "\uF352"; + public const string Markdown = "\uF353"; + public const string Mysql = "\uF354"; + public const string Nordcast = "\uF355"; + public const string Plume = "\uF356"; + public const string Postgresql = "\uF357"; + public const string SassAlt = "\uF359"; + public const string Sass = "\uF358"; + public const string Skate = "\uF35A"; + public const string Sketchfab = "\uF35B"; + public const string Tex = "\uF35C"; + public const string Textpattern = "\uF35D"; + public const string Unity = "\uF35E"; + public const string Hedgedoc = "\uF360"; + public const string Fediverse = "\uF361"; + public const string Proftpd = "\uF362"; + public const string Osi = "\uF363"; + public const string Eyeem = "\uF364"; + public const string EyeemO = "\uF365"; + public const string Codeberg = "\uF366"; + public const string Discourse = "\uF367"; + public const string Mumble = "\uF368"; + public const string Freedesktop = "\uF369"; + public const string Javascript = "\uF370"; + public const string Lemmy = "\uF371"; + public const string Ipfs = "\uF372"; + public const string Canonical = "\uF36A"; + public const string Ubuntu = "\uF36B"; +} diff --git a/src/gaemstone.Client/Utility/ImGuiUtility.cs b/src/gaemstone.Client/Utility/ImGuiUtility.cs new file mode 100644 index 0000000..92f5283 --- /dev/null +++ b/src/gaemstone.Client/Utility/ImGuiUtility.cs @@ -0,0 +1,55 @@ +using System.Numerics; +using ImGuiNET; +using ImGuiInternal = ImGuiNET.Internal.ImGui; + +namespace gaemstone.Client.Utility; + +public static class ImGuiUtility +{ + public static bool UIButtonToggle(int index, string label, string tooltip, ref bool enabled) + { if (UIButton(index, label, tooltip, enabled)) enabled = !enabled; return enabled; } + public static bool UIButton(int index, string label, string tooltip, bool active) + { + var start = new Vector2(4, 4); + var buttonSize = new Vector2(32, 32); + var pos = start + new Vector2(buttonSize.X + 4, 0) * index; + + ImGui.SetNextWindowPos(pos, ImGuiCond.Always); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); + ImGui.Begin("UIToggle" + index, + ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoBackground | + ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | + ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoDocking); + ImGui.PopStyleVar(); + + if (!active) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.6f); + var clicked = ImGui.Button(label, buttonSize); + if (!active) ImGui.PopStyleVar(); + if (ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); + + ImGui.End(); + return clicked; + } + + // public static void AdvancedTooltip(string brief, string detail) + // { + // if (!ImGui.IsItemHovered()) return; + // ImGui.BeginTooltip(); + // ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]); + // ImGui.TextUnformatted(brief); + // ImGui.PopFont(); + // ImGui.Text(detail); + // ImGui.EndTooltip(); + // } + + public static void TextCentered(string text, Vector2 size) + { + var pos = ImGui.GetCursorScreenPos(); + ImGuiInternal.ItemSize(size, 0); + if (!ImGuiInternal.ItemAdd(new() { Min = pos, Max = pos + size }, 0)) return; + + var textSize = ImGui.CalcTextSize(text); + var textPos = pos + (size - textSize) / 2; + ImGuiInternal.RenderText(textPos, text); + } +} diff --git a/src/gaemstone.Client/gaemstone.Client.csproj b/src/gaemstone.Client/gaemstone.Client.csproj index fb6d528..9965755 100644 --- a/src/gaemstone.Client/gaemstone.Client.csproj +++ b/src/gaemstone.Client/gaemstone.Client.csproj @@ -3,9 +3,9 @@ preview net7.0 + true disable enable - true @@ -13,8 +13,9 @@ - + + diff --git a/src/gaemstone.ECS b/src/gaemstone.ECS new file mode 160000 index 0000000..f9fa808 --- /dev/null +++ b/src/gaemstone.ECS @@ -0,0 +1 @@ +Subproject commit f9fa808d6842f5b208bbdfa1925f156342ed8a60 diff --git a/src/gaemstone/Components/TransformComponents.cs b/src/gaemstone/Components/TransformComponents.cs index d536c64..5ef5736 100644 --- a/src/gaemstone/Components/TransformComponents.cs +++ b/src/gaemstone/Components/TransformComponents.cs @@ -1,17 +1,17 @@ +using System.Numerics; using gaemstone.ECS; -using Silk.NET.Maths; namespace gaemstone.Components; [Module] public class TransformComponents { - [Component] + [Symbol, Component] public struct GlobalTransform { - public Matrix4X4 Value; - public GlobalTransform(Matrix4X4 value) => Value = value; - public static implicit operator GlobalTransform(in Matrix4X4 value) => new(value); - public static implicit operator Matrix4X4(in GlobalTransform index) => index.Value; + public Matrix4x4 Value; + public GlobalTransform(Matrix4x4 value) => Value = value; + public static implicit operator GlobalTransform(in Matrix4x4 value) => new(value); + public static implicit operator Matrix4x4(in GlobalTransform index) => index.Value; } } diff --git a/src/gaemstone/Doc.cs b/src/gaemstone/Doc.cs new file mode 100644 index 0000000..6534099 --- /dev/null +++ b/src/gaemstone/Doc.cs @@ -0,0 +1,86 @@ +using System; +using gaemstone.ECS; +using static gaemstone.Flecs.Core; + +namespace gaemstone; + +[Module] +public class Doc +{ + [Tag] + public struct DisplayType { } + + [Tag] + public struct Relation { } + + + // TODO: These need to actually be read at some point. + + /// + /// A display name for this entity. + /// Names in the entity hierarchy must be unique within the parent entity, + /// This doesn't apply to display names - they are mostly informational. + /// Displayed in the Entity Inspector. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] + public class Name : Attribute + { + public string Value { get; } + public Name(string value) => Value = value; + } + + /// + /// A brief description of this entity. + /// Displayed in the Entity Inspector. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] + public class Brief : Attribute + { + public string Value { get; } + public Brief(string value) => Value = value; + } + + /// + /// A detailed description, or full documentation, of this entity's purpose and behaviors. + /// It's encouraged to use multiple paragraphs and markdown formatting if necessary. + /// Displayed in the Entity Inspector. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] + public class Detail : Attribute + { + public string Value { get; } + public Detail(string value) => Value = value; + } + + /// + /// A link to a website relating to this entity, such as + /// a module's repository, or further documentation. + /// Displayed in the Entity Inspector. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] + public class Link : Attribute + { + public string Value { get; } + public Link(string value) => Value = value; + } + + /// + /// A custom color to represent this entity. + /// Displayed in the Entity Inspector. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] + public class Color : Attribute + { + public float Red { get; } + public float Green { get; } + public float Blue { get; } + + public Color(float red, float green, float blue) + { + if ((red < 0.0f) || (red > 1.0f)) throw new ArgumentOutOfRangeException(nameof(red )); + if ((green < 0.0f) || (green > 1.0f)) throw new ArgumentOutOfRangeException(nameof(green)); + if ((blue < 0.0f) || (blue > 1.0f)) throw new ArgumentOutOfRangeException(nameof(blue )); + Red = red; Green = green; Blue = blue; + } + } +} diff --git a/src/gaemstone/ECS/Attributes.cs b/src/gaemstone/ECS/Attributes.cs index e59b81b..3ccf709 100644 --- a/src/gaemstone/ECS/Attributes.cs +++ b/src/gaemstone/ECS/Attributes.cs @@ -7,21 +7,43 @@ namespace gaemstone.ECS; /// When present on an attribute attached to a type that's part of a module /// being registered automatically through , /// an entity is automatically created and -/// called on it, meaning it can be looked up using . +/// called on it, meaning it can be looked up using . /// public interface ICreateEntityAttribute { } +[AttributeUsage(AttributeTargets.Struct)] +public class EntityAttribute : Attribute, ICreateEntityAttribute { } + +/// +/// Use a custom name or path for this entity instead of the type's name. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] +public class PathAttribute : Attribute, ICreateEntityAttribute +{ + public string Value { get; } + public PathAttribute(string value) => Value = value; +} + +/// +/// Register the entity under a globally unique symbol. +/// Uses the type's name by default. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] +public class SymbolAttribute : Attribute, ICreateEntityAttribute +{ + public string? Value { get; } + public SymbolAttribute() { } + public SymbolAttribute(string value) => Value = value; +} + /// /// A singleton is a single instance of a tag or component that can be retrieved /// without explicitly specifying an entity in a query, where it is equivalent /// to with itself as the generic type parameter. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] -public class SingletonAttribute : Attribute -{ - /// Whether to add the entity to itself on registration. - public bool AutoAdd { get; init; } = false; -} +public class SingletonAttribute : Attribute, ICreateEntityAttribute + { public bool AutoAdd { get; init; } = true; } /// /// Register the proxied type instead of the one marked with this attribute. @@ -46,6 +68,25 @@ public class AddAttribute : AddEntityAttribute public class AddAttribute : AddRelationAttribute { public AddAttribute() : base(typeof(TRelation), typeof(TTarget)) { } } +/// +/// Marked entity represents a relationship type. +/// It may be used as the "relation" in a pair. +/// +/// +/// The relationship may have component data associated with +/// it when added to an entity under these circumstances: +/// +/// If marked as a , does not carry data. +/// If marked as a , carries the relation's data. +/// If marked with neither, will carry the target's data, if it's a component. +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] +public class RelationAttribute : Attribute, ICreateEntityAttribute { } + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] +public class ComponentAttribute : Attribute, ICreateEntityAttribute { } + /// [AttributeUsage(AttributeTargets.Struct)] public class TagAttribute : AddAttribute, ICreateEntityAttribute { } diff --git a/src/gaemstone/ECS/Component.cs b/src/gaemstone/ECS/Component.cs deleted file mode 100644 index 1e66053..0000000 --- a/src/gaemstone/ECS/Component.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] -public class ComponentAttribute : EntityAttribute -{ - public ComponentAttribute() { } - public ComponentAttribute(params string[] path) : base(path) { } -} - -public static class ComponentExtensions -{ - public unsafe static EntityRef CreateComponent(this EntityRef entity) - => entity.CreateComponent(typeof(T)); - public unsafe static EntityRef CreateComponent(this EntityRef entity, Type type) - { - // TODO: Do some additional sanity checking for this type. - - var typeInfo = default(ecs_type_info_t); - if (type.IsPrimitive) throw new ArgumentException( - "Must not be primitive", nameof(type)); - var wrapper = TypeWrapper.For(type); - if (type.IsValueType && !wrapper.IsUnmanaged) throw new Exception( - "Struct component must satisfy the unmanaged constraint. " + - "Consider making it a class if you need to store references."); - typeInfo.size = wrapper.Size; - typeInfo.alignment = wrapper.Size; - - var desc = new ecs_component_desc_t { entity = entity, type = typeInfo }; - ecs_component_init(entity.Universe, &desc); - return entity.CreateLookup(type); - } -} diff --git a/src/gaemstone/ECS/ComponentHooks.cs b/src/gaemstone/ECS/ComponentHooks.cs deleted file mode 100644 index 427add4..0000000 --- a/src/gaemstone/ECS/ComponentHooks.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -// FIXME: This API is flawed. -public unsafe class ComponentHooks -{ - public EntityRef Entity { get; } - public ecs_type_hooks_t* Handle { get; } - - private Action? _onAdd; - public event Action? OnAdd { - add { if ((_onAdd += value) != null) Handle->on_add.Data.Pointer = &CallbackOnAdd; } - remove { if ((_onAdd -= value) == null) Handle->on_add.Data.Pointer = null; } - } - private Action? _onSet; - public event Action? OnSet { - add { if ((_onSet += value) != null) Handle->on_set.Data.Pointer = &CallbackOnSet; } - remove { if ((_onSet -= value) == null) Handle->on_set.Data.Pointer = null; } - } - private Action? _onRemove; - public event Action? OnRemove { - add { if ((_onRemove += value) != null) Handle->on_remove.Data.Pointer = &CallbackOnRemove; } - remove { if ((_onRemove -= value) == null) Handle->on_remove.Data.Pointer = null; } - } - - private Action? _constructor; - public event Action? Construct { - add { if ((_constructor += value) != null) Handle->ctor.Data.Pointer = &CallbackConstructor; } - remove { if ((_constructor -= value) == null) Handle->ctor.Data.Pointer = null; } - } - private Action? _destructor; - public event Action? Destruct { - add { if ((_destructor += value) != null) Handle->dtor.Data.Pointer = &CallbackDestructor; } - remove { if ((_destructor -= value) == null) Handle->dtor.Data.Pointer = null; } - } - private Action? _copy; - public event Action? Copy { - add { if ((_copy += value) != null) Handle->copy.Data.Pointer = &CallbackCopy; } - remove { if ((_copy -= value) == null) Handle->copy.Data.Pointer = null; } - } - private Action? _move; - public event Action? Move { - add { if ((_move += value) != null) Handle->move.Data.Pointer = &CallbackMove; } - remove { if ((_move -= value) == null) Handle->move.Data.Pointer = null; } - } - - internal ComponentHooks(EntityRef entity, ecs_type_hooks_t* handle) - { Entity = entity; Handle = handle; } - - - private static ComponentHooks Get(void* ptr) - => CallbackContextHelper.Get((nint)ptr); - - [UnmanagedCallersOnly] private static void CallbackOnAdd(ecs_iter_t* it) - { var hooks = Get(it->binding_ctx); hooks._onAdd?.Invoke(new(hooks.Entity.Universe, null, *it)); } - [UnmanagedCallersOnly] private static void CallbackOnSet(ecs_iter_t* it) - { var hooks = Get(it->binding_ctx); hooks._onSet?.Invoke(new(hooks.Entity.Universe, null, *it)); } - [UnmanagedCallersOnly] private static void CallbackOnRemove(ecs_iter_t* it) - { var hooks = Get(it->binding_ctx); hooks._onRemove?.Invoke(new(hooks.Entity.Universe, null, *it)); } - - [UnmanagedCallersOnly] private static void CallbackConstructor(void* ptr, int count, ecs_type_info_t* type) - => Get(type->hooks.binding_ctx)._constructor?.Invoke((nint)ptr, count); - [UnmanagedCallersOnly] private static void CallbackDestructor(void* ptr, int count, ecs_type_info_t* type) - => Get(type->hooks.binding_ctx)._destructor?.Invoke((nint)ptr, count); - [UnmanagedCallersOnly] private static void CallbackCopy(void* dest, void* src, int count, ecs_type_info_t* type) - => Get(type->hooks.binding_ctx)._copy?.Invoke((nint)dest, (nint)src, count); - [UnmanagedCallersOnly] private static void CallbackMove(void* dest, void* src, int count, ecs_type_info_t* type) - => Get(type->hooks.binding_ctx)._move?.Invoke((nint)dest, (nint)src, count); -} - -public unsafe static class ComponentHooksExtensions -{ - public static ComponentHooks GetComponentHooks(this EntityRef entity) - { - var handle = ecs_get_hooks_id(entity.Universe, entity); - if (handle == null) throw new ArgumentException($"No type info found for {entity}"); - - if (handle->binding_ctx == null) { - var hooks = new ComponentHooks(entity, handle); - handle->binding_ctx = (void*)CallbackContextHelper.Create(hooks); - return hooks; - } else { - return CallbackContextHelper.Get((nint)handle->binding_ctx); - } - } -} diff --git a/src/gaemstone/ECS/Entity.cs b/src/gaemstone/ECS/Entity.cs deleted file mode 100644 index b64df7d..0000000 --- a/src/gaemstone/ECS/Entity.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -[AttributeUsage(AttributeTargets.Struct)] -public class EntityAttribute : Attribute, ICreateEntityAttribute -{ - /// If specified, uses this path instead of the default name. - public string[]? Path { get; } - - /// If true, the path will be absolute instead of relative. - public bool Global { get; init; } - - public EntityAttribute() { } - public EntityAttribute(params string[] path) - { - if (path.Length == 0) throw new ArgumentException( - "Path must not be empty", nameof(path)); - Path = path; - } -} - -public readonly struct Entity - : IEquatable -{ - public static readonly Entity None = default; - - public readonly ecs_entity_t Value; - public uint ID => (uint)Value.Data; - - public bool IsSome => Value.Data != 0; - public bool IsNone => Value.Data == 0; - - public Entity(ecs_entity_t value) => Value = value; - - public bool Equals(Entity other) => Value.Data == other.Value.Data; - public override bool Equals(object? obj) => (obj is Entity other) && Equals(other); - public override int GetHashCode() => Value.Data.GetHashCode(); - public override string? ToString() => $"Entity(0x{Value.Data.Data:X})"; - - public static bool operator ==(Entity left, Entity right) => left.Equals(right); - public static bool operator !=(Entity left, Entity right) => !left.Equals(right); - - public static implicit operator ecs_entity_t(Entity e) => e.Value; - public static implicit operator Identifier(Entity e) => new(e.Value.Data); - public static implicit operator ecs_id_t(Entity e) => e.Value.Data; -} diff --git a/src/gaemstone/ECS/EntityBase.cs b/src/gaemstone/ECS/EntityBase.cs deleted file mode 100644 index a494627..0000000 --- a/src/gaemstone/ECS/EntityBase.cs +++ /dev/null @@ -1,48 +0,0 @@ -using static gaemstone.Flecs.Core; - -namespace gaemstone.ECS; - -public abstract class EntityBase -{ - public abstract Universe Universe { get; } - - public abstract TReturn Add(Identifier id); - public abstract TReturn Remove(Identifier id); - public abstract bool Has(Identifier id); - - public abstract T Get(); - public abstract T? MaybeGet() where T : unmanaged; - public abstract T? MaybeGet(T _ = null!) where T : class; - public abstract ref T GetMut() where T : unmanaged; - public abstract ref T GetRefOrNull() where T : unmanaged; - public abstract ref T GetRefOrThrow() where T : unmanaged; - public abstract void Modified(); - - public abstract TReturn Set(in T value) where T : unmanaged; - public abstract TReturn Set(T obj) where T : class; - - public TReturn Add(string symbol) => Add(Universe.LookupSymbolOrThrow(symbol)); - public TReturn Add() => Add(Universe.LookupOrThrow(typeof(T))); - public TReturn Add(Entity relation, Entity target) => Add(Identifier.Pair(relation, target)); - public TReturn Add(Entity target) => Add(Universe.LookupOrThrow(), target); - public TReturn Add() => Add(Universe.LookupOrThrow(), Universe.LookupOrThrow()); - - public TReturn Remove(string symbol) => Remove(Universe.LookupSymbolOrThrow(symbol)); - public TReturn Remove() => Remove(Universe.LookupOrThrow(typeof(T))); - public TReturn Remove(Entity relation, Entity target) => Remove(Identifier.Pair(relation, target)); - public TReturn Remove(Entity target) => Remove(Universe.LookupOrThrow(), target); - public TReturn Remove() => Remove(Universe.LookupOrThrow(), Universe.LookupOrThrow()); - - public bool Has(string symbol) => Has(Universe.LookupSymbolOrThrow(symbol)); - public bool Has() => Has(Universe.LookupOrThrow(typeof(T))); - public bool Has(Entity relation, Entity target) => Has(Identifier.Pair(relation, target)); - public bool Has(Entity target) => Has(Universe.LookupOrThrow(), target); - public bool Has() => Has(Universe.LookupOrThrow(), Universe.LookupOrThrow()); - - public TReturn ChildOf(Entity parent) => Add(parent); - public TReturn ChildOf() => Add(); - - public TReturn Disable() => Add(); - public TReturn Enable() => Remove(); - public bool IsDisabled => Has(); -} diff --git a/src/gaemstone/ECS/EntityBuilder.cs b/src/gaemstone/ECS/EntityBuilder.cs deleted file mode 100644 index be80477..0000000 --- a/src/gaemstone/ECS/EntityBuilder.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public class EntityBuilder - : EntityBase -{ - public override Universe Universe { get; } - - /// Set to modify existing entity (optional). - public Entity ID { get; set; } - - /// - /// Path of the entity. If no entity is provided, an entity with this path - /// will be looked up first. When an entity is provided, the path will be - /// verified with the existing entity. - /// - public EntityPath? Path { get; set; } - - /// - /// Optional entity symbol. A symbol is an unscoped identifier that can - /// be used to lookup an entity. The primary use case for this is to - /// associate the entity with a language identifier, such as a type or - /// function name, where these identifiers differ from the name they are - /// registered with in flecs. - /// - public EntityBuilder Symbol(string symbol) { _symbol = symbol; return this; } - private string? _symbol = null; - - /// - /// When set to true, a low id (typically reserved for components) - /// will be used to create the entity, if no id is specified. - /// - public bool UseLowID { get; set; } - - /// IDs to add to the new or existing entity. - private readonly HashSet _toAdd = new(); - private Entity _parent = Entity.None; - - /// String expression with components to add. - public string? Expression { get; } - - /// Actions to run once the entity has been created. - private readonly List> _toSet = new(); - - public EntityBuilder(Universe universe, EntityPath? path = null) - { Universe = universe; Path = path; } - - public override EntityBuilder Add(Identifier id) - { - // If adding a ChildOf relation, store the parent separately. - if (id.AsPair(Universe) is (EntityRef relation, EntityRef target) && - (relation == Universe.ChildOf)) { _parent = target; return this; } - - if (_toAdd.Count == 31) throw new NotSupportedException( - "Must not add more than 31 IDs at once with EntityBuilder"); - _toAdd.Add(id); - - return this; - } - public override EntityBuilder Remove(Identifier id) - => throw new NotSupportedException(); - public override bool Has(Identifier id) - => !ecs_id_is_wildcard(id) ? _toAdd.Contains(id) - : throw new NotSupportedException(); // TODO: Support wildcard. - - public override T Get() => throw new NotSupportedException(); - public override T? MaybeGet() => throw new NotSupportedException(); - public override T? MaybeGet(T _ = null!) where T : class => throw new NotSupportedException(); - public override ref T GetMut() => throw new NotSupportedException(); - public override ref T GetRefOrNull() => throw new NotSupportedException(); - public override ref T GetRefOrThrow() => throw new NotSupportedException(); - public override void Modified() => throw new NotImplementedException(); - - public override EntityBuilder Set(in T value) - // "in" can't be used with lambdas, so we make a local copy. - { var copy = value; _toSet.Add(e => e.Set(copy)); return this; } - - public override EntityBuilder Set(T obj) - { _toSet.Add(e => e.Set(obj)); return this; } - - public unsafe EntityRef Build() - { - var parent = _parent; - - if (Path != null) { - if (parent.IsSome && Path.IsAbsolute) throw new InvalidOperationException( - "Entity already has parent set (via ChildOf), so path must not be absolute"); - // If path specifies more than just a name, ensure the parent entity exists. - if (Path.Count > 1) parent = EntityPath.EnsureEntityExists(Universe, parent, Path.Parent!); - } - - using var alloc = TempAllocator.Use(); - var desc = new ecs_entity_desc_t { - id = ID, - name = (Path != null) ? alloc.AllocateCString(Path.Name.AsSpan()) : default, - symbol = alloc.AllocateCString(_symbol), - add_expr = alloc.AllocateCString(Expression), - use_low_id = UseLowID, - sep = CStringExtensions.ETX, - }; - - var add = desc.add; var index = 0; - if (parent.IsSome) add[index++] = Identifier.Pair(Universe.ChildOf, parent); - foreach (var id in _toAdd) add[index++] = id; - - var entityID = ecs_entity_init(Universe, &desc); - var entity = new EntityRef(Universe, new(entityID)); - foreach (var action in _toSet) action(entity); - - return entity; - } -} diff --git a/src/gaemstone/ECS/EntityPath.cs b/src/gaemstone/ECS/EntityPath.cs deleted file mode 100644 index 48f7ce1..0000000 --- a/src/gaemstone/ECS/EntityPath.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public class EntityPath -{ - private readonly byte[][] _parts; - - public bool IsAbsolute { get; } - public bool IsRelative => !IsAbsolute; - public int Count => _parts.Length; - - public UTF8View Name => this[^1]; - public EntityPath? Parent => (Count > 1) ? new(IsAbsolute, _parts[..^1]) : null; - - public UTF8View this[int index] - => (index >= 0 && index < Count) ? new(_parts[index].AsSpan()[..^1]) - : throw new ArgumentOutOfRangeException(nameof(index)); - - public EntityPath this[Range range] - => new(IsAbsolute && (range.GetOffsetAndLength(Count).Offset == 0), _parts[range]); - - internal EntityPath(bool absolute, params byte[][] parts) - { - if (parts.Length == 0) throw new ArgumentException( - "Must have at least one part", nameof(parts)); - IsAbsolute = absolute; - _parts = parts; - } - - public EntityPath(params string[] parts) - : this(false, parts) { } - public EntityPath(bool absolute, params string[] parts) - : this(absolute, parts.Select(part => { - if (GetNameValidationError(part) is string error) - throw new ArgumentException(error); - var byteCount = Encoding.UTF8.GetByteCount(part); - // Includes NUL character at the end of bytes. - var bytes = new byte[byteCount + 1]; - Encoding.UTF8.GetBytes(part, bytes); - return bytes; - }).ToArray()) { } - - public static bool TryParse(string str, [NotNullWhen(true)] out EntityPath? result) - { - result = null; - if (str.Length == 0) return false; - - var strSpan = str.AsSpan(); - var isAbsolute = (str[0] == '/'); - if (isAbsolute) strSpan = strSpan[1..]; - - var numSeparators = 0; - foreach (var chr in strSpan) if (chr == '/') numSeparators++; - - var index = 0; - var parts = new byte[numSeparators + 1][]; - foreach (var part in strSpan.Split('/')) { - if (GetNameValidationError(part) != null) return false; - var byteCount = Encoding.UTF8.GetByteCount(part); - // Includes NUL character at the end of bytes. - var bytes = new byte[byteCount + 1]; - Encoding.UTF8.GetBytes(part, bytes); - parts[index++] = bytes; - } - - result = new(isAbsolute, parts); - return true; - } - - public static EntityPath Parse(string str) - { - if (str.Length == 0) throw new ArgumentException( - "String must not be empty", nameof(str)); - var parts = str.Split('/'); - // If string starts with a slash, first part will be empty, so create an absolute path. - return (parts[0].Length == 0) ? new(true, parts[1..]) : new(parts); - } - - public static string? GetNameValidationError(ReadOnlySpan name) - { - if (name.Length == 0) return "Must not be empty"; - // NOTE: This is a hopefully straightforward way to also prevent "." - // and ".." to be part of paths which may access the file system. - if (name[0] == '.') return "Must not begin with a dot"; - foreach (var chr in name) if (char.IsControl(chr)) - return "Must not contain contol characters"; - return null; - } - - // private static readonly Rune[] _validRunes = { (Rune)'-', (Rune)'.', (Rune)'_' }; - // private static readonly UnicodeCategory[] _validCategories = { - // UnicodeCategory.LowercaseLetter, UnicodeCategory.UppercaseLetter, - // UnicodeCategory.OtherLetter, UnicodeCategory.DecimalDigitNumber }; - - // private static void ValidateRune(Rune rune) - // { - // if (!_validRunes.Contains(rune) && !_validCategories.Contains(Rune.GetUnicodeCategory(rune))) - // throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character"); - // } - - public string[] GetParts() - { - var result = new string[Count]; - for (var i = 0; i < Count; i++) result[i] = this[i]; - return result; - } - - public override string ToString() - { - var builder = new StringBuilder(); - if (IsAbsolute) builder.Append('/'); - foreach (var part in this) builder.Append(part).Append('/'); - return builder.ToString(0, builder.Length - 1); - } - - public static implicit operator EntityPath(string path) => Parse(path); - - public Enumerator GetEnumerator() => new(this); - public ref struct Enumerator - { - private readonly EntityPath _path; - private int index = -1; - public UTF8View Current => _path[index]; - internal Enumerator(EntityPath path) => _path = path; - public bool MoveNext() => (++index < _path.Count); - } - - - internal static unsafe Entity Lookup(Universe universe, Entity parent, EntityPath path) - { - // If path is absolute, ignore parent and start at root. - if (path.IsAbsolute) parent = default; - // Otherwise, if no parent is specified, use the current scope. - else if (parent.IsNone) parent = new(ecs_get_scope(universe)); - - foreach (var part in path) - fixed (byte* ptr = part.AsSpan()) { - // FIXME: This breaks when using large entity IDs. - parent = new(ecs_lookup_child(universe, parent, ptr)); - if (parent.IsNone || !ecs_is_alive(universe, parent)) - return Entity.None; - } - - return parent; - } - - /// Used by . - internal static unsafe Entity EnsureEntityExists( - Universe universe, Entity parent, EntityPath path) - { - // If no parent is specified and path is relative, use the current scope. - if (parent.IsNone && path.IsRelative) parent = new(ecs_get_scope(universe)); - - var skipLookup = parent.IsNone; - foreach (var part in path) - fixed (byte* ptr = part.AsSpan()) - if (skipLookup || (parent = new(ecs_lookup_child(universe, parent, ptr))).IsNone) { - var desc = new ecs_entity_desc_t { name = ptr, sep = CStringExtensions.ETX }; - if (parent.IsSome) desc.add[0] = Identifier.Pair(universe.ChildOf, parent); - parent = new(ecs_entity_init(universe, &desc)); - skipLookup = true; - } - - return parent; - } -} - -public static class EntityPathExtensions -{ - public static unsafe EntityPath GetFullPath(this EntityRef entity) - { - var current = (Entity)entity; - var parts = new List(32); - - do { - var name = ecs_get_name(entity.Universe, current).FlecsToBytes(); - if (name != null) parts.Add(name); - else { - // If name is not set, use the numeric ID, instead. - var id = current.ID.ToString(); - var bytes = new byte[Encoding.UTF8.GetByteCount(id) + 1]; - Encoding.UTF8.GetBytes(id, bytes); - parts.Add(bytes); - } - } while ((current = new(ecs_get_target(entity.Universe, current, EcsChildOf, 0))).IsSome); - - parts.Reverse(); - return new(true, parts.ToArray()); - } -} - -public readonly ref struct UTF8View -{ - private readonly ReadOnlySpan _bytes; - public UTF8View(ReadOnlySpan bytes) - => _bytes = bytes; - - public int Length => _bytes.Length; - public byte this[int index] => _bytes[index]; - - public ReadOnlySpan AsSpan() => _bytes; - public override string ToString() => Encoding.UTF8.GetString(_bytes); - public static implicit operator string(UTF8View view) => view.ToString(); - - public Enumerator GetEnumerator() => new(_bytes); - public ref struct Enumerator - { - private readonly ReadOnlySpan _bytes; - private int index = 0; - private Rune _current = default; - public Rune Current => _current; - - internal Enumerator(ReadOnlySpan bytes) - => _bytes = bytes; - - public bool MoveNext() - { - if (index >= _bytes.Length) return false; - if (Rune.DecodeFromUtf8(_bytes[index..], out _current, out var consumed) != OperationStatus.Done) - throw new InvalidOperationException("Contains invalid UTF8"); - index += consumed; - return true; - } - } -} diff --git a/src/gaemstone/ECS/EntityRef.cs b/src/gaemstone/ECS/EntityRef.cs deleted file mode 100644 index f660077..0000000 --- a/src/gaemstone/ECS/EntityRef.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe sealed class EntityRef - : EntityBase - , IEquatable -{ - public override Universe Universe { get; } - public Entity Entity { get; } - public uint ID => Entity.ID; - - public bool IsAlive => ecs_is_alive(Universe, this); - public EntityType Type => new(Universe, ecs_get_type(Universe, this)); - - public string? Name { - get => ecs_get_name(Universe, this).FlecsToString()!; - set { using var alloc = TempAllocator.Use(); - ecs_set_name(Universe, this, alloc.AllocateCString(value)); } - } - public string? Symbol { - get => ecs_get_symbol(Universe, this).FlecsToString()!; - set { using var alloc = TempAllocator.Use(); - ecs_set_symbol(Universe, this, alloc.AllocateCString(value)); } - } - - - private EntityRef(Universe universe, Entity entity, bool throwOnInvalid) - { - if (throwOnInvalid && !ecs_is_valid(universe, entity)) - throw new InvalidOperationException($"The entity {entity} is not valid"); - Universe = universe; - Entity = entity; - } - - public EntityRef(Universe universe, Entity entity) - : this(universe, entity, true) { } - - public static EntityRef? CreateOrNull(Universe universe, Entity entity) - => ecs_is_valid(universe, entity) ? new(universe, entity) : null; - - - public void Delete() - => ecs_delete(Universe, this); - - public EntityBuilder NewChild(EntityPath? path = null) - => Universe.New(EnsureRelativePath(path)).ChildOf(this); - public EntityRef? Lookup(EntityPath path) - => Universe.Lookup(this, EnsureRelativePath(path)!); - public EntityRef LookupOrThrow(EntityPath path) - => Universe.LookupOrThrow(this, EnsureRelativePath(path)!); - - private static EntityPath? EnsureRelativePath(EntityPath? path) - { if (path?.IsAbsolute == true) throw new ArgumentException("path must not be absolute", nameof(path)); return path; } - - - public EntityRef? Parent - => GetTarget(Universe.ChildOf); - - public IEnumerable GetChildren() - { - foreach (var iter in Iterator.FromTerm(Universe, new(Universe.ChildOf, this))) - for (var i = 0; i < iter.Count; i++) - yield return iter.Entity(i); - } - - - public override EntityRef Add(Identifier id) { ecs_add_id(Universe, this, id); return this; } - public override EntityRef Remove(Identifier id) { ecs_remove_id(Universe, this, id); return this; } - public override bool Has(Identifier id) => ecs_has_id(Universe, this, id); - - public override T Get() - { - var comp = Universe.LookupOrThrow(); - var ptr = ecs_get_id(Universe, this, comp); - if (ptr == null) throw new Exception($"Component {typeof(T)} not found on {this}"); - return typeof(T).IsValueType ? Unsafe.Read(ptr) - : (T)((GCHandle)Unsafe.Read(ptr)).Target!; - } - - public override T? MaybeGet() - { - var comp = Universe.LookupOrThrow(); - var ptr = ecs_get_id(Universe, this, comp); - return (ptr != null) ? Unsafe.Read(ptr) : null; - } - - public override T? MaybeGet(T _ = null!) - where T : class - { - var comp = Universe.LookupOrThrow(); - var ptr = ecs_get_id(Universe, this, comp); - return (ptr != null) ? (T)((GCHandle)Unsafe.Read(ptr)).Target! : null; - } - - public override ref T GetRefOrNull() - { - var comp = Universe.LookupOrThrow(); - var @ref = ecs_ref_init_id(Universe, this, comp); - var ptr = ecs_ref_get_id(Universe, &@ref, comp); - return ref (ptr != null) ? ref Unsafe.AsRef(ptr) : ref Unsafe.NullRef(); - } - - public override ref T GetRefOrThrow() - { - ref var ptr = ref GetRefOrNull(); - if (Unsafe.IsNullRef(ref ptr)) throw new Exception( - $"Component {typeof(T)} not found on {this}"); - return ref ptr; - } - - public override ref T GetMut() - { - var comp = Universe.LookupOrThrow(); - var ptr = ecs_get_mut_id(Universe, this, comp); - // NOTE: Value is added if it doesn't exist on the entity. - return ref Unsafe.AsRef(ptr); - } - - public override void Modified() - => ecs_modified_id(Universe, this, Universe.LookupOrThrow()); - - public override EntityRef Set(in T value) - { - var comp = Universe.LookupOrThrow(); - var size = (ulong)Unsafe.SizeOf(); - fixed (T* ptr = &value) - if (ecs_set_id(Universe, this, comp, size, ptr).Data == 0) - throw new InvalidOperationException(); - return this; - } - - public override EntityRef Set(T obj) where T : class - { - var comp = Universe.LookupOrThrow(); - var handle = (nint)GCHandle.Alloc(obj); - // FIXME: Previous handle needs to be freed. - if (ecs_set_id(Universe, this, comp, (ulong)sizeof(nint), &handle).Data == 0) - throw new InvalidOperationException(); - // FIXME: Handle needs to be freed when component is removed! - return this; - } - - public EntityRef? GetTarget(Entity relation, int index = 0) - => CreateOrNull(Universe, new(ecs_get_target(Universe, this, relation, index))); - public EntityRef? GetTarget(string symbol, int index = 0) - => GetTarget(Universe.LookupSymbolOrThrow(symbol), index); - public EntityRef? GetTarget(int index = 0) - => GetTarget(Universe.LookupOrThrow(typeof(T)), index); - - public bool Equals(EntityRef? other) => (other is not null) && (Universe == other.Universe) && (Entity == other.Entity); - public override bool Equals(object? obj) => Equals(obj as EntityRef); - public override int GetHashCode() => HashCode.Combine(Universe, Entity); - public override string? ToString() => ecs_entity_str(Universe, this).FlecsToStringAndFree()!; - - public static bool operator ==(EntityRef? left, EntityRef? right) => ReferenceEquals(left, right) || (left?.Equals(right) ?? false); - public static bool operator !=(EntityRef? left, EntityRef? right) => !(left == right); - - public static implicit operator Entity(EntityRef? e) => e?.Entity ?? default; - public static implicit operator ecs_entity_t(EntityRef? e) => e?.Entity.Value ?? default; - - public static implicit operator Identifier(EntityRef? e) => new(e?.Entity.Value.Data ?? default); - public static implicit operator ecs_id_t(EntityRef? e) => e?.Entity.Value.Data ?? default; -} diff --git a/src/gaemstone/ECS/EntityType.cs b/src/gaemstone/ECS/EntityType.cs deleted file mode 100644 index 543e4c3..0000000 --- a/src/gaemstone/ECS/EntityType.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe readonly struct EntityType - : IReadOnlyList -{ - public Universe Universe { get; } - public ecs_type_t* Handle { get; } - - public EntityType(Universe universe, ecs_type_t* handle) - { Universe = universe; Handle = handle; } - - public override string ToString() - => ecs_type_str(Universe, Handle).FlecsToStringAndFree()!; - - // IReadOnlyList implementation - public int Count => Handle->count; - public IdentifierRef this[int index] => new(Universe, new(Handle->array[index])); - public IEnumerator GetEnumerator() { for (var i = 0; i < Count; i++) yield return this[i]; } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/src/gaemstone/ECS/Filter.cs b/src/gaemstone/ECS/Filter.cs deleted file mode 100644 index a1cf8bf..0000000 --- a/src/gaemstone/ECS/Filter.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using gaemstone.Utility; -using gaemstone.Utility.IL; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe sealed class Filter - : IDisposable -{ - public Universe Universe { get; } - public ecs_filter_t* Handle { get; } - - public Filter(Universe universe, FilterDesc desc) - { - using var alloc = TempAllocator.Use(); - var flecsDesc = desc.ToFlecs(alloc); - Universe = universe; - Handle = ecs_filter_init(universe, &flecsDesc); - } - - public static void RunOnce(Universe universe, Delegate action) - { - var gen = IterActionGenerator.GetOrBuild(universe, action.Method); - var desc = new FilterDesc(gen.Terms.ToArray()); - using var filter = new Filter(universe, desc); - foreach (var iter in filter.Iter()) gen.RunWithTryCatch(action.Target, iter); - } - - public void Dispose() - => ecs_filter_fini(Handle); - - public Iterator Iter() - => new(Universe, IteratorType.Filter, ecs_filter_iter(Universe, this)); - - public override string ToString() - => ecs_filter_str(Universe, Handle).FlecsToStringAndFree()!; - - public static implicit operator ecs_filter_t*(Filter q) => q.Handle; -} - -public class FilterDesc -{ - public IReadOnlyList Terms { get; } - - public string? Expression { get; } - - /// - /// When true, terms returned by an iterator may either contain 1 or N - /// elements, where terms with N elements are owned, and terms with 1 - /// element are shared, for example from a parent or base entity. When - /// false, the iterator will at most return 1 element when the result - /// contains both owned and shared terms. - /// - public bool Instanced { get; set; } - - /// - /// Entity associated with query (optional). - /// - public Entity Entity { get; set; } - - public FilterDesc(params Term[] terms) - => Terms = terms; - public FilterDesc(string expression) : this() - => Expression = expression; - - public unsafe ecs_filter_desc_t ToFlecs(IAllocator allocator) - { - var desc = new ecs_filter_desc_t { - expr = allocator.AllocateCString(Expression), - instanced = Instanced, - entity = Entity, - }; - var span = desc.terms; - if (Terms.Count > span.Length) { - span = allocator.Allocate(Terms.Count); - desc.terms_buffer = (ecs_term_t*)Unsafe.AsPointer(ref span[0]); - desc.terms_buffer_count = Terms.Count; - } - for (var i = 0; i < Terms.Count; i++) - span[i] = Terms[i].ToFlecs(allocator); - return desc; - } -} diff --git a/src/gaemstone/ECS/FilterExtensions.cs b/src/gaemstone/ECS/FilterExtensions.cs new file mode 100644 index 0000000..48f69f8 --- /dev/null +++ b/src/gaemstone/ECS/FilterExtensions.cs @@ -0,0 +1,16 @@ +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 index 4ab62a9..1b4d2b1 100644 --- a/src/gaemstone/ECS/Game.cs +++ b/src/gaemstone/ECS/Game.cs @@ -6,7 +6,7 @@ namespace gaemstone.ECS; /// Entity for storing global game state and configuration. /// Parameters can use to source this entity. /// -[Entity, Add] +[Singleton] public struct Game { } /// Equivalent to . diff --git a/src/gaemstone/ECS/Identifier.cs b/src/gaemstone/ECS/Identifier.cs deleted file mode 100644 index b157a4f..0000000 --- a/src/gaemstone/ECS/Identifier.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public readonly struct Identifier - : IEquatable -{ - public readonly ecs_id_t Value; - - public bool IsPair => ecs_id_is_pair(Value); - public bool IsWildcard => ecs_id_is_wildcard(Value); - - public IdentifierFlags Flags => (IdentifierFlags)(Value & ECS_ID_FLAGS_MASK); - - public Entity RelationUnsafe => new(new() { Data = (Value & ECS_COMPONENT_MASK) >> 32 }); - public Entity TargetUnsafe => new(new() { Data = Value & ECS_ENTITY_MASK }); - - public Identifier(ecs_id_t value) => Value = value; - - public static Identifier Combine(IdentifierFlags flags, Identifier id) - => new((ulong)flags | id.Value); - - public static Identifier Pair(Entity relation, Entity target) - => Combine(IdentifierFlags.Pair, new( - ((relation.Value.Data << 32) & ECS_COMPONENT_MASK) | - ( target.Value.Data & ECS_ENTITY_MASK ))); - - public (EntityRef Relation, EntityRef Target)? AsPair(Universe universe) - => new IdentifierRef(universe, this).AsPair(); - - public bool Equals(Identifier other) => Value.Data == other.Value.Data; - public override bool Equals(object? obj) => (obj is Identifier other) && Equals(other); - public override int GetHashCode() => Value.Data.GetHashCode(); - public override string? ToString() - => (Flags != default) ? $"Identifier(0x{Value.Data:X}, Flags={Flags})" - : $"Identifier(0x{Value.Data:X})"; - - public static bool operator ==(Identifier left, Identifier right) => left.Equals(right); - public static bool operator !=(Identifier left, Identifier right) => !left.Equals(right); - - public static implicit operator ecs_id_t(Identifier i) => i.Value; -} - -[Flags] -public enum IdentifierFlags : ulong -{ - Pair = 1ul << 63, - Override = 1ul << 62, - Toggle = 1ul << 61, - Or = 1ul << 60, - And = 1ul << 59, - Not = 1ul << 58, -} diff --git a/src/gaemstone/ECS/IdentifierRef.cs b/src/gaemstone/ECS/IdentifierRef.cs deleted file mode 100644 index 82b7de7..0000000 --- a/src/gaemstone/ECS/IdentifierRef.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe class IdentifierRef - : IEquatable -{ - public Universe Universe { get; } - public Identifier ID { get; } - - public IdentifierFlags Flags => ID.Flags; - public bool IsPair => ID.IsPair; - public bool IsWildcard => ID.IsWildcard; - public bool IsValid => ecs_id_is_valid(Universe, ID); - - public IdentifierRef(Universe universe, Identifier id) - { Universe = universe; ID = id; } - - public static IdentifierRef Combine(IdentifierFlags flags, IdentifierRef id) - => new(id.Universe, Identifier.Combine(flags, id)); - public static IdentifierRef Pair(EntityRef relation, Entity target) - => new(relation.Universe, Identifier.Pair(relation, target)); - public static IdentifierRef Pair(Entity relation, EntityRef target) - => new(target.Universe, Identifier.Pair(relation, target)); - - public (EntityRef Relation, EntityRef Target)? AsPair() - => IsPair ? (Universe.LookupOrThrow(ID.RelationUnsafe), Universe.LookupOrThrow(ID.TargetUnsafe)) : null; - - public bool Equals(IdentifierRef? other) => (other is not null) && (Universe == other.Universe) && (ID == other.ID); - public override bool Equals(object? obj) => Equals(obj as IdentifierRef); - public override int GetHashCode() => HashCode.Combine(Universe, ID); - public override string? ToString() => ecs_id_str(Universe, this).FlecsToStringAndFree()!; - - public static bool operator ==(IdentifierRef? left, IdentifierRef? right) => ReferenceEquals(left, right) || (left?.Equals(right) ?? false); - public static bool operator !=(IdentifierRef? left, IdentifierRef? right) => !(left == right); - - public static implicit operator Identifier(IdentifierRef i) => i.ID; - public static implicit operator ecs_id_t(IdentifierRef i) => i.ID.Value; -} diff --git a/src/gaemstone/ECS/Iterator.cs b/src/gaemstone/ECS/Iterator.cs deleted file mode 100644 index fd097e8..0000000 --- a/src/gaemstone/ECS/Iterator.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe class Iterator - : IEnumerable - , IDisposable -{ - public Universe Universe { get; } - public IteratorType? Type { get; } - public ecs_iter_t Value; - - public bool Completed { get; private set; } - public int Count => Value.count; - public TimeSpan DeltaTime => TimeSpan.FromSeconds(Value.delta_time); - public TimeSpan DeltaSystemTime => TimeSpan.FromSeconds(Value.delta_system_time); - - public Iterator(Universe universe, IteratorType? type, ecs_iter_t value) - { Universe = universe; Type = type; Value = value; } - - public static Iterator FromTerm(Universe universe, Term term) - { - using var alloc = TempAllocator.Use(); - var flecsTerm = term.ToFlecs(alloc); - var flecsIter = ecs_term_iter(universe, &flecsTerm); - return new(universe, IteratorType.Term, flecsIter); - } - - public void Dispose() - { - GC.SuppressFinalize(this); - // NOTE: When an iterator is iterated until completion, resources are automatically freed. - if (!Completed) - fixed (ecs_iter_t* ptr = &Value) - ecs_iter_fini(ptr); - } - - public void SetThis(Entity entity) - { - fixed (ecs_iter_t* ptr = &Value) - ecs_iter_set_var(ptr, 0, entity); - } - - public bool Next() - { - fixed (ecs_iter_t* ptr = &Value) { - var result = Type switch { - IteratorType.Term => ecs_term_next(ptr), - IteratorType.Filter => ecs_filter_next(ptr), - IteratorType.Query => ecs_query_next(ptr), - IteratorType.Rule => ecs_rule_next(ptr), - _ => ecs_iter_next(ptr), - }; - Completed = !result; - return result; - } - } - - public EntityRef Entity(int index) - => new(Universe, new(Value.entities[index])); - - public Span Field(int index) - where T : unmanaged - { - fixed (ecs_iter_t* ptr = &Value) { - var size = (ulong)Unsafe.SizeOf(); - var isSelf = ecs_field_is_self(ptr, index); - var pointer = ecs_field_w_size(ptr, size, index); - return new(pointer, isSelf ? Count : 1); - } - } - - public Span MaybeField(int index) - where T : unmanaged => FieldIsSet(index) ? Field(index) : default; - - public SpanToRef FieldRef(int index) - where T : class => new(Field(index)); - - public bool FieldIsSet(int index) - { - fixed (ecs_iter_t* ptr = &Value) - return ecs_field_is_set(ptr, index); - } - - public bool FieldIs(int index) - { - fixed (ecs_iter_t* ptr = &Value) { - var id = ecs_field_id(ptr, index); - var comp = Universe.LookupOrThrow(); - return id == comp.Entity.Value.Data; - } - } - - public override string ToString() - { - fixed (ecs_iter_t* ptr = &Value) - return ecs_iter_str(ptr).FlecsToStringAndFree()!; - } - - // IEnumerable implementation - public IEnumerator GetEnumerator() { while (Next()) yield return this; } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} - -public enum IteratorType -{ - Term, - Filter, - Query, - Rule, -} diff --git a/src/gaemstone/ECS/Module.cs b/src/gaemstone/ECS/Module.cs index 7644352..6522474 100644 --- a/src/gaemstone/ECS/Module.cs +++ b/src/gaemstone/ECS/Module.cs @@ -3,11 +3,7 @@ using System; namespace gaemstone.ECS; [AttributeUsage(AttributeTargets.Class)] -public class ModuleAttribute : EntityAttribute -{ - public ModuleAttribute() { } - public ModuleAttribute(params string[] path) : base(path) { } -} +public class ModuleAttribute : Attribute { } public interface IModuleInitializer { diff --git a/src/gaemstone/ECS/Observer.cs b/src/gaemstone/ECS/Observer.cs index 6f6b9d1..245025d 100644 --- a/src/gaemstone/ECS/Observer.cs +++ b/src/gaemstone/ECS/Observer.cs @@ -1,10 +1,8 @@ using System; using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using gaemstone.Utility; using gaemstone.Utility.IL; -using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -19,21 +17,7 @@ public class ObserverAttribute : ObserverAttribute public static class ObserverExtensions { - public static unsafe EntityRef RegisterObserver(this Universe universe, - string? name, FilterDesc filter, Entity @event, Action callback) - { - using var alloc = TempAllocator.Use(); - var desc = new ecs_observer_desc_t { - filter = filter.ToFlecs(alloc), - entity = universe.New((name != null) ? EntityPath.Parse(name) : null).Build(), - binding_ctx = (void*)CallbackContextHelper.Create((universe, callback)), - callback = new() { Data = new() { Pointer = &Callback } }, - }; - desc.events[0] = @event; - return new(universe, new(ecs_observer_init(universe, &desc))); - } - - public static EntityRef RegisterObserver(this Universe universe, + public static EntityRef InitObserver(this World world, object? instance, MethodInfo method) { var attr = method.Get() ?? throw new ArgumentException( @@ -50,20 +34,12 @@ public static class ObserverExtensions iterAction = (Action)Delegate.CreateDelegate( typeof(Action), instance, method); } else { - var gen = IterActionGenerator.GetOrBuild(universe, method); + var gen = IterActionGenerator.GetOrBuild(world, method); filter = (expr != null) ? new(expr) : new(gen.Terms.ToArray()); iterAction = iter => gen.RunWithTryCatch(instance, iter); } - var @event = universe.LookupOrThrow(attr.Event); - return universe.RegisterObserver(method.Name, filter, @event, iterAction); - } - - [UnmanagedCallersOnly] - private static unsafe void Callback(ecs_iter_t* iter) - { - var (universe, callback) = CallbackContextHelper - .Get<(Universe, Action)>((nint)iter->binding_ctx); - callback(new Iterator(universe, null, *iter)); + var @event = world.LookupByTypeOrThrow(attr.Event); + return world.New(method.Name).Build().InitObserver(@event, filter, iterAction); } } diff --git a/src/gaemstone/ECS/Query.cs b/src/gaemstone/ECS/Query.cs deleted file mode 100644 index 2138b3c..0000000 --- a/src/gaemstone/ECS/Query.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe sealed class Query - : IDisposable -{ - public Universe Universe { get; } - public ecs_query_t* Handle { get; } - - public Query(Universe universe, QueryDesc desc) - { - using var alloc = TempAllocator.Use(); - var flecsDesc = desc.ToFlecs(alloc); - Universe = universe; - Handle = ecs_query_init(universe, &flecsDesc); - } - - public void Dispose() - => ecs_query_fini(this); - - public Iterator Iter() - => new(Universe, IteratorType.Query, ecs_query_iter(Universe, this)); - - public override string ToString() - => ecs_query_str(Handle).FlecsToStringAndFree()!; - - public static implicit operator ecs_query_t*(Query q) => q.Handle; -} - -public class QueryDesc : FilterDesc -{ - public QueryDesc(string expression) : base(expression) { } - public QueryDesc(params Term[] terms) : base(terms) { } - - public new unsafe ecs_query_desc_t ToFlecs(IAllocator allocator) - { - var desc = new ecs_query_desc_t { - filter = base.ToFlecs(allocator), - // TODO: Implement more Query features. - }; - return desc; - } -} diff --git a/src/gaemstone/ECS/Relation.cs b/src/gaemstone/ECS/Relation.cs deleted file mode 100644 index 5b564e0..0000000 --- a/src/gaemstone/ECS/Relation.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace gaemstone.ECS; - -[Tag] -public struct Relation { } - -/// -/// Marked entity represents a relationship type. -/// It may be used as the "relation" in a pair. -/// -/// -/// The relationship may have component data associated with -/// it when added to an entity under these circumstances: -/// -/// If marked as a , does not carry data. -/// If marked as a , carries the relation's data. -/// If marked with neither, will carry the target's data, if it's a component. -/// -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] -public class RelationAttribute : Attribute, ICreateEntityAttribute { } diff --git a/src/gaemstone/ECS/Rule.cs b/src/gaemstone/ECS/Rule.cs deleted file mode 100644 index 44b116a..0000000 --- a/src/gaemstone/ECS/Rule.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe sealed class Rule - : IDisposable -{ - public Universe Universe { get; } - public ecs_rule_t* Handle { get; } - - public Rule(Universe universe, FilterDesc desc) - { - using var alloc = TempAllocator.Use(); - var flecsDesc = desc.ToFlecs(alloc); - Universe = universe; - Handle = ecs_rule_init(universe, &flecsDesc); - } - - public void Dispose() - => ecs_rule_fini(this); - - public Iterator Iter() - => new(Universe, IteratorType.Rule, ecs_rule_iter(Universe, this)); - - public override string ToString() - => ecs_rule_str(Handle).FlecsToStringAndFree()!; - - public static implicit operator ecs_rule_t*(Rule q) => q.Handle; -} diff --git a/src/gaemstone/ECS/System.cs b/src/gaemstone/ECS/System.cs index 4f3d490..9195a6b 100644 --- a/src/gaemstone/ECS/System.cs +++ b/src/gaemstone/ECS/System.cs @@ -1,12 +1,9 @@ using System; using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using gaemstone.Flecs; using gaemstone.Utility; using gaemstone.Utility.IL; -using static flecs_hub.flecs; -using static gaemstone.Flecs.Core; namespace gaemstone.ECS; @@ -29,22 +26,7 @@ public class ExpressionAttribute : Attribute public static class SystemExtensions { - private static unsafe EntityRef RegisterSystem(this Universe universe, - string? name, QueryDesc query, Entity phase, CallbackContext callback) - { - using var alloc = TempAllocator.Use(); - var desc = new ecs_system_desc_t { - query = query.ToFlecs(alloc), - entity = universe.New((name != null) ? EntityPath.Parse(name) : null) - .Add(phase).Add(phase).Build(), - binding_ctx = (void*)CallbackContextHelper.Create(callback), - // TODO: Use binding_ctx_free to remove clear the context. - run = new() { Data = new() { Pointer = &Run } }, - }; - return new(universe, new(ecs_system_init(universe, &desc))); - } - - public static EntityRef RegisterSystem(this Universe universe, Delegate action) + public static EntityRef InitSystem(this World world, Delegate action) { var attr = action.Method.Get(); var expr = action.Method.Get()?.Value; @@ -54,17 +36,16 @@ public static class SystemExtensions query = new(expr ?? throw new ArgumentException( "System must specify ExpressionAttribute", nameof(action))); } else { - var gen = IterActionGenerator.GetOrBuild(universe, action.Method); + 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 = universe.LookupOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); - return universe.RegisterSystem(action.Method.Name, - query, phase, new(universe, action.Method, callback)); + var phase = world.LookupByTypeOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); + return world.New(action.Method.Name).Build().InitSystem(phase, query, callback); } - public static EntityRef RegisterSystem(this Universe universe, + public static EntityRef InitSystem(this World world, object? instance, MethodInfo method) { var attr = method.Get(); @@ -78,48 +59,12 @@ public static class SystemExtensions "System must specify ExpressionAttribute", nameof(method))); callback = (Action)Delegate.CreateDelegate(typeof(Action), instance, method); } else { - var gen = IterActionGenerator.GetOrBuild(universe, method); + var gen = IterActionGenerator.GetOrBuild(world, method); query = (expr != null) ? new(expr) : new(gen.Terms.ToArray()); callback = iter => gen.RunWithTryCatch(instance, iter); } - var phase = universe.LookupOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); - return universe.RegisterSystem(method.Name, - query, phase, new(universe, method, callback)); - } - - private class CallbackContext - { - public Universe Universe { get; } - public MethodInfo Method { get; } - public Action Callback { get; } - - public CallbackContext(Universe universe, MethodInfo method, Action callback) - { Universe = universe; Method = method; Callback = callback; } - - public void Prepare(Iterator iter) - { - // If the method is marked with [Source], set the $This variable. - if (Method.Get()?.Type is Type sourceType) - iter.SetThis(Universe.LookupOrThrow(sourceType)); - } - } - - [UnmanagedCallersOnly] - private static unsafe void Run(ecs_iter_t* flecsIter) - { - var callback = CallbackContextHelper.Get((nint)flecsIter->binding_ctx); - - // This is what flecs does, so I guess we'll do it too! - var type = (&flecsIter->next == (delegate*)&ecs_query_next) - ? IteratorType.Query : (IteratorType?)null; - using var iter = new Iterator(callback.Universe, type, *flecsIter); - - callback.Prepare(iter); - - if (flecsIter->field_count == 0) - callback.Callback(iter); - else while (iter.Next()) - callback.Callback(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/Term.cs b/src/gaemstone/ECS/Term.cs deleted file mode 100644 index ec733dd..0000000 --- a/src/gaemstone/ECS/Term.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public class Term -{ - public Identifier ID { get; set; } - public TermID? Source { get; set; } - public TermID? Relation { get; set; } - public TermID? Target { get; set; } - public TermInOutKind InOut { get; set; } - public TermOperKind Oper { get; set; } - public IdentifierFlags Flags { get; set; } - - public Term() { } - public Term(Identifier id) => ID = id; - public Term(TermID relation, TermID target) - { Relation = relation; Target = target; } - - public static implicit operator Term(EntityRef entity) => new(entity); - public static implicit operator Term(Entity entity) => new(entity); - public static implicit operator Term(IdentifierRef id) => new(id); - public static implicit operator Term(Identifier id) => new(id); - - public Term None { get { InOut = TermInOutKind.None; return this; } } - public Term In { get { InOut = TermInOutKind.In; return this; } } - public Term Out { get { InOut = TermInOutKind.Out; return this; } } - - public Term Or { get { Oper = TermOperKind.Or; return this; } } - public Term Not { get { Oper = TermOperKind.Not; return this; } } - public Term Optional { get { Oper = TermOperKind.Optional; return this; } } - - public ecs_term_t ToFlecs(IAllocator allocator) => new() { - id = ID, - src = Source?.ToFlecs(allocator) ?? default, - first = Relation?.ToFlecs(allocator) ?? default, - second = Target?.ToFlecs(allocator) ?? default, - inout = (ecs_inout_kind_t)InOut, - oper = (ecs_oper_kind_t)Oper, - id_flags = (ecs_id_t)(ulong)Flags, - }; -} - -public enum TermInOutKind -{ - Default = ecs_inout_kind_t.EcsInOutDefault, - None = ecs_inout_kind_t.EcsInOutNone, - InOut = ecs_inout_kind_t.EcsInOut, - In = ecs_inout_kind_t.EcsIn, - Out = ecs_inout_kind_t.EcsOut, -} - -public enum TermOperKind -{ - And = ecs_oper_kind_t.EcsAnd, - Or = ecs_oper_kind_t.EcsOr, - Not = ecs_oper_kind_t.EcsNot, - Optional = ecs_oper_kind_t.EcsOptional, - AndFrom = ecs_oper_kind_t.EcsAndFrom, - OrFrom = ecs_oper_kind_t.EcsOrFrom, - NotFrom = ecs_oper_kind_t.EcsNotFrom, -} - -public class TermID -{ - public static TermID This { get; } = new("$This"); - - public Entity ID { get; } - public string? Name { get; } - public Entity Traverse { get; set; } - public TermTraversalFlags Flags { get; set; } - - public TermID(Entity id) - => ID = id; - public TermID(string name) - { - if (name[0] == '$') { - Name = name[1..]; - Flags = TermTraversalFlags.IsVariable; - } else Name = name; - } - - public static implicit operator TermID(EntityRef entity) => new(entity); - public static implicit operator TermID(Entity entity) => new(entity); - public static implicit operator TermID(string name) => new(name); - - public ecs_term_id_t ToFlecs(IAllocator allocator) => new() { - id = ID, - name = allocator.AllocateCString(Name), - trav = Traverse, - flags = (ecs_flags32_t)(uint)Flags - }; -} - -[Flags] -public enum TermTraversalFlags : uint -{ - /// Match on self. - Self = EcsSelf, - /// Match by traversing upwards. - Up = EcsUp, - /// Match by traversing downwards (derived, cannot be set). - Down = EcsDown, - /// Sort results breadth first. - Cascade = EcsCascade, - /// Short for up(ChildOf). - Parent = EcsParent, - /// Term id is a variable. - IsVariable = EcsIsVariable, - /// Term id is an entity. - IsEntity = EcsIsEntity, - /// Prevent observer from triggering on term. - Filter = EcsFilter, -} diff --git a/src/gaemstone/ECS/Universe+Lookup.cs b/src/gaemstone/ECS/Universe+Lookup.cs deleted file mode 100644 index b38e018..0000000 --- a/src/gaemstone/ECS/Universe+Lookup.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using gaemstone.Utility; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe partial class Universe -{ - private readonly Dictionary _lookupByType = new(); - - public void AddLookupByType(Type type, Entity entity) - => _lookupByType.Add(type, entity); - public void RemoveLookupByType(Type type) - { if (!_lookupByType.Remove(type)) throw new InvalidOperationException( - $"Lookup for {type} does not exist"); } - - - private EntityRef? CreateOrNull(Entity entity) - => EntityRef.CreateOrNull(this, entity); - - public EntityRef? Lookup() - => Lookup(typeof(T)); - public EntityRef? Lookup(Type type) - => Lookup(_lookupByType.GetValueOrDefault(type)); - - public EntityRef? Lookup(Entity value) - => CreateOrNull(new(ecs_get_alive(this, value))); - - public EntityRef? Lookup(EntityPath path) - => Lookup(default, path); - public EntityRef? Lookup(Entity parent, EntityPath path) - => CreateOrNull(EntityPath.Lookup(this, parent, path)); - public EntityRef? LookupSymbol(string symbol) - { - using var alloc = TempAllocator.Use(); - return CreateOrNull(new(ecs_lookup_symbol(this, alloc.AllocateCString(symbol), false))); - } - - public EntityRef LookupOrThrow() => LookupOrThrow(typeof(T)); - public EntityRef LookupOrThrow(Type type) => Lookup(type) - ?? throw new EntityNotFoundException($"Entity of type {type} not found"); - public EntityRef LookupOrThrow(Entity entity) => Lookup(entity) - ?? throw new EntityNotFoundException($"Entity {entity} not alive"); - public EntityRef LookupOrThrow(EntityPath path) => Lookup(default, path) - ?? throw new EntityNotFoundException($"Entity '{path}' not found"); - public EntityRef LookupOrThrow(Entity parent, EntityPath path) => Lookup(parent, path) - ?? throw new EntityNotFoundException($"Child entity of {parent} '{path}' not found"); - public EntityRef LookupSymbolOrThrow(string symbol) => LookupSymbol(symbol) - ?? throw new EntityNotFoundException($"Entity with symbol '{symbol}' not found"); - - - public class EntityNotFoundException : Exception - { public EntityNotFoundException(string message) : base(message) { } } -} - -public static class LookupExtensions -{ - public static EntityRef CreateLookup(this EntityRef entity) - => entity.CreateLookup(typeof(T)); - public static EntityRef CreateLookup(this EntityRef entity, Type type) - { entity.Universe.AddLookupByType(type, entity); return entity; } -} diff --git a/src/gaemstone/ECS/Universe.cs b/src/gaemstone/ECS/Universe.cs deleted file mode 100644 index 14f2707..0000000 --- a/src/gaemstone/ECS/Universe.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Linq; -using static flecs_hub.flecs; - -namespace gaemstone.ECS; - -public unsafe partial class Universe -{ - public ecs_world_t* Handle { get; } - public ModuleManager Modules { get; } - - public EntityRef Wildcard { get; } - public EntityRef Any { get; } - public EntityRef ChildOf { get; } - - public bool IsDeferred => ecs_is_deferred(this); - - public Universe(params string[] args) - { - Handle = ecs_init_w_args(args.Length, null); - Modules = new(this); - - // Bootstrap flecs built-ins that are important to internals. - Wildcard = LookupOrThrow("/flecs/core/*"); - Any = LookupOrThrow("/flecs/core/_"); - ChildOf = LookupOrThrow("/flecs/core/ChildOf"); - - // Bootstrap custom "Relation" tag. - // This will be retrofitted to certain flecs built-ins. - New("/gaemstone/Relation") - .Add(LookupOrThrow("/flecs/core/Tag")) - .Build().CreateLookup(); - - // Register built-in (static) modules, which - // are defined in the "gaemstone.Flecs" namespace. - Modules.RegisterAll(GetType().Assembly.GetTypes() - .Where(t => t.IsAbstract && t.IsSealed)); - - // Create "Game" entity to store global state. - New("/Game").Symbol("Game").Build() - .CreateLookup().Add(); - } - - public EntityBuilder New(EntityPath? path = null) - => new(this, path); - - public bool Progress(TimeSpan delta) - => ecs_progress(this, (float)delta.TotalSeconds); - - public static implicit operator ecs_world_t*(Universe w) => w.Handle; -} diff --git a/src/gaemstone/Flecs/Core.cs b/src/gaemstone/Flecs/Core.cs index 78bd04e..731973a 100644 --- a/src/gaemstone/Flecs/Core.cs +++ b/src/gaemstone/Flecs/Core.cs @@ -2,25 +2,33 @@ using gaemstone.ECS; namespace gaemstone.Flecs; -[Module("flecs", "core")] +[Module, Path("/flecs/core")] public static class Core { // Entity Tags + [Tag] public struct Name { } + [Tag] public struct Symbol { } + [Tag] public struct Alias { } + [Tag] public struct Module { } [Tag] public struct Prefab { } [Tag] public struct SlotOf { } [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 - [Entity] public struct World { } - [Entity("*")] public struct Wildcard { } - [Entity("_")] public struct Any { } - [Entity] public struct This { } - [Entity("$")] public struct Variable { } - [Entity] public struct Flag { } + [Entity] public struct World { } + [Path("*")] public struct Wildcard { } + [Path("_")] public struct Any { } + [Entity] public struct This { } + [Path("$")] public struct Variable { } + [Entity] public struct Flag { } // Entity Relationships @@ -41,4 +49,35 @@ public static class Core [Tag] public struct Acyclic { } [Relation, Tag] public struct With { } [Tag] public struct OneOf { } + + // Components + + [Component] + public readonly struct Component + { + public int Size { get; } + public int Alignment { get; } + } + + [Relation, Component] + public readonly struct Identifier + { +#pragma warning disable IDE0051 // Disable "unused" hint. +#pragma warning disable CS0169 // Disable "unused" warning. +#pragma warning disable CS0649 // Disable "never assigned to" warning. + private unsafe readonly void* _value; + private readonly nint _length; + private readonly ulong _hash; + private readonly ulong _indexHash; + private unsafe readonly void* _index; +#pragma warning restore + + public override string? ToString() { unsafe { + if ((_value == null) || (_length == 0)) return null; + else return new UTF8View(new(_value, (int)_length)).ToString(); + } } + + public static implicit operator string?(Identifier id) + => id.ToString(); + } } diff --git a/src/gaemstone/Flecs/DeletionEvent.cs b/src/gaemstone/Flecs/DeletionEvent.cs index 1263254..69003da 100644 --- a/src/gaemstone/Flecs/DeletionEvent.cs +++ b/src/gaemstone/Flecs/DeletionEvent.cs @@ -2,14 +2,14 @@ using gaemstone.ECS; namespace gaemstone.Flecs; -[Module("flecs", "core")] +[Module, Path("/flecs/core")] public static class DeletionEvent { [Relation, Tag] public struct OnDelete { } [Relation, Tag] public struct OnDeleteTarget { } } -[Module("flecs", "core")] +[Module, Path("/flecs/core")] public static class DeletionBehavior { [Tag] public struct Remove { } diff --git a/src/gaemstone/Flecs/Doc.cs b/src/gaemstone/Flecs/Doc.cs new file mode 100644 index 0000000..c3409e9 --- /dev/null +++ b/src/gaemstone/Flecs/Doc.cs @@ -0,0 +1,81 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using gaemstone.ECS; +using gaemstone.Utility; +using static flecs_hub.flecs; + +namespace gaemstone.Flecs; + +[Module, Path("/flecs/doc")] +public static class Doc +{ + [Tag] public struct Brief { } + [Tag] public struct Detail { } + [Tag] public struct Link { } + [Tag] public struct Color { } + + [Relation, Component] + public struct Description + { + internal unsafe void* Value; + + public override string? ToString() + { unsafe { return Marshal.PtrToStringUTF8((nint)Value); } } + public static implicit operator string?(Description desc) + => desc.ToString(); + } +} + +public static unsafe class DocExtensions +{ + private static EntityRef Set(EntityRef entity, string? value) + { + var world = entity.World; + var descEntity = world.LookupByTypeOrThrow(); + var docEntity = world.LookupByTypeOrThrow(); + var id = Identifier.Pair(descEntity, docEntity); + + var has = 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 (has) { + // 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. + entity.Remove(id); + } + return entity; + } + + public static string? GetDocName(this EntityRef 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 string? GetDocBrief(this EntityRef entity) + => ecs_doc_get_brief(entity.World, entity).FlecsToString()!; + public static EntityRef SetDocBrief(this EntityRef entity, string? value) + => Set(entity, value); + + public static string? GetDocDetail(this EntityRef entity) + => ecs_doc_get_detail(entity.World, entity).FlecsToString()!; + public static EntityRef SetDocDetail(this EntityRef entity, string? value) + => Set(entity, value); + + public static string? GetDocLink(this EntityRef entity) + => ecs_doc_get_link(entity.World, entity).FlecsToString()!; + public static EntityRef SetDocLink(this EntityRef entity, string? value) + => Set(entity, value); + + public static string? GetDocColor(this EntityRef entity) + => ecs_doc_get_color(entity.World, entity).FlecsToString()!; + public static EntityRef SetDocColor(this EntityRef entity, string? value) + => Set(entity, value); +} diff --git a/src/gaemstone/Flecs/FlecsException.cs b/src/gaemstone/Flecs/FlecsException.cs deleted file mode 100644 index d280d8e..0000000 --- a/src/gaemstone/Flecs/FlecsException.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using static flecs_hub.flecs; - -namespace gaemstone.Flecs; - -public class FlecsException - : Exception -{ - public FlecsException() : base() { } - public FlecsException(string message) : base(message) { } -} - -public class FlecsAbortException - : FlecsException -{ - private readonly string _stackTrace = new StackTrace(2, true).ToString(); - public override string? StackTrace => _stackTrace; - - private FlecsAbortException() - : base("Abort was called by flecs") { } - - // TODO: This might not be ideal if we ever want to set other OS API settings. - public unsafe static void SetupHook() - { - ecs_os_set_api_defaults(); - var api = ecs_os_get_api(); - api.abort_ = new FnPtr_Void { Pointer = &Abort }; - ecs_os_set_api(&api); - } - - [UnmanagedCallersOnly] - private static void Abort() - => throw new FlecsAbortException(); -} diff --git a/src/gaemstone/Flecs/ObserverEvent.cs b/src/gaemstone/Flecs/ObserverEvent.cs index 2cfbb74..698ca75 100644 --- a/src/gaemstone/Flecs/ObserverEvent.cs +++ b/src/gaemstone/Flecs/ObserverEvent.cs @@ -2,7 +2,7 @@ using gaemstone.ECS; namespace gaemstone.Flecs; -[Module("flecs", "core")] +[Module, Path("/flecs/core")] public static class ObserverEvent { [Entity] public struct OnAdd { } diff --git a/src/gaemstone/Flecs/Pipeline.cs b/src/gaemstone/Flecs/Pipeline.cs index 8736b1c..939a676 100644 --- a/src/gaemstone/Flecs/Pipeline.cs +++ b/src/gaemstone/Flecs/Pipeline.cs @@ -2,7 +2,7 @@ using gaemstone.ECS; namespace gaemstone.Flecs; -[Module("flecs", "pipeline")] +[Module, Path("/flecs/pipeline")] public static class Pipeline { [Entity] public struct Phase { } diff --git a/src/gaemstone/Flecs/SystemPhase.cs b/src/gaemstone/Flecs/SystemPhase.cs index 1f2cd85..891b0de 100644 --- a/src/gaemstone/Flecs/SystemPhase.cs +++ b/src/gaemstone/Flecs/SystemPhase.cs @@ -3,7 +3,7 @@ using static gaemstone.Flecs.Pipeline; namespace gaemstone.Flecs; -[Module("flecs", "pipeline")] +[Module, Path("/flecs/pipeline")] public static class SystemPhase { [Entity, Add] diff --git a/src/gaemstone/Flecs/Systems/Monitor.cs b/src/gaemstone/Flecs/Systems/Monitor.cs index 865b1b5..e9d1c51 100644 --- a/src/gaemstone/Flecs/Systems/Monitor.cs +++ b/src/gaemstone/Flecs/Systems/Monitor.cs @@ -5,14 +5,14 @@ using static flecs_hub.flecs; namespace gaemstone.Flecs.Systems; -[Module("flecs", "monitor")] +[Module, Path("/flecs/monitor")] public unsafe class Monitor : IModuleInitializer { public void Initialize(EntityRef module) { using var alloc = TempAllocator.Use(); - ecs_import_c(module.Universe, new() { Data = new() { + ecs_import_c(module.World, new() { Data = new() { Pointer = &MonitorImport } }, alloc.AllocateCString("FlecsMonitor")); } diff --git a/src/gaemstone/Flecs/Systems/Rest.cs b/src/gaemstone/Flecs/Systems/Rest.cs index 53722d3..a057002 100644 --- a/src/gaemstone/Flecs/Systems/Rest.cs +++ b/src/gaemstone/Flecs/Systems/Rest.cs @@ -5,14 +5,14 @@ using static flecs_hub.flecs; namespace gaemstone.Flecs.Systems; -[Module("flecs", "rest")] +[Module, Path("/flecs/rest")] public unsafe class Rest : IModuleInitializer { public void Initialize(EntityRef module) { using (var alloc = TempAllocator.Use()) - ecs_import_c(module.Universe, new() { Data = new() { + ecs_import_c(module.World, new() { Data = new() { Pointer = &RestImport } }, alloc.AllocateCString("FlecsRest")); module.NewChild("Rest").Build() @@ -21,6 +21,6 @@ public unsafe class Rest } [UnmanagedCallersOnly] - static void RestImport(ecs_world_t* world) + private static void RestImport(ecs_world_t* world) => FlecsRestImport(world); } diff --git a/src/gaemstone/ECS/Universe+Modules.cs b/src/gaemstone/Universe+Modules.cs similarity index 50% rename from src/gaemstone/ECS/Universe+Modules.cs rename to src/gaemstone/Universe+Modules.cs index 942e54a..641108d 100644 --- a/src/gaemstone/ECS/Universe+Modules.cs +++ b/src/gaemstone/Universe+Modules.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using gaemstone.ECS; using gaemstone.Utility; using static gaemstone.Flecs.Core; using BindingFlags = System.Reflection.BindingFlags; -namespace gaemstone.ECS; +namespace gaemstone; public class ModuleManager { @@ -33,7 +34,7 @@ public class ModuleManager { if (!moduleType.IsClass || moduleType.IsGenericType || moduleType.IsGenericTypeDefinition) throw new Exception( $"Module {moduleType} must be a non-generic class"); - if (moduleType.Get() is not ModuleAttribute attr) throw new Exception( + if (moduleType.Get() is not ModuleAttribute moduleAttr) throw new Exception( $"Module {moduleType} must be marked with ModuleAttribute"); // Check if module type is static. @@ -43,11 +44,11 @@ public class ModuleManager // create entities, only look up existing ones to add type lookups // for use with the Lookup(Type) method. - if (attr.Path == null) throw new Exception( - $"Existing module {moduleType} must have ModuleAttribute.Name set"); - var path = new EntityPath(true, attr.Path); - var module = Universe.Lookup(path) ?? throw new Exception( - $"Existing module {moduleType} with name '{path}' not found"); + if (moduleType.Get()?.Value is not string modulePathStr) throw new Exception( + $"Existing module {moduleType} must have {nameof(PathAttribute)} set"); + var modulePath = EntityPath.Parse(modulePathStr); + var moduleEntity = Universe.LookupByPath(modulePath) ?? throw new Exception( + $"Existing module {moduleType} with name '{modulePath}' not found"); // This implementation is pretty naive. It simply gets all nested // types which are tagged with an ICreateEntityAttribute base @@ -55,13 +56,16 @@ public class ModuleManager foreach (var type in moduleType.GetNestedTypes()) { if (!type.GetCustomAttributes(true).OfType().Any()) continue; - var name = type.Get()?.Path?.Single() ?? type.Name; - var entity = Universe.LookupOrThrow(module, name); + + var attr = type.Get(); + var path = EntityPath.Parse(attr?.Value ?? type.Name); + var entity = Universe.LookupByPathOrThrow(moduleEntity, path); entity.CreateLookup(type); - if (type.Has()) entity.Add(); + + if (type.Has()) entity.Add(); } - return module; + return moduleEntity; } else { @@ -100,12 +104,12 @@ public class ModuleManager if (attr == null) throw new ArgumentException( $"Module {type} must be marked with ModuleAttribute", nameof(type)); - // If module is static, its path will be implictly global. - var global = (type.IsAbstract && type.IsSealed) || attr.Global; + var path = EntityPath.Parse( + (type.Get() is PathAttribute pathAttr) + ? pathAttr.Value : type.Name); - // If global or path are specified in the attribute, return an absolute path. - if (global || attr.Path != null) - return new(true, attr.Path ?? new[] { type.Name }); + // If specified path is absolute, return it now. + if (path.IsAbsolute) return path; // Otherwise, create it based on the type's assembly, namespace and name. var assemblyName = type.Assembly.GetName().Name!; @@ -114,115 +118,113 @@ public class ModuleManager $"Module {type} must be defined under namespace {assemblyName}"); var fullNameWithoutAssembly = type.FullName![(assemblyName.Length + 1)..]; - var parts = fullNameWithoutAssembly.Split('.'); - return new(true, parts.Prepend(assemblyName).ToArray()); + var parts = fullNameWithoutAssembly.Split('.')[..^1]; + return new(true, parts.Prepend(assemblyName).Concat(path.GetParts()).ToArray()); } -} - -internal class ModuleInfo -{ - public Universe Universe { get; } - public Type Type { get; } - public EntityPath Path { get; } - public EntityRef Entity { get; } - public object? Instance { get; internal set; } - public bool IsActive => Instance != null; - - public HashSet MetDependencies { get; } = new(); - public HashSet UnmetDependencies { get; } = new(); - - public ModuleInfo(Universe universe, Type type, EntityPath path) + internal class ModuleInfo { - Universe = universe; - Type = type; - Path = path; - - if (Type.IsAbstract || Type.IsSealed) throw new Exception( - $"Module {Type} must not be abstract or sealed"); - if (Type.GetConstructor(Type.EmptyTypes) == null) throw new Exception( - $"Module {Type} must define public parameterless constructor"); - - var module = Universe.New(Path).Add(); - - // Add module dependencies from [DependsOn<>] attributes. - foreach (var dependsAttr in Type.GetMultiple().Where(attr => - attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) { - var dependsPath = ModuleManager.GetModulePath(dependsAttr.Target); - var dependency = Universe.Lookup(dependsPath) ?? - Universe.New(dependsPath).Add().Disable().Build(); + public Universe Universe { get; } + public Type Type { get; } + public EntityPath Path { get; } + + public EntityRef Entity { get; } + public object? Instance { get; internal set; } + public bool IsActive => Instance != null; + + public HashSet MetDependencies { get; } = new(); + public HashSet UnmetDependencies { get; } = new(); + + public ModuleInfo(Universe universe, Type type, EntityPath path) + { + Universe = universe; + Type = type; + Path = path; + + if (Type.IsAbstract || Type.IsSealed) throw new Exception( + $"Module {Type} must not be abstract, sealed or static"); + if (Type.GetConstructor(Type.EmptyTypes) == null) throw new Exception( + $"Module {Type} must define public parameterless constructor"); + + var module = Universe.New(Path).Add(); + + // Add module dependencies from [DependsOn<>] attributes. + foreach (var dependsAttr in Type.GetMultiple().Where(attr => + attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) { + var dependsPath = GetModulePath(dependsAttr.Target); + var dependency = Universe.LookupByPath(dependsPath) ?? + Universe.New(dependsPath).Add().Disable().Build(); + + var depModule = Universe.Modules.Lookup(dependency); + if (depModule?.IsActive == true) MetDependencies.Add(depModule); + else { UnmetDependencies.Add(dependency); module.Disable(); } + + module.Add(dependency); + } - var depModule = Universe.Modules.Lookup(dependency); - if (depModule?.IsActive == true) MetDependencies.Add(depModule); - else { UnmetDependencies.Add(dependency); module.Disable(); } + Entity = module.Build().CreateLookup(Type); - module.Add(dependency); + // Ensure all parent entities have Module set. + for (var p = Entity.Parent; p != null; p = p.Parent) + p.Add(); } - Entity = module.Build().CreateLookup(Type); - - // Ensure that all the parent entities have Module set, too. - for (var p = Entity.Parent; p != null; p = p.Parent) - p.Add(); - } + public void Enable() + { + Entity.Enable(); + Instance = Activator.CreateInstance(Type)!; + foreach (var type in Type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) + RegisterNestedType(type); + (Instance as IModuleInitializer)?.Initialize(Entity); + RegisterMethods(Instance); + } - public void Enable() - { - Entity.Enable(); - Instance = Activator.CreateInstance(Type)!; - RegisterNestedTypes(); - (Instance as IModuleInitializer)?.Initialize(Entity); - RegisterMethods(Instance); - } - - private void RegisterNestedTypes() - { - foreach (var type in Type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) { - if (!type.GetCustomAttributes(true).OfType().Any()) continue; + private void RegisterNestedType(Type type) + { + if (!type.GetCustomAttributes(true).OfType().Any()) return; // If proxied type is specified, use it instead of the marked type. // Attributes are still read from the original type. var proxyType = type.Get()?.Type ?? type; - if (!type.Has() && (!proxyType.IsValueType || proxyType.GetFields().Length > 0)) { + if (!type.Has() && (!proxyType.IsValueType || (proxyType.GetFields().Length > 0))) { var typeHint = (proxyType != type) ? $"{proxyType.Name} (proxied by {type})" : type.ToString(); throw new Exception($"Type {typeHint} must be an empty, used-defined struct."); } - var path = (type.Get() is EntityAttribute entityAttr) - ? new EntityPath(entityAttr.Global, entityAttr.Path ?? new[] { proxyType.Name }) - : new EntityPath(false, proxyType.Name); - + var path = EntityPath.Parse(type.Get()?.Value ?? proxyType.Name); var builder = path.IsAbsolute ? Universe.New(path) : Entity.NewChild(path); - if (!type.IsPublic) builder.Symbol(path.Name); - if (type.Has()) builder.Add(); + if (type.Get() is SymbolAttribute symbolAttr) + builder.Symbol(symbolAttr.Value ?? path.Name); var entity = builder.Build(); EntityRef Lookup(Type toLookup) - => (type != toLookup) ? Universe.LookupOrThrow(toLookup) : entity; + => (type != toLookup) ? Universe.LookupByTypeOrThrow(toLookup) : entity; foreach (var attr in type.GetMultiple()) entity.Add(Lookup(attr.Entity)); foreach (var attr in type.GetMultiple()) entity.Add(Lookup(attr.Relation), Lookup(attr.Target)); if (type.Get()?.AutoAdd == true) entity.Add(entity); - if (type.Has()) entity.CreateComponent(proxyType); + if (type.Has()) entity.InitComponent(proxyType); else entity.CreateLookup(proxyType); + if (type.Has()) entity.Add(); + if (type.Has()) entity.Add(); } - } - private void RegisterMethods(object? instance) - { - foreach (var method in Type.GetMethods( - BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.Static | BindingFlags.Instance - )) { - if (method.Has()) - Universe.RegisterSystem(instance, method).ChildOf(Entity); - if (method.Has()) - Universe.RegisterObserver(instance, method).ChildOf(Entity); + private void RegisterMethods(object? instance) + { + foreach (var method in Type.GetMethods( + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static | BindingFlags.Instance + )) { + if (method.Has()) + Universe.InitSystem(instance, method).ChildOf(Entity); + if (method.Has()) + Universe.InitObserver(instance, method).ChildOf(Entity); + } } } } diff --git a/src/gaemstone/Universe.cs b/src/gaemstone/Universe.cs new file mode 100644 index 0000000..2bb676d --- /dev/null +++ b/src/gaemstone/Universe.cs @@ -0,0 +1,35 @@ +using System.Linq; +using gaemstone.ECS; + +namespace gaemstone; + +public class Universe : World +{ + public ModuleManager Modules { get; } + + public Universe(params string[] args) + : base(args) + { + Modules = new(this); + + // Bootstrap some stuff, so we can register flecs properly. + New("/gaemstone/Doc/DisplayType") + .Add(LookupByPathOrThrow("/flecs/core/Exclusive")) + .Build().CreateLookup(); + New("/gaemstone/Doc/Relation").Build().CreateLookup(); + LookupByPathOrThrow("/flecs/core/Module" ).CreateLookup(); + LookupByPathOrThrow("/flecs/core/Component").CreateLookup(); + LookupByPathOrThrow("/flecs/core/Tag" ).CreateLookup(); + + // Register built-in (static) modules, which + // are defined in the "gaemstone.Flecs" namespace. + Modules.RegisterAll(GetType().Assembly.GetTypes() + .Where(t => t.IsAbstract && t.IsSealed)); + + Modules.Register(); + + // Create "Game" entity to store global state. + New("/Game").Symbol("Game").Build() + .CreateLookup().Add(); + } +} diff --git a/src/gaemstone/Utility/Allocators.cs b/src/gaemstone/Utility/Allocators.cs deleted file mode 100644 index 5aa3eeb..0000000 --- a/src/gaemstone/Utility/Allocators.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using static flecs_hub.flecs.Runtime; - -namespace gaemstone.Utility; - -public interface IAllocator -{ - nint Allocate(int byteCount); - void Free(nint pointer); -} - -public unsafe static class AllocatorExtensions -{ - public static Span Allocate(this IAllocator allocator, int count) where T : unmanaged - => new((void*)allocator.Allocate(sizeof(T) * count), count); - public static void Free(this IAllocator allocator, Span span) where T : unmanaged - => allocator.Free((nint)Unsafe.AsPointer(ref span[0])); - - public static Span AllocateCopy(this IAllocator allocator, ReadOnlySpan orig) where T : unmanaged - { var copy = allocator.Allocate(orig.Length); orig.CopyTo(copy); return copy; } - - public static ref T Allocate(this IAllocator allocator) where T : unmanaged - => ref Unsafe.AsRef((void*)allocator.Allocate(sizeof(T))); - public static void Free(this IAllocator allocator, ref T value) where T : unmanaged - => allocator.Free((nint)Unsafe.AsPointer(ref value)); - - public static CString AllocateCString(this IAllocator allocator, string? value) - { - if (value == null) return default; - var bytes = Encoding.UTF8.GetByteCount(value); - var span = allocator.Allocate(bytes + 1); - Encoding.UTF8.GetBytes(value, span); - span[^1] = 0; // In case the allocated span is not cleared. - return new((nint)Unsafe.AsPointer(ref span[0])); - } - - public static CString AllocateCString(this IAllocator allocator, ReadOnlySpan utf8) - { - var copy = allocator.Allocate(utf8.Length + 1); - utf8.CopyTo(copy); - copy[^1] = 0; // In case the allocated span is not cleared. - return new((nint)Unsafe.AsPointer(ref copy[0])); - } -} - -public static class TempAllocator -{ - public const int Capacity = 1024 * 1024; // 1 MB - private static readonly ThreadLocal _allocator - = new(() => new(Capacity)); - - public static ResetOnDispose Use() - { - var allocator = _allocator.Value!; - return new(allocator, allocator.Used); - } - - public sealed class ResetOnDispose - : IAllocator - , IDisposable - { - private readonly ArenaAllocator _allocator; - private readonly int _start; - - public ResetOnDispose(ArenaAllocator allocator, int start) - { _allocator = allocator; _start = start; } - - // IAllocator implementation - public nint Allocate(int byteCount) => _allocator.Allocate(byteCount); - public void Free(nint pointer) { /* Do nothing. */ } - - // IDisposable implementation - public void Dispose() => _allocator.Reset(_start); - } -} - -public class GlobalHeapAllocator - : IAllocator -{ - public nint Allocate(int byteCount) - => Marshal.AllocHGlobal(byteCount); - public void Free(nint pointer) - => Marshal.FreeHGlobal(pointer); -} - -public sealed class ArenaAllocator - : IAllocator - , IDisposable -{ - private nint _buffer; - public int Capacity { get; private set; } - public int Used { get; private set; } = 0; - - public ArenaAllocator(int capacity) - { - if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); - _buffer = Marshal.AllocHGlobal(capacity); - Capacity = capacity; - } - - public void Dispose() - { - Marshal.FreeHGlobal(_buffer); - _buffer = default; - Capacity = 0; - } - - public nint Allocate(int byteCount) - { - if (_buffer == default) throw new ObjectDisposedException(nameof(ArenaAllocator)); - if (Used + byteCount > Capacity) throw new InvalidOperationException( - $"Cannot allocate more than {Capacity} bytes with this {nameof(ArenaAllocator)}"); - var ptr = _buffer + Used; - Used += byteCount; - return ptr; - } - - public void Free(nint pointer) - { /* Do nothing. */ } - - public unsafe void Reset(int start = 0) - { - if (start > Used) throw new ArgumentOutOfRangeException(nameof(start)); - new Span((void*)(_buffer + start), Used - start).Clear(); - Used = start; - } -} - -public sealed class RingAllocator - : IAllocator - , IDisposable -{ - private nint _buffer; - private int _current = 0; - public int Capacity { get; private set; } - - public RingAllocator(int capacity) - { - if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); - _buffer = Marshal.AllocHGlobal(capacity); - Capacity = capacity; - } - - public void Dispose() - { - Marshal.FreeHGlobal(_buffer); - _buffer = default; - Capacity = 0; - } - - public nint Allocate(int byteCount) - { - if (_buffer == default) throw new ObjectDisposedException(nameof(RingAllocator)); - if (byteCount > Capacity) throw new ArgumentOutOfRangeException(nameof(byteCount)); - if (_current + byteCount > Capacity) _current = 0; - var ptr = _buffer + _current; - _current += byteCount; - return ptr; - } - - public void Free(nint pointer) - { /* IGNORE */ } -} diff --git a/src/gaemstone/Utility/CStringExtensions.cs b/src/gaemstone/Utility/CStringExtensions.cs deleted file mode 100644 index 0cc756b..0000000 --- a/src/gaemstone/Utility/CStringExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using static flecs_hub.flecs; -using static flecs_hub.flecs.Runtime; - -namespace gaemstone.Utility; - -public unsafe static class CStringExtensions -{ - public static CString Empty { get; } = (CString)""; - public static CString ETX { get; } = (CString)"\x3"; // TODO: Temporary, until flecs supports Empty. - - public static unsafe byte[]? FlecsToBytes(this CString str) - { - if (str.IsNull) return null; - var pointer = (byte*)(nint)str; - // Find length of the string by locating the NUL character. - var length = 0; while (true) if (pointer[length++] == 0) break; - // Create span over the region, NUL included, copy it into an array. - return new Span(pointer, length).ToArray(); - } - - public static byte[]? FlecsToBytesAndFree(this CString str) - { var result = str.FlecsToBytes(); str.FlecsFree(); return result; } - - public static string? FlecsToString(this CString str) - => Marshal.PtrToStringUTF8(str); - - public static string? FlecsToStringAndFree(this CString str) - { var result = str.FlecsToString(); str.FlecsFree(); return result; } - - public static void FlecsFree(this CString str) - { if (!str.IsNull) ecs_os_get_api().free_.Data.Pointer((void*)(nint)str); } -} diff --git a/src/gaemstone/Utility/CallbackContextHelper.cs b/src/gaemstone/Utility/CallbackContextHelper.cs deleted file mode 100644 index 4bca09d..0000000 --- a/src/gaemstone/Utility/CallbackContextHelper.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace gaemstone.Utility; - -public static class CallbackContextHelper -{ - private static readonly List _contexts = new(); - - public static nint Create(T context) where T : notnull - { _contexts.Add(context); return _contexts.Count; } - - public static T Get(nint context) - => (T)_contexts[(int)context - 1]; -} diff --git a/src/gaemstone/Utility/CollectionExtensions.cs b/src/gaemstone/Utility/CollectionExtensions.cs index f40bc7e..ef9afae 100644 --- a/src/gaemstone/Utility/CollectionExtensions.cs +++ b/src/gaemstone/Utility/CollectionExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; namespace gaemstone.Utility; @@ -8,11 +7,6 @@ public static class CollectionExtensions // public static TValue GetOrAdd(this IDictionary dict, // TKey key, Func valueFactory) { } - public static T? MaybeGet(this Span span, int index) - where T : struct => MaybeGet((ReadOnlySpan)span, index); - public static T? MaybeGet(this ReadOnlySpan span, int index) - where T : struct => (index >= 0 && index < span.Length) ? span[index] : null; - public static T? FirstOrNull(this IEnumerable enumerable) where T : struct { diff --git a/src/gaemstone/Utility/IL/IterActionGenerator.cs b/src/gaemstone/Utility/IL/IterActionGenerator.cs index 7de3703..ece7f46 100644 --- a/src/gaemstone/Utility/IL/IterActionGenerator.cs +++ b/src/gaemstone/Utility/IL/IterActionGenerator.cs @@ -15,19 +15,20 @@ public unsafe class IterActionGenerator { private static readonly ConstructorInfo _entityRefCtor = typeof(EntityRef).GetConstructors().Single(); - private static readonly PropertyInfo _iteratorUniverseProp = typeof(Iterator).GetProperty(nameof(Iterator.Universe))!; + private static readonly PropertyInfo _iteratorWorldProp = typeof(Iterator).GetProperty(nameof(Iterator.World))!; private static readonly PropertyInfo _iteratorDeltaTimeProp = typeof(Iterator).GetProperty(nameof(Iterator.DeltaTime))!; private static readonly PropertyInfo _iteratorCountProp = typeof(Iterator).GetProperty(nameof(Iterator.Count))!; private static readonly MethodInfo _iteratorEntityMethod = typeof(Iterator).GetMethod(nameof(Iterator.Entity))!; private static readonly MethodInfo _iteratorFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field))!; - private static readonly MethodInfo _iteratorMaybeFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.MaybeField))!; + private static readonly MethodInfo _iteratorFieldOrEmptyMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldOrEmpty))!; private static readonly MethodInfo _handleFromIntPtrMethod = typeof(GCHandle).GetMethod(nameof(GCHandle.FromIntPtr))!; private static readonly PropertyInfo _handleTargetProp = typeof(GCHandle).GetProperty(nameof(GCHandle.Target))!; private static readonly ConditionalWeakTable _cache = new(); private static readonly Dictionary>> _globalUniqueParameters = new() { - [typeof(Universe)] = (IL, iter) => { IL.Load(iter, _iteratorUniverseProp); }, + [typeof(World)] = (IL, iter) => { IL.Load(iter, _iteratorWorldProp); }, + [typeof(Universe)] = (IL, iter) => { IL.Load(iter, _iteratorWorldProp); IL.Cast(typeof(Universe)); }, [typeof(TimeSpan)] = (IL, iter) => { IL.Load(iter, _iteratorDeltaTimeProp); }, }; private static readonly Dictionary, ILocal>> _uniqueParameters = new() { @@ -35,7 +36,7 @@ public unsafe class IterActionGenerator [typeof(EntityRef)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iteratorEntityMethod); }, }; - public Universe Universe { get; } + public World World { get; } public MethodInfo Method { get; } public IReadOnlyList Parameters { get; } @@ -49,9 +50,9 @@ public unsafe class IterActionGenerator catch { Console.Error.WriteLine(ReadableString); throw; } } - public IterActionGenerator(Universe universe, MethodInfo method) + public IterActionGenerator(World world, MethodInfo method) { - Universe = universe; + World = world; Method = method; Parameters = method.GetParameters().Select(ParamInfo.Build).ToImmutableArray(); @@ -70,8 +71,8 @@ public unsafe class IterActionGenerator { paramData.Add((p, null, null, null)); continue; } // Create a term to add to the query. - var term = new Term(universe.LookupOrThrow(p.UnderlyingType)) { - Source = (p.Source != null) ? (TermID)Universe.LookupOrThrow(p.Source) : null, + 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, @@ -107,17 +108,19 @@ public unsafe class IterActionGenerator break; case ParamKind.Nullable or ParamKind.Or: - IL.Comment($"{p.Info.Name}Field = iterator.MaybeField<{p.FieldType.Name}>({fieldIndex})"); + 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(_iteratorMaybeFieldMethod.MakeGenericMethod(p.FieldType)); + IL.Call(_iteratorFieldOrEmptyMethod.MakeGenericMethod(p.FieldType)); IL.Store(fieldLocal); - IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});"); - tempLocal = IL.Local(p.ParameterType); - IL.LoadAddr(tempLocal); - IL.Init(tempLocal.LocalType); + 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: @@ -164,20 +167,22 @@ public unsafe class IterActionGenerator 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 {info.ParameterType.GetFriendlyName()}"); + IL.Comment($"Global unique parameter {paramName}"); _globalUniqueParameters[info.ParameterType](IL, iteratorArg); break; case ParamKind.Unique: - IL.Comment($"Unique parameter {info.ParameterType.GetFriendlyName()}"); + 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; @@ -187,7 +192,7 @@ public unsafe class IterActionGenerator var spanItemMethod = spanType.GetProperty("Item")!.GetMethod!; var spanLengthMethod = spanType.GetProperty("Length")!.GetMethod!; - IL.Comment($"Parameter {info.ParameterType.GetFriendlyName()}"); + IL.Comment($"Parameter {paramName}"); if (info.IsByRef) { IL.LoadAddr(fieldLocal!); if (info.IsFixed) IL.LoadConst(0); @@ -199,6 +204,15 @@ public unsafe class IterActionGenerator else IL.Load(indexLocal!); IL.Call(spanItemMethod); IL.LoadObj(info.FieldType); + + if (!isValueType) { + IL.Comment($"Convert nint to {paramName}"); + IL.Call(_handleFromIntPtrMethod); + IL.Store(handleLocal!); + IL.LoadAddr(handleLocal!); + IL.Call(_handleTargetProp.GetMethod!); + IL.Cast(info.UnderlyingType); + } } else { var elseLabel = IL.DefineLabel(); var doneLabel = IL.DefineLabel(); @@ -210,24 +224,20 @@ public unsafe class IterActionGenerator else IL.Load(indexLocal!); IL.Call(spanItemMethod); IL.LoadObj(info.FieldType); - if (info.Kind == ParamKind.Nullable) - IL.New(info.ParameterType); + if (!isValueType) { + IL.Comment($"Convert nint to {paramName}"); + IL.Call(_handleFromIntPtrMethod); + IL.Store(handleLocal!); + IL.LoadAddr(handleLocal!); + IL.Call(_handleTargetProp.GetMethod!); + IL.Cast(info.UnderlyingType); + } else IL.New(info.ParameterType); IL.Goto(doneLabel); IL.MarkLabel(elseLabel); - if (info.Kind == ParamKind.Nullable) - IL.LoadObj(tempLocal!); - else IL.LoadNull(); + if (!isValueType) IL.LoadNull(); + else IL.LoadObj(tempLocal!); IL.MarkLabel(doneLabel); } - - if (!isValueType) { - IL.Comment($"Convert nint to {info.UnderlyingType.GetFriendlyName()}"); - IL.Call(_handleFromIntPtrMethod); - IL.Store(handleLocal!); - IL.LoadAddr(handleLocal!); - IL.Call(_handleTargetProp.GetMethod!); - IL.Cast(info.UnderlyingType); - } break; } } @@ -243,8 +253,8 @@ public unsafe class IterActionGenerator ReadableString = IL.ToReadableString(); } - public static IterActionGenerator GetOrBuild(Universe universe, MethodInfo method) - =>_cache.GetValue(method, m => new IterActionGenerator(universe, m)); + public static IterActionGenerator GetOrBuild(World world, MethodInfo method) + =>_cache.GetValue(method, m => new IterActionGenerator(world, m)); public class ParamInfo { @@ -352,7 +362,7 @@ public unsafe class IterActionGenerator /// /// Not part of the resulting query's terms. /// Same value across a single invocation of a callback. - /// For example or . + /// For example or . /// GlobalUnique, diff --git a/src/gaemstone/Utility/SpanToRef.cs b/src/gaemstone/Utility/SpanToRef.cs deleted file mode 100644 index b9f169b..0000000 --- a/src/gaemstone/Utility/SpanToRef.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace gaemstone.Utility; - -public readonly ref struct SpanToRef - where T : class -{ - private readonly Span _span; - - public int Length => _span.Length; - public T? this[int index] => (_span[index] != 0) - ? (T)((GCHandle)_span[index]).Target! : null; - - internal SpanToRef(Span span) => _span = span; - - public void Clear() => _span.Clear(); - public void CopyTo(SpanToRef dest) => _span.CopyTo(dest._span); -} diff --git a/src/gaemstone/gaemstone.csproj b/src/gaemstone/gaemstone.csproj index ff3667a..aeb1b19 100644 --- a/src/gaemstone/gaemstone.csproj +++ b/src/gaemstone/gaemstone.csproj @@ -3,13 +3,13 @@ preview net7.0 + true disable enable - true - +