Big refactor and Entity Inspector

- Split core ECS wrapper into gaemstone.ECS project
  - Module loading part of gaemstone, still
  - Remove ComponentHooks (for now)
- Fix IL for nullable references in IterActionGenerator
- Move from Silk.NET.Maths to System.Numerics
- Add Flecs.Core.Component and .Identifier
- Add Flecs.Doc module
- Add gaemstone.Doc module
- Add [Symbol] to register entities with symbols
  This decision will probably be reversed.
- World generator picks random blocks, more color!

Changes relating to ImGui:
- Use custom ImGUI.NET version with local changes to get
  access to internal functions. Currently waiting for
  upstream to implement this functionality.
- Add font support, and the fonts OpenSans and ForkAwesome
- Add an entity inspector window to browse entities, see
  their contents and find references to other entities.
wip/source-generators
copygirl 1 year ago
parent 0c6d63af21
commit 5268d97828
  1. 4
      .editorconfig
  2. 6
      .gitmodules
  3. 103
      gaemstone.sln
  4. 1
      src/Immersion/Immersion.csproj
  5. 2
      src/Immersion/ObserverTest.cs
  6. 17
      src/Immersion/Program.cs
  7. 1
      src/flecs-cs
  8. 16
      src/gaemstone.Bloxel/BlockFacing.cs
  9. 8
      src/gaemstone.Bloxel/BlockPos.cs
  10. 8
      src/gaemstone.Bloxel/ChunkPos.cs
  11. 34
      src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs
  12. 4
      src/gaemstone.Bloxel/Components/CoreComponents.cs
  13. 4
      src/gaemstone.Bloxel/Neighbor.cs
  14. 28
      src/gaemstone.Bloxel/Systems/BasicWorldGenerator.cs
  15. 2
      src/gaemstone.Bloxel/Utility/ChunkedOctree.cs
  16. 5
      src/gaemstone.Bloxel/gaemstone.Bloxel.csproj
  17. 88
      src/gaemstone.Client/Color.cs
  18. 6
      src/gaemstone.Client/Components/CameraComponents.cs
  19. 20
      src/gaemstone.Client/Components/InputComponents.cs
  20. 16
      src/gaemstone.Client/Components/RenderingComponents.cs
  21. 6
      src/gaemstone.Client/Components/ResourceComponents.cs
  22. BIN
      src/gaemstone.Client/Resources/ForkAwesome.ttf
  23. 95
      src/gaemstone.Client/Resources/LICENSE_ForkAwesome.txt
  24. 93
      src/gaemstone.Client/Resources/LICENSE_OpenSans.txt
  25. BIN
      src/gaemstone.Client/Resources/OpenSans.Bold.ttf
  26. BIN
      src/gaemstone.Client/Resources/OpenSans.Italic.ttf
  27. BIN
      src/gaemstone.Client/Resources/OpenSans.ttf
  28. 763
      src/gaemstone.Client/Systems/EntityInspector.cs
  29. 38
      src/gaemstone.Client/Systems/FreeCameraController.cs
  30. 20
      src/gaemstone.Client/Systems/ImGuiDemoWindow.cs
  31. 233
      src/gaemstone.Client/Systems/ImGuiInputDebug.cs
  32. 96
      src/gaemstone.Client/Systems/ImGuiManager.cs
  33. 24
      src/gaemstone.Client/Systems/InputManager.cs
  34. 10
      src/gaemstone.Client/Systems/MeshManager.cs
  35. 32
      src/gaemstone.Client/Systems/Renderer.cs
  36. 6
      src/gaemstone.Client/Systems/Windowing.cs
  37. 819
      src/gaemstone.Client/Utility/ForkAwesome.cs
  38. 55
      src/gaemstone.Client/Utility/ImGuiUtility.cs
  39. 5
      src/gaemstone.Client/gaemstone.Client.csproj
  40. 1
      src/gaemstone.ECS
  41. 12
      src/gaemstone/Components/TransformComponents.cs
  42. 86
      src/gaemstone/Doc.cs
  43. 53
      src/gaemstone/ECS/Attributes.cs
  44. 36
      src/gaemstone/ECS/Component.cs
  45. 90
      src/gaemstone/ECS/ComponentHooks.cs
  46. 48
      src/gaemstone/ECS/Entity.cs
  47. 48
      src/gaemstone/ECS/EntityBase.cs
  48. 116
      src/gaemstone/ECS/EntityBuilder.cs
  49. 233
      src/gaemstone/ECS/EntityPath.cs
  50. 169
      src/gaemstone/ECS/EntityRef.cs
  51. 25
      src/gaemstone/ECS/EntityType.cs
  52. 87
      src/gaemstone/ECS/Filter.cs
  53. 16
      src/gaemstone/ECS/FilterExtensions.cs
  54. 2
      src/gaemstone/ECS/Game.cs
  55. 54
      src/gaemstone/ECS/Identifier.cs
  56. 41
      src/gaemstone/ECS/IdentifierRef.cs
  57. 116
      src/gaemstone/ECS/Iterator.cs
  58. 6
      src/gaemstone/ECS/Module.cs
  59. 32
      src/gaemstone/ECS/Observer.cs
  60. 46
      src/gaemstone/ECS/Query.cs
  61. 22
      src/gaemstone/ECS/Relation.cs
  62. 31
      src/gaemstone/ECS/Rule.cs
  63. 71
      src/gaemstone/ECS/System.cs
  64. 116
      src/gaemstone/ECS/Term.cs
  65. 63
      src/gaemstone/ECS/Universe+Lookup.cs
  66. 51
      src/gaemstone/ECS/Universe.cs
  67. 53
      src/gaemstone/Flecs/Core.cs
  68. 4
      src/gaemstone/Flecs/DeletionEvent.cs
  69. 81
      src/gaemstone/Flecs/Doc.cs
  70. 36
      src/gaemstone/Flecs/FlecsException.cs
  71. 2
      src/gaemstone/Flecs/ObserverEvent.cs
  72. 2
      src/gaemstone/Flecs/Pipeline.cs
  73. 2
      src/gaemstone/Flecs/SystemPhase.cs
  74. 4
      src/gaemstone/Flecs/Systems/Monitor.cs
  75. 6
      src/gaemstone/Flecs/Systems/Rest.cs
  76. 188
      src/gaemstone/Universe+Modules.cs
  77. 35
      src/gaemstone/Universe.cs
  78. 167
      src/gaemstone/Utility/Allocators.cs
  79. 34
      src/gaemstone/Utility/CStringExtensions.cs
  80. 14
      src/gaemstone/Utility/CallbackContextHelper.cs
  81. 6
      src/gaemstone/Utility/CollectionExtensions.cs
  82. 78
      src/gaemstone/Utility/IL/IterActionGenerator.cs
  83. 19
      src/gaemstone/Utility/SpanToRef.cs
  84. 4
      src/gaemstone/gaemstone.csproj

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

6
.gitmodules vendored

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

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

@ -15,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="../gaemstone.Bloxel/gaemstone.Bloxel.csproj" />
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" />
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
</ItemGroup>
<ItemGroup>

@ -11,7 +11,7 @@ namespace Immersion;
public class ObserverTest
{
[Observer<ObserverEvent.OnSet>]
[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!");
}

@ -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<Game>();
var game = universe.LookupByTypeOrThrow<Game>();
universe.Modules.Register<gaemstone.Flecs.Systems.Rest>();
universe.Modules.Register<gaemstone.Flecs.Systems.Monitor>();
@ -43,6 +42,7 @@ universe.Modules.Register<gaemstone.Components.TransformComponents>();
universe.Modules.Register<gaemstone.Client.Components.RenderingComponents>();
universe.Modules.Register<gaemstone.Client.Systems.Renderer>();
universe.Modules.Register<gaemstone.Client.Systems.ImGuiManager>();
universe.Modules.Register<gaemstone.Client.Systems.ImGuiDemoWindow>();
universe.Modules.Register<gaemstone.Client.Components.ResourceComponents>();
universe.Modules.Register<gaemstone.Client.Systems.TextureManager>();
@ -51,6 +51,7 @@ universe.Modules.Register<gaemstone.Client.Systems.MeshManager>();
universe.Modules.Register<gaemstone.Client.Components.InputComponents>();
universe.Modules.Register<gaemstone.Client.Systems.InputManager>();
universe.Modules.Register<gaemstone.Client.Systems.ImGuiInputDebug>();
universe.Modules.Register<gaemstone.Client.Systems.EntityInspector>();
universe.Modules.Register<gaemstone.Client.Components.CameraComponents>();
universe.Modules.Register<gaemstone.Client.Systems.FreeCameraController>();
@ -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>(texture)

@ -1 +0,0 @@
Subproject commit a2047983917aa462a8c2f34d5315aea48502f4d8

@ -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<float> ToVector3(this BlockFacing self)
public static Vector3 ToVector3(this BlockFacing self)
=> self switch {
BlockFacing.East => Vector3D<float>.UnitX,
BlockFacing.West => -Vector3D<float>.UnitX,
BlockFacing.Up => Vector3D<float>.UnitY,
BlockFacing.Down => -Vector3D<float>.UnitY,
BlockFacing.South => Vector3D<float>.UnitZ,
BlockFacing.North => -Vector3D<float>.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))
};

@ -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<float> GetOrigin() => new(X, Y, Z);
public Vector3D<float> 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<float> self)
public static BlockPos ToBlockPos(this Vector3 self)
=> new((int)MathF.Floor(self.X), (int)MathF.Floor(self.Y), (int)MathF.Floor(self.Z));
}

@ -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<float> GetOrigin() => new(
public Vector3 GetOrigin() => new(
X << ChunkBitShift, Y << ChunkBitShift, Z << ChunkBitShift);
public Vector3D<float> 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<float> 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);

@ -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<float>[][] OffsetPerFacing = {
new Vector3D<float>[]{ new(1,1,1), new(1,0,1), new(1,0,0), new(1,1,0) }, // East (+X)
new Vector3D<float>[]{ new(0,1,0), new(0,0,0), new(0,0,1), new(0,1,1) }, // West (-X)
new Vector3D<float>[]{ new(1,1,0), new(0,1,0), new(0,1,1), new(1,1,1) }, // Up (+Y)
new Vector3D<float>[]{ new(1,0,1), new(0,0,1), new(0,0,0), new(1,0,0) }, // Down (-Y)
new Vector3D<float>[]{ new(0,1,1), new(0,0,1), new(1,0,1), new(1,1,1) }, // South (+Z)
new Vector3D<float>[]{ 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<float>[] _vertices = new Vector3D<float>[StartingCapacity];
private Vector3D<float>[] _normals = new Vector3D<float>[StartingCapacity];
private Vector2D<float>[] _uvs = new Vector2D<float>[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<float>(x, y, z);
var textureCell = block.Get<TextureCoords4>();
var blockVertex = new Vector3(x, y, z);
var textureCell = block.GetOrThrow<TextureCoords4>();
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<Game>().Get<Canvas>().GL;
var GL = universe.LookupByTypeOrThrow<Game>().GetOrThrow<Canvas>().GL;
return (indexCount > 0)
? MeshManager.Create(GL,
_indices.AsSpan(0, indexCount), _vertices.AsSpan(0, vertexCount),

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

@ -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<float> ToVector3(this Neighbor self)
public static Vector3 ToVector3(this Neighbor self)
{ var (x, y, z) = self; return new(x, y, z); }

@ -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<HasBasicWorldGeneration>();
}

@ -72,7 +72,7 @@ public class ChunkedOctree<T>
while (enumerator.MoveNext()) yield return enumerator.Current;
}
public sealed class Enumerator
public class Enumerator
: IEnumerator<(ChunkPos ChunkPos, T Value, float Weight)>
{
private readonly ChunkedOctree<T> _octree;

@ -14,6 +14,11 @@
<ItemGroup>
<ProjectReference Include="../gaemstone/gaemstone.csproj" />
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" />
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Silk.NET" Version="2.16.0" />
</ItemGroup>
</Project>

@ -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<Color>
{
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<float>(Color color) => new(color.R / 255F, color.G / 255F, color.B / 255F, color.A / 255F);
public static implicit operator Vector4D<byte>(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);
/// <summary> Converts a float clamped to range [0.0, 1.0] into a byte in range [0, 255]. </summary>
private static byte F2B(float f) => (byte)(Math.Clamp(f, 0.0f, 1.0f) * 255);
/// <summary> Converts a byte in range [0, 255] into a float in range [0.0, 1.0]. </summary>
private static float B2F(byte b) => b / 255f;
}

@ -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<byte> ClearColor { get; set; }
public Rectangle<int> Viewport { get; set; }
public Color ClearColor { get; set; }
public Rectangle Viewport { get; set; }
}
}

@ -7,30 +7,30 @@ namespace gaemstone.Client.Components;
[Module]
public class InputComponents
{
[Entity(Global = true)]
[Symbol, Path("/Input")]
[Add<Input>]
public struct Input { }
[Entity("Input", "Mouse", Global = true)]
[Symbol, Path("/Input/Mouse")]
[Add<Mouse>]
public struct Mouse { }
[Entity("Input", "Keyboard", Global = true)]
[Symbol, Path("/Input/Keyboard")]
[Add<Keyboard>]
public struct Keyboard { }
[Tag]
[Symbol, Tag]
public struct Gamepad { }
/// <summary> Present on inputs / actions that are currently active. </summary>
[Component] public struct Active { public TimeSpan Duration; }
[Symbol, Component] public struct Active { public TimeSpan Duration; }
/// <summary> Present on inputs / actions were activated this frame. </summary>
[Tag] public struct Activated { }
[Symbol, Tag] public struct Activated { }
/// <summary> Present on inputs / actions were deactivated this frame. </summary>
[Tag] public struct Deactivated { }
[Symbol, Tag] public struct Deactivated { }
/// <summary>
@ -41,7 +41,7 @@ public class InputComponents
/// This is set if a UI element is focused that captures
/// navigational or text input.
/// </remarks>
[Tag, Relation, Exclusive]
[Symbol, Relation, Tag, Exclusive]
public struct InputCapturedBy { }
/// <summary>
@ -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.
/// </remarks>
[Tag, Relation, Exclusive]
[Symbol, Relation, Tag, Exclusive]
public struct MouseInputCapturedBy { }
/// <summary>
@ -62,7 +62,7 @@ public class InputComponents
/// <remarks>
/// This is set when a camera controller assumes control of the mouse.
/// </remarks>
[Tag, Relation, Exclusive]
[Symbol, Relation, Tag, Exclusive]
[With<InputCapturedBy>]
[With<MouseInputCapturedBy>]
public struct CursorCapturedBy { }

@ -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<float> TopLeft { get; }
public Vector2D<float> TopRight { get; }
public Vector2D<float> BottomLeft { get; }
public Vector2D<float> 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)
{

@ -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<Resource>]
[Symbol, Relation, Tag, IsA<Resource>]
public struct Texture { }
[Relation, Tag, IsA<Resource>]
[Symbol, Relation, Tag, IsA<Resource>]
public struct Mesh { }
}

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

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

@ -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<gaemstone.Client.Systems.ImGuiManager>]
public class EntityInspector
: IModuleInitializer
{
[Tag]
public struct InspectorWindow { }
[Relation, Exclusive]
[Add<DeletionEvent.OnDeleteTarget, DeletionBehavior.Delete>]
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<Doc.DisplayType>()
.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<InspectorWindow>();
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<Selected>().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<EntityRef, History?, EntityRef?> 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<ObserverEvent.OnRemove>]
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<Expanded, Core.Wildcard>();
if (IconButtonWithToolTip(Icon.Outdent, "Collapse all items in the Explorer View", hasExpanded))
window.Remove<Expanded, Core.Wildcard>();
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<ScrollToSelected>();
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<EntitySummary>
{
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>(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<T>.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<Expanded>();
List<EntitySummary> GetSummaries(Entity? parent) {
var result = new List<EntitySummary>();
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<Core.Identifier>(2);
var docNames = iter.FieldOrEmpty<Flecs.Doc.Description>(3);
var docColors = iter.FieldOrEmpty<Flecs.Doc.Description>(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<Core.Component>(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<ScrollToSelected>() && isSelected) {
ImGui.SetScrollHereY();
window.Remove<ScrollToSelected>();
}
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<Core.ChildOf>();
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<Core.ChildOf>();
var Wildcard = world.LookupByTypeOrThrow<Core.Wildcard>();
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<InspectorWindow>().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<Selected>(entity);
else window.Remove<Selected, Core.Wildcard>();
for (var parent = entity?.Parent; parent != null; parent = parent.Parent)
window.Add<Expanded>(parent);
if ((entity != null) && scrollTo)
window.Add<ScrollToSelected>();
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<Core.Component>();
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<Core.Component>(component)?.Size == 0))
type = world.LookupByTypeOrThrow<Core.Tag>();
var priority = type.GetOrNull<DocPriority>()?.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<DocIcon>()?.Value.ToString() ?? displayType?.GetOrNull<DocIcon>()?.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();
}
}
}

@ -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<Input>();
var mouse = universe.Lookup<Mouse>();
var keyboard = universe.Lookup<Keyboard>();
var input = universe.LookupByType<Input>();
var mouse = universe.LookupByType<Mouse>();
var keyboard = universe.LookupByType<Keyboard>();
if ((input == null) || (mouse == null) || (keyboard == null)) return;
var module = universe.LookupOrThrow<FreeCameraController>();
var capturedBy = input.GetTarget<CursorCapturedBy>();
var inputCapturedBy = input.GetTarget<MouseInputCapturedBy>();
var module = universe.LookupByTypeOrThrow<FreeCameraController>();
var capturedBy = input.GetTargets<CursorCapturedBy>().FirstOrDefault();
var inputCapturedBy = input.GetTargets<MouseInputCapturedBy>().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<Active>() == true;
&& mouse.LookupChild("Buttons/Right")?.Has<Active>() == true;
if (isMouseDown != isCaptured) {
if (isMouseDown)
input.Add<CursorCapturedBy>(module);
@ -51,27 +51,27 @@ public class FreeCameraController
var mouseMovement = Vector2.Zero;
if (isCaptured) {
var raw = (Vector2?)mouse.Lookup("Delta")?.Get<RawValue2D>() ?? default;
var raw = (Vector2?)mouse.LookupChild("Delta")?.GetOrThrow<RawValue2D>() ?? 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<Active>() == true;
var w = keyboard.Lookup("W")?.Has<Active>() == true;
var a = keyboard.Lookup("A")?.Has<Active>() == true;
var s = keyboard.Lookup("S")?.Has<Active>() == true;
var d = keyboard.Lookup("D")?.Has<Active>() == true;
var shift = keyboard.LookupChild("ShiftLeft")?.Has<Active>() == true;
var w = keyboard.LookupChild("W")?.Has<Active>() == true;
var a = keyboard.LookupChild("A")?.Has<Active>() == true;
var s = keyboard.LookupChild("S")?.Has<Active>() == true;
var d = keyboard.LookupChild("D")?.Has<Active>() == 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<float>(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;
}

@ -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<gaemstone.Client.Systems.ImGuiManager>]
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);
}
}

@ -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<gaemstone.Client.Systems.ImGuiManager>]
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<Input>();
var input = universe.LookupByType<Input>();
if (input == null) return;
ImGui.Begin("Input Information", ImGuiWindowFlags.NoResize);
if (!ImGuiUtility.UIButtonToggle(1, ForkAwesome.Gamepad, "Input Information", ref _isOpen)) return;
if (universe.Lookup<Keyboard>() 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<Mouse>() is EntityRef mouse) {
ImGui.BeginChild("Mouse Info", new(160, 180), true);
if (universe.LookupByType<Keyboard>() is EntityRef keyboard)
DrawKeyboard(keyboard);
ImGui.Text("Position: " + (Vector2?)mouse.Lookup("Position")?.MaybeGet<RawValue2D>());
ImGui.Text("Delta: " + (Vector2?)mouse.Lookup("Delta" )?.MaybeGet<RawValue2D>());
ImGui.Text("Wheel: " + (float?) mouse.Lookup("Wheel" )?.MaybeGet<RawValue1D>());
if (universe.LookupByType<Mouse>() is EntityRef mouse) {
ImGui.BeginChild("Mouse Info", new(160, 180), true);
ImGui.Spacing();
ImGui.Text("Position: " + (Vector2?)mouse.LookupChild("Position")?.GetOrNull<RawValue2D>());
ImGui.Text("Delta: " + (Vector2?)mouse.LookupChild("Delta" )?.GetOrNull<RawValue2D>());
ImGui.Text("Wheel: " + (float?) mouse.LookupChild("Wheel" )?.GetOrNull<RawValue1D>());
var buttons = mouse.Lookup("Buttons")?.GetChildren().ToArray() ?? Array.Empty<EntityRef>();
ImGui.Text("Buttons: " + string.Join(" ", buttons
.Where (button => button.Has<Active>())
.Select(button => $"{button.Name} ({button.Get<Active>().Duration.TotalSeconds:f2}s)")));
ImGui.Text(" Pressed: " + string.Join(" ", buttons
.Where (button => button.Has<Activated>())
.Select(button => button.Name)));
ImGui.Text(" Released: " + string.Join(" ", buttons
.Where (button => button.Has<Deactivated>())
.Select(button => button.Name)));
ImGui.Spacing();
ImGui.EndChild();
}
var buttons = mouse.LookupChild("Buttons")?.GetChildren().ToArray() ?? Array.Empty<EntityRef>();
ImGui.Text("Buttons: " + string.Join(" ", buttons
.Where (button => button.Has<Active>())
.Select(button => $"{button.Name} ({button.GetOrThrow<Active>().Duration.TotalSeconds:f2}s)")));
ImGui.Text(" Pressed: " + string.Join(" ", buttons
.Where (button => button.Has<Activated>())
.Select(button => button.Name)));
ImGui.Text(" Released: " + string.Join(" ", buttons
.Where (button => button.Has<Deactivated>())
.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<EntityRef>();
ImGui.Text("Buttons: " + string.Join(" ", buttons.Where(b => b.Has<Active>())
.Select(b => $"{b.Name} ({b.Get<Active>().Duration.TotalSeconds:f2}s)")));
ImGui.Text(" Pressed: " + string.Join(" ", buttons.Where(b => b.Has<Activated>()).Select(b => b.Name)));
ImGui.Text(" Released: " + string.Join(" ", buttons.Where(b => b.Has<Deactivated>()).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<RawValue1D>() ?? default:f2}";
if (trigger.Has<Activated>()) text += " pressed!";
else if (trigger.Has<Deactivated>()) text += " released!";
else if (trigger.MaybeGet<Active>() 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<RawValue2D>() ?? 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<EntityRef>();
ImGui.Text("Buttons: " + string.Join(" ", buttons.Where(b => b.Has<Active>())
.Select(b => $"{b.Name} ({b.GetOrThrow<Active>().Duration.TotalSeconds:f2}s)")));
ImGui.Text(" Pressed: " + string.Join(" ", buttons.Where(b => b.Has<Activated>()).Select(b => b.Name)));
ImGui.Text(" Released: " + string.Join(" ", buttons.Where(b => b.Has<Deactivated>()).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<RawValue1D>() ?? default:f2}";
if (trigger.Has<Activated>()) text += " pressed!";
else if (trigger.Has<Deactivated>()) text += " released!";
else if (trigger.GetOrNull<Active>() 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<RawValue2D>() ?? 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<Key, string> 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<Active>() == 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<Active>() == 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);
}
}
}
}

@ -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<SystemPhase.OnStore>]
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<ImFontConfigPtr>? 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<byte>(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<char>(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<byte>(fontData);
fixed (byte* dataPtr = fontDataSpan)
return io.Fonts.AddFontFromMemoryTTF((IntPtr)dataPtr, size, size, cfg);
}
[System<SystemPhase.OnLoad>]
public unsafe void Initialize(Universe universe, GameWindow window, Canvas canvas,
[Source<Input>] IInputContext inputContext, [Not] ImGuiData _)
=> universe.LookupOrThrow<ImGuiData>().Set(new ImGuiData(
=> universe.LookupByTypeOrThrow<ImGuiData>().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<ImGuiKey>()) {
@ -62,7 +130,7 @@ public class ImGuiManager
}
if (Enum.TryParse<Key>(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<Mouse>] IMouse impl, ImGuiData _)
{
var input = universe.LookupOrThrow<Input>();
var module = universe.LookupOrThrow<ImGuiManager>();
var capturedBy = input.GetTarget<MouseInputCapturedBy>();
var input = universe.LookupByTypeOrThrow<Input>();
var module = universe.LookupByTypeOrThrow<ImGuiManager>();
var capturedBy = input.GetTargets<MouseInputCapturedBy>().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<ImGuiRenderPhase>]
public static void Render(ImGuiData imgui)
=> imgui.Controller.Render();

@ -15,28 +15,29 @@ namespace gaemstone.Client.Systems;
[DependsOn<gaemstone.Client.Systems.Windowing>]
public class InputManager
{
[Component("InputContext"), Proxy<IInputContext>] public struct ContextProxy { }
[Component, Path("InputContext"), Proxy<IInputContext>] public struct ContextProxy { }
[Component("MouseImpl" ), Proxy<IMouse >] public struct MouseProxy { }
[Component("KeyboardImpl"), Proxy<IKeyboard>] public struct KeyboardProxy { }
[Component("GamepadImpl" ), Proxy<IGamepad >] public struct GamepadProxy { }
[Component, Path("MouseImpl" ), Proxy<IMouse >] public struct MouseProxy { }
[Component, Path("KeyboardImpl"), Proxy<IKeyboard>] public struct KeyboardProxy { }
[Component, Path("GamepadImpl" ), Proxy<IGamepad >] public struct GamepadProxy { }
[System<SystemPhase.OnLoad>]
public static void Initialize(Universe universe,
[Game] GameWindow window, [Source<Input>, Not] IInputContext _)
{
var input = universe.LookupOrThrow<Input>();
var input = universe.LookupByTypeOrThrow<Input>();
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<Gamepad>().Set(impl).Build();
// TODO: Should we even support joysticks?
}
@ -44,13 +45,13 @@ public class InputManager
[Observer<ObserverEvent.OnAdd>]
[Expression("CursorCapturedBy(Input, *)")]
public static void OnCursorCaptured(Universe universe)
=> universe.LookupOrThrow<Mouse>().Get<IMouse>()
=> universe.LookupByTypeOrThrow<Mouse>().GetOrThrow<IMouse>()
.Cursor.CursorMode = CursorMode.Raw;
[Observer<ObserverEvent.OnRemove>]
[Expression("CursorCapturedBy(Input, *)")]
public static void OnCursorReleased(Universe universe)
=> universe.LookupOrThrow<Mouse>().Get<IMouse>()
=> universe.LookupByTypeOrThrow<Mouse>().GetOrThrow<IMouse>()
.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<Deactivated>();
[System<SystemPhase.PostFrame>]
// [Expression("Activated || Deactivated")]
public static void ClearDeActivated(EntityRef entity, [Or] Activated _1, [Or] Deactivated _2)
=> entity.Remove<Activated>().Remove<Deactivated>();
}

@ -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<ushort> indices, ReadOnlySpan<Vector3D<float>> vertices,
ReadOnlySpan<Vector3D<float>> normals, ReadOnlySpan<Vector2D<float>> uvs)
ReadOnlySpan<ushort> indices, ReadOnlySpan<Vector3> vertices,
ReadOnlySpan<Vector3> normals, ReadOnlySpan<Vector2> 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<Vector3D<float>> vertices,
ReadOnlySpan<Vector3D<float>> normals, ReadOnlySpan<Vector2D<float>> uvs)
public static MeshHandle Create(GL GL, ReadOnlySpan<Vector3> vertices,
ReadOnlySpan<Vector3> normals, ReadOnlySpan<Vector2> uvs)
{
var vao = GL.GenVertexArray();
GL.BindVertexArray(vao);

@ -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<float>(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<GlobalTransform>(1);
var meshes = iter.Field<MeshHandle>(3);
// var texPairs = iter.MaybeField<Identifier>(4);
var textures = iter.MaybeField<TextureHandle>(5);
// var texPairs = iter.FieldOrEmpty<Identifier>(4);
var textures = iter.FieldOrEmpty<TextureHandle>(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); }

@ -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<int> Size { get; set; }
public Size Size { get; set; }
public Color BackgroundColor { get; set; }
}
@ -29,7 +29,7 @@ public class Windowing
[System<SystemPhase.PreFrame>]
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();
}
}

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

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

@ -3,9 +3,9 @@
<PropertyGroup>
<LangVersion>preview</LangVersion>
<TargetFramework>net7.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@ -13,8 +13,9 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../flecs-cs/src/cs/production/Flecs/Flecs.csproj" />
<ProjectReference Include="../gaemstone/gaemstone.csproj" />
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
<ProjectReference Include="../ImGui.NET/src/ImGui.NET/ImGui.NET.csproj" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1 @@
Subproject commit f9fa808d6842f5b208bbdfa1925f156342ed8a60

@ -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<float> Value;
public GlobalTransform(Matrix4X4<float> value) => Value = value;
public static implicit operator GlobalTransform(in Matrix4X4<float> value) => new(value);
public static implicit operator Matrix4X4<float>(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;
}
}

@ -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.
/// <summary>
/// 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.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class Name : Attribute
{
public string Value { get; }
public Name(string value) => Value = value;
}
/// <summary>
/// A brief description of this entity.
/// Displayed in the Entity Inspector.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class Brief : Attribute
{
public string Value { get; }
public Brief(string value) => Value = value;
}
/// <summary>
/// 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.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class Detail : Attribute
{
public string Value { get; }
public Detail(string value) => Value = value;
}
/// <summary>
/// A link to a website relating to this entity, such as
/// a module's repository, or further documentation.
/// Displayed in the Entity Inspector.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class Link : Attribute
{
public string Value { get; }
public Link(string value) => Value = value;
}
/// <summary>
/// A custom color to represent this entity.
/// Displayed in the Entity Inspector.
/// </summary>
[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;
}
}
}

@ -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 <see cref="ModuleManager.Register"/>,
/// an entity is automatically created and <see cref="LookupExtensions.CreateLookup"/>
/// called on it, meaning it can be looked up using <see cref="Universe.Lookup(Type)"/>.
/// called on it, meaning it can be looked up using <see cref="World.LookupByType(Type)"/>.
/// </summary>
public interface ICreateEntityAttribute { }
[AttributeUsage(AttributeTargets.Struct)]
public class EntityAttribute : Attribute, ICreateEntityAttribute { }
/// <summary>
/// Use a custom name or path for this entity instead of the type's name.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class PathAttribute : Attribute, ICreateEntityAttribute
{
public string Value { get; }
public PathAttribute(string value) => Value = value;
}
/// <summary>
/// Register the entity under a globally unique symbol.
/// Uses the type's name by default.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class SymbolAttribute : Attribute, ICreateEntityAttribute
{
public string? Value { get; }
public SymbolAttribute() { }
public SymbolAttribute(string value) => Value = value;
}
/// <summary>
/// A singleton is a single instance of a tag or component that can be retrieved
/// without explicitly specifying an entity in a query, where it is equivalent
/// to <see cref="SourceAttribute{}"/> with itself as the generic type parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class SingletonAttribute : Attribute
{
/// <summary> Whether to add the entity to itself on registration. </summary>
public bool AutoAdd { get; init; } = false;
}
public class SingletonAttribute : Attribute, ICreateEntityAttribute
{ public bool AutoAdd { get; init; } = true; }
/// <summary>
/// Register the proxied type instead of the one marked with this attribute.
@ -46,6 +68,25 @@ public class AddAttribute<TEntity> : AddEntityAttribute
public class AddAttribute<TRelation, TTarget> : AddRelationAttribute
{ public AddAttribute() : base(typeof(TRelation), typeof(TTarget)) { } }
/// <summary>
/// Marked entity represents a relationship type.
/// It may be used as the "relation" in a pair.
/// </summary>
/// <remarks>
/// The relationship may have component data associated with
/// it when added to an entity under these circumstances:
/// <list type="bullet">
/// <item>If marked as a <see cref="TagAttribute"/>, does not carry data.</item>
/// <item>If marked as a <see cref="ComponentAttribute"/>, carries the relation's data.</item>
/// <item>If marked with neither, will carry the target's data, if it's a component.</item>
/// </list>
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class RelationAttribute : Attribute, ICreateEntityAttribute { }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class ComponentAttribute : Attribute, ICreateEntityAttribute { }
/// <seealso cref="Tag"/>
[AttributeUsage(AttributeTargets.Struct)]
public class TagAttribute : AddAttribute<Tag>, ICreateEntityAttribute { }

@ -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<T>(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);
}
}

@ -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<Iterator>? _onAdd;
public event Action<Iterator>? 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<Iterator>? _onSet;
public event Action<Iterator>? 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<Iterator>? _onRemove;
public event Action<Iterator>? 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<nint, int>? _constructor;
public event Action<nint, int>? Construct {
add { if ((_constructor += value) != null) Handle->ctor.Data.Pointer = &CallbackConstructor; }
remove { if ((_constructor -= value) == null) Handle->ctor.Data.Pointer = null; }
}
private Action<nint, int>? _destructor;
public event Action<nint, int>? Destruct {
add { if ((_destructor += value) != null) Handle->dtor.Data.Pointer = &CallbackDestructor; }
remove { if ((_destructor -= value) == null) Handle->dtor.Data.Pointer = null; }
}
private Action<nint, nint, int>? _copy;
public event Action<nint, nint, int>? Copy {
add { if ((_copy += value) != null) Handle->copy.Data.Pointer = &CallbackCopy; }
remove { if ((_copy -= value) == null) Handle->copy.Data.Pointer = null; }
}
private Action<nint, nint, int>? _move;
public event Action<nint, nint, int>? 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<ComponentHooks>((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<ComponentHooks>((nint)handle->binding_ctx);
}
}
}

@ -1,48 +0,0 @@
using System;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Struct)]
public class EntityAttribute : Attribute, ICreateEntityAttribute
{
/// <summary> If specified, uses this path instead of the default name. </summary>
public string[]? Path { get; }
/// <summary> If true, the path will be absolute instead of relative. </summary>
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<Entity>
{
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;
}

@ -1,48 +0,0 @@
using static gaemstone.Flecs.Core;
namespace gaemstone.ECS;
public abstract class EntityBase<TReturn>
{
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<T>();
public abstract T? MaybeGet<T>() where T : unmanaged;
public abstract T? MaybeGet<T>(T _ = null!) where T : class;
public abstract ref T GetMut<T>() where T : unmanaged;
public abstract ref T GetRefOrNull<T>() where T : unmanaged;
public abstract ref T GetRefOrThrow<T>() where T : unmanaged;
public abstract void Modified<T>();
public abstract TReturn Set<T>(in T value) where T : unmanaged;
public abstract TReturn Set<T>(T obj) where T : class;
public TReturn Add(string symbol) => Add(Universe.LookupSymbolOrThrow(symbol));
public TReturn Add<T>() => Add(Universe.LookupOrThrow(typeof(T)));
public TReturn Add(Entity relation, Entity target) => Add(Identifier.Pair(relation, target));
public TReturn Add<TRelation>(Entity target) => Add(Universe.LookupOrThrow<TRelation>(), target);
public TReturn Add<TRelation, TTarget>() => Add(Universe.LookupOrThrow<TRelation>(), Universe.LookupOrThrow<TTarget>());
public TReturn Remove(string symbol) => Remove(Universe.LookupSymbolOrThrow(symbol));
public TReturn Remove<T>() => Remove(Universe.LookupOrThrow(typeof(T)));
public TReturn Remove(Entity relation, Entity target) => Remove(Identifier.Pair(relation, target));
public TReturn Remove<TRelation>(Entity target) => Remove(Universe.LookupOrThrow<TRelation>(), target);
public TReturn Remove<TRelation, TTarget>() => Remove(Universe.LookupOrThrow<TRelation>(), Universe.LookupOrThrow<TTarget>());
public bool Has(string symbol) => Has(Universe.LookupSymbolOrThrow(symbol));
public bool Has<T>() => Has(Universe.LookupOrThrow(typeof(T)));
public bool Has(Entity relation, Entity target) => Has(Identifier.Pair(relation, target));
public bool Has<TRelation>(Entity target) => Has(Universe.LookupOrThrow<TRelation>(), target);
public bool Has<TRelation, TTarget>() => Has(Universe.LookupOrThrow<TRelation>(), Universe.LookupOrThrow<TTarget>());
public TReturn ChildOf(Entity parent) => Add<ChildOf>(parent);
public TReturn ChildOf<TParent>() => Add<ChildOf, TParent>();
public TReturn Disable() => Add<Disabled>();
public TReturn Enable() => Remove<Disabled>();
public bool IsDisabled => Has<Disabled>();
}

@ -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<EntityBuilder>
{
public override Universe Universe { get; }
/// <summary> Set to modify existing entity (optional). </summary>
public Entity ID { get; set; }
/// <summary>
/// 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.
/// </summary>
public EntityPath? Path { get; set; }
/// <summary>
/// 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.
/// </summary>
public EntityBuilder Symbol(string symbol) { _symbol = symbol; return this; }
private string? _symbol = null;
/// <summary>
/// When set to true, a low id (typically reserved for components)
/// will be used to create the entity, if no id is specified.
/// </summary>
public bool UseLowID { get; set; }
/// <summary> IDs to add to the new or existing entity. </summary>
private readonly HashSet<Identifier> _toAdd = new();
private Entity _parent = Entity.None;
/// <summary> String expression with components to add. </summary>
public string? Expression { get; }
/// <summary> Actions to run once the entity has been created. </summary>
private readonly List<Action<EntityRef>> _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<T>() => throw new NotSupportedException();
public override T? MaybeGet<T>() => throw new NotSupportedException();
public override T? MaybeGet<T>(T _ = null!) where T : class => throw new NotSupportedException();
public override ref T GetMut<T>() => throw new NotSupportedException();
public override ref T GetRefOrNull<T>() => throw new NotSupportedException();
public override ref T GetRefOrThrow<T>() => throw new NotSupportedException();
public override void Modified<T>() => throw new NotImplementedException();
public override EntityBuilder Set<T>(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>(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;
}
}

@ -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<char> 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;
}
/// <summary> Used by <see cref="EntityBuilder.Build"/>. </summary>
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<byte[]>(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<byte> _bytes;
public UTF8View(ReadOnlySpan<byte> bytes)
=> _bytes = bytes;
public int Length => _bytes.Length;
public byte this[int index] => _bytes[index];
public ReadOnlySpan<byte> 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<byte> _bytes;
private int index = 0;
private Rune _current = default;
public Rune Current => _current;
internal Enumerator(ReadOnlySpan<byte> 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;
}
}
}

@ -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<EntityRef>
, IEquatable<EntityRef>
{
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<EntityRef> 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<T>()
{
var comp = Universe.LookupOrThrow<T>();
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<T>(ptr)
: (T)((GCHandle)Unsafe.Read<nint>(ptr)).Target!;
}
public override T? MaybeGet<T>()
{
var comp = Universe.LookupOrThrow<T>();
var ptr = ecs_get_id(Universe, this, comp);
return (ptr != null) ? Unsafe.Read<T>(ptr) : null;
}
public override T? MaybeGet<T>(T _ = null!)
where T : class
{
var comp = Universe.LookupOrThrow<T>();
var ptr = ecs_get_id(Universe, this, comp);
return (ptr != null) ? (T)((GCHandle)Unsafe.Read<nint>(ptr)).Target! : null;
}
public override ref T GetRefOrNull<T>()
{
var comp = Universe.LookupOrThrow<T>();
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<T>(ptr) : ref Unsafe.NullRef<T>();
}
public override ref T GetRefOrThrow<T>()
{
ref var ptr = ref GetRefOrNull<T>();
if (Unsafe.IsNullRef(ref ptr)) throw new Exception(
$"Component {typeof(T)} not found on {this}");
return ref ptr;
}
public override ref T GetMut<T>()
{
var comp = Universe.LookupOrThrow<T>();
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<T>(ptr);
}
public override void Modified<T>()
=> ecs_modified_id(Universe, this, Universe.LookupOrThrow<T>());
public override EntityRef Set<T>(in T value)
{
var comp = Universe.LookupOrThrow<T>();
var size = (ulong)Unsafe.SizeOf<T>();
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>(T obj) where T : class
{
var comp = Universe.LookupOrThrow<T>();
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<T>(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;
}

@ -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<IdentifierRef>
{
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<IdentifierRef> GetEnumerator() { for (var i = 0; i < Count; i++) yield return this[i]; }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

@ -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<Term> Terms { get; }
public string? Expression { get; }
/// <summary>
/// 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.
/// </summary>
public bool Instanced { get; set; }
/// <summary>
/// Entity associated with query (optional).
/// </summary>
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<ecs_term_t>(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;
}
}

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

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

@ -1,54 +0,0 @@
using System;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public readonly struct Identifier
: IEquatable<Identifier>
{
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,
}

@ -1,41 +0,0 @@
using System;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe class IdentifierRef
: IEquatable<IdentifierRef>
{
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;
}

@ -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<Iterator>
, 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<T> Field<T>(int index)
where T : unmanaged
{
fixed (ecs_iter_t* ptr = &Value) {
var size = (ulong)Unsafe.SizeOf<T>();
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<T> MaybeField<T>(int index)
where T : unmanaged => FieldIsSet(index) ? Field<T>(index) : default;
public SpanToRef<T> FieldRef<T>(int index)
where T : class => new(Field<nint>(index));
public bool FieldIsSet(int index)
{
fixed (ecs_iter_t* ptr = &Value)
return ecs_field_is_set(ptr, index);
}
public bool FieldIs<T>(int index)
{
fixed (ecs_iter_t* ptr = &Value) {
var id = ecs_field_id(ptr, index);
var comp = Universe.LookupOrThrow<T>();
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<Iterator> GetEnumerator() { while (Next()) yield return this; }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public enum IteratorType
{
Term,
Filter,
Query,
Rule,
}

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

@ -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<TEvent> : ObserverAttribute
public static class ObserverExtensions
{
public static unsafe EntityRef RegisterObserver(this Universe universe,
string? name, FilterDesc filter, Entity @event, Action<Iterator> 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<ObserverAttribute>() ?? throw new ArgumentException(
@ -50,20 +34,12 @@ public static class ObserverExtensions
iterAction = (Action<Iterator>)Delegate.CreateDelegate(
typeof(Action<Iterator>), 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<Iterator>)>((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);
}
}

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

@ -1,22 +0,0 @@
using System;
namespace gaemstone.ECS;
[Tag]
public struct Relation { }
/// <summary>
/// Marked entity represents a relationship type.
/// It may be used as the "relation" in a pair.
/// </summary>
/// <remarks>
/// The relationship may have component data associated with
/// it when added to an entity under these circumstances:
/// <list type="bullet">
/// <item>If marked as a <see cref="TagAttribute"/>, does not carry data.</item>
/// <item>If marked as a <see cref="ComponentAttribute"/>, carries the relation's data.</item>
/// <item>If marked with neither, will carry the target's data, if it's a component.</item>
/// </list>
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)]
public class RelationAttribute : Attribute, ICreateEntityAttribute { }

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

@ -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<DependsOn>(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<SystemAttribute>();
var expr = action.Method.Get<ExpressionAttribute>()?.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<SystemAttribute>();
@ -78,48 +59,12 @@ public static class SystemExtensions
"System must specify ExpressionAttribute", nameof(method)));
callback = (Action<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), 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<Iterator> Callback { get; }
public CallbackContext(Universe universe, MethodInfo method, Action<Iterator> 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<SourceAttribute>()?.Type is Type sourceType)
iter.SetThis(Universe.LookupOrThrow(sourceType));
}
}
[UnmanagedCallersOnly]
private static unsafe void Run(ecs_iter_t* flecsIter)
{
var callback = CallbackContextHelper.Get<CallbackContext>((nint)flecsIter->binding_ctx);
// This is what flecs does, so I guess we'll do it too!
var type = (&flecsIter->next == (delegate*<ecs_iter_t*, Runtime.CBool>)&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);
}
}

@ -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
{
/// <summary> Match on self. </summary>
Self = EcsSelf,
/// <summary> Match by traversing upwards. </summary>
Up = EcsUp,
/// <summary> Match by traversing downwards (derived, cannot be set). </summary>
Down = EcsDown,
/// <summary> Sort results breadth first. </summary>
Cascade = EcsCascade,
/// <summary> Short for up(ChildOf). </summary>
Parent = EcsParent,
/// <summary> Term id is a variable. </summary>
IsVariable = EcsIsVariable,
/// <summary> Term id is an entity. </summary>
IsEntity = EcsIsEntity,
/// <summary> Prevent observer from triggering on term. </summary>
Filter = EcsFilter,
}

@ -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<Type, Entity> _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<T>()
=> 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<T>() => 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<T>(this EntityRef entity)
=> entity.CreateLookup(typeof(T));
public static EntityRef CreateLookup(this EntityRef entity, Type type)
{ entity.Universe.AddLookupByType(type, entity); return entity; }
}

@ -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<Relation>();
// 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<Game>().Add<Game>();
}
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;
}

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

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

@ -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<T>(EntityRef entity, string? value)
{
var world = entity.World;
var descEntity = world.LookupByTypeOrThrow<Doc.Description>();
var docEntity = world.LookupByTypeOrThrow<T>();
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<Doc.Description>(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<Doc.Description>(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<Doc.Description, Core.Name>()
? ecs_doc_get_name(entity.World, entity).FlecsToString() : null;
public static EntityRef SetDocName(this EntityRef entity, string? value)
=> Set<Core.Name>(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<Doc.Brief>(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<Doc.Detail>(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<Doc.Link>(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<Doc.Color>(entity, value);
}

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

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

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

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

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

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

@ -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<ModuleAttribute>() is not ModuleAttribute attr) throw new Exception(
if (moduleType.Get<ModuleAttribute>() 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<PathAttribute>()?.Value is not string modulePathStr) throw new Exception(
$"Existing module {moduleType} must have {nameof(PathAttribute)} set");
var modulePath = EntityPath.Parse(modulePathStr);
var moduleEntity = Universe.LookupByPath(modulePath) ?? throw new Exception(
$"Existing module {moduleType} with name '{modulePath}' not found");
// This implementation is pretty naive. It simply gets all nested
// types which are tagged with an ICreateEntityAttribute base
@ -55,13 +56,16 @@ public class ModuleManager
foreach (var type in moduleType.GetNestedTypes()) {
if (!type.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().Any()) continue;
var name = type.Get<EntityAttribute>()?.Path?.Single() ?? type.Name;
var entity = Universe.LookupOrThrow(module, name);
var attr = type.Get<PathAttribute>();
var path = EntityPath.Parse(attr?.Value ?? type.Name);
var entity = Universe.LookupByPathOrThrow(moduleEntity, path);
entity.CreateLookup(type);
if (type.Has<RelationAttribute>()) entity.Add<Relation>();
if (type.Has<RelationAttribute>()) entity.Add<Doc.Relation>();
}
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<PathAttribute>() 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<ModuleInfo> MetDependencies { get; } = new();
public HashSet<Entity> 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<Module>();
// Add module dependencies from [DependsOn<>] attributes.
foreach (var dependsAttr in Type.GetMultiple<AddRelationAttribute>().Where(attr =>
attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) {
var dependsPath = ModuleManager.GetModulePath(dependsAttr.Target);
var dependency = Universe.Lookup(dependsPath) ??
Universe.New(dependsPath).Add<Module>().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<ModuleInfo> MetDependencies { get; } = new();
public HashSet<Entity> 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<Module>();
// Add module dependencies from [DependsOn<>] attributes.
foreach (var dependsAttr in Type.GetMultiple<AddRelationAttribute>().Where(attr =>
attr.GetType().GetGenericTypeDefinition() == typeof(DependsOnAttribute<>))) {
var dependsPath = GetModulePath(dependsAttr.Target);
var dependency = Universe.LookupByPath(dependsPath) ??
Universe.New(dependsPath).Add<Module>().Disable().Build();
var depModule = Universe.Modules.Lookup(dependency);
if (depModule?.IsActive == true) MetDependencies.Add(depModule);
else { UnmetDependencies.Add(dependency); module.Disable(); }
module.Add<DependsOn>(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<DependsOn>(dependency);
// Ensure all parent entities have Module set.
for (var p = Entity.Parent; p != null; p = p.Parent)
p.Add<Module>();
}
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<Module>();
}
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<ICreateEntityAttribute>().Any()) continue;
private void RegisterNestedType(Type type)
{
if (!type.GetCustomAttributes(true).OfType<ICreateEntityAttribute>().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<ProxyAttribute>()?.Type ?? type;
if (!type.Has<ComponentAttribute>() && (!proxyType.IsValueType || proxyType.GetFields().Length > 0)) {
if (!type.Has<ComponentAttribute>() && (!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<EntityAttribute>() is EntityAttribute entityAttr)
? new EntityPath(entityAttr.Global, entityAttr.Path ?? new[] { proxyType.Name })
: new EntityPath(false, proxyType.Name);
var path = EntityPath.Parse(type.Get<PathAttribute>()?.Value ?? proxyType.Name);
var builder = path.IsAbsolute ? Universe.New(path) : Entity.NewChild(path);
if (!type.IsPublic) builder.Symbol(path.Name);
if (type.Has<RelationAttribute>()) builder.Add<Relation>();
if (type.Get<SymbolAttribute>() 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<AddEntityAttribute>())
entity.Add(Lookup(attr.Entity));
foreach (var attr in type.GetMultiple<AddRelationAttribute>())
entity.Add(Lookup(attr.Relation), Lookup(attr.Target));
if (type.Get<SingletonAttribute>()?.AutoAdd == true) entity.Add(entity);
if (type.Has<ComponentAttribute>()) entity.CreateComponent(proxyType);
if (type.Has<ComponentAttribute>()) entity.InitComponent(proxyType);
else entity.CreateLookup(proxyType);
if (type.Has<RelationAttribute>()) entity.Add<Doc.Relation>();
if (type.Has<TagAttribute>()) entity.Add<Tag>();
}
}
private void RegisterMethods(object? instance)
{
foreach (var method in Type.GetMethods(
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance
)) {
if (method.Has<SystemAttribute>())
Universe.RegisterSystem(instance, method).ChildOf(Entity);
if (method.Has<ObserverAttribute>())
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<SystemAttribute>())
Universe.InitSystem(instance, method).ChildOf(Entity);
if (method.Has<ObserverAttribute>())
Universe.InitObserver(instance, method).ChildOf(Entity);
}
}
}
}

@ -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<Doc.DisplayType>();
New("/gaemstone/Doc/Relation").Build().CreateLookup<Doc.Relation>();
LookupByPathOrThrow("/flecs/core/Module" ).CreateLookup<Flecs.Core.Module>();
LookupByPathOrThrow("/flecs/core/Component").CreateLookup<Flecs.Core.Component>();
LookupByPathOrThrow("/flecs/core/Tag" ).CreateLookup<Flecs.Core.Tag>();
// Register built-in (static) modules, which
// are defined in the "gaemstone.Flecs" namespace.
Modules.RegisterAll(GetType().Assembly.GetTypes()
.Where(t => t.IsAbstract && t.IsSealed));
Modules.Register<Doc>();
// Create "Game" entity to store global state.
New("/Game").Symbol("Game").Build()
.CreateLookup<Game>().Add<Game>();
}
}

@ -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<T> Allocate<T>(this IAllocator allocator, int count) where T : unmanaged
=> new((void*)allocator.Allocate(sizeof(T) * count), count);
public static void Free<T>(this IAllocator allocator, Span<T> span) where T : unmanaged
=> allocator.Free((nint)Unsafe.AsPointer(ref span[0]));
public static Span<T> AllocateCopy<T>(this IAllocator allocator, ReadOnlySpan<T> orig) where T : unmanaged
{ var copy = allocator.Allocate<T>(orig.Length); orig.CopyTo(copy); return copy; }
public static ref T Allocate<T>(this IAllocator allocator) where T : unmanaged
=> ref Unsafe.AsRef<T>((void*)allocator.Allocate(sizeof(T)));
public static void Free<T>(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<byte>(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<byte> utf8)
{
var copy = allocator.Allocate<byte>(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<ArenaAllocator> _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<byte>((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 */ }
}

@ -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<byte>(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); }
}

@ -1,14 +0,0 @@
using System.Collections.Generic;
namespace gaemstone.Utility;
public static class CallbackContextHelper
{
private static readonly List<object> _contexts = new();
public static nint Create<T>(T context) where T : notnull
{ _contexts.Add(context); return _contexts.Count; }
public static T Get<T>(nint context)
=> (T)_contexts[(int)context - 1];
}

@ -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<TKey, TValue>(this IDictionary<TKey, TValue> dict,
// TKey key, Func<TKey, TValue> valueFactory) { }
public static T? MaybeGet<T>(this Span<T> span, int index)
where T : struct => MaybeGet((ReadOnlySpan<T>)span, index);
public static T? MaybeGet<T>(this ReadOnlySpan<T> span, int index)
where T : struct => (index >= 0 && index < span.Length) ? span[index] : null;
public static T? FirstOrNull<T>(this IEnumerable<T> enumerable)
where T : struct
{

@ -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<MethodInfo, IterActionGenerator> _cache = new();
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>>> _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<Type, Action<ILGeneratorWrapper, IArgument<Iterator>, ILocal<int>>> _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<ParamInfo> 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
/// <summary>
/// Not part of the resulting query's terms.
/// Same value across a single invocation of a callback.
/// For example <see cref="ECS.Universe"/> or <see cref="TimeSpan"/>.
/// For example <see cref="ECS.World"/> or <see cref="TimeSpan"/>.
/// </summary>
GlobalUnique,

@ -1,19 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace gaemstone.Utility;
public readonly ref struct SpanToRef<T>
where T : class
{
private readonly Span<nint> _span;
public int Length => _span.Length;
public T? this[int index] => (_span[index] != 0)
? (T)((GCHandle)_span[index]).Target! : null;
internal SpanToRef(Span<nint> span) => _span = span;
public void Clear() => _span.Clear();
public void CopyTo(SpanToRef<T> dest) => _span.CopyTo(dest._span);
}

@ -3,13 +3,13 @@
<PropertyGroup>
<LangVersion>preview</LangVersion>
<TargetFramework>net7.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../flecs-cs/src/cs/production/Flecs/Flecs.csproj" />
<ProjectReference Include="../gaemstone.ECS/gaemstone.ECS.csproj" />
</ItemGroup>
<ItemGroup>

Loading…
Cancel
Save