commit
fae12b7963
66 changed files with 4258 additions and 0 deletions
@ -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 |
@ -0,0 +1,4 @@ |
|||||||
|
**/obj/ |
||||||
|
**/bin/ |
||||||
|
/artifacts/ |
||||||
|
/packages/ |
@ -0,0 +1,6 @@ |
|||||||
|
[submodule "src/flecs-cs"] |
||||||
|
path = src/flecs-cs |
||||||
|
url = https://github.com/flecs-hub/flecs-cs |
||||||
|
[submodule "src/FastNoiseLite"] |
||||||
|
path = src/FastNoiseLite |
||||||
|
url = https://github.com/Auburn/FastNoiseLite.git |
@ -0,0 +1,16 @@ |
|||||||
|
{ |
||||||
|
"version": "0.2.0", |
||||||
|
"configurations": [ |
||||||
|
{ |
||||||
|
"name": "Launch Immersion", |
||||||
|
"type": "coreclr", |
||||||
|
"request": "launch", |
||||||
|
"preLaunchTask": "build", |
||||||
|
"program": "${workspaceFolder}/src/Immersion/bin/Debug/net6.0/Immersion.dll", |
||||||
|
"args": [], |
||||||
|
"cwd": "${workspaceFolder}/src/Immersion", |
||||||
|
"console": "internalConsole", |
||||||
|
"stopAtEntry": false |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
{ |
||||||
|
"files.exclude": { |
||||||
|
"**/.git": true, |
||||||
|
"**/.DS_Store": true, |
||||||
|
"**/bin": true, |
||||||
|
"**/obj": true, |
||||||
|
}, |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
{ |
||||||
|
"version": "2.0.0", |
||||||
|
"tasks": [{ |
||||||
|
"label": "build", |
||||||
|
"command": "dotnet", |
||||||
|
"type": "process", |
||||||
|
"args": [ |
||||||
|
"build", |
||||||
|
"${workspaceFolder}/src/Immersion/Immersion.csproj", |
||||||
|
"/property:GenerateFullPaths=true", |
||||||
|
"/consoleloggerparameters:NoSummary" |
||||||
|
], |
||||||
|
"problemMatcher": "$msCompile" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"label": "publish", |
||||||
|
"command": "dotnet", |
||||||
|
"type": "process", |
||||||
|
"args": [ |
||||||
|
"publish", |
||||||
|
"${workspaceFolder}/src/Immersion/Immersion.csproj", |
||||||
|
"/property:GenerateFullPaths=true", |
||||||
|
"/consoleloggerparameters:NoSummary" |
||||||
|
], |
||||||
|
"problemMatcher": "$msCompile" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"label": "watch", |
||||||
|
"command": "dotnet", |
||||||
|
"type": "process", |
||||||
|
"args": [ |
||||||
|
"watch", |
||||||
|
"run", |
||||||
|
"--project", |
||||||
|
"${workspaceFolder}/src/Immersion/Immersion.csproj" |
||||||
|
], |
||||||
|
"problemMatcher": "$msCompile" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
Subproject commit 5923df5d822f7610100d0e77f629c607ed64934a |
@ -0,0 +1,23 @@ |
|||||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||||
|
|
||||||
|
<PropertyGroup> |
||||||
|
<OutputType>Exe</OutputType> |
||||||
|
<TargetFramework>net6.0</TargetFramework> |
||||||
|
<ImplicitUsings>disable</ImplicitUsings> |
||||||
|
<Nullable>enable</Nullable> |
||||||
|
</PropertyGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<EmbeddedResource Include="Resources/*" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<ProjectReference Include="../gaemstone.Bloxel/gaemstone.Bloxel.csproj" /> |
||||||
|
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<PackageReference Include="Silk.NET" Version="2.16.0" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
</Project> |
@ -0,0 +1,11 @@ |
|||||||
|
# Resources License Notices |
||||||
|
|
||||||
|
## Voxelgarden Textures |
||||||
|
|
||||||
|
- terrain.png |
||||||
|
|
||||||
|
**License:** [CC-BY-SA] |
||||||
|
**Source:** [github.com/CasimirKaPazi/Voxelgarden](https://github.com/CasimirKaPazi/Voxelgarden) |
||||||
|
|
||||||
|
|
||||||
|
[CC-BY-SA]: https://creativecommons.org/licenses/by-sa/2.0/ |
@ -0,0 +1,12 @@ |
|||||||
|
#version 330 core |
||||||
|
in vec4 fragColor; |
||||||
|
in vec2 fragUV; |
||||||
|
|
||||||
|
uniform sampler2D textureSampler; |
||||||
|
|
||||||
|
out vec4 color; |
||||||
|
|
||||||
|
void main() |
||||||
|
{ |
||||||
|
color = fragColor * texture(textureSampler, fragUV); |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
#version 330 core |
||||||
|
layout(location = 0) in vec3 vertPosition; |
||||||
|
layout(location = 1) in vec3 vertNormal; |
||||||
|
layout(location = 2) in vec2 vertUV; |
||||||
|
|
||||||
|
uniform mat4 cameraMatrix; |
||||||
|
uniform mat4 modelMatrix; |
||||||
|
|
||||||
|
out vec4 fragColor; |
||||||
|
out vec2 fragUV; |
||||||
|
|
||||||
|
void main() |
||||||
|
{ |
||||||
|
gl_Position = cameraMatrix * modelMatrix * vec4(vertPosition, 1.0); |
||||||
|
// Apply a pseudo-lighting effect based on the object's normals and rotation. |
||||||
|
vec3 normal = mat3(modelMatrix) * vertNormal; |
||||||
|
float l = 0.5 + (normal.y + 1) / 4.0 - (normal.z + 1) / 8.0; |
||||||
|
fragColor = vec4(l, l, l, 1.0); |
||||||
|
fragUV = vertUV; |
||||||
|
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
@ -0,0 +1 @@ |
|||||||
|
Subproject commit 1e36559cffa5ab2fb755feef563c4294a6f32b0c |
@ -0,0 +1,61 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Immutable; |
||||||
|
using Silk.NET.Maths; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel; |
||||||
|
|
||||||
|
public enum BlockFacing |
||||||
|
{ |
||||||
|
East, // +X |
||||||
|
West, // -X |
||||||
|
Up, // +Y |
||||||
|
Down, // -Y |
||||||
|
South, // +Z |
||||||
|
North, // -Z |
||||||
|
} |
||||||
|
|
||||||
|
public static class BlockFacings |
||||||
|
{ |
||||||
|
public static readonly ImmutableHashSet<BlockFacing> Horizontals |
||||||
|
= ImmutableHashSet.Create(BlockFacing.East , BlockFacing.West , |
||||||
|
BlockFacing.South, BlockFacing.North); |
||||||
|
|
||||||
|
public static readonly ImmutableHashSet<BlockFacing> Verticals |
||||||
|
= ImmutableHashSet.Create(BlockFacing.Up, BlockFacing.Down); |
||||||
|
|
||||||
|
public static readonly ImmutableHashSet<BlockFacing> All |
||||||
|
= Horizontals.Union(Verticals); |
||||||
|
} |
||||||
|
|
||||||
|
public static class BlockFacingExtensions |
||||||
|
{ |
||||||
|
public static void Deconstruct(this BlockFacing self, out int x, out int y, out int z) |
||||||
|
=> (x, y, z) = self switch { |
||||||
|
BlockFacing.East => (+1, 0, 0), |
||||||
|
BlockFacing.West => (-1, 0, 0), |
||||||
|
BlockFacing.Up => ( 0, +1, 0), |
||||||
|
BlockFacing.Down => ( 0, -1, 0), |
||||||
|
BlockFacing.South => ( 0, 0, +1), |
||||||
|
BlockFacing.North => ( 0, 0, -1), |
||||||
|
_ => throw new ArgumentException( |
||||||
|
$"'{self}' is not a valid BlockFacing", nameof(self)) |
||||||
|
}; |
||||||
|
|
||||||
|
public static bool IsValid(this BlockFacing self) |
||||||
|
=> (self >= BlockFacing.East) && (self <= BlockFacing.North); |
||||||
|
|
||||||
|
public static BlockFacing GetOpposite(this BlockFacing self) |
||||||
|
=> (BlockFacing)((int)self ^ 0b1); |
||||||
|
|
||||||
|
public static Vector3D<float> ToVector3(this BlockFacing self) |
||||||
|
=> self switch { |
||||||
|
BlockFacing.East => Vector3D<float>.UnitX, |
||||||
|
BlockFacing.West => -Vector3D<float>.UnitX, |
||||||
|
BlockFacing.Up => Vector3D<float>.UnitY, |
||||||
|
BlockFacing.Down => -Vector3D<float>.UnitY, |
||||||
|
BlockFacing.South => Vector3D<float>.UnitZ, |
||||||
|
BlockFacing.North => -Vector3D<float>.UnitZ, |
||||||
|
_ => throw new ArgumentException( |
||||||
|
$"'{self}' is not a valid BlockFacing", nameof(self)) |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,75 @@ |
|||||||
|
using System; |
||||||
|
using Silk.NET.Maths; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel; |
||||||
|
|
||||||
|
public readonly struct BlockPos |
||||||
|
: IEquatable<BlockPos> |
||||||
|
{ |
||||||
|
public static readonly BlockPos Origin = default; |
||||||
|
|
||||||
|
public int X { get; } |
||||||
|
public int Y { get; } |
||||||
|
public int Z { get; } |
||||||
|
|
||||||
|
public BlockPos(int x, int y, int z) => (X, Y, Z) = (x, y, z); |
||||||
|
public void Deconstruct(out int x, out int y, out int z) => (x, y, z) = (X, Y, Z); |
||||||
|
|
||||||
|
public Vector3D<float> GetOrigin() => new(X, Y, Z); |
||||||
|
public Vector3D<float> GetCenter() => new(X + 0.5F, Y + 0.5F, Z + 0.5F); |
||||||
|
|
||||||
|
|
||||||
|
public BlockPos Add(int x, int y, int z) => new(X + x, Y + y, Z + z); |
||||||
|
public BlockPos Add(in BlockPos other) => new(X + other.X, Y + other.Y, Z + other.Z); |
||||||
|
|
||||||
|
public BlockPos Add(BlockFacing facing) |
||||||
|
{ var (x, y, z) = facing; return Add(x, y, z); } |
||||||
|
public BlockPos Add(BlockFacing facing, int factor) |
||||||
|
{ var (x, y, z) = facing; return Add(x * factor, y * factor, z * factor); } |
||||||
|
|
||||||
|
public BlockPos Add(Neighbor neighbor) |
||||||
|
{ var (x, y, z) = neighbor; return Add(x, y, z); } |
||||||
|
public BlockPos Add(Neighbor neighor, int factor) |
||||||
|
{ var (x, y, z) = neighor; return Add(x * factor, y * factor, z * factor); } |
||||||
|
|
||||||
|
|
||||||
|
public BlockPos Subtract(int x, int y, int z) => new(X - x, Y - y, Z - z); |
||||||
|
public BlockPos Subtract(in BlockPos other) => new(X - other.X, Y - other.Y, Z - other.Z); |
||||||
|
|
||||||
|
public BlockPos Subtract(BlockFacing facing) |
||||||
|
{ var (x, y, z) = facing; return Subtract(x, y, z); } |
||||||
|
public BlockPos Subtract(BlockFacing facing, int factor) |
||||||
|
{ var (x, y, z) = facing; return Subtract(x * factor, y * factor, z * factor); } |
||||||
|
|
||||||
|
public BlockPos Subtract(Neighbor neighbor) |
||||||
|
{ var (x, y, z) = neighbor; return Subtract(x, y, z); } |
||||||
|
public BlockPos Subtract(Neighbor neighor, int factor) |
||||||
|
{ var (x, y, z) = neighor; return Subtract(x * factor, y * factor, z * factor); } |
||||||
|
|
||||||
|
|
||||||
|
public bool Equals(BlockPos other) |
||||||
|
=> (X == other.X) && (Y == other.Y) && (Z == other.Z); |
||||||
|
public override bool Equals(object? obj) |
||||||
|
=> (obj is BlockPos pos) && Equals(pos); |
||||||
|
|
||||||
|
public override int GetHashCode() => HashCode.Combine(X, Y, Z); |
||||||
|
public override string ToString() => $"BlockPos({X}:{Y}:{Z})"; |
||||||
|
public string ToShortString() => $"{X}:{Y}:{Z}"; |
||||||
|
|
||||||
|
|
||||||
|
public static BlockPos operator +(BlockPos left, BlockPos right) => left.Add(right); |
||||||
|
public static BlockPos operator -(BlockPos left, BlockPos right) => left.Subtract(right); |
||||||
|
public static BlockPos operator +(BlockPos left, BlockFacing right) => left.Add(right); |
||||||
|
public static BlockPos operator -(BlockPos left, BlockFacing right) => left.Subtract(right); |
||||||
|
public static BlockPos operator +(BlockPos left, Neighbor right) => left.Add(right); |
||||||
|
public static BlockPos operator -(BlockPos left, Neighbor right) => left.Subtract(right); |
||||||
|
|
||||||
|
public static bool operator ==(BlockPos left, BlockPos right) => left.Equals(right); |
||||||
|
public static bool operator !=(BlockPos left, BlockPos right) => !left.Equals(right); |
||||||
|
} |
||||||
|
|
||||||
|
public static class BlockPosExtensions |
||||||
|
{ |
||||||
|
public static BlockPos ToBlockPos(this Vector3D<float> self) |
||||||
|
=> new((int)MathF.Floor(self.X), (int)MathF.Floor(self.Y), (int)MathF.Floor(self.Z)); |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
using gaemstone.ECS; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel; |
||||||
|
|
||||||
|
[Component] |
||||||
|
public readonly struct Chunk |
||||||
|
{ |
||||||
|
// <summary> Length of the egde of a world chunk. </summary> |
||||||
|
public const int LENGTH = 16; |
||||||
|
// <summary> Amount of bit shifting to go from a BlockPos to a ChunkPos. </summary> |
||||||
|
public const int BIT_SHIFT = 4; |
||||||
|
// <summary> Amount of bit masking to go from a BlockPos to a chunk-relative BlockPos. </summary> |
||||||
|
public const int BIT_MASK = 0b1111; |
||||||
|
|
||||||
|
public ChunkPos Position { get; } |
||||||
|
public Chunk(ChunkPos pos) => Position = pos; |
||||||
|
} |
@ -0,0 +1,188 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using gaemstone.ECS; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel; |
||||||
|
|
||||||
|
// Based on "Palette-based compression for chunked discrete voxel data" by /u/Longor1996 |
||||||
|
// https://www.reddit.com/r/VoxelGameDev/comments/9yu8qy/palettebased_compression_for_chunked_discrete/ |
||||||
|
[Component] |
||||||
|
public class ChunkPaletteStorage<T> |
||||||
|
{ |
||||||
|
const int Size = 16 * 16 * 16; |
||||||
|
static readonly EqualityComparer<T> COMPARER |
||||||
|
= EqualityComparer<T>.Default; |
||||||
|
|
||||||
|
BitArray? _data; |
||||||
|
PaletteEntry[]? _palette; |
||||||
|
int _usedPalettes; |
||||||
|
int _indicesLength; |
||||||
|
|
||||||
|
|
||||||
|
public T Default { get; } |
||||||
|
|
||||||
|
public T this[int x, int y, int z] |
||||||
|
{ |
||||||
|
get => Get(x, y, z); |
||||||
|
set => Set(x, y, z, value); |
||||||
|
} |
||||||
|
|
||||||
|
public IEnumerable<T> Blocks |
||||||
|
=> _palette?.Where(entry => !COMPARER.Equals(entry.Value, default!)) |
||||||
|
.Select(entry => entry.Value!) |
||||||
|
?? Enumerable.Empty<T>(); |
||||||
|
|
||||||
|
|
||||||
|
public ChunkPaletteStorage(T @default) |
||||||
|
=> Default = @default; |
||||||
|
|
||||||
|
|
||||||
|
T Get(int x, int y, int z) |
||||||
|
{ |
||||||
|
if (_palette == null) return Default; |
||||||
|
var entry = _palette[GetPaletteIndex(x, y, z)]; |
||||||
|
return !COMPARER.Equals(entry.Value, default!) ? entry.Value : Default; |
||||||
|
} |
||||||
|
|
||||||
|
void Set(int x, int y, int z, T value) |
||||||
|
{ |
||||||
|
if (_palette == null) |
||||||
|
{ |
||||||
|
if (COMPARER.Equals(value, Default)) return; |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
var index = GetIndex(x, y, z); |
||||||
|
ref var current = ref _palette[GetPaletteIndex(index)]; |
||||||
|
if (COMPARER.Equals(value, current.Value)) return; |
||||||
|
|
||||||
|
if (--current.RefCount == 0) |
||||||
|
_usedPalettes--; |
||||||
|
|
||||||
|
var replace = Array.FindIndex(_palette, entry => COMPARER.Equals(value, entry.Value)); |
||||||
|
if (replace != -1) |
||||||
|
{ |
||||||
|
SetPaletteIndex(index, replace); |
||||||
|
_palette[replace].RefCount += 1; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (current.RefCount == 0) |
||||||
|
{ |
||||||
|
current.Value = value; |
||||||
|
current.RefCount = 1; |
||||||
|
_usedPalettes++; |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var newPaletteIndex = NewPaletteEntry(); |
||||||
|
_palette![newPaletteIndex] = new PaletteEntry { Value = value, RefCount = 1 }; |
||||||
|
SetPaletteIndex(x, y, z, newPaletteIndex); |
||||||
|
_usedPalettes++; |
||||||
|
} |
||||||
|
|
||||||
|
int NewPaletteEntry() |
||||||
|
{ |
||||||
|
if (_palette != null) |
||||||
|
{ |
||||||
|
int firstFree = Array.FindIndex(_palette, entry => |
||||||
|
entry.Value == null || entry.RefCount == 0); |
||||||
|
if (firstFree != -1) return firstFree; |
||||||
|
} |
||||||
|
|
||||||
|
GrowPalette(); |
||||||
|
return NewPaletteEntry(); |
||||||
|
} |
||||||
|
|
||||||
|
void GrowPalette() |
||||||
|
{ |
||||||
|
if (_palette == null) |
||||||
|
{ |
||||||
|
_data = new(Size); |
||||||
|
_palette = new PaletteEntry[2]; |
||||||
|
_usedPalettes = 1; |
||||||
|
_indicesLength = 1; |
||||||
|
_palette[0] = new PaletteEntry { Value = Default, RefCount = Size }; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
_indicesLength <<= 1; |
||||||
|
|
||||||
|
var oldIndicesLength = _indicesLength >> 1; |
||||||
|
var newData = new BitArray(Size * _indicesLength); |
||||||
|
for (var i = 0; i < Size; i++) |
||||||
|
for (var j = 0; j < oldIndicesLength; j++) |
||||||
|
newData.Set(i * _indicesLength + j, _data!.Get(i * oldIndicesLength + j)); |
||||||
|
_data = newData; |
||||||
|
|
||||||
|
Array.Resize(ref _palette, 1 << _indicesLength); |
||||||
|
} |
||||||
|
|
||||||
|
// public void FitPalette() { |
||||||
|
// if (_usedPalettes > Mathf.NearestPo2(_usedPalettes) / 2) return; |
||||||
|
|
||||||
|
// // decode all indices |
||||||
|
// int[] indices = new int[size]; |
||||||
|
// for(int i = 0; i < indices.length; i++) { |
||||||
|
// indices[i] = data.get(i * indicesLength, indicesLength); |
||||||
|
// } |
||||||
|
|
||||||
|
// // Create new palette, halfing it in size |
||||||
|
// indicesLength = indicesLength >> 1; |
||||||
|
// PaletteEntry[] newPalette = new PaletteEntry[2 pow indicesLength]; |
||||||
|
|
||||||
|
// // We gotta compress the palette entries! |
||||||
|
// int paletteCounter = 0; |
||||||
|
// for(int pi = 0; pi < palette.length; pi++, paletteCounter++) { |
||||||
|
// PaletteEntry entry = newPalette[paletteCounter] = palette[pi]; |
||||||
|
|
||||||
|
// // Re-encode the indices (find and replace; with limit) |
||||||
|
// for(int di = 0, fc = 0; di < indices.length && fc < entry.refcount; di++) { |
||||||
|
// if(pi == indices[di]) { |
||||||
|
// indices[di] = paletteCounter; |
||||||
|
// fc += 1; |
||||||
|
// } |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
// // Allocate new BitBuffer |
||||||
|
// data = new BitBuffer(size * indicesLength); // the length is in bits, not bytes! |
||||||
|
|
||||||
|
// // Encode the indices |
||||||
|
// for(int i = 0; i < indices.length; i++) { |
||||||
|
// data.set(i * indicesLength, indicesLength, indices[i]); |
||||||
|
// } |
||||||
|
// } |
||||||
|
|
||||||
|
|
||||||
|
int GetPaletteIndex(int x, int y, int z) |
||||||
|
=> GetPaletteIndex(GetIndex(x, y, z)); |
||||||
|
int GetPaletteIndex(int index) |
||||||
|
{ |
||||||
|
var paletteIndex = 0; |
||||||
|
for (var i = 0; i < _indicesLength; i++) |
||||||
|
paletteIndex |= (_data!.Get(index + i) ? 1 : 0) << i; |
||||||
|
return paletteIndex; |
||||||
|
} |
||||||
|
|
||||||
|
void SetPaletteIndex(int x, int y, int z, int paletteIndex) |
||||||
|
=> SetPaletteIndex(GetIndex(x, y, z), paletteIndex); |
||||||
|
void SetPaletteIndex(int index, int paletteIndex) |
||||||
|
{ |
||||||
|
for (var i = 0; i < _indicesLength; i++) |
||||||
|
_data!.Set(index + i, (paletteIndex >> i & 0b1) == 0b1); |
||||||
|
} |
||||||
|
|
||||||
|
int GetIndex(int x, int y, int z) |
||||||
|
=> (x | y << 4 | z << 8) * _indicesLength; |
||||||
|
|
||||||
|
|
||||||
|
struct PaletteEntry |
||||||
|
{ |
||||||
|
public T Value { get; set; } |
||||||
|
public int RefCount { get; set; } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
using System; |
||||||
|
using Silk.NET.Maths; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel; |
||||||
|
|
||||||
|
public readonly struct ChunkPos |
||||||
|
: IEquatable<ChunkPos> |
||||||
|
{ |
||||||
|
public static readonly ChunkPos ORIGIN = new(0, 0, 0); |
||||||
|
|
||||||
|
public int X { get; } |
||||||
|
public int Y { get; } |
||||||
|
public int Z { get; } |
||||||
|
|
||||||
|
public ChunkPos(int x, int y, int z) => (X, Y, Z) = (x, y, z); |
||||||
|
public void Deconstruct(out int x, out int y, out int z) => (x, y, z) = (X, Y, Z); |
||||||
|
|
||||||
|
public Vector3D<float> GetOrigin() => new( |
||||||
|
X << Chunk.BIT_SHIFT, Y << Chunk.BIT_SHIFT, Z << Chunk.BIT_SHIFT); |
||||||
|
public Vector3D<float> GetCenter() => new( |
||||||
|
(X << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2, |
||||||
|
(Y << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2, |
||||||
|
(Z << Chunk.BIT_SHIFT) + Chunk.LENGTH / 2); |
||||||
|
|
||||||
|
|
||||||
|
public ChunkPos Add(int x, int y, int z) |
||||||
|
=> new(X + x, Y + y, Z + z); |
||||||
|
public ChunkPos Add(in ChunkPos other) |
||||||
|
=> new(X + other.X, Y + other.Y, Z + other.Z); |
||||||
|
public ChunkPos Add(BlockFacing facing) |
||||||
|
{ var (x, y, z) = facing; return Add(x, y, z); } |
||||||
|
public ChunkPos Add(Neighbor neighbor) |
||||||
|
{ var (x, y, z) = neighbor; return Add(x, y, z); } |
||||||
|
|
||||||
|
public ChunkPos Subtract(int x, int y, int z) |
||||||
|
=> new(X - x, Y - y, Z - z); |
||||||
|
public ChunkPos Subtract(in ChunkPos other) |
||||||
|
=> new(X - other.X, Y - other.Y, Z - other.Z); |
||||||
|
public ChunkPos Subtract(BlockFacing facing) |
||||||
|
{ var (x, y, z) = facing; return Subtract(x, y, z); } |
||||||
|
public ChunkPos Subtract(Neighbor neighbor) |
||||||
|
{ var (x, y, z) = neighbor; return Subtract(x, y, z); } |
||||||
|
|
||||||
|
|
||||||
|
public bool Equals(ChunkPos other) |
||||||
|
=> (X == other.X) && (Y == other.Y) && (Z == other.Z); |
||||||
|
public override bool Equals(object? obj) |
||||||
|
=> (obj is ChunkPos pos) && Equals(pos); |
||||||
|
|
||||||
|
public override int GetHashCode() => HashCode.Combine(X, Y, Z); |
||||||
|
public override string ToString() => $"ChunkPos ({X}:{Y}:{Z})"; |
||||||
|
public string ToShortString() => $"{X}:{Y}:{Z}"; |
||||||
|
|
||||||
|
|
||||||
|
public static ChunkPos operator +(ChunkPos left, ChunkPos right) => left.Add(right); |
||||||
|
public static ChunkPos operator -(ChunkPos left, ChunkPos right) => left.Subtract(right); |
||||||
|
public static ChunkPos operator +(ChunkPos left, BlockFacing right) => left.Add(right); |
||||||
|
public static ChunkPos operator -(ChunkPos left, BlockFacing right) => left.Subtract(right); |
||||||
|
public static ChunkPos operator +(ChunkPos left, Neighbor right) => left.Add(right); |
||||||
|
public static ChunkPos operator -(ChunkPos left, Neighbor right) => left.Subtract(right); |
||||||
|
|
||||||
|
public static bool operator ==(ChunkPos left, ChunkPos right) => left.Equals(right); |
||||||
|
public static bool operator !=(ChunkPos left, ChunkPos right) => !left.Equals(right); |
||||||
|
} |
||||||
|
|
||||||
|
public static class ChunkPosExtensions |
||||||
|
{ |
||||||
|
public static ChunkPos ToChunkPos(this Vector3D<float> pos) => new( |
||||||
|
(int)MathF.Floor(pos.X) >> Chunk.BIT_SHIFT, |
||||||
|
(int)MathF.Floor(pos.Y) >> Chunk.BIT_SHIFT, |
||||||
|
(int)MathF.Floor(pos.Z) >> Chunk.BIT_SHIFT); |
||||||
|
|
||||||
|
public static ChunkPos ToChunkPos(this BlockPos self) => new( |
||||||
|
self.X >> Chunk.BIT_SHIFT, self.Y >> Chunk.BIT_SHIFT, self.Z >> Chunk.BIT_SHIFT); |
||||||
|
public static BlockPos ToChunkRelative(this BlockPos self) => new( |
||||||
|
self.X & Chunk.BIT_MASK, self.Y & Chunk.BIT_MASK, self.Z & Chunk.BIT_MASK); |
||||||
|
public static BlockPos ToChunkRelative(this BlockPos self, ChunkPos chunk) => new( |
||||||
|
self.X - (chunk.X << Chunk.BIT_SHIFT), |
||||||
|
self.Y - (chunk.Y << Chunk.BIT_SHIFT), |
||||||
|
self.Z - (chunk.Z << Chunk.BIT_SHIFT)); |
||||||
|
} |
@ -0,0 +1,125 @@ |
|||||||
|
using System; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using gaemstone.Client; |
||||||
|
using gaemstone.ECS; |
||||||
|
using Silk.NET.Maths; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
using static gaemstone.Bloxel.WorldGen.BasicWorldGenerator; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel.Client; |
||||||
|
|
||||||
|
[Module] |
||||||
|
public class ChunkMeshGenerator |
||||||
|
{ |
||||||
|
private const int StartingCapacity = 1024; |
||||||
|
|
||||||
|
private static readonly Vector3D<float>[][] OffsetPerFacing = { |
||||||
|
new Vector3D<float>[]{ new(1,1,1), new(1,0,1), new(1,0,0), new(1,1,0) }, // East (+X) |
||||||
|
new Vector3D<float>[]{ new(0,1,0), new(0,0,0), new(0,0,1), new(0,1,1) }, // West (-X) |
||||||
|
new Vector3D<float>[]{ new(1,1,0), new(0,1,0), new(0,1,1), new(1,1,1) }, // Up (+Y) |
||||||
|
new Vector3D<float>[]{ new(1,0,1), new(0,0,1), new(0,0,0), new(1,0,0) }, // Down (-Y) |
||||||
|
new Vector3D<float>[]{ new(0,1,1), new(0,0,1), new(1,0,1), new(1,1,1) }, // South (+Z) |
||||||
|
new Vector3D<float>[]{ new(1,1,0), new(1,0,0), new(0,0,0), new(0,1,0) } // North (-Z) |
||||||
|
}; |
||||||
|
|
||||||
|
private static readonly int[] TriangleIndices |
||||||
|
= { 0, 1, 3, 1, 2, 3 }; |
||||||
|
|
||||||
|
|
||||||
|
private ushort[] _indices = new ushort[StartingCapacity]; |
||||||
|
private Vector3D<float>[] _vertices = new Vector3D<float>[StartingCapacity]; |
||||||
|
private Vector3D<float>[] _normals = new Vector3D<float>[StartingCapacity]; |
||||||
|
private Vector2D<float>[] _uvs = new Vector2D<float>[StartingCapacity]; |
||||||
|
|
||||||
|
[System] |
||||||
|
public void GenerateChunkMeshes(Universe universe, Entity entity, |
||||||
|
in Chunk chunk, ChunkPaletteStorage<ecs_entity_t> storage, |
||||||
|
HasBasicWorldGeneration _1, [Not] Mesh _2) |
||||||
|
{ |
||||||
|
var mesh = Generate(universe, chunk.Position, storage); |
||||||
|
if (mesh is Mesh m) entity.Set(m); |
||||||
|
else entity.Delete(); |
||||||
|
} |
||||||
|
|
||||||
|
public Mesh? Generate(Universe universe, ChunkPos chunkPos, |
||||||
|
ChunkPaletteStorage<ecs_entity_t> centerStorage) |
||||||
|
{ |
||||||
|
// TODO: We'll need a way to get neighbors again. |
||||||
|
// var storages = new ChunkPaletteStorage<ecs_entity_t>[3, 3, 3]; |
||||||
|
// foreach (var (x, y, z) in Neighbors.ALL.Prepend(Neighbor.None)) |
||||||
|
// if (_chunkStore.TryGetEntityID(chunkPos.Add(x, y, z), out var neighborID)) |
||||||
|
// if (_storageStore.TryGet(neighborID, out var storage)) |
||||||
|
// storages[x+1, y+1, z+1] = storage; |
||||||
|
// var centerStorage = storages[1, 1, 1]; |
||||||
|
|
||||||
|
var storages = new ChunkPaletteStorage<ecs_entity_t>[3, 3, 3]; |
||||||
|
storages[1, 1, 1] = centerStorage; |
||||||
|
|
||||||
|
var indexCount = 0; |
||||||
|
var vertexCount = 0; |
||||||
|
for (var x = 0; x < 16; x++) |
||||||
|
for (var y = 0; y < 16; y++) |
||||||
|
for (var z = 0; z < 16; z++) { |
||||||
|
var block = new Entity(universe, centerStorage[x, y, z]); |
||||||
|
if (block.IsNone) continue; |
||||||
|
|
||||||
|
var blockVertex = new Vector3D<float>(x, y, z); |
||||||
|
var textureCell = block.Get<TextureCoords4>(); |
||||||
|
|
||||||
|
foreach (var facing in BlockFacings.All) { |
||||||
|
if (!IsNeighborEmpty(storages, x, y, z, facing)) continue; |
||||||
|
|
||||||
|
if (_indices.Length <= indexCount + 6) |
||||||
|
Array.Resize(ref _indices, _indices.Length << 1); |
||||||
|
if (_vertices.Length <= vertexCount + 4) { |
||||||
|
Array.Resize(ref _vertices, _vertices.Length << 1); |
||||||
|
Array.Resize(ref _normals , _vertices.Length << 1); |
||||||
|
Array.Resize(ref _uvs , _vertices.Length << 1); |
||||||
|
} |
||||||
|
|
||||||
|
for (var i = 0; i < TriangleIndices.Length; i++) |
||||||
|
_indices[indexCount++] = (ushort)(vertexCount + TriangleIndices[i]); |
||||||
|
|
||||||
|
var normal = facing.ToVector3(); |
||||||
|
for (var i = 0; i < 4; i++) { |
||||||
|
var offset = OffsetPerFacing[(int)facing][i]; |
||||||
|
_vertices[vertexCount] = blockVertex + offset; |
||||||
|
_normals[vertexCount] = normal; |
||||||
|
_uvs[vertexCount] = i switch { |
||||||
|
0 => textureCell.TopLeft, |
||||||
|
1 => textureCell.BottomLeft, |
||||||
|
2 => textureCell.BottomRight, |
||||||
|
3 => textureCell.TopRight, |
||||||
|
_ => throw new InvalidOperationException() |
||||||
|
}; |
||||||
|
vertexCount++; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return (indexCount > 0) |
||||||
|
? MeshManager.Create(universe, |
||||||
|
_indices.AsSpan(0, indexCount), _vertices.AsSpan(0, vertexCount), |
||||||
|
_normals.AsSpan(0, vertexCount), _uvs.AsSpan(0, vertexCount)) |
||||||
|
: null; |
||||||
|
} |
||||||
|
|
||||||
|
static bool IsNeighborEmpty( |
||||||
|
ChunkPaletteStorage<ecs_entity_t>[,,] storages, |
||||||
|
int x, int y, int z, BlockFacing facing) |
||||||
|
{ |
||||||
|
var cx = 1; var cy = 1; var cz = 1; |
||||||
|
switch (facing) { |
||||||
|
case BlockFacing.East : x += 1; if (x >= 16) cx += 1; break; |
||||||
|
case BlockFacing.West : x -= 1; if (x < 0) cx -= 1; break; |
||||||
|
case BlockFacing.Up : y += 1; if (y >= 16) cy += 1; break; |
||||||
|
case BlockFacing.Down : y -= 1; if (y < 0) cy -= 1; break; |
||||||
|
case BlockFacing.South : z += 1; if (z >= 16) cz += 1; break; |
||||||
|
case BlockFacing.North : z -= 1; if (z < 0) cz -= 1; break; |
||||||
|
} |
||||||
|
var neighborChunk = storages[cx, cy, cz]; |
||||||
|
if (neighborChunk == null) return true; |
||||||
|
var neighborBlock = neighborChunk[x & 0b1111, y & 0b1111, z & 0b1111]; |
||||||
|
return neighborBlock.Data.Data == 0; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,205 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Immutable; |
||||||
|
using System.Text; |
||||||
|
using Silk.NET.Maths; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel; |
||||||
|
|
||||||
|
[Flags] |
||||||
|
public enum Neighbor : byte |
||||||
|
{ |
||||||
|
None = 0, |
||||||
|
|
||||||
|
// FACINGS |
||||||
|
East = 0b000011, // +X |
||||||
|
West = 0b000010, // -X |
||||||
|
Up = 0b001100, // +Y |
||||||
|
Down = 0b001000, // -Y |
||||||
|
South = 0b110000, // +Z |
||||||
|
North = 0b100000, // -Z |
||||||
|
|
||||||
|
// CARDINALS |
||||||
|
SouthEast = South | East, // +X +Z |
||||||
|
SouthWest = South | West, // -X +Z |
||||||
|
NorthEast = North | East, // +X -Z |
||||||
|
NorthWest = North | West, // -X -Z |
||||||
|
|
||||||
|
// ALL_AXIS_PLANES |
||||||
|
UpEast = Up | East , // +X +Y |
||||||
|
UpWest = Up | West , // -X +Y |
||||||
|
UpSouth = Up | South, // +Z +Y |
||||||
|
UpNorth = Up | North, // -Z +Y |
||||||
|
|
||||||
|
DownEast = Down | East , // +X -Y |
||||||
|
DownWest = Down | West , // -X -Y |
||||||
|
DownSouth = Down | South, // +Z -Y |
||||||
|
DownNorth = Down | North, // -Z -Y |
||||||
|
|
||||||
|
// ALL |
||||||
|
UpSouthEast = Up | South | East, // +X +Y +Z |
||||||
|
UpSouthWest = Up | South | West, // -X +Y +Z |
||||||
|
UpNorthEast = Up | North | East, // +X +Y -Z |
||||||
|
UpNorthWest = Up | North | West, // -X +Y -Z |
||||||
|
|
||||||
|
DownSouthEast = Down | South | East, // +X -Y +Z |
||||||
|
DownSouthWest = Down | South | West, // -X -Y +Z |
||||||
|
DownNorthEast = Down | North | East, // +X -Y -Z |
||||||
|
DownNorthWest = Down | North | West, // -X -Y -Z |
||||||
|
} |
||||||
|
|
||||||
|
public static class Neighbors |
||||||
|
{ |
||||||
|
public static readonly ImmutableHashSet<Neighbor> Horizontals |
||||||
|
= ImmutableHashSet.Create(Neighbor.East , Neighbor.West , |
||||||
|
Neighbor.South, Neighbor.North); |
||||||
|
|
||||||
|
public static readonly ImmutableHashSet<Neighbor> Verticals |
||||||
|
= ImmutableHashSet.Create(Neighbor.Up, Neighbor.Down); |
||||||
|
|
||||||
|
public static readonly ImmutableHashSet<Neighbor> Facings |
||||||
|
= Horizontals.Union(Verticals); |
||||||
|
|
||||||
|
public static readonly ImmutableHashSet<Neighbor> Cardinals |
||||||
|
= Horizontals.Union(new[] { |
||||||
|
Neighbor.SouthEast, Neighbor.SouthWest, |
||||||
|
Neighbor.NorthEast, Neighbor.NorthWest }); |
||||||
|
|
||||||
|
public static readonly ImmutableHashSet<Neighbor> AllAxisPlanes |
||||||
|
= Facings.Union(new[] { |
||||||
|
Neighbor.SouthEast, Neighbor.SouthWest, |
||||||
|
Neighbor.NorthEast, Neighbor.NorthWest, |
||||||
|
Neighbor.UpEast , Neighbor.UpWest , |
||||||
|
Neighbor.UpSouth , Neighbor.UpNorth , |
||||||
|
Neighbor.DownEast , Neighbor.DownWest , |
||||||
|
Neighbor.DownSouth, Neighbor.DownNorth }); |
||||||
|
|
||||||
|
public static readonly ImmutableHashSet<Neighbor> All |
||||||
|
= AllAxisPlanes.Union(new[] { |
||||||
|
Neighbor.UpSouthEast, Neighbor.UpSouthWest, |
||||||
|
Neighbor.UpNorthEast, Neighbor.UpNorthWest, |
||||||
|
Neighbor.DownSouthEast, Neighbor.DownSouthWest, |
||||||
|
Neighbor.DownNorthEast, Neighbor.DownNorthWest }); |
||||||
|
} |
||||||
|
|
||||||
|
public static class NeighborExtensions |
||||||
|
{ |
||||||
|
const int SetBitX = 0b000010, ValueBitX = 0b000001; |
||||||
|
const int SetBitY = 0b001000, ValueBitY = 0b000100; |
||||||
|
const int SetBitZ = 0b100000, ValueBitZ = 0b010000; |
||||||
|
public static void Deconstruct(this Neighbor self, out int x, out int y, out int z) |
||||||
|
{ |
||||||
|
x = (((int)self & SetBitX) != 0) ? ((((int)self & ValueBitX) != 0) ? 1 : -1) : 0; |
||||||
|
y = (((int)self & SetBitY) != 0) ? ((((int)self & ValueBitY) != 0) ? 1 : -1) : 0; |
||||||
|
z = (((int)self & SetBitZ) != 0) ? ((((int)self & ValueBitZ) != 0) ? 1 : -1) : 0; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// public static Neighbor ToNeighbor(this Axis self, int v) |
||||||
|
// { |
||||||
|
// if ((v < -1) || (v > 1)) throw new ArgumentOutOfRangeException( |
||||||
|
// nameof(v), v, $"{nameof(v)} (={v}) must be within (-1, 1)"); |
||||||
|
// return self switch { |
||||||
|
// Axis.X => (v > 0) ? Neighbor.East : Neighbor.West , |
||||||
|
// Axis.Y => (v > 0) ? Neighbor.Up : Neighbor.Down , |
||||||
|
// Axis.Z => (v > 0) ? Neighbor.South : Neighbor.North, |
||||||
|
// _ => Neighbor.None |
||||||
|
// }; |
||||||
|
// } |
||||||
|
|
||||||
|
// public static Axis GetAxis(this Neighbor self) |
||||||
|
// => self switch { |
||||||
|
// Neighbor.East => Axis.X, |
||||||
|
// Neighbor.West => Axis.X, |
||||||
|
// Neighbor.Up => Axis.Y, |
||||||
|
// Neighbor.Down => Axis.Y, |
||||||
|
// Neighbor.South => Axis.Z, |
||||||
|
// Neighbor.North => Axis.Z, |
||||||
|
// _ => throw new ArgumentException(nameof(self), $"{self} is not one of FACINGS") |
||||||
|
// }; |
||||||
|
|
||||||
|
|
||||||
|
public static Neighbor ToNeighbor(this BlockFacing self) |
||||||
|
=> self switch { |
||||||
|
BlockFacing.East => Neighbor.East , |
||||||
|
BlockFacing.West => Neighbor.West , |
||||||
|
BlockFacing.Up => Neighbor.Up , |
||||||
|
BlockFacing.Down => Neighbor.Down , |
||||||
|
BlockFacing.South => Neighbor.South, |
||||||
|
BlockFacing.North => Neighbor.North, |
||||||
|
_ => throw new ArgumentException( |
||||||
|
$"'{self}' is not a valid BlockFacing", nameof(self)) |
||||||
|
}; |
||||||
|
|
||||||
|
public static BlockFacing ToBlockFacing(this Neighbor self) |
||||||
|
=> self switch { |
||||||
|
Neighbor.East => BlockFacing.East , |
||||||
|
Neighbor.West => BlockFacing.West , |
||||||
|
Neighbor.Up => BlockFacing.Up , |
||||||
|
Neighbor.Down => BlockFacing.Down , |
||||||
|
Neighbor.South => BlockFacing.South, |
||||||
|
Neighbor.North => BlockFacing.North, |
||||||
|
_ => throw new ArgumentException( |
||||||
|
$"'{self}' can't be converted to a valid BlockFacing", nameof(self)) |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
public static Neighbor ToNeighbor(this (int x, int y, int z) p) |
||||||
|
{ |
||||||
|
var neighbor = Neighbor.None; |
||||||
|
if (p.x != 0) { |
||||||
|
if (p.x == 1) neighbor |= Neighbor.East; |
||||||
|
else if (p.x == -1) neighbor |= Neighbor.West; |
||||||
|
else throw new ArgumentOutOfRangeException( |
||||||
|
nameof(p), p.x, $"{nameof(p)}.x (={p.x}) must be within (-1, 1)"); |
||||||
|
} |
||||||
|
if (p.y != 0) { |
||||||
|
if (p.y == 1) neighbor |= Neighbor.Up; |
||||||
|
else if (p.y == -1) neighbor |= Neighbor.Down; |
||||||
|
else throw new ArgumentOutOfRangeException( |
||||||
|
nameof(p), p.y, $"{nameof(p)}.y (={p.y}) must be within (-1, 1)"); |
||||||
|
} |
||||||
|
if (p.z != 0) { |
||||||
|
if (p.z == 1) neighbor |= Neighbor.South; |
||||||
|
else if (p.z == -1) neighbor |= Neighbor.North; |
||||||
|
else throw new ArgumentOutOfRangeException( |
||||||
|
nameof(p), p.z, $"{nameof(p)}.z (={p.z}) must be within (-1, 1)"); |
||||||
|
} |
||||||
|
return neighbor; |
||||||
|
} |
||||||
|
|
||||||
|
public static Neighbor GetOpposite(this Neighbor self) |
||||||
|
{ var (x, y, z) = self; return (-x, -y, -z).ToNeighbor(); } |
||||||
|
|
||||||
|
|
||||||
|
public static BlockPos ToProperPos(this Neighbor self) |
||||||
|
{ var (x, y, z) = self; return new(x, y, z); } |
||||||
|
|
||||||
|
public static Vector3D<float> ToVector3(this Neighbor self) |
||||||
|
{ var (x, y, z) = self; return new(x, y, z); } |
||||||
|
|
||||||
|
|
||||||
|
public static bool IsNone(this Neighbor self) |
||||||
|
=> (self == Neighbor.None); |
||||||
|
|
||||||
|
public static bool IsHorizontal(this Neighbor self) |
||||||
|
=> Neighbors.Horizontals.Contains(self); |
||||||
|
public static bool IsVertical(this Neighbor self) |
||||||
|
=> Neighbors.Verticals.Contains(self); |
||||||
|
public static bool IsCardinal(this Neighbor self) |
||||||
|
=> Neighbors.Cardinals.Contains(self); |
||||||
|
public static bool IsFacing(this Neighbor self) |
||||||
|
=> Neighbors.Facings.Contains(self); |
||||||
|
public static bool IsValid(this Neighbor self) |
||||||
|
=> Neighbors.All.Contains(self); |
||||||
|
|
||||||
|
|
||||||
|
public static string ToShortString(this Neighbor self) |
||||||
|
{ |
||||||
|
if (!self.IsValid()) return "-"; |
||||||
|
var sb = new StringBuilder(3); |
||||||
|
foreach (var chr in self.ToString()) |
||||||
|
if ((chr >= 'A') && (chr <= 'Z')) // ASCII IsUpper |
||||||
|
sb.Append(chr + 0x20); // ASCII ToLower |
||||||
|
return sb.ToString(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,135 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections; |
||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel.Utility; |
||||||
|
|
||||||
|
public class ChunkedOctree<T> |
||||||
|
where T : struct
|
||||||
|
{ |
||||||
|
public delegate void UpdateAction(int level, ReadOnlySpan<T> children, ref T parent); |
||||||
|
public delegate float? WeightFunc(int level, ZOrder pos, T value); |
||||||
|
|
||||||
|
private static readonly int[] START_INDEX_LOOKUP = { |
||||||
|
0, 1, 9, 73, 585, 4681, 37449, 299593, 2396745, 19173961, 153391689 }; |
||||||
|
|
||||||
|
|
||||||
|
private readonly IEqualityComparer<T> _comparer = EqualityComparer<T>.Default; |
||||||
|
private readonly Dictionary<ZOrder, T[]> _regions = new(); |
||||||
|
|
||||||
|
public int Depth { get; } |
||||||
|
|
||||||
|
public ChunkedOctree(int depth) |
||||||
|
{ |
||||||
|
if (depth < 1) throw new ArgumentOutOfRangeException(nameof(depth), |
||||||
|
$"{nameof(depth)} must be larger than 0"); |
||||||
|
if (depth >= START_INDEX_LOOKUP.Length) throw new ArgumentOutOfRangeException(nameof(depth), |
||||||
|
$"{nameof(depth)} must be smaller than {START_INDEX_LOOKUP.Length}"); |
||||||
|
Depth = depth; |
||||||
|
} |
||||||
|
|
||||||
|
public T Get(ChunkPos pos) |
||||||
|
=> Get(0, new(pos.X, pos.Y, pos.Z)); |
||||||
|
public T Get(int level, ZOrder pos) |
||||||
|
{ |
||||||
|
var region = _regions.GetValueOrDefault(pos >> Depth - level); |
||||||
|
if (region == null) return default; |
||||||
|
var localPos = pos & ~(~0L << (Depth - level) * 3); |
||||||
|
return region[GetIndex(level, localPos)]; |
||||||
|
} |
||||||
|
private int GetIndex(int level, ZOrder localPos) |
||||||
|
=> START_INDEX_LOOKUP[Depth - level] + (int)localPos.Raw; |
||||||
|
|
||||||
|
public void Update(ChunkPos pos, UpdateAction update) |
||||||
|
{ |
||||||
|
var zPos = new ZOrder(pos.X, pos.Y, pos.Z); |
||||||
|
var localPos = zPos & ~(~0L << Depth * 3); |
||||||
|
var regionPos = zPos >> Depth; |
||||||
|
|
||||||
|
if (!_regions.TryGetValue(regionPos, out var region)) |
||||||
|
_regions.Add(regionPos, region = new T[START_INDEX_LOOKUP[Depth + 1] + 1]); |
||||||
|
|
||||||
|
var children = default(ReadOnlySpan<T>); |
||||||
|
for (var level = 0; level <= Depth; level++) |
||||||
|
{ |
||||||
|
var index = GetIndex(level, localPos); |
||||||
|
|
||||||
|
var previous = region[index]; |
||||||
|
update(0, children, ref region[index]); |
||||||
|
if (_comparer.Equals(region[index], previous)) return; |
||||||
|
|
||||||
|
if (level == Depth) return; |
||||||
|
children = region.AsSpan(GetIndex(level, localPos & ~0b111L), 8); |
||||||
|
localPos >>= 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public IEnumerable<(ChunkPos ChunkPos, T Value, float Weight)> Find( |
||||||
|
WeightFunc weight, params ChunkPos[] searchFrom) |
||||||
|
{ |
||||||
|
var enumerator = new Enumerator(this, weight); |
||||||
|
foreach (var pos in searchFrom) enumerator.SearchFrom(new(pos.X, pos.Y, pos.Z)); |
||||||
|
while (enumerator.MoveNext()) yield return enumerator.Current; |
||||||
|
} |
||||||
|
|
||||||
|
public class Enumerator |
||||||
|
: IEnumerator<(ChunkPos ChunkPos, T Value, float Weight)> |
||||||
|
{ |
||||||
|
private readonly ChunkedOctree<T> _octree; |
||||||
|
private readonly WeightFunc _weight; |
||||||
|
|
||||||
|
private readonly HashSet<ZOrder> _checkedRegions = new(); |
||||||
|
private readonly PriorityQueue<(int Level, ZOrder Pos, T Value), float> _processing = new(); |
||||||
|
private (ChunkPos ChunkPos, T Value, float Weight)? _current; |
||||||
|
|
||||||
|
internal Enumerator(ChunkedOctree<T> octree, WeightFunc weight) |
||||||
|
{ _octree = octree; _weight = weight; _current = null; } |
||||||
|
|
||||||
|
public (ChunkPos ChunkPos, T Value, float Weight) Current |
||||||
|
=> _current ?? throw new InvalidOperationException(); |
||||||
|
object IEnumerator.Current => Current; |
||||||
|
|
||||||
|
public bool MoveNext() |
||||||
|
{ |
||||||
|
while (_processing.TryDequeue(out var element, out var weight)) |
||||||
|
{ |
||||||
|
var (level, nodePos, value) = element; |
||||||
|
if (level == 0) |
||||||
|
{ |
||||||
|
_current = (new(nodePos.X, nodePos.Y, nodePos.Z), value, weight); |
||||||
|
return true; |
||||||
|
} |
||||||
|
else for (var i = 0b000; i <= 0b111; i++) |
||||||
|
PushNode(level - 1, nodePos << 1 | ZOrder.FromRaw(i)); |
||||||
|
} |
||||||
|
_current = null; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public void Reset() => throw new NotSupportedException(); |
||||||
|
public void Dispose() { } |
||||||
|
|
||||||
|
|
||||||
|
internal void SearchFrom(ZOrder nodePos) |
||||||
|
{ |
||||||
|
var regionPos = nodePos >> _octree.Depth; |
||||||
|
for (var x = -1; x <= 1; x++) |
||||||
|
for (var y = -1; y <= 1; y++) |
||||||
|
for (var z = -1; z <= 1; z++) |
||||||
|
SearchRegion(regionPos + new ZOrder(x, y, z)); |
||||||
|
} |
||||||
|
|
||||||
|
private void SearchRegion(ZOrder regionPos) |
||||||
|
{ |
||||||
|
if (_checkedRegions.Add(regionPos)) |
||||||
|
PushNode(_octree.Depth, regionPos); |
||||||
|
} |
||||||
|
|
||||||
|
private void PushNode(int level, ZOrder nodePos) |
||||||
|
{ |
||||||
|
var value = _octree.Get(level, nodePos); |
||||||
|
if (_weight(level, nodePos, value) is float weight) |
||||||
|
_processing.Enqueue((level, nodePos, value), weight); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,154 @@ |
|||||||
|
using System; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel.Utility; |
||||||
|
|
||||||
|
// This struct wraps a primitive integer which represents an index into a space-filling curve |
||||||
|
// called "Z-Order Curve" (https://en.wikipedia.org/wiki/Z-order_curve). Often, this is also |
||||||
|
// referred to as Morton order, code, or encoding. |
||||||
|
// |
||||||
|
// This implementation purely focuses on 3 dimensions. |
||||||
|
// |
||||||
|
// By interleaving the 3 sub-elements into a single integer, some amount of packing can be |
||||||
|
// achieved, at the loss of some bits per elements. For example, with a 64 bit integer, 21 |
||||||
|
// bits per elements are available (2_097_152 distinct values), which may be enough to |
||||||
|
// represent block coordinates in a bloxel game world. |
||||||
|
// |
||||||
|
// One upside of encoding separate coordinates into a single Z-Order index is that it can then |
||||||
|
// be effectively used to index into octrees, and certain operations such as bitwise shifting |
||||||
|
// are quite useful. |
||||||
|
public readonly struct ZOrder |
||||||
|
: IEquatable<ZOrder> |
||||||
|
, IComparable<ZOrder> |
||||||
|
{ |
||||||
|
public const int ELEMENT_MIN = ~0 << BITS_PER_ELEMENT - 1; |
||||||
|
public const int ELEMENT_MAX = ~ELEMENT_MIN; |
||||||
|
|
||||||
|
private const int BITS_SIZE = sizeof(long) * 8; |
||||||
|
private const int BITS_PER_ELEMENT = BITS_SIZE / 3; |
||||||
|
private const int MAX_USABLE_BITS = BITS_PER_ELEMENT * 3; |
||||||
|
private const int SIGN_SHIFT = sizeof(int) * 8 - BITS_PER_ELEMENT; |
||||||
|
private const long USABLE_MASK = ~(~0L << MAX_USABLE_BITS); |
||||||
|
private const long COMPARE_MASK = ~(~0L << 3) << MAX_USABLE_BITS - 3; |
||||||
|
|
||||||
|
private static readonly ulong[] MASKS = { |
||||||
|
0b_00000000_00000000_00000000_00000000_00000000_00011111_11111111_11111111, // 0x1fffff |
||||||
|
0b_00000000_00011111_00000000_00000000_00000000_00000000_11111111_11111111, // 0x1f00000000ffff |
||||||
|
0b_00000000_00011111_00000000_00000000_11111111_00000000_00000000_11111111, // 0x1f0000ff0000ff |
||||||
|
0b_00010000_00001111_00000000_11110000_00001111_00000000_11110000_00001111, // 0x100f00f00f00f00f |
||||||
|
0b_00010000_11000011_00001100_00110000_11000011_00001100_00110000_11000011, // 0x10c30c30c30c30c3 |
||||||
|
0b_00010010_01001001_00100100_10010010_01001001_00100100_10010010_01001001, // 0x1249249249249249 |
||||||
|
}; |
||||||
|
|
||||||
|
private static readonly long X_MASK = (long)MASKS[MASKS.Length - 1]; |
||||||
|
private static readonly long Y_MASK = X_MASK << 1; |
||||||
|
private static readonly long Z_MASK = X_MASK << 2; |
||||||
|
private static readonly long XY_MASK = X_MASK | Y_MASK; |
||||||
|
private static readonly long XZ_MASK = X_MASK | Z_MASK; |
||||||
|
private static readonly long YZ_MASK = Y_MASK | Z_MASK; |
||||||
|
|
||||||
|
|
||||||
|
public long Raw { get; } |
||||||
|
|
||||||
|
public int X => Decode(0); |
||||||
|
public int Y => Decode(1); |
||||||
|
public int Z => Decode(2); |
||||||
|
|
||||||
|
|
||||||
|
private ZOrder(long value) |
||||||
|
=> Raw = value; |
||||||
|
|
||||||
|
public static ZOrder FromRaw(long value) |
||||||
|
=> new(value & USABLE_MASK); |
||||||
|
|
||||||
|
public ZOrder(int x, int y, int z) |
||||||
|
{ |
||||||
|
if (x < ELEMENT_MIN || x > ELEMENT_MAX) throw new ArgumentOutOfRangeException(nameof(x)); |
||||||
|
if (y < ELEMENT_MIN || y > ELEMENT_MAX) throw new ArgumentOutOfRangeException(nameof(y)); |
||||||
|
if (z < ELEMENT_MIN || z > ELEMENT_MAX) throw new ArgumentOutOfRangeException(nameof(z)); |
||||||
|
Raw = Split(x) | Split(y) << 1 | Split(z) << 2; |
||||||
|
} |
||||||
|
|
||||||
|
public void Deconstruct(out int x, out int y, out int z) |
||||||
|
=> (x, y, z) = (X, Y, Z); |
||||||
|
|
||||||
|
|
||||||
|
public ZOrder IncX() => FromRaw((Raw | YZ_MASK) + 1 & X_MASK | Raw & YZ_MASK); |
||||||
|
public ZOrder IncY() => FromRaw((Raw | XZ_MASK) + (1 << 1) & Y_MASK | Raw & XZ_MASK); |
||||||
|
public ZOrder IncZ() => FromRaw((Raw | XY_MASK) + (1 << 2) & Z_MASK | Raw & XY_MASK); |
||||||
|
|
||||||
|
public ZOrder DecX() => FromRaw((Raw & X_MASK) - 1 & X_MASK | Raw & YZ_MASK); |
||||||
|
public ZOrder DecY() => FromRaw((Raw & Y_MASK) - (1 << 1) & Y_MASK | Raw & XZ_MASK); |
||||||
|
public ZOrder DecZ() => FromRaw((Raw & Z_MASK) - (1 << 2) & Z_MASK | Raw & XY_MASK); |
||||||
|
|
||||||
|
public static ZOrder operator +(ZOrder left, ZOrder right) |
||||||
|
{ |
||||||
|
var xSum = (left.Raw | YZ_MASK) + (right.Raw & X_MASK); |
||||||
|
var ySum = (left.Raw | XZ_MASK) + (right.Raw & Y_MASK); |
||||||
|
var zSum = (left.Raw | XY_MASK) + (right.Raw & Z_MASK); |
||||||
|
return FromRaw(xSum & X_MASK | ySum & Y_MASK | zSum & Z_MASK); |
||||||
|
} |
||||||
|
|
||||||
|
public static ZOrder operator -(ZOrder left, ZOrder right) |
||||||
|
{ |
||||||
|
var xDiff = (left.Raw & X_MASK) - (right.Raw & X_MASK); |
||||||
|
var yDiff = (left.Raw & Y_MASK) - (right.Raw & Y_MASK); |
||||||
|
var zDiff = (left.Raw & Z_MASK) - (right.Raw & Z_MASK); |
||||||
|
return FromRaw(xDiff & X_MASK | yDiff & Y_MASK | zDiff & Z_MASK); |
||||||
|
} |
||||||
|
|
||||||
|
public static ZOrder operator &(ZOrder left, long right) => FromRaw(left.Raw & right); |
||||||
|
public static ZOrder operator |(ZOrder left, long right) => FromRaw(left.Raw | right); |
||||||
|
public static ZOrder operator ^(ZOrder left, long right) => FromRaw(left.Raw ^ right); |
||||||
|
|
||||||
|
public static ZOrder operator &(ZOrder left, ZOrder right) => new(left.Raw & right.Raw); |
||||||
|
public static ZOrder operator |(ZOrder left, ZOrder right) => new(left.Raw | right.Raw); |
||||||
|
public static ZOrder operator ^(ZOrder left, ZOrder right) => new(left.Raw ^ right.Raw); |
||||||
|
|
||||||
|
public static ZOrder operator <<(ZOrder left, int right) |
||||||
|
{ |
||||||
|
if (right >= BITS_PER_ELEMENT) throw new ArgumentOutOfRangeException( |
||||||
|
nameof(right), right, $"{nameof(right)} must be smaller than {BITS_PER_ELEMENT}"); |
||||||
|
return FromRaw(left.Raw << right * 3); |
||||||
|
} |
||||||
|
public static ZOrder operator >>(ZOrder left, int right) |
||||||
|
{ |
||||||
|
var result = left.Raw >> right * 3; |
||||||
|
var mask = left.Raw >> MAX_USABLE_BITS - 3 << MAX_USABLE_BITS - right * 3; |
||||||
|
for (var i = 0; i < right; i++) { result |= mask; mask <<= 3; } |
||||||
|
return FromRaw(result); |
||||||
|
} |
||||||
|
|
||||||
|
public int CompareTo(ZOrder other) => (Raw ^ COMPARE_MASK).CompareTo(other.Raw ^ COMPARE_MASK); |
||||||
|
public bool Equals(ZOrder other) => Raw.Equals(other.Raw); |
||||||
|
public override bool Equals(object? obj) => obj is ZOrder order && Equals(order); |
||||||
|
public override int GetHashCode() => Raw.GetHashCode(); |
||||||
|
public override string ToString() => $"<{X},{Y},{Z}>"; |
||||||
|
|
||||||
|
public static bool operator ==(ZOrder left, ZOrder right) => left.Equals(right); |
||||||
|
public static bool operator !=(ZOrder left, ZOrder right) => !left.Equals(right); |
||||||
|
|
||||||
|
|
||||||
|
private static long Split(int i) |
||||||
|
{ |
||||||
|
var l = (ulong)i; |
||||||
|
// l = l & Masks[0]; |
||||||
|
l = (l | l << 32) & MASKS[1]; |
||||||
|
l = (l | l << 16) & MASKS[2]; |
||||||
|
l = (l | l << 8) & MASKS[3]; |
||||||
|
l = (l | l << 4) & MASKS[4]; |
||||||
|
l = (l | l << 2) & MASKS[5]; |
||||||
|
return (long)l; |
||||||
|
} |
||||||
|
|
||||||
|
private int Decode(int index) |
||||||
|
{ |
||||||
|
var l = (ulong)Raw >> index; |
||||||
|
l &= MASKS[5]; |
||||||
|
l = (l ^ l >> 2) & MASKS[4]; |
||||||
|
l = (l ^ l >> 4) & MASKS[3]; |
||||||
|
l = (l ^ l >> 8) & MASKS[2]; |
||||||
|
l = (l ^ l >> 16) & MASKS[1]; |
||||||
|
l = (l ^ l >> 32) & MASKS[0]; |
||||||
|
return (int)l << SIGN_SHIFT >> SIGN_SHIFT; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
using System; |
||||||
|
using gaemstone.ECS; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel.WorldGen; |
||||||
|
|
||||||
|
[Module] |
||||||
|
public class BasicWorldGenerator |
||||||
|
{ |
||||||
|
private readonly FastNoiseLite _noise; |
||||||
|
|
||||||
|
public BasicWorldGenerator() |
||||||
|
{ |
||||||
|
_noise = new(new Random().Next()); |
||||||
|
_noise.SetNoiseType(FastNoiseLite.NoiseType.OpenSimplex2); |
||||||
|
_noise.SetFractalType(FastNoiseLite.FractalType.Ridged); |
||||||
|
_noise.SetFractalOctaves(4); |
||||||
|
_noise.SetFractalGain(0.6F); |
||||||
|
} |
||||||
|
|
||||||
|
[Tag] |
||||||
|
public struct HasBasicWorldGeneration { } |
||||||
|
|
||||||
|
[System] |
||||||
|
public void Populate(Universe universe, Entity entity, |
||||||
|
in Chunk chunk, ChunkPaletteStorage<ecs_entity_t> storage, |
||||||
|
[Not] HasBasicWorldGeneration _) |
||||||
|
{ |
||||||
|
var stone = universe.Lookup("Stone"); |
||||||
|
for (var lx = 0; lx < Chunk.LENGTH; lx++) |
||||||
|
for (var ly = 0; ly < Chunk.LENGTH; ly++) |
||||||
|
for (var lz = 0; lz < Chunk.LENGTH; lz++) { |
||||||
|
var gx = chunk.Position.X << Chunk.BIT_SHIFT | lx; |
||||||
|
var gy = chunk.Position.Y << Chunk.BIT_SHIFT | ly; |
||||||
|
var gz = chunk.Position.Z << Chunk.BIT_SHIFT | lz; |
||||||
|
var bias = Math.Clamp(gy / 32.0F + 1.0F, 0.0F, 1.0F); |
||||||
|
if (_noise.GetNoise(gx, gy, gz) > bias) |
||||||
|
storage[lx, ly, lz] = stone; |
||||||
|
} |
||||||
|
entity.Add<HasBasicWorldGeneration>(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace gaemstone.Bloxel.WorldGen; |
||||||
|
|
||||||
|
// FIXME: There is an issue with this generator where it doesn't generate grass and dirt properly. |
||||||
|
public class SurfaceGrassGenerator |
||||||
|
: IWorldGenerator |
||||||
|
{ |
||||||
|
public static readonly string IDENTIFIER = nameof(SurfaceGrassGenerator); |
||||||
|
|
||||||
|
private const int AIR_BLOCKS_NEEDED = 12; |
||||||
|
private const int DIRT_BLOCKS_BENEATH = 3; |
||||||
|
|
||||||
|
public string Identifier { get; } = IDENTIFIER; |
||||||
|
|
||||||
|
public IEnumerable<string> Dependencies { get; } = new[]{ |
||||||
|
BasicWorldGenerator.IDENTIFIER |
||||||
|
}; |
||||||
|
|
||||||
|
public IEnumerable<(Neighbor, string)> NeighborDependencies { get; } = new[]{ |
||||||
|
(Neighbor.Up, BasicWorldGenerator.IDENTIFIER) |
||||||
|
}; |
||||||
|
|
||||||
|
public void Populate(Chunk chunk) |
||||||
|
{ |
||||||
|
var up = chunk.Neighbors[Neighbor.Up]!; |
||||||
|
for (var lx = 0; lx < Chunk.LENGTH; lx++) |
||||||
|
for (var lz = 0; lz < Chunk.LENGTH; lz++) |
||||||
|
{ |
||||||
|
var numAirBlocks = 0; |
||||||
|
var blockIndex = 0; |
||||||
|
for (var ly = Chunk.LENGTH + AIR_BLOCKS_NEEDED - 1; ly >= 0; ly--) |
||||||
|
{ |
||||||
|
var block = ly >= Chunk.LENGTH |
||||||
|
? up.Storage[lx, ly - Chunk.LENGTH, lz] |
||||||
|
: chunk.Storage[lx, ly, lz]; |
||||||
|
if (block.IsAir) |
||||||
|
{ |
||||||
|
numAirBlocks++; |
||||||
|
blockIndex = 0; |
||||||
|
} |
||||||
|
else if (numAirBlocks >= AIR_BLOCKS_NEEDED || blockIndex > 0) |
||||||
|
{ |
||||||
|
if (ly < Chunk.LENGTH) |
||||||
|
{ |
||||||
|
if (blockIndex == 0) |
||||||
|
chunk.Storage[lx, ly, lz] = Block.GRASS; |
||||||
|
else if (blockIndex <= DIRT_BLOCKS_BENEATH) |
||||||
|
chunk.Storage[lx, ly, lz] = Block.DIRT; |
||||||
|
} |
||||||
|
blockIndex++; |
||||||
|
numAirBlocks = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||||
|
|
||||||
|
<PropertyGroup> |
||||||
|
<TargetFramework>net6.0</TargetFramework> |
||||||
|
<ImplicitUsings>disable</ImplicitUsings> |
||||||
|
<Nullable>enable</Nullable> |
||||||
|
</PropertyGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<Compile Include="../FastNoiseLite/CSharp/FastNoiseLite.cs"></Compile> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<ProjectReference Include="../gaemstone/gaemstone.csproj" /> |
||||||
|
<ProjectReference Include="../gaemstone.Client/gaemstone.Client.csproj" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
</Project> |
@ -0,0 +1,56 @@ |
|||||||
|
using System; |
||||||
|
using System.Diagnostics.CodeAnalysis; |
||||||
|
using System.Runtime.CompilerServices; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using Silk.NET.Maths; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)] |
||||||
|
public readonly struct Color |
||||||
|
: IEquatable<Color> |
||||||
|
{ |
||||||
|
public static Color Transparent { get; } = default; |
||||||
|
|
||||||
|
public static Color Black { get; } = FromRGB(0x000000); |
||||||
|
public static Color White { get; } = FromRGB(0xFFFFFF); |
||||||
|
|
||||||
|
[FieldOffset(0)] |
||||||
|
public readonly uint Value; |
||||||
|
|
||||||
|
[FieldOffset(0)] |
||||||
|
public readonly byte R; |
||||||
|
[FieldOffset(1)] |
||||||
|
public readonly byte G; |
||||||
|
[FieldOffset(2)] |
||||||
|
public readonly byte B; |
||||||
|
[FieldOffset(3)] |
||||||
|
public readonly byte A; |
||||||
|
|
||||||
|
private Color(uint value) |
||||||
|
{ Unsafe.SkipInit(out this); Value = value; } |
||||||
|
private Color(byte r, byte g, byte b, byte a) |
||||||
|
{ Unsafe.SkipInit(out this); R = r; G = g; B = b; A = a; } |
||||||
|
|
||||||
|
public static Color FromRGBA(uint rgba) => new(rgba); |
||||||
|
public static Color FromRGB(uint rgb) => new(rgb | 0xFF000000); |
||||||
|
|
||||||
|
public static Color FromRGBA(byte r, byte g, byte b, byte a) => new(r, g, b, a); |
||||||
|
public static Color FromRGB(byte r, byte g, byte b) => new(r, g, b, 0xFF); |
||||||
|
|
||||||
|
public bool Equals(Color other) |
||||||
|
=> Value == other.Value; |
||||||
|
public override bool Equals([NotNullWhen(true)] object? obj) |
||||||
|
=> (obj is Color color) && Equals(color); |
||||||
|
public override int GetHashCode() |
||||||
|
=> Value.GetHashCode(); |
||||||
|
public override string? ToString() |
||||||
|
=> $"Color(0x{Value:X8})"; |
||||||
|
|
||||||
|
public static bool operator ==(Color left, Color right) => left.Equals(right); |
||||||
|
public static bool operator !=(Color left, Color right) => !left.Equals(right); |
||||||
|
|
||||||
|
public static implicit operator System.Drawing.Color(Color color) => System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B); |
||||||
|
public static implicit operator Vector4D<float>(Color color) => new(color.R / 255F, color.G / 255F, color.B / 255F, color.A / 255F); |
||||||
|
public static implicit operator Vector4D<byte>(Color color) => new(color.R, color.G, color.B, color.A); |
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
using System; |
||||||
|
using System.Runtime.CompilerServices; |
||||||
|
using Silk.NET.OpenGL; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
public static class GLExtensions |
||||||
|
{ |
||||||
|
public static uint CreateAndCompileShader(this GL GL, ShaderType type, string label, string source) |
||||||
|
{ |
||||||
|
var shader = GL.CreateShader(type); |
||||||
|
GL.ObjectLabel(ObjectIdentifier.Shader, shader, (uint)label.Length, label); |
||||||
|
GL.ShaderSource(shader, source); |
||||||
|
GL.CompileShader(shader); |
||||||
|
GL.GetShader(shader, ShaderParameterName.CompileStatus, out var result); |
||||||
|
if (result != (int)GLEnum.True) throw new Exception( |
||||||
|
$"Failed compiling shader \"{label}\" ({shader}):\n{GL.GetShaderInfoLog(shader)}"); |
||||||
|
return shader; |
||||||
|
} |
||||||
|
|
||||||
|
public static uint CreateAndLinkProgram(this GL GL, string label, params uint[] shaders) |
||||||
|
{ |
||||||
|
var program = GL.CreateProgram(); |
||||||
|
GL.ObjectLabel(ObjectIdentifier.Program, program, (uint)label.Length, label); |
||||||
|
foreach (var shader in shaders) GL.AttachShader(program, shader); |
||||||
|
GL.LinkProgram(program); |
||||||
|
foreach (var shader in shaders) GL.DetachShader(program, shader); |
||||||
|
foreach (var shader in shaders) GL.DeleteShader(shader); |
||||||
|
GL.GetProgram(program, ProgramPropertyARB.LinkStatus, out var result); |
||||||
|
if (result != (int)GLEnum.True) throw new Exception( |
||||||
|
$"Failed linking Program \"{label}\" ({program}):\n{GL.GetProgramInfoLog(program)}"); |
||||||
|
return program; |
||||||
|
} |
||||||
|
|
||||||
|
// These overloads are available because without them, the implicit casting |
||||||
|
// (say from T[] to ReadOnlySpan<T>) causes the generic type resolving to break. |
||||||
|
public static uint CreateBufferFromData<T>(this GL GL, T[] data, |
||||||
|
BufferTargetARB target = BufferTargetARB.ArrayBuffer, |
||||||
|
BufferUsageARB usage = BufferUsageARB.StaticDraw) |
||||||
|
where T : unmanaged |
||||||
|
=> GL.CreateBufferFromData((ReadOnlySpan<T>)data, target, usage); |
||||||
|
public static uint CreateBufferFromData<T>(this GL GL, ArraySegment<T> data, |
||||||
|
BufferTargetARB target = BufferTargetARB.ArrayBuffer, |
||||||
|
BufferUsageARB usage = BufferUsageARB.StaticDraw) |
||||||
|
where T : unmanaged |
||||||
|
=> GL.CreateBufferFromData((ReadOnlySpan<T>)data, target, usage); |
||||||
|
public static uint CreateBufferFromData<T>(this GL GL, Span<T> data, |
||||||
|
BufferTargetARB target = BufferTargetARB.ArrayBuffer, |
||||||
|
BufferUsageARB usage = BufferUsageARB.StaticDraw) |
||||||
|
where T : unmanaged |
||||||
|
=> GL.CreateBufferFromData((ReadOnlySpan<T>)data, target, usage); |
||||||
|
public static uint CreateBufferFromData<T>(this GL GL, ReadOnlySpan<T> data, |
||||||
|
BufferTargetARB target = BufferTargetARB.ArrayBuffer, |
||||||
|
BufferUsageARB usage = BufferUsageARB.StaticDraw) |
||||||
|
where T : unmanaged |
||||||
|
{ |
||||||
|
var buffer = GL.GenBuffer(); |
||||||
|
GL.BindBuffer(target, buffer); |
||||||
|
GL.BufferData(target, (nuint)(data.Length * Unsafe.SizeOf<T>()), data, usage); |
||||||
|
return buffer; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
using gaemstone.ECS; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
[Component] |
||||||
|
public readonly struct Mesh |
||||||
|
{ |
||||||
|
public uint Handle { get; } |
||||||
|
public int Count { get; } |
||||||
|
public bool IsIndexed { get; } |
||||||
|
|
||||||
|
public Mesh(uint handle, int count, bool indexed = true) |
||||||
|
{ Handle = handle; Count = count; IsIndexed = indexed; } |
||||||
|
} |
@ -0,0 +1,104 @@ |
|||||||
|
using System; |
||||||
|
using gaemstone.ECS; |
||||||
|
using Silk.NET.Maths; |
||||||
|
using Silk.NET.OpenGL; |
||||||
|
using ModelRoot = SharpGLTF.Schema2.ModelRoot; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
public static class MeshManager |
||||||
|
{ |
||||||
|
const uint PositionAttribIndex = 0; |
||||||
|
const uint NormalAttribIndex = 1; |
||||||
|
const uint UvAttribIndex = 2; |
||||||
|
|
||||||
|
public static Mesh Load(Universe universe, string name) |
||||||
|
{ |
||||||
|
ModelRoot root; |
||||||
|
using (var stream = Resources.GetStream(name)) |
||||||
|
root = ModelRoot.ReadGLB(stream, new()); |
||||||
|
var primitive = root.LogicalMeshes[0].Primitives[0]; |
||||||
|
|
||||||
|
var indices = primitive.IndexAccessor; |
||||||
|
var vertices = primitive.VertexAccessors["POSITION"]; |
||||||
|
var normals = primitive.VertexAccessors["NORMAL"]; |
||||||
|
|
||||||
|
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL; |
||||||
|
var vao = GL.GenVertexArray(); |
||||||
|
GL.BindVertexArray(vao); |
||||||
|
|
||||||
|
GL.CreateBufferFromData(indices.SourceBufferView.Content, |
||||||
|
BufferTargetARB.ElementArrayBuffer); |
||||||
|
|
||||||
|
GL.CreateBufferFromData(vertices.SourceBufferView.Content); |
||||||
|
GL.EnableVertexAttribArray(PositionAttribIndex); |
||||||
|
unsafe { GL.VertexAttribPointer(PositionAttribIndex, 3, |
||||||
|
(VertexAttribPointerType)vertices.Encoding, vertices.Normalized, |
||||||
|
(uint)vertices.SourceBufferView.ByteStride, (void*)vertices.ByteOffset); } |
||||||
|
|
||||||
|
GL.CreateBufferFromData(normals.SourceBufferView.Content); |
||||||
|
GL.EnableVertexAttribArray(NormalAttribIndex); |
||||||
|
unsafe { GL.VertexAttribPointer(NormalAttribIndex, 3, |
||||||
|
(VertexAttribPointerType)vertices.Encoding, vertices.Normalized, |
||||||
|
(uint)vertices.SourceBufferView.ByteStride, (void*)vertices.ByteOffset); } |
||||||
|
|
||||||
|
var numVertices = primitive.IndexAccessor.Count; |
||||||
|
return new(vao, numVertices); |
||||||
|
} |
||||||
|
|
||||||
|
public static Mesh Create(Universe universe, |
||||||
|
ReadOnlySpan<ushort> indices, ReadOnlySpan<Vector3D<float>> vertices, |
||||||
|
ReadOnlySpan<Vector3D<float>> normals, ReadOnlySpan<Vector2D<float>> uvs) |
||||||
|
{ |
||||||
|
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL; |
||||||
|
var vao = GL.GenVertexArray(); |
||||||
|
GL.BindVertexArray(vao); |
||||||
|
|
||||||
|
GL.CreateBufferFromData(indices, BufferTargetARB.ElementArrayBuffer); |
||||||
|
GL.CreateBufferFromData(vertices); |
||||||
|
GL.EnableVertexAttribArray(PositionAttribIndex); |
||||||
|
unsafe { GL.VertexAttribPointer(PositionAttribIndex, 3, |
||||||
|
VertexAttribPointerType.Float, false, 0, (void*)0); } |
||||||
|
if (!normals.IsEmpty) { |
||||||
|
GL.CreateBufferFromData(normals); |
||||||
|
GL.EnableVertexAttribArray(NormalAttribIndex); |
||||||
|
unsafe { GL.VertexAttribPointer(NormalAttribIndex, 3, |
||||||
|
VertexAttribPointerType.Float, false, 0, (void*)0); } |
||||||
|
} |
||||||
|
if (!uvs.IsEmpty) { |
||||||
|
GL.CreateBufferFromData(uvs); |
||||||
|
GL.EnableVertexAttribArray(UvAttribIndex); |
||||||
|
unsafe { GL.VertexAttribPointer(UvAttribIndex, 2, |
||||||
|
VertexAttribPointerType.Float, false, 0, (void*)0); } |
||||||
|
} |
||||||
|
|
||||||
|
return new(vao, indices.Length); |
||||||
|
} |
||||||
|
|
||||||
|
public static Mesh Create(Universe universe, ReadOnlySpan<Vector3D<float>> vertices, |
||||||
|
ReadOnlySpan<Vector3D<float>> normals, ReadOnlySpan<Vector2D<float>> uvs) |
||||||
|
{ |
||||||
|
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL; |
||||||
|
var vao = GL.GenVertexArray(); |
||||||
|
GL.BindVertexArray(vao); |
||||||
|
|
||||||
|
GL.CreateBufferFromData(vertices); |
||||||
|
GL.EnableVertexAttribArray(PositionAttribIndex); |
||||||
|
unsafe { GL.VertexAttribPointer(PositionAttribIndex, 3, |
||||||
|
VertexAttribPointerType.Float, false, 0, (void*)0); } |
||||||
|
if (!normals.IsEmpty) { |
||||||
|
GL.CreateBufferFromData(normals); |
||||||
|
GL.EnableVertexAttribArray(NormalAttribIndex); |
||||||
|
unsafe { GL.VertexAttribPointer(NormalAttribIndex, 3, |
||||||
|
VertexAttribPointerType.Float, false, 0, (void*)0); } |
||||||
|
} |
||||||
|
if (!uvs.IsEmpty) { |
||||||
|
GL.CreateBufferFromData(uvs); |
||||||
|
GL.EnableVertexAttribArray(UvAttribIndex); |
||||||
|
unsafe { GL.VertexAttribPointer(UvAttribIndex, 2, |
||||||
|
VertexAttribPointerType.Float, false, 0, (void*)0); } |
||||||
|
} |
||||||
|
|
||||||
|
return new(vao, vertices.Length, false); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
using System; |
||||||
|
using gaemstone.ECS; |
||||||
|
using Silk.NET.Input; |
||||||
|
using Silk.NET.Maths; |
||||||
|
using static gaemstone.Client.Input; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
[Module] |
||||||
|
[DependsOn(typeof(Input))] |
||||||
|
public class CameraModule |
||||||
|
{ |
||||||
|
[Component] |
||||||
|
public struct Camera |
||||||
|
{ |
||||||
|
public static readonly Camera Default2D = Create2D(); |
||||||
|
public static readonly Camera Default3D = Create3D(80.0F); |
||||||
|
|
||||||
|
public static Camera Create2D(float nearPlane = -100.0F, float farPlane = 100.0F) |
||||||
|
=> new() { NearPlane = nearPlane, FarPlane = farPlane }; |
||||||
|
public static Camera Create3D(float fieldOfView, float nearPlane = 0.1F, float farPlane = 200.0F) |
||||||
|
=> new() { FieldOfView = fieldOfView, NearPlane = nearPlane, FarPlane = farPlane }; |
||||||
|
|
||||||
|
public float FieldOfView { get; set; } |
||||||
|
public float NearPlane { get; set; } |
||||||
|
public float FarPlane { get; set; } |
||||||
|
|
||||||
|
public bool IsOrthographic => (FieldOfView == 0.0F); |
||||||
|
} |
||||||
|
|
||||||
|
[Component] |
||||||
|
public struct CameraViewport |
||||||
|
{ |
||||||
|
public Vector4D<byte> ClearColor { get; set; } |
||||||
|
public Rectangle<int> Viewport { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
[Component] |
||||||
|
public struct CameraController |
||||||
|
{ |
||||||
|
public float MouseSensitivity { get; set; } |
||||||
|
public Vector2D<float>? MouseGrabbedAt { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
[System] |
||||||
|
public static void UpdateCamera(TimeSpan delta, in Camera camera, |
||||||
|
ref GlobalTransform transform, ref CameraController controller, |
||||||
|
[Source(typeof(Game))] RawInput input) |
||||||
|
{ |
||||||
|
var isMouseDown = input.IsDown(MouseButton.Right); |
||||||
|
var isMouseGrabbed = controller.MouseGrabbedAt != null; |
||||||
|
if (isMouseDown != isMouseGrabbed) { |
||||||
|
if (isMouseDown) controller.MouseGrabbedAt = input.MousePosition; |
||||||
|
else controller.MouseGrabbedAt = null; |
||||||
|
} |
||||||
|
|
||||||
|
if (controller.MouseGrabbedAt is not Vector2D<float> pos) return; |
||||||
|
|
||||||
|
var mouseMoved = input.MousePosition - pos; |
||||||
|
input.Context!.Mice[0].Position = pos.ToSystem(); |
||||||
|
|
||||||
|
var dt = (float)delta.TotalSeconds; |
||||||
|
var xMovement = mouseMoved.X * dt * controller.MouseSensitivity; |
||||||
|
var yMovement = mouseMoved.Y * dt * controller.MouseSensitivity; |
||||||
|
|
||||||
|
if (camera.IsOrthographic) { |
||||||
|
transform *= Matrix4X4.CreateTranslation(-xMovement, -yMovement, 0); |
||||||
|
} else { |
||||||
|
var speed = dt * (input.IsDown(Key.ShiftLeft) ? 12 : 4); |
||||||
|
var forwardMovement = ((input.IsDown(Key.W) ? -1 : 0) + (input.IsDown(Key.S) ? 1 : 0)) * speed; |
||||||
|
var sideMovement = ((input.IsDown(Key.A) ? -1 : 0) + (input.IsDown(Key.D) ? 1 : 0)) * speed; |
||||||
|
|
||||||
|
var curTranslation = new Vector3D<float>(transform.Value.M41, transform.Value.M42, transform.Value.M43); |
||||||
|
var yawRotation = Matrix4X4.CreateRotationY(-xMovement / 100, curTranslation); |
||||||
|
var pitchRotation = Matrix4X4.CreateRotationX(-yMovement / 100); |
||||||
|
var translation = Matrix4X4.CreateTranslation(sideMovement, 0, forwardMovement); |
||||||
|
|
||||||
|
transform = translation * pitchRotation * transform * yawRotation; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using gaemstone.ECS; |
||||||
|
using Silk.NET.Input; |
||||||
|
using Silk.NET.Maths; |
||||||
|
using static gaemstone.Client.Windowing; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
[Module] |
||||||
|
[DependsOn(typeof(Windowing))] |
||||||
|
public class Input |
||||||
|
{ |
||||||
|
[Component] |
||||||
|
public class RawInput |
||||||
|
{ |
||||||
|
internal IInputContext? Context { get; set; } |
||||||
|
|
||||||
|
public Dictionary<Key, ButtonState> Keyboard { get; } = new(); |
||||||
|
public Dictionary<MouseButton, ButtonState> MouseButtons { get; } = new(); |
||||||
|
public Vector2D<float> MousePosition { get; set; } |
||||||
|
public float MouseWheel { get; set; } |
||||||
|
public float MouseWheelDelta { get; set; } |
||||||
|
|
||||||
|
public bool IsDown(Key key) => Keyboard.GetValueOrDefault(key)?.IsDown == true; |
||||||
|
public bool IsDown(MouseButton button) => MouseButtons.GetValueOrDefault(button)?.IsDown == true; |
||||||
|
} |
||||||
|
|
||||||
|
public class ButtonState |
||||||
|
{ |
||||||
|
public TimeSpan TimePressed; |
||||||
|
public bool IsDown; |
||||||
|
public bool Pressed; |
||||||
|
public bool Released; |
||||||
|
} |
||||||
|
|
||||||
|
[System(Phase.OnLoad)] |
||||||
|
public static void ProcessInput(GameWindow window, RawInput input, TimeSpan delta) |
||||||
|
{ |
||||||
|
window.Handle.DoEvents(); |
||||||
|
|
||||||
|
input.Context ??= window.Handle.CreateInput(); |
||||||
|
|
||||||
|
foreach (var state in input.Keyboard.Values.Concat(input.MouseButtons.Values)) { |
||||||
|
if (state.IsDown) state.TimePressed += delta; |
||||||
|
state.Pressed = state.Released = false; |
||||||
|
} |
||||||
|
|
||||||
|
var keyboard = input.Context.Keyboards[0]; |
||||||
|
foreach (var key in keyboard.SupportedKeys) { |
||||||
|
var state = input.Keyboard.GetValueOrDefault(key); |
||||||
|
if (keyboard.IsKeyPressed(key)) { |
||||||
|
if (state == null) input.Keyboard.Add(key, state = new()); |
||||||
|
if (!state.IsDown) state.Pressed = true; |
||||||
|
state.IsDown = true; |
||||||
|
} else if (state != null) { |
||||||
|
if (state.IsDown) state.Released = true; |
||||||
|
state.IsDown = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var mouse = input.Context.Mice[0]; |
||||||
|
foreach (var button in mouse.SupportedButtons) { |
||||||
|
var state = input.MouseButtons.GetValueOrDefault(button); |
||||||
|
if (mouse.IsButtonPressed(button)) { |
||||||
|
if (state == null) input.MouseButtons.Add(button, state = new()); |
||||||
|
if (!state.IsDown) state.Pressed = true; |
||||||
|
state.IsDown = true; |
||||||
|
} else if (state != null) { |
||||||
|
if (state.IsDown) state.Released = true; |
||||||
|
state.IsDown = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
input.MousePosition = mouse.Position.ToGeneric(); |
||||||
|
|
||||||
|
input.MouseWheelDelta += mouse.ScrollWheels[0].Y - input.MouseWheel; |
||||||
|
input.MouseWheel = mouse.ScrollWheels[0].Y; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,109 @@ |
|||||||
|
using System; |
||||||
|
using System.Diagnostics; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using gaemstone.ECS; |
||||||
|
using Silk.NET.Maths; |
||||||
|
using Silk.NET.OpenGL; |
||||||
|
using static gaemstone.Client.CameraModule; |
||||||
|
using static gaemstone.Client.Windowing; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
[Module] |
||||||
|
[DependsOn(typeof(Windowing))] |
||||||
|
public class Renderer |
||||||
|
{ |
||||||
|
private readonly uint _program; |
||||||
|
private readonly int _cameraMatrixUniform; |
||||||
|
private readonly int _modelMatrixUniform; |
||||||
|
|
||||||
|
public Renderer(Universe universe) |
||||||
|
{ |
||||||
|
var GL = universe.Lookup<Game>().Get<Canvas>().GL; |
||||||
|
|
||||||
|
GL.Enable(EnableCap.DebugOutputSynchronous); |
||||||
|
GL.DebugMessageCallback(DebugCallback, 0); |
||||||
|
|
||||||
|
GL.Enable(EnableCap.CullFace); |
||||||
|
GL.CullFace(CullFaceMode.Back); |
||||||
|
|
||||||
|
GL.Enable(EnableCap.DepthTest); |
||||||
|
GL.DepthFunc(DepthFunction.Less); |
||||||
|
|
||||||
|
GL.Enable(EnableCap.Blend); |
||||||
|
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); |
||||||
|
|
||||||
|
var vertexShaderSource = Resources.GetString("default.vs.glsl"); |
||||||
|
var fragmentShaderSource = Resources.GetString("default.fs.glsl"); |
||||||
|
|
||||||
|
var vertexShader = GL.CreateAndCompileShader(ShaderType.VertexShader , "vertex" , vertexShaderSource); |
||||||
|
var fragmentShader = GL.CreateAndCompileShader(ShaderType.FragmentShader, "fragment", fragmentShaderSource); |
||||||
|
|
||||||
|
_program = GL.CreateAndLinkProgram("program", vertexShader, fragmentShader); |
||||||
|
_cameraMatrixUniform = GL.GetUniformLocation(_program, "cameraMatrix"); |
||||||
|
_modelMatrixUniform = GL.GetUniformLocation(_program, "modelMatrix"); |
||||||
|
} |
||||||
|
|
||||||
|
[System] |
||||||
|
public void Render(Universe universe, Canvas canvas) |
||||||
|
{ |
||||||
|
var GL = canvas.GL; |
||||||
|
GL.UseProgram(_program); |
||||||
|
GL.Viewport(default, canvas.Size); |
||||||
|
GL.ClearColor(new Vector4D<float>(0, 0, 0, 255)); |
||||||
|
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); |
||||||
|
|
||||||
|
Filter.RunOnce(universe, (in GlobalTransform transform, in Camera camera, CameraViewport? viewport) => { |
||||||
|
var color = viewport?.ClearColor ?? new(0x4B, 0x00, 0x82, 255); |
||||||
|
var bounds = viewport?.Viewport ?? new(default, canvas.Size); |
||||||
|
|
||||||
|
GL.Enable(EnableCap.ScissorTest); |
||||||
|
GL.Viewport(bounds); GL.Scissor(bounds.Origin.X, bounds.Origin.Y, (uint)bounds.Size.X, (uint)bounds.Size.Y); |
||||||
|
GL.ClearColor(color); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); |
||||||
|
GL.Disable(EnableCap.ScissorTest); |
||||||
|
|
||||||
|
// Get the camera's transform matrix and invert it. |
||||||
|
Matrix4X4.Invert<float>(transform, out var cameraTransform); |
||||||
|
|
||||||
|
// Create the camera's projection matrix. |
||||||
|
var cameraProjection = camera.IsOrthographic |
||||||
|
? Matrix4X4.CreateOrthographic( |
||||||
|
bounds.Size.X, -bounds.Size.Y, |
||||||
|
camera.NearPlane, camera.FarPlane) |
||||||
|
: Matrix4X4.CreatePerspectiveFieldOfView( |
||||||
|
camera.FieldOfView * MathF.PI / 180, // Degrees => Radians |
||||||
|
(float)bounds.Size.X / bounds.Size.Y, // Aspect Ratio |
||||||
|
camera.NearPlane, camera.FarPlane); |
||||||
|
|
||||||
|
// Set the uniform to the combined transform and projection. |
||||||
|
var cameraMatrix = cameraTransform * cameraProjection; |
||||||
|
GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.Row1.X); |
||||||
|
|
||||||
|
Filter.RunOnce(universe, (in GlobalTransform transform, in Mesh mesh, Texture? texture) => |
||||||
|
{ |
||||||
|
// If entity has Texture, bind it now. |
||||||
|
if (texture.HasValue) GL.BindTexture(texture.Value.Target, texture.Value.Handle); |
||||||
|
|
||||||
|
// Draw the mesh. |
||||||
|
GL.UniformMatrix4(_modelMatrixUniform, 1, false, in transform.Value.Row1.X); |
||||||
|
GL.BindVertexArray(mesh.Handle); |
||||||
|
if (!mesh.IsIndexed) GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)mesh.Count); |
||||||
|
else unsafe { GL.DrawElements(PrimitiveType.Triangles, (uint)mesh.Count, DrawElementsType.UnsignedShort, null); } |
||||||
|
|
||||||
|
// If entity has Texture, unbind it after it has been rendered. |
||||||
|
if (texture.HasValue) GL.BindTexture(texture.Value.Target, 0); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
[DebuggerStepThrough] |
||||||
|
private static void DebugCallback(GLEnum source, GLEnum _type, int id, GLEnum _severity, |
||||||
|
int length, nint _message, nint userParam) |
||||||
|
{ |
||||||
|
var type = (DebugType)_type; |
||||||
|
var severity = (DebugSeverity)_severity; |
||||||
|
var message = Marshal.PtrToStringAnsi(_message, length); |
||||||
|
Console.WriteLine($"[GLDebug] [{severity}] {type}/{id}: {message}"); |
||||||
|
if (type == DebugType.DebugTypeError) throw new Exception(message); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
using gaemstone.ECS; |
||||||
|
using Silk.NET.Maths; |
||||||
|
using Silk.NET.OpenGL; |
||||||
|
using Silk.NET.Windowing; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
[Module] |
||||||
|
public class Windowing |
||||||
|
{ |
||||||
|
[Component] |
||||||
|
public class Canvas |
||||||
|
{ |
||||||
|
public GL GL { get; } |
||||||
|
public Canvas(GL gl) => GL = gl; |
||||||
|
|
||||||
|
public Vector2D<int> Size { get; set; } |
||||||
|
public Color BackgroundColor { get; set; } |
||||||
|
} |
||||||
|
|
||||||
|
[Component] |
||||||
|
public class GameWindow |
||||||
|
{ |
||||||
|
public IWindow Handle { get; } |
||||||
|
public GameWindow(IWindow handle) => Handle = handle; |
||||||
|
} |
||||||
|
|
||||||
|
[System(Phase.PreFrame)] |
||||||
|
public static void ProcessWindow(GameWindow window, Canvas canvas) |
||||||
|
=> canvas.Size = window.Handle.Size; |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Reflection; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
public static class Resources |
||||||
|
{ |
||||||
|
public static Assembly ResourceAssembly { get; set; } = null!; |
||||||
|
|
||||||
|
public static Stream GetStream(string name) |
||||||
|
=> ResourceAssembly.GetManifestResourceStream( |
||||||
|
ResourceAssembly.GetName().Name + ".Resources." + name) |
||||||
|
?? throw new ArgumentException($"Could not find embedded resource '{name}'"); |
||||||
|
|
||||||
|
public static string GetString(string name) |
||||||
|
{ |
||||||
|
using var stream = GetStream(name); |
||||||
|
using var reader = new StreamReader(stream); |
||||||
|
return reader.ReadToEnd(); |
||||||
|
} |
||||||
|
|
||||||
|
public static byte[] GetBytes(string name) |
||||||
|
{ |
||||||
|
using var stream = GetStream(name); |
||||||
|
using var memoryStream = new MemoryStream(); |
||||||
|
stream.CopyTo(memoryStream); |
||||||
|
return memoryStream.ToArray(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
using gaemstone.ECS; |
||||||
|
using Silk.NET.OpenGL; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
[Component] |
||||||
|
public readonly struct Texture |
||||||
|
{ |
||||||
|
public TextureTarget Target { get; } |
||||||
|
public uint Handle { get; } |
||||||
|
|
||||||
|
public Texture(TextureTarget target, uint handle) |
||||||
|
=> (Target, Handle) = (target, handle); |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
using System.Drawing; |
||||||
|
using gaemstone.ECS; |
||||||
|
using Silk.NET.Maths; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
[Component] |
||||||
|
public readonly struct TextureCoords4 |
||||||
|
{ |
||||||
|
public Vector2D<float> TopLeft { get; } |
||||||
|
public Vector2D<float> TopRight { get; } |
||||||
|
public Vector2D<float> BottomLeft { get; } |
||||||
|
public Vector2D<float> BottomRight { get; } |
||||||
|
|
||||||
|
public TextureCoords4(float x1, float y1, float x2, float y2) |
||||||
|
{ |
||||||
|
TopLeft = new(x1, y1); |
||||||
|
TopRight = new(x2, y1); |
||||||
|
BottomLeft = new(x1, y2); |
||||||
|
BottomRight = new(x2, y2); |
||||||
|
} |
||||||
|
|
||||||
|
public static TextureCoords4 FromIntCoords(Size textureSize, Point origin, Size size) |
||||||
|
=> FromIntCoords(textureSize, origin.X, origin.Y, size.Width, size.Height); |
||||||
|
public static TextureCoords4 FromIntCoords(Size textureSize, int x, int y, int width, int height) => new( |
||||||
|
x / (float)textureSize.Width + 0.001F, |
||||||
|
y / (float)textureSize.Height + 0.001F, |
||||||
|
(x + width) / (float)textureSize.Width - 0.001F, |
||||||
|
(y + height) / (float)textureSize.Height - 0.001F); |
||||||
|
|
||||||
|
public static TextureCoords4 FromGrid(int numCellsX, int numCellsY, int cellX, int cellY) => new( |
||||||
|
cellX / (float)numCellsX + 0.001F, |
||||||
|
cellY / (float)numCellsY + 0.001F, |
||||||
|
(cellX + 1) / (float)numCellsX - 0.001F, |
||||||
|
(cellY + 1) / (float)numCellsY - 0.001F); |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using gaemstone.ECS; |
||||||
|
using Silk.NET.OpenGL; |
||||||
|
using SixLabors.ImageSharp; |
||||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||||
|
using Size = System.Drawing.Size; |
||||||
|
|
||||||
|
namespace gaemstone.Client; |
||||||
|
|
||||||
|
public static class TextureManager |
||||||
|
{ |
||||||
|
private static readonly Dictionary<Texture, TextureInfo> _byTexture = new(); |
||||||
|
private static readonly Dictionary<string, TextureInfo> _bySourceFile = new(); |
||||||
|
|
||||||
|
public static void Initialize(Universe universe) |
||||||
|
{ |
||||||
|
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL; |
||||||
|
// Upload single-pixel white texture into texture slot 0, so when |
||||||
|
// "no" texture is bound, we can still use the texture sampler. |
||||||
|
GL.BindTexture(TextureTarget.Texture2D, 0); |
||||||
|
Span<byte> pixel = stackalloc byte[4]; |
||||||
|
pixel.Fill(255); |
||||||
|
GL.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, |
||||||
|
1, 1, 0, PixelFormat.Rgba, PixelType.UnsignedByte, in pixel[0]); |
||||||
|
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest); |
||||||
|
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); |
||||||
|
} |
||||||
|
|
||||||
|
public static Texture Load(Universe universe, string name) |
||||||
|
{ |
||||||
|
using var stream = Resources.GetStream(name); |
||||||
|
return CreateFromStream(universe, stream, name); |
||||||
|
} |
||||||
|
|
||||||
|
public static Texture CreateFromStream(Universe universe, Stream stream, string? sourceFile = null) |
||||||
|
{ |
||||||
|
var GL = universe.Lookup<Game>().Get<Windowing.Canvas>().GL; |
||||||
|
var texture = new Texture(TextureTarget.Texture2D, GL.GenTexture()); |
||||||
|
GL.BindTexture(texture.Target, texture.Handle); |
||||||
|
|
||||||
|
var image = Image.Load<Rgba32>(stream); |
||||||
|
ref var origin = ref image.Frames[0].PixelBuffer[0, 0]; |
||||||
|
|
||||||
|
GL.TexImage2D(texture.Target, 0, (int)PixelFormat.Rgba, |
||||||
|
(uint)image.Width, (uint)image.Height, 0, |
||||||
|
PixelFormat.Rgba, PixelType.UnsignedByte, origin); |
||||||
|
GL.TexParameter(texture.Target, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest); |
||||||
|
GL.TexParameter(texture.Target, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); |
||||||
|
|
||||||
|
var info = new TextureInfo(texture, sourceFile, new(image.Width, image.Height)); |
||||||
|
_byTexture.Add(texture, info); |
||||||
|
if (sourceFile != null) _bySourceFile.Add(sourceFile, info); |
||||||
|
|
||||||
|
GL.BindTexture(texture.Target, 0); |
||||||
|
return texture; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static TextureInfo? Lookup(Texture texture) |
||||||
|
=> _byTexture.TryGetValue(texture, out var value) ? value : null; |
||||||
|
|
||||||
|
public static TextureInfo? Lookup(string sourceFile) |
||||||
|
=> _bySourceFile.TryGetValue(sourceFile, out var value) ? value : null; |
||||||
|
} |
||||||
|
|
||||||
|
public class TextureInfo |
||||||
|
{ |
||||||
|
public Texture Texture { get; } |
||||||
|
public string? SourceFile { get; } |
||||||
|
public Size Size { get; } |
||||||
|
|
||||||
|
public TextureInfo(Texture texture, string? sourceFile, Size size) |
||||||
|
=> (Texture, SourceFile, Size) = (texture, sourceFile, size); |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||||
|
|
||||||
|
<PropertyGroup> |
||||||
|
<TargetFramework>net6.0</TargetFramework> |
||||||
|
<ImplicitUsings>disable</ImplicitUsings> |
||||||
|
<Nullable>enable</Nullable> |
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
||||||
|
</PropertyGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<ProjectReference Include="../flecs-cs/src/cs/production/Flecs/Flecs.csproj" /> |
||||||
|
<ProjectReference Include="../gaemstone/gaemstone.csproj" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.0-alpha0026" /> |
||||||
|
<PackageReference Include="Silk.NET" Version="2.16.0" /> |
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
</Project> |
@ -0,0 +1,12 @@ |
|||||||
|
using System; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] |
||||||
|
public class ComponentAttribute : Attribute { } |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Struct)] |
||||||
|
public class TagAttribute : Attribute { } |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] |
||||||
|
public class RelationAttribute : Attribute { } |
@ -0,0 +1,193 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Runtime.CompilerServices; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] |
||||||
|
public class EntityAttribute : Attribute { } |
||||||
|
|
||||||
|
public unsafe readonly struct Entity |
||||||
|
{ |
||||||
|
public Universe Universe { get; } |
||||||
|
public ecs_entity_t Value { get; } |
||||||
|
|
||||||
|
public EntityType Type => new(Universe, ecs_get_type(Universe, Value)); |
||||||
|
public string Name => ecs_get_name(Universe, Value).ToStringAndFree(); |
||||||
|
public string FullPath => ecs_get_path_w_sep(Universe, default, Value, ".", default).ToStringAndFree(); |
||||||
|
|
||||||
|
public bool IsNone => Value.Data == 0; |
||||||
|
public bool IsAlive => ecs_is_alive(Universe, Value); |
||||||
|
|
||||||
|
public IEnumerable<Entity> Children { get { |
||||||
|
var term = new ecs_term_t { id = Universe.EcsChildOf & this }; |
||||||
|
var iter = Iterator.FromTerm(Universe, term); |
||||||
|
while (iter.Next()) |
||||||
|
for (var i = 0; i < iter.Count; i++) |
||||||
|
yield return iter.Entity(i); |
||||||
|
} } |
||||||
|
|
||||||
|
public Entity(Universe universe, ecs_entity_t value) |
||||||
|
{ Universe = universe; Value = value; } |
||||||
|
|
||||||
|
public void ThrowIfNone() { if (IsNone) throw new InvalidOperationException("Entity isn't valid"); } |
||||||
|
public void ThrowIfDead() { if (!IsAlive) throw new InvalidOperationException("Entity is dead"); } |
||||||
|
|
||||||
|
public void Delete() => ecs_delete(Universe, Value); |
||||||
|
|
||||||
|
|
||||||
|
public Entity Add(ecs_id_t id) { ecs_add_id(Universe, this, id); return this; } |
||||||
|
public Entity Add(Identifier id) { ecs_add_id(Universe, this, id); return this; } |
||||||
|
public Entity Add(Entity relation, Entity target) => Add(relation & target); |
||||||
|
|
||||||
|
public Entity Add<T>() |
||||||
|
=> Add(Universe.Lookup<T>()); |
||||||
|
public Entity Add<TRelation, TTarget>() |
||||||
|
=> Add(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>()); |
||||||
|
public Entity Add<TRelation>(Entity target) |
||||||
|
=> Add(Universe.Lookup<TRelation>(), target); |
||||||
|
|
||||||
|
|
||||||
|
public Entity Override(ecs_id_t id) { ecs_override_id(Universe, this, id); return this; } |
||||||
|
public Entity Override(Identifier id) { ecs_override_id(Universe, this, id); return this; } |
||||||
|
public Entity Override(Entity relation, Entity target) => Override(relation & target); |
||||||
|
|
||||||
|
public Entity Override<T>() |
||||||
|
=> Override(Universe.Lookup<T>()); |
||||||
|
public Entity Override<TRelation, TTarget>() |
||||||
|
=> Override(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>()); |
||||||
|
public Entity Override<TRelation>(Entity target) |
||||||
|
=> Override(Universe.Lookup<TRelation>(), target); |
||||||
|
|
||||||
|
|
||||||
|
public void Remove(ecs_id_t id) => ecs_remove_id(Universe, this, id); |
||||||
|
public void Remove(Identifier id) => ecs_remove_id(Universe, this, id); |
||||||
|
public void Remove<T>() => Remove(Universe.Lookup<T>()); |
||||||
|
|
||||||
|
|
||||||
|
public bool Has(ecs_id_t id) => ecs_has_id(Universe, this, id); |
||||||
|
public bool Has(Identifier id) => ecs_has_id(Universe, this, id); |
||||||
|
public bool Has(Entity relation, Entity target) => Has(relation & target); |
||||||
|
|
||||||
|
public bool Has<T>() |
||||||
|
=> Has(Universe.Lookup<T>()); |
||||||
|
public bool Has<TRelation, TTarget>() |
||||||
|
=> Has(Universe.Lookup<TRelation>(), Universe.Lookup<TTarget>()); |
||||||
|
public bool Has<TRelation>(Entity target) |
||||||
|
=> Has(Universe.Lookup<TRelation>(), target); |
||||||
|
|
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets a component value from this entity. If the component is a value |
||||||
|
/// type, this will return a copy. If the component is a reference type, |
||||||
|
/// it will return the reference itself. |
||||||
|
/// When modifying a reference, consider calling <see cref="Modified"/>. |
||||||
|
/// </summary> |
||||||
|
public T Get<T>() |
||||||
|
{ |
||||||
|
var comp = Universe.Lookup<T>(); |
||||||
|
var ptr = ecs_get_id(Universe, this, comp); |
||||||
|
if (typeof(T).IsValueType) { |
||||||
|
return Unsafe.Read<T>(ptr); |
||||||
|
} else { |
||||||
|
var handle = (GCHandle)Unsafe.Read<nint>(ptr); |
||||||
|
return (T)handle.Target!; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Gets a reference to a component value from this entity. Only works for |
||||||
|
/// value types. When modifying, consider calling <see cref="Modified"/>. |
||||||
|
/// </summary> |
||||||
|
public ref T GetRef<T>() |
||||||
|
where T : unmanaged |
||||||
|
{ |
||||||
|
var comp = Universe.Lookup<T>(); |
||||||
|
var ptr = ecs_get_mut_id(Universe, this, comp); |
||||||
|
return ref Unsafe.AsRef<T>(ptr); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Marks a component as modified. Do this after getting a reference to |
||||||
|
/// it with <see cref="Get"/> or <see cref="GetRef"/>, making sure change |
||||||
|
/// detection will kick in. |
||||||
|
/// </summary> |
||||||
|
public void Modified<T>() |
||||||
|
{ |
||||||
|
var comp = Universe.Lookup<T>(); |
||||||
|
ecs_modified_id(Universe, this, comp); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public Entity Set<T>(in T value) |
||||||
|
where T : unmanaged |
||||||
|
{ |
||||||
|
var comp = Universe.Lookup<T>(); |
||||||
|
var size = (ulong)Unsafe.SizeOf<T>(); |
||||||
|
fixed (T* ptr = &value) ecs_set_id(Universe, this, comp, size, ptr); |
||||||
|
return this; |
||||||
|
} |
||||||
|
public Entity SetOverride<T>(in T value) |
||||||
|
where T : unmanaged |
||||||
|
{ |
||||||
|
var comp = Universe.Lookup<T>(); |
||||||
|
var size = (ulong)Unsafe.SizeOf<T>(); |
||||||
|
ecs_add_id(Universe, this, Universe.ECS_OVERRIDE | comp); |
||||||
|
fixed (T* ptr = &value) ecs_set_id(Universe, this, comp, size, ptr); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
public Entity Set(Type type, object obj) |
||||||
|
{ |
||||||
|
var comp = Universe.Lookup(type); |
||||||
|
var handle = (nint)GCHandle.Alloc(obj); |
||||||
|
ecs_set_id(Universe, this, comp, (ulong)sizeof(nint), &handle); |
||||||
|
// FIXME: Handle needs to be freed when component is removed! |
||||||
|
return this; |
||||||
|
} |
||||||
|
public Entity Set<T>(T obj) where T : class
|
||||||
|
=> Set(typeof(T), obj); |
||||||
|
public Entity SetOverride<T>(T obj) |
||||||
|
where T : class
|
||||||
|
{ |
||||||
|
var comp = Universe.Lookup<T>(); |
||||||
|
var handle = (nint)GCHandle.Alloc(obj); |
||||||
|
ecs_add_id(Universe, this, Universe.ECS_OVERRIDE | comp); |
||||||
|
ecs_set_id(Universe, this, comp, (ulong)sizeof(nint), &handle); |
||||||
|
// FIXME: Handle needs to be freed when component is removed! |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static Identifier operator &(Entity first, Entity second) => Identifier.Pair(first, second); |
||||||
|
public static Identifier operator &(ecs_entity_t first, Entity second) => Identifier.Pair(first, second); |
||||||
|
public static Identifier operator |(ecs_id_t left, Entity right) => new(right.Universe, left | right.Value.Data); |
||||||
|
|
||||||
|
public static implicit operator ecs_id_t(Entity e) => e.Value.Data; |
||||||
|
public static implicit operator ecs_entity_t(Entity e) => e.Value; |
||||||
|
public static implicit operator Identifier(Entity e) => new(e.Universe, e); |
||||||
|
} |
||||||
|
|
||||||
|
public unsafe readonly struct EntityType |
||||||
|
: IEnumerable<Identifier> |
||||||
|
{ |
||||||
|
public Universe Universe { get; } |
||||||
|
public unsafe ecs_type_t* Handle { get; } |
||||||
|
|
||||||
|
public int Count => Handle->count; |
||||||
|
public Identifier this[int index] => new(Universe, Handle->array[index]); |
||||||
|
|
||||||
|
public EntityType(Universe universe, ecs_type_t* handle) |
||||||
|
{ Universe = universe; Handle = handle; } |
||||||
|
|
||||||
|
public IEnumerator<Identifier> GetEnumerator() |
||||||
|
{ for (var i = 0; i < Count; i++) yield return this[i]; } |
||||||
|
IEnumerator IEnumerable.GetEnumerator() |
||||||
|
=> GetEnumerator(); |
||||||
|
|
||||||
|
public override string ToString() |
||||||
|
=> ecs_type_str(Universe, Handle).ToStringAndFree(); |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
public struct EntityDesc |
||||||
|
{ |
||||||
|
public ecs_entity_desc_t Value; |
||||||
|
|
||||||
|
public string? Name { get => Value.name; set => Value.name.Set(value); } |
||||||
|
public string? Symbol { get => Value.symbol; set => Value.symbol.Set(value); } |
||||||
|
|
||||||
|
public EntityDesc(params ecs_id_t[] ids) |
||||||
|
{ |
||||||
|
Value = default; |
||||||
|
for (var i = 0; i < ids.Length; i++) |
||||||
|
Value.add[i] = ids[i]; |
||||||
|
} |
||||||
|
|
||||||
|
public static explicit operator ecs_entity_desc_t(EntityDesc desc) => desc.Value; |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections; |
||||||
|
using System.Collections.Generic; |
||||||
|
using gaemstone.Utility.IL; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
public unsafe class Filter |
||||||
|
: IEnumerable<Iterator> |
||||||
|
, IDisposable |
||||||
|
{ |
||||||
|
public Universe Universe { get; } |
||||||
|
public ecs_filter_t* Handle { get; } |
||||||
|
|
||||||
|
private Filter(Universe universe, ecs_filter_t* handle) |
||||||
|
{ Universe = universe; Handle = handle; } |
||||||
|
|
||||||
|
public Filter(Universe universe, ecs_filter_desc_t desc) |
||||||
|
: this(universe, ecs_filter_init(universe, &desc)) { } |
||||||
|
public Filter(Universe universe, string expression) |
||||||
|
: this(universe, new ecs_filter_desc_t { expr = expression }) { } |
||||||
|
|
||||||
|
public static void RunOnce(Universe universe, Delegate action) |
||||||
|
{ |
||||||
|
var gen = QueryActionGenerator.GetOrBuild(universe, action.Method); |
||||||
|
using var filter = new Filter(universe, gen.Filter); |
||||||
|
foreach (var iter in filter) gen.RunWithTryCatch(action.Target, iter); |
||||||
|
} |
||||||
|
|
||||||
|
~Filter() => Dispose(); |
||||||
|
public void Dispose() { ecs_filter_fini(Handle); GC.SuppressFinalize(this); } |
||||||
|
|
||||||
|
public Iterator Iter() => new(Universe, IteratorType.Filter, ecs_filter_iter(Universe, this)); |
||||||
|
public IEnumerator<Iterator> GetEnumerator() => Iter().GetEnumerator(); |
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
||||||
|
|
||||||
|
public static implicit operator ecs_filter_t*(Filter q) => q.Handle; |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
using System; |
||||||
|
using System.Diagnostics; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
public class FlecsException : Exception |
||||||
|
{ |
||||||
|
public FlecsException() : base() { } |
||||||
|
public FlecsException(string message) : base(message) { } |
||||||
|
} |
||||||
|
|
||||||
|
public class FlecsAbortException : FlecsException |
||||||
|
{ |
||||||
|
private readonly string _stackTrace = new StackTrace(2, true).ToString(); |
||||||
|
internal FlecsAbortException() : base("Abort was called by flecs") { } |
||||||
|
public override string? StackTrace => _stackTrace; |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
using System; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
public unsafe readonly struct Identifier |
||||||
|
{ |
||||||
|
public Universe Universe { get; } |
||||||
|
public ecs_id_t Value { get; } |
||||||
|
|
||||||
|
public bool IsPair => ecs_id_is_pair(Value); |
||||||
|
public IdentifierFlags Flags => (IdentifierFlags)(Value.Data & ECS_ID_FLAGS_MASK); |
||||||
|
|
||||||
|
public Identifier(Universe universe, ecs_id_t value) |
||||||
|
{ Universe = universe; Value = value; } |
||||||
|
|
||||||
|
public static Identifier Pair(Entity first, Entity second) |
||||||
|
=> new(first.Universe, Universe.ECS_PAIR | ((first.Value.Data << 32) + (uint)second.Value.Data)); |
||||||
|
public static Identifier Pair(ecs_entity_t first, Entity second) |
||||||
|
=> new(second.Universe, Universe.ECS_PAIR | ((first.Data << 32) + (uint)second.Value.Data)); |
||||||
|
|
||||||
|
public (Entity, Entity) AsPair() |
||||||
|
=> (Universe.Lookup((ecs_id_t)((Value & ECS_COMPONENT_MASK) >> 32)), |
||||||
|
Universe.Lookup((ecs_id_t)(Value & ECS_ENTITY_MASK))); |
||||||
|
|
||||||
|
// public Entity AsComponent() |
||||||
|
// { |
||||||
|
// var value = Value.Data & ECS_COMPONENT_MASK; |
||||||
|
// return new Entity(Universe, new() { Data = value }); |
||||||
|
// } |
||||||
|
|
||||||
|
public override string ToString() |
||||||
|
=> ecs_id_str(Universe, Value).ToStringAndFree(); |
||||||
|
|
||||||
|
|
||||||
|
public static implicit operator ecs_id_t(Identifier e) => e.Value; |
||||||
|
|
||||||
|
public static Identifier operator |(ecs_id_t left, Identifier right) |
||||||
|
=> new(right.Universe, left | right.Value); |
||||||
|
public static Identifier operator |(Identifier left, Identifier right) |
||||||
|
=> new(left.Universe, left.Value | right.Value); |
||||||
|
} |
||||||
|
|
||||||
|
[Flags] |
||||||
|
public enum IdentifierFlags : ulong |
||||||
|
{ |
||||||
|
Pair = 1ul << 63, |
||||||
|
Override = 1ul << 62, |
||||||
|
Toggle = 1ul << 61, |
||||||
|
Or = 1ul << 60, |
||||||
|
And = 1ul << 59, |
||||||
|
Not = 1ul << 58, |
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Runtime.CompilerServices; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
public unsafe class Iterator |
||||||
|
: IEnumerable<Iterator> |
||||||
|
{ |
||||||
|
public Universe Universe { get; } |
||||||
|
public IteratorType? Type { get; } |
||||||
|
public ecs_iter_t Value; |
||||||
|
|
||||||
|
public int Count => Value.count; |
||||||
|
public TimeSpan DeltaTime => TimeSpan.FromSeconds(Value.delta_time); |
||||||
|
public TimeSpan DeltaSystemTime => TimeSpan.FromSeconds(Value.delta_system_time); |
||||||
|
|
||||||
|
public Iterator(Universe universe, IteratorType? type, ecs_iter_t value) |
||||||
|
{ Universe = universe; Type = type; Value = value; } |
||||||
|
|
||||||
|
public static Iterator FromTerm(Universe universe, in ecs_term_t term) |
||||||
|
{ |
||||||
|
fixed (ecs_term_t* ptr = &term) |
||||||
|
return new(universe, IteratorType.Term, ecs_term_iter(universe, ptr)); |
||||||
|
} |
||||||
|
|
||||||
|
public bool Next() |
||||||
|
{ |
||||||
|
fixed (ecs_iter_t* ptr = &Value) |
||||||
|
return Type switch { |
||||||
|
IteratorType.Term => ecs_term_next(ptr), |
||||||
|
IteratorType.Filter => ecs_filter_next(ptr), |
||||||
|
IteratorType.Query => ecs_query_next(ptr), |
||||||
|
IteratorType.Rule => ecs_rule_next(ptr), |
||||||
|
_ => ecs_iter_next(ptr), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
public Entity Entity(int index) |
||||||
|
=> new(Universe, Value.entities[index]); |
||||||
|
|
||||||
|
public Span<T> Field<T>(int index) |
||||||
|
where T : unmanaged |
||||||
|
{ |
||||||
|
fixed (ecs_iter_t* ptr = &Value) { |
||||||
|
var size = (ulong)Unsafe.SizeOf<T>(); |
||||||
|
var pointer = ecs_field_w_size(ptr, size, index); |
||||||
|
return new Span<T>(pointer, Count); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public SpanToRef<T> FieldRef<T>(int index) |
||||||
|
where T : class => new(Field<nint>(index)); |
||||||
|
|
||||||
|
public bool FieldIsSet(int index) |
||||||
|
{ |
||||||
|
fixed (ecs_iter_t* ptr = &Value) |
||||||
|
return ecs_field_is_set(ptr, index); |
||||||
|
} |
||||||
|
|
||||||
|
public bool FieldIs<T>(int index) |
||||||
|
where T : unmanaged |
||||||
|
{ |
||||||
|
fixed (ecs_iter_t* ptr = &Value) { |
||||||
|
var id = ecs_field_id(ptr, index); |
||||||
|
var comp = Universe.Lookup<T>(); |
||||||
|
return id == comp.Value.Data; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public readonly ref struct SpanToRef<T> |
||||||
|
{ |
||||||
|
private readonly Span<nint> _span; |
||||||
|
internal SpanToRef(Span<nint> span) => _span = span; |
||||||
|
public int Length => _span.Length; |
||||||
|
public T this[int index] => (T)((GCHandle)_span[index]).Target!; |
||||||
|
} |
||||||
|
|
||||||
|
// IEnumerable implementation |
||||||
|
public IEnumerator<Iterator> GetEnumerator() { while (Next()) yield return this; } |
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
||||||
|
} |
||||||
|
|
||||||
|
public enum IteratorType |
||||||
|
{ |
||||||
|
Term, |
||||||
|
Filter, |
||||||
|
Query, |
||||||
|
Rule, |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
using System; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)] |
||||||
|
public class ModuleAttribute : Attribute { } |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] |
||||||
|
public class DependsOnAttribute : Attribute |
||||||
|
{ |
||||||
|
public Type Target { get; } |
||||||
|
public DependsOnAttribute(Type target) => Target = target; |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
using System; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method)] |
||||||
|
public class ObserverAttribute : Attribute |
||||||
|
{ |
||||||
|
public Event Event { get; } |
||||||
|
public ObserverAttribute(Event @event) |
||||||
|
=> Event = @event; |
||||||
|
} |
||||||
|
|
||||||
|
public enum Event |
||||||
|
{ |
||||||
|
OnAdd, |
||||||
|
OnSet, |
||||||
|
OnRemove, |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections; |
||||||
|
using System.Collections.Generic; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
public unsafe class Query |
||||||
|
: IEnumerable<Iterator> |
||||||
|
, IDisposable |
||||||
|
{ |
||||||
|
public Universe Universe { get; } |
||||||
|
public ecs_query_t* Handle { get; } |
||||||
|
|
||||||
|
private Query(Universe universe, ecs_query_t* handle) |
||||||
|
{ Universe = universe; Handle = handle; } |
||||||
|
|
||||||
|
public Query(Universe universe, ecs_query_desc_t desc) |
||||||
|
: this(universe, ecs_query_init(universe, &desc)) { } |
||||||
|
public Query(Universe universe, ecs_filter_desc_t desc) |
||||||
|
: this(universe, new ecs_query_desc_t { filter = desc }) { } |
||||||
|
public Query(Universe universe, string expression) |
||||||
|
: this(universe, new ecs_filter_desc_t { expr = expression }) { } |
||||||
|
|
||||||
|
~Query() => Dispose(); |
||||||
|
public void Dispose() { ecs_query_fini(this); GC.SuppressFinalize(this); } |
||||||
|
|
||||||
|
public Iterator Iter() => new(Universe, IteratorType.Query, ecs_query_iter(Universe, this)); |
||||||
|
public IEnumerator<Iterator> GetEnumerator() => Iter().GetEnumerator(); |
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
||||||
|
|
||||||
|
public static implicit operator ecs_query_t*(Query q) => q.Handle; |
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
using System; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method)] |
||||||
|
public class SystemAttribute : Attribute |
||||||
|
{ |
||||||
|
public string? Expression { get; set; } |
||||||
|
public Phase Phase { get; set; } |
||||||
|
|
||||||
|
public SystemAttribute() : this(Phase.OnUpdate) { } |
||||||
|
public SystemAttribute(Phase phase) => Phase = phase; |
||||||
|
} |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter)] |
||||||
|
public class SourceAttribute : Attribute |
||||||
|
{ |
||||||
|
public Type Type { get; } |
||||||
|
public SourceAttribute(Type type) => Type = type; |
||||||
|
} |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter)] |
||||||
|
public class HasAttribute : Attribute { } |
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter)] |
||||||
|
public class NotAttribute : Attribute { } |
||||||
|
|
||||||
|
public enum Phase |
||||||
|
{ |
||||||
|
PreFrame, |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// This phase contains all the systems that load data into your ECS. |
||||||
|
/// This would be a good place to load keyboard and mouse inputs. |
||||||
|
/// </summary> |
||||||
|
OnLoad, |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Often the imported data needs to be processed. Maybe you want to associate |
||||||
|
/// your keypresses with high level actions rather than comparing explicitly |
||||||
|
/// in your game code if the user pressed the 'K' key. |
||||||
|
/// </summary> |
||||||
|
PostLoad, |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Now that the input is loaded and processed, it's time to get ready to |
||||||
|
/// start processing our game logic. Anything that needs to happen after |
||||||
|
/// input processing but before processing the game logic can happen here. |
||||||
|
/// This can be a good place to prepare the frame, maybe clean up some |
||||||
|
/// things from the previous frame, etcetera. |
||||||
|
/// </summary> |
||||||
|
PreUpdate, |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// This is usually where the magic happens! This is where you put all of |
||||||
|
/// your gameplay systems. By default systems are added to this phase. |
||||||
|
/// </summary> |
||||||
|
OnUpdate, |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// This phase was introduced to deal with validating the state of the game |
||||||
|
/// after processing the gameplay systems. Sometimes you entities too close |
||||||
|
/// to each other, or the speed of an entity is increased too much. |
||||||
|
/// This phase is for righting that wrong. A typical feature to implement |
||||||
|
/// in this phase would be collision detection. |
||||||
|
/// </summary> |
||||||
|
OnValidate, |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// When your game logic has been updated, and your validation pass has ran, |
||||||
|
/// you may want to apply some corrections. For example, if your collision |
||||||
|
/// detection system detected collisions in the <c>OnValidate</c> phase, |
||||||
|
/// you may want to move the entities so that they no longer overlap. |
||||||
|
/// </summary> |
||||||
|
PostUpdate, |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Now that all of the frame data is computed, validated and corrected for, |
||||||
|
/// it is time to prepare the frame for rendering. Any systems that need to |
||||||
|
/// run before rendering, but after processing the game logic should go here. |
||||||
|
/// A good example would be a system that calculates transform matrices from |
||||||
|
/// a scene graph. |
||||||
|
/// </summary> |
||||||
|
PreStore, |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// This is where it all comes together. Your frame is ready to be |
||||||
|
/// rendered, and that is exactly what you would do in this phase. |
||||||
|
/// </summary> |
||||||
|
OnStore, |
||||||
|
|
||||||
|
PostFrame, |
||||||
|
} |
@ -0,0 +1,140 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Collections.Immutable; |
||||||
|
using System.Linq; |
||||||
|
using System.Reflection; |
||||||
|
using gaemstone.Utility; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
public unsafe partial class Universe |
||||||
|
{ |
||||||
|
public void RegisterModule<T>() where T : class
|
||||||
|
=> RegisterModule(typeof(T)); |
||||||
|
public void RegisterModule(Type type) |
||||||
|
{ |
||||||
|
var builder = new ModuleBuilder(this, type); |
||||||
|
|
||||||
|
builder.UnmetDependencies.ExceptWith(Modules._modules.Keys); |
||||||
|
if (builder.UnmetDependencies.Count > 0) { |
||||||
|
// If builder has unmet dependencies, defer the registration. |
||||||
|
Modules._deferred.Add(type, builder); |
||||||
|
} else { |
||||||
|
// Otherwise register it right away, .. |
||||||
|
Modules._modules.Add(type, new ModuleInfo(builder)); |
||||||
|
// .. and tell other deferred modules this one is now loaded. |
||||||
|
RemoveDependency(builder.Type); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void RemoveDependency(Type type) |
||||||
|
{ |
||||||
|
var resolved = Modules._deferred.Values |
||||||
|
.Where(d => d.UnmetDependencies.Remove(type) |
||||||
|
&& d.UnmetDependencies.Count == 0) |
||||||
|
.ToArray(); |
||||||
|
foreach (var builder in resolved) { |
||||||
|
Modules._deferred.Remove(builder.Type); |
||||||
|
Modules._modules.Add(type, new ModuleInfo(builder)); |
||||||
|
RemoveDependency(builder.Type); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public class UniverseModules |
||||||
|
{ |
||||||
|
internal readonly Dictionary<Type, ModuleInfo> _modules = new(); |
||||||
|
internal readonly Dictionary<Type, ModuleBuilder> _deferred = new(); |
||||||
|
|
||||||
|
internal UniverseModules(Universe universe) { } |
||||||
|
} |
||||||
|
|
||||||
|
public class ModuleInfo |
||||||
|
{ |
||||||
|
public Universe Universe { get; } |
||||||
|
public object Instance { get; } |
||||||
|
|
||||||
|
public IReadOnlyList<Entity> Relations { get; } |
||||||
|
public IReadOnlyList<Entity> Components { get; } |
||||||
|
public IReadOnlyList<Entity> Tags { get; } |
||||||
|
public IReadOnlyList<Entity> Entities { get; } |
||||||
|
public IReadOnlyList<SystemInfo> Systems { get; } |
||||||
|
|
||||||
|
internal ModuleInfo(ModuleBuilder builder) |
||||||
|
{ |
||||||
|
Universe = builder.Universe; |
||||||
|
Instance = builder.HasSimpleConstructor |
||||||
|
? Activator.CreateInstance(builder.Type)! |
||||||
|
: Activator.CreateInstance(builder.Type, Universe)!; |
||||||
|
|
||||||
|
Relations = builder.Relations .Select(Universe.RegisterRelation ).ToImmutableList(); |
||||||
|
Components = builder.Components.Select(Universe.RegisterComponent).ToImmutableList(); |
||||||
|
Tags = builder.Tags .Select(Universe.RegisterTag ).ToImmutableList(); |
||||||
|
Entities = builder.Entities .Select(Universe.RegisterEntity ).ToImmutableList(); |
||||||
|
|
||||||
|
Systems = builder.Systems.Select(s => Universe.RegisterSystem(Instance, s)).ToImmutableList(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public class ModuleBuilder |
||||||
|
{ |
||||||
|
public Universe Universe { get; } |
||||||
|
public Type Type { get; } |
||||||
|
public IReadOnlyList<Type> DependsOn { get; } |
||||||
|
public bool HasSimpleConstructor { get; } |
||||||
|
|
||||||
|
public IReadOnlyList<Type> Relations { get; } |
||||||
|
public IReadOnlyList<Type> Components { get; } |
||||||
|
public IReadOnlyList<Type> Tags { get; } |
||||||
|
public IReadOnlyList<Type> Entities { get; } |
||||||
|
public IReadOnlyList<MethodInfo> Systems { get; } |
||||||
|
|
||||||
|
public HashSet<Type> UnmetDependencies { get; } |
||||||
|
|
||||||
|
internal ModuleBuilder(Universe universe, Type type) |
||||||
|
{ |
||||||
|
if (!type.IsClass || type.IsAbstract) throw new Exception( |
||||||
|
"Module must be a non-abstract class"); |
||||||
|
if (!type.Has<ModuleAttribute>()) throw new Exception( |
||||||
|
"Module must be marked with ModuleAttribute"); |
||||||
|
|
||||||
|
Universe = universe; |
||||||
|
Type = type; |
||||||
|
DependsOn = type.GetMultiple<DependsOnAttribute>() |
||||||
|
.Select(d => d.Target).ToImmutableList(); |
||||||
|
|
||||||
|
HasSimpleConstructor = type.GetConstructor(Type.EmptyTypes) != null; |
||||||
|
var hasUniverseConstructor = type.GetConstructor(new[] { typeof(Universe) }) != null; |
||||||
|
if (!HasSimpleConstructor && !hasUniverseConstructor) throw new Exception( |
||||||
|
$"Module {Type} must define a public constructor with either no parameters, or a single {nameof(Universe)} parameter"); |
||||||
|
|
||||||
|
var relations = new List<Type>(); |
||||||
|
var components = new List<Type>(); |
||||||
|
var tags = new List<Type>(); |
||||||
|
var entities = new List<Type>(); |
||||||
|
var systems = new List<MethodInfo>(); |
||||||
|
|
||||||
|
foreach (var nested in Type.GetNestedTypes()) { |
||||||
|
if (nested.Has<RelationAttribute>()) relations.Add(nested); |
||||||
|
else if (nested.Has<ComponentAttribute>()) components.Add(nested); |
||||||
|
else if (nested.Has<TagAttribute>()) tags.Add(nested); |
||||||
|
else if (nested.Has<EntityAttribute>()) entities.Add(nested); |
||||||
|
} |
||||||
|
foreach (var method in Type.GetMethods()) |
||||||
|
if (method.Has<SystemAttribute>()) |
||||||
|
systems.Add(method); |
||||||
|
|
||||||
|
var elements = new IList[] { relations, components, tags, entities, systems }; |
||||||
|
if (elements.Sum(l => l.Count) == 0) throw new Exception( |
||||||
|
"Module must define at least one ECS related type or method"); |
||||||
|
|
||||||
|
Relations = relations.AsReadOnly(); |
||||||
|
Components = components.AsReadOnly(); |
||||||
|
Tags = tags.AsReadOnly(); |
||||||
|
Entities = entities.AsReadOnly(); |
||||||
|
Systems = systems.AsReadOnly(); |
||||||
|
|
||||||
|
UnmetDependencies = DependsOn.ToHashSet(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,167 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Reflection; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using System.Threading; |
||||||
|
using gaemstone.Utility; |
||||||
|
using gaemstone.Utility.IL; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
public unsafe partial class Universe |
||||||
|
{ |
||||||
|
public SystemInfo RegisterSystem(Action<Iterator> callback, string expression, |
||||||
|
Phase? phase = null, string? name = null) |
||||||
|
=> RegisterSystem(name ?? callback.Method.Name, expression, phase ?? Phase.OnUpdate, new() { expr = expression }, callback); |
||||||
|
public SystemInfo RegisterSystem(Action<Iterator> callback, ecs_filter_desc_t filter, |
||||||
|
Phase? phase = null, string? name = null) |
||||||
|
=> RegisterSystem(name ?? callback.Method.Name, null, phase ?? Phase.OnUpdate, filter, callback); |
||||||
|
|
||||||
|
public SystemInfo RegisterSystem(string name, string? expression, |
||||||
|
Phase phase, ecs_filter_desc_t filter, Action<Iterator> callback) |
||||||
|
{ |
||||||
|
var _phase = Systems._phaseLookup[phase]; |
||||||
|
var entityDesc = default(ecs_entity_desc_t); |
||||||
|
entityDesc.name = name; |
||||||
|
entityDesc.add[0] = !_phase.IsNone ? (EcsDependsOn & _phase) : default; |
||||||
|
entityDesc.add[1] = _phase; |
||||||
|
// TODO: Provide a nice way to create these entity descriptors. |
||||||
|
|
||||||
|
var systemDesc = default(ecs_system_desc_t); |
||||||
|
systemDesc.entity = Create(entityDesc); |
||||||
|
systemDesc.binding_ctx = (void*)UniverseSystems.CreateSystemCallbackContext(this, callback); |
||||||
|
systemDesc.callback.Data.Pointer = &UniverseSystems.SystemCallback; |
||||||
|
systemDesc.query.filter = filter; |
||||||
|
|
||||||
|
var entity = new Entity(this, ecs_system_init(Handle, &systemDesc)); |
||||||
|
var system = new SystemInfo(this, entity, name, expression, phase, filter, callback); |
||||||
|
Systems._systems.Add(system); |
||||||
|
return system; |
||||||
|
} |
||||||
|
|
||||||
|
public SystemInfo RegisterSystem(Delegate action) |
||||||
|
{ |
||||||
|
var name = action.Method.Name; |
||||||
|
var attr = action.Method.Get<SystemAttribute>(); |
||||||
|
var phase = attr?.Phase ?? Phase.OnUpdate; |
||||||
|
|
||||||
|
if (action is Action<Iterator> iterAction) { |
||||||
|
if (attr?.Expression == null) throw new Exception( |
||||||
|
"System must specify expression in SystemAttribute"); |
||||||
|
return RegisterSystem(name, attr.Expression, phase, new() { expr = attr.Expression }, iterAction); |
||||||
|
} else { |
||||||
|
var method = action.GetType().GetMethod("Invoke")!; |
||||||
|
var gen = QueryActionGenerator.GetOrBuild(this, method); |
||||||
|
var filter = (attr?.Expression == null) ? gen.Filter : new() { expr = attr.Expression }; |
||||||
|
return RegisterSystem(name, attr?.Expression, phase, filter, |
||||||
|
iter => gen.RunWithTryCatch(action.Target, iter)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public SystemInfo RegisterSystem(object? instance, MethodInfo method) |
||||||
|
{ |
||||||
|
var attr = method.Get<SystemAttribute>(); |
||||||
|
var phase = attr?.Phase ?? Phase.OnUpdate; |
||||||
|
|
||||||
|
var param = method.GetParameters(); |
||||||
|
if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) { |
||||||
|
if (attr?.Expression == null) throw new Exception( |
||||||
|
"System must specify expression in SystemAttribute"); |
||||||
|
var action = (Action<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), instance, method); |
||||||
|
return RegisterSystem(method.Name, attr.Expression, phase, new() { expr = attr.Expression }, action); |
||||||
|
} else { |
||||||
|
var gen = QueryActionGenerator.GetOrBuild(this, method); |
||||||
|
var filter = (attr?.Expression == null) ? gen.Filter : new() { expr = attr.Expression }; |
||||||
|
return RegisterSystem(method.Name, attr?.Expression, phase, filter, |
||||||
|
iter => gen.RunWithTryCatch(instance, iter)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public class UniverseSystems |
||||||
|
: IReadOnlyCollection<SystemInfo> |
||||||
|
{ |
||||||
|
public readonly struct SystemCallbackContext |
||||||
|
{ |
||||||
|
public Universe Universe { get; } |
||||||
|
public Action<Iterator> Callback { get; } |
||||||
|
|
||||||
|
public SystemCallbackContext(Universe universe, Action<Iterator> callback) |
||||||
|
{ Universe = universe; Callback = callback; } |
||||||
|
} |
||||||
|
|
||||||
|
private static SystemCallbackContext[] _systemCallbackContexts = new SystemCallbackContext[64]; |
||||||
|
private static int _systemCallbackContextsCount = 0; |
||||||
|
|
||||||
|
public static nint CreateSystemCallbackContext(Universe universe, Action<Iterator> callback) |
||||||
|
{ |
||||||
|
var data = new SystemCallbackContext(universe, callback); |
||||||
|
var count = Interlocked.Increment(ref _systemCallbackContextsCount); |
||||||
|
if (count > _systemCallbackContexts.Length) |
||||||
|
Array.Resize(ref _systemCallbackContexts, count * 2); |
||||||
|
_systemCallbackContexts[count - 1] = data; |
||||||
|
return count; |
||||||
|
} |
||||||
|
|
||||||
|
public static SystemCallbackContext GetSystemCallbackContext(nint context) |
||||||
|
=> _systemCallbackContexts[(int)context - 1]; |
||||||
|
|
||||||
|
[UnmanagedCallersOnly] |
||||||
|
internal static void SystemCallback(ecs_iter_t* iter) |
||||||
|
{ |
||||||
|
var data = GetSystemCallbackContext((nint)iter->binding_ctx); |
||||||
|
data.Callback(new Iterator(data.Universe, null, *iter)); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
internal readonly List<SystemInfo> _systems = new(); |
||||||
|
internal readonly Dictionary<Phase, Entity> _phaseLookup = new(); |
||||||
|
|
||||||
|
internal UniverseSystems(Universe universe) |
||||||
|
{ |
||||||
|
_phaseLookup.Add(Phase.PreFrame, new(universe, pinvoke_EcsPreFrame())); |
||||||
|
_phaseLookup.Add(Phase.OnLoad, new(universe, pinvoke_EcsOnLoad())); |
||||||
|
_phaseLookup.Add(Phase.PostLoad, new(universe, pinvoke_EcsPostLoad())); |
||||||
|
_phaseLookup.Add(Phase.PreUpdate, new(universe, pinvoke_EcsPreUpdate())); |
||||||
|
_phaseLookup.Add(Phase.OnUpdate, new(universe, pinvoke_EcsOnUpdate())); |
||||||
|
_phaseLookup.Add(Phase.OnValidate, new(universe, pinvoke_EcsOnValidate())); |
||||||
|
_phaseLookup.Add(Phase.PostUpdate, new(universe, pinvoke_EcsPostUpdate())); |
||||||
|
_phaseLookup.Add(Phase.PreStore, new(universe, pinvoke_EcsPreStore())); |
||||||
|
_phaseLookup.Add(Phase.OnStore, new(universe, pinvoke_EcsOnStore())); |
||||||
|
_phaseLookup.Add(Phase.PostFrame, new(universe, pinvoke_EcsPostFrame())); |
||||||
|
} |
||||||
|
|
||||||
|
// IReadOnlyCollection implementation |
||||||
|
public int Count => _systems.Count; |
||||||
|
public IEnumerator<SystemInfo> GetEnumerator() => _systems.GetEnumerator(); |
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
||||||
|
} |
||||||
|
|
||||||
|
public class SystemInfo |
||||||
|
{ |
||||||
|
public Universe Universe { get; } |
||||||
|
public Entity Entity { get; } |
||||||
|
|
||||||
|
public string Name { get; } |
||||||
|
public string? Expression { get; } |
||||||
|
|
||||||
|
public Phase Phase { get; } |
||||||
|
public ecs_filter_desc_t Filter { get; } |
||||||
|
public Action<Iterator> Callback { get; } |
||||||
|
|
||||||
|
internal SystemInfo(Universe universe, Entity entity, string name, string? expression, |
||||||
|
Phase phase, ecs_filter_desc_t filter, Action<Iterator> callback) |
||||||
|
{ |
||||||
|
Universe = universe; |
||||||
|
Entity = entity; |
||||||
|
|
||||||
|
Name = name; |
||||||
|
Expression = expression; |
||||||
|
|
||||||
|
Phase = phase; |
||||||
|
Filter = filter; |
||||||
|
Callback = callback; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,184 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Diagnostics; |
||||||
|
using System.Linq; |
||||||
|
using System.Reflection; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using gaemstone.Utility; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone.ECS; |
||||||
|
|
||||||
|
[Entity] |
||||||
|
public struct Game { } |
||||||
|
|
||||||
|
[Component] |
||||||
|
public unsafe partial class Universe |
||||||
|
{ |
||||||
|
// Roles |
||||||
|
public static ecs_id_t ECS_PAIR { get; } = pinvoke_ECS_PAIR(); |
||||||
|
public static ecs_id_t ECS_OVERRIDE { get; } = pinvoke_ECS_OVERRIDE(); |
||||||
|
|
||||||
|
// Relationships |
||||||
|
public static ecs_entity_t EcsIsA { get; } = pinvoke_EcsIsA(); |
||||||
|
public static ecs_entity_t EcsDependsOn { get; } = pinvoke_EcsDependsOn(); |
||||||
|
public static ecs_entity_t EcsChildOf { get; } = pinvoke_EcsChildOf(); |
||||||
|
public static ecs_entity_t EcsSlotOf { get; } = pinvoke_EcsSlotOf(); |
||||||
|
|
||||||
|
// Entity Tags |
||||||
|
public static ecs_entity_t EcsPrefab { get; } = pinvoke_EcsPrefab(); |
||||||
|
|
||||||
|
|
||||||
|
private readonly Dictionary<Type, ecs_entity_t> _byType = new(); |
||||||
|
|
||||||
|
public ecs_world_t* Handle { get; } |
||||||
|
|
||||||
|
public UniverseSystems Systems { get; } |
||||||
|
public UniverseModules Modules { get; } |
||||||
|
|
||||||
|
public Universe(string[]? args = null) |
||||||
|
{ |
||||||
|
[UnmanagedCallersOnly] |
||||||
|
static void Abort() => throw new FlecsAbortException(); |
||||||
|
|
||||||
|
ecs_os_set_api_defaults(); |
||||||
|
var api = ecs_os_get_api(); |
||||||
|
api.abort_ = new FnPtr_Void { Pointer = &Abort }; |
||||||
|
ecs_os_set_api(&api); |
||||||
|
|
||||||
|
if (args?.Length > 0) { |
||||||
|
var argv = Runtime.CStrings.CStringArray(args); |
||||||
|
Handle = ecs_init_w_args(args.Length, argv); |
||||||
|
Runtime.CStrings.FreeCStrings(argv, args.Length); |
||||||
|
} else { |
||||||
|
Handle = ecs_init(); |
||||||
|
} |
||||||
|
|
||||||
|
Systems = new(this); |
||||||
|
Modules = new(this); |
||||||
|
|
||||||
|
RegisterAll(typeof(Universe).Assembly); |
||||||
|
} |
||||||
|
|
||||||
|
public bool Progress(TimeSpan delta) |
||||||
|
{ |
||||||
|
if (Modules._deferred.Count > 0) throw new Exception( |
||||||
|
"Modules with unmet dependencies: \n" + |
||||||
|
string.Join(" \n", Modules._deferred.Values.Select( |
||||||
|
m => m.Type + " is missing " + string.Join(", ", m.UnmetDependencies)))); |
||||||
|
return ecs_progress(this, (float)delta.TotalSeconds); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public Entity Lookup<T>() |
||||||
|
=> Lookup(typeof(T)); |
||||||
|
public Entity Lookup(Type type) |
||||||
|
=> _byType.TryGetValue(type, out var e) ? new(this, e) : default; |
||||||
|
|
||||||
|
public Entity Lookup(string path) |
||||||
|
=> new(this, !path.Contains('.') ? ecs_lookup(this, path) |
||||||
|
: ecs_lookup_path_w_sep(this, default, path, ".", default, true)); |
||||||
|
public Entity Lookup(ecs_entity_t value) |
||||||
|
=> new(this, ecs_get_alive(this, value)); |
||||||
|
|
||||||
|
|
||||||
|
public void RegisterAll(Assembly? from = null) |
||||||
|
{ |
||||||
|
from ??= Assembly.GetEntryAssembly()!; |
||||||
|
foreach (var type in from.GetTypes()) { |
||||||
|
var isPartOfModule = type.DeclaringType?.Has<ModuleAttribute>() == true; |
||||||
|
if (type.Has<RelationAttribute>()) { |
||||||
|
if (!isPartOfModule) RegisterRelation(type); |
||||||
|
} else if (type.Has<ComponentAttribute>()) { |
||||||
|
if (!isPartOfModule) RegisterComponent(type); |
||||||
|
} else if (type.Has<TagAttribute>()) { |
||||||
|
if (!isPartOfModule) RegisterTag(type); |
||||||
|
} else if (type.Has<EntityAttribute>()) { |
||||||
|
if (!isPartOfModule) RegisterEntity(type); |
||||||
|
} else if (type.Has<ModuleAttribute>()) |
||||||
|
RegisterModule(type); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public Entity RegisterRelation<T>() |
||||||
|
=> RegisterRelation(typeof(T)); |
||||||
|
public Entity RegisterRelation(Type type) |
||||||
|
=> throw new NotImplementedException(); |
||||||
|
|
||||||
|
public Entity RegisterComponent<T>() |
||||||
|
=> RegisterComponent(typeof(T)); |
||||||
|
public Entity RegisterComponent(Type type) |
||||||
|
{ |
||||||
|
var typeInfo = default(ecs_type_info_t); |
||||||
|
if (type.IsValueType) { |
||||||
|
var wrapper = TypeWrapper.For(type); |
||||||
|
if (!wrapper.IsUnmanaged) throw new Exception( |
||||||
|
"Component struct must satisfy the unmanaged constraint. " + |
||||||
|
"(Must not contain any reference types or structs that contain references.)"); |
||||||
|
var structLayout = type.StructLayoutAttribute; |
||||||
|
if (structLayout == null || structLayout.Value == LayoutKind.Auto) throw new Exception( |
||||||
|
"Component struct must have a StructLayout attribute with LayoutKind sequential or explicit. " + |
||||||
|
"This is to ensure that the struct fields are not reorganized by the C# compiler."); |
||||||
|
typeInfo.size = wrapper.Size; |
||||||
|
typeInfo.alignment = structLayout.Pack; |
||||||
|
} else { |
||||||
|
typeInfo.size = sizeof(nint); |
||||||
|
typeInfo.alignment = sizeof(nint); |
||||||
|
} |
||||||
|
|
||||||
|
var name = type.GetFriendlyName(); |
||||||
|
var entityDesc = new ecs_entity_desc_t { name = name, symbol = name }; |
||||||
|
var componentDesc = new ecs_component_desc_t { entity = Create(entityDesc), type = typeInfo }; |
||||||
|
|
||||||
|
var id = ecs_component_init(Handle, &componentDesc); |
||||||
|
_byType[type] = id; |
||||||
|
// TODO: SetHooks(hooks, id); |
||||||
|
var entity = new Entity(this, id); |
||||||
|
|
||||||
|
if (type.Has<EntityAttribute>()) { |
||||||
|
if (type.IsValueType) entity.Add(entity); |
||||||
|
else entity.Set(type, Activator.CreateInstance(type)!); |
||||||
|
} |
||||||
|
|
||||||
|
return entity; |
||||||
|
} |
||||||
|
|
||||||
|
public Entity RegisterTag<T>() |
||||||
|
where T : unmanaged |
||||||
|
=> RegisterTag(typeof(T)); |
||||||
|
public Entity RegisterTag(Type type) |
||||||
|
{ |
||||||
|
if (!type.IsValueType || type.IsPrimitive || type.GetFields().Length > 0) |
||||||
|
throw new Exception("Tag must be an empty, used-defined struct."); |
||||||
|
var entity = Create(type.GetFriendlyName()); |
||||||
|
_byType.Add(type, entity); |
||||||
|
return entity; |
||||||
|
} |
||||||
|
|
||||||
|
public Entity RegisterEntity<T>() |
||||||
|
where T : unmanaged |
||||||
|
=> RegisterEntity(typeof(T)); |
||||||
|
public Entity RegisterEntity(Type type) |
||||||
|
{ |
||||||
|
if (!type.IsValueType || type.IsPrimitive || type.GetFields().Length > 0) |
||||||
|
throw new Exception("Entity must be an empty, used-defined struct."); |
||||||
|
var entity = Create(type.GetFriendlyName()); |
||||||
|
_byType.Add(type, entity); |
||||||
|
return entity; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public Entity Create() |
||||||
|
=> Create(new ecs_entity_desc_t()); |
||||||
|
public Entity Create(string name) |
||||||
|
=> Create(new ecs_entity_desc_t { name = name }); |
||||||
|
public Entity Create(ecs_entity_desc_t desc) |
||||||
|
{ |
||||||
|
var entity = ecs_entity_init(Handle, &desc); |
||||||
|
Debug.Assert(entity.Data != 0, "ECS_INVALID_PARAMETER"); |
||||||
|
return new(this, entity); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static implicit operator ecs_world_t*(Universe w) => w.Handle; |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
using gaemstone.ECS; |
||||||
|
using Silk.NET.Maths; |
||||||
|
|
||||||
|
namespace gaemstone; |
||||||
|
|
||||||
|
[Component] |
||||||
|
public struct GlobalTransform |
||||||
|
{ |
||||||
|
public Matrix4X4<float> Value; |
||||||
|
public GlobalTransform(Matrix4X4<float> value) => Value = value; |
||||||
|
public static implicit operator GlobalTransform(in Matrix4X4<float> value) => new(value); |
||||||
|
public static implicit operator Matrix4X4<float>(in GlobalTransform index) => index.Value; |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
using System.Runtime.InteropServices; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone; |
||||||
|
|
||||||
|
internal static class CStringExtensions |
||||||
|
{ |
||||||
|
public static string ToStringAndFree(this Runtime.CString str) |
||||||
|
{ |
||||||
|
var result = Marshal.PtrToStringAnsi(str)!; |
||||||
|
Marshal.FreeHGlobal(str); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
public static void Set(this ref Runtime.CString str, string? value) |
||||||
|
{ |
||||||
|
if (!str.IsNull) Marshal.FreeHGlobal(str); |
||||||
|
str = (value != null) ? new(Marshal.StringToHGlobalAnsi(value)) : default; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,291 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Reflection; |
||||||
|
using System.Reflection.Emit; |
||||||
|
using System.Text; |
||||||
|
|
||||||
|
namespace gaemstone.Utility.IL; |
||||||
|
|
||||||
|
public class ILGeneratorWrapper |
||||||
|
{ |
||||||
|
private readonly DynamicMethod _method; |
||||||
|
private readonly ILGenerator _il; |
||||||
|
private readonly List<ILocal> _locals = new(); |
||||||
|
private readonly List<(int Offset, int Indent, OpCode Code, object? Arg)> _instructions = new(); |
||||||
|
private readonly Dictionary<Label, int> _labelToOffset = new(); |
||||||
|
private readonly Stack<BlockImpl> _indents = new(); |
||||||
|
|
||||||
|
public ILGeneratorWrapper(DynamicMethod method) |
||||||
|
{ |
||||||
|
_method = method; |
||||||
|
_il = method.GetILGenerator(); |
||||||
|
} |
||||||
|
|
||||||
|
public string ToReadableString() |
||||||
|
{ |
||||||
|
var sb = new StringBuilder(); |
||||||
|
sb.AppendLine("Parameters:"); |
||||||
|
foreach (var (param, index) in _method.GetParameters().Select((p, i) => (p, i))) |
||||||
|
sb.AppendLine($" Argument({index}, {param.ParameterType.Name})"); |
||||||
|
sb.AppendLine("Return:"); |
||||||
|
sb.AppendLine($" {_method.ReturnType.Name}"); |
||||||
|
sb.AppendLine(); |
||||||
|
|
||||||
|
sb.AppendLine("Locals:"); |
||||||
|
foreach (var local in _locals) |
||||||
|
sb.AppendLine($" {local}"); |
||||||
|
sb.AppendLine(); |
||||||
|
|
||||||
|
sb.AppendLine("Instructions:"); |
||||||
|
foreach (var (offset, indent, code, arg) in _instructions) { |
||||||
|
sb.Append(" "); |
||||||
|
|
||||||
|
// Append instruction offset. |
||||||
|
if (offset < 0) sb.Append(" "); |
||||||
|
else sb.Append($"0x{offset:X4} "); |
||||||
|
|
||||||
|
// Append instruction opcode. |
||||||
|
if (code == OpCodes.Nop) sb.Append(" "); |
||||||
|
else sb.Append($"{code.Name,-12}"); |
||||||
|
|
||||||
|
// Append indents. |
||||||
|
for (var i = 0; i < indent; i++) |
||||||
|
sb.Append("| "); |
||||||
|
|
||||||
|
// Append instruction argument. |
||||||
|
if (code == OpCodes.Nop) sb.Append("// "); |
||||||
|
switch (arg) { |
||||||
|
case Label label: sb.Append($"Label(0x{_labelToOffset.GetValueOrDefault(label, -1):X4})"); break; |
||||||
|
case not null: sb.Append(arg); break; |
||||||
|
} |
||||||
|
|
||||||
|
sb.AppendLine(); |
||||||
|
} |
||||||
|
return sb.ToString(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public IArgument Argument(int index) |
||||||
|
{ |
||||||
|
var type = _method.GetParameters()[index].ParameterType; |
||||||
|
if (type.IsByRefLike) return new ArgumentImpl(index, type); |
||||||
|
return (IArgument)Activator.CreateInstance(typeof(ArgumentImpl<>).MakeGenericType(type), index)!; |
||||||
|
} |
||||||
|
public IArgument<T> Argument<T>(int index) => (IArgument<T>)Argument(index); |
||||||
|
|
||||||
|
public ILocal Local(Type type, string? name = null) |
||||||
|
{ |
||||||
|
var builder = _il.DeclareLocal(type); |
||||||
|
var local = type.IsByRefLike ? new LocalImpl(builder, name) |
||||||
|
: (ILocal)Activator.CreateInstance(typeof(LocalImpl<>).MakeGenericType(type), builder, name)!; |
||||||
|
_locals.Add(local); |
||||||
|
return local; |
||||||
|
} |
||||||
|
public ILocal<T> Local<T>(string? name = null) => (ILocal<T>)Local(typeof(T), name); |
||||||
|
public ILocal<Array> LocalArray(Type type, string? name = null) => (ILocal<Array>)Local(type.MakeArrayType(), name); |
||||||
|
public ILocal<T[]> LocalArray<T>(string? name = null) => (ILocal<T[]>)Local(typeof(T).MakeArrayType(), name); |
||||||
|
|
||||||
|
public Label DefineLabel() => _il.DefineLabel(); |
||||||
|
public void MarkLabel(Label label) |
||||||
|
{ |
||||||
|
_instructions.Add((-1, _indents.Count, OpCodes.Nop, label)); |
||||||
|
_labelToOffset.Add(label, _il.ILOffset); |
||||||
|
_il.MarkLabel(label); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private void AddInstr(OpCode code, object? arg = null) => _instructions.Add((_il.ILOffset, _indents.Count, code, arg)); |
||||||
|
public void Comment(string comment) => _instructions.Add((-1, _indents.Count, OpCodes.Nop, comment)); |
||||||
|
|
||||||
|
internal void Emit(OpCode code) { AddInstr(code, null); _il.Emit(code); } |
||||||
|
internal void Emit(OpCode code, int arg) { AddInstr(code, arg); _il.Emit(code, arg); } |
||||||
|
internal void Emit(OpCode code, Type type) { AddInstr(code, type); _il.Emit(code, type); } |
||||||
|
internal void Emit(OpCode code, Label label) { AddInstr(code, label); _il.Emit(code, label); } |
||||||
|
internal void Emit(OpCode code, ILocal local) { AddInstr(code, local); _il.Emit(code, local.Builder); } |
||||||
|
internal void Emit(OpCode code, IArgument arg) { AddInstr(code, arg); _il.Emit(code, arg.Index); } |
||||||
|
internal void Emit(OpCode code, MethodInfo method) { AddInstr(code, method); _il.Emit(code, method); } |
||||||
|
internal void Emit(OpCode code, ConstructorInfo constr) { AddInstr(code, constr); _il.Emit(code, constr); } |
||||||
|
|
||||||
|
public void LoadNull() => Emit(OpCodes.Ldnull); |
||||||
|
public void LoadConst(int value) => Emit(OpCodes.Ldc_I4, value); |
||||||
|
|
||||||
|
public void Load(IArgument arg) => Emit(OpCodes.Ldarg, arg); |
||||||
|
public void LoadAddr(IArgument arg) => Emit(OpCodes.Ldarga, arg); |
||||||
|
|
||||||
|
public void Load(ILocal local) => Emit(OpCodes.Ldloc, local); |
||||||
|
public void LoadAddr(ILocal local) => Emit(OpCodes.Ldloca, local); |
||||||
|
public void Store(ILocal local) => Emit(OpCodes.Stloc, local); |
||||||
|
public void Set(ILocal<int> local, int value) { LoadConst(value); Store(local); } |
||||||
|
|
||||||
|
public void LoadLength() { Emit(OpCodes.Ldlen); Emit(OpCodes.Conv_I4); } |
||||||
|
public void LoadLength(IArgument<Array> array) { Load(array); LoadLength(); } |
||||||
|
public void LoadLength(ILocal<Array> array) { Load(array); LoadLength(); } |
||||||
|
|
||||||
|
public void LoadObj(Type type) => Emit(OpCodes.Ldobj, type); |
||||||
|
public void LoadObj(ILocal local) { LoadAddr(local); LoadObj(local.LocalType); } |
||||||
|
public void LoadObj(IArgument arg) { LoadAddr(arg); LoadObj(arg.ArgumentType); } |
||||||
|
|
||||||
|
public void LoadElem(Type type) => Emit(OpCodes.Ldelem, type); |
||||||
|
public void LoadElem(Type type, int index) { LoadConst(index); LoadElem(type); } |
||||||
|
public void LoadElem(Type type, ILocal<int> index) { Load(index); LoadElem(type); } |
||||||
|
public void LoadElem(Type type, IArgument<Array> array, int index) { Load(array); LoadElem(type, index); } |
||||||
|
public void LoadElem(Type type, IArgument<Array> array, ILocal<int> index) { Load(array); LoadElem(type, index); } |
||||||
|
public void LoadElem(Type type, ILocal<Array> array, int index) { Load(array); LoadElem(type, index); } |
||||||
|
public void LoadElem(Type type, ILocal<Array> array, ILocal<int> index) { Load(array); LoadElem(type, index); } |
||||||
|
|
||||||
|
public void LoadElemRef() => Emit(OpCodes.Ldelem_Ref); |
||||||
|
public void LoadElemRef(int index) { LoadConst(index); LoadElemRef(); } |
||||||
|
public void LoadElemRef(ILocal<int> index) { Load(index); LoadElemRef(); } |
||||||
|
public void LoadElemRef(IArgument<Array> array, int index) { Load(array); LoadElemRef(index); } |
||||||
|
public void LoadElemRef(IArgument<Array> array, ILocal<int> index) { Load(array); LoadElemRef(index); } |
||||||
|
public void LoadElemRef(ILocal<Array> array, int index) { Load(array); LoadElemRef(index); } |
||||||
|
public void LoadElemRef(ILocal<Array> array, ILocal<int> index) { Load(array); LoadElemRef(index); } |
||||||
|
|
||||||
|
public void LoadElemEither(Type type) { if (type.IsValueType) LoadElem(type); else LoadElemRef(); } |
||||||
|
public void LoadElemEither(Type type, int index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); } |
||||||
|
public void LoadElemEither(Type type, ILocal<int> index) { if (type.IsValueType) LoadElem(type, index); else LoadElemRef(index); } |
||||||
|
public void LoadElemEither(Type type, IArgument<Array> array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } |
||||||
|
public void LoadElemEither(Type type, IArgument<Array> array, ILocal<int> index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } |
||||||
|
public void LoadElemEither(Type type, ILocal<Array> array, int index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } |
||||||
|
public void LoadElemEither(Type type, ILocal<Array> array, ILocal<int> index) { if (type.IsValueType) LoadElem(type, array, index); else LoadElemRef(array, index); } |
||||||
|
|
||||||
|
public void LoadElemAddr(Type type) => Emit(OpCodes.Ldelema, type); |
||||||
|
public void LoadElemAddr(Type type, int index) { LoadConst(index); LoadElemAddr(type); } |
||||||
|
public void LoadElemAddr(Type type, ILocal<int> index) { Load(index); LoadElemAddr(type); } |
||||||
|
public void LoadElemAddr(Type type, IArgument<Array> array, int index) { Load(array); LoadElemAddr(type, index); } |
||||||
|
public void LoadElemAddr(Type type, IArgument<Array> array, ILocal<int> index) { Load(array); LoadElemAddr(type, index); } |
||||||
|
public void LoadElemAddr(Type type, ILocal<Array> array, int index) { Load(array); LoadElemAddr(type, index); } |
||||||
|
public void LoadElemAddr(Type type, ILocal<Array> array, ILocal<int> index) { Load(array); LoadElemAddr(type, index); } |
||||||
|
|
||||||
|
public void Load(PropertyInfo info) => CallVirt(info.GetMethod!); |
||||||
|
public void Load(ILocal obj, PropertyInfo info) { Load(obj); Load(info); } |
||||||
|
public void Load(IArgument obj, PropertyInfo info) { Load(obj); Load(info); } |
||||||
|
|
||||||
|
public void Add() => Emit(OpCodes.Add); |
||||||
|
public void Increment(ILocal<int> local) { Load(local); LoadConst(1); Add(); Store(local); } |
||||||
|
|
||||||
|
public void Init(Type type) => Emit(OpCodes.Initobj, type); |
||||||
|
public void Init<T>() where T : struct => Emit(OpCodes.Initobj, typeof(T)); |
||||||
|
|
||||||
|
public void New(ConstructorInfo constructor) => Emit(OpCodes.Newobj, constructor); |
||||||
|
public void New(Type type) => New(type.GetConstructors().Single()); |
||||||
|
public void New(Type type, params Type[] paramTypes) => New(type.GetConstructor(paramTypes)!); |
||||||
|
|
||||||
|
public void Cast(Type type) => Emit(OpCodes.Castclass, type); |
||||||
|
public void Cast<T>() => Cast(typeof(T)); |
||||||
|
|
||||||
|
public void Goto(Label label) => Emit(OpCodes.Br, label); |
||||||
|
public void GotoIfTrue(Label label) => Emit(OpCodes.Brtrue, label); |
||||||
|
public void GotoIfFalse(Label label) => Emit(OpCodes.Brfalse, label); |
||||||
|
|
||||||
|
public void GotoIf(Label label, ILocal<int> a, Comparison op, ILocal<int> b) |
||||||
|
{ Load(a); Load(b); Emit(op.Code, label); } |
||||||
|
public void GotoIfNull(Label label, ILocal<object> local) |
||||||
|
{ Load(local); Emit(OpCodes.Brfalse, label); } |
||||||
|
public void GotoIfNotNull(Label label, ILocal<object> local) |
||||||
|
{ Load(local); Emit(OpCodes.Brtrue, label); } |
||||||
|
|
||||||
|
public void Call(MethodInfo method) => Emit(OpCodes.Call, method); |
||||||
|
public void CallVirt(MethodInfo method) => Emit(OpCodes.Callvirt, method); |
||||||
|
|
||||||
|
public void Return() => Emit(OpCodes.Ret); |
||||||
|
|
||||||
|
|
||||||
|
public IDisposable For(Action loadMax, out ILocal<int> current) |
||||||
|
{ |
||||||
|
var r = Random.Shared.Next(10000, 100000); |
||||||
|
Comment($"INIT for loop {r}"); |
||||||
|
|
||||||
|
var curLocal = current = Local<int>($"index_{r}"); |
||||||
|
var maxLocal = Local<int>($"length_{r}"); |
||||||
|
|
||||||
|
var bodyLabel = DefineLabel(); |
||||||
|
var testLabel = DefineLabel(); |
||||||
|
|
||||||
|
Set(curLocal, 0); |
||||||
|
loadMax(); Store(maxLocal); |
||||||
|
|
||||||
|
Comment($"BEGIN for loop {r}"); |
||||||
|
Goto(testLabel); |
||||||
|
MarkLabel(bodyLabel); |
||||||
|
var indent = Indent(); |
||||||
|
|
||||||
|
return Block(() => { |
||||||
|
Increment(curLocal); |
||||||
|
MarkLabel(testLabel); |
||||||
|
GotoIf(bodyLabel, curLocal, Comparison.LessThan, maxLocal); |
||||||
|
indent.Dispose(); |
||||||
|
Comment($"END for loop {r}"); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public IDisposable Block(Action onClose) |
||||||
|
=> new BlockImpl(onClose); |
||||||
|
public IDisposable Indent() |
||||||
|
{ |
||||||
|
BlockImpl indent = null!; |
||||||
|
indent = new(() => { if (_indents.Pop() != indent) throw new InvalidOperationException(); }); |
||||||
|
_indents.Push(indent); |
||||||
|
return indent; |
||||||
|
} |
||||||
|
|
||||||
|
internal class BlockImpl : IDisposable |
||||||
|
{ |
||||||
|
public Action OnClose { get; } |
||||||
|
public BlockImpl(Action onClose) => OnClose = onClose; |
||||||
|
public void Dispose() => OnClose(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
internal class ArgumentImpl : IArgument |
||||||
|
{ |
||||||
|
public int Index { get; } |
||||||
|
public Type ArgumentType { get; } |
||||||
|
public ArgumentImpl(int index, Type type) { Index = index; ArgumentType = type; } |
||||||
|
public override string ToString() => $"Argument({Index}, {ArgumentType.Name})"; |
||||||
|
} |
||||||
|
internal class ArgumentImpl<T> : ArgumentImpl, IArgument<T> |
||||||
|
{ public ArgumentImpl(int index) : base(index, typeof(T)) { } } |
||||||
|
|
||||||
|
internal class LocalImpl : ILocal |
||||||
|
{ |
||||||
|
public LocalBuilder Builder { get; } |
||||||
|
public string? Name { get; } |
||||||
|
public Type LocalType => Builder.LocalType; |
||||||
|
public LocalImpl(LocalBuilder builder, string? name) { Builder = builder; Name = name; } |
||||||
|
public override string ToString() => $"Local({Builder.LocalIndex}, {LocalType.Name}){(Name != null ? $" // {Name}" : "")}"; |
||||||
|
} |
||||||
|
internal class LocalImpl<T> : LocalImpl, ILocal<T> |
||||||
|
{ public LocalImpl(LocalBuilder builder, string? name) : base(builder, name) { } } |
||||||
|
} |
||||||
|
|
||||||
|
public class Comparison |
||||||
|
{ |
||||||
|
public static Comparison NotEqual { get; } = new(OpCodes.Bne_Un); |
||||||
|
public static Comparison LessThan { get; } = new(OpCodes.Blt); |
||||||
|
public static Comparison LessOrEq { get; } = new(OpCodes.Ble); |
||||||
|
public static Comparison Equal { get; } = new(OpCodes.Beq); |
||||||
|
public static Comparison GreaterOrEq { get; } = new(OpCodes.Bge); |
||||||
|
public static Comparison GreaterThan { get; } = new(OpCodes.Bgt); |
||||||
|
|
||||||
|
public OpCode Code { get; } |
||||||
|
private Comparison(OpCode code) => Code = code; |
||||||
|
} |
||||||
|
|
||||||
|
public interface IArgument |
||||||
|
{ |
||||||
|
int Index { get; } |
||||||
|
Type ArgumentType { get; } |
||||||
|
} |
||||||
|
public interface IArgument<out T> |
||||||
|
: IArgument { } |
||||||
|
|
||||||
|
public interface ILocal |
||||||
|
{ |
||||||
|
LocalBuilder Builder { get; } |
||||||
|
Type LocalType { get; } |
||||||
|
} |
||||||
|
public interface ILocal<out T> |
||||||
|
: ILocal { } |
@ -0,0 +1,327 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Reflection; |
||||||
|
using System.Reflection.Emit; |
||||||
|
using System.Runtime.CompilerServices; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using gaemstone.ECS; |
||||||
|
using static flecs_hub.flecs; |
||||||
|
|
||||||
|
namespace gaemstone.Utility.IL; |
||||||
|
|
||||||
|
public unsafe class QueryActionGenerator |
||||||
|
{ |
||||||
|
private static readonly PropertyInfo _iteratorUniverseProp = typeof(Iterator).GetProperty(nameof(Iterator.Universe))!; |
||||||
|
private static readonly PropertyInfo _iteratorDeltaTimeProp = typeof(Iterator).GetProperty(nameof(Iterator.DeltaTime))!; |
||||||
|
private static readonly PropertyInfo _iteratorCountProp = typeof(Iterator).GetProperty(nameof(Iterator.Count))!; |
||||||
|
private static readonly MethodInfo _iteratorFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field))!; |
||||||
|
private static readonly MethodInfo _iteratorFieldIsSetMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldIsSet))!; |
||||||
|
private static readonly MethodInfo _iteratorEntityMethod = typeof(Iterator).GetMethod(nameof(Iterator.Entity))!; |
||||||
|
|
||||||
|
private static readonly MethodInfo _handleFromIntPtrMethod = typeof(GCHandle).GetMethod(nameof(GCHandle.FromIntPtr))!; |
||||||
|
private static readonly PropertyInfo _handleTargetProp = typeof(GCHandle).GetProperty(nameof(GCHandle.Target))!; |
||||||
|
|
||||||
|
private static readonly ConditionalWeakTable<MethodInfo, QueryActionGenerator> _cache = new(); |
||||||
|
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>, ILocal<int>>> _uniqueParameters = new() { |
||||||
|
[typeof(Iterator)] = (IL, iter, i) => { IL.Load(iter); }, |
||||||
|
[typeof(Universe)] = (IL, iter, i) => { IL.Load(iter, _iteratorUniverseProp); }, |
||||||
|
[typeof(TimeSpan)] = (IL, iter, i) => { IL.Load(iter, _iteratorDeltaTimeProp); }, |
||||||
|
[typeof(Entity)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iteratorEntityMethod); }, |
||||||
|
}; |
||||||
|
|
||||||
|
public Universe Universe { get; } |
||||||
|
public MethodInfo Method { get; } |
||||||
|
public ParamInfo[] Parameters { get; } |
||||||
|
|
||||||
|
public ecs_filter_desc_t Filter { get; } |
||||||
|
public Action<object?, Iterator> GeneratedAction { get; } |
||||||
|
public string ReadableString { get; } |
||||||
|
|
||||||
|
public void RunWithTryCatch(object? instance, Iterator iter) |
||||||
|
{ |
||||||
|
try { GeneratedAction(instance, iter); } catch { |
||||||
|
Console.WriteLine("Exception occured while running:"); |
||||||
|
Console.WriteLine(" " + Method); |
||||||
|
Console.WriteLine(); |
||||||
|
Console.WriteLine("Method's IL code:"); |
||||||
|
Console.WriteLine(ReadableString); |
||||||
|
Console.WriteLine(); |
||||||
|
throw; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public QueryActionGenerator(Universe universe, MethodInfo method) |
||||||
|
{ |
||||||
|
Universe = universe; |
||||||
|
Method = method; |
||||||
|
|
||||||
|
Parameters = method.GetParameters().Select(ParamInfo.Build).ToArray(); |
||||||
|
if (!Parameters.Any(c => c.IsRequired && (c.Kind != ParamKind.Unique))) |
||||||
|
throw new ArgumentException($"At least one parameter in {method} is required"); |
||||||
|
|
||||||
|
var filter = default(ecs_filter_desc_t); |
||||||
|
var name = "<>Query_" + string.Join("_", Parameters.Select(p => p.UnderlyingType.Name)); |
||||||
|
var genMethod = new DynamicMethod(name, null, new[] { typeof(object), typeof(Iterator) }); |
||||||
|
var IL = new ILGeneratorWrapper(genMethod); |
||||||
|
|
||||||
|
var instanceArg = IL.Argument<object?>(0); |
||||||
|
var iteratorArg = IL.Argument<Iterator>(1); |
||||||
|
|
||||||
|
var counter = 0; // Counter for fields actually part of the filter terms. |
||||||
|
var fieldLocals = new ILocal[Parameters.Length]; |
||||||
|
var tempLocals = new ILocal[Parameters.Length]; |
||||||
|
|
||||||
|
for (var i = 0; i < Parameters.Length; i++) { |
||||||
|
var p = Parameters[i]; |
||||||
|
if (p.Kind == ParamKind.Unique) continue; |
||||||
|
|
||||||
|
// Update the flecs filter to look for this type. |
||||||
|
// Term index is 0-based and field index (used below) is 1-based, so increasing counter here works out. |
||||||
|
ref var term = ref filter.terms[counter++]; |
||||||
|
term.id = Universe.Lookup(p.UnderlyingType); |
||||||
|
term.inout = p.Kind switch { |
||||||
|
ParamKind.In => ecs_inout_kind_t.EcsIn, |
||||||
|
ParamKind.Out => ecs_inout_kind_t.EcsOut, |
||||||
|
ParamKind.Has or ParamKind.Not => ecs_inout_kind_t.EcsInOutNone, |
||||||
|
_ => ecs_inout_kind_t.EcsInOut, |
||||||
|
}; |
||||||
|
term.oper = p.Kind switch { |
||||||
|
ParamKind.Not => ecs_oper_kind_t.EcsNot, |
||||||
|
_ when !p.IsRequired => ecs_oper_kind_t.EcsOptional, |
||||||
|
_ => ecs_oper_kind_t.EcsAnd, |
||||||
|
}; |
||||||
|
if (p.Source != null) term.src = new() { id = Universe.Lookup(p.Source) }; |
||||||
|
|
||||||
|
// Create a Span<T> local and initialize it to iterator.Field<T>(i). |
||||||
|
var spanType = typeof(Span<>).MakeGenericType(p.FieldType); |
||||||
|
fieldLocals[i] = IL.Local(spanType, $"field_{counter}"); |
||||||
|
if (p.Kind is ParamKind.Has or ParamKind.Not) { |
||||||
|
// If a "has" or "not" parameter is a struct, we require a temporary local that |
||||||
|
// we can later load onto the stack when loading the arguments for the action. |
||||||
|
if (p.ParameterType.IsValueType) { |
||||||
|
IL.Comment($"temp_{counter} = default({p.ParameterType});"); |
||||||
|
tempLocals[i] = IL.Local(p.ParameterType); |
||||||
|
IL.LoadAddr(tempLocals[i]); |
||||||
|
IL.Init(tempLocals[i].LocalType); |
||||||
|
} |
||||||
|
} else if (p.IsRequired) { |
||||||
|
IL.Comment($"field_{counter} = iterator.Field<{p.FieldType.Name}>({counter})"); |
||||||
|
IL.Load(iteratorArg); |
||||||
|
IL.LoadConst(counter); |
||||||
|
IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType)); |
||||||
|
IL.Store(fieldLocals[i]); |
||||||
|
} else { |
||||||
|
IL.Comment($"field_{counter} = iterator.FieldIsSet({counter}) ? iterator.Field<{p.FieldType.Name}>({counter}) : default"); |
||||||
|
var elseLabel = IL.DefineLabel(); |
||||||
|
var doneLabel = IL.DefineLabel(); |
||||||
|
IL.Load(iteratorArg); |
||||||
|
IL.LoadConst(counter); |
||||||
|
IL.Call(_iteratorFieldIsSetMethod); |
||||||
|
IL.GotoIfFalse(elseLabel); |
||||||
|
IL.Load(iteratorArg); |
||||||
|
IL.LoadConst(counter); |
||||||
|
IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType)); |
||||||
|
IL.Store(fieldLocals[i]); |
||||||
|
IL.Goto(doneLabel); |
||||||
|
IL.MarkLabel(elseLabel); |
||||||
|
IL.LoadAddr(fieldLocals[i]); |
||||||
|
IL.Init(spanType); |
||||||
|
IL.MarkLabel(doneLabel); |
||||||
|
} |
||||||
|
|
||||||
|
if (p.Kind == ParamKind.Nullable) { |
||||||
|
IL.Comment($"temp_{counter} = default({p.ParameterType});"); |
||||||
|
tempLocals[i] = IL.Local(p.ParameterType); |
||||||
|
IL.LoadAddr(tempLocals[i]); |
||||||
|
IL.Init(tempLocals[i].LocalType); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If there's any reference type parameters, we need to define a GCHandle local. |
||||||
|
var hasReferenceType = Parameters |
||||||
|
.Where(p => p.Kind != ParamKind.Unique) |
||||||
|
.Any(p => !p.UnderlyingType.IsValueType); |
||||||
|
var handleLocal = hasReferenceType ? IL.Local<GCHandle>() : null; |
||||||
|
|
||||||
|
using (IL.For(() => IL.Load(iteratorArg, _iteratorCountProp), out var currentLocal)) { |
||||||
|
if (!Method.IsStatic) |
||||||
|
IL.Load(instanceArg); |
||||||
|
for (var i = 0; i < Parameters.Length; i++) { |
||||||
|
var p = Parameters[i]; |
||||||
|
if (p.Kind == ParamKind.Unique) { |
||||||
|
IL.Comment($"Unique parameter {p.ParameterType}"); |
||||||
|
_uniqueParameters[p.ParameterType](IL, iteratorArg, currentLocal); |
||||||
|
} else if (p.Kind is ParamKind.Has or ParamKind.Not) { |
||||||
|
if (p.ParameterType.IsValueType) |
||||||
|
IL.LoadObj(tempLocals[i]!); |
||||||
|
else IL.LoadNull(); |
||||||
|
} else { |
||||||
|
var spanType = typeof(Span<>).MakeGenericType(p.FieldType); |
||||||
|
var spanItemMethod = spanType.GetProperty("Item")!.GetMethod!; |
||||||
|
var spanLengthMethod = spanType.GetProperty("Length")!.GetMethod!; |
||||||
|
|
||||||
|
IL.Comment($"Parameter {p.ParameterType}"); |
||||||
|
if (p.IsByRef) { |
||||||
|
IL.LoadAddr(fieldLocals[i]!); |
||||||
|
IL.Load(currentLocal); |
||||||
|
IL.Call(spanItemMethod); |
||||||
|
} else if (p.IsRequired) { |
||||||
|
IL.LoadAddr(fieldLocals[i]!); |
||||||
|
IL.Load(currentLocal); |
||||||
|
IL.Call(spanItemMethod); |
||||||
|
IL.LoadObj(p.FieldType); |
||||||
|
} else { |
||||||
|
var elseLabel = IL.DefineLabel(); |
||||||
|
var doneLabel = IL.DefineLabel(); |
||||||
|
IL.LoadAddr(fieldLocals[i]!); |
||||||
|
IL.Call(spanLengthMethod); |
||||||
|
IL.GotoIfFalse(elseLabel); |
||||||
|
IL.LoadAddr(fieldLocals[i]!); |
||||||
|
IL.Load(currentLocal); |
||||||
|
IL.Call(spanItemMethod); |
||||||
|
IL.LoadObj(p.FieldType); |
||||||
|
if (p.Kind == ParamKind.Nullable) |
||||||
|
IL.New(p.ParameterType); |
||||||
|
IL.Goto(doneLabel); |
||||||
|
IL.MarkLabel(elseLabel); |
||||||
|
if (p.Kind == ParamKind.Nullable) |
||||||
|
IL.LoadObj(tempLocals[i]!); |
||||||
|
else IL.LoadNull(); |
||||||
|
IL.MarkLabel(doneLabel); |
||||||
|
} |
||||||
|
|
||||||
|
if (!p.UnderlyingType.IsValueType) { |
||||||
|
IL.Comment($"Convert nint to {p.UnderlyingType}"); |
||||||
|
IL.Call(_handleFromIntPtrMethod); |
||||||
|
IL.Store(handleLocal!); |
||||||
|
IL.LoadAddr(handleLocal!); |
||||||
|
IL.Call(_handleTargetProp.GetMethod!); |
||||||
|
IL.Cast(p.UnderlyingType); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
IL.Call(Method); |
||||||
|
} |
||||||
|
|
||||||
|
IL.Return(); |
||||||
|
|
||||||
|
Filter = filter; |
||||||
|
GeneratedAction = genMethod.CreateDelegate<Action<object?, Iterator>>(); |
||||||
|
ReadableString = IL.ToReadableString(); |
||||||
|
} |
||||||
|
|
||||||
|
public static QueryActionGenerator GetOrBuild(Universe universe, MethodInfo method) |
||||||
|
=>_cache.GetValue(method, m => new QueryActionGenerator(universe, m)); |
||||||
|
|
||||||
|
public class ParamInfo |
||||||
|
{ |
||||||
|
public ParameterInfo Info { get; } |
||||||
|
public int Index { get; } |
||||||
|
|
||||||
|
public ParamKind Kind { get; } |
||||||
|
public Type ParameterType { get; } |
||||||
|
public Type UnderlyingType { get; } |
||||||
|
public Type FieldType { get; } |
||||||
|
|
||||||
|
public Type? Source { get; } |
||||||
|
|
||||||
|
public bool IsRequired => (Kind < ParamKind.Nullable); |
||||||
|
public bool IsByRef => (Kind >= ParamKind.In) && (Kind <= ParamKind.Ref); |
||||||
|
|
||||||
|
private ParamInfo( |
||||||
|
ParameterInfo info, int index, ParamKind kind, |
||||||
|
Type paramType, Type underlyingType) |
||||||
|
{ |
||||||
|
Info = info; |
||||||
|
Index = index; |
||||||
|
|
||||||
|
Kind = kind; |
||||||
|
ParameterType = paramType; |
||||||
|
UnderlyingType = underlyingType; |
||||||
|
// Reference types have a backing type of nint - they're pointers. |
||||||
|
FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint); |
||||||
|
|
||||||
|
// If the underlying type has EntityAttribute, it's a singleton. |
||||||
|
if (UnderlyingType.Has<EntityAttribute>()) Source = underlyingType; |
||||||
|
if (Info.Get<SourceAttribute>() is SourceAttribute attr) Source = attr.Type; |
||||||
|
} |
||||||
|
|
||||||
|
public static ParamInfo Build(ParameterInfo info, int index) |
||||||
|
{ |
||||||
|
if (info.IsOptional) throw new ArgumentException("Optional parameters are not supported\nParameter: " + info); |
||||||
|
if (info.ParameterType.IsArray) throw new ArgumentException("Arrays are not supported\nParameter: " + info); |
||||||
|
if (info.ParameterType.IsPointer) throw new ArgumentException("Pointers are not supported\nParameter: " + info); |
||||||
|
|
||||||
|
if (_uniqueParameters.ContainsKey(info.ParameterType)) |
||||||
|
return new(info, index, ParamKind.Unique, info.ParameterType, info.ParameterType); |
||||||
|
|
||||||
|
var isByRef = info.ParameterType.IsByRef; |
||||||
|
var isNullable = info.IsNullable(); |
||||||
|
|
||||||
|
if (info.Has<NotAttribute>()) { |
||||||
|
if (isByRef || isNullable) throw new ArgumentException( |
||||||
|
"Parameter with NotAttribute must not be ByRef or nullable\nParameter: " + info); |
||||||
|
return new(info, index, ParamKind.Not, info.ParameterType, info.ParameterType); |
||||||
|
} |
||||||
|
|
||||||
|
if (info.Has<HasAttribute>() || info.ParameterType.Has<TagAttribute>()) { |
||||||
|
if (isByRef || isNullable) throw new ArgumentException( |
||||||
|
"Parameter with HasAttribute / TagAttribute must not be ByRef or nullable\nParameter: " + info); |
||||||
|
return new(info, index, ParamKind.Has, info.ParameterType, info.ParameterType); |
||||||
|
} |
||||||
|
|
||||||
|
var kind = ParamKind.Normal; |
||||||
|
var underlyingType = info.ParameterType; |
||||||
|
|
||||||
|
if (info.IsNullable()) { |
||||||
|
if (info.ParameterType.IsValueType) |
||||||
|
underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!; |
||||||
|
kind = ParamKind.Nullable; |
||||||
|
} |
||||||
|
|
||||||
|
if (info.ParameterType.IsByRef) { |
||||||
|
if (kind == ParamKind.Nullable) throw new ArgumentException( |
||||||
|
"ByRef and Nullable are not supported together\nParameter: " + info); |
||||||
|
underlyingType = info.ParameterType.GetElementType()!; |
||||||
|
if (!underlyingType.IsValueType) throw new ArgumentException( |
||||||
|
"Reference types can't also be ByRef\nParameter: " + info); |
||||||
|
kind = info.IsIn ? ParamKind.In |
||||||
|
: info.IsOut ? ParamKind.Out |
||||||
|
: ParamKind.Ref; |
||||||
|
} |
||||||
|
|
||||||
|
if (underlyingType.IsPrimitive) throw new ArgumentException( |
||||||
|
"Primitives are not supported\nParameter: " + info); |
||||||
|
|
||||||
|
return new(info, index, kind, info.ParameterType, underlyingType); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public enum ParamKind |
||||||
|
{ |
||||||
|
/// <summary> Parameter is not part of terms, handled uniquely, such as Universe and Entity. </summary> |
||||||
|
Unique, |
||||||
|
/// <summary> Passed by value. </summary> |
||||||
|
Normal, |
||||||
|
/// <summary> Struct passed with the "in" modifier. </summary> |
||||||
|
In, |
||||||
|
/// <summary> Struct passed with the "out" modifier. </summary> |
||||||
|
Out, |
||||||
|
/// <summary> Struct passed with the "ref" modifier. </summary> |
||||||
|
Ref, |
||||||
|
/// <summary> |
||||||
|
/// Only checks for presence. |
||||||
|
/// Manually applied with <see cref="HasAttribute"/>. |
||||||
|
/// Automatically applied for types with <see cref="TagAttribute"/>. |
||||||
|
/// </summary> |
||||||
|
Has, |
||||||
|
/// <summary> Struct passed as <c>Nullable<T></c>. </summary> |
||||||
|
Nullable, |
||||||
|
/// <summary> |
||||||
|
/// Only checks for absence. |
||||||
|
/// Applied with <see cref="NotAttribute"/>. |
||||||
|
/// </summary> |
||||||
|
Not, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
|
||||||
|
namespace gaemstone.Utility; |
||||||
|
|
||||||
|
public static class RandomExtensions |
||||||
|
{ |
||||||
|
public static bool NextBool(this Random rnd, double chance) |
||||||
|
=> rnd.NextDouble() < chance; |
||||||
|
|
||||||
|
public static double NextDouble(this Random rnd, double max) |
||||||
|
=> rnd.NextDouble() * max; |
||||||
|
public static double NextDouble(this Random rnd, double min, double max) |
||||||
|
=> min + rnd.NextDouble() * (max - min); |
||||||
|
|
||||||
|
public static float NextFloat(this Random rnd) |
||||||
|
=> (float)rnd.NextDouble(); |
||||||
|
public static float NextFloat(this Random rnd, float max) |
||||||
|
=> (float)rnd.NextDouble() * max; |
||||||
|
public static float NextFloat(this Random rnd, float min, float max) |
||||||
|
=> min + (float)rnd.NextDouble() * (max - min); |
||||||
|
|
||||||
|
public static T Pick<T>(this Random rnd, params T[] elements) |
||||||
|
=> elements[rnd.Next(elements.Length)]; |
||||||
|
public static T Pick<T>(this Random rnd, IReadOnlyList<T> elements) |
||||||
|
=> elements[rnd.Next(elements.Count)]; |
||||||
|
public static T Pick<T>(this Random rnd, Span<T> elements) |
||||||
|
=> elements[rnd.Next(elements.Length)]; |
||||||
|
|
||||||
|
#pragma warning disable CS8509 // Switch expression is not exhaustive. |
||||||
|
public static T Pick<T>(this Random rnd, T elem1, T elem2) |
||||||
|
=> rnd.Next(2) switch { 0 => elem1, 1 => elem2 }; |
||||||
|
public static T Pick<T>(this Random rnd, T elem1, T elem2, T elem3) |
||||||
|
=> rnd.Next(3) switch { 0 => elem1, 1 => elem2, 2 => elem3 }; |
||||||
|
public static T Pick<T>(this Random rnd, T elem1, T elem2, T elem3, T elem4) |
||||||
|
=> rnd.Next(4) switch { 0 => elem1, 1 => elem2, 2 => elem3, 3 => elem4 }; |
||||||
|
#pragma warning restore CS8509 |
||||||
|
} |
@ -0,0 +1,73 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Collections.ObjectModel; |
||||||
|
using System.Linq; |
||||||
|
using System.Reflection; |
||||||
|
using System.Text; |
||||||
|
|
||||||
|
namespace gaemstone.Utility; |
||||||
|
|
||||||
|
public static class ReflectionExtensions |
||||||
|
{ |
||||||
|
public static string GetFriendlyName(this Type type) |
||||||
|
{ |
||||||
|
if (!type.IsGenericType) return type.Name; |
||||||
|
var name = type.Name; |
||||||
|
var sb = new StringBuilder(name[..name.IndexOf('`')]); |
||||||
|
sb.Append('<'); |
||||||
|
sb.AppendJoin(",", type.GenericTypeArguments.Select(GetFriendlyName)); |
||||||
|
sb.Append('>'); |
||||||
|
return sb.ToString(); |
||||||
|
} |
||||||
|
|
||||||
|
public static T? Get<T>(this MemberInfo member) |
||||||
|
where T : Attribute => member.GetCustomAttribute<T>(); |
||||||
|
public static IEnumerable<T> GetMultiple<T>(this MemberInfo member) |
||||||
|
where T : Attribute => member.GetCustomAttributes<T>(); |
||||||
|
public static bool Has<T>(this MemberInfo member) |
||||||
|
where T : Attribute => member.GetCustomAttribute<T>() != null; |
||||||
|
|
||||||
|
public static T? Get<T>(this ParameterInfo member) |
||||||
|
where T : Attribute => member.GetCustomAttribute<T>(); |
||||||
|
public static IEnumerable<T> GetMultiple<T>(this ParameterInfo member) |
||||||
|
where T : Attribute => member.GetCustomAttributes<T>(); |
||||||
|
public static bool Has<T>(this ParameterInfo member) |
||||||
|
where T : Attribute => member.GetCustomAttribute<T>() != null; |
||||||
|
|
||||||
|
|
||||||
|
public static bool IsNullable(this PropertyInfo property) => |
||||||
|
IsNullable(property.PropertyType, property.DeclaringType, property.CustomAttributes); |
||||||
|
public static bool IsNullable(this FieldInfo field) => |
||||||
|
IsNullable(field.FieldType, field.DeclaringType, field.CustomAttributes); |
||||||
|
public static bool IsNullable(this ParameterInfo parameter) => |
||||||
|
IsNullable(parameter.ParameterType, parameter.Member, parameter.CustomAttributes); |
||||||
|
|
||||||
|
// https://stackoverflow.com/a/58454489 |
||||||
|
static bool IsNullable(Type memberType, MemberInfo? declaringType, IEnumerable<CustomAttributeData> customAttributes) |
||||||
|
{ |
||||||
|
if (memberType.IsValueType) return (Nullable.GetUnderlyingType(memberType) != null); |
||||||
|
|
||||||
|
var nullable = customAttributes.FirstOrDefault( |
||||||
|
x => (x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute")); |
||||||
|
if ((nullable != null) && (nullable.ConstructorArguments.Count == 1)) { |
||||||
|
var attributeArgument = nullable.ConstructorArguments[0]; |
||||||
|
if (attributeArgument.ArgumentType == typeof(byte[])) { |
||||||
|
var args = (ReadOnlyCollection<CustomAttributeTypedArgument>)attributeArgument.Value!; |
||||||
|
if ((args.Count > 0) && (args[0].ArgumentType == typeof(byte))) |
||||||
|
return (byte)args[0].Value! == 2; |
||||||
|
} else if (attributeArgument.ArgumentType == typeof(byte)) |
||||||
|
return (byte)attributeArgument.Value! == 2; |
||||||
|
} |
||||||
|
|
||||||
|
for (var type = declaringType; type != null; type = type.DeclaringType) { |
||||||
|
var context = type.CustomAttributes.FirstOrDefault( |
||||||
|
x => (x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute")); |
||||||
|
if ((context != null) && (context.ConstructorArguments.Count == 1) && |
||||||
|
(context.ConstructorArguments[0].ArgumentType == typeof(byte))) |
||||||
|
return (byte)context.ConstructorArguments[0].Value! == 2; |
||||||
|
} |
||||||
|
|
||||||
|
// Couldn't find a suitable attribute. |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,229 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Reflection; |
||||||
|
using System.Reflection.Emit; |
||||||
|
using System.Runtime.CompilerServices; |
||||||
|
|
||||||
|
namespace gaemstone.Utility; |
||||||
|
|
||||||
|
public interface ITypeWrapper |
||||||
|
{ |
||||||
|
Type Type { get; } |
||||||
|
|
||||||
|
int Size { get; } |
||||||
|
bool IsUnmanaged { get; } |
||||||
|
|
||||||
|
IFieldWrapper GetFieldForAutoProperty(string propertyName); |
||||||
|
IFieldWrapper GetFieldForAutoProperty(PropertyInfo property); |
||||||
|
IFieldWrapper GetField(string fieldName); |
||||||
|
IFieldWrapper GetField(FieldInfo field); |
||||||
|
} |
||||||
|
|
||||||
|
public interface IFieldWrapper |
||||||
|
{ |
||||||
|
ITypeWrapper DeclaringType { get; } |
||||||
|
FieldInfo FieldInfo { get; } |
||||||
|
PropertyInfo? PropertyInfo { get; } |
||||||
|
|
||||||
|
Func<object, object?> ClassGetter { get; } |
||||||
|
Action<object, object?> ClassSetter { get; } |
||||||
|
} |
||||||
|
|
||||||
|
public static class TypeWrapper |
||||||
|
{ |
||||||
|
static readonly Dictionary<Type, ITypeWrapper> _typeCache = new(); |
||||||
|
|
||||||
|
public static TypeWrapper<T> For<T>() |
||||||
|
=> TypeWrapper<T>.Instance; |
||||||
|
public static ITypeWrapper For(Type type) |
||||||
|
{ |
||||||
|
if (!_typeCache.TryGetValue(type, out var wrapper)) |
||||||
|
_typeCache.Add(type, wrapper = (ITypeWrapper)typeof(TypeWrapper<>) |
||||||
|
.MakeGenericType(type).GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic)! |
||||||
|
.GetMethod!.Invoke(null, Array.Empty<object>())!); |
||||||
|
return wrapper; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public class TypeWrapper<TType> : ITypeWrapper |
||||||
|
{ |
||||||
|
internal static TypeWrapper<TType> Instance { get; } = new(); |
||||||
|
|
||||||
|
readonly Dictionary<FieldInfo, IFieldWrapperForType> _fieldCache = new(); |
||||||
|
|
||||||
|
public Type Type => typeof(TType); |
||||||
|
|
||||||
|
public int Size { get; } = Unsafe.SizeOf<TType>(); |
||||||
|
public bool IsUnmanaged { get; } = !RuntimeHelpers.IsReferenceOrContainsReferences<TType>(); |
||||||
|
|
||||||
|
TypeWrapper() { } |
||||||
|
|
||||||
|
IFieldWrapper ITypeWrapper.GetFieldForAutoProperty(string propertyName) => GetFieldForAutoProperty(propertyName); |
||||||
|
IFieldWrapper ITypeWrapper.GetFieldForAutoProperty(PropertyInfo property) => GetFieldForAutoProperty(property); |
||||||
|
IFieldWrapper ITypeWrapper.GetField(string fieldName) => GetField(fieldName); |
||||||
|
IFieldWrapper ITypeWrapper.GetField(FieldInfo field) => GetField(field); |
||||||
|
|
||||||
|
public IFieldWrapperForType GetFieldForAutoProperty(string propertyName) |
||||||
|
{ |
||||||
|
var property = Type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); |
||||||
|
if (property == null) throw new MissingMemberException(Type.FullName, propertyName); |
||||||
|
var field = Type.GetField($"<{property.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); |
||||||
|
if (field == null) throw new ArgumentException($"Could not find backing field for property {property}"); |
||||||
|
return GetField(field, property); |
||||||
|
} |
||||||
|
public IFieldWrapperForType GetFieldForAutoProperty(PropertyInfo property) |
||||||
|
{ |
||||||
|
if (property.DeclaringType != Type) throw new ArgumentException( |
||||||
|
$"Specified PropertyInfo {property} needs to be a member of type {Type}", nameof(property)); |
||||||
|
var field = Type.GetField($"<{property.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); |
||||||
|
if (field == null) throw new ArgumentException($"Could not find backing field for property {property}"); |
||||||
|
return GetField(field, property); |
||||||
|
} |
||||||
|
|
||||||
|
public IFieldWrapperForType GetField(string fieldName) |
||||||
|
{ |
||||||
|
var field = Type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); |
||||||
|
if (field == null) throw new MissingFieldException(Type.FullName, fieldName); |
||||||
|
return GetField(field, null); |
||||||
|
} |
||||||
|
public IFieldWrapperForType GetField(FieldInfo field) |
||||||
|
{ |
||||||
|
if (field.DeclaringType != Type) throw new ArgumentException( |
||||||
|
$"Specified FieldInfo {field} needs to be a member of type {Type}", nameof(field)); |
||||||
|
return GetField(field, null); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public FieldWrapper<TField> GetFieldForAutoProperty<TField>(string propertyName) |
||||||
|
{ |
||||||
|
var property = Type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); |
||||||
|
if (property == null) throw new MissingMemberException(Type.FullName, propertyName); |
||||||
|
var field = Type.GetField($"<{property.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); |
||||||
|
if (field == null) throw new ArgumentException($"Could not find backing field for property {property}"); |
||||||
|
return GetField<TField>(field, property); |
||||||
|
} |
||||||
|
public FieldWrapper<TField> GetFieldForAutoProperty<TField>(PropertyInfo property) |
||||||
|
{ |
||||||
|
if (property.DeclaringType != Type) throw new ArgumentException( |
||||||
|
$"Specified PropertyInfo {property} needs to be a member of type {Type}", nameof(property)); |
||||||
|
var field = Type.GetField($"<{property.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); |
||||||
|
if (field == null) throw new ArgumentException($"Could not find backing field for property {property}"); |
||||||
|
return GetField<TField>(field, property); |
||||||
|
} |
||||||
|
|
||||||
|
public FieldWrapper<TField> GetField<TField>(string fieldName) |
||||||
|
{ |
||||||
|
var field = Type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); |
||||||
|
if (field == null) throw new MissingFieldException(Type.FullName, fieldName); |
||||||
|
return GetField<TField>(field, null); |
||||||
|
} |
||||||
|
public FieldWrapper<TField> GetField<TField>(FieldInfo field) |
||||||
|
{ |
||||||
|
if (field.DeclaringType != Type) throw new ArgumentException( |
||||||
|
$"Specified FieldInfo {field} needs to be a member of type {Type}", nameof(field)); |
||||||
|
return GetField<TField>(field, null); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
IFieldWrapperForType GetField(FieldInfo field, PropertyInfo? property) |
||||||
|
{ |
||||||
|
if (_fieldCache.TryGetValue(field, out var cached)) return cached; |
||||||
|
var type = typeof(FieldWrapper<>).MakeGenericType(typeof(TType), field.FieldType); |
||||||
|
var wrapper = (IFieldWrapperForType)Activator.CreateInstance( |
||||||
|
type, BindingFlags.Instance | BindingFlags.NonPublic, |
||||||
|
null, new object?[] { this, field, property }, null)!; |
||||||
|
_fieldCache.Add(field, wrapper); |
||||||
|
return wrapper; |
||||||
|
} |
||||||
|
|
||||||
|
FieldWrapper<TField> GetField<TField>(FieldInfo field, PropertyInfo? property) |
||||||
|
{ |
||||||
|
if (_fieldCache.TryGetValue(field, out var cached)) return (FieldWrapper<TField>)cached; |
||||||
|
if (field.FieldType != typeof(TField)) throw new ArgumentException( |
||||||
|
$"FieldType ({field.FieldType}) does not match TField ({typeof(TField)})", nameof(TField)); |
||||||
|
var wrapper = new FieldWrapper<TField>(this, field, property); |
||||||
|
_fieldCache.Add(field, wrapper); |
||||||
|
return wrapper; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public interface IFieldWrapperForType : IFieldWrapper |
||||||
|
{ |
||||||
|
delegate object? ValueGetterAction(in TType obj); |
||||||
|
delegate void ValueSetterAction(ref TType obj, object? value); |
||||||
|
|
||||||
|
Func<object, object?> IFieldWrapper.ClassGetter => (obj) => ClassGetter((TType)obj); |
||||||
|
Action<object, object?> IFieldWrapper.ClassSetter => (obj, value) => ClassSetter((TType)obj, value); |
||||||
|
new Func<TType, object?> ClassGetter { get; } |
||||||
|
new Action<TType, object?> ClassSetter { get; } |
||||||
|
|
||||||
|
ValueGetterAction ByRefGetter { get; } |
||||||
|
ValueSetterAction ByRefSetter { get; } |
||||||
|
} |
||||||
|
|
||||||
|
public class FieldWrapper<TField> : IFieldWrapperForType |
||||||
|
{ |
||||||
|
public delegate TField ValueGetterAction(in TType obj); |
||||||
|
public delegate void ValueSetterAction(ref TType obj, TField value); |
||||||
|
|
||||||
|
Func<TType, TField>? _classGetter; |
||||||
|
Action<TType, TField>? _classSetter; |
||||||
|
ValueGetterAction? _byRefGetter; |
||||||
|
ValueSetterAction? _byRefSetter; |
||||||
|
|
||||||
|
public ITypeWrapper DeclaringType { get; } |
||||||
|
public FieldInfo FieldInfo { get; } |
||||||
|
public PropertyInfo? PropertyInfo { get; } |
||||||
|
|
||||||
|
internal FieldWrapper(ITypeWrapper type, FieldInfo field, PropertyInfo? property) |
||||||
|
{ DeclaringType = type; FieldInfo = field; PropertyInfo = property; } |
||||||
|
|
||||||
|
Func<TType, object?> IFieldWrapperForType.ClassGetter => (obj) => ClassGetter(obj); |
||||||
|
Action<TType, object?> IFieldWrapperForType.ClassSetter => (obj, value) => ClassSetter(obj, (TField)value!); |
||||||
|
public Func<TType, TField> ClassGetter => _classGetter ??= BuildGetter<Func<TType, TField>>(false); |
||||||
|
public Action<TType, TField> ClassSetter => _classSetter ??= BuildSetter<Action<TType, TField>>(false); |
||||||
|
|
||||||
|
IFieldWrapperForType.ValueGetterAction IFieldWrapperForType.ByRefGetter => (in TType obj) => ByRefGetter(in obj); |
||||||
|
IFieldWrapperForType.ValueSetterAction IFieldWrapperForType.ByRefSetter => (ref TType obj, object? value) => ByRefSetter(ref obj, (TField)value!); |
||||||
|
public ValueGetterAction ByRefGetter => _byRefGetter ??= BuildGetter<ValueGetterAction>(true); |
||||||
|
public ValueSetterAction ByRefSetter => _byRefSetter ??= BuildSetter<ValueSetterAction>(true); |
||||||
|
|
||||||
|
|
||||||
|
TDelegate BuildGetter<TDelegate>(bool byRef) |
||||||
|
where TDelegate : Delegate |
||||||
|
{ |
||||||
|
if (DeclaringType.Type.IsValueType && !byRef) throw new InvalidOperationException( |
||||||
|
$"Can't build getter for value type ({DeclaringType.Type}) without using ref"); |
||||||
|
var method = new DynamicMethod( |
||||||
|
$"Get_{DeclaringType.Type.Name}_{FieldInfo.Name}{(byRef ? "_ByRef" : "")}", |
||||||
|
typeof(TField), new[] { byRef ? typeof(TType).MakeByRefType() : typeof(TType) }, |
||||||
|
typeof(TType).Module, true); |
||||||
|
var il = method.GetILGenerator(); |
||||||
|
il.Emit(OpCodes.Ldarg_0); |
||||||
|
if (byRef && !DeclaringType.Type.IsValueType) |
||||||
|
il.Emit(OpCodes.Ldind_Ref); |
||||||
|
il.Emit(OpCodes.Ldfld, FieldInfo); |
||||||
|
il.Emit(OpCodes.Ret); |
||||||
|
return method.CreateDelegate<TDelegate>(); |
||||||
|
} |
||||||
|
|
||||||
|
TDelegate BuildSetter<TDelegate>(bool byRef) |
||||||
|
where TDelegate : Delegate |
||||||
|
{ |
||||||
|
if (DeclaringType.Type.IsValueType && !byRef) throw new InvalidOperationException( |
||||||
|
$"Can't build setter for value type ({DeclaringType.Type}) without using ref"); |
||||||
|
var method = new DynamicMethod( |
||||||
|
$"Set_{DeclaringType.Type.Name}_{FieldInfo.Name}{(byRef ? "_ByRef" : "")}", |
||||||
|
null, new[] { byRef ? typeof(TType).MakeByRefType() : typeof(TType), typeof(TField) }, |
||||||
|
typeof(TType).Module, true); |
||||||
|
var il = method.GetILGenerator(); |
||||||
|
il.Emit(OpCodes.Ldarg_0); |
||||||
|
if (byRef && !DeclaringType.Type.IsValueType) |
||||||
|
il.Emit(OpCodes.Ldind_Ref); |
||||||
|
il.Emit(OpCodes.Ldarg_1); |
||||||
|
il.Emit(OpCodes.Stfld, FieldInfo); |
||||||
|
il.Emit(OpCodes.Ret); |
||||||
|
return method.CreateDelegate<TDelegate>(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||||
|
|
||||||
|
<PropertyGroup> |
||||||
|
<TargetFramework>net6.0</TargetFramework> |
||||||
|
<ImplicitUsings>disable</ImplicitUsings> |
||||||
|
<Nullable>enable</Nullable> |
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
||||||
|
</PropertyGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<ProjectReference Include="../flecs-cs/src/cs/production/Flecs/Flecs.csproj" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<PackageReference Include="Silk.NET.Maths" Version="2.16.0" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
</Project> |
Loading…
Reference in new issue