commit fae12b7963fe6e5347f0fc531e1497d3d05c4c6d Author: copygirl Date: Fri Sep 16 22:06:44 2022 +0200 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..dc74b1f --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f26c96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/obj/ +**/bin/ +/artifacts/ +/packages/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8a9c26f --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4327687 --- /dev/null +++ b/.vscode/launch.json @@ -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 + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bfa239e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bin": true, + "**/obj": true, + }, +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..5adaf07 --- /dev/null +++ b/.vscode/tasks.json @@ -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" + } + ] +} diff --git a/gaemstone.sln b/gaemstone.sln new file mode 100644 index 0000000..ff837b3 --- /dev/null +++ b/gaemstone.sln @@ -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 diff --git a/src/FastNoiseLite b/src/FastNoiseLite new file mode 160000 index 0000000..5923df5 --- /dev/null +++ b/src/FastNoiseLite @@ -0,0 +1 @@ +Subproject commit 5923df5d822f7610100d0e77f629c607ed64934a diff --git a/src/Immersion/Immersion.csproj b/src/Immersion/Immersion.csproj new file mode 100644 index 0000000..95950e3 --- /dev/null +++ b/src/Immersion/Immersion.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + disable + enable + + + + + + + + + + + + + + + + diff --git a/src/Immersion/Program.cs b/src/Immersion/Program.cs new file mode 100644 index 0000000..e6615e2 --- /dev/null +++ b/src/Immersion/Program.cs @@ -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(); +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(); + +game.Set(new Canvas(window.CreateOpenGL())); +game.Set(new GameWindow(window)); + +TextureManager.Initialize(universe); + +universe.RegisterComponent(); +universe.RegisterComponent(); +universe.RegisterComponent(); + +universe.RegisterModule(); +universe.RegisterModule(); +universe.RegisterModule(); + +game.Set(new RawInput()); + +// TODO: Find a way to automatically register this chunk storage. +universe.RegisterComponent>(); +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(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(); diff --git a/src/Immersion/Resources/LICENSE_NOTICES.md b/src/Immersion/Resources/LICENSE_NOTICES.md new file mode 100644 index 0000000..d202a84 --- /dev/null +++ b/src/Immersion/Resources/LICENSE_NOTICES.md @@ -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/ diff --git a/src/Immersion/Resources/default.fs.glsl b/src/Immersion/Resources/default.fs.glsl new file mode 100644 index 0000000..0f5aa8e --- /dev/null +++ b/src/Immersion/Resources/default.fs.glsl @@ -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); +} diff --git a/src/Immersion/Resources/default.vs.glsl b/src/Immersion/Resources/default.vs.glsl new file mode 100644 index 0000000..595f341 --- /dev/null +++ b/src/Immersion/Resources/default.vs.glsl @@ -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; +} diff --git a/src/Immersion/Resources/heart.blend b/src/Immersion/Resources/heart.blend new file mode 100644 index 0000000..a95b440 Binary files /dev/null and b/src/Immersion/Resources/heart.blend differ diff --git a/src/Immersion/Resources/heart.glb b/src/Immersion/Resources/heart.glb new file mode 100644 index 0000000..70e6912 Binary files /dev/null and b/src/Immersion/Resources/heart.glb differ diff --git a/src/Immersion/Resources/sword.blend b/src/Immersion/Resources/sword.blend new file mode 100644 index 0000000..ccf803b Binary files /dev/null and b/src/Immersion/Resources/sword.blend differ diff --git a/src/Immersion/Resources/sword.glb b/src/Immersion/Resources/sword.glb new file mode 100644 index 0000000..320f76b Binary files /dev/null and b/src/Immersion/Resources/sword.glb differ diff --git a/src/Immersion/Resources/terrain.png b/src/Immersion/Resources/terrain.png new file mode 100644 index 0000000..ec63444 Binary files /dev/null and b/src/Immersion/Resources/terrain.png differ diff --git a/src/flecs-cs b/src/flecs-cs new file mode 160000 index 0000000..1e36559 --- /dev/null +++ b/src/flecs-cs @@ -0,0 +1 @@ +Subproject commit 1e36559cffa5ab2fb755feef563c4294a6f32b0c diff --git a/src/gaemstone.Bloxel/BlockFacing.cs b/src/gaemstone.Bloxel/BlockFacing.cs new file mode 100644 index 0000000..6820400 --- /dev/null +++ b/src/gaemstone.Bloxel/BlockFacing.cs @@ -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 Horizontals + = ImmutableHashSet.Create(BlockFacing.East , BlockFacing.West , + BlockFacing.South, BlockFacing.North); + + public static readonly ImmutableHashSet Verticals + = ImmutableHashSet.Create(BlockFacing.Up, BlockFacing.Down); + + public static readonly ImmutableHashSet 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 ToVector3(this BlockFacing self) + => self switch { + BlockFacing.East => Vector3D.UnitX, + BlockFacing.West => -Vector3D.UnitX, + BlockFacing.Up => Vector3D.UnitY, + BlockFacing.Down => -Vector3D.UnitY, + BlockFacing.South => Vector3D.UnitZ, + BlockFacing.North => -Vector3D.UnitZ, + _ => throw new ArgumentException( + $"'{self}' is not a valid BlockFacing", nameof(self)) + }; +} diff --git a/src/gaemstone.Bloxel/BlockPos.cs b/src/gaemstone.Bloxel/BlockPos.cs new file mode 100644 index 0000000..51d6ca3 --- /dev/null +++ b/src/gaemstone.Bloxel/BlockPos.cs @@ -0,0 +1,75 @@ +using System; +using Silk.NET.Maths; + +namespace gaemstone.Bloxel; + +public readonly struct BlockPos + : IEquatable +{ + 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 GetOrigin() => new(X, Y, Z); + public Vector3D 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 self) + => new((int)MathF.Floor(self.X), (int)MathF.Floor(self.Y), (int)MathF.Floor(self.Z)); +} diff --git a/src/gaemstone.Bloxel/Chunk.cs b/src/gaemstone.Bloxel/Chunk.cs new file mode 100644 index 0000000..b700df9 --- /dev/null +++ b/src/gaemstone.Bloxel/Chunk.cs @@ -0,0 +1,17 @@ +using gaemstone.ECS; + +namespace gaemstone.Bloxel; + +[Component] +public readonly struct Chunk +{ + // Length of the egde of a world chunk. + public const int LENGTH = 16; + // Amount of bit shifting to go from a BlockPos to a ChunkPos. + public const int BIT_SHIFT = 4; + // Amount of bit masking to go from a BlockPos to a chunk-relative BlockPos. + public const int BIT_MASK = 0b1111; + + public ChunkPos Position { get; } + public Chunk(ChunkPos pos) => Position = pos; +} diff --git a/src/gaemstone.Bloxel/ChunkPaletteStorage.cs b/src/gaemstone.Bloxel/ChunkPaletteStorage.cs new file mode 100644 index 0000000..48527e1 --- /dev/null +++ b/src/gaemstone.Bloxel/ChunkPaletteStorage.cs @@ -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 +{ + const int Size = 16 * 16 * 16; + static readonly EqualityComparer COMPARER + = EqualityComparer.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 Blocks + => _palette?.Where(entry => !COMPARER.Equals(entry.Value, default!)) + .Select(entry => entry.Value!) + ?? Enumerable.Empty(); + + + 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; } + } +} diff --git a/src/gaemstone.Bloxel/ChunkPos.cs b/src/gaemstone.Bloxel/ChunkPos.cs new file mode 100644 index 0000000..4f1b193 --- /dev/null +++ b/src/gaemstone.Bloxel/ChunkPos.cs @@ -0,0 +1,81 @@ +using System; +using Silk.NET.Maths; + +namespace gaemstone.Bloxel; + +public readonly struct ChunkPos + : IEquatable +{ + 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 GetOrigin() => new( + X << Chunk.BIT_SHIFT, Y << Chunk.BIT_SHIFT, Z << Chunk.BIT_SHIFT); + public Vector3D 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 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)); +} diff --git a/src/gaemstone.Bloxel/Client/ChunkMeshGenerator.cs b/src/gaemstone.Bloxel/Client/ChunkMeshGenerator.cs new file mode 100644 index 0000000..d7bfb4c --- /dev/null +++ b/src/gaemstone.Bloxel/Client/ChunkMeshGenerator.cs @@ -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[][] OffsetPerFacing = { + new Vector3D[]{ new(1,1,1), new(1,0,1), new(1,0,0), new(1,1,0) }, // East (+X) + new Vector3D[]{ new(0,1,0), new(0,0,0), new(0,0,1), new(0,1,1) }, // West (-X) + new Vector3D[]{ new(1,1,0), new(0,1,0), new(0,1,1), new(1,1,1) }, // Up (+Y) + new Vector3D[]{ new(1,0,1), new(0,0,1), new(0,0,0), new(1,0,0) }, // Down (-Y) + new Vector3D[]{ new(0,1,1), new(0,0,1), new(1,0,1), new(1,1,1) }, // South (+Z) + new Vector3D[]{ new(1,1,0), new(1,0,0), new(0,0,0), new(0,1,0) } // North (-Z) + }; + + private static readonly int[] TriangleIndices + = { 0, 1, 3, 1, 2, 3 }; + + + private ushort[] _indices = new ushort[StartingCapacity]; + private Vector3D[] _vertices = new Vector3D[StartingCapacity]; + private Vector3D[] _normals = new Vector3D[StartingCapacity]; + private Vector2D[] _uvs = new Vector2D[StartingCapacity]; + + [System] + public void GenerateChunkMeshes(Universe universe, Entity entity, + in Chunk chunk, ChunkPaletteStorage 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 centerStorage) + { + // TODO: We'll need a way to get neighbors again. + // var storages = new ChunkPaletteStorage[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[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(x, y, z); + var textureCell = block.Get(); + + 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[,,] 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; + } +} diff --git a/src/gaemstone.Bloxel/Neighbor.cs b/src/gaemstone.Bloxel/Neighbor.cs new file mode 100644 index 0000000..6e7643b --- /dev/null +++ b/src/gaemstone.Bloxel/Neighbor.cs @@ -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 Horizontals + = ImmutableHashSet.Create(Neighbor.East , Neighbor.West , + Neighbor.South, Neighbor.North); + + public static readonly ImmutableHashSet Verticals + = ImmutableHashSet.Create(Neighbor.Up, Neighbor.Down); + + public static readonly ImmutableHashSet Facings + = Horizontals.Union(Verticals); + + public static readonly ImmutableHashSet Cardinals + = Horizontals.Union(new[] { + Neighbor.SouthEast, Neighbor.SouthWest, + Neighbor.NorthEast, Neighbor.NorthWest }); + + public static readonly ImmutableHashSet 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 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 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(); + } +} diff --git a/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs b/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs new file mode 100644 index 0000000..d28a394 --- /dev/null +++ b/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace gaemstone.Bloxel.Utility; + +public class ChunkedOctree + where T : struct +{ + public delegate void UpdateAction(int level, ReadOnlySpan 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 _comparer = EqualityComparer.Default; + private readonly Dictionary _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); + 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 _octree; + private readonly WeightFunc _weight; + + private readonly HashSet _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 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); + } + } +} diff --git a/src/gaemstone.Bloxel/Utility/ZOrder.cs b/src/gaemstone.Bloxel/Utility/ZOrder.cs new file mode 100644 index 0000000..2047a9b --- /dev/null +++ b/src/gaemstone.Bloxel/Utility/ZOrder.cs @@ -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 + , IComparable +{ + 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; + } +} diff --git a/src/gaemstone.Bloxel/WorldGen/BasicWorldGenerator.cs b/src/gaemstone.Bloxel/WorldGen/BasicWorldGenerator.cs new file mode 100644 index 0000000..354bb2b --- /dev/null +++ b/src/gaemstone.Bloxel/WorldGen/BasicWorldGenerator.cs @@ -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 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(); + } +} diff --git a/src/gaemstone.Bloxel/WorldGen/SurfaceGrassGenerator.cs.disabled b/src/gaemstone.Bloxel/WorldGen/SurfaceGrassGenerator.cs.disabled new file mode 100644 index 0000000..a615398 --- /dev/null +++ b/src/gaemstone.Bloxel/WorldGen/SurfaceGrassGenerator.cs.disabled @@ -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 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; + } + } + } + } +} diff --git a/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj b/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj new file mode 100644 index 0000000..d017c6b --- /dev/null +++ b/src/gaemstone.Bloxel/gaemstone.Bloxel.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + disable + enable + + + + + + + + + + + + diff --git a/src/gaemstone.Client/Color.cs b/src/gaemstone.Client/Color.cs new file mode 100644 index 0000000..ba7bc4d --- /dev/null +++ b/src/gaemstone.Client/Color.cs @@ -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 +{ + 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(Color color) => new(color.R / 255F, color.G / 255F, color.B / 255F, color.A / 255F); + public static implicit operator Vector4D(Color color) => new(color.R, color.G, color.B, color.A); +} diff --git a/src/gaemstone.Client/GLExtensions.cs b/src/gaemstone.Client/GLExtensions.cs new file mode 100644 index 0000000..d163860 --- /dev/null +++ b/src/gaemstone.Client/GLExtensions.cs @@ -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) causes the generic type resolving to break. + public static uint CreateBufferFromData(this GL GL, T[] data, + BufferTargetARB target = BufferTargetARB.ArrayBuffer, + BufferUsageARB usage = BufferUsageARB.StaticDraw) + where T : unmanaged + => GL.CreateBufferFromData((ReadOnlySpan)data, target, usage); + public static uint CreateBufferFromData(this GL GL, ArraySegment data, + BufferTargetARB target = BufferTargetARB.ArrayBuffer, + BufferUsageARB usage = BufferUsageARB.StaticDraw) + where T : unmanaged + => GL.CreateBufferFromData((ReadOnlySpan)data, target, usage); + public static uint CreateBufferFromData(this GL GL, Span data, + BufferTargetARB target = BufferTargetARB.ArrayBuffer, + BufferUsageARB usage = BufferUsageARB.StaticDraw) + where T : unmanaged + => GL.CreateBufferFromData((ReadOnlySpan)data, target, usage); + public static uint CreateBufferFromData(this GL GL, ReadOnlySpan 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()), data, usage); + return buffer; + } +} diff --git a/src/gaemstone.Client/Mesh.cs b/src/gaemstone.Client/Mesh.cs new file mode 100644 index 0000000..f0cacbf --- /dev/null +++ b/src/gaemstone.Client/Mesh.cs @@ -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; } +} diff --git a/src/gaemstone.Client/MeshManager.cs b/src/gaemstone.Client/MeshManager.cs new file mode 100644 index 0000000..b2434fc --- /dev/null +++ b/src/gaemstone.Client/MeshManager.cs @@ -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().Get().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 indices, ReadOnlySpan> vertices, + ReadOnlySpan> normals, ReadOnlySpan> uvs) + { + var GL = universe.Lookup().Get().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> vertices, + ReadOnlySpan> normals, ReadOnlySpan> uvs) + { + var GL = universe.Lookup().Get().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); + } +} diff --git a/src/gaemstone.Client/Modules/CameraModule.cs b/src/gaemstone.Client/Modules/CameraModule.cs new file mode 100644 index 0000000..522851b --- /dev/null +++ b/src/gaemstone.Client/Modules/CameraModule.cs @@ -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 ClearColor { get; set; } + public Rectangle Viewport { get; set; } + } + + [Component] + public struct CameraController + { + public float MouseSensitivity { get; set; } + public Vector2D? 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 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(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; + } + } +} diff --git a/src/gaemstone.Client/Modules/Input.cs b/src/gaemstone.Client/Modules/Input.cs new file mode 100644 index 0000000..b8d60f1 --- /dev/null +++ b/src/gaemstone.Client/Modules/Input.cs @@ -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 Keyboard { get; } = new(); + public Dictionary MouseButtons { get; } = new(); + public Vector2D 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; + } +} diff --git a/src/gaemstone.Client/Modules/Renderer.cs b/src/gaemstone.Client/Modules/Renderer.cs new file mode 100644 index 0000000..8d8c533 --- /dev/null +++ b/src/gaemstone.Client/Modules/Renderer.cs @@ -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().Get().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(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(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); + } +} diff --git a/src/gaemstone.Client/Modules/Windowing.cs b/src/gaemstone.Client/Modules/Windowing.cs new file mode 100644 index 0000000..82d3345 --- /dev/null +++ b/src/gaemstone.Client/Modules/Windowing.cs @@ -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 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; +} diff --git a/src/gaemstone.Client/Resources.cs b/src/gaemstone.Client/Resources.cs new file mode 100644 index 0000000..c68751f --- /dev/null +++ b/src/gaemstone.Client/Resources.cs @@ -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(); + } +} diff --git a/src/gaemstone.Client/Texture.cs b/src/gaemstone.Client/Texture.cs new file mode 100644 index 0000000..3a02fd0 --- /dev/null +++ b/src/gaemstone.Client/Texture.cs @@ -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); +} diff --git a/src/gaemstone.Client/TextureCoords4.cs b/src/gaemstone.Client/TextureCoords4.cs new file mode 100644 index 0000000..70cd3fe --- /dev/null +++ b/src/gaemstone.Client/TextureCoords4.cs @@ -0,0 +1,36 @@ +using System.Drawing; +using gaemstone.ECS; +using Silk.NET.Maths; + +namespace gaemstone.Client; + +[Component] +public readonly struct TextureCoords4 +{ + public Vector2D TopLeft { get; } + public Vector2D TopRight { get; } + public Vector2D BottomLeft { get; } + public Vector2D 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); +} diff --git a/src/gaemstone.Client/TextureManager.cs b/src/gaemstone.Client/TextureManager.cs new file mode 100644 index 0000000..e44e4d8 --- /dev/null +++ b/src/gaemstone.Client/TextureManager.cs @@ -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 _byTexture = new(); + private static readonly Dictionary _bySourceFile = new(); + + public static void Initialize(Universe universe) + { + var GL = universe.Lookup().Get().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 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().Get().GL; + var texture = new Texture(TextureTarget.Texture2D, GL.GenTexture()); + GL.BindTexture(texture.Target, texture.Handle); + + var image = Image.Load(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); +} diff --git a/src/gaemstone.Client/gaemstone.Client.csproj b/src/gaemstone.Client/gaemstone.Client.csproj new file mode 100644 index 0000000..8d853d5 --- /dev/null +++ b/src/gaemstone.Client/gaemstone.Client.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + disable + enable + true + + + + + + + + + + + + + + diff --git a/src/gaemstone/ECS/Attributes.cs b/src/gaemstone/ECS/Attributes.cs new file mode 100644 index 0000000..593ba40 --- /dev/null +++ b/src/gaemstone/ECS/Attributes.cs @@ -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 { } diff --git a/src/gaemstone/ECS/Entity.cs b/src/gaemstone/ECS/Entity.cs new file mode 100644 index 0000000..a87a800 --- /dev/null +++ b/src/gaemstone/ECS/Entity.cs @@ -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 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() + => Add(Universe.Lookup()); + public Entity Add() + => Add(Universe.Lookup(), Universe.Lookup()); + public Entity Add(Entity target) + => Add(Universe.Lookup(), 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() + => Override(Universe.Lookup()); + public Entity Override() + => Override(Universe.Lookup(), Universe.Lookup()); + public Entity Override(Entity target) + => Override(Universe.Lookup(), 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() => Remove(Universe.Lookup()); + + + 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() + => Has(Universe.Lookup()); + public bool Has() + => Has(Universe.Lookup(), Universe.Lookup()); + public bool Has(Entity target) + => Has(Universe.Lookup(), target); + + + /// + /// 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 . + /// + public T Get() + { + var comp = Universe.Lookup(); + var ptr = ecs_get_id(Universe, this, comp); + if (typeof(T).IsValueType) { + return Unsafe.Read(ptr); + } else { + var handle = (GCHandle)Unsafe.Read(ptr); + return (T)handle.Target!; + } + } + + /// + /// Gets a reference to a component value from this entity. Only works for + /// value types. When modifying, consider calling . + /// + public ref T GetRef() + where T : unmanaged + { + var comp = Universe.Lookup(); + var ptr = ecs_get_mut_id(Universe, this, comp); + return ref Unsafe.AsRef(ptr); + } + + /// + /// Marks a component as modified. Do this after getting a reference to + /// it with or , making sure change + /// detection will kick in. + /// + public void Modified() + { + var comp = Universe.Lookup(); + ecs_modified_id(Universe, this, comp); + } + + + public Entity Set(in T value) + where T : unmanaged + { + var comp = Universe.Lookup(); + var size = (ulong)Unsafe.SizeOf(); + fixed (T* ptr = &value) ecs_set_id(Universe, this, comp, size, ptr); + return this; + } + public Entity SetOverride(in T value) + where T : unmanaged + { + var comp = Universe.Lookup(); + var size = (ulong)Unsafe.SizeOf(); + 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 obj) where T : class + => Set(typeof(T), obj); + public Entity SetOverride(T obj) + where T : class + { + var comp = Universe.Lookup(); + 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 +{ + 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 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(); +} diff --git a/src/gaemstone/ECS/EntityDesc.cs.disabled b/src/gaemstone/ECS/EntityDesc.cs.disabled new file mode 100644 index 0000000..0a3e44b --- /dev/null +++ b/src/gaemstone/ECS/EntityDesc.cs.disabled @@ -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; +} diff --git a/src/gaemstone/ECS/Filter.cs b/src/gaemstone/ECS/Filter.cs new file mode 100644 index 0000000..c2d77d8 --- /dev/null +++ b/src/gaemstone/ECS/Filter.cs @@ -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 + , 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 GetEnumerator() => Iter().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public static implicit operator ecs_filter_t*(Filter q) => q.Handle; +} diff --git a/src/gaemstone/ECS/FlecsException.cs b/src/gaemstone/ECS/FlecsException.cs new file mode 100644 index 0000000..f4b8745 --- /dev/null +++ b/src/gaemstone/ECS/FlecsException.cs @@ -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; +} diff --git a/src/gaemstone/ECS/Identifier.cs b/src/gaemstone/ECS/Identifier.cs new file mode 100644 index 0000000..fa459ba --- /dev/null +++ b/src/gaemstone/ECS/Identifier.cs @@ -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, +} diff --git a/src/gaemstone/ECS/Iterator.cs b/src/gaemstone/ECS/Iterator.cs new file mode 100644 index 0000000..695061b --- /dev/null +++ b/src/gaemstone/ECS/Iterator.cs @@ -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 +{ + 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 Field(int index) + where T : unmanaged + { + fixed (ecs_iter_t* ptr = &Value) { + var size = (ulong)Unsafe.SizeOf(); + var pointer = ecs_field_w_size(ptr, size, index); + return new Span(pointer, Count); + } + } + + public SpanToRef FieldRef(int index) + where T : class => new(Field(index)); + + public bool FieldIsSet(int index) + { + fixed (ecs_iter_t* ptr = &Value) + return ecs_field_is_set(ptr, index); + } + + public bool FieldIs(int index) + where T : unmanaged + { + fixed (ecs_iter_t* ptr = &Value) { + var id = ecs_field_id(ptr, index); + var comp = Universe.Lookup(); + return id == comp.Value.Data; + } + } + + public readonly ref struct SpanToRef + { + private readonly Span _span; + internal SpanToRef(Span span) => _span = span; + public int Length => _span.Length; + public T this[int index] => (T)((GCHandle)_span[index]).Target!; + } + + // IEnumerable implementation + public IEnumerator GetEnumerator() { while (Next()) yield return this; } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +public enum IteratorType +{ + Term, + Filter, + Query, + Rule, +} diff --git a/src/gaemstone/ECS/Module.cs b/src/gaemstone/ECS/Module.cs new file mode 100644 index 0000000..97513ac --- /dev/null +++ b/src/gaemstone/ECS/Module.cs @@ -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; +} diff --git a/src/gaemstone/ECS/Observer.cs b/src/gaemstone/ECS/Observer.cs new file mode 100644 index 0000000..2b03487 --- /dev/null +++ b/src/gaemstone/ECS/Observer.cs @@ -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, +} diff --git a/src/gaemstone/ECS/Query.cs b/src/gaemstone/ECS/Query.cs new file mode 100644 index 0000000..9677a17 --- /dev/null +++ b/src/gaemstone/ECS/Query.cs @@ -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 + , 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 GetEnumerator() => Iter().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public static implicit operator ecs_query_t*(Query q) => q.Handle; +} diff --git a/src/gaemstone/ECS/System.cs b/src/gaemstone/ECS/System.cs new file mode 100644 index 0000000..05f8da3 --- /dev/null +++ b/src/gaemstone/ECS/System.cs @@ -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, + + /// + /// This phase contains all the systems that load data into your ECS. + /// This would be a good place to load keyboard and mouse inputs. + /// + OnLoad, + + /// + /// Often the imported data needs to be processed. Maybe you want to associate + /// your keypresses with high level actions rather than comparing explicitly + /// in your game code if the user pressed the 'K' key. + /// + PostLoad, + + /// + /// Now that the input is loaded and processed, it's time to get ready to + /// start processing our game logic. Anything that needs to happen after + /// input processing but before processing the game logic can happen here. + /// This can be a good place to prepare the frame, maybe clean up some + /// things from the previous frame, etcetera. + /// + PreUpdate, + + /// + /// This is usually where the magic happens! This is where you put all of + /// your gameplay systems. By default systems are added to this phase. + /// + OnUpdate, + + /// + /// This phase was introduced to deal with validating the state of the game + /// after processing the gameplay systems. Sometimes you entities too close + /// to each other, or the speed of an entity is increased too much. + /// This phase is for righting that wrong. A typical feature to implement + /// in this phase would be collision detection. + /// + OnValidate, + + /// + /// When your game logic has been updated, and your validation pass has ran, + /// you may want to apply some corrections. For example, if your collision + /// detection system detected collisions in the OnValidate phase, + /// you may want to move the entities so that they no longer overlap. + /// + PostUpdate, + + /// + /// Now that all of the frame data is computed, validated and corrected for, + /// it is time to prepare the frame for rendering. Any systems that need to + /// run before rendering, but after processing the game logic should go here. + /// A good example would be a system that calculates transform matrices from + /// a scene graph. + /// + PreStore, + + /// + /// This is where it all comes together. Your frame is ready to be + /// rendered, and that is exactly what you would do in this phase. + /// + OnStore, + + PostFrame, +} diff --git a/src/gaemstone/ECS/Universe+Modules.cs b/src/gaemstone/ECS/Universe+Modules.cs new file mode 100644 index 0000000..d52f58d --- /dev/null +++ b/src/gaemstone/ECS/Universe+Modules.cs @@ -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() 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 _modules = new(); + internal readonly Dictionary _deferred = new(); + + internal UniverseModules(Universe universe) { } + } + + public class ModuleInfo + { + public Universe Universe { get; } + public object Instance { get; } + + public IReadOnlyList Relations { get; } + public IReadOnlyList Components { get; } + public IReadOnlyList Tags { get; } + public IReadOnlyList Entities { get; } + public IReadOnlyList 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 DependsOn { get; } + public bool HasSimpleConstructor { get; } + + public IReadOnlyList Relations { get; } + public IReadOnlyList Components { get; } + public IReadOnlyList Tags { get; } + public IReadOnlyList Entities { get; } + public IReadOnlyList Systems { get; } + + public HashSet 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()) throw new Exception( + "Module must be marked with ModuleAttribute"); + + Universe = universe; + Type = type; + DependsOn = type.GetMultiple() + .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(); + var components = new List(); + var tags = new List(); + var entities = new List(); + var systems = new List(); + + foreach (var nested in Type.GetNestedTypes()) { + if (nested.Has()) relations.Add(nested); + else if (nested.Has()) components.Add(nested); + else if (nested.Has()) tags.Add(nested); + else if (nested.Has()) entities.Add(nested); + } + foreach (var method in Type.GetMethods()) + if (method.Has()) + 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(); + } + } +} diff --git a/src/gaemstone/ECS/Universe+Systems.cs b/src/gaemstone/ECS/Universe+Systems.cs new file mode 100644 index 0000000..1266efc --- /dev/null +++ b/src/gaemstone/ECS/Universe+Systems.cs @@ -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 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 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 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(); + var phase = attr?.Phase ?? Phase.OnUpdate; + + if (action is Action 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(); + 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)Delegate.CreateDelegate(typeof(Action), 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 + { + public readonly struct SystemCallbackContext + { + public Universe Universe { get; } + public Action Callback { get; } + + public SystemCallbackContext(Universe universe, Action callback) + { Universe = universe; Callback = callback; } + } + + private static SystemCallbackContext[] _systemCallbackContexts = new SystemCallbackContext[64]; + private static int _systemCallbackContextsCount = 0; + + public static nint CreateSystemCallbackContext(Universe universe, Action 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 _systems = new(); + internal readonly Dictionary _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 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 Callback { get; } + + internal SystemInfo(Universe universe, Entity entity, string name, string? expression, + Phase phase, ecs_filter_desc_t filter, Action callback) + { + Universe = universe; + Entity = entity; + + Name = name; + Expression = expression; + + Phase = phase; + Filter = filter; + Callback = callback; + } + } +} diff --git a/src/gaemstone/ECS/Universe.cs b/src/gaemstone/ECS/Universe.cs new file mode 100644 index 0000000..56fe78a --- /dev/null +++ b/src/gaemstone/ECS/Universe.cs @@ -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 _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() + => 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() == true; + if (type.Has()) { + if (!isPartOfModule) RegisterRelation(type); + } else if (type.Has()) { + if (!isPartOfModule) RegisterComponent(type); + } else if (type.Has()) { + if (!isPartOfModule) RegisterTag(type); + } else if (type.Has()) { + if (!isPartOfModule) RegisterEntity(type); + } else if (type.Has()) + RegisterModule(type); + } + } + + public Entity RegisterRelation() + => RegisterRelation(typeof(T)); + public Entity RegisterRelation(Type type) + => throw new NotImplementedException(); + + public Entity RegisterComponent() + => 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()) { + if (type.IsValueType) entity.Add(entity); + else entity.Set(type, Activator.CreateInstance(type)!); + } + + return entity; + } + + public Entity RegisterTag() + 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() + 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; +} diff --git a/src/gaemstone/GlobalTransform.cs b/src/gaemstone/GlobalTransform.cs new file mode 100644 index 0000000..d4b9d83 --- /dev/null +++ b/src/gaemstone/GlobalTransform.cs @@ -0,0 +1,13 @@ +using gaemstone.ECS; +using Silk.NET.Maths; + +namespace gaemstone; + +[Component] +public struct GlobalTransform +{ + public Matrix4X4 Value; + public GlobalTransform(Matrix4X4 value) => Value = value; + public static implicit operator GlobalTransform(in Matrix4X4 value) => new(value); + public static implicit operator Matrix4X4(in GlobalTransform index) => index.Value; +} diff --git a/src/gaemstone/Utility/CStringExtensions.cs b/src/gaemstone/Utility/CStringExtensions.cs new file mode 100644 index 0000000..660e0d1 --- /dev/null +++ b/src/gaemstone/Utility/CStringExtensions.cs @@ -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; + } +} diff --git a/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs b/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs new file mode 100644 index 0000000..9a6c3ca --- /dev/null +++ b/src/gaemstone/Utility/IL/ILGeneratorWrapper.cs @@ -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 _locals = new(); + private readonly List<(int Offset, int Indent, OpCode Code, object? Arg)> _instructions = new(); + private readonly Dictionary _labelToOffset = new(); + private readonly Stack _indents = new(); + + public ILGeneratorWrapper(DynamicMethod method) + { + _method = method; + _il = method.GetILGenerator(); + } + + public string ToReadableString() + { + var sb = new StringBuilder(); + sb.AppendLine("Parameters:"); + foreach (var (param, index) in _method.GetParameters().Select((p, i) => (p, i))) + sb.AppendLine($" Argument({index}, {param.ParameterType.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 Argument(int index) => (IArgument)Argument(index); + + public ILocal Local(Type type, string? name = null) + { + var builder = _il.DeclareLocal(type); + var local = type.IsByRefLike ? new LocalImpl(builder, name) + : (ILocal)Activator.CreateInstance(typeof(LocalImpl<>).MakeGenericType(type), builder, name)!; + _locals.Add(local); + return local; + } + public ILocal Local(string? name = null) => (ILocal)Local(typeof(T), name); + public ILocal LocalArray(Type type, string? name = null) => (ILocal)Local(type.MakeArrayType(), name); + public ILocal LocalArray(string? name = null) => (ILocal)Local(typeof(T).MakeArrayType(), name); + + public Label DefineLabel() => _il.DefineLabel(); + public void MarkLabel(Label label) + { + _instructions.Add((-1, _indents.Count, OpCodes.Nop, label)); + _labelToOffset.Add(label, _il.ILOffset); + _il.MarkLabel(label); + } + + + private void AddInstr(OpCode code, object? arg = null) => _instructions.Add((_il.ILOffset, _indents.Count, code, arg)); + public void Comment(string comment) => _instructions.Add((-1, _indents.Count, OpCodes.Nop, comment)); + + 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 local, int value) { LoadConst(value); Store(local); } + + public void LoadLength() { Emit(OpCodes.Ldlen); Emit(OpCodes.Conv_I4); } + public void LoadLength(IArgument array) { Load(array); LoadLength(); } + public void LoadLength(ILocal array) { Load(array); LoadLength(); } + + public void LoadObj(Type type) => Emit(OpCodes.Ldobj, type); + public void LoadObj(ILocal local) { LoadAddr(local); LoadObj(local.LocalType); } + public void LoadObj(IArgument arg) { LoadAddr(arg); LoadObj(arg.ArgumentType); } + + public void LoadElem(Type type) => Emit(OpCodes.Ldelem, type); + public void LoadElem(Type type, int index) { LoadConst(index); LoadElem(type); } + public void LoadElem(Type type, ILocal index) { Load(index); LoadElem(type); } + public void LoadElem(Type type, IArgument array, int index) { Load(array); LoadElem(type, index); } + public void LoadElem(Type type, IArgument array, ILocal index) { Load(array); LoadElem(type, index); } + public void LoadElem(Type type, ILocal array, int index) { Load(array); LoadElem(type, index); } + public void LoadElem(Type type, ILocal array, ILocal index) { Load(array); LoadElem(type, index); } + + public void LoadElemRef() => Emit(OpCodes.Ldelem_Ref); + public void LoadElemRef(int index) { LoadConst(index); LoadElemRef(); } + public void LoadElemRef(ILocal index) { Load(index); LoadElemRef(); } + public void LoadElemRef(IArgument array, int index) { Load(array); LoadElemRef(index); } + public void LoadElemRef(IArgument array, ILocal index) { Load(array); LoadElemRef(index); } + public void LoadElemRef(ILocal array, int index) { Load(array); LoadElemRef(index); } + public void LoadElemRef(ILocal array, ILocal index) { Load(array); LoadElemRef(index); } + + public void LoadElemEither(Type type) { if (type.IsValueType) LoadElem(type); else LoadElemRef(); } + public void LoadElemEither(Type type, int index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); } + public void LoadElemEither(Type type, ILocal index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); } + public void LoadElemEither(Type type, IArgument array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } + public void LoadElemEither(Type type, IArgument array, ILocal index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } + public void LoadElemEither(Type type, ILocal array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } + public void LoadElemEither(Type type, ILocal array, ILocal index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } + + public void LoadElemAddr(Type type) => Emit(OpCodes.Ldelema, type); + public void LoadElemAddr(Type type, int index) { LoadConst(index); LoadElemAddr(type); } + public void LoadElemAddr(Type type, ILocal index) { Load(index); LoadElemAddr(type); } + public void LoadElemAddr(Type type, IArgument array, int index) { Load(array); LoadElemAddr(type, index); } + public void LoadElemAddr(Type type, IArgument array, ILocal index) { Load(array); LoadElemAddr(type, index); } + public void LoadElemAddr(Type type, ILocal array, int index) { Load(array); LoadElemAddr(type, index); } + public void LoadElemAddr(Type type, ILocal array, ILocal index) { Load(array); LoadElemAddr(type, index); } + + public void Load(PropertyInfo info) => CallVirt(info.GetMethod!); + public void Load(ILocal obj, PropertyInfo info) { Load(obj); Load(info); } + public void Load(IArgument obj, PropertyInfo info) { Load(obj); Load(info); } + + public void Add() => Emit(OpCodes.Add); + public void Increment(ILocal local) { Load(local); LoadConst(1); Add(); Store(local); } + + public void Init(Type type) => Emit(OpCodes.Initobj, type); + public void Init() where T : struct => 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() => 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 a, Comparison op, ILocal b) + { Load(a); Load(b); Emit(op.Code, label); } + public void GotoIfNull(Label label, ILocal local) + { Load(local); Emit(OpCodes.Brfalse, label); } + public void GotoIfNotNull(Label label, ILocal local) + { Load(local); Emit(OpCodes.Brtrue, label); } + + public void Call(MethodInfo method) => Emit(OpCodes.Call, method); + public void CallVirt(MethodInfo method) => Emit(OpCodes.Callvirt, method); + + public void Return() => Emit(OpCodes.Ret); + + + public IDisposable For(Action loadMax, out ILocal current) + { + var r = Random.Shared.Next(10000, 100000); + Comment($"INIT for loop {r}"); + + var curLocal = current = Local($"index_{r}"); + var maxLocal = Local($"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 : ArgumentImpl, IArgument + { public ArgumentImpl(int index) : base(index, typeof(T)) { } } + + internal class LocalImpl : ILocal + { + public LocalBuilder Builder { get; } + public string? Name { get; } + public Type LocalType => Builder.LocalType; + public LocalImpl(LocalBuilder builder, string? name) { Builder = builder; Name = name; } + public override string ToString() => $"Local({Builder.LocalIndex}, {LocalType.Name}){(Name != null ? $" // {Name}" : "")}"; + } + internal class LocalImpl : LocalImpl, ILocal + { public LocalImpl(LocalBuilder builder, string? name) : base(builder, name) { } } +} + +public class Comparison +{ + public static Comparison NotEqual { get; } = new(OpCodes.Bne_Un); + public static Comparison LessThan { get; } = new(OpCodes.Blt); + public static Comparison LessOrEq { get; } = new(OpCodes.Ble); + public static Comparison Equal { get; } = new(OpCodes.Beq); + public static Comparison GreaterOrEq { get; } = new(OpCodes.Bge); + public static Comparison GreaterThan { get; } = new(OpCodes.Bgt); + + public OpCode Code { get; } + private Comparison(OpCode code) => Code = code; +} + +public interface IArgument +{ + int Index { get; } + Type ArgumentType { get; } +} +public interface IArgument + : IArgument { } + +public interface ILocal +{ + LocalBuilder Builder { get; } + Type LocalType { get; } +} +public interface ILocal + : ILocal { } diff --git a/src/gaemstone/Utility/IL/QueryActionGenerator.cs b/src/gaemstone/Utility/IL/QueryActionGenerator.cs new file mode 100644 index 0000000..c345db0 --- /dev/null +++ b/src/gaemstone/Utility/IL/QueryActionGenerator.cs @@ -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 _cache = new(); + private static readonly Dictionary, ILocal>> _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 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(0); + var iteratorArg = IL.Argument(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 local and initialize it to iterator.Field(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() : 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>(); + 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()) Source = underlyingType; + if (Info.Get() 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()) { + 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() || info.ParameterType.Has()) { + 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 + { + /// Parameter is not part of terms, handled uniquely, such as Universe and Entity. + Unique, + /// Passed by value. + Normal, + /// Struct passed with the "in" modifier. + In, + /// Struct passed with the "out" modifier. + Out, + /// Struct passed with the "ref" modifier. + Ref, + /// + /// Only checks for presence. + /// Manually applied with . + /// Automatically applied for types with . + /// + Has, + /// Struct passed as Nullable<T>. + Nullable, + /// + /// Only checks for absence. + /// Applied with . + /// + Not, + } +} diff --git a/src/gaemstone/Utility/RandomExtensions.cs b/src/gaemstone/Utility/RandomExtensions.cs new file mode 100644 index 0000000..be007b9 --- /dev/null +++ b/src/gaemstone/Utility/RandomExtensions.cs @@ -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(this Random rnd, params T[] elements) + => elements[rnd.Next(elements.Length)]; + public static T Pick(this Random rnd, IReadOnlyList elements) + => elements[rnd.Next(elements.Count)]; + public static T Pick(this Random rnd, Span elements) + => elements[rnd.Next(elements.Length)]; + +#pragma warning disable CS8509 // Switch expression is not exhaustive. + public static T Pick(this Random rnd, T elem1, T elem2) + => rnd.Next(2) switch { 0 => elem1, 1 => elem2 }; + public static T Pick(this Random rnd, T elem1, T elem2, T elem3) + => rnd.Next(3) switch { 0 => elem1, 1 => elem2, 2 => elem3 }; + public static T Pick(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 +} diff --git a/src/gaemstone/Utility/ReflectionExtensions.cs b/src/gaemstone/Utility/ReflectionExtensions.cs new file mode 100644 index 0000000..99882bc --- /dev/null +++ b/src/gaemstone/Utility/ReflectionExtensions.cs @@ -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(this MemberInfo member) + where T : Attribute => member.GetCustomAttribute(); + public static IEnumerable GetMultiple(this MemberInfo member) + where T : Attribute => member.GetCustomAttributes(); + public static bool Has(this MemberInfo member) + where T : Attribute => member.GetCustomAttribute() != null; + + public static T? Get(this ParameterInfo member) + where T : Attribute => member.GetCustomAttribute(); + public static IEnumerable GetMultiple(this ParameterInfo member) + where T : Attribute => member.GetCustomAttributes(); + public static bool Has(this ParameterInfo member) + where T : Attribute => member.GetCustomAttribute() != 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 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)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; + } +} diff --git a/src/gaemstone/Utility/TypeWrapper.cs b/src/gaemstone/Utility/TypeWrapper.cs new file mode 100644 index 0000000..af44813 --- /dev/null +++ b/src/gaemstone/Utility/TypeWrapper.cs @@ -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 ClassGetter { get; } + Action ClassSetter { get; } +} + +public static class TypeWrapper +{ + static readonly Dictionary _typeCache = new(); + + public static TypeWrapper For() + => TypeWrapper.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())!); + return wrapper; + } +} + +public class TypeWrapper : ITypeWrapper +{ + internal static TypeWrapper Instance { get; } = new(); + + readonly Dictionary _fieldCache = new(); + + public Type Type => typeof(TType); + + public int Size { get; } = Unsafe.SizeOf(); + public bool IsUnmanaged { get; } = !RuntimeHelpers.IsReferenceOrContainsReferences(); + + 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 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 FieldWrapper 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 FieldWrapper 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 FieldWrapper 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); + } + + + 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 GetField(FieldInfo field, PropertyInfo? property) + { + if (_fieldCache.TryGetValue(field, out var cached)) return (FieldWrapper)cached; + if (field.FieldType != typeof(TField)) throw new ArgumentException( + $"FieldType ({field.FieldType}) does not match TField ({typeof(TField)})", nameof(TField)); + var wrapper = new FieldWrapper(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 IFieldWrapper.ClassGetter => (obj) => ClassGetter((TType)obj); + Action IFieldWrapper.ClassSetter => (obj, value) => ClassSetter((TType)obj, value); + new Func ClassGetter { get; } + new Action ClassSetter { get; } + + ValueGetterAction ByRefGetter { get; } + ValueSetterAction ByRefSetter { get; } + } + + public class FieldWrapper : IFieldWrapperForType + { + public delegate TField ValueGetterAction(in TType obj); + public delegate void ValueSetterAction(ref TType obj, TField value); + + Func? _classGetter; + Action? _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 IFieldWrapperForType.ClassGetter => (obj) => ClassGetter(obj); + Action IFieldWrapperForType.ClassSetter => (obj, value) => ClassSetter(obj, (TField)value!); + public Func ClassGetter => _classGetter ??= BuildGetter>(false); + public Action ClassSetter => _classSetter ??= BuildSetter>(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(true); + public ValueSetterAction ByRefSetter => _byRefSetter ??= BuildSetter(true); + + + TDelegate BuildGetter(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 BuildSetter(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(); + } + } +} diff --git a/src/gaemstone/gaemstone.csproj b/src/gaemstone/gaemstone.csproj new file mode 100644 index 0000000..92bd2a3 --- /dev/null +++ b/src/gaemstone/gaemstone.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + disable + enable + true + + + + + + + + + + +