Initial commit

wip/source-generators
copygirl 2 years ago
commit fae12b7963
  1. 20
      .editorconfig
  2. 4
      .gitignore
  3. 6
      .gitmodules
  4. 16
      .vscode/launch.json
  5. 8
      .vscode/settings.json
  6. 40
      .vscode/tasks.json
  7. 48
      gaemstone.sln
  8. 1
      src/FastNoiseLite
  9. 23
      src/Immersion/Immersion.csproj
  10. 91
      src/Immersion/Program.cs
  11. 11
      src/Immersion/Resources/LICENSE_NOTICES.md
  12. 12
      src/Immersion/Resources/default.fs.glsl
  13. 20
      src/Immersion/Resources/default.vs.glsl
  14. BIN
      src/Immersion/Resources/heart.blend
  15. BIN
      src/Immersion/Resources/heart.glb
  16. BIN
      src/Immersion/Resources/sword.blend
  17. BIN
      src/Immersion/Resources/sword.glb
  18. BIN
      src/Immersion/Resources/terrain.png
  19. 1
      src/flecs-cs
  20. 61
      src/gaemstone.Bloxel/BlockFacing.cs
  21. 75
      src/gaemstone.Bloxel/BlockPos.cs
  22. 17
      src/gaemstone.Bloxel/Chunk.cs
  23. 188
      src/gaemstone.Bloxel/ChunkPaletteStorage.cs
  24. 81
      src/gaemstone.Bloxel/ChunkPos.cs
  25. 125
      src/gaemstone.Bloxel/Client/ChunkMeshGenerator.cs
  26. 205
      src/gaemstone.Bloxel/Neighbor.cs
  27. 135
      src/gaemstone.Bloxel/Utility/ChunkedOctree.cs
  28. 154
      src/gaemstone.Bloxel/Utility/ZOrder.cs
  29. 42
      src/gaemstone.Bloxel/WorldGen/BasicWorldGenerator.cs
  30. 57
      src/gaemstone.Bloxel/WorldGen/SurfaceGrassGenerator.cs.disabled
  31. 18
      src/gaemstone.Bloxel/gaemstone.Bloxel.csproj
  32. 56
      src/gaemstone.Client/Color.cs
  33. 62
      src/gaemstone.Client/GLExtensions.cs
  34. 14
      src/gaemstone.Client/Mesh.cs
  35. 104
      src/gaemstone.Client/MeshManager.cs
  36. 81
      src/gaemstone.Client/Modules/CameraModule.cs
  37. 81
      src/gaemstone.Client/Modules/Input.cs
  38. 109
      src/gaemstone.Client/Modules/Renderer.cs
  39. 31
      src/gaemstone.Client/Modules/Windowing.cs
  40. 30
      src/gaemstone.Client/Resources.cs
  41. 14
      src/gaemstone.Client/Texture.cs
  42. 36
      src/gaemstone.Client/TextureCoords4.cs
  43. 76
      src/gaemstone.Client/TextureManager.cs
  44. 21
      src/gaemstone.Client/gaemstone.Client.csproj
  45. 12
      src/gaemstone/ECS/Attributes.cs
  46. 193
      src/gaemstone/ECS/Entity.cs
  47. 20
      src/gaemstone/ECS/EntityDesc.cs.disabled
  48. 39
      src/gaemstone/ECS/Filter.cs
  49. 17
      src/gaemstone/ECS/FlecsException.cs
  50. 53
      src/gaemstone/ECS/Identifier.cs
  51. 93
      src/gaemstone/ECS/Iterator.cs
  52. 13
      src/gaemstone/ECS/Module.cs
  53. 18
      src/gaemstone/ECS/Observer.cs
  54. 33
      src/gaemstone/ECS/Query.cs
  55. 93
      src/gaemstone/ECS/System.cs
  56. 140
      src/gaemstone/ECS/Universe+Modules.cs
  57. 167
      src/gaemstone/ECS/Universe+Systems.cs
  58. 184
      src/gaemstone/ECS/Universe.cs
  59. 13
      src/gaemstone/GlobalTransform.cs
  60. 20
      src/gaemstone/Utility/CStringExtensions.cs
  61. 291
      src/gaemstone/Utility/IL/ILGeneratorWrapper.cs
  62. 327
      src/gaemstone/Utility/IL/QueryActionGenerator.cs
  63. 38
      src/gaemstone/Utility/RandomExtensions.cs
  64. 73
      src/gaemstone/Utility/ReflectionExtensions.cs
  65. 229
      src/gaemstone/Utility/TypeWrapper.cs
  66. 18
      src/gaemstone/gaemstone.csproj

@ -0,0 +1,20 @@
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.cs]
indent_style = tab
indent_size = 4
# IDE0005: Using directive is unnecessary
dotnet_diagnostic.IDE0005.severity = suggestion
# IDE0047: Parentheses can be removed
dotnet_diagnostic.IDE0047.severity = none
[*.md]
# Allows placing double-space at end of lines.
trim_trailing_whitespace = false

4
.gitignore vendored

@ -0,0 +1,4 @@
**/obj/
**/bin/
/artifacts/
/packages/

6
.gitmodules vendored

@ -0,0 +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

@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Immersion",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/Immersion/bin/Debug/net6.0/Immersion.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Immersion",
"console": "internalConsole",
"stopAtEntry": false
}
]
}

@ -0,0 +1,8 @@
{
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/bin": true,
"**/obj": true,
},
}

40
.vscode/tasks.json vendored

@ -0,0 +1,40 @@
{
"version": "2.0.0",
"tasks": [{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/Immersion/Immersion.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/Immersion/Immersion.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/src/Immersion/Immersion.csproj"
],
"problemMatcher": "$msCompile"
}
]
}

@ -0,0 +1,48 @@

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

@ -0,0 +1 @@
Subproject commit 5923df5d822f7610100d0e77f629c607ed64934a

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../gaemstone.Bloxel/gaemstone.Bloxel.csproj" />
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Silk.NET" Version="2.16.0" />
</ItemGroup>
</Project>

@ -0,0 +1,91 @@
using System;
using gaemstone;
using gaemstone.Bloxel;
using gaemstone.Client;
using gaemstone.ECS;
using gaemstone.Utility;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
using static flecs_hub.flecs;
using static gaemstone.Client.CameraModule;
using static gaemstone.Client.Input;
using static gaemstone.Client.Windowing;
var universe = new Universe();
var game = universe.Lookup<Game>();
Resources.ResourceAssembly = typeof(Program).Assembly;
var window = Window.Create(WindowOptions.Default with {
Title = "gæmstone",
Size = new(1280, 720),
FramesPerSecond = 60.0,
PreferredDepthBufferBits = 24,
});
window.Initialize();
window.Center();
universe.RegisterModule<Windowing>();
game.Set(new Canvas(window.CreateOpenGL()));
game.Set(new GameWindow(window));
TextureManager.Initialize(universe);
universe.RegisterComponent<Mesh>();
universe.RegisterComponent<gaemstone.Client.Texture>();
universe.RegisterComponent<TextureCoords4>();
universe.RegisterModule<Input>();
universe.RegisterModule<CameraModule>();
universe.RegisterModule<Renderer>();
game.Set(new RawInput());
// TODO: Find a way to automatically register this chunk storage.
universe.RegisterComponent<ChunkPaletteStorage<ecs_entity_t>>();
universe.RegisterAll(typeof(Chunk).Assembly);
universe.Create("MainCamera")
.Set(Camera.Default3D)
.Set((GlobalTransform)Matrix4X4.CreateTranslation(0.0F, 2.0F, 0.0F))
.Set(new CameraController { MouseSensitivity = 12.0F });
var heartMesh = MeshManager.Load(universe, "heart.glb");
var swordMesh = MeshManager.Load(universe, "sword.glb");
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));
universe.Create()
.Set((GlobalTransform)(rotation * position))
.Set(rnd.Pick(heartMesh, swordMesh));
}
var texture = TextureManager.Load(universe, "terrain.png");
var stone = universe.Create("Stone").Set(TextureCoords4.FromGrid(4, 4, 1, 0));
var dirt = universe.Create("Dirt" ).Set(TextureCoords4.FromGrid(4, 4, 2, 0));
var grass = universe.Create("Grass").Set(TextureCoords4.FromGrid(4, 4, 3, 0));
var sizeH = 4;
var sizeY = 2;
for (var cx = -sizeH; cx < sizeH; cx++)
for (var cy = -sizeY; cy < sizeY; cy++)
for (var cz = -sizeH; cz < sizeH; cz++) {
var pos = new ChunkPos(cx, cy - 2, cz);
var storage = new ChunkPaletteStorage<ecs_entity_t>(default);
universe.Create()
.Set((GlobalTransform)Matrix4X4.CreateTranslation(pos.GetOrigin()))
.Set(new Chunk(pos))
.Set(storage)
.Set(texture);
}
window.Render += (delta) => {
if (!universe.Progress(TimeSpan.FromSeconds(delta)))
window.Close();
};
window.Run();

@ -0,0 +1,11 @@
# Resources License Notices
## Voxelgarden Textures
- terrain.png
**License:** [CC-BY-SA]
**Source:** [github.com/CasimirKaPazi/Voxelgarden](https://github.com/CasimirKaPazi/Voxelgarden)
[CC-BY-SA]: https://creativecommons.org/licenses/by-sa/2.0/

@ -0,0 +1,12 @@
#version 330 core
in vec4 fragColor;
in vec2 fragUV;
uniform sampler2D textureSampler;
out vec4 color;
void main()
{
color = fragColor * texture(textureSampler, fragUV);
}

@ -0,0 +1,20 @@
#version 330 core
layout(location = 0) in vec3 vertPosition;
layout(location = 1) in vec3 vertNormal;
layout(location = 2) in vec2 vertUV;
uniform mat4 cameraMatrix;
uniform mat4 modelMatrix;
out vec4 fragColor;
out vec2 fragUV;
void main()
{
gl_Position = cameraMatrix * modelMatrix * vec4(vertPosition, 1.0);
// Apply a pseudo-lighting effect based on the object's normals and rotation.
vec3 normal = mat3(modelMatrix) * vertNormal;
float l = 0.5 + (normal.y + 1) / 4.0 - (normal.z + 1) / 8.0;
fragColor = vec4(l, l, l, 1.0);
fragUV = vertUV;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

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

@ -0,0 +1,61 @@
using System;
using System.Collections.Immutable;
using Silk.NET.Maths;
namespace gaemstone.Bloxel;
public enum BlockFacing
{
East, // +X
West, // -X
Up, // +Y
Down, // -Y
South, // +Z
North, // -Z
}
public static class BlockFacings
{
public static readonly ImmutableHashSet<BlockFacing> Horizontals
= ImmutableHashSet.Create(BlockFacing.East , BlockFacing.West ,
BlockFacing.South, BlockFacing.North);
public static readonly ImmutableHashSet<BlockFacing> Verticals
= ImmutableHashSet.Create(BlockFacing.Up, BlockFacing.Down);
public static readonly ImmutableHashSet<BlockFacing> All
= Horizontals.Union(Verticals);
}
public static class BlockFacingExtensions
{
public static void Deconstruct(this BlockFacing self, out int x, out int y, out int z)
=> (x, y, z) = self switch {
BlockFacing.East => (+1, 0, 0),
BlockFacing.West => (-1, 0, 0),
BlockFacing.Up => ( 0, +1, 0),
BlockFacing.Down => ( 0, -1, 0),
BlockFacing.South => ( 0, 0, +1),
BlockFacing.North => ( 0, 0, -1),
_ => throw new ArgumentException(
$"'{self}' is not a valid BlockFacing", nameof(self))
};
public static bool IsValid(this BlockFacing self)
=> (self >= BlockFacing.East) && (self <= BlockFacing.North);
public static BlockFacing GetOpposite(this BlockFacing self)
=> (BlockFacing)((int)self ^ 0b1);
public static Vector3D<float> 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,
_ => throw new ArgumentException(
$"'{self}' is not a valid BlockFacing", nameof(self))
};
}

@ -0,0 +1,75 @@
using System;
using Silk.NET.Maths;
namespace gaemstone.Bloxel;
public readonly struct BlockPos
: IEquatable<BlockPos>
{
public static readonly BlockPos Origin = default;
public int X { get; }
public int Y { get; }
public int Z { get; }
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 BlockPos Add(int x, int y, int z) => new(X + x, Y + y, Z + z);
public BlockPos Add(in BlockPos other) => new(X + other.X, Y + other.Y, Z + other.Z);
public BlockPos Add(BlockFacing facing)
{ var (x, y, z) = facing; return Add(x, y, z); }
public BlockPos Add(BlockFacing facing, int factor)
{ var (x, y, z) = facing; return Add(x * factor, y * factor, z * factor); }
public BlockPos Add(Neighbor neighbor)
{ var (x, y, z) = neighbor; return Add(x, y, z); }
public BlockPos Add(Neighbor neighor, int factor)
{ var (x, y, z) = neighor; return Add(x * factor, y * factor, z * factor); }
public BlockPos Subtract(int x, int y, int z) => new(X - x, Y - y, Z - z);
public BlockPos Subtract(in BlockPos other) => new(X - other.X, Y - other.Y, Z - other.Z);
public BlockPos Subtract(BlockFacing facing)
{ var (x, y, z) = facing; return Subtract(x, y, z); }
public BlockPos Subtract(BlockFacing facing, int factor)
{ var (x, y, z) = facing; return Subtract(x * factor, y * factor, z * factor); }
public BlockPos Subtract(Neighbor neighbor)
{ var (x, y, z) = neighbor; return Subtract(x, y, z); }
public BlockPos Subtract(Neighbor neighor, int factor)
{ var (x, y, z) = neighor; return Subtract(x * factor, y * factor, z * factor); }
public bool Equals(BlockPos other)
=> (X == other.X) && (Y == other.Y) && (Z == other.Z);
public override bool Equals(object? obj)
=> (obj is BlockPos pos) && Equals(pos);
public override int GetHashCode() => HashCode.Combine(X, Y, Z);
public override string ToString() => $"BlockPos({X}:{Y}:{Z})";
public string ToShortString() => $"{X}:{Y}:{Z}";
public static BlockPos operator +(BlockPos left, BlockPos right) => left.Add(right);
public static BlockPos operator -(BlockPos left, BlockPos right) => left.Subtract(right);
public static BlockPos operator +(BlockPos left, BlockFacing right) => left.Add(right);
public static BlockPos operator -(BlockPos left, BlockFacing right) => left.Subtract(right);
public static BlockPos operator +(BlockPos left, Neighbor right) => left.Add(right);
public static BlockPos operator -(BlockPos left, Neighbor right) => left.Subtract(right);
public static bool operator ==(BlockPos left, BlockPos right) => left.Equals(right);
public static bool operator !=(BlockPos left, BlockPos right) => !left.Equals(right);
}
public static class BlockPosExtensions
{
public static BlockPos ToBlockPos(this Vector3D<float> self)
=> new((int)MathF.Floor(self.X), (int)MathF.Floor(self.Y), (int)MathF.Floor(self.Z));
}

@ -0,0 +1,17 @@
using gaemstone.ECS;
namespace gaemstone.Bloxel;
[Component]
public readonly struct Chunk
{
// <summary> Length of the egde of a world chunk. </summary>
public const int LENGTH = 16;
// <summary> Amount of bit shifting to go from a BlockPos to a ChunkPos. </summary>
public const int BIT_SHIFT = 4;
// <summary> Amount of bit masking to go from a BlockPos to a chunk-relative BlockPos. </summary>
public const int BIT_MASK = 0b1111;
public ChunkPos Position { get; }
public Chunk(ChunkPos pos) => Position = pos;
}

@ -0,0 +1,188 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using gaemstone.ECS;
namespace gaemstone.Bloxel;
// Based on "Palette-based compression for chunked discrete voxel data" by /u/Longor1996
// https://www.reddit.com/r/VoxelGameDev/comments/9yu8qy/palettebased_compression_for_chunked_discrete/
[Component]
public class ChunkPaletteStorage<T>
{
const int Size = 16 * 16 * 16;
static readonly EqualityComparer<T> COMPARER
= EqualityComparer<T>.Default;
BitArray? _data;
PaletteEntry[]? _palette;
int _usedPalettes;
int _indicesLength;
public T Default { get; }
public T this[int x, int y, int z]
{
get => Get(x, y, z);
set => Set(x, y, z, value);
}
public IEnumerable<T> Blocks
=> _palette?.Where(entry => !COMPARER.Equals(entry.Value, default!))
.Select(entry => entry.Value!)
?? Enumerable.Empty<T>();
public ChunkPaletteStorage(T @default)
=> Default = @default;
T Get(int x, int y, int z)
{
if (_palette == null) return Default;
var entry = _palette[GetPaletteIndex(x, y, z)];
return !COMPARER.Equals(entry.Value, default!) ? entry.Value : Default;
}
void Set(int x, int y, int z, T value)
{
if (_palette == null)
{
if (COMPARER.Equals(value, Default)) return;
}
else
{
var index = GetIndex(x, y, z);
ref var current = ref _palette[GetPaletteIndex(index)];
if (COMPARER.Equals(value, current.Value)) return;
if (--current.RefCount == 0)
_usedPalettes--;
var replace = Array.FindIndex(_palette, entry => COMPARER.Equals(value, entry.Value));
if (replace != -1)
{
SetPaletteIndex(index, replace);
_palette[replace].RefCount += 1;
return;
}
if (current.RefCount == 0)
{
current.Value = value;
current.RefCount = 1;
_usedPalettes++;
return;
}
}
var newPaletteIndex = NewPaletteEntry();
_palette![newPaletteIndex] = new PaletteEntry { Value = value, RefCount = 1 };
SetPaletteIndex(x, y, z, newPaletteIndex);
_usedPalettes++;
}
int NewPaletteEntry()
{
if (_palette != null)
{
int firstFree = Array.FindIndex(_palette, entry =>
entry.Value == null || entry.RefCount == 0);
if (firstFree != -1) return firstFree;
}
GrowPalette();
return NewPaletteEntry();
}
void GrowPalette()
{
if (_palette == null)
{
_data = new(Size);
_palette = new PaletteEntry[2];
_usedPalettes = 1;
_indicesLength = 1;
_palette[0] = new PaletteEntry { Value = Default, RefCount = Size };
return;
}
_indicesLength <<= 1;
var oldIndicesLength = _indicesLength >> 1;
var newData = new BitArray(Size * _indicesLength);
for (var i = 0; i < Size; i++)
for (var j = 0; j < oldIndicesLength; j++)
newData.Set(i * _indicesLength + j, _data!.Get(i * oldIndicesLength + j));
_data = newData;
Array.Resize(ref _palette, 1 << _indicesLength);
}
// public void FitPalette() {
// if (_usedPalettes > Mathf.NearestPo2(_usedPalettes) / 2) return;
// // decode all indices
// int[] indices = new int[size];
// for(int i = 0; i < indices.length; i++) {
// indices[i] = data.get(i * indicesLength, indicesLength);
// }
// // Create new palette, halfing it in size
// indicesLength = indicesLength >> 1;
// PaletteEntry[] newPalette = new PaletteEntry[2 pow indicesLength];
// // We gotta compress the palette entries!
// int paletteCounter = 0;
// for(int pi = 0; pi < palette.length; pi++, paletteCounter++) {
// PaletteEntry entry = newPalette[paletteCounter] = palette[pi];
// // Re-encode the indices (find and replace; with limit)
// for(int di = 0, fc = 0; di < indices.length && fc < entry.refcount; di++) {
// if(pi == indices[di]) {
// indices[di] = paletteCounter;
// fc += 1;
// }
// }
// }
// // Allocate new BitBuffer
// data = new BitBuffer(size * indicesLength); // the length is in bits, not bytes!
// // Encode the indices
// for(int i = 0; i < indices.length; i++) {
// data.set(i * indicesLength, indicesLength, indices[i]);
// }
// }
int GetPaletteIndex(int x, int y, int z)
=> GetPaletteIndex(GetIndex(x, y, z));
int GetPaletteIndex(int index)
{
var paletteIndex = 0;
for (var i = 0; i < _indicesLength; i++)
paletteIndex |= (_data!.Get(index + i) ? 1 : 0) << i;
return paletteIndex;
}
void SetPaletteIndex(int x, int y, int z, int paletteIndex)
=> SetPaletteIndex(GetIndex(x, y, z), paletteIndex);
void SetPaletteIndex(int index, int paletteIndex)
{
for (var i = 0; i < _indicesLength; i++)
_data!.Set(index + i, (paletteIndex >> i & 0b1) == 0b1);
}
int GetIndex(int x, int y, int z)
=> (x | y << 4 | z << 8) * _indicesLength;
struct PaletteEntry
{
public T Value { get; set; }
public int RefCount { get; set; }
}
}

@ -0,0 +1,81 @@
using System;
using Silk.NET.Maths;
namespace gaemstone.Bloxel;
public readonly struct ChunkPos
: IEquatable<ChunkPos>
{
public static readonly ChunkPos ORIGIN = new(0, 0, 0);
public int X { get; }
public int Y { get; }
public int Z { get; }
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(
X << Chunk.BIT_SHIFT, Y << Chunk.BIT_SHIFT, Z << Chunk.BIT_SHIFT);
public Vector3D<float> GetCenter() => new(
(X << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2,
(Y << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2,
(Z << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2);
public ChunkPos Add(int x, int y, int z)
=> new(X + x, Y + y, Z + z);
public ChunkPos Add(in ChunkPos other)
=> new(X + other.X, Y + other.Y, Z + other.Z);
public ChunkPos Add(BlockFacing facing)
{ var (x, y, z) = facing; return Add(x, y, z); }
public ChunkPos Add(Neighbor neighbor)
{ var (x, y, z) = neighbor; return Add(x, y, z); }
public ChunkPos Subtract(int x, int y, int z)
=> new(X - x, Y - y, Z - z);
public ChunkPos Subtract(in ChunkPos other)
=> new(X - other.X, Y - other.Y, Z - other.Z);
public ChunkPos Subtract(BlockFacing facing)
{ var (x, y, z) = facing; return Subtract(x, y, z); }
public ChunkPos Subtract(Neighbor neighbor)
{ var (x, y, z) = neighbor; return Subtract(x, y, z); }
public bool Equals(ChunkPos other)
=> (X == other.X) && (Y == other.Y) && (Z == other.Z);
public override bool Equals(object? obj)
=> (obj is ChunkPos pos) && Equals(pos);
public override int GetHashCode() => HashCode.Combine(X, Y, Z);
public override string ToString() => $"ChunkPos ({X}:{Y}:{Z})";
public string ToShortString() => $"{X}:{Y}:{Z}";
public static ChunkPos operator +(ChunkPos left, ChunkPos right) => left.Add(right);
public static ChunkPos operator -(ChunkPos left, ChunkPos right) => left.Subtract(right);
public static ChunkPos operator +(ChunkPos left, BlockFacing right) => left.Add(right);
public static ChunkPos operator -(ChunkPos left, BlockFacing right) => left.Subtract(right);
public static ChunkPos operator +(ChunkPos left, Neighbor right) => left.Add(right);
public static ChunkPos operator -(ChunkPos left, Neighbor right) => left.Subtract(right);
public static bool operator ==(ChunkPos left, ChunkPos right) => left.Equals(right);
public static bool operator !=(ChunkPos left, ChunkPos right) => !left.Equals(right);
}
public static class ChunkPosExtensions
{
public static ChunkPos ToChunkPos(this Vector3D<float> pos) => new(
(int)MathF.Floor(pos.X) >> Chunk.BIT_SHIFT,
(int)MathF.Floor(pos.Y) >> Chunk.BIT_SHIFT,
(int)MathF.Floor(pos.Z) >> Chunk.BIT_SHIFT);
public static ChunkPos ToChunkPos(this BlockPos self) => new(
self.X >> Chunk.BIT_SHIFT, self.Y >> Chunk.BIT_SHIFT, self.Z >> Chunk.BIT_SHIFT);
public static BlockPos ToChunkRelative(this BlockPos self) => new(
self.X & Chunk.BIT_MASK, self.Y & Chunk.BIT_MASK, self.Z & Chunk.BIT_MASK);
public static BlockPos ToChunkRelative(this BlockPos self, ChunkPos chunk) => new(
self.X - (chunk.X << Chunk.BIT_SHIFT),
self.Y - (chunk.Y << Chunk.BIT_SHIFT),
self.Z - (chunk.Z << Chunk.BIT_SHIFT));
}

@ -0,0 +1,125 @@
using System;
using System.Runtime.InteropServices;
using gaemstone.Client;
using gaemstone.ECS;
using Silk.NET.Maths;
using static flecs_hub.flecs;
using static gaemstone.Bloxel.WorldGen.BasicWorldGenerator;
namespace gaemstone.Bloxel.Client;
[Module]
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 int[] TriangleIndices
= { 0, 1, 3, 1, 2, 3 };
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];
[System]
public void GenerateChunkMeshes(Universe universe, Entity entity,
in Chunk chunk, ChunkPaletteStorage<ecs_entity_t> storage,
HasBasicWorldGeneration _1, [Not] Mesh _2)
{
var mesh = Generate(universe, chunk.Position, storage);
if (mesh is Mesh m) entity.Set(m);
else entity.Delete();
}
public Mesh? Generate(Universe universe, ChunkPos chunkPos,
ChunkPaletteStorage<ecs_entity_t> centerStorage)
{
// TODO: We'll need a way to get neighbors again.
// var storages = new ChunkPaletteStorage<ecs_entity_t>[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))
// storages[x+1, y+1, z+1] = storage;
// var centerStorage = storages[1, 1, 1];
var storages = new ChunkPaletteStorage<ecs_entity_t>[3, 3, 3];
storages[1, 1, 1] = centerStorage;
var indexCount = 0;
var vertexCount = 0;
for (var x = 0; x < 16; x++)
for (var y = 0; y < 16; y++)
for (var z = 0; z < 16; z++) {
var block = new Entity(universe, centerStorage[x, y, z]);
if (block.IsNone) continue;
var blockVertex = new Vector3D<float>(x, y, z);
var textureCell = block.Get<TextureCoords4>();
foreach (var facing in BlockFacings.All) {
if (!IsNeighborEmpty(storages, x, y, z, facing)) continue;
if (_indices.Length <= indexCount + 6)
Array.Resize(ref _indices, _indices.Length << 1);
if (_vertices.Length <= vertexCount + 4) {
Array.Resize(ref _vertices, _vertices.Length << 1);
Array.Resize(ref _normals , _vertices.Length << 1);
Array.Resize(ref _uvs , _vertices.Length << 1);
}
for (var i = 0; i < TriangleIndices.Length; i++)
_indices[indexCount++] = (ushort)(vertexCount + TriangleIndices[i]);
var normal = facing.ToVector3();
for (var i = 0; i < 4; i++) {
var offset = OffsetPerFacing[(int)facing][i];
_vertices[vertexCount] = blockVertex + offset;
_normals[vertexCount] = normal;
_uvs[vertexCount] = i switch {
0 => textureCell.TopLeft,
1 => textureCell.BottomLeft,
2 => textureCell.BottomRight,
3 => textureCell.TopRight,
_ => throw new InvalidOperationException()
};
vertexCount++;
}
}
}
return (indexCount > 0)
? MeshManager.Create(universe,
_indices.AsSpan(0, indexCount), _vertices.AsSpan(0, vertexCount),
_normals.AsSpan(0, vertexCount), _uvs.AsSpan(0, vertexCount))
: null;
}
static bool IsNeighborEmpty(
ChunkPaletteStorage<ecs_entity_t>[,,] storages,
int x, int y, int z, BlockFacing facing)
{
var cx = 1; var cy = 1; var cz = 1;
switch (facing) {
case BlockFacing.East : x += 1; if (x >= 16) cx += 1; break;
case BlockFacing.West : x -= 1; if (x < 0) cx -= 1; break;
case BlockFacing.Up : y += 1; if (y >= 16) cy += 1; break;
case BlockFacing.Down : y -= 1; if (y < 0) cy -= 1; break;
case BlockFacing.South : z += 1; if (z >= 16) cz += 1; break;
case BlockFacing.North : z -= 1; if (z < 0) cz -= 1; break;
}
var neighborChunk = storages[cx, cy, cz];
if (neighborChunk == null) return true;
var neighborBlock = neighborChunk[x & 0b1111, y & 0b1111, z & 0b1111];
return neighborBlock.Data.Data == 0;
}
}

@ -0,0 +1,205 @@
using System;
using System.Collections.Immutable;
using System.Text;
using Silk.NET.Maths;
namespace gaemstone.Bloxel;
[Flags]
public enum Neighbor : byte
{
None = 0,
// FACINGS
East = 0b000011, // +X
West = 0b000010, // -X
Up = 0b001100, // +Y
Down = 0b001000, // -Y
South = 0b110000, // +Z
North = 0b100000, // -Z
// CARDINALS
SouthEast = South | East, // +X +Z
SouthWest = South | West, // -X +Z
NorthEast = North | East, // +X -Z
NorthWest = North | West, // -X -Z
// ALL_AXIS_PLANES
UpEast = Up | East , // +X +Y
UpWest = Up | West , // -X +Y
UpSouth = Up | South, // +Z +Y
UpNorth = Up | North, // -Z +Y
DownEast = Down | East , // +X -Y
DownWest = Down | West , // -X -Y
DownSouth = Down | South, // +Z -Y
DownNorth = Down | North, // -Z -Y
// ALL
UpSouthEast = Up | South | East, // +X +Y +Z
UpSouthWest = Up | South | West, // -X +Y +Z
UpNorthEast = Up | North | East, // +X +Y -Z
UpNorthWest = Up | North | West, // -X +Y -Z
DownSouthEast = Down | South | East, // +X -Y +Z
DownSouthWest = Down | South | West, // -X -Y +Z
DownNorthEast = Down | North | East, // +X -Y -Z
DownNorthWest = Down | North | West, // -X -Y -Z
}
public static class Neighbors
{
public static readonly ImmutableHashSet<Neighbor> Horizontals
= ImmutableHashSet.Create(Neighbor.East , Neighbor.West ,
Neighbor.South, Neighbor.North);
public static readonly ImmutableHashSet<Neighbor> Verticals
= ImmutableHashSet.Create(Neighbor.Up, Neighbor.Down);
public static readonly ImmutableHashSet<Neighbor> Facings
= Horizontals.Union(Verticals);
public static readonly ImmutableHashSet<Neighbor> Cardinals
= Horizontals.Union(new[] {
Neighbor.SouthEast, Neighbor.SouthWest,
Neighbor.NorthEast, Neighbor.NorthWest });
public static readonly ImmutableHashSet<Neighbor> AllAxisPlanes
= Facings.Union(new[] {
Neighbor.SouthEast, Neighbor.SouthWest,
Neighbor.NorthEast, Neighbor.NorthWest,
Neighbor.UpEast , Neighbor.UpWest ,
Neighbor.UpSouth , Neighbor.UpNorth ,
Neighbor.DownEast , Neighbor.DownWest ,
Neighbor.DownSouth, Neighbor.DownNorth });
public static readonly ImmutableHashSet<Neighbor> All
= AllAxisPlanes.Union(new[] {
Neighbor.UpSouthEast, Neighbor.UpSouthWest,
Neighbor.UpNorthEast, Neighbor.UpNorthWest,
Neighbor.DownSouthEast, Neighbor.DownSouthWest,
Neighbor.DownNorthEast, Neighbor.DownNorthWest });
}
public static class NeighborExtensions
{
const int SetBitX = 0b000010, ValueBitX = 0b000001;
const int SetBitY = 0b001000, ValueBitY = 0b000100;
const int SetBitZ = 0b100000, ValueBitZ = 0b010000;
public static void Deconstruct(this Neighbor self, out int x, out int y, out int z)
{
x = (((int)self & SetBitX) != 0) ? ((((int)self & ValueBitX) != 0) ? 1 : -1) : 0;
y = (((int)self & SetBitY) != 0) ? ((((int)self & ValueBitY) != 0) ? 1 : -1) : 0;
z = (((int)self & SetBitZ) != 0) ? ((((int)self & ValueBitZ) != 0) ? 1 : -1) : 0;
}
// public static Neighbor ToNeighbor(this Axis self, int v)
// {
// if ((v < -1) || (v > 1)) throw new ArgumentOutOfRangeException(
// nameof(v), v, $"{nameof(v)} (={v}) must be within (-1, 1)");
// return self switch {
// Axis.X => (v > 0) ? Neighbor.East : Neighbor.West ,
// Axis.Y => (v > 0) ? Neighbor.Up : Neighbor.Down ,
// Axis.Z => (v > 0) ? Neighbor.South : Neighbor.North,
// _ => Neighbor.None
// };
// }
// public static Axis GetAxis(this Neighbor self)
// => self switch {
// Neighbor.East => Axis.X,
// Neighbor.West => Axis.X,
// Neighbor.Up => Axis.Y,
// Neighbor.Down => Axis.Y,
// Neighbor.South => Axis.Z,
// Neighbor.North => Axis.Z,
// _ => throw new ArgumentException(nameof(self), $"{self} is not one of FACINGS")
// };
public static Neighbor ToNeighbor(this BlockFacing self)
=> self switch {
BlockFacing.East => Neighbor.East ,
BlockFacing.West => Neighbor.West ,
BlockFacing.Up => Neighbor.Up ,
BlockFacing.Down => Neighbor.Down ,
BlockFacing.South => Neighbor.South,
BlockFacing.North => Neighbor.North,
_ => throw new ArgumentException(
$"'{self}' is not a valid BlockFacing", nameof(self))
};
public static BlockFacing ToBlockFacing(this Neighbor self)
=> self switch {
Neighbor.East => BlockFacing.East ,
Neighbor.West => BlockFacing.West ,
Neighbor.Up => BlockFacing.Up ,
Neighbor.Down => BlockFacing.Down ,
Neighbor.South => BlockFacing.South,
Neighbor.North => BlockFacing.North,
_ => throw new ArgumentException(
$"'{self}' can't be converted to a valid BlockFacing", nameof(self))
};
public static Neighbor ToNeighbor(this (int x, int y, int z) p)
{
var neighbor = Neighbor.None;
if (p.x != 0) {
if (p.x == 1) neighbor |= Neighbor.East;
else if (p.x == -1) neighbor |= Neighbor.West;
else throw new ArgumentOutOfRangeException(
nameof(p), p.x, $"{nameof(p)}.x (={p.x}) must be within (-1, 1)");
}
if (p.y != 0) {
if (p.y == 1) neighbor |= Neighbor.Up;
else if (p.y == -1) neighbor |= Neighbor.Down;
else throw new ArgumentOutOfRangeException(
nameof(p), p.y, $"{nameof(p)}.y (={p.y}) must be within (-1, 1)");
}
if (p.z != 0) {
if (p.z == 1) neighbor |= Neighbor.South;
else if (p.z == -1) neighbor |= Neighbor.North;
else throw new ArgumentOutOfRangeException(
nameof(p), p.z, $"{nameof(p)}.z (={p.z}) must be within (-1, 1)");
}
return neighbor;
}
public static Neighbor GetOpposite(this Neighbor self)
{ var (x, y, z) = self; return (-x, -y, -z).ToNeighbor(); }
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)
{ var (x, y, z) = self; return new(x, y, z); }
public static bool IsNone(this Neighbor self)
=> (self == Neighbor.None);
public static bool IsHorizontal(this Neighbor self)
=> Neighbors.Horizontals.Contains(self);
public static bool IsVertical(this Neighbor self)
=> Neighbors.Verticals.Contains(self);
public static bool IsCardinal(this Neighbor self)
=> Neighbors.Cardinals.Contains(self);
public static bool IsFacing(this Neighbor self)
=> Neighbors.Facings.Contains(self);
public static bool IsValid(this Neighbor self)
=> Neighbors.All.Contains(self);
public static string ToShortString(this Neighbor self)
{
if (!self.IsValid()) return "-";
var sb = new StringBuilder(3);
foreach (var chr in self.ToString())
if ((chr >= 'A') && (chr <= 'Z')) // ASCII IsUpper
sb.Append(chr + 0x20); // ASCII ToLower
return sb.ToString();
}
}

@ -0,0 +1,135 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace gaemstone.Bloxel.Utility;
public class ChunkedOctree<T>
where T : struct
{
public delegate void UpdateAction(int level, ReadOnlySpan<T> children, ref T parent);
public delegate float? WeightFunc(int level, ZOrder pos, T value);
private static readonly int[] START_INDEX_LOOKUP = {
0, 1, 9, 73, 585, 4681, 37449, 299593, 2396745, 19173961, 153391689 };
private readonly IEqualityComparer<T> _comparer = EqualityComparer<T>.Default;
private readonly Dictionary<ZOrder, T[]> _regions = new();
public int Depth { get; }
public ChunkedOctree(int depth)
{
if (depth < 1) throw new ArgumentOutOfRangeException(nameof(depth),
$"{nameof(depth)} must be larger than 0");
if (depth >= START_INDEX_LOOKUP.Length) throw new ArgumentOutOfRangeException(nameof(depth),
$"{nameof(depth)} must be smaller than {START_INDEX_LOOKUP.Length}");
Depth = depth;
}
public T Get(ChunkPos pos)
=> Get(0, new(pos.X, pos.Y, pos.Z));
public T Get(int level, ZOrder pos)
{
var region = _regions.GetValueOrDefault(pos >> Depth - level);
if (region == null) return default;
var localPos = pos & ~(~0L << (Depth - level) * 3);
return region[GetIndex(level, localPos)];
}
private int GetIndex(int level, ZOrder localPos)
=> START_INDEX_LOOKUP[Depth - level] + (int)localPos.Raw;
public void Update(ChunkPos pos, UpdateAction update)
{
var zPos = new ZOrder(pos.X, pos.Y, pos.Z);
var localPos = zPos & ~(~0L << Depth * 3);
var regionPos = zPos >> Depth;
if (!_regions.TryGetValue(regionPos, out var region))
_regions.Add(regionPos, region = new T[START_INDEX_LOOKUP[Depth + 1] + 1]);
var children = default(ReadOnlySpan<T>);
for (var level = 0; level <= Depth; level++)
{
var index = GetIndex(level, localPos);
var previous = region[index];
update(0, children, ref region[index]);
if (_comparer.Equals(region[index], previous)) return;
if (level == Depth) return;
children = region.AsSpan(GetIndex(level, localPos & ~0b111L), 8);
localPos >>= 1;
}
}
public IEnumerable<(ChunkPos ChunkPos, T Value, float Weight)> Find(
WeightFunc weight, params ChunkPos[] searchFrom)
{
var enumerator = new Enumerator(this, weight);
foreach (var pos in searchFrom) enumerator.SearchFrom(new(pos.X, pos.Y, pos.Z));
while (enumerator.MoveNext()) yield return enumerator.Current;
}
public class Enumerator
: IEnumerator<(ChunkPos ChunkPos, T Value, float Weight)>
{
private readonly ChunkedOctree<T> _octree;
private readonly WeightFunc _weight;
private readonly HashSet<ZOrder> _checkedRegions = new();
private readonly PriorityQueue<(int Level, ZOrder Pos, T Value), float> _processing = new();
private (ChunkPos ChunkPos, T Value, float Weight)? _current;
internal Enumerator(ChunkedOctree<T> octree, WeightFunc weight)
{ _octree = octree; _weight = weight; _current = null; }
public (ChunkPos ChunkPos, T Value, float Weight) Current
=> _current ?? throw new InvalidOperationException();
object IEnumerator.Current => Current;
public bool MoveNext()
{
while (_processing.TryDequeue(out var element, out var weight))
{
var (level, nodePos, value) = element;
if (level == 0)
{
_current = (new(nodePos.X, nodePos.Y, nodePos.Z), value, weight);
return true;
}
else for (var i = 0b000; i <= 0b111; i++)
PushNode(level - 1, nodePos << 1 | ZOrder.FromRaw(i));
}
_current = null;
return false;
}
public void Reset() => throw new NotSupportedException();
public void Dispose() { }
internal void SearchFrom(ZOrder nodePos)
{
var regionPos = nodePos >> _octree.Depth;
for (var x = -1; x <= 1; x++)
for (var y = -1; y <= 1; y++)
for (var z = -1; z <= 1; z++)
SearchRegion(regionPos + new ZOrder(x, y, z));
}
private void SearchRegion(ZOrder regionPos)
{
if (_checkedRegions.Add(regionPos))
PushNode(_octree.Depth, regionPos);
}
private void PushNode(int level, ZOrder nodePos)
{
var value = _octree.Get(level, nodePos);
if (_weight(level, nodePos, value) is float weight)
_processing.Enqueue((level, nodePos, value), weight);
}
}
}

@ -0,0 +1,154 @@
using System;
namespace gaemstone.Bloxel.Utility;
// This struct wraps a primitive integer which represents an index into a space-filling curve
// called "Z-Order Curve" (https://en.wikipedia.org/wiki/Z-order_curve). Often, this is also
// referred to as Morton order, code, or encoding.
//
// This implementation purely focuses on 3 dimensions.
//
// By interleaving the 3 sub-elements into a single integer, some amount of packing can be
// achieved, at the loss of some bits per elements. For example, with a 64 bit integer, 21
// bits per elements are available (2_097_152 distinct values), which may be enough to
// represent block coordinates in a bloxel game world.
//
// One upside of encoding separate coordinates into a single Z-Order index is that it can then
// be effectively used to index into octrees, and certain operations such as bitwise shifting
// are quite useful.
public readonly struct ZOrder
: IEquatable<ZOrder>
, IComparable<ZOrder>
{
public const int ELEMENT_MIN = ~0 << BITS_PER_ELEMENT - 1;
public const int ELEMENT_MAX = ~ELEMENT_MIN;
private const int BITS_SIZE = sizeof(long) * 8;
private const int BITS_PER_ELEMENT = BITS_SIZE / 3;
private const int MAX_USABLE_BITS = BITS_PER_ELEMENT * 3;
private const int SIGN_SHIFT = sizeof(int) * 8 - BITS_PER_ELEMENT;
private const long USABLE_MASK = ~(~0L << MAX_USABLE_BITS);
private const long COMPARE_MASK = ~(~0L << 3) << MAX_USABLE_BITS - 3;
private static readonly ulong[] MASKS = {
0b_00000000_00000000_00000000_00000000_00000000_00011111_11111111_11111111, // 0x1fffff
0b_00000000_00011111_00000000_00000000_00000000_00000000_11111111_11111111, // 0x1f00000000ffff
0b_00000000_00011111_00000000_00000000_11111111_00000000_00000000_11111111, // 0x1f0000ff0000ff
0b_00010000_00001111_00000000_11110000_00001111_00000000_11110000_00001111, // 0x100f00f00f00f00f
0b_00010000_11000011_00001100_00110000_11000011_00001100_00110000_11000011, // 0x10c30c30c30c30c3
0b_00010010_01001001_00100100_10010010_01001001_00100100_10010010_01001001, // 0x1249249249249249
};
private static readonly long X_MASK = (long)MASKS[MASKS.Length - 1];
private static readonly long Y_MASK = X_MASK << 1;
private static readonly long Z_MASK = X_MASK << 2;
private static readonly long XY_MASK = X_MASK | Y_MASK;
private static readonly long XZ_MASK = X_MASK | Z_MASK;
private static readonly long YZ_MASK = Y_MASK | Z_MASK;
public long Raw { get; }
public int X => Decode(0);
public int Y => Decode(1);
public int Z => Decode(2);
private ZOrder(long value)
=> Raw = value;
public static ZOrder FromRaw(long value)
=> new(value & USABLE_MASK);
public ZOrder(int x, int y, int z)
{
if (x < ELEMENT_MIN || x > ELEMENT_MAX) throw new ArgumentOutOfRangeException(nameof(x));
if (y < ELEMENT_MIN || y > ELEMENT_MAX) throw new ArgumentOutOfRangeException(nameof(y));
if (z < ELEMENT_MIN || z > ELEMENT_MAX) throw new ArgumentOutOfRangeException(nameof(z));
Raw = Split(x) | Split(y) << 1 | Split(z) << 2;
}
public void Deconstruct(out int x, out int y, out int z)
=> (x, y, z) = (X, Y, Z);
public ZOrder IncX() => FromRaw((Raw | YZ_MASK) + 1 & X_MASK | Raw & YZ_MASK);
public ZOrder IncY() => FromRaw((Raw | XZ_MASK) + (1 << 1) & Y_MASK | Raw & XZ_MASK);
public ZOrder IncZ() => FromRaw((Raw | XY_MASK) + (1 << 2) & Z_MASK | Raw & XY_MASK);
public ZOrder DecX() => FromRaw((Raw & X_MASK) - 1 & X_MASK | Raw & YZ_MASK);
public ZOrder DecY() => FromRaw((Raw & Y_MASK) - (1 << 1) & Y_MASK | Raw & XZ_MASK);
public ZOrder DecZ() => FromRaw((Raw & Z_MASK) - (1 << 2) & Z_MASK | Raw & XY_MASK);
public static ZOrder operator +(ZOrder left, ZOrder right)
{
var xSum = (left.Raw | YZ_MASK) + (right.Raw & X_MASK);
var ySum = (left.Raw | XZ_MASK) + (right.Raw & Y_MASK);
var zSum = (left.Raw | XY_MASK) + (right.Raw & Z_MASK);
return FromRaw(xSum & X_MASK | ySum & Y_MASK | zSum & Z_MASK);
}
public static ZOrder operator -(ZOrder left, ZOrder right)
{
var xDiff = (left.Raw & X_MASK) - (right.Raw & X_MASK);
var yDiff = (left.Raw & Y_MASK) - (right.Raw & Y_MASK);
var zDiff = (left.Raw & Z_MASK) - (right.Raw & Z_MASK);
return FromRaw(xDiff & X_MASK | yDiff & Y_MASK | zDiff & Z_MASK);
}
public static ZOrder operator &(ZOrder left, long right) => FromRaw(left.Raw & right);
public static ZOrder operator |(ZOrder left, long right) => FromRaw(left.Raw | right);
public static ZOrder operator ^(ZOrder left, long right) => FromRaw(left.Raw ^ right);
public static ZOrder operator &(ZOrder left, ZOrder right) => new(left.Raw & right.Raw);
public static ZOrder operator |(ZOrder left, ZOrder right) => new(left.Raw | right.Raw);
public static ZOrder operator ^(ZOrder left, ZOrder right) => new(left.Raw ^ right.Raw);
public static ZOrder operator <<(ZOrder left, int right)
{
if (right >= BITS_PER_ELEMENT) throw new ArgumentOutOfRangeException(
nameof(right), right, $"{nameof(right)} must be smaller than {BITS_PER_ELEMENT}");
return FromRaw(left.Raw << right * 3);
}
public static ZOrder operator >>(ZOrder left, int right)
{
var result = left.Raw >> right * 3;
var mask = left.Raw >> MAX_USABLE_BITS - 3 << MAX_USABLE_BITS - right * 3;
for (var i = 0; i < right; i++) { result |= mask; mask <<= 3; }
return FromRaw(result);
}
public int CompareTo(ZOrder other) => (Raw ^ COMPARE_MASK).CompareTo(other.Raw ^ COMPARE_MASK);
public bool Equals(ZOrder other) => Raw.Equals(other.Raw);
public override bool Equals(object? obj) => obj is ZOrder order && Equals(order);
public override int GetHashCode() => Raw.GetHashCode();
public override string ToString() => $"<{X},{Y},{Z}>";
public static bool operator ==(ZOrder left, ZOrder right) => left.Equals(right);
public static bool operator !=(ZOrder left, ZOrder right) => !left.Equals(right);
private static long Split(int i)
{
var l = (ulong)i;
// l = l & Masks[0];
l = (l | l << 32) & MASKS[1];
l = (l | l << 16) & MASKS[2];
l = (l | l << 8) & MASKS[3];
l = (l | l << 4) & MASKS[4];
l = (l | l << 2) & MASKS[5];
return (long)l;
}
private int Decode(int index)
{
var l = (ulong)Raw >> index;
l &= MASKS[5];
l = (l ^ l >> 2) & MASKS[4];
l = (l ^ l >> 4) & MASKS[3];
l = (l ^ l >> 8) & MASKS[2];
l = (l ^ l >> 16) & MASKS[1];
l = (l ^ l >> 32) & MASKS[0];
return (int)l << SIGN_SHIFT >> SIGN_SHIFT;
}
}

@ -0,0 +1,42 @@
using System;
using gaemstone.ECS;
using static flecs_hub.flecs;
namespace gaemstone.Bloxel.WorldGen;
[Module]
public class BasicWorldGenerator
{
private readonly FastNoiseLite _noise;
public BasicWorldGenerator()
{
_noise = new(new Random().Next());
_noise.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2);
_noise.SetFractalType(FastNoiseLite.FractalType.Ridged);
_noise.SetFractalOctaves(4);
_noise.SetFractalGain(0.6F);
}
[Tag]
public struct HasBasicWorldGeneration { }
[System]
public void Populate(Universe universe, Entity entity,
in Chunk chunk, ChunkPaletteStorage<ecs_entity_t> storage,
[Not] HasBasicWorldGeneration _)
{
var stone = universe.Lookup("Stone");
for (var lx = 0; lx < Chunk.LENGTH; lx++)
for (var ly = 0; ly < Chunk.LENGTH; ly++)
for (var lz = 0; lz < Chunk.LENGTH; lz++) {
var gx = chunk.Position.X << Chunk.BIT_SHIFT | lx;
var gy = chunk.Position.Y << Chunk.BIT_SHIFT | ly;
var gz = chunk.Position.Z << Chunk.BIT_SHIFT | lz;
var bias = Math.Clamp(gy / 32.0F + 1.0F, 0.0F, 1.0F);
if (_noise.GetNoise(gx, gy, gz) > bias)
storage[lx, ly, lz] = stone;
}
entity.Add<HasBasicWorldGeneration>();
}
}

@ -0,0 +1,57 @@
using System.Collections.Generic;
namespace gaemstone.Bloxel.WorldGen;
// FIXME: There is an issue with this generator where it doesn't generate grass and dirt properly.
public class SurfaceGrassGenerator
: IWorldGenerator
{
public static readonly string IDENTIFIER = nameof(SurfaceGrassGenerator);
private const int AIR_BLOCKS_NEEDED = 12;
private const int DIRT_BLOCKS_BENEATH = 3;
public string Identifier { get; } = IDENTIFIER;
public IEnumerable<string> Dependencies { get; } = new[]{
BasicWorldGenerator.IDENTIFIER
};
public IEnumerable<(Neighbor, string)> NeighborDependencies { get; } = new[]{
(Neighbor.Up, BasicWorldGenerator.IDENTIFIER)
};
public void Populate(Chunk chunk)
{
var up = chunk.Neighbors[Neighbor.Up]!;
for (var lx = 0; lx < Chunk.LENGTH; lx++)
for (var lz = 0; lz < Chunk.LENGTH; lz++)
{
var numAirBlocks = 0;
var blockIndex = 0;
for (var ly = Chunk.LENGTH + AIR_BLOCKS_NEEDED - 1; ly >= 0; ly--)
{
var block = ly >= Chunk.LENGTH
? up.Storage[lx, ly - Chunk.LENGTH, lz]
: chunk.Storage[lx, ly, lz];
if (block.IsAir)
{
numAirBlocks++;
blockIndex = 0;
}
else if (numAirBlocks >= AIR_BLOCKS_NEEDED || blockIndex > 0)
{
if (ly < Chunk.LENGTH)
{
if (blockIndex == 0)
chunk.Storage[lx, ly, lz] = Block.GRASS;
else if (blockIndex <= DIRT_BLOCKS_BENEATH)
chunk.Storage[lx, ly, lz] = Block.DIRT;
}
blockIndex++;
numAirBlocks = 0;
}
}
}
}
}

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Include="../FastNoiseLite/CSharp/FastNoiseLite.cs"></Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../gaemstone/gaemstone.csproj" />
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,56 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Silk.NET.Maths;
namespace gaemstone.Client;
[StructLayout(LayoutKind.Explicit)]
public readonly struct Color
: IEquatable<Color>
{
public static Color Transparent { get; } = default;
public static Color Black { get; } = FromRGB(0x000000);
public static Color White { get; } = FromRGB(0xFFFFFF);
[FieldOffset(0)]
public readonly uint Value;
[FieldOffset(0)]
public readonly byte R;
[FieldOffset(1)]
public readonly byte G;
[FieldOffset(2)]
public readonly byte B;
[FieldOffset(3)]
public readonly byte A;
private Color(uint value)
{ Unsafe.SkipInit(out this); Value = value; }
private Color(byte r, byte g, byte b, byte a)
{ Unsafe.SkipInit(out this); R = r; G = g; B = b; A = a; }
public static Color FromRGBA(uint rgba) => new(rgba);
public static Color FromRGB(uint rgb) => new(rgb | 0xFF000000);
public static Color FromRGBA(byte r, byte g, byte b, byte a) => new(r, g, b, a);
public static Color FromRGB(byte r, byte g, byte b) => new(r, g, b, 0xFF);
public bool Equals(Color other)
=> Value == other.Value;
public override bool Equals([NotNullWhen(true)] object? obj)
=> (obj is Color color) && Equals(color);
public override int GetHashCode()
=> Value.GetHashCode();
public override string? ToString()
=> $"Color(0x{Value:X8})";
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);
}

@ -0,0 +1,62 @@
using System;
using System.Runtime.CompilerServices;
using Silk.NET.OpenGL;
namespace gaemstone.Client;
public static class GLExtensions
{
public static uint CreateAndCompileShader(this GL GL, ShaderType type, string label, string source)
{
var shader = GL.CreateShader(type);
GL.ObjectLabel(ObjectIdentifier.Shader, shader, (uint)label.Length, label);
GL.ShaderSource(shader, source);
GL.CompileShader(shader);
GL.GetShader(shader, ShaderParameterName.CompileStatus, out var result);
if (result != (int)GLEnum.True) throw new Exception(
$"Failed compiling shader \"{label}\" ({shader}):\n{GL.GetShaderInfoLog(shader)}");
return shader;
}
public static uint CreateAndLinkProgram(this GL GL, string label, params uint[] shaders)
{
var program = GL.CreateProgram();
GL.ObjectLabel(ObjectIdentifier.Program, program, (uint)label.Length, label);
foreach (var shader in shaders) GL.AttachShader(program, shader);
GL.LinkProgram(program);
foreach (var shader in shaders) GL.DetachShader(program, shader);
foreach (var shader in shaders) GL.DeleteShader(shader);
GL.GetProgram(program, ProgramPropertyARB.LinkStatus, out var result);
if (result != (int)GLEnum.True) throw new Exception(
$"Failed linking Program \"{label}\" ({program}):\n{GL.GetProgramInfoLog(program)}");
return program;
}
// These overloads are available because without them, the implicit casting
// (say from T[] to ReadOnlySpan<T>) causes the generic type resolving to break.
public static uint CreateBufferFromData<T>(this GL GL, T[] data,
BufferTargetARB target = BufferTargetARB.ArrayBuffer,
BufferUsageARB usage = BufferUsageARB.StaticDraw)
where T : unmanaged
=> GL.CreateBufferFromData((ReadOnlySpan<T>)data, target, usage);
public static uint CreateBufferFromData<T>(this GL GL, ArraySegment<T> data,
BufferTargetARB target = BufferTargetARB.ArrayBuffer,
BufferUsageARB usage = BufferUsageARB.StaticDraw)
where T : unmanaged
=> GL.CreateBufferFromData((ReadOnlySpan<T>)data, target, usage);
public static uint CreateBufferFromData<T>(this GL GL, Span<T> data,
BufferTargetARB target = BufferTargetARB.ArrayBuffer,
BufferUsageARB usage = BufferUsageARB.StaticDraw)
where T : unmanaged
=> GL.CreateBufferFromData((ReadOnlySpan<T>)data, target, usage);
public static uint CreateBufferFromData<T>(this GL GL, ReadOnlySpan<T> data,
BufferTargetARB target = BufferTargetARB.ArrayBuffer,
BufferUsageARB usage = BufferUsageARB.StaticDraw)
where T : unmanaged
{
var buffer = GL.GenBuffer();
GL.BindBuffer(target, buffer);
GL.BufferData(target, (nuint)(data.Length * Unsafe.SizeOf<T>()), data, usage);
return buffer;
}
}

@ -0,0 +1,14 @@
using gaemstone.ECS;
namespace gaemstone.Client;
[Component]
public readonly struct Mesh
{
public uint Handle { get; }
public int Count { get; }
public bool IsIndexed { get; }
public Mesh(uint handle, int count, bool indexed = true)
{ Handle = handle; Count = count; IsIndexed = indexed; }
}

@ -0,0 +1,104 @@
using System;
using gaemstone.ECS;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using ModelRoot = SharpGLTF.Schema2.ModelRoot;
namespace gaemstone.Client;
public static class MeshManager
{
const uint PositionAttribIndex = 0;
const uint NormalAttribIndex = 1;
const uint UvAttribIndex = 2;
public static Mesh Load(Universe universe, string name)
{
ModelRoot root;
using (var stream = Resources.GetStream(name))
root = ModelRoot.ReadGLB(stream, new());
var primitive = root.LogicalMeshes[0].Primitives[0];
var indices = primitive.IndexAccessor;
var vertices = primitive.VertexAccessors["POSITION"];
var normals = primitive.VertexAccessors["NORMAL"];
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL;
var vao = GL.GenVertexArray();
GL.BindVertexArray(vao);
GL.CreateBufferFromData(indices.SourceBufferView.Content,
BufferTargetARB.ElementArrayBuffer);
GL.CreateBufferFromData(vertices.SourceBufferView.Content);
GL.EnableVertexAttribArray(PositionAttribIndex);
unsafe { GL.VertexAttribPointer(PositionAttribIndex, 3,
(VertexAttribPointerType)vertices.Encoding, vertices.Normalized,
(uint)vertices.SourceBufferView.ByteStride, (void*)vertices.ByteOffset); }
GL.CreateBufferFromData(normals.SourceBufferView.Content);
GL.EnableVertexAttribArray(NormalAttribIndex);
unsafe { GL.VertexAttribPointer(NormalAttribIndex, 3,
(VertexAttribPointerType)vertices.Encoding, vertices.Normalized,
(uint)vertices.SourceBufferView.ByteStride, (void*)vertices.ByteOffset); }
var numVertices = primitive.IndexAccessor.Count;
return new(vao, numVertices);
}
public static Mesh Create(Universe universe,
ReadOnlySpan<ushort> indices, ReadOnlySpan<Vector3D<float>> vertices,
ReadOnlySpan<Vector3D<float>> normals, ReadOnlySpan<Vector2D<float>> uvs)
{
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL;
var vao = GL.GenVertexArray();
GL.BindVertexArray(vao);
GL.CreateBufferFromData(indices, BufferTargetARB.ElementArrayBuffer);
GL.CreateBufferFromData(vertices);
GL.EnableVertexAttribArray(PositionAttribIndex);
unsafe { GL.VertexAttribPointer(PositionAttribIndex, 3,
VertexAttribPointerType.Float, false, 0, (void*)0); }
if (!normals.IsEmpty) {
GL.CreateBufferFromData(normals);
GL.EnableVertexAttribArray(NormalAttribIndex);
unsafe { GL.VertexAttribPointer(NormalAttribIndex, 3,
VertexAttribPointerType.Float, false, 0, (void*)0); }
}
if (!uvs.IsEmpty) {
GL.CreateBufferFromData(uvs);
GL.EnableVertexAttribArray(UvAttribIndex);
unsafe { GL.VertexAttribPointer(UvAttribIndex, 2,
VertexAttribPointerType.Float, false, 0, (void*)0); }
}
return new(vao, indices.Length);
}
public static Mesh Create(Universe universe, ReadOnlySpan<Vector3D<float>> vertices,
ReadOnlySpan<Vector3D<float>> normals, ReadOnlySpan<Vector2D<float>> uvs)
{
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL;
var vao = GL.GenVertexArray();
GL.BindVertexArray(vao);
GL.CreateBufferFromData(vertices);
GL.EnableVertexAttribArray(PositionAttribIndex);
unsafe { GL.VertexAttribPointer(PositionAttribIndex, 3,
VertexAttribPointerType.Float, false, 0, (void*)0); }
if (!normals.IsEmpty) {
GL.CreateBufferFromData(normals);
GL.EnableVertexAttribArray(NormalAttribIndex);
unsafe { GL.VertexAttribPointer(NormalAttribIndex, 3,
VertexAttribPointerType.Float, false, 0, (void*)0); }
}
if (!uvs.IsEmpty) {
GL.CreateBufferFromData(uvs);
GL.EnableVertexAttribArray(UvAttribIndex);
unsafe { GL.VertexAttribPointer(UvAttribIndex, 2,
VertexAttribPointerType.Float, false, 0, (void*)0); }
}
return new(vao, vertices.Length, false);
}
}

@ -0,0 +1,81 @@
using System;
using gaemstone.ECS;
using Silk.NET.Input;
using Silk.NET.Maths;
using static gaemstone.Client.Input;
namespace gaemstone.Client;
[Module]
[DependsOn(typeof(Input))]
public class CameraModule
{
[Component]
public struct Camera
{
public static readonly Camera Default2D = Create2D();
public static readonly Camera Default3D = Create3D(80.0F);
public static Camera Create2D(float nearPlane = -100.0F, float farPlane = 100.0F)
=> new() { NearPlane = nearPlane, FarPlane = farPlane };
public static Camera Create3D(float fieldOfView, float nearPlane = 0.1F, float farPlane = 200.0F)
=> new() { FieldOfView = fieldOfView, NearPlane = nearPlane, FarPlane = farPlane };
public float FieldOfView { get; set; }
public float NearPlane { get; set; }
public float FarPlane { get; set; }
public bool IsOrthographic => (FieldOfView == 0.0F);
}
[Component]
public struct CameraViewport
{
public Vector4D<byte> ClearColor { get; set; }
public Rectangle<int> Viewport { get; set; }
}
[Component]
public struct CameraController
{
public float MouseSensitivity { get; set; }
public Vector2D<float>? MouseGrabbedAt { get; set; }
}
[System]
public static void UpdateCamera(TimeSpan delta, in Camera camera,
ref GlobalTransform transform, ref CameraController controller,
[Source(typeof(Game))] RawInput input)
{
var isMouseDown = input.IsDown(MouseButton.Right);
var isMouseGrabbed = controller.MouseGrabbedAt != null;
if (isMouseDown != isMouseGrabbed) {
if (isMouseDown) controller.MouseGrabbedAt = input.MousePosition;
else controller.MouseGrabbedAt = null;
}
if (controller.MouseGrabbedAt is not Vector2D<float> pos) return;
var mouseMoved = input.MousePosition - pos;
input.Context!.Mice[0].Position = pos.ToSystem();
var dt = (float)delta.TotalSeconds;
var xMovement = mouseMoved.X * dt * controller.MouseSensitivity;
var yMovement = mouseMoved.Y * dt * controller.MouseSensitivity;
if (camera.IsOrthographic) {
transform *= Matrix4X4.CreateTranslation(-xMovement, -yMovement, 0);
} else {
var speed = dt * (input.IsDown(Key.ShiftLeft) ? 12 : 4);
var forwardMovement = ((input.IsDown(Key.W) ? -1 : 0) + (input.IsDown(Key.S) ? 1 : 0)) * speed;
var sideMovement = ((input.IsDown(Key.A) ? -1 : 0) + (input.IsDown(Key.D) ? 1 : 0)) * speed;
var curTranslation = new Vector3D<float>(transform.Value.M41, transform.Value.M42, transform.Value.M43);
var yawRotation = Matrix4X4.CreateRotationY(-xMovement / 100, curTranslation);
var pitchRotation = Matrix4X4.CreateRotationX(-yMovement / 100);
var translation = Matrix4X4.CreateTranslation(sideMovement, 0, forwardMovement);
transform = translation * pitchRotation * transform * yawRotation;
}
}
}

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using gaemstone.ECS;
using Silk.NET.Input;
using Silk.NET.Maths;
using static gaemstone.Client.Windowing;
namespace gaemstone.Client;
[Module]
[DependsOn(typeof(Windowing))]
public class Input
{
[Component]
public class RawInput
{
internal IInputContext? Context { get; set; }
public Dictionary<Key, ButtonState> Keyboard { get; } = new();
public Dictionary<MouseButton, ButtonState> MouseButtons { get; } = new();
public Vector2D<float> MousePosition { get; set; }
public float MouseWheel { get; set; }
public float MouseWheelDelta { get; set; }
public bool IsDown(Key key) => Keyboard.GetValueOrDefault(key)?.IsDown == true;
public bool IsDown(MouseButton button) => MouseButtons.GetValueOrDefault(button)?.IsDown == true;
}
public class ButtonState
{
public TimeSpan TimePressed;
public bool IsDown;
public bool Pressed;
public bool Released;
}
[System(Phase.OnLoad)]
public static void ProcessInput(GameWindow window, RawInput input, TimeSpan delta)
{
window.Handle.DoEvents();
input.Context ??= window.Handle.CreateInput();
foreach (var state in input.Keyboard.Values.Concat(input.MouseButtons.Values)) {
if (state.IsDown) state.TimePressed += delta;
state.Pressed = state.Released = false;
}
var keyboard = input.Context.Keyboards[0];
foreach (var key in keyboard.SupportedKeys) {
var state = input.Keyboard.GetValueOrDefault(key);
if (keyboard.IsKeyPressed(key)) {
if (state == null) input.Keyboard.Add(key, state = new());
if (!state.IsDown) state.Pressed = true;
state.IsDown = true;
} else if (state != null) {
if (state.IsDown) state.Released = true;
state.IsDown = false;
}
}
var mouse = input.Context.Mice[0];
foreach (var button in mouse.SupportedButtons) {
var state = input.MouseButtons.GetValueOrDefault(button);
if (mouse.IsButtonPressed(button)) {
if (state == null) input.MouseButtons.Add(button, state = new());
if (!state.IsDown) state.Pressed = true;
state.IsDown = true;
} else if (state != null) {
if (state.IsDown) state.Released = true;
state.IsDown = false;
}
}
input.MousePosition = mouse.Position.ToGeneric();
input.MouseWheelDelta += mouse.ScrollWheels[0].Y - input.MouseWheel;
input.MouseWheel = mouse.ScrollWheels[0].Y;
}
}

@ -0,0 +1,109 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using gaemstone.ECS;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using static gaemstone.Client.CameraModule;
using static gaemstone.Client.Windowing;
namespace gaemstone.Client;
[Module]
[DependsOn(typeof(Windowing))]
public class Renderer
{
private readonly uint _program;
private readonly int _cameraMatrixUniform;
private readonly int _modelMatrixUniform;
public Renderer(Universe universe)
{
var GL = universe.Lookup<Game>().Get<Canvas>().GL;
GL.Enable(EnableCap.DebugOutputSynchronous);
GL.DebugMessageCallback(DebugCallback, 0);
GL.Enable(EnableCap.CullFace);
GL.CullFace(CullFaceMode.Back);
GL.Enable(EnableCap.DepthTest);
GL.DepthFunc(DepthFunction.Less);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
var vertexShaderSource = Resources.GetString("default.vs.glsl");
var fragmentShaderSource = Resources.GetString("default.fs.glsl");
var vertexShader = GL.CreateAndCompileShader(ShaderType.VertexShader , "vertex" , vertexShaderSource);
var fragmentShader = GL.CreateAndCompileShader(ShaderType.FragmentShader, "fragment", fragmentShaderSource);
_program = GL.CreateAndLinkProgram("program", vertexShader, fragmentShader);
_cameraMatrixUniform = GL.GetUniformLocation(_program, "cameraMatrix");
_modelMatrixUniform = GL.GetUniformLocation(_program, "modelMatrix");
}
[System]
public void Render(Universe universe, Canvas canvas)
{
var GL = canvas.GL;
GL.UseProgram(_program);
GL.Viewport(default, canvas.Size);
GL.ClearColor(new Vector4D<float>(0, 0, 0, 255));
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
Filter.RunOnce(universe, (in GlobalTransform transform, in Camera camera, CameraViewport? viewport) => {
var color = viewport?.ClearColor ?? new(0x4B, 0x00, 0x82, 255);
var bounds = viewport?.Viewport ?? new(default, canvas.Size);
GL.Enable(EnableCap.ScissorTest);
GL.Viewport(bounds); GL.Scissor(bounds.Origin.X, bounds.Origin.Y, (uint)bounds.Size.X, (uint)bounds.Size.Y);
GL.ClearColor(color); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
GL.Disable(EnableCap.ScissorTest);
// Get the camera's transform matrix and invert it.
Matrix4X4.Invert<float>(transform, out var cameraTransform);
// Create the camera's projection matrix.
var cameraProjection = camera.IsOrthographic
? Matrix4X4.CreateOrthographic(
bounds.Size.X, -bounds.Size.Y,
camera.NearPlane, camera.FarPlane)
: Matrix4X4.CreatePerspectiveFieldOfView(
camera.FieldOfView * MathF.PI / 180, // Degrees => Radians
(float)bounds.Size.X / bounds.Size.Y, // Aspect Ratio
camera.NearPlane, camera.FarPlane);
// Set the uniform to the combined transform and projection.
var cameraMatrix = cameraTransform * cameraProjection;
GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.Row1.X);
Filter.RunOnce(universe, (in GlobalTransform transform, in Mesh mesh, Texture? texture) =>
{
// 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 transform.Value.Row1.X);
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); }
// If entity has Texture, unbind it after it has been rendered.
if (texture.HasValue) GL.BindTexture(texture.Value.Target, 0);
});
});
}
[DebuggerStepThrough]
private static void DebugCallback(GLEnum source, GLEnum _type, int id, GLEnum _severity,
int length, nint _message, nint userParam)
{
var type = (DebugType)_type;
var severity = (DebugSeverity)_severity;
var message = Marshal.PtrToStringAnsi(_message, length);
Console.WriteLine($"[GLDebug] [{severity}] {type}/{id}: {message}");
if (type == DebugType.DebugTypeError) throw new Exception(message);
}
}

@ -0,0 +1,31 @@
using gaemstone.ECS;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace gaemstone.Client;
[Module]
public class Windowing
{
[Component]
public class Canvas
{
public GL GL { get; }
public Canvas(GL gl) => GL = gl;
public Vector2D<int> Size { get; set; }
public Color BackgroundColor { get; set; }
}
[Component]
public class GameWindow
{
public IWindow Handle { get; }
public GameWindow(IWindow handle) => Handle = handle;
}
[System(Phase.PreFrame)]
public static void ProcessWindow(GameWindow window, Canvas canvas)
=> canvas.Size = window.Handle.Size;
}

@ -0,0 +1,30 @@
using System;
using System.IO;
using System.Reflection;
namespace gaemstone.Client;
public static class Resources
{
public static Assembly ResourceAssembly { get; set; } = null!;
public static Stream GetStream(string name)
=> ResourceAssembly.GetManifestResourceStream(
ResourceAssembly.GetName().Name + ".Resources." + name)
?? throw new ArgumentException($"Could not find embedded resource '{name}'");
public static string GetString(string name)
{
using var stream = GetStream(name);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
public static byte[] GetBytes(string name)
{
using var stream = GetStream(name);
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
return memoryStream.ToArray();
}
}

@ -0,0 +1,14 @@
using gaemstone.ECS;
using Silk.NET.OpenGL;
namespace gaemstone.Client;
[Component]
public readonly struct Texture
{
public TextureTarget Target { get; }
public uint Handle { get; }
public Texture(TextureTarget target, uint handle)
=> (Target, Handle) = (target, handle);
}

@ -0,0 +1,36 @@
using System.Drawing;
using gaemstone.ECS;
using Silk.NET.Maths;
namespace gaemstone.Client;
[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 TextureCoords4(float x1, float y1, float x2, float y2)
{
TopLeft = new(x1, y1);
TopRight = new(x2, y1);
BottomLeft = new(x1, y2);
BottomRight = new(x2, y2);
}
public static TextureCoords4 FromIntCoords(Size textureSize, Point origin, Size size)
=> FromIntCoords(textureSize, origin.X, origin.Y, size.Width, size.Height);
public static TextureCoords4 FromIntCoords(Size textureSize, int x, int y, int width, int height) => new(
x / (float)textureSize.Width + 0.001F,
y / (float)textureSize.Height + 0.001F,
(x + width) / (float)textureSize.Width - 0.001F,
(y + height) / (float)textureSize.Height - 0.001F);
public static TextureCoords4 FromGrid(int numCellsX, int numCellsY, int cellX, int cellY) => new(
cellX / (float)numCellsX + 0.001F,
cellY / (float)numCellsY + 0.001F,
(cellX + 1) / (float)numCellsX - 0.001F,
(cellY + 1) / (float)numCellsY - 0.001F);
}

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.IO;
using gaemstone.ECS;
using Silk.NET.OpenGL;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Size = System.Drawing.Size;
namespace gaemstone.Client;
public static class TextureManager
{
private static readonly Dictionary<Texture, TextureInfo> _byTexture = new();
private static readonly Dictionary<string, TextureInfo> _bySourceFile = new();
public static void Initialize(Universe universe)
{
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL;
// Upload single-pixel white texture into texture slot 0, so when
// "no" texture is bound, we can still use the texture sampler.
GL.BindTexture(TextureTarget.Texture2D, 0);
Span<byte> pixel = stackalloc byte[4];
pixel.Fill(255);
GL.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba,
1, 1, 0, PixelFormat.Rgba, PixelType.UnsignedByte, in pixel[0]);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
}
public static Texture Load(Universe universe, string name)
{
using var stream = Resources.GetStream(name);
return CreateFromStream(universe, stream, name);
}
public static Texture CreateFromStream(Universe universe, Stream stream, string? sourceFile = null)
{
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL;
var texture = new Texture(TextureTarget.Texture2D, GL.GenTexture());
GL.BindTexture(texture.Target, texture.Handle);
var image = Image.Load<Rgba32>(stream);
ref var origin = ref image.Frames[0].PixelBuffer[0, 0];
GL.TexImage2D(texture.Target, 0, (int)PixelFormat.Rgba,
(uint)image.Width, (uint)image.Height, 0,
PixelFormat.Rgba, PixelType.UnsignedByte, origin);
GL.TexParameter(texture.Target, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
GL.TexParameter(texture.Target, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
var info = new TextureInfo(texture, sourceFile, new(image.Width, image.Height));
_byTexture.Add(texture, info);
if (sourceFile != null) _bySourceFile.Add(sourceFile, info);
GL.BindTexture(texture.Target, 0);
return texture;
}
public static TextureInfo? Lookup(Texture texture)
=> _byTexture.TryGetValue(texture, out var value) ? value : null;
public static TextureInfo? Lookup(string sourceFile)
=> _bySourceFile.TryGetValue(sourceFile, out var value) ? value : null;
}
public class TextureInfo
{
public Texture Texture { get; }
public string? SourceFile { get; }
public Size Size { get; }
public TextureInfo(Texture texture, string? sourceFile, Size size)
=> (Texture, SourceFile, Size) = (texture, sourceFile, size);
}

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../flecs-cs/src/cs/production/Flecs/Flecs.csproj" />
<ProjectReference Include="../gaemstone/gaemstone.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.0-alpha0026" />
<PackageReference Include="Silk.NET" Version="2.16.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
</ItemGroup>
</Project>

@ -0,0 +1,12 @@
using System;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class ComponentAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Struct)]
public class TagAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class RelationAttribute : Attribute { }

@ -0,0 +1,193 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class EntityAttribute : Attribute { }
public unsafe readonly struct Entity
{
public Universe Universe { get; }
public ecs_entity_t Value { get; }
public EntityType Type => new(Universe, ecs_get_type(Universe, Value));
public string Name => ecs_get_name(Universe, Value).ToStringAndFree();
public string FullPath => ecs_get_path_w_sep(Universe, default, Value, ".", default).ToStringAndFree();
public bool IsNone => Value.Data == 0;
public bool IsAlive => ecs_is_alive(Universe, Value);
public IEnumerable<Entity> Children { get {
var term = new ecs_term_t { id = Universe.EcsChildOf & this };
var iter = Iterator.FromTerm(Universe, term);
while (iter.Next())
for (var i = 0; i < iter.Count; i++)
yield return iter.Entity(i);
} }
public Entity(Universe universe, ecs_entity_t value)
{ Universe = universe; Value = value; }
public void ThrowIfNone() { if (IsNone) throw new InvalidOperationException("Entity isn't valid"); }
public void ThrowIfDead() { if (!IsAlive) throw new InvalidOperationException("Entity is dead"); }
public void Delete() => ecs_delete(Universe, Value);
public Entity Add(ecs_id_t id) { ecs_add_id(Universe, this, id); return this; }
public Entity Add(Identifier id) { ecs_add_id(Universe, this, id); return this; }
public Entity Add(Entity relation, Entity target) => Add(relation & target);
public Entity Add<T>()
=> Add(Universe.Lookup<T>());
public Entity Add<TRelation, TTarget>()
=> Add(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>());
public Entity Add<TRelation>(Entity target)
=> Add(Universe.Lookup<TRelation>(), target);
public Entity Override(ecs_id_t id) { ecs_override_id(Universe, this, id); return this; }
public Entity Override(Identifier id) { ecs_override_id(Universe, this, id); return this; }
public Entity Override(Entity relation, Entity target) => Override(relation & target);
public Entity Override<T>()
=> Override(Universe.Lookup<T>());
public Entity Override<TRelation, TTarget>()
=> Override(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>());
public Entity Override<TRelation>(Entity target)
=> Override(Universe.Lookup<TRelation>(), target);
public void Remove(ecs_id_t id) => ecs_remove_id(Universe, this, id);
public void Remove(Identifier id) => ecs_remove_id(Universe, this, id);
public void Remove<T>() => Remove(Universe.Lookup<T>());
public bool Has(ecs_id_t id) => ecs_has_id(Universe, this, id);
public bool Has(Identifier id) => ecs_has_id(Universe, this, id);
public bool Has(Entity relation, Entity target) => Has(relation & target);
public bool Has<T>()
=> Has(Universe.Lookup<T>());
public bool Has<TRelation, TTarget>()
=> Has(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>());
public bool Has<TRelation>(Entity target)
=> Has(Universe.Lookup<TRelation>(), target);
/// <summary>
/// Gets a component value from this entity. If the component is a value
/// type, this will return a copy. If the component is a reference type,
/// it will return the reference itself.
/// When modifying a reference, consider calling <see cref="Modified"/>.
/// </summary>
public T Get<T>()
{
var comp = Universe.Lookup<T>();
var ptr = ecs_get_id(Universe, this, comp);
if (typeof(T).IsValueType) {
return Unsafe.Read<T>(ptr);
} else {
var handle = (GCHandle)Unsafe.Read<nint>(ptr);
return (T)handle.Target!;
}
}
/// <summary>
/// Gets a reference to a component value from this entity. Only works for
/// value types. When modifying, consider calling <see cref="Modified"/>.
/// </summary>
public ref T GetRef<T>()
where T : unmanaged
{
var comp = Universe.Lookup<T>();
var ptr = ecs_get_mut_id(Universe, this, comp);
return ref Unsafe.AsRef<T>(ptr);
}
/// <summary>
/// Marks a component as modified. Do this after getting a reference to
/// it with <see cref="Get"/> or <see cref="GetRef"/>, making sure change
/// detection will kick in.
/// </summary>
public void Modified<T>()
{
var comp = Universe.Lookup<T>();
ecs_modified_id(Universe, this, comp);
}
public Entity Set<T>(in T value)
where T : unmanaged
{
var comp = Universe.Lookup<T>();
var size = (ulong)Unsafe.SizeOf<T>();
fixed (T* ptr = &value) ecs_set_id(Universe, this, comp, size, ptr);
return this;
}
public Entity SetOverride<T>(in T value)
where T : unmanaged
{
var comp = Universe.Lookup<T>();
var size = (ulong)Unsafe.SizeOf<T>();
ecs_add_id(Universe, this, Universe.ECS_OVERRIDE | comp);
fixed (T* ptr = &value) ecs_set_id(Universe, this, comp, size, ptr);
return this;
}
public Entity Set(Type type, object obj)
{
var comp = Universe.Lookup(type);
var handle = (nint)GCHandle.Alloc(obj);
ecs_set_id(Universe, this, comp, (ulong)sizeof(nint), &handle);
// FIXME: Handle needs to be freed when component is removed!
return this;
}
public Entity Set<T>(T obj) where T : class
=> Set(typeof(T), obj);
public Entity SetOverride<T>(T obj)
where T : class
{
var comp = Universe.Lookup<T>();
var handle = (nint)GCHandle.Alloc(obj);
ecs_add_id(Universe, this, Universe.ECS_OVERRIDE | comp);
ecs_set_id(Universe, this, comp, (ulong)sizeof(nint), &handle);
// FIXME: Handle needs to be freed when component is removed!
return this;
}
public static Identifier operator &(Entity first, Entity second) => Identifier.Pair(first, second);
public static Identifier operator &(ecs_entity_t first, Entity second) => Identifier.Pair(first, second);
public static Identifier operator |(ecs_id_t left, Entity right) => new(right.Universe, left | right.Value.Data);
public static implicit operator ecs_id_t(Entity e) => e.Value.Data;
public static implicit operator ecs_entity_t(Entity e) => e.Value;
public static implicit operator Identifier(Entity e) => new(e.Universe, e);
}
public unsafe readonly struct EntityType
: IEnumerable<Identifier>
{
public Universe Universe { get; }
public unsafe ecs_type_t* Handle { get; }
public int Count => Handle->count;
public Identifier this[int index] => new(Universe, Handle->array[index]);
public EntityType(Universe universe, ecs_type_t* handle)
{ Universe = universe; Handle = handle; }
public IEnumerator<Identifier> GetEnumerator()
{ for (var i = 0; i < Count; i++) yield return this[i]; }
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public override string ToString()
=> ecs_type_str(Universe, Handle).ToStringAndFree();
}

@ -0,0 +1,20 @@
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public struct EntityDesc
{
public ecs_entity_desc_t Value;
public string? Name { get => Value.name; set => Value.name.Set(value); }
public string? Symbol { get => Value.symbol; set => Value.symbol.Set(value); }
public EntityDesc(params ecs_id_t[] ids)
{
Value = default;
for (var i = 0; i < ids.Length; i++)
Value.add[i] = ids[i];
}
public static explicit operator ecs_entity_desc_t(EntityDesc desc) => desc.Value;
}

@ -0,0 +1,39 @@
using System;
using System.Collections;
using System.Collections.Generic;
using gaemstone.Utility.IL;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe class Filter
: IEnumerable<Iterator>
, IDisposable
{
public Universe Universe { get; }
public ecs_filter_t* Handle { get; }
private Filter(Universe universe, ecs_filter_t* handle)
{ Universe = universe; Handle = handle; }
public Filter(Universe universe, ecs_filter_desc_t desc)
: this(universe, ecs_filter_init(universe, &desc)) { }
public Filter(Universe universe, string expression)
: this(universe, new ecs_filter_desc_t { expr = expression }) { }
public static void RunOnce(Universe universe, Delegate action)
{
var gen = QueryActionGenerator.GetOrBuild(universe, action.Method);
using var filter = new Filter(universe, gen.Filter);
foreach (var iter in filter) gen.RunWithTryCatch(action.Target, iter);
}
~Filter() => Dispose();
public void Dispose() { ecs_filter_fini(Handle); GC.SuppressFinalize(this); }
public Iterator Iter() => new(Universe, IteratorType.Filter, ecs_filter_iter(Universe, this));
public IEnumerator<Iterator> GetEnumerator() => Iter().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public static implicit operator ecs_filter_t*(Filter q) => q.Handle;
}

@ -0,0 +1,17 @@
using System;
using System.Diagnostics;
namespace gaemstone.ECS;
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();
internal FlecsAbortException() : base("Abort was called by flecs") { }
public override string? StackTrace => _stackTrace;
}

@ -0,0 +1,53 @@
using System;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe readonly struct Identifier
{
public Universe Universe { get; }
public ecs_id_t Value { get; }
public bool IsPair => ecs_id_is_pair(Value);
public IdentifierFlags Flags => (IdentifierFlags)(Value.Data & ECS_ID_FLAGS_MASK);
public Identifier(Universe universe, ecs_id_t value)
{ Universe = universe; Value = value; }
public static Identifier Pair(Entity first, Entity second)
=> new(first.Universe, Universe.ECS_PAIR | ((first.Value.Data << 32) + (uint)second.Value.Data));
public static Identifier Pair(ecs_entity_t first, Entity second)
=> new(second.Universe, Universe.ECS_PAIR | ((first.Data << 32) + (uint)second.Value.Data));
public (Entity, Entity) AsPair()
=> (Universe.Lookup((ecs_id_t)((Value & ECS_COMPONENT_MASK) >> 32)),
Universe.Lookup((ecs_id_t)(Value & ECS_ENTITY_MASK)));
// public Entity AsComponent()
// {
// var value = Value.Data & ECS_COMPONENT_MASK;
// return new Entity(Universe, new() { Data = value });
// }
public override string ToString()
=> ecs_id_str(Universe, Value).ToStringAndFree();
public static implicit operator ecs_id_t(Identifier e) => e.Value;
public static Identifier operator |(ecs_id_t left, Identifier right)
=> new(right.Universe, left | right.Value);
public static Identifier operator |(Identifier left, Identifier right)
=> new(left.Universe, left.Value | right.Value);
}
[Flags]
public enum IdentifierFlags : ulong
{
Pair = 1ul << 63,
Override = 1ul << 62,
Toggle = 1ul << 61,
Or = 1ul << 60,
And = 1ul << 59,
Not = 1ul << 58,
}

@ -0,0 +1,93 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe class Iterator
: IEnumerable<Iterator>
{
public Universe Universe { get; }
public IteratorType? Type { get; }
public ecs_iter_t Value;
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, in ecs_term_t term)
{
fixed (ecs_term_t* ptr = &term)
return new(universe, IteratorType.Term, ecs_term_iter(universe, ptr));
}
public bool Next()
{
fixed (ecs_iter_t* ptr = &Value)
return 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),
};
}
public Entity Entity(int index)
=> new(Universe, 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 pointer = ecs_field_w_size(ptr, size, index);
return new Span<T>(pointer, Count);
}
}
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)
where T : unmanaged
{
fixed (ecs_iter_t* ptr = &Value) {
var id = ecs_field_id(ptr, index);
var comp = Universe.Lookup<T>();
return id == comp.Value.Data;
}
}
public readonly ref struct SpanToRef<T>
{
private readonly Span<nint> _span;
internal SpanToRef(Span<nint> span) => _span = span;
public int Length => _span.Length;
public T this[int index] => (T)((GCHandle)_span[index]).Target!;
}
// IEnumerable implementation
public IEnumerator<Iterator> GetEnumerator() { while (Next()) yield return this; }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public enum IteratorType
{
Term,
Filter,
Query,
Rule,
}

@ -0,0 +1,13 @@
using System;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Class)]
public class ModuleAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DependsOnAttribute : Attribute
{
public Type Target { get; }
public DependsOnAttribute(Type target) => Target = target;
}

@ -0,0 +1,18 @@
using System;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Method)]
public class ObserverAttribute : Attribute
{
public Event Event { get; }
public ObserverAttribute(Event @event)
=> Event = @event;
}
public enum Event
{
OnAdd,
OnSet,
OnRemove,
}

@ -0,0 +1,33 @@
using System;
using System.Collections;
using System.Collections.Generic;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe class Query
: IEnumerable<Iterator>
, IDisposable
{
public Universe Universe { get; }
public ecs_query_t* Handle { get; }
private Query(Universe universe, ecs_query_t* handle)
{ Universe = universe; Handle = handle; }
public Query(Universe universe, ecs_query_desc_t desc)
: this(universe, ecs_query_init(universe, &desc)) { }
public Query(Universe universe, ecs_filter_desc_t desc)
: this(universe, new ecs_query_desc_t { filter = desc }) { }
public Query(Universe universe, string expression)
: this(universe, new ecs_filter_desc_t { expr = expression }) { }
~Query() => Dispose();
public void Dispose() { ecs_query_fini(this); GC.SuppressFinalize(this); }
public Iterator Iter() => new(Universe, IteratorType.Query, ecs_query_iter(Universe, this));
public IEnumerator<Iterator> GetEnumerator() => Iter().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public static implicit operator ecs_query_t*(Query q) => q.Handle;
}

@ -0,0 +1,93 @@
using System;
namespace gaemstone.ECS;
[AttributeUsage(AttributeTargets.Method)]
public class SystemAttribute : Attribute
{
public string? Expression { get; set; }
public Phase Phase { get; set; }
public SystemAttribute() : this(Phase.OnUpdate) { }
public SystemAttribute(Phase phase) => Phase = phase;
}
[AttributeUsage(AttributeTargets.Parameter)]
public class SourceAttribute : Attribute
{
public Type Type { get; }
public SourceAttribute(Type type) => Type = type;
}
[AttributeUsage(AttributeTargets.Parameter)]
public class HasAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Parameter)]
public class NotAttribute : Attribute { }
public enum Phase
{
PreFrame,
/// <summary>
/// This phase contains all the systems that load data into your ECS.
/// This would be a good place to load keyboard and mouse inputs.
/// </summary>
OnLoad,
/// <summary>
/// Often the imported data needs to be processed. Maybe you want to associate
/// your keypresses with high level actions rather than comparing explicitly
/// in your game code if the user pressed the 'K' key.
/// </summary>
PostLoad,
/// <summary>
/// Now that the input is loaded and processed, it's time to get ready to
/// start processing our game logic. Anything that needs to happen after
/// input processing but before processing the game logic can happen here.
/// This can be a good place to prepare the frame, maybe clean up some
/// things from the previous frame, etcetera.
/// </summary>
PreUpdate,
/// <summary>
/// This is usually where the magic happens! This is where you put all of
/// your gameplay systems. By default systems are added to this phase.
/// </summary>
OnUpdate,
/// <summary>
/// This phase was introduced to deal with validating the state of the game
/// after processing the gameplay systems. Sometimes you entities too close
/// to each other, or the speed of an entity is increased too much.
/// This phase is for righting that wrong. A typical feature to implement
/// in this phase would be collision detection.
/// </summary>
OnValidate,
/// <summary>
/// When your game logic has been updated, and your validation pass has ran,
/// you may want to apply some corrections. For example, if your collision
/// detection system detected collisions in the <c>OnValidate</c> phase,
/// you may want to move the entities so that they no longer overlap.
/// </summary>
PostUpdate,
/// <summary>
/// Now that all of the frame data is computed, validated and corrected for,
/// it is time to prepare the frame for rendering. Any systems that need to
/// run before rendering, but after processing the game logic should go here.
/// A good example would be a system that calculates transform matrices from
/// a scene graph.
/// </summary>
PreStore,
/// <summary>
/// This is where it all comes together. Your frame is ready to be
/// rendered, and that is exactly what you would do in this phase.
/// </summary>
OnStore,
PostFrame,
}

@ -0,0 +1,140 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using gaemstone.Utility;
namespace gaemstone.ECS;
public unsafe partial class Universe
{
public void RegisterModule<T>() where T : class
=> RegisterModule(typeof(T));
public void RegisterModule(Type type)
{
var builder = new ModuleBuilder(this, type);
builder.UnmetDependencies.ExceptWith(Modules._modules.Keys);
if (builder.UnmetDependencies.Count > 0) {
// If builder has unmet dependencies, defer the registration.
Modules._deferred.Add(type, builder);
} else {
// Otherwise register it right away, ..
Modules._modules.Add(type, new ModuleInfo(builder));
// .. and tell other deferred modules this one is now loaded.
RemoveDependency(builder.Type);
}
}
private void RemoveDependency(Type type)
{
var resolved = Modules._deferred.Values
.Where(d => d.UnmetDependencies.Remove(type)
&& d.UnmetDependencies.Count == 0)
.ToArray();
foreach (var builder in resolved) {
Modules._deferred.Remove(builder.Type);
Modules._modules.Add(type, new ModuleInfo(builder));
RemoveDependency(builder.Type);
}
}
public class UniverseModules
{
internal readonly Dictionary<Type, ModuleInfo> _modules = new();
internal readonly Dictionary<Type, ModuleBuilder> _deferred = new();
internal UniverseModules(Universe universe) { }
}
public class ModuleInfo
{
public Universe Universe { get; }
public object Instance { get; }
public IReadOnlyList<Entity> Relations { get; }
public IReadOnlyList<Entity> Components { get; }
public IReadOnlyList<Entity> Tags { get; }
public IReadOnlyList<Entity> Entities { get; }
public IReadOnlyList<SystemInfo> Systems { get; }
internal ModuleInfo(ModuleBuilder builder)
{
Universe = builder.Universe;
Instance = builder.HasSimpleConstructor
? Activator.CreateInstance(builder.Type)!
: Activator.CreateInstance(builder.Type, Universe)!;
Relations = builder.Relations .Select(Universe.RegisterRelation ).ToImmutableList();
Components = builder.Components.Select(Universe.RegisterComponent).ToImmutableList();
Tags = builder.Tags .Select(Universe.RegisterTag ).ToImmutableList();
Entities = builder.Entities .Select(Universe.RegisterEntity ).ToImmutableList();
Systems = builder.Systems.Select(s => Universe.RegisterSystem(Instance, s)).ToImmutableList();
}
}
public class ModuleBuilder
{
public Universe Universe { get; }
public Type Type { get; }
public IReadOnlyList<Type> DependsOn { get; }
public bool HasSimpleConstructor { get; }
public IReadOnlyList<Type> Relations { get; }
public IReadOnlyList<Type> Components { get; }
public IReadOnlyList<Type> Tags { get; }
public IReadOnlyList<Type> Entities { get; }
public IReadOnlyList<MethodInfo> Systems { get; }
public HashSet<Type> UnmetDependencies { get; }
internal ModuleBuilder(Universe universe, Type type)
{
if (!type.IsClass || type.IsAbstract) throw new Exception(
"Module must be a non-abstract class");
if (!type.Has<ModuleAttribute>()) throw new Exception(
"Module must be marked with ModuleAttribute");
Universe = universe;
Type = type;
DependsOn = type.GetMultiple<DependsOnAttribute>()
.Select(d => d.Target).ToImmutableList();
HasSimpleConstructor = type.GetConstructor(Type.EmptyTypes) != null;
var hasUniverseConstructor = type.GetConstructor(new[] { typeof(Universe) }) != null;
if (!HasSimpleConstructor && !hasUniverseConstructor) throw new Exception(
$"Module {Type} must define a public constructor with either no parameters, or a single {nameof(Universe)} parameter");
var relations = new List<Type>();
var components = new List<Type>();
var tags = new List<Type>();
var entities = new List<Type>();
var systems = new List<MethodInfo>();
foreach (var nested in Type.GetNestedTypes()) {
if (nested.Has<RelationAttribute>()) relations.Add(nested);
else if (nested.Has<ComponentAttribute>()) components.Add(nested);
else if (nested.Has<TagAttribute>()) tags.Add(nested);
else if (nested.Has<EntityAttribute>()) entities.Add(nested);
}
foreach (var method in Type.GetMethods())
if (method.Has<SystemAttribute>())
systems.Add(method);
var elements = new IList[] { relations, components, tags, entities, systems };
if (elements.Sum(l => l.Count) == 0) throw new Exception(
"Module must define at least one ECS related type or method");
Relations = relations.AsReadOnly();
Components = components.AsReadOnly();
Tags = tags.AsReadOnly();
Entities = entities.AsReadOnly();
Systems = systems.AsReadOnly();
UnmetDependencies = DependsOn.ToHashSet();
}
}
}

@ -0,0 +1,167 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using gaemstone.Utility;
using gaemstone.Utility.IL;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
public unsafe partial class Universe
{
public SystemInfo RegisterSystem(Action<Iterator> callback, string expression,
Phase? phase = null, string? name = null)
=> RegisterSystem(name ?? callback.Method.Name, expression, phase ?? Phase.OnUpdate, new() { expr = expression }, callback);
public SystemInfo RegisterSystem(Action<Iterator> callback, ecs_filter_desc_t filter,
Phase? phase = null, string? name = null)
=> RegisterSystem(name ?? callback.Method.Name, null, phase ?? Phase.OnUpdate, filter, callback);
public SystemInfo RegisterSystem(string name, string? expression,
Phase phase, ecs_filter_desc_t filter, Action<Iterator> callback)
{
var _phase = Systems._phaseLookup[phase];
var entityDesc = default(ecs_entity_desc_t);
entityDesc.name = name;
entityDesc.add[0] = !_phase.IsNone ? (EcsDependsOn & _phase) : default;
entityDesc.add[1] = _phase;
// TODO: Provide a nice way to create these entity descriptors.
var systemDesc = default(ecs_system_desc_t);
systemDesc.entity = Create(entityDesc);
systemDesc.binding_ctx = (void*)UniverseSystems.CreateSystemCallbackContext(this, callback);
systemDesc.callback.Data.Pointer = &UniverseSystems.SystemCallback;
systemDesc.query.filter = filter;
var entity = new Entity(this, ecs_system_init(Handle, &systemDesc));
var system = new SystemInfo(this, entity, name, expression, phase, filter, callback);
Systems._systems.Add(system);
return system;
}
public SystemInfo RegisterSystem(Delegate action)
{
var name = action.Method.Name;
var attr = action.Method.Get<SystemAttribute>();
var phase = attr?.Phase ?? Phase.OnUpdate;
if (action is Action<Iterator> iterAction) {
if (attr?.Expression == null) throw new Exception(
"System must specify expression in SystemAttribute");
return RegisterSystem(name, attr.Expression, phase, new() { expr = attr.Expression }, iterAction);
} else {
var method = action.GetType().GetMethod("Invoke")!;
var gen = QueryActionGenerator.GetOrBuild(this, method);
var filter = (attr?.Expression == null) ? gen.Filter : new() { expr = attr.Expression };
return RegisterSystem(name, attr?.Expression, phase, filter,
iter => gen.RunWithTryCatch(action.Target, iter));
}
}
public SystemInfo RegisterSystem(object? instance, MethodInfo method)
{
var attr = method.Get<SystemAttribute>();
var phase = attr?.Phase ?? Phase.OnUpdate;
var param = method.GetParameters();
if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) {
if (attr?.Expression == null) throw new Exception(
"System must specify expression in SystemAttribute");
var action = (Action<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), instance, method);
return RegisterSystem(method.Name, attr.Expression, phase, new() { expr = attr.Expression }, action);
} else {
var gen = QueryActionGenerator.GetOrBuild(this, method);
var filter = (attr?.Expression == null) ? gen.Filter : new() { expr = attr.Expression };
return RegisterSystem(method.Name, attr?.Expression, phase, filter,
iter => gen.RunWithTryCatch(instance, iter));
}
}
public class UniverseSystems
: IReadOnlyCollection<SystemInfo>
{
public readonly struct SystemCallbackContext
{
public Universe Universe { get; }
public Action<Iterator> Callback { get; }
public SystemCallbackContext(Universe universe, Action<Iterator> callback)
{ Universe = universe; Callback = callback; }
}
private static SystemCallbackContext[] _systemCallbackContexts = new SystemCallbackContext[64];
private static int _systemCallbackContextsCount = 0;
public static nint CreateSystemCallbackContext(Universe universe, Action<Iterator> callback)
{
var data = new SystemCallbackContext(universe, callback);
var count = Interlocked.Increment(ref _systemCallbackContextsCount);
if (count > _systemCallbackContexts.Length)
Array.Resize(ref _systemCallbackContexts, count * 2);
_systemCallbackContexts[count - 1] = data;
return count;
}
public static SystemCallbackContext GetSystemCallbackContext(nint context)
=> _systemCallbackContexts[(int)context - 1];
[UnmanagedCallersOnly]
internal static void SystemCallback(ecs_iter_t* iter)
{
var data = GetSystemCallbackContext((nint)iter->binding_ctx);
data.Callback(new Iterator(data.Universe, null, *iter));
}
internal readonly List<SystemInfo> _systems = new();
internal readonly Dictionary<Phase, Entity> _phaseLookup = new();
internal UniverseSystems(Universe universe)
{
_phaseLookup.Add(Phase.PreFrame, new(universe, pinvoke_EcsPreFrame()));
_phaseLookup.Add(Phase.OnLoad, new(universe, pinvoke_EcsOnLoad()));
_phaseLookup.Add(Phase.PostLoad, new(universe, pinvoke_EcsPostLoad()));
_phaseLookup.Add(Phase.PreUpdate, new(universe, pinvoke_EcsPreUpdate()));
_phaseLookup.Add(Phase.OnUpdate, new(universe, pinvoke_EcsOnUpdate()));
_phaseLookup.Add(Phase.OnValidate, new(universe, pinvoke_EcsOnValidate()));
_phaseLookup.Add(Phase.PostUpdate, new(universe, pinvoke_EcsPostUpdate()));
_phaseLookup.Add(Phase.PreStore, new(universe, pinvoke_EcsPreStore()));
_phaseLookup.Add(Phase.OnStore, new(universe, pinvoke_EcsOnStore()));
_phaseLookup.Add(Phase.PostFrame, new(universe, pinvoke_EcsPostFrame()));
}
// IReadOnlyCollection implementation
public int Count => _systems.Count;
public IEnumerator<SystemInfo> GetEnumerator() => _systems.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class SystemInfo
{
public Universe Universe { get; }
public Entity Entity { get; }
public string Name { get; }
public string? Expression { get; }
public Phase Phase { get; }
public ecs_filter_desc_t Filter { get; }
public Action<Iterator> Callback { get; }
internal SystemInfo(Universe universe, Entity entity, string name, string? expression,
Phase phase, ecs_filter_desc_t filter, Action<Iterator> callback)
{
Universe = universe;
Entity = entity;
Name = name;
Expression = expression;
Phase = phase;
Filter = filter;
Callback = callback;
}
}
}

@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
[Entity]
public struct Game { }
[Component]
public unsafe partial class Universe
{
// Roles
public static ecs_id_t ECS_PAIR { get; } = pinvoke_ECS_PAIR();
public static ecs_id_t ECS_OVERRIDE { get; } = pinvoke_ECS_OVERRIDE();
// Relationships
public static ecs_entity_t EcsIsA { get; } = pinvoke_EcsIsA();
public static ecs_entity_t EcsDependsOn { get; } = pinvoke_EcsDependsOn();
public static ecs_entity_t EcsChildOf { get; } = pinvoke_EcsChildOf();
public static ecs_entity_t EcsSlotOf { get; } = pinvoke_EcsSlotOf();
// Entity Tags
public static ecs_entity_t EcsPrefab { get; } = pinvoke_EcsPrefab();
private readonly Dictionary<Type, ecs_entity_t> _byType = new();
public ecs_world_t* Handle { get; }
public UniverseSystems Systems { get; }
public UniverseModules Modules { get; }
public Universe(string[]? args = null)
{
[UnmanagedCallersOnly]
static void Abort() => throw new FlecsAbortException();
ecs_os_set_api_defaults();
var api = ecs_os_get_api();
api.abort_ = new FnPtr_Void { Pointer = &Abort };
ecs_os_set_api(&api);
if (args?.Length > 0) {
var argv = Runtime.CStrings.CStringArray(args);
Handle = ecs_init_w_args(args.Length, argv);
Runtime.CStrings.FreeCStrings(argv, args.Length);
} else {
Handle = ecs_init();
}
Systems = new(this);
Modules = new(this);
RegisterAll(typeof(Universe).Assembly);
}
public bool Progress(TimeSpan delta)
{
if (Modules._deferred.Count > 0) throw new Exception(
"Modules with unmet dependencies: \n" +
string.Join(" \n", Modules._deferred.Values.Select(
m => m.Type + " is missing " + string.Join(", ", m.UnmetDependencies))));
return ecs_progress(this, (float)delta.TotalSeconds);
}
public Entity Lookup<T>()
=> Lookup(typeof(T));
public Entity Lookup(Type type)
=> _byType.TryGetValue(type, out var e) ? new(this, e) : default;
public Entity Lookup(string path)
=> new(this, !path.Contains('.') ? ecs_lookup(this, path)
: ecs_lookup_path_w_sep(this, default, path, ".", default, true));
public Entity Lookup(ecs_entity_t value)
=> new(this, ecs_get_alive(this, value));
public void RegisterAll(Assembly? from = null)
{
from ??= Assembly.GetEntryAssembly()!;
foreach (var type in from.GetTypes()) {
var isPartOfModule = type.DeclaringType?.Has<ModuleAttribute>() == true;
if (type.Has<RelationAttribute>()) {
if (!isPartOfModule) RegisterRelation(type);
} else if (type.Has<ComponentAttribute>()) {
if (!isPartOfModule) RegisterComponent(type);
} else if (type.Has<TagAttribute>()) {
if (!isPartOfModule) RegisterTag(type);
} else if (type.Has<EntityAttribute>()) {
if (!isPartOfModule) RegisterEntity(type);
} else if (type.Has<ModuleAttribute>())
RegisterModule(type);
}
}
public Entity RegisterRelation<T>()
=> RegisterRelation(typeof(T));
public Entity RegisterRelation(Type type)
=> throw new NotImplementedException();
public Entity RegisterComponent<T>()
=> RegisterComponent(typeof(T));
public Entity RegisterComponent(Type type)
{
var typeInfo = default(ecs_type_info_t);
if (type.IsValueType) {
var wrapper = TypeWrapper.For(type);
if (!wrapper.IsUnmanaged) throw new Exception(
"Component struct must satisfy the unmanaged constraint. " +
"(Must not contain any reference types or structs that contain references.)");
var structLayout = type.StructLayoutAttribute;
if (structLayout == null || structLayout.Value == LayoutKind.Auto) throw new Exception(
"Component struct must have a StructLayout attribute with LayoutKind sequential or explicit. " +
"This is to ensure that the struct fields are not reorganized by the C# compiler.");
typeInfo.size = wrapper.Size;
typeInfo.alignment = structLayout.Pack;
} else {
typeInfo.size = sizeof(nint);
typeInfo.alignment = sizeof(nint);
}
var name = type.GetFriendlyName();
var entityDesc = new ecs_entity_desc_t { name = name, symbol = name };
var componentDesc = new ecs_component_desc_t { entity = Create(entityDesc), type = typeInfo };
var id = ecs_component_init(Handle, &componentDesc);
_byType[type] = id;
// TODO: SetHooks(hooks, id);
var entity = new Entity(this, id);
if (type.Has<EntityAttribute>()) {
if (type.IsValueType) entity.Add(entity);
else entity.Set(type, Activator.CreateInstance(type)!);
}
return entity;
}
public Entity RegisterTag<T>()
where T : unmanaged
=> RegisterTag(typeof(T));
public Entity RegisterTag(Type type)
{
if (!type.IsValueType || type.IsPrimitive || type.GetFields().Length > 0)
throw new Exception("Tag must be an empty, used-defined struct.");
var entity = Create(type.GetFriendlyName());
_byType.Add(type, entity);
return entity;
}
public Entity RegisterEntity<T>()
where T : unmanaged
=> RegisterEntity(typeof(T));
public Entity RegisterEntity(Type type)
{
if (!type.IsValueType || type.IsPrimitive || type.GetFields().Length > 0)
throw new Exception("Entity must be an empty, used-defined struct.");
var entity = Create(type.GetFriendlyName());
_byType.Add(type, entity);
return entity;
}
public Entity Create()
=> Create(new ecs_entity_desc_t());
public Entity Create(string name)
=> Create(new ecs_entity_desc_t { name = name });
public Entity Create(ecs_entity_desc_t desc)
{
var entity = ecs_entity_init(Handle, &desc);
Debug.Assert(entity.Data != 0, "ECS_INVALID_PARAMETER");
return new(this, entity);
}
public static implicit operator ecs_world_t*(Universe w) => w.Handle;
}

@ -0,0 +1,13 @@
using gaemstone.ECS;
using Silk.NET.Maths;
namespace gaemstone;
[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;
}

@ -0,0 +1,20 @@
using System.Runtime.InteropServices;
using static flecs_hub.flecs;
namespace gaemstone;
internal static class CStringExtensions
{
public static string ToStringAndFree(this Runtime.CString str)
{
var result = Marshal.PtrToStringAnsi(str)!;
Marshal.FreeHGlobal(str);
return result;
}
public static void Set(this ref Runtime.CString str, string? value)
{
if (!str.IsNull) Marshal.FreeHGlobal(str);
str = (value != null) ? new(Marshal.StringToHGlobalAnsi(value)) : default;
}
}

@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
namespace gaemstone.Utility.IL;
public class ILGeneratorWrapper
{
private readonly DynamicMethod _method;
private readonly ILGenerator _il;
private readonly List<ILocal> _locals = new();
private readonly List<(int Offset, int Indent, OpCode Code, object? Arg)> _instructions = new();
private readonly Dictionary<Label, int> _labelToOffset = new();
private readonly Stack<BlockImpl> _indents = new();
public ILGeneratorWrapper(DynamicMethod method)
{
_method = method;
_il = method.GetILGenerator();
}
public string ToReadableString()
{
var sb = new StringBuilder();
sb.AppendLine("Parameters:");
foreach (var (param, index) in _method.GetParameters().Select((p, i) => (p, i)))
sb.AppendLine($" Argument({index}, {param.ParameterType.Name})");
sb.AppendLine("Return:");
sb.AppendLine($" {_method.ReturnType.Name}");
sb.AppendLine();
sb.AppendLine("Locals:");
foreach (var local in _locals)
sb.AppendLine($" {local}");
sb.AppendLine();
sb.AppendLine("Instructions:");
foreach (var (offset, indent, code, arg) in _instructions) {
sb.Append(" ");
// Append instruction offset.
if (offset < 0) sb.Append(" ");
else sb.Append($"0x{offset:X4} ");
// Append instruction opcode.
if (code == OpCodes.Nop) sb.Append(" ");
else sb.Append($"{code.Name,-12}");
// Append indents.
for (var i = 0; i < indent; i++)
sb.Append("| ");
// Append instruction argument.
if (code == OpCodes.Nop) sb.Append("// ");
switch (arg) {
case Label label: sb.Append($"Label(0x{_labelToOffset.GetValueOrDefault(label, -1):X4})"); break;
case not null: sb.Append(arg); break;
}
sb.AppendLine();
}
return sb.ToString();
}
public IArgument Argument(int index)
{
var type = _method.GetParameters()[index].ParameterType;
if (type.IsByRefLike) return new ArgumentImpl(index, type);
return (IArgument)Activator.CreateInstance(typeof(ArgumentImpl<>).MakeGenericType(type), index)!;
}
public IArgument<T> Argument<T>(int index) => (IArgument<T>)Argument(index);
public ILocal Local(Type type, string? name = null)
{
var builder = _il.DeclareLocal(type);
var local = type.IsByRefLike ? new LocalImpl(builder, name)
: (ILocal)Activator.CreateInstance(typeof(LocalImpl<>).MakeGenericType(type), builder, name)!;
_locals.Add(local);
return local;
}
public ILocal<T> Local<T>(string? name = null) => (ILocal<T>)Local(typeof(T), name);
public ILocal<Array> LocalArray(Type type, string? name = null) => (ILocal<Array>)Local(type.MakeArrayType(), name);
public ILocal<T[]> LocalArray<T>(string? name = null) => (ILocal<T[]>)Local(typeof(T).MakeArrayType(), name);
public Label DefineLabel() => _il.DefineLabel();
public void MarkLabel(Label label)
{
_instructions.Add((-1, _indents.Count, OpCodes.Nop, label));
_labelToOffset.Add(label, _il.ILOffset);
_il.MarkLabel(label);
}
private void AddInstr(OpCode code, object? arg = null) => _instructions.Add((_il.ILOffset, _indents.Count, code, arg));
public void Comment(string comment) => _instructions.Add((-1, _indents.Count, OpCodes.Nop, comment));
internal void Emit(OpCode code) { AddInstr(code, null); _il.Emit(code); }
internal void Emit(OpCode code, int arg) { AddInstr(code, arg); _il.Emit(code, arg); }
internal void Emit(OpCode code, Type type) { AddInstr(code, type); _il.Emit(code, type); }
internal void Emit(OpCode code, Label label) { AddInstr(code, label); _il.Emit(code, label); }
internal void Emit(OpCode code, ILocal local) { AddInstr(code, local); _il.Emit(code, local.Builder); }
internal void Emit(OpCode code, IArgument arg) { AddInstr(code, arg); _il.Emit(code, arg.Index); }
internal void Emit(OpCode code, MethodInfo method) { AddInstr(code, method); _il.Emit(code, method); }
internal void Emit(OpCode code, ConstructorInfo constr) { AddInstr(code, constr); _il.Emit(code, constr); }
public void LoadNull() => Emit(OpCodes.Ldnull);
public void LoadConst(int value) => Emit(OpCodes.Ldc_I4, value);
public void Load(IArgument arg) => Emit(OpCodes.Ldarg, arg);
public void LoadAddr(IArgument arg) => Emit(OpCodes.Ldarga, arg);
public void Load(ILocal local) => Emit(OpCodes.Ldloc, local);
public void LoadAddr(ILocal local) => Emit(OpCodes.Ldloca, local);
public void Store(ILocal local) => Emit(OpCodes.Stloc, local);
public void Set(ILocal<int> local, int value) { LoadConst(value); Store(local); }
public void LoadLength() { Emit(OpCodes.Ldlen); Emit(OpCodes.Conv_I4); }
public void LoadLength(IArgument<Array> array) { Load(array); LoadLength(); }
public void LoadLength(ILocal<Array> array) { Load(array); LoadLength(); }
public void LoadObj(Type type) => Emit(OpCodes.Ldobj, type);
public void LoadObj(ILocal local) { LoadAddr(local); LoadObj(local.LocalType); }
public void LoadObj(IArgument arg) { LoadAddr(arg); LoadObj(arg.ArgumentType); }
public void LoadElem(Type type) => Emit(OpCodes.Ldelem, type);
public void LoadElem(Type type, int index) { LoadConst(index); LoadElem(type); }
public void LoadElem(Type type, ILocal<int> index) { Load(index); LoadElem(type); }
public void LoadElem(Type type, IArgument<Array> array, int index) { Load(array); LoadElem(type, index); }
public void LoadElem(Type type, IArgument<Array> array, ILocal<int> index) { Load(array); LoadElem(type, index); }
public void LoadElem(Type type, ILocal<Array> array, int index) { Load(array); LoadElem(type, index); }
public void LoadElem(Type type, ILocal<Array> array, ILocal<int> index) { Load(array); LoadElem(type, index); }
public void LoadElemRef() => Emit(OpCodes.Ldelem_Ref);
public void LoadElemRef(int index) { LoadConst(index); LoadElemRef(); }
public void LoadElemRef(ILocal<int> index) { Load(index); LoadElemRef(); }
public void LoadElemRef(IArgument<Array> array, int index) { Load(array); LoadElemRef(index); }
public void LoadElemRef(IArgument<Array> array, ILocal<int> index) { Load(array); LoadElemRef(index); }
public void LoadElemRef(ILocal<Array> array, int index) { Load(array); LoadElemRef(index); }
public void LoadElemRef(ILocal<Array> array, ILocal<int> index) { Load(array); LoadElemRef(index); }
public void LoadElemEither(Type type) { if (type.IsValueType) LoadElem(type); else LoadElemRef(); }
public void LoadElemEither(Type type, int index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); }
public void LoadElemEither(Type type, ILocal<int> index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); }
public void LoadElemEither(Type type, IArgument<Array> array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); }
public void LoadElemEither(Type type, IArgument<Array> array, ILocal<int> index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); }
public void LoadElemEither(Type type, ILocal<Array> array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); }
public void LoadElemEither(Type type, ILocal<Array> array, ILocal<int> index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); }
public void LoadElemAddr(Type type) => Emit(OpCodes.Ldelema, type);
public void LoadElemAddr(Type type, int index) { LoadConst(index); LoadElemAddr(type); }
public void LoadElemAddr(Type type, ILocal<int> index) { Load(index); LoadElemAddr(type); }
public void LoadElemAddr(Type type, IArgument<Array> array, int index) { Load(array); LoadElemAddr(type, index); }
public void LoadElemAddr(Type type, IArgument<Array> array, ILocal<int> index) { Load(array); LoadElemAddr(type, index); }
public void LoadElemAddr(Type type, ILocal<Array> array, int index) { Load(array); LoadElemAddr(type, index); }
public void LoadElemAddr(Type type, ILocal<Array> array, ILocal<int> index) { Load(array); LoadElemAddr(type, index); }
public void Load(PropertyInfo info) => CallVirt(info.GetMethod!);
public void Load(ILocal obj, PropertyInfo info) { Load(obj); Load(info); }
public void Load(IArgument obj, PropertyInfo info) { Load(obj); Load(info); }
public void Add() => Emit(OpCodes.Add);
public void Increment(ILocal<int> local) { Load(local); LoadConst(1); Add(); Store(local); }
public void Init(Type type) => Emit(OpCodes.Initobj, type);
public void Init<T>() where T : struct => Emit(OpCodes.Initobj, typeof(T));
public void New(ConstructorInfo constructor) => Emit(OpCodes.Newobj, constructor);
public void New(Type type) => New(type.GetConstructors().Single());
public void New(Type type, params Type[] paramTypes) => New(type.GetConstructor(paramTypes)!);
public void Cast(Type type) => Emit(OpCodes.Castclass, type);
public void Cast<T>() => Cast(typeof(T));
public void Goto(Label label) => Emit(OpCodes.Br, label);
public void GotoIfTrue(Label label) => Emit(OpCodes.Brtrue, label);
public void GotoIfFalse(Label label) => Emit(OpCodes.Brfalse, label);
public void GotoIf(Label label, ILocal<int> a, Comparison op, ILocal<int> b)
{ Load(a); Load(b); Emit(op.Code, label); }
public void GotoIfNull(Label label, ILocal<object> local)
{ Load(local); Emit(OpCodes.Brfalse, label); }
public void GotoIfNotNull(Label label, ILocal<object> local)
{ Load(local); Emit(OpCodes.Brtrue, label); }
public void Call(MethodInfo method) => Emit(OpCodes.Call, method);
public void CallVirt(MethodInfo method) => Emit(OpCodes.Callvirt, method);
public void Return() => Emit(OpCodes.Ret);
public IDisposable For(Action loadMax, out ILocal<int> current)
{
var r = Random.Shared.Next(10000, 100000);
Comment($"INIT for loop {r}");
var curLocal = current = Local<int>($"index_{r}");
var maxLocal = Local<int>($"length_{r}");
var bodyLabel = DefineLabel();
var testLabel = DefineLabel();
Set(curLocal, 0);
loadMax(); Store(maxLocal);
Comment($"BEGIN for loop {r}");
Goto(testLabel);
MarkLabel(bodyLabel);
var indent = Indent();
return Block(() => {
Increment(curLocal);
MarkLabel(testLabel);
GotoIf(bodyLabel, curLocal, Comparison.LessThan, maxLocal);
indent.Dispose();
Comment($"END for loop {r}");
});
}
public IDisposable Block(Action onClose)
=> new BlockImpl(onClose);
public IDisposable Indent()
{
BlockImpl indent = null!;
indent = new(() => { if (_indents.Pop() != indent) throw new InvalidOperationException(); });
_indents.Push(indent);
return indent;
}
internal class BlockImpl : IDisposable
{
public Action OnClose { get; }
public BlockImpl(Action onClose) => OnClose = onClose;
public void Dispose() => OnClose();
}
internal class ArgumentImpl : IArgument
{
public int Index { get; }
public Type ArgumentType { get; }
public ArgumentImpl(int index, Type type) { Index = index; ArgumentType = type; }
public override string ToString() => $"Argument({Index}, {ArgumentType.Name})";
}
internal class ArgumentImpl<T> : ArgumentImpl, IArgument<T>
{ public ArgumentImpl(int index) : base(index, typeof(T)) { } }
internal class LocalImpl : ILocal
{
public LocalBuilder Builder { get; }
public string? Name { get; }
public Type LocalType => Builder.LocalType;
public LocalImpl(LocalBuilder builder, string? name) { Builder = builder; Name = name; }
public override string ToString() => $"Local({Builder.LocalIndex}, {LocalType.Name}){(Name != null ? $" // {Name}" : "")}";
}
internal class LocalImpl<T> : LocalImpl, ILocal<T>
{ public LocalImpl(LocalBuilder builder, string? name) : base(builder, name) { } }
}
public class Comparison
{
public static Comparison NotEqual { get; } = new(OpCodes.Bne_Un);
public static Comparison LessThan { get; } = new(OpCodes.Blt);
public static Comparison LessOrEq { get; } = new(OpCodes.Ble);
public static Comparison Equal { get; } = new(OpCodes.Beq);
public static Comparison GreaterOrEq { get; } = new(OpCodes.Bge);
public static Comparison GreaterThan { get; } = new(OpCodes.Bgt);
public OpCode Code { get; }
private Comparison(OpCode code) => Code = code;
}
public interface IArgument
{
int Index { get; }
Type ArgumentType { get; }
}
public interface IArgument<out T>
: IArgument { }
public interface ILocal
{
LocalBuilder Builder { get; }
Type LocalType { get; }
}
public interface ILocal<out T>
: ILocal { }

@ -0,0 +1,327 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using gaemstone.ECS;
using static flecs_hub.flecs;
namespace gaemstone.Utility.IL;
public unsafe class QueryActionGenerator
{
private static readonly PropertyInfo _iteratorUniverseProp = typeof(Iterator).GetProperty(nameof(Iterator.Universe))!;
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 _iteratorFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field))!;
private static readonly MethodInfo _iteratorFieldIsSetMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldIsSet))!;
private static readonly MethodInfo _iteratorEntityMethod = typeof(Iterator).GetMethod(nameof(Iterator.Entity))!;
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, QueryActionGenerator> _cache = new();
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>, ILocal<int>>> _uniqueParameters = new() {
[typeof(Iterator)] = (IL, iter, i) => { IL.Load(iter); },
[typeof(Universe)] = (IL, iter, i) => { IL.Load(iter, _iteratorUniverseProp); },
[typeof(TimeSpan)] = (IL, iter, i) => { IL.Load(iter, _iteratorDeltaTimeProp); },
[typeof(Entity)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iteratorEntityMethod); },
};
public Universe Universe { get; }
public MethodInfo Method { get; }
public ParamInfo[] Parameters { get; }
public ecs_filter_desc_t Filter { get; }
public Action<object?, Iterator> GeneratedAction { get; }
public string ReadableString { get; }
public void RunWithTryCatch(object? instance, Iterator iter)
{
try { GeneratedAction(instance, iter); } catch {
Console.WriteLine("Exception occured while running:");
Console.WriteLine(" " + Method);
Console.WriteLine();
Console.WriteLine("Method's IL code:");
Console.WriteLine(ReadableString);
Console.WriteLine();
throw;
}
}
public QueryActionGenerator(Universe universe, MethodInfo method)
{
Universe = universe;
Method = method;
Parameters = method.GetParameters().Select(ParamInfo.Build).ToArray();
if (!Parameters.Any(c => c.IsRequired && (c.Kind != ParamKind.Unique)))
throw new ArgumentException($"At least one parameter in {method} is required");
var filter = default(ecs_filter_desc_t);
var name = "<>Query_" + string.Join("_", Parameters.Select(p => p.UnderlyingType.Name));
var genMethod = new DynamicMethod(name, null, new[] { typeof(object), typeof(Iterator) });
var IL = new ILGeneratorWrapper(genMethod);
var instanceArg = IL.Argument<object?>(0);
var iteratorArg = IL.Argument<Iterator>(1);
var counter = 0; // Counter for fields actually part of the filter terms.
var fieldLocals = new ILocal[Parameters.Length];
var tempLocals = new ILocal[Parameters.Length];
for (var i = 0; i < Parameters.Length; i++) {
var p = Parameters[i];
if (p.Kind == ParamKind.Unique) continue;
// Update the flecs filter to look for this type.
// Term index is 0-based and field index (used below) is 1-based, so increasing counter here works out.
ref var term = ref filter.terms[counter++];
term.id = Universe.Lookup(p.UnderlyingType);
term.inout = p.Kind switch {
ParamKind.In => ecs_inout_kind_t.EcsIn,
ParamKind.Out => ecs_inout_kind_t.EcsOut,
ParamKind.Has or ParamKind.Not => ecs_inout_kind_t.EcsInOutNone,
_ => ecs_inout_kind_t.EcsInOut,
};
term.oper = p.Kind switch {
ParamKind.Not => ecs_oper_kind_t.EcsNot,
_ when !p.IsRequired => ecs_oper_kind_t.EcsOptional,
_ => ecs_oper_kind_t.EcsAnd,
};
if (p.Source != null) term.src = new() { id = Universe.Lookup(p.Source) };
// Create a Span<T> local and initialize it to iterator.Field<T>(i).
var spanType = typeof(Span<>).MakeGenericType(p.FieldType);
fieldLocals[i] = IL.Local(spanType, $"field_{counter}");
if (p.Kind is ParamKind.Has or ParamKind.Not) {
// If a "has" or "not" parameter is a struct, we require a temporary local that
// we can later load onto the stack when loading the arguments for the action.
if (p.ParameterType.IsValueType) {
IL.Comment($"temp_{counter} = default({p.ParameterType});");
tempLocals[i] = IL.Local(p.ParameterType);
IL.LoadAddr(tempLocals[i]);
IL.Init(tempLocals[i].LocalType);
}
} else if (p.IsRequired) {
IL.Comment($"field_{counter} = iterator.Field<{p.FieldType.Name}>({counter})");
IL.Load(iteratorArg);
IL.LoadConst(counter);
IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocals[i]);
} else {
IL.Comment($"field_{counter} = iterator.FieldIsSet({counter}) ? iterator.Field<{p.FieldType.Name}>({counter}) : default");
var elseLabel = IL.DefineLabel();
var doneLabel = IL.DefineLabel();
IL.Load(iteratorArg);
IL.LoadConst(counter);
IL.Call(_iteratorFieldIsSetMethod);
IL.GotoIfFalse(elseLabel);
IL.Load(iteratorArg);
IL.LoadConst(counter);
IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocals[i]);
IL.Goto(doneLabel);
IL.MarkLabel(elseLabel);
IL.LoadAddr(fieldLocals[i]);
IL.Init(spanType);
IL.MarkLabel(doneLabel);
}
if (p.Kind == ParamKind.Nullable) {
IL.Comment($"temp_{counter} = default({p.ParameterType});");
tempLocals[i] = IL.Local(p.ParameterType);
IL.LoadAddr(tempLocals[i]);
IL.Init(tempLocals[i].LocalType);
}
}
// If there's any reference type parameters, we need to define a GCHandle local.
var hasReferenceType = Parameters
.Where(p => p.Kind != ParamKind.Unique)
.Any(p => !p.UnderlyingType.IsValueType);
var handleLocal = hasReferenceType ? IL.Local<GCHandle>() : null;
using (IL.For(() => IL.Load(iteratorArg, _iteratorCountProp), out var currentLocal)) {
if (!Method.IsStatic)
IL.Load(instanceArg);
for (var i = 0; i < Parameters.Length; i++) {
var p = Parameters[i];
if (p.Kind == ParamKind.Unique) {
IL.Comment($"Unique parameter {p.ParameterType}");
_uniqueParameters[p.ParameterType](IL, iteratorArg, currentLocal);
} else if (p.Kind is ParamKind.Has or ParamKind.Not) {
if (p.ParameterType.IsValueType)
IL.LoadObj(tempLocals[i]!);
else IL.LoadNull();
} else {
var spanType = typeof(Span<>).MakeGenericType(p.FieldType);
var spanItemMethod = spanType.GetProperty("Item")!.GetMethod!;
var spanLengthMethod = spanType.GetProperty("Length")!.GetMethod!;
IL.Comment($"Parameter {p.ParameterType}");
if (p.IsByRef) {
IL.LoadAddr(fieldLocals[i]!);
IL.Load(currentLocal);
IL.Call(spanItemMethod);
} else if (p.IsRequired) {
IL.LoadAddr(fieldLocals[i]!);
IL.Load(currentLocal);
IL.Call(spanItemMethod);
IL.LoadObj(p.FieldType);
} else {
var elseLabel = IL.DefineLabel();
var doneLabel = IL.DefineLabel();
IL.LoadAddr(fieldLocals[i]!);
IL.Call(spanLengthMethod);
IL.GotoIfFalse(elseLabel);
IL.LoadAddr(fieldLocals[i]!);
IL.Load(currentLocal);
IL.Call(spanItemMethod);
IL.LoadObj(p.FieldType);
if (p.Kind == ParamKind.Nullable)
IL.New(p.ParameterType);
IL.Goto(doneLabel);
IL.MarkLabel(elseLabel);
if (p.Kind == ParamKind.Nullable)
IL.LoadObj(tempLocals[i]!);
else IL.LoadNull();
IL.MarkLabel(doneLabel);
}
if (!p.UnderlyingType.IsValueType) {
IL.Comment($"Convert nint to {p.UnderlyingType}");
IL.Call(_handleFromIntPtrMethod);
IL.Store(handleLocal!);
IL.LoadAddr(handleLocal!);
IL.Call(_handleTargetProp.GetMethod!);
IL.Cast(p.UnderlyingType);
}
}
}
IL.Call(Method);
}
IL.Return();
Filter = filter;
GeneratedAction = genMethod.CreateDelegate<Action<object?, Iterator>>();
ReadableString = IL.ToReadableString();
}
public static QueryActionGenerator GetOrBuild(Universe universe, MethodInfo method)
=>_cache.GetValue(method, m => new QueryActionGenerator(universe, m));
public class ParamInfo
{
public ParameterInfo Info { get; }
public int Index { get; }
public ParamKind Kind { get; }
public Type ParameterType { get; }
public Type UnderlyingType { get; }
public Type FieldType { get; }
public Type? Source { get; }
public bool IsRequired => (Kind < ParamKind.Nullable);
public bool IsByRef => (Kind >= ParamKind.In) && (Kind <= ParamKind.Ref);
private ParamInfo(
ParameterInfo info, int index, ParamKind kind,
Type paramType, Type underlyingType)
{
Info = info;
Index = index;
Kind = kind;
ParameterType = paramType;
UnderlyingType = underlyingType;
// Reference types have a backing type of nint - they're pointers.
FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint);
// If the underlying type has EntityAttribute, it's a singleton.
if (UnderlyingType.Has<EntityAttribute>()) Source = underlyingType;
if (Info.Get<SourceAttribute>() is SourceAttribute attr) Source = attr.Type;
}
public static ParamInfo Build(ParameterInfo info, int index)
{
if (info.IsOptional) throw new ArgumentException("Optional parameters are not supported\nParameter: " + info);
if (info.ParameterType.IsArray) throw new ArgumentException("Arrays are not supported\nParameter: " + info);
if (info.ParameterType.IsPointer) throw new ArgumentException("Pointers are not supported\nParameter: " + info);
if (_uniqueParameters.ContainsKey(info.ParameterType))
return new(info, index, ParamKind.Unique, info.ParameterType, info.ParameterType);
var isByRef = info.ParameterType.IsByRef;
var isNullable = info.IsNullable();
if (info.Has<NotAttribute>()) {
if (isByRef || isNullable) throw new ArgumentException(
"Parameter with NotAttribute must not be ByRef or nullable\nParameter: " + info);
return new(info, index, ParamKind.Not, info.ParameterType, info.ParameterType);
}
if (info.Has<HasAttribute>() || info.ParameterType.Has<TagAttribute>()) {
if (isByRef || isNullable) throw new ArgumentException(
"Parameter with HasAttribute / TagAttribute must not be ByRef or nullable\nParameter: " + info);
return new(info, index, ParamKind.Has, info.ParameterType, info.ParameterType);
}
var kind = ParamKind.Normal;
var underlyingType = info.ParameterType;
if (info.IsNullable()) {
if (info.ParameterType.IsValueType)
underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!;
kind = ParamKind.Nullable;
}
if (info.ParameterType.IsByRef) {
if (kind == ParamKind.Nullable) throw new ArgumentException(
"ByRef and Nullable are not supported together\nParameter: " + info);
underlyingType = info.ParameterType.GetElementType()!;
if (!underlyingType.IsValueType) throw new ArgumentException(
"Reference types can't also be ByRef\nParameter: " + info);
kind = info.IsIn ? ParamKind.In
: info.IsOut ? ParamKind.Out
: ParamKind.Ref;
}
if (underlyingType.IsPrimitive) throw new ArgumentException(
"Primitives are not supported\nParameter: " + info);
return new(info, index, kind, info.ParameterType, underlyingType);
}
}
public enum ParamKind
{
/// <summary> Parameter is not part of terms, handled uniquely, such as Universe and Entity. </summary>
Unique,
/// <summary> Passed by value. </summary>
Normal,
/// <summary> Struct passed with the "in" modifier. </summary>
In,
/// <summary> Struct passed with the "out" modifier. </summary>
Out,
/// <summary> Struct passed with the "ref" modifier. </summary>
Ref,
/// <summary>
/// Only checks for presence.
/// Manually applied with <see cref="HasAttribute"/>.
/// Automatically applied for types with <see cref="TagAttribute"/>.
/// </summary>
Has,
/// <summary> Struct passed as <c>Nullable&lt;T&gt;</c>. </summary>
Nullable,
/// <summary>
/// Only checks for absence.
/// Applied with <see cref="NotAttribute"/>.
/// </summary>
Not,
}
}

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
namespace gaemstone.Utility;
public static class RandomExtensions
{
public static bool NextBool(this Random rnd, double chance)
=> rnd.NextDouble() < chance;
public static double NextDouble(this Random rnd, double max)
=> rnd.NextDouble() * max;
public static double NextDouble(this Random rnd, double min, double max)
=> min + rnd.NextDouble() * (max - min);
public static float NextFloat(this Random rnd)
=> (float)rnd.NextDouble();
public static float NextFloat(this Random rnd, float max)
=> (float)rnd.NextDouble() * max;
public static float NextFloat(this Random rnd, float min, float max)
=> min + (float)rnd.NextDouble() * (max - min);
public static T Pick<T>(this Random rnd, params T[] elements)
=> elements[rnd.Next(elements.Length)];
public static T Pick<T>(this Random rnd, IReadOnlyList<T> elements)
=> elements[rnd.Next(elements.Count)];
public static T Pick<T>(this Random rnd, Span<T> elements)
=> elements[rnd.Next(elements.Length)];
#pragma warning disable CS8509 // Switch expression is not exhaustive.
public static T Pick<T>(this Random rnd, T elem1, T elem2)
=> rnd.Next(2) switch { 0 => elem1, 1 => elem2 };
public static T Pick<T>(this Random rnd, T elem1, T elem2, T elem3)
=> rnd.Next(3) switch { 0 => elem1, 1 => elem2, 2 => elem3 };
public static T Pick<T>(this Random rnd, T elem1, T elem2, T elem3, T elem4)
=> rnd.Next(4) switch { 0 => elem1, 1 => elem2, 2 => elem3, 3 => elem4 };
#pragma warning restore CS8509
}

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Text;
namespace gaemstone.Utility;
public static class ReflectionExtensions
{
public static string GetFriendlyName(this Type type)
{
if (!type.IsGenericType) return type.Name;
var name = type.Name;
var sb = new StringBuilder(name[..name.IndexOf('`')]);
sb.Append('<');
sb.AppendJoin(",", type.GenericTypeArguments.Select(GetFriendlyName));
sb.Append('>');
return sb.ToString();
}
public static T? Get<T>(this MemberInfo member)
where T : Attribute => member.GetCustomAttribute<T>();
public static IEnumerable<T> GetMultiple<T>(this MemberInfo member)
where T : Attribute => member.GetCustomAttributes<T>();
public static bool Has<T>(this MemberInfo member)
where T : Attribute => member.GetCustomAttribute<T>() != null;
public static T? Get<T>(this ParameterInfo member)
where T : Attribute => member.GetCustomAttribute<T>();
public static IEnumerable<T> GetMultiple<T>(this ParameterInfo member)
where T : Attribute => member.GetCustomAttributes<T>();
public static bool Has<T>(this ParameterInfo member)
where T : Attribute => member.GetCustomAttribute<T>() != null;
public static bool IsNullable(this PropertyInfo property) =>
IsNullable(property.PropertyType, property.DeclaringType, property.CustomAttributes);
public static bool IsNullable(this FieldInfo field) =>
IsNullable(field.FieldType, field.DeclaringType, field.CustomAttributes);
public static bool IsNullable(this ParameterInfo parameter) =>
IsNullable(parameter.ParameterType, parameter.Member, parameter.CustomAttributes);
// https://stackoverflow.com/a/58454489
static bool IsNullable(Type memberType, MemberInfo? declaringType, IEnumerable<CustomAttributeData> customAttributes)
{
if (memberType.IsValueType) return (Nullable.GetUnderlyingType(memberType) != null);
var nullable = customAttributes.FirstOrDefault(
x => (x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute"));
if ((nullable != null) && (nullable.ConstructorArguments.Count == 1)) {
var attributeArgument = nullable.ConstructorArguments[0];
if (attributeArgument.ArgumentType == typeof(byte[])) {
var args = (ReadOnlyCollection<CustomAttributeTypedArgument>)attributeArgument.Value!;
if ((args.Count > 0) && (args[0].ArgumentType == typeof(byte)))
return (byte)args[0].Value! == 2;
} else if (attributeArgument.ArgumentType == typeof(byte))
return (byte)attributeArgument.Value! == 2;
}
for (var type = declaringType; type != null; type = type.DeclaringType) {
var context = type.CustomAttributes.FirstOrDefault(
x => (x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute"));
if ((context != null) && (context.ConstructorArguments.Count == 1) &&
(context.ConstructorArguments[0].ArgumentType == typeof(byte)))
return (byte)context.ConstructorArguments[0].Value! == 2;
}
// Couldn't find a suitable attribute.
return false;
}
}

@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
namespace gaemstone.Utility;
public interface ITypeWrapper
{
Type Type { get; }
int Size { get; }
bool IsUnmanaged { get; }
IFieldWrapper GetFieldForAutoProperty(string propertyName);
IFieldWrapper GetFieldForAutoProperty(PropertyInfo property);
IFieldWrapper GetField(string fieldName);
IFieldWrapper GetField(FieldInfo field);
}
public interface IFieldWrapper
{
ITypeWrapper DeclaringType { get; }
FieldInfo FieldInfo { get; }
PropertyInfo? PropertyInfo { get; }
Func<object, object?> ClassGetter { get; }
Action<object, object?> ClassSetter { get; }
}
public static class TypeWrapper
{
static readonly Dictionary<Type, ITypeWrapper> _typeCache = new();
public static TypeWrapper<T> For<T>()
=> TypeWrapper<T>.Instance;
public static ITypeWrapper For(Type type)
{
if (!_typeCache.TryGetValue(type, out var wrapper))
_typeCache.Add(type, wrapper = (ITypeWrapper)typeof(TypeWrapper<>)
.MakeGenericType(type).GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic)!
.GetMethod!.Invoke(null, Array.Empty<object>())!);
return wrapper;
}
}
public class TypeWrapper<TType> : ITypeWrapper
{
internal static TypeWrapper<TType> Instance { get; } = new();
readonly Dictionary<FieldInfo, IFieldWrapperForType> _fieldCache = new();
public Type Type => typeof(TType);
public int Size { get; } = Unsafe.SizeOf<TType>();
public bool IsUnmanaged { get; } = !RuntimeHelpers.IsReferenceOrContainsReferences<TType>();
TypeWrapper() { }
IFieldWrapper ITypeWrapper.GetFieldForAutoProperty(string propertyName) => GetFieldForAutoProperty(propertyName);
IFieldWrapper ITypeWrapper.GetFieldForAutoProperty(PropertyInfo property) => GetFieldForAutoProperty(property);
IFieldWrapper ITypeWrapper.GetField(string fieldName) => GetField(fieldName);
IFieldWrapper ITypeWrapper.GetField(FieldInfo field) => GetField(field);
public IFieldWrapperForType GetFieldForAutoProperty(string propertyName)
{
var property = Type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (property == null) throw new MissingMemberException(Type.FullName, propertyName);
var field = Type.GetField($"<{property.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null) throw new ArgumentException($"Could not find backing field for property {property}");
return GetField(field, property);
}
public IFieldWrapperForType GetFieldForAutoProperty(PropertyInfo property)
{
if (property.DeclaringType != Type) throw new ArgumentException(
$"Specified PropertyInfo {property} needs to be a member of type {Type}", nameof(property));
var field = Type.GetField($"<{property.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null) throw new ArgumentException($"Could not find backing field for property {property}");
return GetField(field, property);
}
public IFieldWrapperForType GetField(string fieldName)
{
var field = Type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field == null) throw new MissingFieldException(Type.FullName, fieldName);
return GetField(field, null);
}
public IFieldWrapperForType GetField(FieldInfo field)
{
if (field.DeclaringType != Type) throw new ArgumentException(
$"Specified FieldInfo {field} needs to be a member of type {Type}", nameof(field));
return GetField(field, null);
}
public FieldWrapper<TField> GetFieldForAutoProperty<TField>(string propertyName)
{
var property = Type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (property == null) throw new MissingMemberException(Type.FullName, propertyName);
var field = Type.GetField($"<{property.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null) throw new ArgumentException($"Could not find backing field for property {property}");
return GetField<TField>(field, property);
}
public FieldWrapper<TField> GetFieldForAutoProperty<TField>(PropertyInfo property)
{
if (property.DeclaringType != Type) throw new ArgumentException(
$"Specified PropertyInfo {property} needs to be a member of type {Type}", nameof(property));
var field = Type.GetField($"<{property.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null) throw new ArgumentException($"Could not find backing field for property {property}");
return GetField<TField>(field, property);
}
public FieldWrapper<TField> GetField<TField>(string fieldName)
{
var field = Type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field == null) throw new MissingFieldException(Type.FullName, fieldName);
return GetField<TField>(field, null);
}
public FieldWrapper<TField> GetField<TField>(FieldInfo field)
{
if (field.DeclaringType != Type) throw new ArgumentException(
$"Specified FieldInfo {field} needs to be a member of type {Type}", nameof(field));
return GetField<TField>(field, null);
}
IFieldWrapperForType GetField(FieldInfo field, PropertyInfo? property)
{
if (_fieldCache.TryGetValue(field, out var cached)) return cached;
var type = typeof(FieldWrapper<>).MakeGenericType(typeof(TType), field.FieldType);
var wrapper = (IFieldWrapperForType)Activator.CreateInstance(
type, BindingFlags.Instance | BindingFlags.NonPublic,
null, new object?[] { this, field, property }, null)!;
_fieldCache.Add(field, wrapper);
return wrapper;
}
FieldWrapper<TField> GetField<TField>(FieldInfo field, PropertyInfo? property)
{
if (_fieldCache.TryGetValue(field, out var cached)) return (FieldWrapper<TField>)cached;
if (field.FieldType != typeof(TField)) throw new ArgumentException(
$"FieldType ({field.FieldType}) does not match TField ({typeof(TField)})", nameof(TField));
var wrapper = new FieldWrapper<TField>(this, field, property);
_fieldCache.Add(field, wrapper);
return wrapper;
}
public interface IFieldWrapperForType : IFieldWrapper
{
delegate object? ValueGetterAction(in TType obj);
delegate void ValueSetterAction(ref TType obj, object? value);
Func<object, object?> IFieldWrapper.ClassGetter => (obj) => ClassGetter((TType)obj);
Action<object, object?> IFieldWrapper.ClassSetter => (obj, value) => ClassSetter((TType)obj, value);
new Func<TType, object?> ClassGetter { get; }
new Action<TType, object?> ClassSetter { get; }
ValueGetterAction ByRefGetter { get; }
ValueSetterAction ByRefSetter { get; }
}
public class FieldWrapper<TField> : IFieldWrapperForType
{
public delegate TField ValueGetterAction(in TType obj);
public delegate void ValueSetterAction(ref TType obj, TField value);
Func<TType, TField>? _classGetter;
Action<TType, TField>? _classSetter;
ValueGetterAction? _byRefGetter;
ValueSetterAction? _byRefSetter;
public ITypeWrapper DeclaringType { get; }
public FieldInfo FieldInfo { get; }
public PropertyInfo? PropertyInfo { get; }
internal FieldWrapper(ITypeWrapper type, FieldInfo field, PropertyInfo? property)
{ DeclaringType = type; FieldInfo = field; PropertyInfo = property; }
Func<TType, object?> IFieldWrapperForType.ClassGetter => (obj) => ClassGetter(obj);
Action<TType, object?> IFieldWrapperForType.ClassSetter => (obj, value) => ClassSetter(obj, (TField)value!);
public Func<TType, TField> ClassGetter => _classGetter ??= BuildGetter<Func<TType, TField>>(false);
public Action<TType, TField> ClassSetter => _classSetter ??= BuildSetter<Action<TType, TField>>(false);
IFieldWrapperForType.ValueGetterAction IFieldWrapperForType.ByRefGetter => (in TType obj) => ByRefGetter(in obj);
IFieldWrapperForType.ValueSetterAction IFieldWrapperForType.ByRefSetter => (ref TType obj, object? value) => ByRefSetter(ref obj, (TField)value!);
public ValueGetterAction ByRefGetter => _byRefGetter ??= BuildGetter<ValueGetterAction>(true);
public ValueSetterAction ByRefSetter => _byRefSetter ??= BuildSetter<ValueSetterAction>(true);
TDelegate BuildGetter<TDelegate>(bool byRef)
where TDelegate : Delegate
{
if (DeclaringType.Type.IsValueType && !byRef) throw new InvalidOperationException(
$"Can't build getter for value type ({DeclaringType.Type}) without using ref");
var method = new DynamicMethod(
$"Get_{DeclaringType.Type.Name}_{FieldInfo.Name}{(byRef ? "_ByRef" : "")}",
typeof(TField), new[] { byRef ? typeof(TType).MakeByRefType() : typeof(TType) },
typeof(TType).Module, true);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
if (byRef && !DeclaringType.Type.IsValueType)
il.Emit(OpCodes.Ldind_Ref);
il.Emit(OpCodes.Ldfld, FieldInfo);
il.Emit(OpCodes.Ret);
return method.CreateDelegate<TDelegate>();
}
TDelegate BuildSetter<TDelegate>(bool byRef)
where TDelegate : Delegate
{
if (DeclaringType.Type.IsValueType && !byRef) throw new InvalidOperationException(
$"Can't build setter for value type ({DeclaringType.Type}) without using ref");
var method = new DynamicMethod(
$"Set_{DeclaringType.Type.Name}_{FieldInfo.Name}{(byRef ? "_ByRef" : "")}",
null, new[] { byRef ? typeof(TType).MakeByRefType() : typeof(TType), typeof(TField) },
typeof(TType).Module, true);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
if (byRef && !DeclaringType.Type.IsValueType)
il.Emit(OpCodes.Ldind_Ref);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stfld, FieldInfo);
il.Emit(OpCodes.Ret);
return method.CreateDelegate<TDelegate>();
}
}
}

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../flecs-cs/src/cs/production/Flecs/Flecs.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Silk.NET.Maths" Version="2.16.0" />
</ItemGroup>
</Project>
Loading…
Cancel
Save