test
This commit is contained in:
@@ -1,88 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2012
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SjkScripts", "SjkScripts", "{A04208C6-F985-C0CB-74E4-F24675E8FC46}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SjkScripts", "src\SjkScripts\SjkScripts\SjkScripts.csproj", "{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "voxelgame", "voxelgame", "{CF22DAD7-7E92-97D6-EC07-2DE8EB5A2C9D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VoxelGame", "src\voxelgame\VoxelGame.csproj", "{01D4C862-C9DA-4444-A108-1D7C82B36B3C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||
ExportDebug|x64 = ExportDebug|x64
|
||||
ExportDebug|x86 = ExportDebug|x86
|
||||
ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||
ExportRelease|x64 = ExportRelease|x64
|
||||
ExportRelease|x86 = ExportRelease|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportDebug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportDebug|x64.ActiveCfg = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportDebug|x64.Build.0 = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportDebug|x86.ActiveCfg = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportDebug|x86.Build.0 = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportRelease|x64.ActiveCfg = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportRelease|x64.Build.0 = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportRelease|x86.ActiveCfg = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.ExportRelease|x86.Build.0 = Debug|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportDebug|x64.ActiveCfg = ExportDebug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportDebug|x64.Build.0 = ExportDebug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportDebug|x86.ActiveCfg = ExportDebug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportDebug|x86.Build.0 = ExportDebug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportRelease|x64.ActiveCfg = ExportRelease|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportRelease|x64.Build.0 = ExportRelease|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportRelease|x86.ActiveCfg = ExportRelease|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.ExportRelease|x86.Build.0 = ExportRelease|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Release|x64.ActiveCfg = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Release|x64.Build.0 = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Release|x86.ActiveCfg = Debug|Any CPU
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C}.Release|x86.Build.0 = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{A04208C6-F985-C0CB-74E4-F24675E8FC46} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{981E9EB4-1B52-43F3-969D-76FCB8F21E6E} = {A04208C6-F985-C0CB-74E4-F24675E8FC46}
|
||||
{CF22DAD7-7E92-97D6-EC07-2DE8EB5A2C9D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{01D4C862-C9DA-4444-A108-1D7C82B36B3C} = {CF22DAD7-7E92-97D6-EC07-2DE8EB5A2C9D}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Submodule src/SjkScripts updated: 1fa265a1c3...3af30d3447
1005
src/voxelgame/ChunkSurfaceNetTest.cs
Normal file
1005
src/voxelgame/ChunkSurfaceNetTest.cs
Normal file
File diff suppressed because it is too large
Load Diff
1
src/voxelgame/ChunkSurfaceNetTest.cs.uid
Normal file
1
src/voxelgame/ChunkSurfaceNetTest.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://q2phyhqtiacu
|
||||
45
src/voxelgame/FloodFill/VoxelFloodFill.cs
Normal file
45
src/voxelgame/FloodFill/VoxelFloodFill.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
// public interface IVoxelGraph<TVoxel>
|
||||
// {
|
||||
// bool Is
|
||||
// }
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
using SJK.Functional;
|
||||
using SJK.Voxels;
|
||||
|
||||
public static class VoxelFloodFill
|
||||
{
|
||||
public static IEnumerable<(Vector3I,uint)> Fill(Vector3I start, Func<Vector3I, bool> predicate, uint maxCount = uint.MaxValue, uint maxValue = uint.MaxValue)
|
||||
{
|
||||
var visited = new HashSet<Vector3I>();
|
||||
var queue = new Queue<(Vector3I Position, uint Distance)>();
|
||||
queue.Enqueue((start, 0));
|
||||
visited.Add(start);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
(var pos, var distance) = queue.Dequeue();
|
||||
yield return (pos, distance);
|
||||
if (visited.Count >= maxCount)
|
||||
yield break;
|
||||
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
var offset = ((Direction)i).ToOffset();
|
||||
if (distance >= maxValue)
|
||||
continue;
|
||||
if (predicate(pos + offset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!visited.Add(pos + offset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
queue.Enqueue((pos + offset, distance + 1));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/FloodFill/VoxelFloodFill.cs.uid
Normal file
1
src/voxelgame/FloodFill/VoxelFloodFill.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bb46scw4c0yhh
|
||||
@@ -5,4 +5,8 @@ using System;
|
||||
public partial class NewScript : Node
|
||||
{
|
||||
IOption<int> OptionTest;
|
||||
void test()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
194
src/voxelgame/SimpleDataBase.cs
Normal file
194
src/voxelgame/SimpleDataBase.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using SqlKata;
|
||||
|
||||
public sealed class SimpleDataBase : IDisposable
|
||||
{
|
||||
//TODO Need an value to store table index, and offset to index as when laoding, database does not know where database entrys start
|
||||
private const int MAGIC = 0x53444231; // "SDB1"
|
||||
private const int VERSION = 0;
|
||||
private const int INITIAILBLOCKS = 32;
|
||||
private const int BLOCKSSTART = sizeof(int) *3 + sizeof(long);
|
||||
private const int BLOCKDATASIZE = sizeof(long) + sizeof(int) + sizeof(bool);
|
||||
private readonly Stream stream;
|
||||
bool _dirty = true;
|
||||
private List<BlockData> blocks = new(32);
|
||||
public SimpleDataBase(string filePath) : this(new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) { }
|
||||
public SimpleDataBase(Stream stream)
|
||||
{
|
||||
this.stream = stream;
|
||||
Load();
|
||||
}
|
||||
public int Allocate(byte[] data)
|
||||
{
|
||||
for (int i = 0; i < blocks.Count; i++)
|
||||
{
|
||||
var block = blocks[i];
|
||||
if (!block.InUse && block.Size <= data.Length)
|
||||
{
|
||||
WriteBlock(block.Offset, data);
|
||||
block.Size = data.Length;
|
||||
block.InUse = true;
|
||||
blocks[i] = block;
|
||||
_dirty = true;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
BlockData newBlock = new();
|
||||
newBlock.Offset = stream.Position;
|
||||
stream.Write(data);
|
||||
newBlock.Size = data.Length;
|
||||
newBlock.InUse = true;
|
||||
_dirty = true;
|
||||
blocks.Add(newBlock);
|
||||
Godot.GD.PrintS(newBlock.Offset, newBlock.Size, newBlock.InUse);
|
||||
return blocks.Count - 1;
|
||||
|
||||
}
|
||||
public IEnumerable<int> GetTableEnrtys()
|
||||
{
|
||||
for (int i = 0; i < blocks.Count; i++)
|
||||
{
|
||||
if (blocks[i].InUse)
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
public byte[] Get(int id)
|
||||
{
|
||||
|
||||
if (id < 0 || id >= blocks.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(id));
|
||||
|
||||
var blk = blocks[id];
|
||||
if (!blk.InUse)
|
||||
throw new InvalidOperationException("Block deleted.");
|
||||
|
||||
var block = blocks[id];
|
||||
stream.Seek(block.Offset, SeekOrigin.Begin);
|
||||
byte[] bytes = new byte[block.Size];
|
||||
var error = stream.Read(bytes, 0, block.Size);
|
||||
return bytes;
|
||||
}
|
||||
private void Load()
|
||||
{
|
||||
if (stream.Length < sizeof(int))
|
||||
{
|
||||
CreateHeader();
|
||||
|
||||
}
|
||||
using var reader = new BinaryReader(stream, encoding: Encoding.UTF8, leaveOpen: true);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
if (reader.ReadInt32() != MAGIC)
|
||||
{
|
||||
stream.Close();
|
||||
throw new Exception("Stream does not have Magic Number, CLosing Stream");
|
||||
}
|
||||
if (reader.ReadInt32() != VERSION)
|
||||
{
|
||||
stream.Close();
|
||||
throw new Exception($"Stream Version does not match");
|
||||
}
|
||||
var blockData = GetRootTable();
|
||||
if (blockData.Offset >= 0 && blockData.Size >=0)
|
||||
{
|
||||
stream.Seek(blockData.Offset, SeekOrigin.Begin);
|
||||
|
||||
blocks = new(BytesToTable(stream));
|
||||
}
|
||||
}
|
||||
public void Save()
|
||||
{
|
||||
// if (!_dirty)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
// var blockData = GetRootTable();
|
||||
// if (table >= 0)
|
||||
// {
|
||||
// Delete(table);
|
||||
// }
|
||||
var id = Allocate(TableToBytes(blocks.ToArray()));
|
||||
var blockData = blocks[id];
|
||||
SetRootTable(blockData);
|
||||
|
||||
_dirty = false;
|
||||
}
|
||||
private byte[] TableToBytes(BlockData[] blocks)
|
||||
{
|
||||
using var memory = new MemoryStream();
|
||||
using var writer = new BinaryWriter(memory);
|
||||
writer.Write(blocks.Length);
|
||||
for (int i = 0; i < blocks.Length; i++)
|
||||
{
|
||||
WriteBlockData(writer, blocks[i]);
|
||||
Godot.GD.PrintS(blocks[i].Size, blocks[i].Offset, blocks[i].InUse);
|
||||
}
|
||||
return memory.ToArray();
|
||||
|
||||
}
|
||||
private BlockData[] BytesToTable(Stream stream)
|
||||
{
|
||||
using var reader = new BinaryReader(stream, Encoding.UTF8,leaveOpen: true);
|
||||
var length = reader.ReadInt32();
|
||||
BlockData[] blockDatas = new BlockData[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
blockDatas[i] = ReadBlockData(reader);
|
||||
Godot.GD.PrintS(blockDatas[i].Size, blockDatas[i].Offset, blockDatas[i].InUse);
|
||||
}
|
||||
return blockDatas;
|
||||
}
|
||||
public void Delete(int id)
|
||||
{
|
||||
var block = blocks[id];
|
||||
block.InUse = false;
|
||||
blocks[id] = block;
|
||||
Godot.GD.Print("ID ", id);
|
||||
}
|
||||
private BlockData ReadBlockData(BinaryReader reader) => new BlockData() { Offset = reader.ReadInt64(), Size = reader.ReadInt32(), InUse = reader.ReadBoolean() };
|
||||
private void WriteBlockData(BinaryWriter writer, BlockData block) {
|
||||
writer.Write(block.Offset);
|
||||
writer.Write(block.Size);
|
||||
writer.Write(block.InUse);
|
||||
}
|
||||
|
||||
private void CreateHeader()
|
||||
{
|
||||
using var writer = new BinaryWriter(stream,Encoding.UTF8,leaveOpen: true);
|
||||
writer.Write(MAGIC);
|
||||
writer.Write(VERSION);
|
||||
WriteBlockData(writer,new BlockData(){Offset = -1,Size = -1, InUse = false});
|
||||
}
|
||||
private void SetRootTable(BlockData block) {
|
||||
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
|
||||
stream.Seek(sizeof(int) * 2, SeekOrigin.Begin);
|
||||
WriteBlockData(writer, block);
|
||||
}
|
||||
private BlockData GetRootTable() {
|
||||
stream.Seek(sizeof(int) * 2, SeekOrigin.Begin);
|
||||
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
|
||||
return ReadBlockData(reader);
|
||||
}
|
||||
private void WriteBlock(long offset, byte[] data)
|
||||
{
|
||||
stream.Seek(offset, SeekOrigin.Begin);
|
||||
stream.Write(data);
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct BlockData
|
||||
{
|
||||
public long Offset;
|
||||
public int Size;
|
||||
public bool InUse;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
Save();
|
||||
stream?.Dispose();
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/SimpleDataBase.cs.uid
Normal file
1
src/voxelgame/SimpleDataBase.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://duss0kqmyo2ee
|
||||
@@ -1,9 +1,19 @@
|
||||
<Project Sdk="Godot.NET.Sdk/4.4.1">
|
||||
<Project Sdk="Godot.NET.Sdk/4.5.1">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- <PackageReference Include="Dapper" Version="2.1.66" /> -->
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
<PackageReference Include="SqlKata" Version="4.0.1" />
|
||||
<PackageReference Include="SqlKata.Execution" Version="4.0.1" />
|
||||
<ProjectReference Include="..\SjkScripts\SjkScripts\SjkScripts.csproj" />
|
||||
<ProjectReference Include="..\SjkScripts\SJKGodotHelpers\SJKGodotHelpers.csproj" />
|
||||
<PackageReference Include="ImGui.NET" Version="1.91.6.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
19
src/voxelgame/VoxelGame.csproj.old
Normal file
19
src/voxelgame/VoxelGame.csproj.old
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Godot.NET.Sdk/4.4.1">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- <PackageReference Include="Dapper" Version="2.1.66" /> -->
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
<PackageReference Include="SqlKata" Version="4.0.1" />
|
||||
<PackageReference Include="SqlKata.Execution" Version="4.0.1" />
|
||||
<ProjectReference Include="..\SjkScripts\SjkScripts\SjkScripts.csproj" />
|
||||
<ProjectReference Include="..\SjkScripts\SJKGodotHelpers\SJKGodotHelpers.csproj" />
|
||||
<PackageReference Include="ImGui.NET" Version="1.91.6.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
102
src/voxelgame/VoxelGame.sln
Normal file
102
src/voxelgame/VoxelGame.sln
Normal file
@@ -0,0 +1,102 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2012
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VoxelGame", "VoxelGame.csproj", "{2D3A7228-05F3-44F1-B105-AED07769FB30}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SjkScripts", "..\SjkScripts\SjkScripts\SjkScripts.csproj", "{CB613391-62B4-486F-8E89-693B981EFACD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SJKGodotHelpers", "..\SjkScripts\SJKGodotHelpers\SJKGodotHelpers.csproj", "{FB3F8FCC-6245-4D99-8268-F4663885ADC5}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||
ExportDebug|x64 = ExportDebug|x64
|
||||
ExportDebug|x86 = ExportDebug|x86
|
||||
ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||
ExportRelease|x64 = ExportRelease|x64
|
||||
ExportRelease|x86 = ExportRelease|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportDebug|x64.ActiveCfg = ExportDebug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportDebug|x64.Build.0 = ExportDebug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportDebug|x86.ActiveCfg = ExportDebug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportDebug|x86.Build.0 = ExportDebug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportRelease|x64.ActiveCfg = ExportRelease|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportRelease|x64.Build.0 = ExportRelease|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportRelease|x86.ActiveCfg = ExportRelease|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.ExportRelease|x86.Build.0 = ExportRelease|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Release|x64.ActiveCfg = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Release|x64.Build.0 = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Release|x86.ActiveCfg = Debug|Any CPU
|
||||
{2D3A7228-05F3-44F1-B105-AED07769FB30}.Release|x86.Build.0 = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportDebug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportDebug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportDebug|x64.Build.0 = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportDebug|x86.ActiveCfg = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportDebug|x86.Build.0 = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportRelease|x64.ActiveCfg = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportRelease|x64.Build.0 = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportRelease|x86.ActiveCfg = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.ExportRelease|x86.Build.0 = Debug|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Release|x64.Build.0 = Release|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{CB613391-62B4-486F-8E89-693B981EFACD}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportDebug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportDebug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportDebug|x64.Build.0 = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportDebug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportDebug|x86.Build.0 = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportRelease|x64.ActiveCfg = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportRelease|x64.Build.0 = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportRelease|x86.ActiveCfg = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.ExportRelease|x86.Build.0 = Debug|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FB3F8FCC-6245-4D99-8268-F4663885ADC5}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
142
src/voxelgame/Voxels/BoxelMesher.cs
Normal file
142
src/voxelgame/Voxels/BoxelMesher.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
|
||||
namespace SJK.Voxels;
|
||||
|
||||
public class BoxelMesher<TVoxel>
|
||||
{
|
||||
private readonly IBoxelMesherApi<TVoxel> api;
|
||||
|
||||
public BoxelMesher(IBoxelMesherApi<TVoxel> api)
|
||||
{
|
||||
this.api = api;
|
||||
}
|
||||
public Mesh Mesh(IVoxelChunk<TVoxel> values)
|
||||
{
|
||||
var vertices = new List<Vector3>();
|
||||
var normals = new List<Vector3>();
|
||||
var triangles = new List<int>();
|
||||
for (int x = values.Bounds.Min.X; x < values.Bounds.Max.X; x++)
|
||||
{
|
||||
for (int y = values.Bounds.Min.Y; y < values.Bounds.Max.Y; y++)
|
||||
{
|
||||
for (int z = values.Bounds.Min.Z; z < values.Bounds.Max.Z; z++)
|
||||
{
|
||||
var pos = new Vector3I(x, y, z);
|
||||
var voxel = values.GetVoxel(pos);
|
||||
|
||||
if (!api.IsSolid(voxel))
|
||||
continue;
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
var neighborPos = pos + ((Direction)i).ToOffset();
|
||||
// GD.Print(neighborPos);
|
||||
if (!values.Bounds.IsInBounds(neighborPos) || !api.IsSolid(values.GetVoxel(neighborPos)))
|
||||
{
|
||||
AddFace((Direction)i, pos, vertices, triangles, normals);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vertices.Count == 0)
|
||||
return new BoxMesh();
|
||||
Godot.Collections.Array surfaceArray = [];
|
||||
surfaceArray.Resize((int)Godot.Mesh.ArrayType.Max);
|
||||
surfaceArray[(int)Godot.Mesh.ArrayType.Vertex] = vertices.ToArray();
|
||||
// surfaceArray[(int)Mesh.ArrayType.TexUV] = uvs.ToArray();
|
||||
surfaceArray[(int)Godot.Mesh.ArrayType.Normal] = normals.ToArray();
|
||||
surfaceArray[(int)Godot.Mesh.ArrayType.Index] = triangles.ToArray();
|
||||
var arrMesh = new ArrayMesh();
|
||||
// No blendshapes, lods, or compression used.
|
||||
arrMesh.AddSurfaceFromArrays(Godot.Mesh.PrimitiveType.Triangles, surfaceArray);
|
||||
return arrMesh;
|
||||
}
|
||||
private static void AddFace(Direction dir, Vector3 pos, List<Vector3> vertices, List<int> triangles,List<Vector3> normals)
|
||||
{
|
||||
int index = vertices.Count;
|
||||
|
||||
// Normal of the face
|
||||
Vector3 normal = dir.ToOffset();
|
||||
|
||||
// Pick two orthogonal vectors to span the quad
|
||||
Vector3 tangent, bitangent;
|
||||
if (normal.X != 0) // Face along X → use Y,Z as axes
|
||||
{
|
||||
tangent = new Vector3(0, 1, 0);
|
||||
bitangent = new Vector3(0, 0, -1);
|
||||
}
|
||||
else if (normal.Y != 0) // Face along Y → use X,Z as axes
|
||||
{
|
||||
tangent = new Vector3(1, 0, 0);
|
||||
bitangent = new Vector3(0, 0, -1);
|
||||
}
|
||||
else // Face along Z → use X,Y as axes
|
||||
{
|
||||
tangent = new Vector3(1, 0, 0);
|
||||
bitangent = new Vector3(0, 1, 0);
|
||||
}
|
||||
|
||||
// Scale to half-unit cube
|
||||
tangent *= 0.5f;
|
||||
bitangent *= 0.5f;
|
||||
normal *= 0.5f;
|
||||
|
||||
// Quad center is offset in face direction
|
||||
Vector3 center = pos + normal;
|
||||
|
||||
// Generate 4 vertices of the quad
|
||||
vertices.Add(center - tangent - bitangent); // bottom-left
|
||||
vertices.Add(center + tangent - bitangent); // bottom-right
|
||||
vertices.Add(center - tangent + bitangent); // top-left
|
||||
vertices.Add(center + tangent + bitangent); // top-right
|
||||
normals.Add(dir.ToOffset());
|
||||
normals.Add(dir.ToOffset());
|
||||
normals.Add(dir.ToOffset());
|
||||
normals.Add(dir.ToOffset());
|
||||
|
||||
// Add two triangles (winding order consistent with normal)
|
||||
if (normal.X>0 ||normal.Y<0 ||normal.Z<0 )
|
||||
{
|
||||
triangles.Add(index + 0);
|
||||
triangles.Add(index + 1);
|
||||
triangles.Add(index + 2);
|
||||
triangles.Add(index + 1);
|
||||
triangles.Add(index + 3);
|
||||
triangles.Add(index + 2);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
triangles.Add(index + 0);
|
||||
triangles.Add(index + 2);
|
||||
triangles.Add(index + 1);
|
||||
triangles.Add(index + 1);
|
||||
triangles.Add(index + 2);
|
||||
triangles.Add(index + 3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
public interface IBoxelMesherApi<TVoxel>
|
||||
{
|
||||
public void GetUvs(TVoxel id, Direction direction);
|
||||
public bool IsSolid(TVoxel id);
|
||||
|
||||
}
|
||||
public static class DirectionExtensions
|
||||
{
|
||||
public static Vector3I ToOffset(this Direction dir) => dir switch
|
||||
{
|
||||
Direction.Forward => new Vector3I(0, 0, -1),
|
||||
Direction.BackWord => new Vector3I(0, 0, 1),
|
||||
Direction.Right => new Vector3I(1, 0, 0),
|
||||
Direction.Left => new Vector3I(-1, 0, 0),
|
||||
Direction.Up => new Vector3I(0, 1, 0),
|
||||
Direction.Down => new Vector3I(0, -1, 0),
|
||||
_ => Vector3I.Zero
|
||||
};
|
||||
}
|
||||
1
src/voxelgame/Voxels/BoxelMesher.cs.uid
Normal file
1
src/voxelgame/Voxels/BoxelMesher.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://puaveig1esds
|
||||
464
src/voxelgame/Voxels/Chunks/ChunkPos.cs
Normal file
464
src/voxelgame/Voxels/Chunks/ChunkPos.cs
Normal file
@@ -0,0 +1,464 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Godot;
|
||||
using GodotSteam; // or Godot's Vector3I if you’re in Godot
|
||||
public interface IChunkPos//: IEquatable<IChunkPos>
|
||||
{
|
||||
public int X { get; }
|
||||
public int Y { get; }
|
||||
public int Z { get; }
|
||||
long ToKey();
|
||||
}
|
||||
public record struct Dimension(int Id);
|
||||
public interface IChunkPos<TSelf> :
|
||||
IChunkPos,
|
||||
IAdditionOperators<TSelf, TSelf, TSelf>,
|
||||
ISubtractionOperators<TSelf, TSelf, TSelf>,
|
||||
IEquatable<TSelf>
|
||||
where TSelf : struct, IChunkPos<TSelf>
|
||||
{
|
||||
static abstract TSelf Create(int x, int y, int z);
|
||||
static abstract TSelf FromKey(long key);
|
||||
// static abstract int MaxWidth {get;}
|
||||
// static abstract int MaxHeight {get;}
|
||||
// static abstract int MaxDepth {get;}
|
||||
// static abstract int MinWidth {get;}
|
||||
// static abstract int MinHeight {get;}
|
||||
// static abstract int MinDepth {get;}
|
||||
|
||||
}
|
||||
|
||||
public readonly struct ChunkIndex3D :IChunkPos<ChunkIndex3D>,
|
||||
IEquatable<ChunkIndex3D>,
|
||||
IAdditionOperators<ChunkIndex3D, ChunkIndex3D, ChunkIndex3D>,
|
||||
ISubtractionOperators<ChunkIndex3D, ChunkIndex3D, ChunkIndex3D>
|
||||
{
|
||||
private readonly long _value;
|
||||
|
||||
public long Value => _value;
|
||||
|
||||
// Bit sizes
|
||||
const int ChunkXBits = 21;
|
||||
const int ChunkZBits = 21;
|
||||
const int ChunkYBits = 9;
|
||||
const int GuardBits = 1;
|
||||
const int DimensionBits = 10;
|
||||
const int ReservedBits = 2;
|
||||
|
||||
// Bit shifts
|
||||
const int ChunkXShift = 0;
|
||||
const int ChunkZShift = ChunkXShift + ChunkXBits;
|
||||
const int ChunkYShift = ChunkZShift + ChunkZBits;
|
||||
const int GuardShift = ChunkYShift + ChunkYBits;
|
||||
const int DimensionShift = GuardShift + GuardBits;
|
||||
const int ReservedShift = DimensionShift + DimensionBits;
|
||||
|
||||
// Masks
|
||||
const long ChunkXMask = (1L << ChunkXBits) - 1;
|
||||
const long ChunkZMask = (1L << ChunkZBits) - 1;
|
||||
const long ChunkYMask = (1L << ChunkYBits) - 1;
|
||||
const long DimensionMask = (1L << DimensionBits) - 1;
|
||||
|
||||
public ChunkIndex3D(long packed)
|
||||
{
|
||||
_value = packed;
|
||||
}
|
||||
|
||||
public ChunkIndex3D(int x, int y, int z, int dimension = 0)
|
||||
{
|
||||
_value =
|
||||
((long)x & ChunkXMask) << ChunkXShift |
|
||||
((long)z & ChunkZMask) << ChunkZShift |
|
||||
((long)y & ChunkYMask) << ChunkYShift |
|
||||
((long)dimension & DimensionMask) << DimensionShift;
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// Field Accessors
|
||||
// --------------------------------
|
||||
|
||||
public int X => (int)((_value >> ChunkXShift) & ChunkXMask);
|
||||
|
||||
public int Z => (int)((_value >> ChunkZShift) & ChunkZMask);
|
||||
|
||||
public int Y => (int)((_value >> ChunkYShift) & ChunkYMask);
|
||||
|
||||
public int Dimension => (int)((_value >> DimensionShift) & DimensionMask);
|
||||
|
||||
// --------------------------------
|
||||
// Offset Helpers
|
||||
// --------------------------------
|
||||
|
||||
public ChunkIndex3D Offset(int dx, int dy, int dz)
|
||||
{
|
||||
return new ChunkIndex3D(
|
||||
X + dx,
|
||||
Y + dy,
|
||||
Z + dz,
|
||||
Dimension
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// Operators
|
||||
// --------------------------------
|
||||
|
||||
public static ChunkIndex3D operator +(ChunkIndex3D a, ChunkIndex3D b)
|
||||
{
|
||||
if (a.Dimension != b.Dimension)
|
||||
throw new InvalidOperationException("Cannot add indices from different dimensions.");
|
||||
|
||||
return new ChunkIndex3D(
|
||||
a.X + b.X,
|
||||
a.Y + b.Y,
|
||||
a.Z + b.Z,
|
||||
a.Dimension
|
||||
);
|
||||
}
|
||||
|
||||
public static ChunkIndex3D operator -(ChunkIndex3D a, ChunkIndex3D b)
|
||||
{
|
||||
if (a.Dimension != b.Dimension)
|
||||
throw new InvalidOperationException("Cannot subtract indices from different dimensions.");
|
||||
|
||||
return new ChunkIndex3D(
|
||||
a.X - b.X,
|
||||
a.Y - b.Y,
|
||||
a.Z - b.Z,
|
||||
a.Dimension
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// Equality
|
||||
// --------------------------------
|
||||
|
||||
public bool Equals(ChunkIndex3D other) => _value == other._value;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is ChunkIndex3D other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => _value.GetHashCode();
|
||||
|
||||
public static bool operator ==(ChunkIndex3D a, ChunkIndex3D b)
|
||||
=> a._value == b._value;
|
||||
|
||||
public static bool operator !=(ChunkIndex3D a, ChunkIndex3D b)
|
||||
=> a._value != b._value;
|
||||
|
||||
public override string ToString()
|
||||
=> $"ChunkIndex3D({X},{Y},{Z},Dim:{Dimension})";
|
||||
|
||||
public static ChunkIndex3D Create(int x, int y, int z)=> new(x,y,z);
|
||||
|
||||
public static ChunkIndex3D FromKey(long key)=>new(key);
|
||||
|
||||
|
||||
public long ToKey()=>Value;
|
||||
// }
|
||||
public static ChunkIndex3D FromGlobal(int x, int y, int z)
|
||||
{
|
||||
return new ChunkIndex3D(
|
||||
x >> Chunk32.BitsPerAxis,
|
||||
y >> Chunk32.BitsPerAxis,
|
||||
z >> Chunk32.BitsPerAxis
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
public readonly struct ChunkPos<TChunk> : IEquatable<ChunkPos<TChunk>>, IChunkPos<ChunkPos<TChunk>> where TChunk : IChunkSize<TChunk>
|
||||
{
|
||||
public readonly Vector3I Value;
|
||||
private readonly long _data;
|
||||
public int X => Value.X;
|
||||
public int Y => Value.Y;
|
||||
public int Z => Value.Z;
|
||||
public ChunkPos(Vector3I value)
|
||||
{
|
||||
if (value.X < MinWidth || value.Y < MinHeight || value.Z < MinDepth || value.X > MaxWidth || value.Y > MaxHeight || value.Z > MaxDepth )
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public ChunkPos(int x, int y, int z) : this(new(x,y,z))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Create from global voxel position
|
||||
public static ChunkPos<TChunk> FromGlobal(Vector3I globalPos) => FromGlobal(globalPos.X, globalPos.Y, globalPos.Z);
|
||||
// public static ChunkPos<TChunk> FromGlobal(int x, int y, int z)
|
||||
// {
|
||||
// return new ChunkPos<TChunk>(new Vector3I(
|
||||
// Math.DivRem(x, TChunk.Size, out int _),
|
||||
// Math.DivRem(y, TChunk.Size, out int _),
|
||||
// Math.DivRem(z, TChunk.Size, out int _)
|
||||
// ));
|
||||
// }
|
||||
public static ChunkPos<TChunk> FromGlobal(int x, int y, int z)
|
||||
{
|
||||
return new ChunkPos<TChunk>(new Vector3I(
|
||||
x >> TChunk.BitsPerAxis,
|
||||
y >> TChunk.BitsPerAxis,
|
||||
z >> TChunk.BitsPerAxis
|
||||
));
|
||||
}
|
||||
|
||||
// Convert chunk position -> global voxel origin
|
||||
public Vector3I ToGlobalOrigin()
|
||||
{
|
||||
return new Vector3I(
|
||||
Value.X * TChunk.Size,
|
||||
Value.Y * TChunk.Size,
|
||||
Value.Z * TChunk.Size
|
||||
);
|
||||
}
|
||||
|
||||
// Convert global voxel position -> local position inside this chunk
|
||||
public static VoxelPos<TChunk> ToLocal(Vector3I globalPos) => ToLocal(globalPos.X, globalPos.Y, globalPos.Z);
|
||||
// public static VoxelPos<TChunk> ToLocal(int x, int y, int z)
|
||||
// {
|
||||
// return new VoxelPos<TChunk>(
|
||||
// Mod(x, TChunk.Size),
|
||||
// Mod(y, TChunk.Size),
|
||||
// Mod(z, TChunk.Size)
|
||||
// );
|
||||
// }
|
||||
public static VoxelPos<TChunk> ToLocal(int x, int y, int z)
|
||||
{
|
||||
int mask = TChunk.Size - 1;
|
||||
|
||||
return new VoxelPos<TChunk>(
|
||||
x & mask,
|
||||
y & mask,
|
||||
z & mask);
|
||||
}
|
||||
|
||||
// Bounds check
|
||||
// public bool IsInBounds(Vector3I localPos)
|
||||
// {
|
||||
// return localPos.X >= 0 && localPos.X < TChunk.Size &&
|
||||
// localPos.Y >= 0 && localPos.Y < TChunk.Size &&
|
||||
// localPos.Z >= 0 && localPos.Z < TChunk.Size;
|
||||
// }
|
||||
// Cast to Vector3I
|
||||
public static implicit operator Vector3I(ChunkPos<TChunk> pos) => pos.Value;
|
||||
|
||||
// Equality
|
||||
public bool Equals(ChunkPos<TChunk> other) => Value == other.Value;
|
||||
public override bool Equals(object? obj) => obj is ChunkPos<TChunk> other && Equals(other);
|
||||
public override int GetHashCode() => Value.GetHashCode();
|
||||
|
||||
public override string ToString() => $"ChunkPos({Value.X}, {Value.Y}, {Value.Z})";
|
||||
|
||||
// --- Casting operators ---
|
||||
public static explicit operator ChunkPos<TChunk>(Vector3I v) => new ChunkPos<TChunk>(v);
|
||||
|
||||
// --- Arithmetic helpers (optional) ---
|
||||
public static ChunkPos<TChunk> operator +(ChunkPos<TChunk> a, ChunkPos<TChunk> b) => new ChunkPos<TChunk>(a.Value + b.Value);
|
||||
public static ChunkPos<TChunk> operator -(ChunkPos<TChunk> a, ChunkPos<TChunk> b) => new ChunkPos<TChunk>(a.Value - b.Value);
|
||||
|
||||
// Proper mod to handle negatives
|
||||
private static int Mod(int a, int b) => (a % b + b) % b;
|
||||
private const int BitsPerAxis = 21;
|
||||
private const int BitsXZAxis = 21;
|
||||
private const int BitsYAxis = 21;
|
||||
private const int BitsDimension = 10;
|
||||
private const long Mask = (1L << BitsPerAxis) - 1;
|
||||
private const int OffsetY = BitsPerAxis;
|
||||
private const int OffsetZ = BitsPerAxis * 2;
|
||||
private const int Bias = 1 << (BitsPerAxis - 1);
|
||||
public static int MinWidth{get;} = (int)-(1L << (BitsXZAxis - 1));
|
||||
public static int MaxWidth{get;} = (int)(1L << (BitsXZAxis - 1)) - 1;
|
||||
public static int MinHeight{get;} = (int)-(1L << (BitsYAxis - 1));
|
||||
public static int MaxHeight{get;} = (int)(1L << (BitsYAxis - 1)) - 1;
|
||||
public static int MinDepth{get;} = MinWidth;
|
||||
public static int MaxDepth{get;} = MaxWidth;
|
||||
|
||||
|
||||
public long ToKey()
|
||||
{
|
||||
long x = (long)(X + Bias) & Mask;
|
||||
long y = (long)(Y + Bias) & Mask;
|
||||
long z = (long)(Z + Bias) & Mask;
|
||||
return (z << OffsetZ) | (y << OffsetY) | x;
|
||||
}
|
||||
|
||||
public static ChunkPos<TChunk> FromKey(long key)
|
||||
{
|
||||
int x = (int)((key & Mask) - Bias);
|
||||
int y = (int)(((key >> OffsetY) & Mask) - Bias);
|
||||
int z = (int)(((key >> OffsetZ) & Mask) - Bias);
|
||||
return new ChunkPos<TChunk>(x, y, z);
|
||||
}
|
||||
|
||||
public static ChunkPos<TChunk> Create(int x, int y, int z)
|
||||
{
|
||||
return new ChunkPos<TChunk>(x, y, z);
|
||||
}
|
||||
|
||||
// public bool Equals(IChunkPos other)=> X==other.X && Y == other.Y && Z == other.Z;
|
||||
}
|
||||
public interface IChunkSize<TSelf>
|
||||
where TSelf : IChunkSize<TSelf>
|
||||
{
|
||||
static abstract int BitsPerAxis { get; }
|
||||
static abstract int Size { get; }
|
||||
static int SizeSquared => TSelf.Size * TSelf.Size;
|
||||
static int SizeCubed => TSelf.Size * TSelf.Size * TSelf.Size;
|
||||
static bool IsInBounds(Vector3I position)=>
|
||||
position.X >= 0 && position.X < TSelf.Size
|
||||
&& position.Y >= 0 && position.Y < TSelf.Size
|
||||
&& position.Z >= 0 && position.Z < TSelf.Size;
|
||||
}
|
||||
public readonly struct Chunk16 : IChunkSize<Chunk16>
|
||||
{
|
||||
public static int BitsPerAxis => 4; // 2^5 = 32
|
||||
|
||||
public static int Size => 16;
|
||||
}
|
||||
public readonly struct Chunk32 : IChunkSize<Chunk32>
|
||||
{
|
||||
public static int BitsPerAxis => 5; // 2^5 = 32
|
||||
public static int Size => 32;
|
||||
}
|
||||
|
||||
public readonly struct Chunk64 : IChunkSize<Chunk64>
|
||||
{
|
||||
public static int BitsPerAxis => 6; // 2^6 = 64
|
||||
|
||||
public static int Size => 64;
|
||||
}
|
||||
|
||||
public readonly struct Chunk256 : IChunkSize<Chunk256>
|
||||
{
|
||||
public static int BitsPerAxis => 8; // 2^6 = 64
|
||||
public static int Size => 256;
|
||||
}
|
||||
public readonly struct VoxelPos : IEquatable<VoxelPos>
|
||||
{
|
||||
public readonly int X;
|
||||
public readonly int Y;
|
||||
public readonly int Z;
|
||||
public ChunkPos<TChunk> GetChunkPos<TChunk>() where TChunk : IChunkSize<TChunk> => ChunkPos<TChunk>.FromGlobal(X, Y, Z);
|
||||
public VoxelPos<TChunk> GetVoxelPos<TChunk>() where TChunk : IChunkSize<TChunk> => ChunkPos<TChunk>.ToLocal(X, Y, Z);
|
||||
|
||||
public bool Equals(VoxelPos other) => !(X != other.X|| Y != other.Y|| Z != other.Z);
|
||||
public static VoxelPos operator +(VoxelPos a, VoxelPos b) => new VoxelPos(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
|
||||
public static VoxelPos operator -(VoxelPos a, VoxelPos b) => new VoxelPos(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
|
||||
public static implicit operator VoxelPos(Vector3I pos) => new VoxelPos(pos.X,pos.Y,pos.Z);
|
||||
public static explicit operator Vector3I(VoxelPos pos) => new Vector3I(pos.X,pos.Y,pos.Z);
|
||||
public VoxelPos(int x, int y, int z)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
}
|
||||
public VoxelPos()
|
||||
{
|
||||
X = 0;
|
||||
Y = 0;
|
||||
Z = 0;
|
||||
}
|
||||
public override string ToString() => $"({X},{Y},{Z})";
|
||||
public override int GetHashCode()=> HashCode.Combine(X,Y,Z);
|
||||
}
|
||||
public readonly struct VoxelPos<TChunk>
|
||||
where TChunk : IChunkSize<TChunk>
|
||||
{
|
||||
private readonly uint _packed;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]//TODO see if atualt preformace increase
|
||||
public VoxelPos(int x, int y, int z)
|
||||
{
|
||||
if ((uint)x >= TChunk.Size || (uint)y >= TChunk.Size || (uint)z >= TChunk.Size)
|
||||
throw new ArgumentOutOfRangeException("Voxel position must be inside chunk bounds");//TODO check if negitive case are handled
|
||||
|
||||
int bits = TChunk.BitsPerAxis;
|
||||
int mask = (1 << bits) - 1;
|
||||
_packed = (uint)((x & mask) | ((y & mask) << bits) | ((z & mask) << (bits * 2)));
|
||||
}
|
||||
public VoxelPos(uint index)
|
||||
{
|
||||
if (index >= TChunk.Size * TChunk.Size * TChunk.Size)
|
||||
throw new ArgumentOutOfRangeException("Voxel position must be inside chunk bounds");//TODO check if negitive case are handled
|
||||
_packed = index;
|
||||
}
|
||||
public static bool TryCreateVoxelPos(VoxelPos pos, [NotNullWhen(true)] out VoxelPos<TChunk>? voxelPos)
|
||||
{
|
||||
|
||||
if ((uint)pos.X < TChunk.Size || (uint)pos.Y < TChunk.Size || (uint)pos.Z < TChunk.Size)
|
||||
{
|
||||
voxelPos = new VoxelPos<TChunk>(pos.X,pos.Y,pos.Z);
|
||||
return true;
|
||||
}
|
||||
|
||||
voxelPos = null;
|
||||
return false;
|
||||
}
|
||||
public VoxelPos ToGlobal(ChunkPos<TChunk> chunkPos) => chunkPos.ToGlobalOrigin() + new VoxelPos(X,Y,Z);
|
||||
public (ChunkPos<TChunk> ChunkPos, VoxelPos<TChunk> VoxelPos) Offset(Vector3I offset, ChunkPos<TChunk> currentChunk)
|
||||
{
|
||||
int nx = X;
|
||||
int ny = Y;
|
||||
int nz = Z;
|
||||
|
||||
int cx = currentChunk.X;
|
||||
int cy = currentChunk.Y;
|
||||
int cz = currentChunk.Z;
|
||||
nx += offset.X;
|
||||
ny += offset.Y;
|
||||
nz += offset.Z;
|
||||
|
||||
// X
|
||||
if (nx < 0 || nx >= TChunk.Size)
|
||||
{
|
||||
int dx = Math.DivRem(nx, TChunk.Size, out nx); // fast div + remainder
|
||||
cx += dx;
|
||||
}
|
||||
// Y
|
||||
if (ny < 0 || ny >= TChunk.Size)
|
||||
{
|
||||
int dy = Math.DivRem(ny, TChunk.Size, out ny);
|
||||
cy += dy;
|
||||
}
|
||||
// Z
|
||||
if (nz < 0 || nz >= TChunk.Size)
|
||||
{
|
||||
int dz = Math.DivRem(nz, TChunk.Size, out nz);
|
||||
cz += dz;
|
||||
}
|
||||
|
||||
return (new ChunkPos<TChunk>(cx, cy, cz), new VoxelPos<TChunk>(nx, ny, nz));
|
||||
}
|
||||
// public VoxelPos(uint packed) => _packed = packed;
|
||||
|
||||
public uint Index => _packed;
|
||||
|
||||
private static int Bits => TChunk.BitsPerAxis;
|
||||
private static int Mask => (1 << Bits) - 1;
|
||||
|
||||
public int X => (int)(_packed & Mask);
|
||||
public int Y => (int)((_packed >> Bits) & Mask);
|
||||
public int Z => (int)((_packed >> (Bits * 2)) & Mask);
|
||||
|
||||
public override string ToString() => $"({X}, {Y}, {Z}) idx={Index}";
|
||||
public override int GetHashCode() => _packed.GetHashCode();
|
||||
|
||||
// Implicit conversion to raw index
|
||||
public static explicit operator uint(VoxelPos<TChunk> pos) => pos._packed;
|
||||
public static implicit operator Vector3I(VoxelPos<TChunk> pos) => new Vector3I(pos.X, pos.Y, pos.Z);
|
||||
public static explicit operator VoxelPos(VoxelPos<TChunk> pos) => new Vector3I(pos.X, pos.Y, pos.Z);
|
||||
|
||||
public static VoxelPos<TChunk> operator +(VoxelPos<TChunk> a, Vector3I b) =>
|
||||
new VoxelPos<TChunk>(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
|
||||
|
||||
public static VoxelPos<TChunk> operator -(VoxelPos<TChunk> a, Vector3I b) =>
|
||||
new VoxelPos<TChunk>(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
|
||||
|
||||
}
|
||||
|
||||
1
src/voxelgame/Voxels/Chunks/ChunkPos.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/ChunkPos.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dlgeisueoqh40
|
||||
193
src/voxelgame/Voxels/Chunks/IVoxelChunk.cs
Normal file
193
src/voxelgame/Voxels/Chunks/IVoxelChunk.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
using SJK.Functional;
|
||||
using System.Linq;
|
||||
|
||||
namespace SJK.Voxels;
|
||||
public interface IVoxelChunk<TVoxel> : IReadOnlyVoxelChunk<TVoxel>
|
||||
{
|
||||
void SetVoxel(VoxelPos voxelPos, TVoxel voxel);
|
||||
virtual void Fill(TVoxel voxel)
|
||||
{
|
||||
for (int x = Bounds.Min.X; x < this.Bounds.Max.X; x++)
|
||||
{
|
||||
for (int y = Bounds.Min.Y; y < this.Bounds.Max.Y; y++)
|
||||
{
|
||||
for (int z = Bounds.Min.Z; z < Bounds.Max.Z; z++)
|
||||
{
|
||||
SetVoxel(new VoxelPos(x,y,z),voxel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public interface IVoxelChunk<TVoxel, TChunk> : IVoxelChunk<TVoxel>, IReadOnlyVoxelChunk<TVoxel,TChunk> where TChunk : IChunkSize<TChunk>
|
||||
{
|
||||
void SetVoxel(VoxelPos<TChunk> position, TVoxel voxel);
|
||||
}
|
||||
// public interface IVoxelChunk : IReadOnlyVoxelChunk
|
||||
// {
|
||||
|
||||
// }
|
||||
|
||||
// public interface IReadOnlyVoxelChunk
|
||||
// {
|
||||
// }
|
||||
public interface IReadOnlyVoxelChunk<TVoxel>// : IReadOnlyVoxelChunk
|
||||
{
|
||||
TVoxel GetVoxel(VoxelPos voxelPos);
|
||||
VoxelChunkEnumerator<TVoxel> GetEnumerator() => new VoxelChunkEnumerator<TVoxel>(this);
|
||||
virtual bool ContainsPos(VoxelPos position) => Bounds.IsInBounds((Vector3I)position);
|
||||
VoxelBounds Bounds {get;}
|
||||
|
||||
}
|
||||
public readonly record struct VoxelBounds(Vector3I Min, Vector3I Max)
|
||||
{
|
||||
public Vector3I Size => Max-Min;
|
||||
public bool IsInBounds(Vector3I pos) => !(pos.X < Min.X || pos.Y < Min.Y || pos.Z < Min.Z || pos.X > Max.X || pos.Y > Max.Y || pos.Z > Max.Z);
|
||||
}
|
||||
public interface IReadOnlyVoxelChunk<TVoxel, TChunk> : IReadOnlyVoxelChunk<TVoxel> where TChunk : IChunkSize<TChunk>
|
||||
{
|
||||
TVoxel GetVoxel(VoxelPos<TChunk> position);
|
||||
bool IsInBounds(Vector3I position) => IChunkSize<TChunk>.IsInBounds(position);
|
||||
virtual bool ContainsPos(VoxelPos<TChunk> position) => Bounds.IsInBounds((Vector3I)position);
|
||||
new VoxelChunkEnumerator<TVoxel,TChunk> GetEnumerator() => new VoxelChunkEnumerator<TVoxel,TChunk>(this);
|
||||
}
|
||||
public static class VoxelExtensions{
|
||||
public static void SetVoxel<TVoxel>(this IVoxelChunk<TVoxel> voxelChunk, int x, int y, int z, TVoxel value) =>voxelChunk.SetVoxel(new VoxelPos(x,y,z),value);
|
||||
public static TVoxel GetVoxel<TVoxel>(this IVoxelChunk<TVoxel> voxelChunk, int x, int y, int z) =>voxelChunk.GetVoxel(new VoxelPos(x,y,z));
|
||||
}
|
||||
public interface ILayerMap<TVoxel> {
|
||||
bool TryGetChunk(Vector3I chunkPos, out IVoxelChunk<TVoxel> chunk); // Untyped
|
||||
void SetChunk(Vector3I chunkPos, IVoxelChunk<TVoxel> chunk);
|
||||
}
|
||||
public class LayerMap<TVoxel> : ILayerMap<TVoxel> {
|
||||
private readonly Dictionary<Vector3I, IVoxelChunk<TVoxel>> chunks = new();
|
||||
|
||||
public bool TryGetChunk(Vector3I chunkPos, out IVoxelChunk<TVoxel> chunk)
|
||||
{
|
||||
var r =chunks.TryGetValue(chunkPos, out var result);
|
||||
chunk = result;
|
||||
return r;
|
||||
}
|
||||
public void SetChunk(Vector3I chunkPos, IVoxelChunk<TVoxel> chunk) =>
|
||||
chunks[chunkPos] = chunk;
|
||||
}
|
||||
public interface IWorldWrapper
|
||||
{
|
||||
Vector3I Wrap(Vector3I position);
|
||||
Vector3I GetBounds();
|
||||
AxisMode AxisX { get; }
|
||||
AxisMode AxisY {get;}
|
||||
AxisMode AxisZ {get;}
|
||||
bool IsInsideBounds(Vector3I position);
|
||||
enum AxisMode : byte { Constrined, Infinate, Wrapping }
|
||||
|
||||
}
|
||||
public sealed class LimitedSizeWorld(Vector3I size) : IWorldWrapper
|
||||
{
|
||||
private readonly Vector3I size = size;
|
||||
|
||||
public IWorldWrapper.AxisMode AxisX => IWorldWrapper.AxisMode.Constrined;
|
||||
|
||||
public IWorldWrapper.AxisMode AxisY => IWorldWrapper.AxisMode.Constrined;
|
||||
|
||||
public IWorldWrapper.AxisMode AxisZ => IWorldWrapper.AxisMode.Constrined;
|
||||
|
||||
public Vector3I GetBounds() => size;
|
||||
|
||||
public bool IsInsideBounds(Vector3I position) =>
|
||||
position.X >= 0 && position.X < size.X &&
|
||||
position.Y >= 0 && position.Y < size.Y &&
|
||||
position.Z >= 0 && position.Z < size.Z;
|
||||
|
||||
public Vector3I Wrap(Vector3I position)
|
||||
{
|
||||
return position;
|
||||
}
|
||||
}
|
||||
public sealed class EndlessSizeXZWorld(int maxHeight) : IWorldWrapper
|
||||
{
|
||||
public int MaxHeight { get; } = maxHeight;
|
||||
|
||||
public IWorldWrapper.AxisMode AxisX => IWorldWrapper.AxisMode.Infinate;
|
||||
|
||||
public IWorldWrapper.AxisMode AxisY => IWorldWrapper.AxisMode.Constrined;
|
||||
|
||||
public IWorldWrapper.AxisMode AxisZ => IWorldWrapper.AxisMode.Infinate;
|
||||
|
||||
public Vector3I Wrap(Vector3I position)
|
||||
=> position;
|
||||
|
||||
public Vector3I GetBounds()
|
||||
=> new Vector3I(0, MaxHeight, 0);
|
||||
public bool IsInsideBounds(Vector3I position) => position.Y >= 0 && position.Y < MaxHeight;
|
||||
|
||||
|
||||
}
|
||||
public sealed class ToroidalWrapping(Vector3I size) : IWorldWrapper
|
||||
{
|
||||
private readonly Vector3I size = size;
|
||||
|
||||
public IWorldWrapper.AxisMode AxisX => IWorldWrapper.AxisMode.Wrapping;
|
||||
|
||||
public IWorldWrapper.AxisMode AxisY => IWorldWrapper.AxisMode.Constrined;
|
||||
|
||||
public IWorldWrapper.AxisMode AxisZ => IWorldWrapper.AxisMode.Wrapping;
|
||||
|
||||
public Vector3I GetBounds() => size;
|
||||
|
||||
public Vector3I Wrap(Vector3I position) => new Vector3I(Mod(position.X, size.X), position.Y, Mod(position.Z, size.Z));
|
||||
public bool IsInsideBounds(Vector3I position) => position.Y >= 0 && position.Y < size.Y;
|
||||
private int Mod(int a, int b)
|
||||
{
|
||||
int r = a % b;
|
||||
return r < 0 ? r + b : r;
|
||||
}
|
||||
}
|
||||
public sealed class MirroredZWrappingXOffset(Vector3I size) : IWorldWrapper
|
||||
{
|
||||
private readonly Vector3I size = size;
|
||||
private readonly int halfX = size.X / 2;
|
||||
|
||||
public IWorldWrapper.AxisMode AxisX => IWorldWrapper.AxisMode.Wrapping;
|
||||
public IWorldWrapper.AxisMode AxisY => IWorldWrapper.AxisMode.Constrined;
|
||||
public IWorldWrapper.AxisMode AxisZ => IWorldWrapper.AxisMode.Wrapping;
|
||||
|
||||
public Vector3I GetBounds() => size;
|
||||
|
||||
public Vector3I Wrap(Vector3I position)
|
||||
{
|
||||
var result = TwistedWrapInt(new Vector2I(position.X, position.Z), size.X, size.Z);
|
||||
return new(result.local.X, position.Y, result.local.Y);
|
||||
}
|
||||
|
||||
private (Vector2I local, bool flipped) TwistedWrapInt(Vector2I global, int W, int H)
|
||||
{
|
||||
int gx = global.X;
|
||||
int gy = global.Y;
|
||||
|
||||
int yWrap = FloorDiv(gy, H);
|
||||
bool flip = (yWrap & 1) != 0;
|
||||
|
||||
int tileY = gy % H;
|
||||
if (tileY < 0) tileY += H;
|
||||
int ly = flip ? (H - 1 - tileY) : tileY;
|
||||
|
||||
int xOffset = flip ? W / 2 : 0;
|
||||
int wrappedX = gx + xOffset;
|
||||
|
||||
int tileX = wrappedX % W;
|
||||
if (tileX < 0) tileX += W;
|
||||
int lx = flip ? (W - 1 - tileX) : tileX;
|
||||
|
||||
return (new Vector2I(lx, ly), flip);
|
||||
}
|
||||
|
||||
private int FloorDiv(int a, int b)
|
||||
{
|
||||
return (a >= 0) ? a / b : ((a + 1) / b) - 1;
|
||||
}
|
||||
public bool IsInsideBounds(Vector3I position) => position.Y >= 0 && position.Y < size.Y;
|
||||
}
|
||||
1
src/voxelgame/Voxels/Chunks/IVoxelChunk.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/IVoxelChunk.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bn4dkifwcftqm
|
||||
316
src/voxelgame/Voxels/Chunks/Mesher/ChunkMesher.cs
Normal file
316
src/voxelgame/Voxels/Chunks/Mesher/ChunkMesher.cs
Normal file
@@ -0,0 +1,316 @@
|
||||
// https://github.com/MrAlvaroRamirez/Unity-SurfaceNet-VoxelTerrain/blob/main/Assets/Scripts/Data/ChunkMeshBuilder.cs
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
using SJK.Voxels;
|
||||
|
||||
public static class ChunkMeshBuilder
|
||||
{
|
||||
private static bool IsVoid<TVoxel>(Vector3I pos, IReadOnlyVoxelChunk<TVoxel> voxels,Func<TVoxel,bool> isVoid)
|
||||
{
|
||||
if (voxels.Bounds.IsInBounds(pos))
|
||||
return isVoid(voxels.GetVoxel(pos));
|
||||
return false;
|
||||
// if (voxels[pos.x + (pos.z * Constants.CHUNK_SIZE) + (pos.y * Constants.CHUNK_AREA)] == 0)
|
||||
// {
|
||||
// return true;
|
||||
// }
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ChunkMeshData BuildChunkMesh<TVoxel>(
|
||||
IReadOnlyVoxelChunk<TVoxel> voxels,
|
||||
// Vector3I chunkPos,
|
||||
Func<TVoxel,bool> isVoid,
|
||||
Func<TVoxel, Direction,(Color,int)> faceUvs
|
||||
|
||||
// IVoxelWorld<TVoxel,IVoxelChunk<TVoxel>> world
|
||||
)
|
||||
{
|
||||
ChunkMeshData meshData = new ChunkMeshData();
|
||||
meshData.ClearData();
|
||||
|
||||
int counter = 0;
|
||||
|
||||
// Iterate through all voxels in the chunk
|
||||
// We start from -1 in order to also check the quads on the edges of the chunk
|
||||
for (int x = voxels.Bounds.Min.X; x < voxels.Bounds.Max.X ; x++)
|
||||
{
|
||||
for (int y = voxels.Bounds.Min.Y; y < voxels.Bounds.Max.Y ; y++)
|
||||
{
|
||||
for (int z = voxels.Bounds.Min.Z; z < voxels.Bounds.Max.Z ; z++)
|
||||
{
|
||||
Vector3I localPos = new Vector3I(x, y, z);
|
||||
|
||||
// TODO? Instead of passing all this data, we can make the method local
|
||||
CreateSurfaceQuads(localPos, voxels, meshData, ref counter, isVoid, faceUvs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return meshData;
|
||||
}
|
||||
|
||||
// This function will iterate through all the positive axis directions of a voxel
|
||||
// If it finds a voxel that is not the same as the current voxel, it will create a quad
|
||||
private static void CreateSurfaceQuads<TVoxel>(
|
||||
Vector3I localPos,
|
||||
IReadOnlyVoxelChunk<TVoxel> voxels,
|
||||
ChunkMeshData meshData,
|
||||
ref int counter,
|
||||
Func<TVoxel,bool> isVoid,
|
||||
Func<TVoxel, Direction,(Color,int)> faceUvs
|
||||
)
|
||||
{
|
||||
for (int i = 0; i < voxelAxis.Length; i++)
|
||||
{
|
||||
Vector3I axis = voxelAxis[i];
|
||||
Vector3I neighborPos = localPos + axis;
|
||||
|
||||
bool voxelExists = !IsVoid(localPos, voxels,isVoid);
|
||||
bool neighboorExists = !IsVoid(neighborPos, voxels,isVoid);
|
||||
|
||||
// We need to handle quad direction differently for each case
|
||||
if ((!voxelExists && neighboorExists) || (voxelExists && !neighboorExists))
|
||||
{
|
||||
Vector3[] quadVertices = AddQuad(localPos, i, voxels, isVoid);
|
||||
var color = faceUvs(voxels.GetVoxel(!voxelExists?localPos:neighborPos), (Direction)i);
|
||||
// Add vertices and triangles to the mesh data
|
||||
for (int j = 0; j < 6; j++)
|
||||
{
|
||||
// Split the quad into two triangles
|
||||
if (voxelExists)
|
||||
meshData.vertices.Add(quadVertices[voxelTriangles[j]]);
|
||||
else
|
||||
meshData.vertices.Add(quadVertices[voxelTrianglesReverse[j]]);
|
||||
|
||||
meshData.uvs.Add(voxelUVs[voxelTriangles[j]]);
|
||||
meshData.uvs2.Add(new Vector2(color.Item2,0));
|
||||
meshData.colors.Add(color.Item1);
|
||||
meshData.triangles.Add(counter++);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3[] AddQuad<TVoxel>(Vector3I localPos, int axis, IReadOnlyVoxelChunk<TVoxel> voxels,Func<TVoxel,bool> isVoid)
|
||||
{
|
||||
Vector3[] vertices = new Vector3[4];
|
||||
|
||||
for (int i = 0; i < voxelQuads[axis].Length; i++)
|
||||
{
|
||||
Vector3I quadVertexPos = localPos + voxelQuads[axis][i];
|
||||
vertices[i] = AverageVertex(quadVertexPos, voxels,isVoid);
|
||||
|
||||
// To disable smoothing, comment the line above and add this one:
|
||||
// vertices[i] = quadVertexPos;
|
||||
}
|
||||
|
||||
return vertices;
|
||||
}
|
||||
|
||||
// Naive surface net implementation
|
||||
private static Vector3 AverageVertex<TVoxel>(Vector3I pos, IReadOnlyVoxelChunk<TVoxel> voxels,Func<TVoxel,bool> isVoid)
|
||||
{
|
||||
Vector3 total = Vector3.Zero;
|
||||
int count = 0;
|
||||
|
||||
// Iterate over the edges of the current voxel
|
||||
// For each edge we will check if the voxel at the end of the edge is filled or not
|
||||
// We will calculate the average of all these values to estimate the position of the vertex
|
||||
// As a result, the final mesh will be smoothed
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
Vector3I edgePos = pos + edgeOffsets[i, 0];
|
||||
Vector3I edgeNeighborPos = pos + edgeOffsets[i, 1];
|
||||
|
||||
if (IsVoid(edgePos, voxels,isVoid) != IsVoid(edgeNeighborPos, voxels,isVoid))
|
||||
{
|
||||
total += (Vector3)(edgePos + edgeNeighborPos) / 2f;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return total / count;
|
||||
}
|
||||
|
||||
#region Voxel Statics
|
||||
// Constant for the positive axis directions
|
||||
static readonly Vector3I[] voxelAxis = new Vector3I[3]
|
||||
{
|
||||
new Vector3I(1, 0, 0),
|
||||
new Vector3I(0, 1, 0),
|
||||
new Vector3I(0, 0, 1),
|
||||
};
|
||||
|
||||
// Vertex index lookup table for forming quads
|
||||
static readonly Vector3I[][] voxelQuads = new Vector3I[3][]
|
||||
{
|
||||
new Vector3I[4]
|
||||
{
|
||||
new Vector3I(0, 0, -1),
|
||||
new Vector3I(0, -1, -1),
|
||||
new Vector3I(0, -1, 0),
|
||||
new Vector3I(0, 0, 0)
|
||||
},
|
||||
new Vector3I[4]
|
||||
{
|
||||
new Vector3I(0, 0, -1),
|
||||
new Vector3I(0, 0, 0),
|
||||
new Vector3I(-1, 0, 0),
|
||||
new Vector3I(-1, 0, -1)
|
||||
},
|
||||
new Vector3I[4]
|
||||
{
|
||||
new Vector3I(0, 0, 0),
|
||||
new Vector3I(0, -1, 0),
|
||||
new Vector3I(-1, -1, 0),
|
||||
new Vector3I(-1, 0, 0)
|
||||
},
|
||||
};
|
||||
|
||||
static readonly Vector3I[,] edgeOffsets = new Vector3I[12, 2]
|
||||
{
|
||||
// Edges on min Z axis
|
||||
{ new Vector3I(0, 0, 0), new Vector3I(1, 0, 0) },
|
||||
{ new Vector3I(1, 0, 0), new Vector3I(1, 1, 0) },
|
||||
{ new Vector3I(1, 1, 0), new Vector3I(0, 1, 0) },
|
||||
{ new Vector3I(0, 1, 0), new Vector3I(0, 0, 0) },
|
||||
// Edges on max Z axisCreateSurfaceQuads
|
||||
{ new Vector3I(0, 0, 1), new Vector3I(1, 0, 1) },
|
||||
{ new Vector3I(1, 0, 1), new Vector3I(1, 1, 1) },
|
||||
{ new Vector3I(1, 1, 1), new Vector3I(0, 1, 1) },
|
||||
{ new Vector3I(0, 1, 1), new Vector3I(0, 0, 1) },
|
||||
// Edges connecting min Z to max Z
|
||||
{ new Vector3I(0, 0, 0), new Vector3I(0, 0, 1) },
|
||||
{ new Vector3I(1, 0, 0), new Vector3I(1, 0, 1) },
|
||||
{ new Vector3I(1, 1, 0), new Vector3I(1, 1, 1) },
|
||||
{ new Vector3I(0, 1, 0), new Vector3I(0, 1, 1) }
|
||||
};
|
||||
|
||||
static readonly Vector2[] voxelUVs = new Vector2[4]
|
||||
{
|
||||
new Vector2(0, 0),
|
||||
new Vector2(1, 0),
|
||||
new Vector2(1, 1),
|
||||
new Vector2(1, 0)
|
||||
};
|
||||
|
||||
static readonly int[] voxelTriangles = new int[] { 0, 3, 2, 0, 2, 1 };
|
||||
static readonly int[] voxelTrianglesReverse = new int[] { 0, 2, 3, 0, 1, 2 };
|
||||
#endregion
|
||||
|
||||
public class ChunkMeshData
|
||||
{
|
||||
// public Mesh mesh;
|
||||
public List<Vector3> vertices;
|
||||
public List<int> triangles;
|
||||
public List<Color> colors;
|
||||
public List<Vector2> uvs;
|
||||
public List<Vector2> uvs2;
|
||||
|
||||
public bool initialized;
|
||||
|
||||
public void ClearData()
|
||||
{
|
||||
if (!initialized)
|
||||
{
|
||||
vertices = new List<Vector3>();
|
||||
triangles = new List<int>();
|
||||
uvs = new List<Vector2>();
|
||||
uvs2 = new List<Vector2>();
|
||||
colors = new List<Color>();
|
||||
|
||||
initialized = true;
|
||||
// mesh = new Mesh();
|
||||
}
|
||||
else
|
||||
{
|
||||
vertices.Clear();
|
||||
triangles.Clear();
|
||||
uvs.Clear();
|
||||
uvs2.Clear();
|
||||
colors.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public Mesh UploadMesh(bool sharedVertices = false)
|
||||
{
|
||||
if (vertices.Count == 0)
|
||||
{
|
||||
return new BoxMesh();
|
||||
}
|
||||
Godot.Collections.Array surfaceArray = [];
|
||||
surfaceArray.Resize((int)Mesh.ArrayType.Max);
|
||||
surfaceArray[(int)Mesh.ArrayType.Vertex] = vertices.ToArray();
|
||||
surfaceArray[(int)Mesh.ArrayType.TexUV] = uvs.ToArray();
|
||||
surfaceArray[(int)Mesh.ArrayType.TexUV2] = uvs2.ToArray();
|
||||
surfaceArray[(int)Mesh.ArrayType.Color] = colors.ToArray();
|
||||
surfaceArray[(int)Mesh.ArrayType.Normal] = new Vector3[vertices.Count];
|
||||
surfaceArray[(int)Mesh.ArrayType.Index] = triangles.ToArray();
|
||||
var arrMesh = new ArrayMesh();
|
||||
// No blendshapes, lods, or compression used.
|
||||
arrMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surfaceArray);
|
||||
MeshDataTool meshDataTool = new MeshDataTool();
|
||||
meshDataTool.CreateFromSurface(arrMesh, 0);
|
||||
for (int i = 0; i < meshDataTool.GetFaceCount(); i++)
|
||||
{
|
||||
// # Get the index in the vertex array.
|
||||
var a = meshDataTool.GetFaceVertex(i, 0);
|
||||
var b = meshDataTool.GetFaceVertex(i, 1);
|
||||
var c = meshDataTool.GetFaceVertex(i, 2);
|
||||
// # Get vertex position using vertex index.
|
||||
var ap = meshDataTool.GetVertex(a);
|
||||
var bp = meshDataTool.GetVertex(b);
|
||||
var cp = meshDataTool.GetVertex(c);
|
||||
// # Calculate face normal.
|
||||
Vector3 nn = (bp - cp).Cross(ap - bp).Normalized();
|
||||
// # Add face normal to current vertex normal.
|
||||
// # This will not result in perfect normals, but it will be close.
|
||||
meshDataTool.SetVertexNormal(a, nn + meshDataTool.GetVertexNormal(a));
|
||||
meshDataTool.SetVertexNormal(b, nn + meshDataTool.GetVertexNormal(b));
|
||||
meshDataTool.SetVertexNormal(c, nn + meshDataTool.GetVertexNormal(c));
|
||||
|
||||
}
|
||||
for (int i = 0; i < meshDataTool.GetVertexCount(); i++)
|
||||
{
|
||||
var v = meshDataTool.GetVertexNormal(i).Normalized();
|
||||
meshDataTool.SetVertexNormal(i, v);
|
||||
}
|
||||
arrMesh.ClearSurfaces();
|
||||
meshDataTool.CommitToSurface(arrMesh);
|
||||
return arrMesh;
|
||||
// if (sharedVertices)
|
||||
// {
|
||||
// Dictionary<Vector3, int> vertexIndices =
|
||||
// new Dictionary<Vector3, int>();
|
||||
// List<int> sharedTriangles = new List<int>();
|
||||
|
||||
// for (int i = 0; i < vertices.Count; i++)
|
||||
// {
|
||||
// var key = vertices[i];
|
||||
// if (!vertexIndices.ContainsKey(key))
|
||||
// {
|
||||
// vertexIndices.Add(key, vertexIndices.Count);
|
||||
// }
|
||||
// sharedTriangles.Add(vertexIndices[key]);
|
||||
// }
|
||||
|
||||
// mesh.SetVertices(vertexIndices.Keys.ToList());
|
||||
// mesh.SetTriangles(sharedTriangles, 0, false);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// mesh.SetVertices(vertices);
|
||||
// mesh.SetTriangles(triangles, 0, false);
|
||||
// mesh.SetUVs(0, uvs);
|
||||
// mesh.SetColors(colors);
|
||||
// }
|
||||
// mesh.Optimize();
|
||||
// mesh.RecalculateNormals();
|
||||
// mesh.RecalculateBounds();voxelRegister.GetVoxel(voxel).GetAttribute(()=> new TextureCube(Colors.White)).Color
|
||||
// mesh.UploadMeshData(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/Voxels/Chunks/Mesher/ChunkMesher.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/Mesher/ChunkMesher.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bd586qflhhi13
|
||||
1071
src/voxelgame/Voxels/Chunks/Mesher/GreedyMesher.cs
Normal file
1071
src/voxelgame/Voxels/Chunks/Mesher/GreedyMesher.cs
Normal file
File diff suppressed because it is too large
Load Diff
1
src/voxelgame/Voxels/Chunks/Mesher/GreedyMesher.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/Mesher/GreedyMesher.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b13215rwdt03o
|
||||
1
src/voxelgame/Voxels/Chunks/OctTree.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/OctTree.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dh21dhxsoadt4
|
||||
224
src/voxelgame/Voxels/Chunks/Octree.cs
Normal file
224
src/voxelgame/Voxels/Chunks/Octree.cs
Normal file
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Godot;
|
||||
|
||||
namespace SJK.Voxels;
|
||||
#nullable enable
|
||||
public class NonePowerTwoOctree<TVoxel> : IVoxelChunk<TVoxel>
|
||||
{
|
||||
public Vector3I Size { get; }
|
||||
|
||||
public VoxelBounds Bounds => new VoxelBounds(Vector3I.Zero,Size);
|
||||
|
||||
private OctreeNode<TVoxel> root;
|
||||
public NonePowerTwoOctree(Vector3I size, TVoxel @default)
|
||||
{
|
||||
Size = size;
|
||||
var extent = SJK.Math.SJKMath.UpperPowerOfTwo(SJK.Math.SJKMath.Max(size.ToArrayXYZ()));
|
||||
|
||||
root = new OctreeNode<TVoxel>(Vector3I.Zero, extent, 1, @default);
|
||||
|
||||
}
|
||||
public VoxelChunkEnumerator<TVoxel> GetEnumerator()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos position) => root.GetVoxel(position);
|
||||
|
||||
|
||||
public bool IsInBounds(VoxelPos position) =>
|
||||
position.X >= 0 && position.X < Size.X
|
||||
&& position.Y >= 0 && position.Y < Size.Y
|
||||
&& position.Z >= 0 && position.Z < Size.Z;
|
||||
|
||||
public void SetVoxel(VoxelPos position, TVoxel voxel) => root.SetVoxel(position, voxel);
|
||||
public void SetVoxel(VoxelPos position, object voxel)
|
||||
{
|
||||
if (voxel is TVoxel voxel1)
|
||||
root.SetVoxel(position,voxel1);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class Octree<TVoxel,TChunk> : IVoxelChunk<TVoxel,TChunk>, IReadOnlyVoxelChunk<TVoxel,TChunk> where TChunk : IChunkSize<TChunk>
|
||||
{
|
||||
public Vector3I Size => new Vector3I(TChunk.Size, TChunk.Size, TChunk.Size);
|
||||
|
||||
public VoxelBounds Bounds => new(Vector3I.Zero,Size);
|
||||
|
||||
private OctreeNode<TVoxel> root;
|
||||
public Octree(TVoxel @default)
|
||||
{
|
||||
root = new OctreeNode<TVoxel>(Vector3I.Zero, TChunk.Size, 1, @default);
|
||||
|
||||
}
|
||||
public VoxelChunkEnumerator<TVoxel> GetEnumerator() => new VoxelChunkEnumerator<TVoxel>(this);
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos position) => root.GetVoxel(position);
|
||||
|
||||
|
||||
public bool IsInBounds(VoxelPos position) =>
|
||||
position.X >= 0 && position.X < Size.X
|
||||
&& position.Y >= 0 && position.Y < Size.Y
|
||||
&& position.Z >= 0 && position.Z < Size.Z;
|
||||
|
||||
public void SetVoxel(VoxelPos position, TVoxel voxel) => root.SetVoxel(position, voxel);
|
||||
public OctreeNode<TVoxel> GetRoot() => root;
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos<TChunk> position)=> GetVoxel(new VoxelPos(position.X,position.Y,position.Z));
|
||||
|
||||
public void SetVoxel(VoxelPos<TChunk> position, TVoxel voxel)=> SetVoxel(new VoxelPos(position.X,position.Y,position.Z),voxel);
|
||||
VoxelChunkEnumerator<TVoxel, TChunk> IReadOnlyVoxelChunk<TVoxel, TChunk>.GetEnumerator()
|
||||
{
|
||||
return new VoxelChunkEnumerator<TVoxel, TChunk>(this);
|
||||
}
|
||||
}
|
||||
|
||||
public struct OctreeNode<TVoxel>
|
||||
{
|
||||
public TVoxel? Value;
|
||||
public OctreeNode<TVoxel>[]? ChildNodes = null;
|
||||
|
||||
private readonly Vector3I origin;
|
||||
private readonly int size;
|
||||
private readonly int minResolution;
|
||||
public OctreeNode(Vector3I origin, int size, int minResolution, TVoxel @default)
|
||||
{
|
||||
this.origin = origin;
|
||||
this.size = size;
|
||||
this.minResolution = minResolution;
|
||||
Value = @default;
|
||||
|
||||
}
|
||||
[MemberNotNullWhen(true, nameof(Value))]
|
||||
[MemberNotNullWhen(false, nameof(ChildNodes))]
|
||||
public bool IsLeaf()
|
||||
{
|
||||
return ChildNodes is null;
|
||||
}
|
||||
public TVoxel GetVoxel(VoxelPos pos)
|
||||
{
|
||||
if (IsLeaf())
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
int half = size / 2;
|
||||
int idx = ChildIndex(pos, half);
|
||||
return ChildNodes[idx].GetVoxel(pos);
|
||||
|
||||
}
|
||||
public void SetVoxel(VoxelPos pos, TVoxel voxel)
|
||||
{
|
||||
if (size <= minResolution)
|
||||
{
|
||||
ChildNodes = null;
|
||||
Value = voxel;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ChildNodes is null)
|
||||
Subdivide();
|
||||
|
||||
int half = size / 2;
|
||||
int idx = ChildIndex(pos, half);
|
||||
ChildNodes[idx].SetVoxel(pos, voxel);
|
||||
|
||||
// Check if all children are uniform → collapse
|
||||
TVoxel first = ChildNodes[0].GetVoxel(origin);
|
||||
bool uniform = true;
|
||||
for (int i = 1; i < 8; i++)
|
||||
{
|
||||
if (!EqualityComparer<TVoxel>.Default.Equals(first, ChildNodes[i].GetVoxel(origin)))
|
||||
{
|
||||
uniform = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (uniform)
|
||||
{
|
||||
Value = first;
|
||||
ChildNodes = null;
|
||||
}
|
||||
}
|
||||
public void Trim()
|
||||
{
|
||||
if (ChildNodes == null) return;
|
||||
|
||||
foreach (var child in ChildNodes)
|
||||
child.Trim();
|
||||
|
||||
// Try collapse again
|
||||
TVoxel first = ChildNodes[0].Value!;
|
||||
bool uniform = true;
|
||||
for (int i = 1; i < 8; i++)
|
||||
{
|
||||
if (!EqualityComparer<TVoxel>.Default.Equals(first, ChildNodes[i].Value))
|
||||
{
|
||||
uniform = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (uniform)
|
||||
{
|
||||
Value = first;
|
||||
ChildNodes = null;
|
||||
}
|
||||
|
||||
}
|
||||
[MemberNotNull(nameof(ChildNodes))]
|
||||
private void Subdivide()
|
||||
{
|
||||
ChildNodes = new OctreeNode<TVoxel>[8];
|
||||
int half = size / 2;
|
||||
|
||||
for (int dx = 0; dx < 2; dx++)
|
||||
{
|
||||
for (int dy = 0; dy < 2; dy++)
|
||||
{
|
||||
for (int dz = 0; dz < 2; dz++)
|
||||
{
|
||||
int idx = (dx << 2) | (dy << 1) | dz;
|
||||
Vector3I childOrigin = origin + new Vector3I(dx * half, dy * half, dz * half);
|
||||
ChildNodes[idx] = new OctreeNode<TVoxel>(childOrigin, half, minResolution, Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Value = default;
|
||||
}
|
||||
public int ChildIndex(VoxelPos pos, int half)
|
||||
{
|
||||
int dx = (pos.X - origin.X) >= half ? 1 : 0;
|
||||
int dy = (pos.Y - origin.Y) >= half ? 1 : 0;
|
||||
int dz = (pos.Z - origin.Z) >= half ? 1 : 0;
|
||||
return (dx << 2) | (dy << 1) | dz;
|
||||
}
|
||||
public void TraverseBounds(Vector3 origin, Action<Vector3, int,bool> visitor)
|
||||
{
|
||||
visitor(origin, size,IsLeaf());
|
||||
|
||||
if (!IsLeaf())
|
||||
{
|
||||
int half = size / 2;
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
var child = ChildNodes[i];
|
||||
|
||||
Vector3 childOrigin = origin + new Vector3(
|
||||
(i & 1) != 0 ? half : 0,
|
||||
(i & 2) != 0 ? half : 0,
|
||||
(i & 4) != 0 ? half : 0);
|
||||
|
||||
child.TraverseBounds(childOrigin, visitor);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/Voxels/Chunks/Octree.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/Octree.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ctjgia5dlt8xf
|
||||
58
src/voxelgame/Voxels/Chunks/SparseVoxelChunk.cs
Normal file
58
src/voxelgame/Voxels/Chunks/SparseVoxelChunk.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SJK.Voxels;
|
||||
|
||||
public sealed class SparseVoxelChunk<TVoxel, TChunk> : IVoxelChunk<TVoxel,TChunk> where TChunk : IChunkSize<TChunk> where TVoxel : IEquatable<TVoxel>
|
||||
{
|
||||
// public Vector3I Size => new Vector3I(TChunk.Size, TChunk.Size, TChunk.Size);
|
||||
private Dictionary<VoxelPos<TChunk>, TVoxel> _data;
|
||||
private readonly TVoxel _default;
|
||||
|
||||
public VoxelBounds Bounds => new(Vector3I.Zero,new(TChunk.Size,TChunk.Size,TChunk.Size));
|
||||
|
||||
public SparseVoxelChunk(TVoxel @defualt = default)
|
||||
{
|
||||
_data = new Dictionary<VoxelPos<TChunk>, TVoxel>();
|
||||
this._default = defualt;
|
||||
}
|
||||
public void SetVoxel(VoxelPos position, TVoxel voxel) => SetVoxel(new VoxelPos<TChunk>(position.X, position.Y, position.Z), voxel);
|
||||
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos position) => GetVoxel(new VoxelPos<TChunk>(position.X,position.Y,position.Z));
|
||||
|
||||
public VoxelChunkEnumerator<TVoxel> GetEnumerator() => new VoxelChunkEnumerator<TVoxel>(this);
|
||||
|
||||
|
||||
|
||||
public bool IsInBounds(VoxelPos position) =>
|
||||
position.X >= 0 && position.X < TChunk.Size
|
||||
&& position.Y >= 0 && position.Y < TChunk.Size
|
||||
&& position.Z >= 0 && position.Z < TChunk.Size;
|
||||
|
||||
public void SetVoxel(VoxelPos<TChunk> position, TVoxel voxel)
|
||||
{
|
||||
// Debug.Assert(IsInBounds(position), $"{nameof(position)} is not in bounds with value of {position}, Size {TChunk.Size}");
|
||||
if (voxel.Equals(_default))
|
||||
{
|
||||
_data.Remove(position);
|
||||
return;
|
||||
}
|
||||
_data[position] = voxel;
|
||||
}
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos<TChunk> position)
|
||||
{
|
||||
if (_data.TryGetValue(position, out var voxel))
|
||||
{
|
||||
return voxel;
|
||||
}
|
||||
return _default;
|
||||
}
|
||||
VoxelChunkEnumerator<TVoxel, TChunk> IReadOnlyVoxelChunk<TVoxel, TChunk>.GetEnumerator()
|
||||
{
|
||||
return new VoxelChunkEnumerator<TVoxel, TChunk>(this);
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/Voxels/Chunks/SparseVoxelChunk.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/SparseVoxelChunk.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://btrlde2p7lphe
|
||||
384
src/voxelgame/Voxels/Chunks/SurfaceNet.cs
Normal file
384
src/voxelgame/Voxels/Chunks/SurfaceNet.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
|
||||
public static class SurfaceNetMesher
|
||||
{
|
||||
|
||||
/*
|
||||
* Give each voxel a position, an index, a corner mask, and an edge mask.
|
||||
* isOnSurface basically asks if our mesh goes through this voxel
|
||||
*/
|
||||
private struct Voxel
|
||||
{
|
||||
public int vertexIndex;
|
||||
public Vector3 vertexPosition;
|
||||
public bool isOnSurface;
|
||||
public int voxelEdgeMask;
|
||||
public int cornerMask;
|
||||
}
|
||||
|
||||
private struct Sample
|
||||
{
|
||||
public Vector3 point;
|
||||
public float value;
|
||||
}
|
||||
#region SurfaceNet Tables
|
||||
|
||||
// Define the edge table and cube edges with a set size
|
||||
static private int[] edgeTable = new int[256];
|
||||
static private int[] cubeEdges = new int[24];
|
||||
static SurfaceNetMesher()
|
||||
{
|
||||
GenerateCubeEdgesTable();
|
||||
GenerateIntersectionTable();
|
||||
}
|
||||
/*
|
||||
* Set up the 8 corners of each voxel like this:
|
||||
* Very important that this is set up the same way as the cube edges
|
||||
*
|
||||
* y z
|
||||
* ^ /
|
||||
* |
|
||||
* 6----7
|
||||
* /| /|
|
||||
* 4----5 |
|
||||
* | 2--|-3
|
||||
* |/ |/
|
||||
* 0----1 --> x
|
||||
*
|
||||
*/
|
||||
|
||||
private static Vector3[] voxelCornerOffsets = new Vector3[8] {
|
||||
new Vector3(0,0,0), // 0
|
||||
new Vector3(1,0,0), // 1
|
||||
new Vector3(0,1,0), // 2
|
||||
new Vector3(1,1,0), // 3
|
||||
new Vector3(0,0,1), // 4
|
||||
new Vector3(1,0,1), // 5
|
||||
new Vector3(0,1,1), // 6
|
||||
new Vector3(1,1,1) // 7
|
||||
};
|
||||
|
||||
/*
|
||||
* Build an intersection table. This is a 2^(cube config) -> 2^(edge config) map
|
||||
* There is only one entry for each possible cube configuration
|
||||
* and the output is a 12-bit vector enumerating all edges
|
||||
* crossing the 0-level
|
||||
*/
|
||||
|
||||
private static void GenerateIntersectionTable()
|
||||
{
|
||||
|
||||
for (int i = 0; i < 256; ++i)
|
||||
{
|
||||
int em = 0;
|
||||
for (int j = 0; j < 24; j += 2)
|
||||
{
|
||||
var a = Convert.ToBoolean(i & (1 << cubeEdges[j]));
|
||||
var b = Convert.ToBoolean(i & (1 << cubeEdges[j + 1]));
|
||||
em |= a != b ? (1 << (j >> 1)) : 0;
|
||||
}
|
||||
edgeTable[i] = em;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Utility function to build a table of possible edges for a cube with each
|
||||
* pair of points representing one edge i.e. [0,1,0,2,0,4,...] would be the
|
||||
* edges from points 0 to 1, 0 to 2, and 0 to 4 respectively:
|
||||
*
|
||||
* y z
|
||||
* ^ /
|
||||
* |
|
||||
* 6----7
|
||||
* /| /|
|
||||
* 4----5 |
|
||||
* | 2--|-3
|
||||
* |/ |/
|
||||
* 0----1 --> x
|
||||
*
|
||||
*/
|
||||
|
||||
private static void GenerateCubeEdgesTable()
|
||||
{
|
||||
int k = 0;
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
for (int j = 1; j <= 4; j <<= 1)
|
||||
{
|
||||
int p = i ^ j;
|
||||
if (i <= p)
|
||||
{
|
||||
cubeEdges[k++] = i;
|
||||
cubeEdges[k++] = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static Mesh CreateMesh(Vector3I size, Func<Vector3I,float> sampler,Func<Vector3I,bool> isCube)
|
||||
{
|
||||
var vertices = new List<Vector3>();
|
||||
var triangles = new List<int>();
|
||||
|
||||
CalculateVertexPositions(size,vertices, out var voxels,sampler,isCube);
|
||||
// set the size of our buffer
|
||||
// var buffer = new int[4096];
|
||||
|
||||
// get the width, height, and depth of the sample space for our nested for loops
|
||||
int width = size.X ;
|
||||
int height = size.Y ;
|
||||
int depth = size.Z ;
|
||||
|
||||
|
||||
Vector3I pos = new ();
|
||||
float[] grid = new float[8];
|
||||
|
||||
// resize the buffer if it's not big enough
|
||||
// if (R[2] * 2 > buffer.Length)
|
||||
// buffer = new int[R[2] * 2];
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int z = 0; z < depth; z++)
|
||||
{
|
||||
pos = new(x, y, z);
|
||||
// get the corner mask we calculated earlier
|
||||
var mask = voxels[pos.X, pos.Y, pos.Z].cornerMask;
|
||||
// Early Termination Check
|
||||
if (mask == 0 || mask == 0xff)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// get edge mask
|
||||
var edgeMask = edgeTable[mask];
|
||||
|
||||
var vertex = new Vector3();
|
||||
var edgeIndex = 0;
|
||||
//For Every Cube Edge
|
||||
for (var i = 0; i < 12; i++)
|
||||
{
|
||||
//Use Edge Mask to Check if Crossed
|
||||
if (!Convert.ToBoolean(edgeMask & (1 << i)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//If So, Increment Edge Crossing #
|
||||
edgeIndex++;
|
||||
|
||||
//Find Intersection Point
|
||||
var e0 = cubeEdges[i << 1];
|
||||
var e1 = cubeEdges[(i << 1) + 1];
|
||||
var g0 = grid[e0];
|
||||
var g1 = grid[e1];
|
||||
var t = g0 - g1;
|
||||
if (Math.Abs(t) > 1e-16)
|
||||
t = g0 / t;
|
||||
else
|
||||
continue;
|
||||
|
||||
//Interpolate Vertices, Add Intersections
|
||||
for (int j = 0, k = 1; j < 3; j++, k <<= 1)
|
||||
{
|
||||
var a = e0 & k;
|
||||
var b = e1 & k;
|
||||
if (a != b)
|
||||
vertex[j] += Convert.ToBoolean(a) ? 1f - t : t;
|
||||
else
|
||||
vertex[j] += Convert.ToBoolean(a) ? 1f : 0;
|
||||
}
|
||||
}
|
||||
//Average Edge Intersections, Add to Coordinate
|
||||
|
||||
var s = 1f / edgeIndex;
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
vertex[i] = pos[i] + s * vertex[i];
|
||||
}
|
||||
vertex = voxels[pos[0], pos[1], pos[2]].vertexPosition;
|
||||
|
||||
//Add Faces (Loop Over 3 Base Components)
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
//First 3 Entries Indicate Crossings on Edge
|
||||
if (!Convert.ToBoolean(edgeMask & (1 << i)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//i - Axes, iu, iv - Ortho Axes
|
||||
var iu = (i + 1) % 3;
|
||||
var iv = (i + 2) % 3;
|
||||
|
||||
//Skip if on Boundary
|
||||
if (pos[iu] == 0 || pos[iv] == 0)
|
||||
continue;
|
||||
|
||||
var a = GetVoxelVertexIndex(pos, i,voxels);
|
||||
var b = GetVoxelVertexIndex(Offset(pos, iu, -1), i,voxels);
|
||||
var c = GetVoxelVertexIndex(Offset(pos, iv, -1), i,voxels);
|
||||
var d = GetVoxelVertexIndex(Offset(Offset(pos, iu, -1), iv, -1), i,voxels);
|
||||
|
||||
if (a < 0 || b < 0 || c < 0 || d < 0)
|
||||
continue;
|
||||
|
||||
if ((mask & 1) != 0)
|
||||
{
|
||||
triangles.Add(a); triangles.Add(d); triangles.Add(b);
|
||||
triangles.Add(a); triangles.Add(c); triangles.Add(d);
|
||||
}
|
||||
else
|
||||
{
|
||||
triangles.Add(a); triangles.Add(b); triangles.Add(d);
|
||||
triangles.Add(a); triangles.Add(d); triangles.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vertices.Count == 0)
|
||||
{
|
||||
return new BoxMesh();
|
||||
}
|
||||
Godot.Collections.Array surfaceArray = [];
|
||||
surfaceArray.Resize((int)Mesh.ArrayType.Max);
|
||||
surfaceArray[(int)Mesh.ArrayType.Vertex] = vertices.ToArray();
|
||||
// surfaceArray[(int)Mesh.ArrayType.TexUV] = uvs.ToArray();
|
||||
// surfaceArray[(int)Mesh.ArrayType.Normal] = new Vector3[vertices.Count];
|
||||
surfaceArray[(int)Mesh.ArrayType.Index] = triangles.ToArray();
|
||||
var arrMesh = new ArrayMesh();
|
||||
// No blendshapes, lods, or compression used.
|
||||
arrMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surfaceArray);
|
||||
MeshDataTool meshDataTool = new MeshDataTool();
|
||||
meshDataTool.CreateFromSurface(arrMesh, 0);
|
||||
for (int i = 0; i < meshDataTool.GetFaceCount(); i++)
|
||||
{
|
||||
// # Get the index in the vertex array.
|
||||
var a = meshDataTool.GetFaceVertex(i, 0);
|
||||
var b = meshDataTool.GetFaceVertex(i, 1);
|
||||
var c = meshDataTool.GetFaceVertex(i, 2);
|
||||
// # Get vertex position using vertex index.
|
||||
var ap = meshDataTool.GetVertex(a);
|
||||
var bp = meshDataTool.GetVertex(b);
|
||||
var cp = meshDataTool.GetVertex(c);
|
||||
// # Calculate face normal.
|
||||
Vector3 nn = (bp - cp).Cross(ap - bp).Normalized();
|
||||
// # Add face normal to current vertex normal.
|
||||
// # This will not result in perfect normals, but it will be close.
|
||||
meshDataTool.SetVertexNormal(a, nn + meshDataTool.GetVertexNormal(a));
|
||||
meshDataTool.SetVertexNormal(b, nn + meshDataTool.GetVertexNormal(b));
|
||||
meshDataTool.SetVertexNormal(c, nn + meshDataTool.GetVertexNormal(c));
|
||||
|
||||
}
|
||||
for (int i = 0; i < meshDataTool.GetVertexCount(); i++)
|
||||
{
|
||||
var v = meshDataTool.GetVertexNormal(i).Normalized();
|
||||
meshDataTool.SetVertexNormal(i,v);
|
||||
}
|
||||
arrMesh.ClearSurfaces();
|
||||
meshDataTool.CommitToSurface(arrMesh);
|
||||
return arrMesh;
|
||||
}
|
||||
private static int GetVoxelVertexIndex(Vector3I pos, int axis, Voxel[,,] voxels)
|
||||
{
|
||||
if (pos[0] < 0 || pos[1] < 0 || pos[2] < 0) return -1;
|
||||
if (pos[0] >= voxels.GetLength(0) || pos[1] >= voxels.GetLength(1) || pos[2] >= voxels.GetLength(2)) return -1;
|
||||
return voxels[pos[0], pos[1], pos[2]].vertexIndex;
|
||||
}
|
||||
|
||||
private static Vector3I Offset(Vector3I pos, int axis, int delta)
|
||||
{
|
||||
Vector3I p = pos;
|
||||
p[axis] += delta;
|
||||
return p ;
|
||||
}
|
||||
|
||||
private const float threshold = 0;
|
||||
/**
|
||||
* calculate the postion of each vertex. this sets up our 3 dimensional grid of voxels
|
||||
* while also sampling each voxel
|
||||
**/
|
||||
|
||||
static void CalculateVertexPositions(Vector3I size,List<Vector3> vertices, out Voxel[,,] voxels,Func<Vector3I,float> sampler,Func<Vector3I,bool> isCube)
|
||||
{
|
||||
voxels = new Voxel[size.X, size.Y, size.Z];
|
||||
for (int x = 0; x < size.X - 1; x++)
|
||||
{
|
||||
for (int y = 0; y < size.Y - 1; y++)
|
||||
{
|
||||
for (int z = 0; z < size.Z - 1; z++)
|
||||
{
|
||||
|
||||
// default values.
|
||||
voxels[x, y, z].isOnSurface = false;
|
||||
voxels[x, y, z].voxelEdgeMask = 0;
|
||||
voxels[x, y, z].vertexIndex = -1;
|
||||
voxels[x, y, z].cornerMask = 0;
|
||||
|
||||
int cornerMask = 0;
|
||||
|
||||
// sample the 8 corners for the voxel and create a corner mask
|
||||
Sample[] samples = new Sample[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
var offset = voxelCornerOffsets[i];
|
||||
var pos = new Vector3I(x + (int)offset.X, y + (int)offset.Y, z + (int)offset.Z);
|
||||
var sample = sampler(new(pos.X, pos.Y, pos.Z));
|
||||
samples[i].value = sample;
|
||||
samples[i].point = pos;
|
||||
cornerMask |= ((sample > threshold) ? (1 << i) : 0);
|
||||
}
|
||||
//Check for early termination if cell does not intersect boundary
|
||||
if (cornerMask == 0 || cornerMask == 0xff)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// get edgemask from table using our corner mask
|
||||
int edgeMask = edgeTable[cornerMask];
|
||||
int edgeCrossings = 0;
|
||||
var vertPos = Vector3.Zero;
|
||||
|
||||
for (int i = 0; i < 12; ++i)
|
||||
{
|
||||
|
||||
//Use edge mask to check if it is crossed
|
||||
if (!((edgeMask & (1 << i)) > 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//If it did, increment number of edge crossings
|
||||
++edgeCrossings;
|
||||
|
||||
//Now find the point of intersection
|
||||
int e0 = cubeEdges[i << 1];
|
||||
int e1 = cubeEdges[(i << 1) + 1];
|
||||
float g0 = samples[e0].value;
|
||||
float g1 = samples[e1].value;
|
||||
float t = (threshold - g0) / (g1 - g0);
|
||||
vertPos += SJKMath.Lerp(samples[e0].point, samples[e1].point, t);
|
||||
|
||||
}
|
||||
vertPos /= edgeCrossings;
|
||||
vertPos = isCube(new Vector3I(x, y, z)) ? new Vector3(x, y, z) : vertPos;
|
||||
voxels[x, y, z].vertexPosition = vertPos;
|
||||
voxels[x, y, z].isOnSurface = true;
|
||||
voxels[x, y, z].voxelEdgeMask = edgeMask;
|
||||
voxels[x, y, z].vertexIndex = vertices.Count;
|
||||
voxels[x, y, z].cornerMask = cornerMask;
|
||||
vertices.Add(vertPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
1
src/voxelgame/Voxels/Chunks/SurfaceNet.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/SurfaceNet.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cmmco4k75cl77
|
||||
409
src/voxelgame/Voxels/Chunks/SurfaceNetMesher.cs
Normal file
409
src/voxelgame/Voxels/Chunks/SurfaceNetMesher.cs
Normal file
@@ -0,0 +1,409 @@
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
using System;
|
||||
|
||||
namespace SJK.Voxels;
|
||||
|
||||
public class SurfaceNetMesher<TVoxel>
|
||||
{
|
||||
private readonly IReadOnlyVoxelChunk<TVoxel> voxelChunk;
|
||||
private readonly Func<Vector3I,TVoxel, (bool isCube,float sdf)> sampler;
|
||||
private readonly float threshold;
|
||||
private readonly EdgeMode edgeMode;
|
||||
/*
|
||||
* Give each voxel a position, an index, a corner mask, and an edge mask.
|
||||
* isOnSurface basically asks if our mesh goes through this voxel
|
||||
*/
|
||||
private struct Voxel
|
||||
{
|
||||
public int vertexIndex;
|
||||
public Vector3 vertexPosition;
|
||||
public bool isOnSurface;
|
||||
public int voxelEdgeMask;
|
||||
public int cornerMask;
|
||||
public bool isCube;
|
||||
}
|
||||
|
||||
private struct Sample
|
||||
{
|
||||
public Vector3 point;
|
||||
public float value;
|
||||
}
|
||||
private Voxel[,,] voxels;
|
||||
private List<Vector3> vertices;
|
||||
private List<Vector3> newVertices;
|
||||
private List<int> triangles;
|
||||
#region SurfaceNet Tables
|
||||
|
||||
// Define the edge table and cube edges with a set size
|
||||
static private int[] edgeTable = new int[256];
|
||||
static private int[] cubeEdges = new int[24];
|
||||
static SurfaceNetMesher()
|
||||
{
|
||||
GenerateCubeEdgesTable();
|
||||
GenerateIntersectionTable();
|
||||
}
|
||||
/*
|
||||
* Set up the 8 corners of each voxel like this:
|
||||
* Very important that this is set up the same way as the cube edges
|
||||
*
|
||||
* y z
|
||||
* ^ /
|
||||
* |
|
||||
* 6----7
|
||||
* /| /|
|
||||
* 4----5 |
|
||||
* | 2--|-3
|
||||
* |/ |/
|
||||
* 0----1 --> x
|
||||
*
|
||||
*/
|
||||
|
||||
private Vector3[] voxelCornerOffsets = new Vector3[8] {
|
||||
new Vector3(0,0,0), // 0
|
||||
new Vector3(1,0,0), // 1
|
||||
new Vector3(0,1,0), // 2
|
||||
new Vector3(1,1,0), // 3
|
||||
new Vector3(0,0,1), // 4
|
||||
new Vector3(1,0,1), // 5
|
||||
new Vector3(0,1,1), // 6
|
||||
new Vector3(1,1,1) // 7
|
||||
};
|
||||
|
||||
/*
|
||||
* Build an intersection table. This is a 2^(cube config) -> 2^(edge config) map
|
||||
* There is only one entry for each possible cube configuration
|
||||
* and the output is a 12-bit vector enumerating all edges
|
||||
* crossing the 0-level
|
||||
*/
|
||||
|
||||
private static void GenerateIntersectionTable()
|
||||
{
|
||||
|
||||
for (int i = 0; i < 256; ++i)
|
||||
{
|
||||
int em = 0;
|
||||
for (int j = 0; j < 24; j += 2)
|
||||
{
|
||||
var a = Convert.ToBoolean(i & (1 << cubeEdges[j]));
|
||||
var b = Convert.ToBoolean(i & (1 << cubeEdges[j + 1]));
|
||||
em |= a != b ? (1 << (j >> 1)) : 0;
|
||||
}
|
||||
edgeTable[i] = em;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Utility function to build a table of possible edges for a cube with each
|
||||
* pair of points representing one edge i.e. [0,1,0,2,0,4,...] would be the
|
||||
* edges from points 0 to 1, 0 to 2, and 0 to 4 respectively:
|
||||
*
|
||||
* y z
|
||||
* ^ /
|
||||
* |
|
||||
* 6----7
|
||||
* /| /|
|
||||
* 4----5 |
|
||||
* | 2--|-3
|
||||
* |/ |/
|
||||
* 0----1 --> x
|
||||
*
|
||||
*/
|
||||
|
||||
private static void GenerateCubeEdgesTable()
|
||||
{
|
||||
int k = 0;
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
for (int j = 1; j <= 4; j <<= 1)
|
||||
{
|
||||
int p = i ^ j;
|
||||
if (i <= p)
|
||||
{
|
||||
cubeEdges[k++] = i;
|
||||
cubeEdges[k++] = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public SurfaceNetMesher(IReadOnlyVoxelChunk<TVoxel> voxelChunk, Func<Vector3I,TVoxel, (bool,float)> sampler, float threshold, EdgeMode edgeMode)
|
||||
{
|
||||
this.voxelChunk = voxelChunk;
|
||||
this.sampler = sampler;
|
||||
this.threshold = threshold;
|
||||
this.edgeMode = edgeMode;
|
||||
}
|
||||
public Mesh CreateMesh()
|
||||
{
|
||||
vertices = new List<Vector3>();
|
||||
triangles = new List<int>();
|
||||
newVertices = new List<Vector3>();
|
||||
CalculateVertexPositions();
|
||||
// set the size of our buffer
|
||||
var buffer = new int[4096];
|
||||
|
||||
// get the width, height, and depth of the sample space for our nested for loops
|
||||
int width = voxelChunk.Bounds.Size.X + 1;
|
||||
int height = voxelChunk.Bounds.Size.Y + 1;
|
||||
int depth = voxelChunk.Bounds.Size.Z + 1;
|
||||
|
||||
|
||||
int n = 0;
|
||||
int[] pos = new int[3];
|
||||
int[] R = new int[] { 1, width + 1, (width + 1) * (height + 1) };
|
||||
float[] grid = new float[8];
|
||||
int bufferNumber = 1;
|
||||
|
||||
// resize the buffer if it's not big enough
|
||||
if (R[2] * 2 > buffer.Length)
|
||||
buffer = new int[R[2] * 2];
|
||||
|
||||
for (pos[2] = 0; pos[2] < depth - 1; pos[2]++, n += width, bufferNumber ^= 1, R[2] = -R[2])
|
||||
{
|
||||
var bufferIndex = 1 + (width + 1) * (1 + bufferNumber * (height + 1));
|
||||
|
||||
for (pos[1] = 0; pos[1] < height - 1; pos[1]++, n++, bufferIndex += 2)
|
||||
{
|
||||
for (pos[0] = 0; pos[0] < width - 1; pos[0]++, n++, bufferIndex++)
|
||||
{
|
||||
// get the corner mask we calculated earlier
|
||||
var mask = voxels[pos[0], pos[1], pos[2]].cornerMask;
|
||||
|
||||
// Early Termination Check
|
||||
if (mask == 0 || mask == 0xff)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// get edge mask
|
||||
var edgeMask = edgeTable[mask];
|
||||
|
||||
var vertex = new Vector3();
|
||||
var edgeIndex = 0;
|
||||
//For Every Cube Edge
|
||||
for (var i = 0; i < 12; i++)
|
||||
{
|
||||
//Use Edge Mask to Check if Crossed
|
||||
if (!Convert.ToBoolean(edgeMask & (1 << i)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//If So, Increment Edge Crossing #
|
||||
edgeIndex++;
|
||||
|
||||
//Find Intersection Point
|
||||
var e0 = cubeEdges[i << 1];
|
||||
var e1 = cubeEdges[(i << 1) + 1];
|
||||
var g0 = grid[e0];
|
||||
var g1 = grid[e1];
|
||||
var t = g0 - g1;
|
||||
if (System.Math.Abs(t) > 1e-16)
|
||||
t = g0 / t;
|
||||
else
|
||||
continue;
|
||||
|
||||
//Interpolate Vertices, Add Intersections
|
||||
if (!voxels[pos[0], pos[1], pos[2]].isCube)
|
||||
for (int j = 0, k = 1; j < 3; j++, k <<= 1)
|
||||
{
|
||||
var a = e0 & k;
|
||||
var b = e1 & k;
|
||||
if (a != b)
|
||||
vertex[j] += Convert.ToBoolean(a) ? 1f - t : t;
|
||||
else
|
||||
vertex[j] += Convert.ToBoolean(a) ? 1f : 0;
|
||||
}
|
||||
}
|
||||
|
||||
//Average Edge Intersections, Add to Coordinate
|
||||
if (!voxels[pos[0], pos[1], pos[2]].isCube)
|
||||
{
|
||||
var s = 1f / edgeIndex;
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
vertex[i] = pos[i] + s * vertex[i];
|
||||
}
|
||||
}else
|
||||
vertex = voxels[pos[0], pos[1], pos[2]].vertexPosition;
|
||||
//Add Vertex to Buffer, Store Pointer to Vertex Index
|
||||
buffer[bufferIndex] = newVertices.Count;
|
||||
newVertices.Add(vertex);
|
||||
|
||||
//Add Faces (Loop Over 3 Base Components)
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
//First 3 Entries Indicate Crossings on Edge
|
||||
if (!Convert.ToBoolean(edgeMask & (1 << i)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//i - Axes, iu, iv - Ortho Axes
|
||||
var iu = (i + 1) % 3;
|
||||
var iv = (i + 2) % 3;
|
||||
|
||||
//Skip if on Boundary
|
||||
if (pos[iu] == 0 || pos[iv] == 0)
|
||||
continue;
|
||||
|
||||
//Otherwise, Look Up Adjacent Edges in Buffer
|
||||
var du = R[iu];
|
||||
var dv = R[iv];
|
||||
|
||||
//Flip Orientation Depending on Corner Sign
|
||||
if (Convert.ToBoolean(mask & 1))
|
||||
{
|
||||
triangles.Add(buffer[bufferIndex]);
|
||||
triangles.Add(buffer[bufferIndex - du - dv]);
|
||||
triangles.Add(buffer[bufferIndex - du]);
|
||||
triangles.Add(buffer[bufferIndex]);
|
||||
triangles.Add(buffer[bufferIndex - dv]);
|
||||
triangles.Add(buffer[bufferIndex - du - dv]);
|
||||
}
|
||||
else
|
||||
{
|
||||
triangles.Add(buffer[bufferIndex]);
|
||||
triangles.Add(buffer[bufferIndex - du - dv]);
|
||||
triangles.Add(buffer[bufferIndex - dv]);
|
||||
triangles.Add(buffer[bufferIndex]);
|
||||
triangles.Add(buffer[bufferIndex - du]);
|
||||
triangles.Add(buffer[bufferIndex - du - dv]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Godot.Collections.Array surfaceArray = [];
|
||||
surfaceArray.Resize((int)Mesh.ArrayType.Max);
|
||||
surfaceArray[(int)Mesh.ArrayType.Vertex] = vertices.ToArray();
|
||||
// surfaceArray[(int)Mesh.ArrayType.TexUV] = uvs.ToArray();
|
||||
surfaceArray[(int)Mesh.ArrayType.Normal] = new Vector3[vertices.Count];
|
||||
surfaceArray[(int)Mesh.ArrayType.Index] = triangles.ToArray();
|
||||
|
||||
var arrMesh = new ArrayMesh();
|
||||
// No blendshapes, lods, or compression used.
|
||||
arrMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Triangles, surfaceArray);
|
||||
MeshDataTool meshDataTool = new MeshDataTool();
|
||||
meshDataTool.CreateFromSurface(arrMesh, 0);
|
||||
for (int i = 0; i < meshDataTool.GetFaceCount(); i++)
|
||||
{
|
||||
// # Get the index in the vertex array.
|
||||
var a = meshDataTool.GetFaceVertex(i, 0);
|
||||
var b = meshDataTool.GetFaceVertex(i, 1);
|
||||
var c = meshDataTool.GetFaceVertex(i, 2);
|
||||
// # Get vertex position using vertex index.
|
||||
var ap = meshDataTool.GetVertex(a);
|
||||
var bp = meshDataTool.GetVertex(b);
|
||||
var cp = meshDataTool.GetVertex(c);
|
||||
// # Calculate face normal.
|
||||
Vector3 nn = (bp - cp).Cross(ap - bp).Normalized();
|
||||
// # Add face normal to current vertex normal.
|
||||
// # This will not result in perfect normals, but it will be close.
|
||||
meshDataTool.SetVertexNormal(a, nn + meshDataTool.GetVertexNormal(a));
|
||||
meshDataTool.SetVertexNormal(b, nn + meshDataTool.GetVertexNormal(b));
|
||||
meshDataTool.SetVertexNormal(c, nn + meshDataTool.GetVertexNormal(c));
|
||||
|
||||
}
|
||||
for (int i = 0; i < meshDataTool.GetVertexCount(); i++)
|
||||
{
|
||||
var v = meshDataTool.GetVertexNormal(i).Normalized();
|
||||
meshDataTool.SetVertexNormal(i,v);
|
||||
}
|
||||
arrMesh.ClearSurfaces();
|
||||
meshDataTool.CommitToSurface(arrMesh);
|
||||
return arrMesh;
|
||||
}
|
||||
/**
|
||||
* calculate the postion of each vertex. this sets up our 3 dimensional grid of voxels
|
||||
* while also sampling each voxel
|
||||
**/
|
||||
|
||||
void CalculateVertexPositions()
|
||||
{
|
||||
voxels = new Voxel[voxelChunk.Bounds.Size.X, voxelChunk.Bounds.Size.Y, voxelChunk.Bounds.Size.Z];
|
||||
for (int x = 0; x < voxelChunk.Bounds.Size.X - 1; x++)
|
||||
{
|
||||
for (int y = 0; y < voxelChunk.Bounds.Size.Y - 1; y++)
|
||||
{
|
||||
for (int z = 0; z < voxelChunk.Bounds.Size.Z - 1; z++)
|
||||
{
|
||||
|
||||
// default values.
|
||||
voxels[x, y, z].isOnSurface = false;
|
||||
voxels[x, y, z].voxelEdgeMask = 0;
|
||||
voxels[x, y, z].vertexIndex = -1;
|
||||
voxels[x, y, z].cornerMask = 0;
|
||||
voxels[x, y, z].isCube = false;
|
||||
|
||||
int cornerMask = 0;
|
||||
|
||||
// sample the 8 corners for the voxel and create a corner mask
|
||||
Sample[] samples = new Sample[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
var offset = voxelCornerOffsets[i];
|
||||
var pos = new Vector3I(x + (int)offset.X, y + (int)offset.Y, z + (int)offset.Z);
|
||||
var sample = sampler(new(pos.X,pos.Y,pos.Z),voxelChunk.GetVoxel(pos));
|
||||
voxels[x, y, z].isCube = voxels[x, y, z].isCube | sample.isCube;
|
||||
samples[i].value = sample.sdf;
|
||||
samples[i].point = pos;
|
||||
cornerMask |= ((sample.sdf > threshold) ? (1 << i) : 0);
|
||||
}
|
||||
|
||||
//Check for early termination if cell does not intersect boundary
|
||||
if (cornerMask == 0 || cornerMask == 0xff)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// get edgemask from table using our corner mask
|
||||
int edgeMask = edgeTable[cornerMask];
|
||||
int edgeCrossings = 0;
|
||||
var vertPos = Vector3.Zero;
|
||||
|
||||
for (int i = 0; i < 12; ++i)
|
||||
{
|
||||
|
||||
//Use edge mask to check if it is crossed
|
||||
if (!((edgeMask & (1 << i)) > 0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//If it did, increment number of edge crossings
|
||||
++edgeCrossings;
|
||||
|
||||
//Now find the point of intersection
|
||||
int e0 = cubeEdges[i << 1];
|
||||
int e1 = cubeEdges[(i << 1) + 1];
|
||||
float g0 = samples[e0].value;
|
||||
float g1 = samples[e1].value;
|
||||
float t = (threshold - g0) / (g1 - g0);
|
||||
vertPos += SJKMath.Lerp(samples[e0].point, samples[e1].point, voxels[x, y, z].isCube?.5f : t);
|
||||
|
||||
}
|
||||
vertPos /= edgeCrossings;
|
||||
|
||||
voxels[x, y, z].vertexPosition = vertPos;
|
||||
voxels[x, y, z].isOnSurface = true;
|
||||
voxels[x, y, z].voxelEdgeMask = edgeMask;
|
||||
voxels[x, y, z].vertexIndex = vertices.Count;
|
||||
voxels[x, y, z].cornerMask = cornerMask;
|
||||
vertices.Add(vertPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum EdgeMode : byte
|
||||
{
|
||||
NoEdge,//do not create voxels on edges of chunk
|
||||
NegitiveEdges,//Create edges on - xyz axis only
|
||||
PositveEdges,//Create edges on + xyz axis only
|
||||
Outline,// Indcates taht the outer line of voxels are outside the chunk
|
||||
}
|
||||
|
||||
}
|
||||
1
src/voxelgame/Voxels/Chunks/SurfaceNetMesher.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/SurfaceNetMesher.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bn37xdn3uyrdx
|
||||
106
src/voxelgame/Voxels/Chunks/VoxelChunk.cs
Normal file
106
src/voxelgame/Voxels/Chunks/VoxelChunk.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using Godot;
|
||||
using SJK.Voxels.Registry;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SJK.Voxels;
|
||||
|
||||
public sealed class VoxelChunk<TVoxel,TChunk> : IVoxelChunk<TVoxel,TChunk>, IReadOnlyVoxelChunk<TVoxel,TChunk> where TChunk : IChunkSize<TChunk>
|
||||
{
|
||||
// public Vector3I Size => new(TChunk.Size, TChunk.Size, TChunk.Size);
|
||||
public VoxelBounds Bounds => new(Vector3I.Zero,new(TChunk.Size,TChunk.Size,TChunk.Size));
|
||||
// TVoxel[,,] voxels;
|
||||
TVoxel[] voxels;
|
||||
|
||||
|
||||
|
||||
public VoxelChunk()
|
||||
{
|
||||
voxels = new TVoxel[TChunk.Size*TChunk.Size*TChunk.Size];
|
||||
// voxels = new TVoxel[Size.X, Size.Y, Size.Z];
|
||||
}
|
||||
public VoxelChunk(TVoxel[] voxels)
|
||||
{
|
||||
// if (voxels.GetLength(0) != TChunk.Size && voxels.GetLength(1) != TChunk.Size && voxels.GetLength(2) != TChunk.Size)
|
||||
// {
|
||||
// throw new Exception($"{nameof(voxels)} must be of size {Size}. Supplied array was of length ({voxels.GetLength(0)}, {voxels.GetLength(1)}, {voxels.GetLength(0)})");
|
||||
// }
|
||||
this.voxels = voxels;
|
||||
}
|
||||
public VoxelChunk(TVoxel fill) : this()
|
||||
{
|
||||
Array.Fill(voxels,fill);
|
||||
}
|
||||
public TVoxel GetVoxel(VoxelPos position)
|
||||
{
|
||||
Debug.Assert(IsInBounds(position),$" Position {position} is out of bounds, Size:{TChunk.Size} plese check {nameof(IsInBounds)} before Calling");
|
||||
return voxels[new VoxelPos<TChunk>(position.X, position.Y, position.Z).Index];
|
||||
}
|
||||
|
||||
public bool IsInBounds(VoxelPos position) => IChunkSize<TChunk>.IsInBounds((Vector3I)position);
|
||||
|
||||
|
||||
|
||||
public void SetVoxel(VoxelPos position, TVoxel voxel)
|
||||
{
|
||||
Debug.Assert(IsInBounds(position),$" Position {position} is out of bounds, Size:{TChunk.Size} plese check {nameof(IsInBounds)} before Calling");
|
||||
voxels[new VoxelPos<TChunk>(position.X, position.Y, position.Z).Index] = voxel;
|
||||
}
|
||||
|
||||
|
||||
public TVoxel[] GetRawVoxelData() => voxels;
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos<TChunk> position) => voxels[position.Index];
|
||||
|
||||
public void SetVoxel(VoxelPos<TChunk> position, TVoxel voxel) => voxels[position.Index] = voxel;
|
||||
|
||||
public void Fill(TVoxel voxel)=> Array.Fill(voxels,voxel);
|
||||
}
|
||||
public class PaletteVoxelChunk<TVoxel, TChunkSize> : IVoxelChunk<TVoxel, TChunkSize> where TVoxel : IEquatable<TVoxel> where TChunkSize : IChunkSize<TChunkSize>
|
||||
{
|
||||
private VoxelPalette<VoxelHandle<PaletteLevel>, PaletteLevel, TVoxel> voxelPalette;
|
||||
private readonly IVoxelChunk<VoxelHandle<PaletteLevel>,TChunkSize> innerStore;
|
||||
|
||||
public VoxelBounds Bounds => new(Vector3I.Zero,new(TChunkSize.Size,TChunkSize.Size,TChunkSize.Size));
|
||||
public PaletteVoxelChunk()
|
||||
{
|
||||
innerStore = new VoxelChunk<VoxelHandle<PaletteLevel>,TChunkSize>();
|
||||
}
|
||||
public PaletteVoxelChunk(IVoxelChunk<VoxelHandle<PaletteLevel>,TChunkSize> innerStore)
|
||||
{
|
||||
this.innerStore = innerStore;
|
||||
}
|
||||
public VoxelChunkEnumerator<TVoxel> GetEnumerator() => new VoxelChunkEnumerator<TVoxel>(this);
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos<TChunkSize> position)
|
||||
{
|
||||
var handle = innerStore.GetVoxel(position);
|
||||
return voxelPalette.Get(handle);
|
||||
}
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos position) => GetVoxel(new VoxelPos<TChunkSize>(position.X, position.Y, position.Z));
|
||||
|
||||
public bool IsInBounds(VoxelPos position) =>
|
||||
position.X >= 0 && position.X < TChunkSize.Size
|
||||
&& position.Y >= 0 && position.Y < TChunkSize.Size
|
||||
&& position.Z >= 0 && position.Z < TChunkSize.Size;
|
||||
|
||||
|
||||
public void SetVoxel(VoxelPos<TChunkSize> position, TVoxel voxel)
|
||||
{
|
||||
innerStore.SetVoxel(position, voxelPalette.GetOrAddEntry(voxel));
|
||||
}
|
||||
|
||||
public void SetVoxel(VoxelPos position, TVoxel voxel) => SetVoxel(new VoxelPos<TChunkSize>(position.X, position.Y, position.Z), voxel);
|
||||
|
||||
VoxelChunkEnumerator<TVoxel, TChunkSize> IReadOnlyVoxelChunk<TVoxel, TChunkSize>.GetEnumerator()
|
||||
{
|
||||
return new VoxelChunkEnumerator<TVoxel, TChunkSize>(this);
|
||||
}
|
||||
|
||||
public void Fill(TVoxel voxel)
|
||||
{
|
||||
voxelPalette.Clear();
|
||||
innerStore.Fill(voxelPalette.GetOrAddEntry(voxel));
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/Voxels/Chunks/VoxelChunk.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/VoxelChunk.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dsvwlpnv2gyrv
|
||||
99
src/voxelgame/Voxels/Chunks/VoxelChunkEnumerator.cs
Normal file
99
src/voxelgame/Voxels/Chunks/VoxelChunkEnumerator.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Godot;
|
||||
|
||||
namespace SJK.Voxels;
|
||||
//TODO need to have an version for not bound to TChunkSize, and VoxelChunkEnurmatero for <TVoxel,TSize>, and likly be a good idea for fversions that change directions
|
||||
public struct VoxelChunkEnumerator<TVoxel>
|
||||
{
|
||||
private readonly IReadOnlyVoxelChunk<TVoxel> chunk;
|
||||
// private readonly Vector3I size;
|
||||
private int x, y, z;
|
||||
|
||||
public VoxelChunkEnumerator(IReadOnlyVoxelChunk<TVoxel> chunk)
|
||||
{
|
||||
this.chunk = chunk;
|
||||
// this.size = chunk.Size;
|
||||
this.x = chunk.Bounds.Min.X-1;
|
||||
this.y = chunk.Bounds.Min.Y;
|
||||
this.z = chunk.Bounds.Min.Z;
|
||||
Current = default;
|
||||
}
|
||||
|
||||
public (Vector3I Position, TVoxel Voxel) Current { get; private set; }
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
x++;
|
||||
if (x >= chunk.Bounds.Max.X)
|
||||
{
|
||||
x = chunk.Bounds.Min.X;
|
||||
y++;
|
||||
if (y >= chunk.Bounds.Max.Y)
|
||||
{
|
||||
y = chunk.Bounds.Min.Y;
|
||||
z++;
|
||||
if (z >= chunk.Bounds.Max.Z)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var pos = new Vector3I(x, y, z);
|
||||
Current = (pos, chunk.GetVoxel(pos));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public struct VoxelChunkEnumerator<TVoxel,TChunkSize> where TChunkSize : IChunkSize<TChunkSize>
|
||||
{
|
||||
private readonly IReadOnlyVoxelChunk<TVoxel,TChunkSize> chunk;
|
||||
// private readonly Vector3I size;
|
||||
private uint pos = 0;
|
||||
|
||||
public VoxelChunkEnumerator(IReadOnlyVoxelChunk<TVoxel,TChunkSize> chunk)
|
||||
{
|
||||
this.chunk = chunk;
|
||||
// this.size = chunk.Size;
|
||||
Current = default;
|
||||
}
|
||||
|
||||
public (VoxelPos<TChunkSize> Position, TVoxel Voxel) Current { get; private set; }
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (pos >= IChunkSize<TChunkSize>.SizeCubed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
pos++;
|
||||
// var pos = new Vector3I(x, y, z);
|
||||
var adjustedPos = new VoxelPos<TChunkSize>(pos-1);
|
||||
Current = (adjustedPos, chunk.GetVoxel(adjustedPos));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public struct VoxelPositionChunkEnumerator<TVoxel,TChunkSize> where TChunkSize : IChunkSize<TChunkSize>
|
||||
{
|
||||
private readonly IReadOnlyVoxelChunk<TVoxel,TChunkSize> chunk;
|
||||
// private readonly Vector3I size;
|
||||
private uint pos = 0;
|
||||
|
||||
public VoxelPositionChunkEnumerator(IReadOnlyVoxelChunk<TVoxel,TChunkSize> chunk)
|
||||
{
|
||||
this.chunk = chunk;
|
||||
// this.size = chunk.Size;
|
||||
Current = default;
|
||||
}
|
||||
|
||||
public VoxelPos<TChunkSize> Current { get; private set; }
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (pos >= IChunkSize<TChunkSize>.SizeCubed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
pos++;
|
||||
// var pos = new Vector3I(x, y, z);
|
||||
var adjustedPos = new VoxelPos<TChunkSize>(pos-1);
|
||||
// Current = (adjustedPos, chunk.GetVoxel(adjustedPos));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/Voxels/Chunks/VoxelChunkEnumerator.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/VoxelChunkEnumerator.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cd5ybj683y0be
|
||||
202
src/voxelgame/Voxels/Chunks/VoxelSlice.cs
Normal file
202
src/voxelgame/Voxels/Chunks/VoxelSlice.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SJK.Voxels;
|
||||
|
||||
public sealed class VoxelSlice<TVoxel> : IVoxelChunk<TVoxel>, IReadOnlyVoxelChunk<TVoxel>
|
||||
{
|
||||
private readonly IVoxelChunk<TVoxel> voxelChunk;
|
||||
|
||||
public VoxelSlice(IVoxelChunk<TVoxel> voxelChunk, Vector3I offset, Vector3I size)
|
||||
{
|
||||
if (voxelChunk is VoxelSlice<TVoxel> slice)
|
||||
{
|
||||
voxelChunk = slice.voxelChunk;
|
||||
offset = offset + slice.Offset;
|
||||
}
|
||||
#if DEBUG
|
||||
if (offset.X < voxelChunk.Bounds.Min.X || offset.Y < voxelChunk.Bounds.Min.Y || offset.Z < voxelChunk.Bounds.Min.Z)
|
||||
{
|
||||
throw new IndexOutOfRangeException($"{nameof(offset)} has an value less then Zero : {offset}");
|
||||
}
|
||||
if (offset.X + size.X > voxelChunk.Bounds.Max.X || offset.Y + size.Y > voxelChunk.Bounds.Max.Y || offset.Z + size.Z > voxelChunk.Bounds.Max.Z)
|
||||
{
|
||||
throw new IndexOutOfRangeException($"{nameof(offset)} has an value greater then the size of the {nameof(voxelChunk)} : {offset + size},Size : {voxelChunk.Bounds}");
|
||||
}
|
||||
#endif
|
||||
|
||||
this.voxelChunk = voxelChunk;
|
||||
Offset = offset;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
public Vector3I Offset { get; }
|
||||
public Vector3I Size { get; }
|
||||
|
||||
public VoxelBounds Bounds => throw new NotImplementedException();
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos position) => voxelChunk.GetVoxel(position + Offset);
|
||||
|
||||
// public bool IsInBounds(VoxelPos position) => voxelChunk.IsInBounds(position + Offset);
|
||||
|
||||
public void SetVoxel(VoxelPos position, TVoxel voxel) => voxelChunk.SetVoxel(position + Offset, voxel);
|
||||
|
||||
public VoxelChunkEnumerator<TVoxel> GetEnumerator()
|
||||
=> new VoxelChunkEnumerator<TVoxel>(this);
|
||||
|
||||
}
|
||||
public class ReadOnlyVirtualChunk<TVoxel> : IReadOnlyVoxelChunk<TVoxel>
|
||||
{
|
||||
private readonly Func<VoxelPos, TVoxel> func;
|
||||
|
||||
public ReadOnlyVirtualChunk(Vector3I size, Func<VoxelPos, TVoxel> func)
|
||||
{
|
||||
Size = size;
|
||||
this.func = func;
|
||||
}
|
||||
public Vector3I Size { get; }
|
||||
|
||||
public VoxelBounds Bounds => new VoxelBounds(Vector3I.Zero,Size);
|
||||
|
||||
public VoxelChunkEnumerator<TVoxel> GetEnumerator() => new VoxelChunkEnumerator<TVoxel>(this);
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos position) => func(position);
|
||||
|
||||
public bool IsInBounds(VoxelPos position)=>
|
||||
position.X >= 0 && position.X < Size.X
|
||||
&& position.Y >= 0 && position.Y < Size.Y
|
||||
&& position.Z >= 0 && position.Z < Size.Z ;
|
||||
|
||||
}
|
||||
public class NeighborAwareChunk<TVoxel,TChunkSize> : IReadOnlyVoxelChunk<TVoxel,TChunkSize> where TChunkSize : IChunkSize<TChunkSize>
|
||||
{
|
||||
private readonly ChunkPos<TChunkSize> chunkPos;
|
||||
private readonly IReadOnlyVoxelChunk<TVoxel,TChunkSize> centerChunk;
|
||||
private readonly IReadOnlyVoxelChunk<TVoxel,TChunkSize>?[] neigbors;
|
||||
private readonly Func<VoxelPos,TVoxel> getWorld;
|
||||
private readonly TVoxel _default;
|
||||
|
||||
public NeighborAwareChunk(ChunkPos<TChunkSize> chunkPos,IReadOnlyVoxelChunk<TVoxel,TChunkSize> centerChunk, IReadOnlyVoxelChunk<TVoxel,TChunkSize>?[] neigbors, Func<VoxelPos,TVoxel> getWorld, TVoxel @default = default)
|
||||
{
|
||||
this.chunkPos = chunkPos;
|
||||
this.centerChunk = centerChunk;
|
||||
this.neigbors = neigbors;
|
||||
this.getWorld = getWorld;
|
||||
this._default = @default;
|
||||
}
|
||||
public VoxelBounds Bounds => centerChunk.Bounds;
|
||||
|
||||
public VoxelChunkEnumerator<TVoxel> GetEnumerator() => new VoxelChunkEnumerator<TVoxel>(this);
|
||||
|
||||
|
||||
public bool IsInBounds(VoxelPos position) =>
|
||||
position.X >= -1 && position.X < TChunkSize.Size + 1
|
||||
&& position.Y >= -1 && position.Y < TChunkSize.Size + 1
|
||||
&& position.Z >= -1 && position.Z < TChunkSize.Size + 1;
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos pos)
|
||||
{
|
||||
// shift into neighbor-aware index space
|
||||
int x = pos.X;
|
||||
int y = pos.Y;
|
||||
int z = pos.Z;
|
||||
|
||||
int sizeX = TChunkSize.Size;
|
||||
int sizeY = TChunkSize.Size;
|
||||
int sizeZ = TChunkSize.Size;
|
||||
|
||||
// Check if within center chunk
|
||||
if (x >= 0 && x < sizeX &&
|
||||
y >= 0 && y < sizeY &&
|
||||
z >= 0 && z < sizeZ)
|
||||
{
|
||||
return centerChunk.GetVoxel(pos);
|
||||
}
|
||||
|
||||
// Encode offsets (-1,0,+1) into a small index
|
||||
int dx = (x < 0) ? -1 : (x >= sizeX ? 1 : 0);
|
||||
int dy = (y < 0) ? -1 : (y >= sizeY ? 1 : 0);
|
||||
int dz = (z < 0) ? -1 : (z >= sizeZ ? 1 : 0);
|
||||
|
||||
int code = ((dx + 1) * 9) + ((dy + 1) * 3) + (dz + 1);
|
||||
// That gives you a unique 0..26 index for neighbors
|
||||
// GD.PrintS(x, y, z, dx, dy, dz, code, new Vector3I(sizeX - 1, y, z));
|
||||
if (neigbors is null)
|
||||
{
|
||||
return HandleEdgeOrCorner(dx, dy, dz, x, y, z);
|
||||
}
|
||||
try
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case 13: // (0,0,0) center, already handled
|
||||
return _default;
|
||||
|
||||
case 12: // (-1,0,0) left
|
||||
return neigbors[(int)Direction.Forward] is not null ? neigbors[(int)Direction.Forward].GetVoxel(new Vector3I(x, y, z + sizeZ)) : _default;
|
||||
case 14: // (1,0,0) right
|
||||
return neigbors[(int)Direction.BackWord] is not null ? neigbors[(int)Direction.BackWord].GetVoxel(new Vector3I(x, y, z - sizeZ)) : _default;
|
||||
|
||||
case 10: // (0,-1,0) down
|
||||
return neigbors[(int)Direction.Down] is not null ? neigbors[(int)Direction.Down].GetVoxel(new Vector3I(x, y + sizeY, z)) : _default;
|
||||
case 16: // (0,1,0) up
|
||||
return neigbors[(int)Direction.Up] is not null ? neigbors[(int)Direction.Up].GetVoxel(new Vector3I(x, y - sizeY, z)) : _default;
|
||||
|
||||
case 4: // (0,0,-1) back
|
||||
return neigbors[(int)Direction.Left] is not null ? neigbors[(int)Direction.Left].GetVoxel(new Vector3I(x + sizeX, y, z)) : _default;
|
||||
case 22: // (0,0,1) front
|
||||
return neigbors[(int)Direction.Right] is not null ? neigbors[(int)Direction.Right].GetVoxel(new Vector3I(x - sizeX, y, z)) : _default;
|
||||
|
||||
// Edges and corners:
|
||||
default:
|
||||
return HandleEdgeOrCorner(dx, dy, dz, x, y, z);
|
||||
}
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
GD.PrintS(code, x, y, z);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private TVoxel HandleEdgeOrCorner(int dx, int dy, int dz, int x, int y, int z)
|
||||
{
|
||||
// Example: if two offsets non-zero, it’s edge; three = corner.
|
||||
// int nonZero = (dx != 0 ? 1 : 0) + (dy != 0 ? 1 : 0) + (dz != 0 ? 1 : 0);
|
||||
|
||||
// if (nonZero == 2)
|
||||
// {
|
||||
// // Edge neighbor: need 2 chunks combined (you can decide to require them both or fallback)
|
||||
// // Example for (-1,-1,0):
|
||||
// if (dx == -1 && dy == -1)
|
||||
// return Left?.GetVoxel(new Vector3I(x + Size.X, y, z))
|
||||
// ?? Down?.GetVoxel(new Vector3I(x, y + Size.Y, z))
|
||||
// ?? default;
|
||||
// }
|
||||
// else if (nonZero == 3)
|
||||
// {
|
||||
// // Corner
|
||||
// return default; // or pick fallback value
|
||||
// }
|
||||
|
||||
return getWorld(new Vector3I(x, y, z) + (chunkPos * new Vector3I(TChunkSize.Size,TChunkSize.Size,TChunkSize.Size)));
|
||||
return _default;
|
||||
}
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos<TChunkSize> position) => GetVoxel(new Vector3I(position.X, position.Y, position.Z));
|
||||
|
||||
VoxelChunkEnumerator<TVoxel, TChunkSize> IReadOnlyVoxelChunk<TVoxel, TChunkSize>.GetEnumerator()
|
||||
{
|
||||
return new VoxelChunkEnumerator<TVoxel, TChunkSize>(this);
|
||||
}
|
||||
}
|
||||
public enum Direction : byte
|
||||
{
|
||||
Forward,
|
||||
Right,
|
||||
BackWord,
|
||||
Left,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
1
src/voxelgame/Voxels/Chunks/VoxelSlice.cs.uid
Normal file
1
src/voxelgame/Voxels/Chunks/VoxelSlice.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b2rcodqw5v2ku
|
||||
136
src/voxelgame/Voxels/ESCThing/StructuralInstance.Builder.cs
Normal file
136
src/voxelgame/Voxels/ESCThing/StructuralInstance.Builder.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
namespace ChickenGameTest;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
// using System.Management;
|
||||
|
||||
// public partial class StructuralInstance<TAttribute> where TAttribute : class
|
||||
// {
|
||||
public sealed class StructuralBuilder<TAttribute> where TAttribute : class
|
||||
{
|
||||
|
||||
private readonly Dictionary<int, TAttribute> _components = [];
|
||||
|
||||
public StructuralBuilder() { }
|
||||
|
||||
public StructuralBuilder(IStructuralInstance<TAttribute> existing)
|
||||
{
|
||||
foreach (var (id, value) in existing.GetAttributes())
|
||||
{
|
||||
_components[id] = value;
|
||||
}
|
||||
// for (int i = 0; i < existing.ComponentCount; i++)
|
||||
// {
|
||||
// var typeId = existing._attributesTypeIds[i];
|
||||
// var component = existing.GetComponentAt(i);
|
||||
|
||||
// _components[typeId] = component;
|
||||
// }
|
||||
}
|
||||
|
||||
public StructuralBuilder<TAttribute> Add<T>(T component) where T : TAttribute
|
||||
{
|
||||
int id = ComponentTypeRegistry.GetId<T>();
|
||||
_components[id] = component;
|
||||
return this;
|
||||
}
|
||||
public StructuralBuilder<TAttribute> Upsert<T>(Func<T, T> ifExists, Func<T> none) where T : TAttribute
|
||||
{
|
||||
int id = ComponentTypeRegistry.GetId<T>();
|
||||
_components[id] = _components.TryGetValue(id, out var old) ? ifExists((T)old) : none();
|
||||
return this;
|
||||
}
|
||||
public StructuralBuilder<TAttribute> CombineWith(IStructuralInstance<TAttribute> other, Action<CombineBinder>? configure = null)
|
||||
{
|
||||
var binder = new CombineBinder();
|
||||
configure?.Invoke(binder);
|
||||
foreach (var (id, value) in other.GetAttributes())
|
||||
{
|
||||
if (binder._ignores.Contains(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (_components.TryGetValue(id, out var existing)
|
||||
&& binder._rules.TryGetValue(id, out var rule))
|
||||
{
|
||||
_components[id] = rule.Invoke(existing, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
_components[id] = value;
|
||||
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public StructuralBuilder<TAttribute> Remove<T>() where T : TAttribute
|
||||
{
|
||||
int id = ComponentTypeRegistry.GetId<T>();
|
||||
_components.Remove(id);
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Has<T>() where T : TAttribute
|
||||
{
|
||||
int id = ComponentTypeRegistry.GetId<T>();
|
||||
return _components.ContainsKey(id);
|
||||
}
|
||||
|
||||
public StructuralInstance<TAttribute> Build()
|
||||
{
|
||||
int count = _components.Count;
|
||||
|
||||
var typeIds = new int[count];
|
||||
var components = new TAttribute[count];
|
||||
|
||||
int i = 0;
|
||||
foreach (var kv in _components)
|
||||
{
|
||||
typeIds[i] = kv.Key;
|
||||
components[i] = kv.Value;
|
||||
i++;
|
||||
}
|
||||
|
||||
Array.Sort(typeIds, components);
|
||||
return new StructuralInstance<TAttribute>(typeIds, components);
|
||||
}
|
||||
public MutableStructuralInstance<TAttribute> BuildMutable()
|
||||
{
|
||||
// int count = _components.Count;
|
||||
|
||||
var sortedList = new SortedList<int, TAttribute>(_components);
|
||||
// var typeIds = new int[count];
|
||||
// var components = new TAttribute[count];
|
||||
|
||||
// int i = 0;
|
||||
// foreach (var kv in _components)
|
||||
// {
|
||||
// typeIds[i] = kv.Key;
|
||||
// components[i] = kv.Value;
|
||||
// i++;
|
||||
// }
|
||||
|
||||
// Array.Sort(typeIds, components);
|
||||
return new MutableStructuralInstance<TAttribute>(sortedList);
|
||||
}
|
||||
public class CombineBinder
|
||||
{
|
||||
internal readonly Dictionary<int, Func<TAttribute, TAttribute, TAttribute>> _rules = [];
|
||||
internal readonly HashSet<int> _ignores = [];
|
||||
public CombineBinder Bind<T>(Func<T, T, T> func) where T : TAttribute
|
||||
{
|
||||
_rules[ComponentType<T>.Id] = (a, b) => func((T)a, (T)b);
|
||||
return this;
|
||||
}
|
||||
public CombineBinder Ignore<T>() where T : TAttribute
|
||||
{
|
||||
_ignores.Add(ComponentType<T>.Id);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }
|
||||
@@ -0,0 +1 @@
|
||||
uid://bk721rnhegl0x
|
||||
258
src/voxelgame/Voxels/ESCThing/StructuralInstance.cs
Normal file
258
src/voxelgame/Voxels/ESCThing/StructuralInstance.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
namespace ChickenGameTest;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
// using Chickensoft.Sync.Primitives;
|
||||
using SJK.Functional;
|
||||
|
||||
public interface IStructuralInstance<TAttribute> : IEquatable<IStructuralInstance<TAttribute>>
|
||||
{
|
||||
int ComponentCount { get; }
|
||||
bool Has<T>() where T : TAttribute;
|
||||
Option<T> Get<T>() where T : TAttribute;
|
||||
// TAttribute GetComponentAt(int index);
|
||||
IEnumerable<(int Id, TAttribute Value)> GetAttributes();
|
||||
bool IEquatable<IStructuralInstance<TAttribute>>.Equals(IStructuralInstance<TAttribute>? other) => Enumerable.SequenceEqual(GetAttributes(), other.GetAttributes(), EqualityComparer<(int, TAttribute)>.Default);
|
||||
|
||||
int ComputeHashCode()
|
||||
{
|
||||
HashCode hash = new HashCode();
|
||||
hash.Add(ComponentCount);
|
||||
foreach (var item in GetAttributes())
|
||||
{
|
||||
hash.Add(item.Id);
|
||||
hash.Add(item.Value);
|
||||
}
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class StructuralInstance<TAttribute> : IStructuralInstance<TAttribute>, IEquatable<StructuralInstance<TAttribute>> where TAttribute : class
|
||||
{
|
||||
private readonly int _hashCode;
|
||||
internal readonly int[] _attributesTypeIds;
|
||||
internal readonly TAttribute[] _attributes;
|
||||
|
||||
public int ComponentCount => _attributes.Length;
|
||||
|
||||
public TAttribute GetComponentAt(int index) => _attributes[index];
|
||||
internal StructuralInstance(int[] componentTypeIds, TAttribute[] attributes)
|
||||
{
|
||||
_attributesTypeIds = componentTypeIds;
|
||||
_attributes = attributes;
|
||||
_hashCode = (this as IStructuralInstance<TAttribute>).ComputeHashCode();
|
||||
}
|
||||
|
||||
public bool Has<T>() where T : TAttribute
|
||||
{
|
||||
// int typeId = ComponentTypeRegistry.GetId<T>();
|
||||
var typeId = ComponentType<T>.Id;
|
||||
return Array.BinarySearch(_attributesTypeIds, typeId) >= 0;
|
||||
}
|
||||
|
||||
public Option<T> Get<T>() where T : TAttribute
|
||||
{
|
||||
// int typeId = ComponentTypeRegistry.GetId<T>();
|
||||
var typeId = ComponentType<T>.Id;
|
||||
|
||||
int index = Array.BinarySearch(_attributesTypeIds, typeId);
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
return Option<T>.None;
|
||||
}
|
||||
|
||||
return Option<T>.Some((T)_attributes[index]);
|
||||
}
|
||||
|
||||
public bool Equals(StructuralInstance<TAttribute>? other)
|
||||
{
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return _hashCode == other._hashCode;// && Enumerable.SequenceEqual(_attributes, other._attributes, EqualityComparer<TAttribute>.Default);// && AttributesAreEqual(other);
|
||||
}
|
||||
public override bool Equals(object? obj) => obj is StructuralInstance<TAttribute> value? Equals(value) : obj is IStructuralInstance<TAttribute> v && v.Equals(this);
|
||||
|
||||
public override int GetHashCode() => _hashCode;
|
||||
public IEnumerable<(int Id, TAttribute Value)> GetAttributes()
|
||||
{
|
||||
for (int i = 0; i < ComponentCount; i++)
|
||||
{
|
||||
yield return (_attributesTypeIds[i], _attributes[i]);
|
||||
}
|
||||
}
|
||||
public override string ToString() => base.ToString();
|
||||
}
|
||||
public sealed class MutableStructuralInstance<TAttribute> : IStructuralInstance<TAttribute>, IEquatable<MutableStructuralInstance<TAttribute>> where TAttribute : class
|
||||
{
|
||||
private readonly SortedList<int, TAttribute> _attributes = [];
|
||||
public int ComponentCount => _attributes.Count;
|
||||
public MutableStructuralInstance(SortedList<int, TAttribute> attributes)
|
||||
{
|
||||
_attributes = attributes;
|
||||
}
|
||||
public Option<T> Get<T>() where T : TAttribute => _attributes.TryGetValue(ComponentType<T>.Id, out var attribute) ? Option<T>.Some((T)attribute) : Option<T>.None;
|
||||
public IEnumerable<(int Id, TAttribute Value)> GetAttributes() => _attributes.Select(x => (x.Key, x.Value));
|
||||
public TAttribute GetComponentAt(int index) => _attributes[index];
|
||||
public bool Has<T>() where T : TAttribute => _attributes.ContainsKey(ComponentType<T>.Id);
|
||||
public void Set<T>(T value) where T : TAttribute => _attributes[ComponentType<T>.Id] = value;
|
||||
public override int GetHashCode() => (this as IStructuralInstance<TAttribute>).ComputeHashCode();
|
||||
public bool Equals(MutableStructuralInstance<TAttribute>? other) => other is not null && Enumerable.SequenceEqual(_attributes, other._attributes, EqualityComparer<KeyValuePair<int, TAttribute>>.Default);
|
||||
public override bool Equals(object? obj) => obj is MutableStructuralInstance<TAttribute> other ? Equals(other) : obj is IStructuralInstance<TAttribute> v && v.Equals(this);
|
||||
}
|
||||
|
||||
public static class ComponentTypeRegistry
|
||||
{
|
||||
private static readonly Dictionary<Type, int> _typeToId = [];
|
||||
private static readonly List<Type> _idToType = [];
|
||||
|
||||
public static int GetId(Type type)
|
||||
{
|
||||
if (_typeToId.TryGetValue(type, out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
id = _idToType.Count;
|
||||
_typeToId[type] = id;
|
||||
_idToType.Add(type);
|
||||
|
||||
return id;
|
||||
}
|
||||
public static int GetId<T>() => GetId(typeof(T));
|
||||
}
|
||||
public static class ComponentType<T>
|
||||
{
|
||||
public static readonly int Id = ComponentTypeRegistry.GetId<T>();
|
||||
public static readonly bool CacheTransitions = Resolve();
|
||||
|
||||
private static bool Resolve()
|
||||
{
|
||||
var attr = typeof(T).GetCustomAttributes(typeof(ComponentOptionsAttribute), false);
|
||||
return attr.OfType<ComponentOptionsAttribute>().FirstOrDefault().ToOption().Map(static f => f.CacheTransitions).Or(true);
|
||||
}
|
||||
}
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
|
||||
public sealed class ComponentOptionsAttribute : Attribute
|
||||
{
|
||||
public bool CacheTransitions { get; init; } = true;
|
||||
}
|
||||
public interface IStructureRegistry<TAttribute> where TAttribute : class
|
||||
{
|
||||
StructuralInstance<TAttribute> Canonicalize(StructuralInstance<TAttribute> value);
|
||||
StructuralInstance<TAttribute> AddOrReplaceAttribute<T>(StructuralInstance<TAttribute> instance, T attribute) where T : TAttribute;
|
||||
StructuralInstance<TAttribute> RemoveAttribute<T>(StructuralInstance<TAttribute> instance) where T : TAttribute;
|
||||
}
|
||||
public class StructuralInstanceManger<TAttribute> : IStructureRegistry<TAttribute> where TAttribute : class
|
||||
{
|
||||
private readonly HashSet<StructuralInstance<TAttribute>> _instances = [];
|
||||
public StructuralInstance<TAttribute> Add<T>(StructuralInstance<TAttribute> instance, T component) where T : TAttribute
|
||||
{
|
||||
var builder = new StructuralBuilder<TAttribute>(instance).Add(component);
|
||||
return Canonicalize(builder.Build());
|
||||
|
||||
}
|
||||
public StructuralInstance<TAttribute> Canonicalize(StructuralInstance<TAttribute> value)
|
||||
{
|
||||
|
||||
if (_instances.TryGetValue(value, out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
_instances.Add(value);
|
||||
return value;
|
||||
}
|
||||
public StructuralInstance<TAttribute> AddOrReplaceAttribute<T>(StructuralInstance<TAttribute> instance, T attribute) where T : TAttribute
|
||||
{
|
||||
if (!graph.TryGetValue(instance, out var results))
|
||||
{
|
||||
graph[instance] = results = [];
|
||||
}
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
if (results[i] is AddTransitionEntry<T> addTransitionEntry && EqualityComparer<T>.Default.Equals(addTransitionEntry.Value, attribute))
|
||||
{
|
||||
return addTransitionEntry.Result;
|
||||
}
|
||||
}
|
||||
var result =new AddTransitionEntry<T>(ComponentType<T>.Id, attribute, Canonicalize(new StructuralBuilder<TAttribute>(instance).Add(attribute).Build()));
|
||||
if (ComponentType<T>.CacheTransitions)
|
||||
{
|
||||
results.Add(result);
|
||||
}
|
||||
return result.Result;
|
||||
}
|
||||
public StructuralInstance<TAttribute> RemoveAttribute<T>(StructuralInstance<TAttribute> instance) where T : TAttribute
|
||||
{
|
||||
if (!graph.TryGetValue(instance, out var results))
|
||||
{
|
||||
graph[instance] = results = [];
|
||||
}
|
||||
var id = ComponentType<T>.Id;
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
if (results[i] is RemoveTransitionEntry removeTransitionEntry && removeTransitionEntry.Id == id)
|
||||
{
|
||||
return removeTransitionEntry.Result;
|
||||
}
|
||||
}
|
||||
var result =new RemoveTransitionEntry(id, Canonicalize(new StructuralBuilder<TAttribute>(instance).Remove<T>().Build()));
|
||||
if (ComponentType<T>.CacheTransitions)
|
||||
{
|
||||
results.Add(result);
|
||||
}
|
||||
return result.Result;
|
||||
}
|
||||
private Dictionary<StructuralInstance<TAttribute>, List<TransitionEntry>> graph = [];
|
||||
|
||||
private record TransitionEntry(int Id);
|
||||
|
||||
private record AddTransitionEntry<T>(int Id, T Value, StructuralInstance<TAttribute> Result) : TransitionEntry(Id) where T : TAttribute;
|
||||
|
||||
private record RemoveTransitionEntry(int Id, StructuralInstance<TAttribute> Result) : TransitionEntry(Id);
|
||||
// private record ModifyTransitionEntry<T>(int Id, TAttribute Value, StructuralInstance<TAttribute> Result) : TransitionEntry(Id) where T : TAttribute;
|
||||
|
||||
}
|
||||
public class AutoStructureInstance<TAttribute> where TAttribute : class
|
||||
{
|
||||
private StructuralInstance<TAttribute> _structuralInstance;
|
||||
private IStructureRegistry<TAttribute> _registry;
|
||||
private Dictionary<int,List<Delegate>> _bindings = [];
|
||||
|
||||
public void Set<T>(T? value) where T : TAttribute
|
||||
{
|
||||
var old = _structuralInstance.Get<T>();
|
||||
if (old.HasValue && old.Value.Equals(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (value is null)
|
||||
{
|
||||
_structuralInstance = _registry.RemoveAttribute<T>(_structuralInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
_structuralInstance = _registry.AddOrReplaceAttribute(_structuralInstance, value);
|
||||
}
|
||||
_bindings[ComponentType<T>.Id].ForEach(item => item.DynamicInvoke(old,value));
|
||||
|
||||
}
|
||||
public void Bind<T>(Action<T?, T?> callback) where T : TAttribute
|
||||
{
|
||||
int id = ComponentType<T>.Id;
|
||||
|
||||
if (!_bindings.TryGetValue(id, out var list))
|
||||
{
|
||||
_bindings[id] = list = [];
|
||||
}
|
||||
|
||||
list.Add(callback);
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/Voxels/ESCThing/StructuralInstance.cs.uid
Normal file
1
src/voxelgame/Voxels/ESCThing/StructuralInstance.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cexmjk01dgtbe
|
||||
66
src/voxelgame/Voxels/ESCThing/TestStructural.cs
Normal file
66
src/voxelgame/Voxels/ESCThing/TestStructural.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
namespace ChickenGameTest;
|
||||
|
||||
using System.Diagnostics;
|
||||
using Godot;
|
||||
using SJK.Functional;
|
||||
|
||||
public class TestStructural
|
||||
{
|
||||
public static void Test()
|
||||
{
|
||||
var a = new StructuralBuilder<object>()
|
||||
.Add(new Vector3(0,0,0))
|
||||
.Add(new Vector2(0,5))
|
||||
.Add(new Vector4(0,0,0,0))
|
||||
.Add(new Aabb())
|
||||
.Add(new int())
|
||||
.Add(new float())
|
||||
.Add(new Node())
|
||||
.Add(new Node())
|
||||
.Add(new Node2D())
|
||||
.Add("")
|
||||
.Add(new test(){a = 5, b = "gg"})
|
||||
.Build();
|
||||
var registy = new StructuralInstanceManger<object>();
|
||||
var b = registy.Add(a,new Vector2(1,2));
|
||||
var c = registy.Add(a,new Vector2(1,2));
|
||||
var d = registy.Canonicalize(new StructuralBuilder<object>(a).Add(new Vector2(1,2)).Build());
|
||||
var g = new StructuralBuilder<object>(a).Add(Option<int>.Some(5)).Build();
|
||||
var h = new StructuralBuilder<object>(a).Add(Option<int>.Some(5)).BuildMutable();
|
||||
GD.Print(g.Equals(h));
|
||||
GD.Print(ReferenceEquals(b,c));
|
||||
GD.Print(ReferenceEquals(b,d));
|
||||
GD.Print(ComponentType<Vector3>.Id);
|
||||
GD.Print(ComponentType<Vector2>.Id);
|
||||
|
||||
var help = new StructuralBuilder<object>(a)
|
||||
.CombineWith(b, static configure => configure
|
||||
.Bind<Vector2>(static (a, b) => a + b)
|
||||
.Ignore<Vector4>())
|
||||
.Add(Vector2I.Zero)
|
||||
.Build();
|
||||
GD.Print(help.Get<Vector2>().Map(i=>$"{i}").OrDefault("none"));
|
||||
var timer = new Stopwatch();
|
||||
timer.Start();
|
||||
for (int i = 0; i < 100000; i++)
|
||||
{
|
||||
var e = registy.AddOrReplaceAttribute(a,new test());
|
||||
}
|
||||
timer.Stop();
|
||||
GD.Print(timer.Elapsed);
|
||||
timer.Reset();
|
||||
timer.Start();
|
||||
for (int i = 0; i < 100000; i++)
|
||||
{
|
||||
var e = registy.Canonicalize(new StructuralBuilder<object>(a).Add(new test()).Build());
|
||||
}
|
||||
timer.Stop();
|
||||
GD.Print(timer.Elapsed);
|
||||
}
|
||||
}
|
||||
[ComponentOptions(CacheTransitions = true)]
|
||||
record test
|
||||
{
|
||||
public int a;
|
||||
public string b;
|
||||
}
|
||||
1
src/voxelgame/Voxels/ESCThing/TestStructural.cs.uid
Normal file
1
src/voxelgame/Voxels/ESCThing/TestStructural.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://br6rfqtryq0cx
|
||||
BIN
src/voxelgame/Voxels/Registry/.VoxelDefinition.cs.kate-swp
Normal file
BIN
src/voxelgame/Voxels/Registry/.VoxelDefinition.cs.kate-swp
Normal file
Binary file not shown.
21
src/voxelgame/Voxels/Registry/IBitBacking.cs
Normal file
21
src/voxelgame/Voxels/Registry/IBitBacking.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace SJK.Voxels.Registry;
|
||||
|
||||
public interface IBitBacking<TAttribute, TBitBacking>
|
||||
where TAttribute : class
|
||||
where TBitBacking : unmanaged, IBinaryInteger<TBitBacking> {
|
||||
|
||||
static abstract AttributeDescriptor<TAttribute, TBitBacking> AttributeBacking();
|
||||
static abstract int BitLength { get; }
|
||||
TBitBacking GetBits();
|
||||
}
|
||||
public interface IBitBacking<T, TAttribute, TBitBacking> : IBitBacking<TAttribute, TBitBacking>
|
||||
where T : TAttribute, IBitBacking<T, TAttribute, TBitBacking>
|
||||
where TAttribute : class
|
||||
where TBitBacking : unmanaged, IBinaryInteger<TBitBacking>
|
||||
{
|
||||
static abstract T Create(TBitBacking bits);
|
||||
static abstract TBitBacking GetBits(T value);
|
||||
|
||||
}
|
||||
1
src/voxelgame/Voxels/Registry/IBitBacking.cs.uid
Normal file
1
src/voxelgame/Voxels/Registry/IBitBacking.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bemjbjlx8y6q6
|
||||
501
src/voxelgame/Voxels/Registry/VoxelDefinition.cs
Normal file
501
src/voxelgame/Voxels/Registry/VoxelDefinition.cs
Normal file
@@ -0,0 +1,501 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Godot;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SJK.Functional;
|
||||
|
||||
namespace SJK.Voxels.Registry;
|
||||
|
||||
public enum AttributeBacking
|
||||
{
|
||||
Auto, // try bits first, fall back to object if no free bits
|
||||
Bits, // force bit packing
|
||||
Object // force object storage
|
||||
}
|
||||
|
||||
public sealed record AttributeDescriptor<TAttribute,TBitBacking>(
|
||||
AttributeBacking Backing,
|
||||
int BitOffset ,
|
||||
int BitLength
|
||||
|
||||
) where TBitBacking : unmanaged , IBinaryInteger<TBitBacking> ;
|
||||
|
||||
public interface IVoxelDefinition<TAttrribute> where TAttrribute : class
|
||||
{
|
||||
string Name { get; }
|
||||
VoxelInstanceBuilder<TAttrribute> CreateInstance();
|
||||
IVoxelInstance<TAttrribute> CreateDefaultInstance();
|
||||
T GetAttribute<T>(Func<T> ctor) where T : TAttrribute;
|
||||
Option<T> GetAttribute<T>() where T : TAttrribute;
|
||||
}
|
||||
public class VoxelDefinition<TAttrribute> : IVoxelDefinition<TAttrribute>, IEquatable<VoxelDefinition<TAttrribute>> where TAttrribute : class
|
||||
{
|
||||
public VoxelDefinition(string name, IReadOnlyDictionary<Type, TAttrribute> attributes)
|
||||
{
|
||||
Name = name;
|
||||
Attributes = attributes;
|
||||
HashCode = ComputeHashCode();
|
||||
}
|
||||
public readonly int HashCode;
|
||||
public string Name { get; }
|
||||
public IReadOnlyDictionary<Type, TAttrribute> Attributes { get; }
|
||||
|
||||
public T GetAttribute<T>(Func<T> ctor) where T : TAttrribute
|
||||
{
|
||||
return Attributes.TryGetValue(typeof(T), out var v) ? (T)v : ctor();
|
||||
}
|
||||
public Option<T> GetAttribute<T>() where T : TAttrribute
|
||||
{
|
||||
return Attributes.TryGetValue(typeof(T), out var v) ? Option<T>.Some((T)v) : Option<T>.None;
|
||||
}
|
||||
public static VoxelDefinition<TAttrribute> Create(Action<VoxelDefinitionBuilder<TAttrribute>> config)
|
||||
{
|
||||
var builder = new VoxelDefinitionBuilder<TAttrribute>();
|
||||
config(builder);
|
||||
return builder.Build();
|
||||
}
|
||||
public VoxelInstanceBuilder<TAttrribute> CreateInstance()
|
||||
{
|
||||
return new VoxelInstanceBuilder<TAttrribute>(this);
|
||||
}
|
||||
|
||||
public bool Equals(VoxelDefinition<TAttrribute> other)
|
||||
{
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
if (HashCode != other.HashCode)
|
||||
return false;
|
||||
|
||||
var valueCompare = EqualityComparer<TAttrribute>.Default;
|
||||
return Name == other.Name && Attributes.Count == other.Attributes.Count && Attributes.Keys.All(key => other.Attributes.ContainsKey(key) && valueCompare.Equals(Attributes[key], other.Attributes[key]));
|
||||
}
|
||||
private int ComputeHashCode()
|
||||
{
|
||||
HashCode hash = new HashCode();
|
||||
hash.Add(Name.GetHashCode());
|
||||
foreach (var item in Attributes.Values.OrderBy(i=>i.GetType().Name))
|
||||
{
|
||||
hash.Add(item);
|
||||
}
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
public override int GetHashCode() => HashCode;
|
||||
|
||||
public IVoxelInstance<TAttrribute> CreateDefaultInstance() => new VoxelInstance<TAttrribute>(this);
|
||||
}
|
||||
public sealed class VoxelDefinition<TAttrribute, TBitBacking> : VoxelDefinition<TAttrribute>, IEquatable<VoxelDefinition<TAttrribute,TBitBacking>>
|
||||
where TAttrribute : class where TBitBacking : unmanaged, IBinaryInteger<TBitBacking>
|
||||
|
||||
{
|
||||
public readonly IReadOnlyDictionary<Type, AttributeDescriptor<TAttrribute, TBitBacking>> Descriptor;
|
||||
|
||||
public VoxelDefinition(string name, IReadOnlyDictionary<Type, TAttrribute> attributes, IReadOnlyDictionary<Type, AttributeDescriptor<TAttrribute, TBitBacking>> descriptor) : base(name,attributes)
|
||||
{
|
||||
Descriptor = descriptor;
|
||||
}
|
||||
|
||||
// private Dictionary<Type, AttributeDescriptor> _descriptors =descripters;
|
||||
public static VoxelDefinition<TAttrribute, TBitBacking> Create(Action<VoxelDefinitionBuilder<TAttrribute, TBitBacking>> config)
|
||||
{
|
||||
var builder = new VoxelDefinitionBuilder<TAttrribute, TBitBacking>();
|
||||
config(builder);
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
// public T? GetAttribute<T>() where T : TAttrribute => Attributes.TryGetValue(typeof(T), out var v) ? (T)v : default;
|
||||
public Option<T> GetAttribute<T>(TBitBacking data) where T : TAttrribute, IBitBacking<T, TAttrribute, TBitBacking>
|
||||
{
|
||||
if (Descriptor.TryGetValue(typeof(T), out var d))
|
||||
{
|
||||
if (d.Backing == AttributeBacking.Bits)
|
||||
{
|
||||
var attr = T.Create(data >> d.BitOffset & ((TBitBacking.One << d.BitLength) - TBitBacking.One));//WIP
|
||||
return Option<T>.Some(attr);
|
||||
}
|
||||
}
|
||||
return Attributes.TryGetValue(typeof(T), out var v) ? Option<T>.Some((T)v) : Option<T>.None;
|
||||
}
|
||||
|
||||
// public VoxelHandle SetAttributeData<T>(int data, T value) where T : TAttrribute, IBitBacking<T, TAttrribute,TBitBacking>
|
||||
// {
|
||||
// // return data | (T.GetBits(value) << );
|
||||
// throw new NotImplementedException();//TODO Createnew instance if needed or somthign
|
||||
// }
|
||||
|
||||
public IEnumerable<TInterface> GetAttributesOfType<TInterface>()
|
||||
{
|
||||
foreach (var attribute in Attributes)
|
||||
{
|
||||
if (attribute.Value is TInterface @interface)
|
||||
{
|
||||
yield return @interface;
|
||||
}
|
||||
}
|
||||
}
|
||||
public TBitBacking ComputeBits(TBitBacking existingBits) => ComputeBits(existingBits, Attributes);
|
||||
public TBitBacking ComputeBits(TBitBacking existingBits, IEnumerable<KeyValuePair<Type,TAttrribute>> keyValuePairs)
|
||||
{
|
||||
TBitBacking result = existingBits;
|
||||
foreach (var item in keyValuePairs.Where(kvp => Descriptor.ContainsKey(kvp.Key)))//TODO Cache Descripto so not to hash twice
|
||||
{
|
||||
if (item is IBitBacking<TAttrribute, TBitBacking> bitBacking)
|
||||
{
|
||||
Descriptor.TryGetValue(item.Key, out var descriptor);
|
||||
var bits = bitBacking.GetBits();
|
||||
result &= ~(((TBitBacking.One << descriptor.BitLength) - TBitBacking.One) << descriptor.BitOffset);//RemoveMask may not be needed
|
||||
result |= bits << descriptor.BitOffset;
|
||||
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
public VoxelInstanceBuilder<TAttrribute, TBitBacking> CreateBitBuilderInstance()
|
||||
{
|
||||
return new VoxelInstanceBuilder<TAttrribute, TBitBacking>(this);
|
||||
}
|
||||
|
||||
public bool Equals(VoxelDefinition<TAttrribute, TBitBacking> other)
|
||||
{
|
||||
var valueCompare = EqualityComparer<AttributeDescriptor<TAttrribute,TBitBacking>>.Default;
|
||||
return base.Equals((VoxelDefinition<TAttrribute>)other) && Descriptor.Count == other.Descriptor.Count && Descriptor.Keys.All(key => other.Descriptor.ContainsKey(key) && valueCompare.Equals(Descriptor[key], other.Descriptor[key]));
|
||||
}
|
||||
}
|
||||
public class VoxelDefinitionConverter<THandle,TLevel,TAttribute, TBitBacking> : JsonConverter<VoxelDefinition<TAttribute, TBitBacking>> where TAttribute : class where TBitBacking : unmanaged , IBinaryInteger<TBitBacking> where THandle : IVoxelHandle<TLevel>
|
||||
{
|
||||
private const string Type = "Type";
|
||||
private const string Data = "Data";
|
||||
public override VoxelDefinition<TAttribute, TBitBacking> ReadJson(JsonReader reader, Type objectType, VoxelDefinition<TAttribute, TBitBacking> existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
var deffId = jObj[nameof(VoxelDefinition<TAttribute, TBitBacking>.Name)].ToString();
|
||||
|
||||
var DescriptorToken = jObj[nameof(VoxelDefinition<TAttribute, TBitBacking>.Descriptor)];
|
||||
var descriptorDict = new Dictionary<Type, AttributeDescriptor<TAttribute,TBitBacking>>();
|
||||
if (DescriptorToken is JArray descrorArray) {
|
||||
foreach (var item in descrorArray)
|
||||
{
|
||||
var typeName = item[Type].ToString();
|
||||
var type = System.Type.GetType(typeName, throwOnError: true);
|
||||
var dataToken = item[Data];
|
||||
var descroterr = (AttributeDescriptor<TAttribute,TBitBacking>)serializer.Deserialize(dataToken.CreateReader(), type);
|
||||
descriptorDict.Add(type, descroterr);
|
||||
}
|
||||
}
|
||||
var attributesToken = jObj[nameof(VoxelDefinition<TAttribute, TBitBacking>.Attributes)];
|
||||
var attrDict = new Dictionary<Type, TAttribute>();
|
||||
if (attributesToken is JArray attrArray) {
|
||||
foreach (var item in attrArray)
|
||||
{
|
||||
var typeName = item[Type].ToString();
|
||||
var type = System.Type.GetType(typeName, throwOnError: true);
|
||||
var dataToken = item[Data];
|
||||
var attr = (TAttribute)serializer.Deserialize(dataToken.CreateReader(), type);
|
||||
attrDict.Add(type, attr);
|
||||
}
|
||||
}
|
||||
|
||||
return new VoxelDefinition<TAttribute, TBitBacking>(deffId, attrDict,descriptorDict);
|
||||
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, VoxelDefinition<TAttribute, TBitBacking> value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName(nameof(VoxelDefinition<TAttribute, TBitBacking>.Name));
|
||||
writer.WriteValue(value.Name);
|
||||
if (value.Descriptor.Count > 0)
|
||||
{
|
||||
writer.WritePropertyName(nameof(VoxelDefinition<TAttribute, TBitBacking>.Descriptor));
|
||||
writer.WriteStartArray();
|
||||
foreach (var kvp in value.Descriptor)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName(Type);
|
||||
writer.WriteValue(kvp.Key.FullName);//AssemblyQualifiedName
|
||||
writer.WritePropertyName(Data);
|
||||
serializer.Serialize(writer, kvp.Value);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
if (value.Attributes.Count > 0)
|
||||
{
|
||||
writer.WritePropertyName(nameof(VoxelDefinition<TAttribute, TBitBacking>.Attributes));
|
||||
writer.WriteStartArray();
|
||||
foreach (var kvp in value.Attributes)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName(Type);
|
||||
writer.WriteValue(kvp.Key.FullName);//AssemblyQualifiedName
|
||||
writer.WritePropertyName(Data);
|
||||
serializer.Serialize(writer, kvp.Value);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class VoxelInstanceConverter<THandle,TLevel,TAttribute, TBitBacking> : JsonConverter<VoxelInstance<TAttribute, TBitBacking>> where TAttribute : class where TBitBacking : unmanaged , IBinaryInteger<TBitBacking> where THandle : IVoxelHandle<TLevel>
|
||||
{
|
||||
private const string Type = "Type";
|
||||
private const string Data = "Data";
|
||||
private VoxelPalette<THandle, TLevel, VoxelDefinition<TAttribute, TBitBacking>> _registery;
|
||||
public VoxelInstanceConverter(VoxelPalette<THandle, TLevel, VoxelDefinition<TAttribute, TBitBacking>> registery) {
|
||||
_registery = registery;
|
||||
}
|
||||
public override VoxelInstance<TAttribute, TBitBacking> ReadJson(JsonReader reader, Type objectType, VoxelInstance<TAttribute, TBitBacking> existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
var deffId = jObj[nameof(VoxelInstance<TAttribute, TBitBacking>.Definition)].ToString();
|
||||
var definition = _registery.Get(item => item.Name == deffId);
|
||||
if (!definition.HasValue)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
var childSerilazers = new JsonSerializerSettings()
|
||||
{
|
||||
Converters = serializer.Converters
|
||||
};
|
||||
var converter = JsonSerializer.Create(childSerilazers);
|
||||
jObj["test"].ToObject<VoxelInstance<TAttribute>>(converter);
|
||||
|
||||
var attributesToken = jObj[nameof(VoxelInstance<TAttribute, TBitBacking>.Attributes)];
|
||||
var dict = new Dictionary<Type, TAttribute>();
|
||||
if (attributesToken is JArray attrArray) {
|
||||
foreach (var item in attrArray)
|
||||
{
|
||||
var typeName = item[Type].ToString();
|
||||
var type = System.Type.GetType(typeName, throwOnError: true);
|
||||
var dataToken = item[Data];
|
||||
var attr = (TAttribute)serializer.Deserialize(dataToken.CreateReader(), type);
|
||||
dict.Add(type, attr);
|
||||
}
|
||||
}
|
||||
|
||||
return new(definition.Value, dict.Count > 0 ? dict.ToOption() : None<Dictionary<Type, TAttribute>>.Of());
|
||||
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, VoxelInstance<TAttribute, TBitBacking> value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName(nameof(VoxelInstance<TAttribute, TBitBacking>.Definition));
|
||||
writer.WriteValue(value.Definition.Name);
|
||||
if (value.Attributes.HasValue(out var attrbutes) && attrbutes.Count >0)
|
||||
{
|
||||
writer.WritePropertyName(nameof(VoxelInstance<TAttribute, TBitBacking>.Attributes));
|
||||
writer.WriteStartArray();
|
||||
foreach (var kvp in attrbutes)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName(Type);
|
||||
writer.WriteValue(kvp.Key.FullName);//AssemblyQualifiedName
|
||||
writer.WritePropertyName(Data);
|
||||
serializer.Serialize(writer, kvp.Value);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DefinitionLevel { private DefinitionLevel() { } }
|
||||
public sealed class PaletteLevel { private PaletteLevel() { } }
|
||||
public interface IVoxelAttribute { } //Test
|
||||
public readonly record struct IsSoild(bool Solid) : IVoxelAttribute ;
|
||||
public record TextureCube(Color Color,int index) : IVoxelAttribute ;
|
||||
public record Hardness(byte Value) : IVoxelAttribute, IBitBacking<Hardness, IVoxelAttribute,int>
|
||||
{
|
||||
public static int BitLength => 8;
|
||||
|
||||
public static AttributeDescriptor<IVoxelAttribute,int> AttributeBacking()
|
||||
{
|
||||
return new AttributeDescriptor<IVoxelAttribute,int>(Voxels.Registry.AttributeBacking.Bits,BitOffset: 0, BitLength: BitLength);
|
||||
}
|
||||
|
||||
public static Hardness Create(int bits)
|
||||
{
|
||||
return new Hardness((byte)(bits & byte.MaxValue));
|
||||
}
|
||||
|
||||
public static int GetBits(Hardness value)
|
||||
{
|
||||
return value.Value & byte.MaxValue;
|
||||
}
|
||||
|
||||
int IBitBacking<IVoxelAttribute, int>.GetBits()
|
||||
{
|
||||
return GetBits(this);
|
||||
}
|
||||
}
|
||||
// public class Registry
|
||||
// {
|
||||
// private List<VoxelDefinition<IVoxelAttribute, int>> voxelInstances = new();
|
||||
// public VoxelDefinition<IVoxelAttribute, int> GetVoxel(VoxelHandle<DefinitionLevel> voxelHandle)
|
||||
// {
|
||||
// return voxelInstances[voxelHandle.Index];
|
||||
// }
|
||||
// public VoxelHandle<DefinitionLevel> Register(VoxelDefinition<IVoxelAttribute, int> voxelDefinition)
|
||||
// {
|
||||
// var handle = new VoxelHandle<DefinitionLevel>(voxelInstances.Count);
|
||||
// voxelInstances.Add(voxelDefinition);
|
||||
// return handle;
|
||||
// }
|
||||
|
||||
// }
|
||||
public sealed class VoxelPalette<THandle, TLevel, TEntry> : Palette<TEntry> where THandle : IVoxelHandle<TLevel> where TEntry : IEquatable<TEntry>
|
||||
{
|
||||
|
||||
private readonly Func<int, TEntry, THandle> ctor;
|
||||
|
||||
public TEntry Get(THandle handle)
|
||||
{
|
||||
if (handle.Index < 0)
|
||||
throw new Exception();
|
||||
return Get(handle.Index);
|
||||
}
|
||||
public VoxelPalette(Func<int, TEntry, THandle> ctor)
|
||||
{
|
||||
this.ctor = ctor;
|
||||
}
|
||||
public THandle GetOrAddEntry(TEntry entry) => ctor(GetOrAddIndex(entry), entry);
|
||||
|
||||
public THandle ModifyHandle(THandle handle, Func<TEntry, TEntry> modify)
|
||||
{
|
||||
var oldEntry = Get(handle);
|
||||
var newEntry = modify(oldEntry);
|
||||
if (oldEntry.Equals(newEntry))
|
||||
{
|
||||
return handle;
|
||||
}
|
||||
return GetOrAddEntry(newEntry);
|
||||
}
|
||||
}
|
||||
public sealed class VoxelRegistry<TEntry, TVoxelDeff, THandle,TAttrubuite> : IVoxelRegistry<THandle, TEntry,TVoxelDeff,TAttrubuite> where TEntry : IVoxelInstance<TAttrubuite> where TAttrubuite : class where THandle : IVoxelHandle<DefinitionLevel> where TVoxelDeff : IVoxelDefinition<TAttrubuite> ,IEquatable<TVoxelDeff>
|
||||
{
|
||||
private VoxelPalette<THandle,DefinitionLevel, TVoxelDeff> voxelPalette;
|
||||
public VoxelRegistry(VoxelPalette<THandle, DefinitionLevel, TVoxelDeff> voxelPalette)
|
||||
{
|
||||
this.voxelPalette = voxelPalette;
|
||||
}
|
||||
|
||||
public TEntry Get(THandle handle) => (TEntry)voxelPalette.Get(handle).CreateDefaultInstance();
|
||||
|
||||
public TVoxelDeff GetDeff(THandle handle) => voxelPalette.Get(handle);
|
||||
public THandle GetOrAddEntry(TVoxelDeff entry)
|
||||
{
|
||||
return voxelPalette.GetOrAddEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public class VoxelPaletteJsonConverter<THandle, TLevel, TEntry> : JsonConverter
|
||||
where THandle : IVoxelHandle<TLevel>
|
||||
where TEntry : IEquatable<TEntry>
|
||||
{
|
||||
private readonly Func<int, TEntry, THandle> _ctor;
|
||||
|
||||
public VoxelPaletteJsonConverter(Func<int, TEntry, THandle> ctor)
|
||||
{
|
||||
_ctor = ctor;
|
||||
}
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
=> objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(VoxelPalette<,,>);
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var palette = (VoxelPalette<THandle, TLevel, TEntry>)value!;
|
||||
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("entries");
|
||||
writer.WriteStartArray();
|
||||
for (int i = 0; i < palette.Count; i++)
|
||||
{
|
||||
serializer.Serialize(writer, palette.Get(i));
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
// writer.WritePropertyName("count");
|
||||
// writer.WriteValue(palette.Count);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jObj = JObject.Load(reader);
|
||||
var entries = jObj["entries"]?.ToObject<List<TEntry>>(serializer) ?? new List<TEntry>();
|
||||
|
||||
var palette = new VoxelPalette<THandle, TLevel, TEntry>(_ctor);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
palette.GetOrAddEntry(entry);
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PaletteExtensions
|
||||
{
|
||||
public static THandle ModifyVoxelHandle<THandle,TLevel,TAttribute, TBitBacking>(
|
||||
this VoxelPalette<THandle,TLevel,VoxelInstance<TAttribute,TBitBacking>> palette,
|
||||
THandle handle,
|
||||
Func<VoxelInstance<TAttribute,TBitBacking>, (VoxelInstance<TAttribute,TBitBacking> entry, TBitBacking newBits)> modify)
|
||||
where THandle : IVoxelHandle<TLevel>,IVoxelHandle<TLevel, TBitBacking>
|
||||
where TBitBacking : unmanaged, IBinaryInteger<TBitBacking>
|
||||
where TAttribute : class
|
||||
{
|
||||
TBitBacking data = handle.Data;
|
||||
var newHandle = palette.ModifyHandle(handle, (old) =>
|
||||
{
|
||||
var result = modify(old);
|
||||
data = result.newBits;
|
||||
return result.entry;
|
||||
});
|
||||
return (THandle)newHandle.WithData(data);
|
||||
}
|
||||
}
|
||||
// public class test
|
||||
// {
|
||||
|
||||
// public void test2()
|
||||
// {
|
||||
// var deff = VoxelDefinition<IVoxelAttribute>.Create(builder =>
|
||||
// {
|
||||
// builder.Name = "Stone";
|
||||
// builder.WithAttribute(new Hardness(1));
|
||||
// });
|
||||
// var deff2 = deff.WithVariant(config =>
|
||||
// {
|
||||
// config.Name = "SandStone";
|
||||
// config.WithAttribute(new Hardness(2));
|
||||
|
||||
// });
|
||||
|
||||
// var block = deff.CreateInstance().Set(new Hardness(3)).Build();
|
||||
// var block2 = deff2.CreateInstance().Modfiy<Hardness>(existing => new Hardness((byte)(existing.Value + 1))).Build();
|
||||
// // block.SetAttribute(new Hardness(3));ndel.Data = Hardness.GetBits(new Hardness(4));
|
||||
|
||||
// //With Bits
|
||||
// var deff3 = VoxelDefinition<IVoxelAttribute, int>.Create(builder =>
|
||||
// {
|
||||
// builder.Name = "Grass";
|
||||
// builder.WithAttribute(new Hardness(3), Hardness.AttributeBacking() with { BitOffset = 0, Backing = AttributeBacking.Bits });
|
||||
// builder.WithAttribute(new IsSoild(false));
|
||||
// });
|
||||
// var block3 = deff3.CreateInstance().Build();
|
||||
// // if (block3.GetAttribute<Hardness>(handel.Data).HasValue && block3.GetAttribute<Hardness>(handel.Data).Value == new Hardness(5))
|
||||
// // {
|
||||
// // }
|
||||
|
||||
// }
|
||||
// }V<TAttribute>();
|
||||
1
src/voxelgame/Voxels/Registry/VoxelDefinition.cs.uid
Normal file
1
src/voxelgame/Voxels/Registry/VoxelDefinition.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://tphqa7lth1n1
|
||||
64
src/voxelgame/Voxels/Registry/VoxelDefinitionBuilder.cs
Normal file
64
src/voxelgame/Voxels/Registry/VoxelDefinitionBuilder.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SJK.Voxels.Registry;
|
||||
|
||||
public class VoxelDefinitionBuilder<TAttrribute> where TAttrribute : class
|
||||
{
|
||||
public string Name { get; set; }
|
||||
protected readonly Dictionary<Type, TAttrribute> Attributes = new();
|
||||
public VoxelDefinitionBuilder<TAttrribute> WithAttribute<T>(T attr) where T : TAttrribute
|
||||
{
|
||||
Attributes[typeof(T)] = attr;
|
||||
// _descriptors[typeof(T)] = attributeDescriptor;
|
||||
return this;
|
||||
}
|
||||
public virtual VoxelDefinition<TAttrribute> Build()
|
||||
{
|
||||
return new VoxelDefinition<TAttrribute>(Name, Attributes);
|
||||
}
|
||||
|
||||
}
|
||||
public class VoxelDefinitionBuilder<TAttrribute,TBitBacking> : VoxelDefinitionBuilder<TAttrribute> where TAttrribute : class where TBitBacking : unmanaged, IBinaryInteger<TBitBacking>{
|
||||
private readonly Dictionary<Type, AttributeDescriptor<TAttrribute,TBitBacking>> _descriptor = new();
|
||||
// private Dictionary<Type, VoxelDefinition<TAttrribute>.AttributeDescriptor> _descriptors = new();
|
||||
public VoxelDefinitionBuilder<TAttrribute, TBitBacking> WithAttribute<T>(T attr, AttributeDescriptor<TAttrribute, TBitBacking> attributeDescriptor) where T : TAttrribute
|
||||
{
|
||||
WithAttribute(attr);
|
||||
_descriptor[typeof(T)] = attributeDescriptor;
|
||||
if (!isValidOverlap(attributeDescriptor, _descriptor.Values))
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
// _descriptors[typeof(T)] = attributeDescriptor;
|
||||
return this;
|
||||
}
|
||||
private bool isValidOverlap(AttributeDescriptor<TAttrribute, TBitBacking> a, IEnumerable<AttributeDescriptor<TAttrribute, TBitBacking>> others)
|
||||
{
|
||||
if (a.Backing != AttributeBacking.Bits)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
int newstart = a.BitOffset;
|
||||
int newend = a.BitOffset + a.BitOffset - 1;
|
||||
foreach (var d in others)
|
||||
{
|
||||
if (d.Backing != AttributeBacking.Bits)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int start = d.BitOffset;
|
||||
int end = start + d.BitLength - 1;
|
||||
if (!(newend < start || newstart > end))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public override VoxelDefinition<TAttrribute,TBitBacking> Build()
|
||||
{
|
||||
return new VoxelDefinition<TAttrribute,TBitBacking>(Name, Attributes,_descriptor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bgd33blsocwb6
|
||||
38
src/voxelgame/Voxels/Registry/VoxelHandle.cs
Normal file
38
src/voxelgame/Voxels/Registry/VoxelHandle.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SJK.Voxels.Registry;
|
||||
|
||||
public readonly struct VoxelHandle<TLevel>(int index) : IVoxelHandle<TLevel>, IEquatable<VoxelHandle<TLevel>>
|
||||
{
|
||||
public readonly int Index { get; } = index;
|
||||
|
||||
public bool Equals(IVoxelHandle<TLevel> other) => other is VoxelHandle<TLevel> otherHandle && Equals(otherHandle);
|
||||
public bool Equals(VoxelHandle<TLevel> other) => Index == other.Index;
|
||||
}
|
||||
|
||||
public interface IVoxelHandle<TLevel> : IEquatable<IVoxelHandle<TLevel>> {
|
||||
int Index { get; }
|
||||
}
|
||||
public interface IVoxelHandle<TLevel, TBitBacking> : IEquatable<IVoxelHandle<TLevel, TBitBacking>> {
|
||||
TBitBacking Data { get; }
|
||||
IVoxelHandle<TLevel, TBitBacking> WithData(TBitBacking bitBacking);
|
||||
}
|
||||
public readonly struct VoxelHandle<TLevel, TBitBacking>(int index, TBitBacking data) : IVoxelHandle<TLevel>,IVoxelHandle<TLevel,TBitBacking>, IEquatable<VoxelHandle<TLevel, TBitBacking>> where TBitBacking : unmanaged, IBinaryInteger<TBitBacking>
|
||||
{
|
||||
public readonly int Index { get; } = index;
|
||||
|
||||
|
||||
public readonly TBitBacking Data { get; } = data;
|
||||
|
||||
|
||||
public bool Equals(IVoxelHandle<TLevel> other) => other is VoxelHandle<TLevel, TBitBacking> otherHandle && Equals(otherHandle);
|
||||
|
||||
public bool Equals(VoxelHandle<TLevel, TBitBacking> other) => Index == other.Index && Data == other.Data;
|
||||
|
||||
public bool Equals(IVoxelHandle<TLevel, TBitBacking> other) => other is VoxelHandle<TLevel, TBitBacking> other2 && Equals(other2);
|
||||
public VoxelHandle<TLevel, TBitBacking> WithData(TBitBacking bitBacking) => new VoxelHandle<TLevel, TBitBacking>(Index, bitBacking);
|
||||
|
||||
IVoxelHandle<TLevel, TBitBacking> IVoxelHandle<TLevel, TBitBacking>.WithData(TBitBacking bitBacking) => WithData(bitBacking);
|
||||
|
||||
}
|
||||
1
src/voxelgame/Voxels/Registry/VoxelHandle.cs.uid
Normal file
1
src/voxelgame/Voxels/Registry/VoxelHandle.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://4w8vs8tnuopn
|
||||
229
src/voxelgame/Voxels/Registry/VoxelInstance.cs
Normal file
229
src/voxelgame/Voxels/Registry/VoxelInstance.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Godot;
|
||||
using SJK.Functional;
|
||||
|
||||
namespace SJK.Voxels.Registry;
|
||||
|
||||
public interface IVoxelInstance<TAttribute> where TAttribute : class
|
||||
{
|
||||
|
||||
public IVoxelDefinition<TAttribute> Definition { get; }
|
||||
|
||||
}
|
||||
public sealed class VoxelInstance<TAttribute> : IVoxelInstance<TAttribute> , IEquatable<VoxelInstance<TAttribute>> where TAttribute : class
|
||||
{
|
||||
public IVoxelDefinition<TAttribute> Definition { get; }
|
||||
public readonly IOption<Dictionary<Type, TAttribute>> Attributes;
|
||||
public readonly int HashCode;
|
||||
public VoxelInstance(IVoxelDefinition<TAttribute> definition, IOption<Dictionary<Type, TAttribute>> attributes)
|
||||
{
|
||||
Definition = definition;
|
||||
Attributes = attributes;
|
||||
HashCode = ComputeHashCode();
|
||||
}
|
||||
public VoxelInstance(VoxelDefinition<TAttribute> definition, Dictionary<Type, TAttribute>? attributes = null)
|
||||
{
|
||||
Definition = definition;
|
||||
Attributes = attributes.ToOption();
|
||||
HashCode = ComputeHashCode();
|
||||
}
|
||||
|
||||
private int ComputeHashCode()
|
||||
{
|
||||
HashCode hash = new HashCode();
|
||||
hash.Add(Definition.GetHashCode());
|
||||
Attributes.IfSome(attributes => attributes.Values.Order().ToList().ForEach(hash.Add));
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
// public Option<T> GetAttribute<T>(int data) where T : TAttribute, IBitBacking<T,TAttribute,> =>
|
||||
// Attributes.HasValue(out var att) && att.TryGetValue(typeof(T), out var v)
|
||||
// ? Option<T>.Some((T)v)
|
||||
// : Definition.GetAttribute<T>(data);
|
||||
public T GetAttribute<T>(Func<T> ctor) where T : TAttribute =>
|
||||
Attributes.HasValue(out var att) && att.TryGetValue(typeof(T), out var v)
|
||||
? (T)v
|
||||
: Definition.GetAttribute<T>(ctor);
|
||||
public IEnumerable<TInterface> GetAttributesOfType<TInterface>()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
// var set = new HashSet<Type>();
|
||||
// if (Attributes.HasValue(out var attributes))
|
||||
// {
|
||||
// foreach (var item in attributes)
|
||||
// {
|
||||
// if (item.Value is TInterface @interface)
|
||||
// {
|
||||
// set.Add(item.Key);
|
||||
// yield return @interface;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// foreach (var attributeKVP in Definition.Attributes)
|
||||
// {
|
||||
// if (attributeKVP.Value is not TInterface @interface || set.Contains(attributeKVP.Key))
|
||||
// {
|
||||
// continue;
|
||||
// }
|
||||
// yield return @interface;
|
||||
|
||||
// }
|
||||
}
|
||||
/// <summary>
|
||||
/// Creates a builder initialized with this instance’s attributes.
|
||||
/// </summary>
|
||||
public VoxelInstanceBuilder<TAttribute> ToBuilder() =>
|
||||
new VoxelInstanceBuilder<TAttribute>(this);
|
||||
public override bool Equals(object obj) => obj is VoxelInstance<TAttribute> other && Equals(other);
|
||||
public bool Equals(VoxelInstance<TAttribute> other) {
|
||||
|
||||
if (HashCode != other.HashCode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!Definition.Equals(other.Definition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!Attributes.HasValue() && !other.Attributes.HasValue())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!Attributes.HasValue(out var a) || !other.Attributes.HasValue(out var b))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (a.Count != b.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var valueCompare = EqualityComparer<TAttribute>.Default;
|
||||
return a.Keys.All(key => b.ContainsKey(key) && valueCompare.Equals(a[key], b[key]));
|
||||
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode;
|
||||
// {
|
||||
// var hash = Definition.GetHashCode();
|
||||
// Attributes.IfSome(some =>
|
||||
// {
|
||||
// foreach (var kvp in some.OrderBy(k => k.Key.FullName))
|
||||
// {
|
||||
// hash = HashCode.Combine(hash, kvp.Key.GetHashCode(), kvp.Value.GetHashCode());
|
||||
// }
|
||||
// });
|
||||
// return hash;
|
||||
// }
|
||||
}
|
||||
public sealed class VoxelInstance<TAttribute,TBitBacking>: IVoxelInstance<TAttribute>, IEquatable<VoxelInstance<TAttribute,TBitBacking>> where TAttribute : class where TBitBacking : unmanaged, IBinaryInteger<TBitBacking>
|
||||
{
|
||||
public VoxelDefinition<TAttribute,TBitBacking> Definition { get; }
|
||||
|
||||
IVoxelDefinition<TAttribute> IVoxelInstance<TAttribute>.Definition => Definition;
|
||||
|
||||
public readonly IOption<Dictionary<Type, TAttribute>> Attributes;
|
||||
public readonly int HashCode;
|
||||
public VoxelInstance(VoxelDefinition<TAttribute, TBitBacking> definition, IOption<Dictionary<Type, TAttribute>> attributes)
|
||||
{
|
||||
Definition = definition;
|
||||
Attributes = attributes;
|
||||
HashCode = ComputeHashCode();
|
||||
}
|
||||
|
||||
private int ComputeHashCode()
|
||||
{
|
||||
HashCode hash = new HashCode();
|
||||
hash.Add(Definition.GetHashCode());
|
||||
Attributes.IfSome(attributes => attributes.Values.Order().ToList().ForEach(hash.Add));
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
public Option<T> GetAttribute<T>(TBitBacking data) where T : TAttribute, IBitBacking<T,TAttribute,TBitBacking> =>
|
||||
Attributes.HasValue(out var att) && att.TryGetValue(typeof(T), out var v)
|
||||
? Option<T>.Some((T)v)
|
||||
: Definition.GetAttribute<T>(data);
|
||||
/// <summary>
|
||||
/// Does not respect Data, only based of Deffination
|
||||
/// </summary>
|
||||
/// <typeparam name="TInterface"></typeparam>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<TInterface> GetAttributesOfType<TInterface>()
|
||||
{
|
||||
var set = new HashSet<Type>();
|
||||
if (Attributes.HasValue(out var attributes))
|
||||
{
|
||||
foreach (var item in attributes)
|
||||
{
|
||||
if (item.Value is TInterface @interface)
|
||||
{
|
||||
yield return @interface;
|
||||
set.Add(item.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var attributeKVP in Definition.Attributes)
|
||||
{
|
||||
if (attributeKVP.Value is not TInterface @interface || set.Contains(attributeKVP.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
yield return @interface;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
public TBitBacking ComputeBits(TBitBacking existingBits) =>Attributes.Match(some =>Definition.ComputeBits(Definition.ComputeBits(existingBits),some),()=>Definition.ComputeBits(existingBits));
|
||||
/// <summary>
|
||||
/// Creates a builder initialized with this instance’s attributes.
|
||||
/// </summary>
|
||||
public VoxelInstanceBuilder<TAttribute, TBitBacking> ToBuilder(TBitBacking existingBits) =>//<THandle,TLevel>() where THandle : IVoxelHandle<TLevel,TBitBacking> =>
|
||||
new VoxelInstanceBuilder<TAttribute, TBitBacking>(this,existingBits);
|
||||
|
||||
public bool Equals(VoxelInstance<TAttribute, TBitBacking> other)
|
||||
{
|
||||
if (HashCode != other.HashCode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!Definition.Equals(other.Definition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!Attributes.HasValue() && !other.Attributes.HasValue())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!Attributes.HasValue(out var a) || !other.Attributes.HasValue(out var b))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (a.Count != b.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var valueCompare = EqualityComparer<TAttribute>.Default;
|
||||
return a.Keys.All(key => b.ContainsKey(key) && valueCompare.Equals(a[key], b[key]));
|
||||
}
|
||||
public override bool Equals(object obj) => obj is VoxelInstance<TAttribute, TBitBacking> other && Equals(other);
|
||||
public override int GetHashCode() => HashCode;
|
||||
// {
|
||||
// var hash = Definition.GetHashCode();
|
||||
// Attributes.IfSome(some =>
|
||||
// {
|
||||
// foreach (var kvp in some.OrderBy(k => k.Key.FullName))
|
||||
// {
|
||||
// hash = HashCode.Combine(hash, kvp.Key.GetHashCode(), kvp.Value.GetHashCode());
|
||||
// }
|
||||
// });
|
||||
// return hash;
|
||||
// }
|
||||
}
|
||||
1
src/voxelgame/Voxels/Registry/VoxelInstance.cs.uid
Normal file
1
src/voxelgame/Voxels/Registry/VoxelInstance.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bes4tnuxpkexq
|
||||
125
src/voxelgame/Voxels/Registry/VoxelInstanceBuilder.cs
Normal file
125
src/voxelgame/Voxels/Registry/VoxelInstanceBuilder.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using SJK.Functional;
|
||||
|
||||
namespace SJK.Voxels.Registry;
|
||||
|
||||
/// <summary>
|
||||
/// Builder for creating modified VoxelInstances without mutating the original.
|
||||
/// </summary>
|
||||
public sealed class VoxelInstanceBuilder<TAttribute> where TAttribute : class
|
||||
{
|
||||
private readonly IVoxelDefinition<TAttribute> _definition;
|
||||
private Dictionary<Type, TAttribute> _attributes;
|
||||
|
||||
public VoxelInstanceBuilder(VoxelInstance<TAttribute> source)
|
||||
{
|
||||
_definition = source.Definition;
|
||||
// Copy only if source had attributes
|
||||
_attributes = new Dictionary<Type, TAttribute>(source.Attributes.Or([]));
|
||||
}
|
||||
public VoxelInstanceBuilder(VoxelDefinition<TAttribute> source)
|
||||
{
|
||||
_definition = source;
|
||||
_attributes = [];
|
||||
}
|
||||
|
||||
public VoxelInstanceBuilder<TAttribute> Set<T>(T attr) where T : TAttribute
|
||||
{
|
||||
|
||||
if (attr.Equals(_definition.GetAttribute<T>()))
|
||||
{
|
||||
Remove<T>();
|
||||
}
|
||||
else
|
||||
{
|
||||
_attributes[typeof(T)] = attr;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public VoxelInstanceBuilder<TAttribute> Modfiy<T>(Func<T, T> map) where T : TAttribute
|
||||
{
|
||||
if (_attributes.TryGetValue(typeof(T), out var attribute))
|
||||
{
|
||||
var newAttr = map((T)attribute);
|
||||
Set(newAttr);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public VoxelInstanceBuilder<TAttribute> Remove<T>() where T : TAttribute
|
||||
{
|
||||
_attributes.Remove(typeof(T));
|
||||
return this;
|
||||
}
|
||||
|
||||
public VoxelInstance<TAttribute> Build() =>
|
||||
new VoxelInstance<TAttribute>(_definition, _attributes.ToOption());
|
||||
}
|
||||
public sealed class VoxelInstanceBuilder<TAttribute,TBitBacking> where TAttribute : class where TBitBacking : unmanaged,IBinaryInteger<TBitBacking>
|
||||
{
|
||||
private readonly VoxelDefinition<TAttribute,TBitBacking> _definition;
|
||||
private readonly Dictionary<Type, TAttribute> _attributes;
|
||||
private readonly TBitBacking existingBits;
|
||||
public TBitBacking NewBits;
|
||||
|
||||
public VoxelInstanceBuilder(VoxelInstance<TAttribute, TBitBacking> source, TBitBacking existingData)
|
||||
{
|
||||
|
||||
_definition = source.Definition;
|
||||
// Copy only if source had attributes
|
||||
_attributes = new Dictionary<Type, TAttribute>(source.Attributes.Or([]));
|
||||
this.existingBits = existingData;
|
||||
this.NewBits = existingBits;
|
||||
}
|
||||
public VoxelInstanceBuilder(VoxelDefinition<TAttribute,TBitBacking> source)
|
||||
{
|
||||
_definition = source;
|
||||
_attributes = [];
|
||||
}
|
||||
|
||||
public VoxelInstanceBuilder<TAttribute,TBitBacking> Set<T>(T attr) where T : TAttribute
|
||||
{
|
||||
if (_definition.Descriptor.TryGetValue(typeof(T), out var descriptor)
|
||||
&& descriptor.Backing == AttributeBacking.Bits
|
||||
&& attr is IBitBacking<TAttribute, TBitBacking> bitBacking)
|
||||
{
|
||||
var bits = bitBacking.GetBits();
|
||||
NewBits &= ~(((TBitBacking.One << descriptor.BitLength) - TBitBacking.One) << descriptor.BitOffset);//RemoveMask may not be needed
|
||||
NewBits |= bits << descriptor.BitOffset;
|
||||
return this;
|
||||
}
|
||||
if (attr.Equals(_definition.GetAttribute<T>()))
|
||||
{
|
||||
Remove<T>();
|
||||
}
|
||||
else
|
||||
{
|
||||
_attributes[typeof(T)] = attr;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public VoxelInstanceBuilder<TAttribute,TBitBacking> Modfiy<T>(Func<T, T> map) where T : TAttribute
|
||||
{
|
||||
if (_attributes.TryGetValue(typeof(T), out var attribute))
|
||||
{
|
||||
var newAttr = map((T)attribute);
|
||||
Set(newAttr);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public VoxelInstanceBuilder<TAttribute,TBitBacking> Remove<T>() where T : TAttribute
|
||||
{
|
||||
_attributes.Remove(typeof(T));
|
||||
return this;
|
||||
}
|
||||
// [Export]
|
||||
|
||||
public VoxelInstance<TAttribute, TBitBacking> Build(out TBitBacking newBits)
|
||||
{
|
||||
newBits = NewBits;
|
||||
return new VoxelInstance<TAttribute, TBitBacking>(_definition, _attributes.ToOption());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b3bek2f2qoo8l
|
||||
386
src/voxelgame/Voxels/SpatialTree.cs
Normal file
386
src/voxelgame/Voxels/SpatialTree.cs
Normal file
@@ -0,0 +1,386 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Godot;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using SqlKata;
|
||||
using SqlKata.Compilers;
|
||||
namespace SJK.Voxels;
|
||||
public interface ISpatialTree<T>// where T : ISpatialEntry
|
||||
{
|
||||
void Insert(T entry);
|
||||
void Remove(T entry);
|
||||
IEnumerable<T> Query(Aabb region);
|
||||
}
|
||||
public interface ISpatialEntry
|
||||
{
|
||||
Guid Guid { get; }
|
||||
Aabb Bounds { get; }
|
||||
}
|
||||
public sealed class SpatialDataLayer<TEntry> : ISpatialTree<TEntry> where TEntry : class, ISpatialEntry
|
||||
{
|
||||
private Dictionary<Guid, TEntry> _entries = new();
|
||||
private record struct Entry(Guid Guid, Aabb Bounds) : ISpatialEntry;
|
||||
GridSpatialIndex<Entry, OctreeEntryStore<Entry>> gridSpatialIndex;
|
||||
public SpatialDataLayer(int gridSize = 128)
|
||||
{
|
||||
gridSpatialIndex = new(() => new OctreeEntryStore<Entry>(new Aabb(Vector3.Zero, Vector3.One * gridSize)), gridSize);
|
||||
}
|
||||
public void Insert(TEntry entry)
|
||||
{
|
||||
gridSpatialIndex.Insert(new(entry.Guid, entry.Bounds));
|
||||
_entries.Add(entry.Guid, entry);
|
||||
|
||||
}
|
||||
// public async ValueTask Save(string @namespace, IDataStore dataStore, IDataSerializer<TEntry[]> serializer)
|
||||
// {
|
||||
// var data = await serializer.SerializeAsync(_entries.Values.ToArray());
|
||||
// await dataStore.WriteAsync(new DataKey(@namespace, nameof(_entries)), data);
|
||||
// }
|
||||
// public async ValueTask Load(string @namespace, IDataStore dataStore, IDataSerializer<TEntry[]> serializer)
|
||||
// {
|
||||
|
||||
// gridSpatialIndex.Clear();
|
||||
// _entries.Clear();
|
||||
// var data = await dataStore.ReadAsync(new DataKey(@namespace, nameof(_entries)));
|
||||
// var e = await serializer.DeserializeAsync(data.Value);
|
||||
|
||||
// e.ToList().ForEach(Insert);
|
||||
// }
|
||||
public IEnumerable<TEntry> Query(Aabb region)
|
||||
{
|
||||
|
||||
return gridSpatialIndex.Query(region).Where(e => _entries.ContainsKey(e.Guid)).Select(e => _entries[e.Guid]);
|
||||
}
|
||||
|
||||
public void Remove(TEntry entry)
|
||||
{
|
||||
gridSpatialIndex.Remove(new(entry.Guid, entry.Bounds));
|
||||
_entries.Remove(entry.Guid);
|
||||
}
|
||||
}
|
||||
public class CollectionSpatialTree<TEntry> : ISpatialTree<TEntry> where TEntry : ISpatialEntry
|
||||
{
|
||||
private Dictionary<Guid,TEntry> entries = new();
|
||||
public void Insert(TEntry entry)
|
||||
{
|
||||
entries[entry.Guid] = entry;
|
||||
}
|
||||
|
||||
public IEnumerable<TEntry> Query(Aabb region)
|
||||
{
|
||||
foreach (var item in entries)
|
||||
{
|
||||
if (region.Intersects(item.Value.Bounds))
|
||||
{
|
||||
yield return item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(TEntry entry)
|
||||
{
|
||||
entries.Remove(entry.Guid);
|
||||
}
|
||||
}
|
||||
public class GridSpatialIndex<TEntry, TChunkSpatialTree> : ISpatialTree<TEntry> where TEntry : ISpatialEntry where TChunkSpatialTree : ISpatialTree<TEntry>
|
||||
{
|
||||
private readonly int _cellSize;
|
||||
private readonly Func<TChunkSpatialTree> factory;
|
||||
private readonly Dictionary<Vector3I, TChunkSpatialTree> _cells = new();
|
||||
public GridSpatialIndex(Func<TChunkSpatialTree> factory, int cellSize = 32)
|
||||
{
|
||||
this._cellSize = cellSize;
|
||||
this.factory = factory;
|
||||
}
|
||||
private Vector3I GetCellCoord(Vector3 position)
|
||||
{
|
||||
int cx = (int)MathF.Floor(position.X / _cellSize);
|
||||
int cy = (int)MathF.Floor(position.Y / _cellSize);
|
||||
int cz = (int)MathF.Floor(position.Z / _cellSize);
|
||||
return new(cx, cy, cz);
|
||||
}
|
||||
private Vector3I[] GetOverlappingCells(Aabb bounds)
|
||||
{
|
||||
var minCell = GetCellCoord(bounds.Min);
|
||||
var maxCell = GetCellCoord(bounds.Max);
|
||||
|
||||
var cells = new List<Vector3I>();
|
||||
for (int x = minCell.X; x <= maxCell.X; x++)
|
||||
for (int y = minCell.Y; y <= maxCell.Y; y++)
|
||||
for (int z = minCell.Z; z <= maxCell.Z; z++)
|
||||
cells.Add(new(x, y, z));
|
||||
return cells.ToArray();
|
||||
}
|
||||
public void Insert(TEntry entry)
|
||||
{
|
||||
foreach (var cell in GetOverlappingCells(entry.Bounds))
|
||||
{
|
||||
if (!_cells.TryGetValue(cell, out var tree))
|
||||
{
|
||||
_cells[cell] = tree = factory();
|
||||
}
|
||||
tree.Insert(entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<TEntry> Query(Aabb region)
|
||||
{
|
||||
var visted = new HashSet<TEntry>();
|
||||
foreach (var cell in GetOverlappingCells(region))
|
||||
{
|
||||
if (_cells.TryGetValue(cell, out var tree))
|
||||
{
|
||||
foreach (var entry in tree.Query(region))
|
||||
{
|
||||
if (visted.Add(entry))
|
||||
{
|
||||
yield return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(TEntry entry)
|
||||
{
|
||||
foreach (var cell in GetOverlappingCells(entry.Bounds))
|
||||
{
|
||||
if (_cells.TryGetValue(cell, out var tree))
|
||||
{
|
||||
tree.Remove(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
public void Clear()
|
||||
{
|
||||
_cells.Clear();
|
||||
}
|
||||
}
|
||||
public readonly record struct Aabb
|
||||
{
|
||||
public readonly Vector3 Min { get; }
|
||||
public readonly Vector3 Max { get; }
|
||||
public Aabb(Vector3 min, Vector3 max)
|
||||
{
|
||||
Min = min.Min(max);
|
||||
Max = min.Max(max);
|
||||
}
|
||||
public bool Intersects(Aabb other)
|
||||
{
|
||||
return !(Max.X < other.Min.X || Min.X > other.Max.X ||
|
||||
Max.Y < other.Min.Y || Min.Y > other.Max.Y ||
|
||||
Max.Z < other.Min.Z || Min.Z > other.Max.Z);
|
||||
}
|
||||
public bool Contains(Vector3 point)
|
||||
{
|
||||
return point.X >= Min.X && point.X <= Max.X &&
|
||||
point.Y >= Min.Y && point.Y <= Max.Y &&
|
||||
point.Z >= Min.Z && point.Z <= Max.Z;
|
||||
}
|
||||
public Aabb Expand(Vector3I value) =>
|
||||
new Aabb(
|
||||
Min.Min(value),
|
||||
Max.Max(value)
|
||||
);
|
||||
public Vector3 Center => (Min + Max) * 0.5f;
|
||||
public Vector3 Size => Max - Min;
|
||||
|
||||
}
|
||||
public sealed class OctreeEntryStore<T> : ISpatialTree<T> where T : ISpatialEntry
|
||||
{
|
||||
private readonly OctreeNode<T> _root;
|
||||
|
||||
public OctreeEntryStore(Aabb worldBounds, int maxDepth = 8, int maxPerNode = 16)
|
||||
{
|
||||
_root = new OctreeNode<T>(worldBounds, maxDepth, maxPerNode);
|
||||
}
|
||||
|
||||
public void Insert(T entry) => _root.Insert(entry);
|
||||
public bool Remove(T entry) => _root.Remove(entry);
|
||||
public IEnumerable<T> Query(Aabb bounds) => _root.Query(bounds);
|
||||
// public IEnumerable<T> QuerySphere(Vector3 center, float radius) => _root.QuerySphere(center, radius);
|
||||
|
||||
void ISpatialTree<T>.Remove(T entry)
|
||||
{
|
||||
this.Remove(entry);
|
||||
}
|
||||
|
||||
// --- Internal Octree Node ---
|
||||
private sealed class OctreeNode<TNode> where TNode : ISpatialEntry
|
||||
{
|
||||
private readonly Aabb _bounds;
|
||||
private readonly int _maxDepth;
|
||||
private readonly int _maxPerNode;
|
||||
private readonly int _depth;
|
||||
|
||||
private List<TNode>? _entries;
|
||||
private OctreeNode<TNode>[]? _children;
|
||||
|
||||
public OctreeNode(Aabb bounds, int maxDepth, int maxPerNode, int depth = 0)
|
||||
{
|
||||
_bounds = bounds;
|
||||
_maxDepth = maxDepth;
|
||||
_maxPerNode = maxPerNode;
|
||||
_depth = depth;
|
||||
_entries = new List<TNode>();
|
||||
}
|
||||
|
||||
public void Insert(TNode entry)
|
||||
{
|
||||
// If we have children, try to pass entry down.
|
||||
if (_children != null)
|
||||
{
|
||||
foreach (var child in _children)
|
||||
{
|
||||
if (child._bounds.Intersects(entry.Bounds))
|
||||
{
|
||||
child.Insert(entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, keep it here.
|
||||
_entries ??= new List<TNode>();
|
||||
_entries.Add(entry);
|
||||
|
||||
// Subdivide if too many and not too deep
|
||||
if (_entries.Count > _maxPerNode && _depth < _maxDepth)
|
||||
Subdivide();
|
||||
}
|
||||
|
||||
public bool Remove(TNode entry)
|
||||
{
|
||||
bool removed = _entries?.Remove(entry) ?? false;
|
||||
if (_children != null)
|
||||
{
|
||||
foreach (var child in _children)
|
||||
removed |= child.Remove(entry);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
public IEnumerable<TNode> Query(Aabb bounds)
|
||||
{
|
||||
if (!_bounds.Intersects(bounds))
|
||||
yield break;
|
||||
|
||||
if (_entries != null)
|
||||
{
|
||||
foreach (var e in _entries)
|
||||
if (e.Bounds.Intersects(bounds))
|
||||
yield return e;
|
||||
}
|
||||
|
||||
if (_children != null)
|
||||
{
|
||||
foreach (var child in _children)
|
||||
{
|
||||
foreach (var e in child.Query(bounds))
|
||||
yield return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// public IEnumerable<TNode> QuerySphere(Vector3 center, float radius)
|
||||
// {
|
||||
// // Quick reject if AABB doesn't overlap sphere
|
||||
// if (!AabbSphereIntersect(_bounds, center, radius))
|
||||
// yield break;
|
||||
|
||||
// if (_entries != null)
|
||||
// {
|
||||
// foreach (var e in _entries)
|
||||
// {
|
||||
// var distSq = e.Position.DistanceSquaredTo(center);
|
||||
// if (distSq <= radius * radius)
|
||||
// yield return e;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (_children != null)
|
||||
// {
|
||||
// foreach (var child in _children)
|
||||
// {
|
||||
// foreach (var e in child.QuerySphere(center, radius))
|
||||
// yield return e;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private void Subdivide()
|
||||
{
|
||||
if (_children != null) return;
|
||||
|
||||
_children = new OctreeNode<TNode>[8];
|
||||
var center = _bounds.Center;
|
||||
var size = _bounds.Size * 0.5f;
|
||||
|
||||
int i = 0;
|
||||
for (int x = 0; x <= 1; x++)
|
||||
for (int y = 0; y <= 1; y++)
|
||||
for (int z = 0; z <= 1; z++)
|
||||
{
|
||||
var min = new Vector3(
|
||||
x == 0 ? _bounds.Min.X : center.X,
|
||||
y == 0 ? _bounds.Min.Y : center.Y,
|
||||
z == 0 ? _bounds.Min.Z : center.Z);
|
||||
var max = min + size;
|
||||
_children[i++] = new OctreeNode<TNode>(new Aabb(min, max), _maxDepth, _maxPerNode, _depth + 1);
|
||||
}
|
||||
|
||||
// Redistribute current entries
|
||||
var oldEntries = _entries!;
|
||||
_entries = new List<TNode>();
|
||||
foreach (var entry in oldEntries)
|
||||
Insert(entry);
|
||||
}
|
||||
|
||||
private static bool AabbSphereIntersect(Aabb aabb, Vector3 center, float radius)
|
||||
{
|
||||
float sqDist = 0f;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
float v = center[i];
|
||||
if (v < aabb.Min[i]) sqDist += (aabb.Min[i] - v) * (aabb.Min[i] - v);
|
||||
if (v > aabb.Max[i]) sqDist += (v - aabb.Max[i]) * (v - aabb.Max[i]);
|
||||
}
|
||||
return sqDist <= radius * radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
public readonly struct SpatialBounds
|
||||
{
|
||||
public readonly Vector3 Center;
|
||||
public readonly Vector3 Extents;
|
||||
|
||||
public SpatialBounds(Vector3 center, Vector3 extents)
|
||||
{
|
||||
Center = center;
|
||||
Extents = extents;
|
||||
}
|
||||
|
||||
public static SpatialBounds Point(Vector3 position) => new(position, Vector3.Zero);
|
||||
|
||||
public Aabb ToAabb() => new(Center - Extents, Center + Extents);
|
||||
|
||||
public bool Intersects(Aabb region) => region.Intersects(ToAabb());
|
||||
}
|
||||
public readonly struct SpatialEntry<T>
|
||||
{
|
||||
public readonly Guid Id;
|
||||
// public readonly Aabb Bounds;
|
||||
public readonly SpatialBounds Bounds;
|
||||
public readonly T Value;
|
||||
|
||||
// public SpatialEntry(Guid id, T value, Aabb bounds)
|
||||
public SpatialEntry(Guid id, T value, SpatialBounds bounds)
|
||||
{
|
||||
Id = id;
|
||||
Value = value;
|
||||
Bounds = bounds;
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/Voxels/SpatialTree.cs.uid
Normal file
1
src/voxelgame/Voxels/SpatialTree.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dik0ryeup2wah
|
||||
14
src/voxelgame/Voxels/TestWraper.tscn
Normal file
14
src/voxelgame/Voxels/TestWraper.tscn
Normal file
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cchpoo5dqwn2w"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://kdg8pxsdacor" path="res://Voxels/UVWrapVisualizer.cs" id="1_m0iwv"]
|
||||
|
||||
[node name="TestWraper" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
scale = Vector2(12.215, 12.215)
|
||||
script = ExtResource("1_m0iwv")
|
||||
ChunkSize = 64
|
||||
111
src/voxelgame/Voxels/UVWrapVisualizer.cs
Normal file
111
src/voxelgame/Voxels/UVWrapVisualizer.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using Godot;
|
||||
using SJK.Voxels;
|
||||
using System;
|
||||
|
||||
public partial class UVWrapVisualizer : Control
|
||||
{
|
||||
public enum WrapperType
|
||||
{
|
||||
Normal,
|
||||
Twisted
|
||||
}
|
||||
|
||||
[Export] public int WorldSize = 8;
|
||||
[Export] public int ChunkSize = 16;
|
||||
[Export] public int RenderChunks = 4;
|
||||
[Export] public WrapperType WrapMode = WrapperType.Twisted;
|
||||
|
||||
private ImageTexture texture;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
GenerateTexture();
|
||||
}
|
||||
private IWorldWrapper wrapper = new EndlessSizeXZWorld(10);
|
||||
private void GenerateTexture()
|
||||
{
|
||||
int texSize = WorldSize * ChunkSize * RenderChunks;
|
||||
var image = Godot.Image.CreateEmpty(texSize, texSize, false, Image.Format.Rgb8);
|
||||
wrapper = WrapMode switch
|
||||
{
|
||||
WrapperType.Normal => new ToroidalWrapping(Vector3I.One * WorldSize),
|
||||
WrapperType.Twisted => new MirroredZWrappingXOffset(Vector3I.One * WorldSize),
|
||||
_ => throw new Exception(),
|
||||
};
|
||||
GD.Print(wrapper.GetType());
|
||||
for (int gz = 0; gz < texSize; gz++)
|
||||
{
|
||||
for (int gx = 0; gx < texSize; gx++)
|
||||
{
|
||||
Vector2I global = new Vector2I(gx - texSize / 2, gz - texSize / 2); // center origin
|
||||
var v3 = wrapper.Wrap(new Vector3I(global.X, 1, global.Y));
|
||||
Vector2I local = new(v3.X, v3.Z);
|
||||
bool flip = false;
|
||||
|
||||
// switch (WrapMode)
|
||||
// {
|
||||
// case WrapperType.Twisted:
|
||||
// (local, flip) = TwistedWrapInt(global, WorldSize, WorldSize);
|
||||
// break;
|
||||
// default:
|
||||
// local = new Vector2I(Mod(global.X, WorldSize), Mod(global.Y, WorldSize));
|
||||
// flip = false;
|
||||
// break;
|
||||
// }
|
||||
|
||||
// Encode local position into RGB (U and V channels)
|
||||
float u = local.X / (float)WorldSize;
|
||||
float v = local.Y / (float)WorldSize;
|
||||
Color color = new Color(u, v, flip ? 1.0f : 0.0f);
|
||||
image.SetPixel(gx, gz, color);
|
||||
}
|
||||
}
|
||||
|
||||
texture = ImageTexture.CreateFromImage(image);
|
||||
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
public override void _Draw()
|
||||
{
|
||||
if (texture != null)
|
||||
{
|
||||
DrawTexture(texture, Vector2.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Wrapping Logic ==========
|
||||
|
||||
private (Vector2I local, bool flipped) TwistedWrapInt(Vector2I global, int W, int H)
|
||||
{
|
||||
int gx = global.X;
|
||||
int gy = global.Y;
|
||||
|
||||
int yWrap = FloorDiv(gy, H);
|
||||
bool flip = (yWrap & 1) != 0;
|
||||
|
||||
int tileY = gy % H;
|
||||
if (tileY < 0) tileY += H;
|
||||
int ly = flip ? (H - 1 - tileY) : tileY;
|
||||
|
||||
int xOffset = flip ? W / 2 : 0;
|
||||
int wrappedX = gx + xOffset;
|
||||
|
||||
int tileX = wrappedX % W;
|
||||
if (tileX < 0) tileX += W;
|
||||
int lx = flip ? (W - 1 - tileX) : tileX;
|
||||
|
||||
return (new Vector2I(lx, ly), flip);
|
||||
}
|
||||
|
||||
private int FloorDiv(int a, int b)
|
||||
{
|
||||
return (a >= 0) ? a / b : ((a + 1) / b) - 1;
|
||||
}
|
||||
|
||||
private int Mod(int a, int b)
|
||||
{
|
||||
int r = a % b;
|
||||
return r < 0 ? r + b : r;
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/Voxels/UVWrapVisualizer.cs.uid
Normal file
1
src/voxelgame/Voxels/UVWrapVisualizer.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://kdg8pxsdacor
|
||||
194
src/voxelgame/Voxels/VoxelQuery.cs
Normal file
194
src/voxelgame/Voxels/VoxelQuery.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public interface IVoxelQuery : IEnumerable<VoxelPos>
|
||||
{
|
||||
IVoxelQuery Union(IVoxelQuery other);
|
||||
IVoxelQuery Exclude(IVoxelQuery other);
|
||||
IVoxelQuery Intersect(IVoxelQuery other);
|
||||
IVoxelQuery Where(Func<VoxelPos, bool> predicate);
|
||||
IVoxelQuery Offset(int x, int y, int z);
|
||||
// IVoxelQuery Rotate(float yaw, float pitch, float roll);
|
||||
}
|
||||
public abstract class VoxelQueryBase : IVoxelQuery
|
||||
{
|
||||
public abstract IEnumerator<VoxelPos> GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public IVoxelQuery Where(Func<VoxelPos, bool> predicate)
|
||||
=> new FilterQuery(this, predicate);
|
||||
|
||||
public IVoxelQuery Union(IVoxelQuery other)
|
||||
=> new UnionQuery(this, other);
|
||||
public IVoxelQuery Intersect(IVoxelQuery other)
|
||||
=> new IntersectQuery(this, other);
|
||||
|
||||
public IVoxelQuery Exclude(IVoxelQuery other)
|
||||
=> new ExcludeQuery(this, other);
|
||||
|
||||
public IVoxelQuery Offset(int x, int y, int z)
|
||||
=> new TransformQuery(this, pos => new VoxelPos(pos.X + x, pos.Y + y, pos.Z + z));
|
||||
// public IVoxelQuery Rotate(float yaw, float pitch, float roll)
|
||||
// => new TransformQuery(this, pos => RotatePoint(pos, yaw, pitch, roll));
|
||||
|
||||
// protected static VoxelPos RotatePoint(VoxelPos pos, float yaw, float pitch, float roll)
|
||||
// {
|
||||
// // simple rotation around origin
|
||||
// var rx = (int)Math.Round(pos.X * Math.Cos(yaw) - pos.Z * Math.Sin(yaw));
|
||||
// var rz = (int)Math.Round(pos.X * Math.Sin(yaw) + pos.Z * Math.Cos(yaw));
|
||||
// return new(rx, pos.Y, rz);
|
||||
// }
|
||||
}
|
||||
public sealed class TransformQuery : VoxelQueryBase
|
||||
{
|
||||
private readonly IVoxelQuery source;
|
||||
private readonly Func<VoxelPos,VoxelPos> offset;
|
||||
|
||||
public TransformQuery(IVoxelQuery source, Func<VoxelPos,VoxelPos> offset)
|
||||
{
|
||||
this.source = source;
|
||||
this.offset = offset;
|
||||
}
|
||||
public override IEnumerator<VoxelPos> GetEnumerator()
|
||||
{
|
||||
foreach (var pos in source)
|
||||
{
|
||||
yield return offset(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
public sealed class SphereQuery : VoxelQueryBase
|
||||
{
|
||||
private readonly VoxelPos _center;
|
||||
private readonly int _radius;
|
||||
|
||||
public SphereQuery(VoxelPos center, int radius)
|
||||
{
|
||||
_center = center;
|
||||
_radius = radius;
|
||||
}
|
||||
|
||||
public override IEnumerator<VoxelPos> GetEnumerator()
|
||||
{
|
||||
int r2 = _radius * _radius;
|
||||
for (int x = -_radius; x <= _radius; x++)
|
||||
for (int y = -_radius; y <= _radius; y++)
|
||||
for (int z = -_radius; z <= _radius; z++)
|
||||
{
|
||||
if (x * x + y * y + z * z <= r2)
|
||||
yield return new VoxelPos(_center.X + x, _center.Y + y, _center.Z + z);
|
||||
}
|
||||
}
|
||||
}
|
||||
public sealed class BoxQuery : VoxelQueryBase
|
||||
{
|
||||
private readonly VoxelPos _min, _max;
|
||||
|
||||
public BoxQuery(VoxelPos min, VoxelPos max)
|
||||
{
|
||||
_min = min;
|
||||
_max = max;
|
||||
}
|
||||
|
||||
public override IEnumerator<VoxelPos> GetEnumerator()
|
||||
{
|
||||
for (int x = _min.X; x <= _max.X; x++)
|
||||
for (int y = _min.Y; y <= _max.Y; y++)
|
||||
for (int z = _min.Z; z <= _max.Z; z++)
|
||||
yield return new(x, y, z);
|
||||
}
|
||||
}
|
||||
public sealed class FilterQuery : VoxelQueryBase
|
||||
{
|
||||
private readonly IVoxelQuery _source;
|
||||
private readonly Func<VoxelPos, bool> _predicate;
|
||||
|
||||
public FilterQuery(IVoxelQuery source, Func<VoxelPos, bool> predicate)
|
||||
{
|
||||
_source = source;
|
||||
_predicate = predicate;
|
||||
}
|
||||
|
||||
public override IEnumerator<VoxelPos> GetEnumerator()
|
||||
{
|
||||
foreach (var pos in _source)
|
||||
if (_predicate(pos))
|
||||
yield return pos;
|
||||
}
|
||||
}
|
||||
public sealed class UnionQuery : VoxelQueryBase
|
||||
{
|
||||
private readonly IVoxelQuery _a, _b;
|
||||
|
||||
public UnionQuery(IVoxelQuery a, IVoxelQuery b)
|
||||
{
|
||||
_a = a;
|
||||
_b = b;
|
||||
}
|
||||
|
||||
public override IEnumerator<VoxelPos> GetEnumerator()
|
||||
{
|
||||
var seen = new HashSet<VoxelPos>();
|
||||
foreach (var pos in _a)
|
||||
if (seen.Add(pos)) yield return pos;
|
||||
foreach (var pos in _b)
|
||||
if (seen.Add(pos)) yield return pos;
|
||||
}
|
||||
}
|
||||
public sealed class IntersectQuery : VoxelQueryBase
|
||||
{
|
||||
private readonly IVoxelQuery _a, _b;
|
||||
|
||||
public IntersectQuery(IVoxelQuery a, IVoxelQuery b)
|
||||
{
|
||||
_a = a;
|
||||
_b = b;
|
||||
}
|
||||
|
||||
public override IEnumerator<VoxelPos> GetEnumerator()
|
||||
{
|
||||
var b = new HashSet<VoxelPos>(_b);
|
||||
foreach (var pos in _a)
|
||||
if (b.Contains(pos)) yield return pos;
|
||||
|
||||
}
|
||||
}
|
||||
public sealed class ExcludeQuery : VoxelQueryBase
|
||||
{
|
||||
private readonly IVoxelQuery _source, _exclude;
|
||||
|
||||
public ExcludeQuery(IVoxelQuery source, IVoxelQuery exclude)
|
||||
{
|
||||
_source = source;
|
||||
_exclude = exclude;
|
||||
}
|
||||
|
||||
public override IEnumerator<VoxelPos> GetEnumerator()
|
||||
{
|
||||
var excludeSet = new HashSet<VoxelPos>(_exclude);
|
||||
foreach (var pos in _source)
|
||||
if (!excludeSet.Contains(pos))
|
||||
yield return pos;
|
||||
}
|
||||
}
|
||||
public static class VoxelQueryExtensions
|
||||
{
|
||||
public static IEnumerable<(VoxelPos pos, TVoxel voxel)> WithData<TVoxel>(
|
||||
this IVoxelQuery query,
|
||||
Func<VoxelPos, TVoxel> source)
|
||||
{
|
||||
foreach (var pos in query)
|
||||
{
|
||||
var voxel = source(pos);
|
||||
yield return (pos, voxel);
|
||||
}
|
||||
}
|
||||
// public static IEnumerable<(ChunkPos<TChunkSize> ChunkPos, VoxelPos<TChunkSize> LocalPos)> ToChunkLocal<TChunkSize>(this IEnumerable<VoxelPos> self) where TChunkSize : IChunkSize<TChunkSize>
|
||||
// {
|
||||
// foreach (var pos in self)
|
||||
// {
|
||||
// yield return pos.ToVoxelPos<TChunkSize>();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
1
src/voxelgame/Voxels/VoxelQuery.cs.uid
Normal file
1
src/voxelgame/Voxels/VoxelQuery.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://f4gkrcovyyc
|
||||
91
src/voxelgame/Voxels/Worlds/IWorldChunk.cs
Normal file
91
src/voxelgame/Voxels/Worlds/IWorldChunk.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using ChickenGameTest;
|
||||
using Godot;
|
||||
using SJK.Functional;
|
||||
using SJK.Voxels.Registry;
|
||||
|
||||
namespace SJK.Voxels;
|
||||
public interface IWorldChunk
|
||||
{
|
||||
bool IsLoaded {get;}
|
||||
}
|
||||
public interface IWorldChunk<TVoxel,TChunkSize> :IWorldChunk, IReadOnlyVoxelChunk<TVoxel,TChunkSize> where TChunkSize : IChunkSize<TChunkSize>
|
||||
{
|
||||
ChunkPos<TChunkSize> ChunkPos {get;set;}
|
||||
Option<T> GetCustomData<T>() where T : IChunkData;
|
||||
void SetCustomData<T>(T value)where T : IChunkData;
|
||||
TVoxel GetVoxelAt(VoxelPos<TChunkSize> voxelPos);
|
||||
void SetVoxelAt(VoxelPos<TChunkSize> voxelPos, TVoxel voxel);
|
||||
|
||||
}
|
||||
public class WorldChunk<TVoxel,TChunkSize> : IWorldChunk<TVoxel,TChunkSize> where TChunkSize : IChunkSize<TChunkSize> where TVoxel : IEquatable<TVoxel>
|
||||
{
|
||||
public ChunkPos<TChunkSize> ChunkPos { get; set;}
|
||||
|
||||
public bool IsLoaded {get;set;}
|
||||
|
||||
public VoxelBounds Bounds => new VoxelBounds(Vector3I.Zero,new Vector3I(TChunkSize.Size,TChunkSize.Size,TChunkSize.Size));
|
||||
|
||||
private readonly RefPalette<TVoxel> _palette;
|
||||
private IVoxelChunk<int,TChunkSize> _data;
|
||||
private readonly SortedDictionary<int,IChunkData> _customData = [];
|
||||
public WorldChunk(TVoxel @default)
|
||||
{
|
||||
_palette = new (@default);
|
||||
_data = new VoxelChunk<int, TChunkSize>();
|
||||
}
|
||||
public Option<T> GetCustomData<T>() where T : IChunkData
|
||||
{
|
||||
if (_customData.TryGetValue(ComponentType<T>.Id, out var data))
|
||||
{
|
||||
return Option<T>.Some((T)data);
|
||||
}
|
||||
return Option<T>.None;
|
||||
}
|
||||
|
||||
public TVoxel GetVoxelAt(VoxelPos<TChunkSize> voxelPos)
|
||||
{
|
||||
return _palette.Get(_data.GetVoxel(voxelPos));
|
||||
|
||||
}
|
||||
|
||||
public void SetCustomData<T>(T value) where T : IChunkData
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void SetVoxelAt(VoxelPos<TChunkSize> voxelPos, TVoxel voxel)
|
||||
{
|
||||
_data.SetVoxel(voxelPos.X,voxelPos.Y,voxelPos.Z,_palette.GetOrAddIndex(voxel));
|
||||
}
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos<TChunkSize> position)=>GetVoxelAt(position);
|
||||
|
||||
public TVoxel GetVoxel(VoxelPos voxelPos)=> GetVoxelAt(voxelPos.GetVoxelPos<TChunkSize>());
|
||||
}
|
||||
public interface IChunkData
|
||||
{
|
||||
ValueTask Serialize(Stream stream);
|
||||
ValueTask Deserialize(Stream stream);
|
||||
}
|
||||
public struct CustomEntry<T>(T data, Action<T,Stream> serialize, Func<Stream, T> deserialize) : IChunkData
|
||||
{
|
||||
private T data = data;
|
||||
private readonly Action<T,Stream> serialize = serialize;
|
||||
private readonly Func<Stream, T> deserialize = deserialize;
|
||||
public T GetData()=>data;
|
||||
public ValueTask Deserialize(Stream stream)
|
||||
{
|
||||
data = deserialize(stream);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask Serialize(Stream stream)
|
||||
{
|
||||
serialize(data,stream);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/Voxels/Worlds/IWorldChunk.cs.uid
Normal file
1
src/voxelgame/Voxels/Worlds/IWorldChunk.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ct81cqcwbb6wk
|
||||
860
src/voxelgame/Voxels/Worlds/VoxelSerializer.cs
Normal file
860
src/voxelgame/Voxels/Worlds/VoxelSerializer.cs
Normal file
@@ -0,0 +1,860 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Godot;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using SJK.Functional;
|
||||
using SJK.Voxels;
|
||||
using SqlKata;
|
||||
|
||||
public interface IDataSerializer<T>
|
||||
{
|
||||
ValueTask<byte[]> SerializeAsync(T value);
|
||||
ValueTask<T> DeserializeAsync(Span<byte> bytes);
|
||||
|
||||
|
||||
}
|
||||
public interface IDataSerializer<T,T2>
|
||||
{
|
||||
ValueTask<T2> SerializeAsync(T value);
|
||||
ValueTask<T> DeserializeAsync(T2 bytes);
|
||||
|
||||
|
||||
}
|
||||
// public interface IDataStore
|
||||
// {
|
||||
// ValueTask<Option<byte[]>> ReadAsync(DataKey path, CancellationToken token = default);
|
||||
// ValueTask WriteAsync(DataKey path, byte[] data, CancellationToken token = default);
|
||||
// ValueTask<bool> ExistsAsync(DataKey path, CancellationToken token = default);
|
||||
// ValueTask DeleteAsync(DataKey path, CancellationToken token = default);
|
||||
// public bool IsReadOnly { get; }
|
||||
// }
|
||||
public interface IDataStore<TDomainKey,T>
|
||||
{
|
||||
ValueTask<Option<T>> ReadAsync(TDomainKey key, CancellationToken token = default);
|
||||
ValueTask WriteAsync(TDomainKey key, T value, CancellationToken token = default);
|
||||
ValueTask<bool> ExistsAsync(TDomainKey key, CancellationToken token = default);
|
||||
ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default);
|
||||
bool IsReadOnly { get; }
|
||||
}
|
||||
public interface ISqliteSchema
|
||||
{
|
||||
string IntilizeSchema();
|
||||
(string Table, string Query) GetReadQuery(DataKey key);
|
||||
(string Table, string Query) GetWriteQuery(DataKey key);
|
||||
(string Table, string Query) GetExistQuery(DataKey key);
|
||||
(string Table, string Query) GetRemoveQuery(DataKey key);
|
||||
void BindKey(SqliteCommand cmd, DataKey key);
|
||||
void BindData(SqliteCommand cmd, ReadOnlyMemory<byte> data);
|
||||
}
|
||||
public interface ISqliteSchema<TKey, TData>
|
||||
{
|
||||
string TableName { get; }
|
||||
void EnsureSchema(SqliteConnection connection);
|
||||
SqliteCommand CreateSelectCommand(SqliteConnection connection, TKey key);
|
||||
SqliteCommand CreateInsertCommand(SqliteConnection connection, TKey key, TData data);
|
||||
SqliteCommand CreateDeleteCommand(SqliteConnection connection, TKey key);
|
||||
|
||||
}
|
||||
public interface ISqliteProvider<TKey, TData>
|
||||
{
|
||||
Task<Option<TData>> QueryAsync(SqliteConnection conn, TKey key, CancellationToken token);
|
||||
Task UpsertAsync(SqliteConnection conn, TKey key, TData data, CancellationToken token);
|
||||
Task DeleteAsync(SqliteConnection conn, TKey key, CancellationToken token);
|
||||
}
|
||||
|
||||
public sealed class SQLiteDataStore<TKey,TData> : IDataStore<TKey,TData>
|
||||
{
|
||||
private readonly SqliteConnection connection;
|
||||
private readonly ISqliteSchema<TKey,TData> schema;
|
||||
private readonly ISqliteProvider<TKey,TData> provider;
|
||||
|
||||
public SQLiteDataStore(SqliteConnection connection, ISqliteSchema<TKey,TData> schema, ISqliteProvider<TKey,TData> provider, bool isReadOnly)
|
||||
{
|
||||
this.connection = connection;
|
||||
this.schema = schema;
|
||||
IsReadOnly = isReadOnly;
|
||||
schema.EnsureSchema(connection);
|
||||
connection.Open();
|
||||
}
|
||||
|
||||
public bool IsReadOnly { get; }
|
||||
|
||||
public async ValueTask DeleteAsync(TKey path, CancellationToken token = default)
|
||||
{
|
||||
using var command = schema.CreateDeleteCommand(connection, path);
|
||||
// var (table, query) = schema.GetRemoveQuery(path);
|
||||
// using var cmd = connection.CreateCommand();
|
||||
// cmd.CommandText = query;
|
||||
// schema.BindKey(cmd, path);
|
||||
// var results = await cmd.ExecuteScalarAsync(token);
|
||||
var results = await command.ExecuteScalarAsync(token);
|
||||
}
|
||||
|
||||
public ValueTask<bool> ExistsAsync(TKey path, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
// var (table, query) = schema.GetExistQuery(path);
|
||||
// using var cmd = connection.CreateCommand();
|
||||
// cmd.CommandText = query;
|
||||
// schema.BindKey(cmd, path);
|
||||
// var results = await cmd.ExecuteScalarAsync(token);
|
||||
// return results is true;
|
||||
}
|
||||
|
||||
public async ValueTask<Option<TData>> ReadAsync(TKey path, CancellationToken token = default)
|
||||
{
|
||||
using var command = schema.CreateSelectCommand(connection, path);
|
||||
// var (table, query) = schema.GetReadQuery(path);
|
||||
// using var cmd = connection.CreateCommand();
|
||||
// cmd.CommandText = query;
|
||||
// schema.BindKey(cmd, path);
|
||||
// var results = await cmd.ExecuteScalarAsync(token);
|
||||
var results = await command.ExecuteScalarAsync(token);
|
||||
return results is TData bytes
|
||||
? Option<TData>.Some(bytes)
|
||||
: Option<TData>.None;
|
||||
}
|
||||
|
||||
public Option<TData> Read(TKey path)
|
||||
{
|
||||
using var command = schema.CreateSelectCommand(connection, path);
|
||||
// var (table, query) = schema.GetReadQuery(path);
|
||||
// using var cmd = connection.CreateCommand();
|
||||
// cmd.CommandText = query;
|
||||
// schema.BindKey(cmd, path);
|
||||
// var results = await cmd.ExecuteScalarAsync(token);
|
||||
var results = command.ExecuteScalar();
|
||||
return results is TData bytes
|
||||
? Option<TData>.Some(bytes)
|
||||
: Option<TData>.None;
|
||||
}
|
||||
|
||||
public async ValueTask WriteAsync(TKey path, TData data, CancellationToken token = default)
|
||||
{
|
||||
using var command = schema.CreateInsertCommand(connection, path,data);
|
||||
// var (table, query) = schema.GetWriteQuery(path);
|
||||
// using var cmd = connection.CreateCommand();
|
||||
// cmd.CommandText = query;
|
||||
// schema.BindKey(cmd, path);
|
||||
// schema.BindData(cmd, data);
|
||||
await command.ExecuteNonQueryAsync(token);
|
||||
}
|
||||
}
|
||||
public sealed class FileDataStore : IDataStore<string,Stream>
|
||||
{
|
||||
private readonly string rootDir;
|
||||
|
||||
public FileDataStore(string rootDir,bool isReadOnly = false)
|
||||
{
|
||||
this.rootDir = rootDir;
|
||||
IsReadOnly = isReadOnly;
|
||||
}
|
||||
|
||||
public bool IsReadOnly { get; }
|
||||
|
||||
public ValueTask DeleteAsync(string path, CancellationToken token = default)
|
||||
{
|
||||
if (IsReadOnly) return ValueTask.CompletedTask;
|
||||
var file = Path.Combine(rootDir, path);// _pathResolver.ResolvePath(path));
|
||||
if (File.Exists(file))
|
||||
File.Delete(file);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask<bool> ExistsAsync(string path, CancellationToken token = default)
|
||||
{
|
||||
var file = Path.Combine(rootDir, path);
|
||||
return ValueTask.FromResult(File.Exists(file));
|
||||
}
|
||||
|
||||
public ValueTask<Option<Stream>> ReadAsync(string path, CancellationToken token = default)
|
||||
{
|
||||
// var filePath = rootDir + "/" + path;
|
||||
// File.Exists(filePath)
|
||||
|
||||
var file = Path.Combine(rootDir, path); //_pathResolver.ResolvePath(path));
|
||||
return ValueTask.FromResult(File.Exists(file) ? Option<Stream>.Some(File.OpenRead(file)):Option<Stream>.None);
|
||||
// ? Option<byte[]>.Some(await File.ReadAllBytesAsync(file, token))
|
||||
// : Option<byte[]>.None;
|
||||
|
||||
}
|
||||
|
||||
public async ValueTask WriteAsync(string path, Stream data, CancellationToken token = default)
|
||||
{
|
||||
if (IsReadOnly) return;
|
||||
var file = Path.Combine(rootDir, path); //_pathResolver.ResolvePath(path));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(file)!);
|
||||
var write = File.OpenWrite(file);
|
||||
await data.CopyToAsync(write);
|
||||
}
|
||||
}
|
||||
// public sealed class MemoryDataStore : IDataStore
|
||||
// {
|
||||
// private readonly System.Collections.Concurrent.ConcurrentDictionary<string, byte[]> _cache = new();
|
||||
|
||||
|
||||
// public bool IsReadOnly => false;
|
||||
|
||||
|
||||
// public ValueTask<bool> ExistsAsync(DataKey path, CancellationToken token = default)
|
||||
// => new(_cache.ContainsKey(path.ToString()));
|
||||
|
||||
// public ValueTask<Option<byte[]>> ReadAsync(DataKey path, CancellationToken token = default)
|
||||
// {
|
||||
// GD.PrintS("Read", path, _cache.TryGetValue(path.ToString(), out var data2), data2);
|
||||
// if (_cache.TryGetValue(path.ToString(), out var data))
|
||||
// return ValueTask.FromResult(Option<byte[]>.Some(data));
|
||||
|
||||
// // if not in cache, try next layer (and cache it)
|
||||
// // if (_nextLayer is not null)
|
||||
// // return ReadThroughAsync(path, token);
|
||||
|
||||
// return ValueTask.FromResult(Option<byte[]>.None);
|
||||
// }
|
||||
// public ValueTask WriteAsync(DataKey path, byte[] data, CancellationToken token = default)
|
||||
// {
|
||||
// _cache[path.ToString()] = data;
|
||||
// GD.PrintS("Write", path, _cache.TryGetValue(path.ToString(), out var data2), data2);
|
||||
|
||||
// // Optional: immediate flush to next layer
|
||||
// // if (_nextLayer is not null)
|
||||
// // {
|
||||
// // ms.Position = 0;
|
||||
// // await _nextLayer.WriteAsync(path, new MemoryStream(_cache[path]), token);
|
||||
// // }
|
||||
// return ValueTask.CompletedTask;
|
||||
// }
|
||||
|
||||
// public ValueTask DeleteAsync(DataKey path, CancellationToken token = default)
|
||||
// {
|
||||
// _cache.TryRemove(path.ToString(), out _);
|
||||
// return ValueTask.CompletedTask;
|
||||
// }
|
||||
|
||||
// // public async ValueTask FlushAsync(CancellationToken token = default)
|
||||
// // {
|
||||
// // if (_nextLayer is null) return;
|
||||
|
||||
// // foreach (var (path, data) in _cache)
|
||||
// // {
|
||||
// // using var ms = new MemoryStream(data);
|
||||
// // await _nextLayer.WriteAsync(path, ms, token);
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// public void Clear() => _cache.Clear();
|
||||
|
||||
// }
|
||||
|
||||
// public interface IDataCache<T>
|
||||
// {
|
||||
// ValueTask<Option<T>> GetDataAsync(DataKey key);
|
||||
// ValueTask SetDataAsync(DataKey key, T value, bool dirty = true);
|
||||
// ValueTask FlushDataAsync(DataKey key);
|
||||
// ValueTask FlushAllAsync();
|
||||
|
||||
|
||||
// }
|
||||
// public sealed class MemoryDataLayer<T> : IDataCache<T>
|
||||
// {
|
||||
// private readonly IDataStore _storage;
|
||||
// private readonly IDataSerializer<T> _serializer;
|
||||
// private readonly ConcurrentDictionary<DataKey, (T data, bool dirty)> _cache = new();
|
||||
// private readonly bool _autoCache;
|
||||
// private readonly bool _autoWriteThrough;
|
||||
|
||||
// public MemoryDataLayer(IDataStore storage, IDataSerializer<T> serializer, bool autoCache = true, bool autoWriteThrough = false)
|
||||
// {
|
||||
// _storage = storage;
|
||||
// _serializer = serializer;
|
||||
// _autoCache = autoCache;
|
||||
// _autoWriteThrough = autoWriteThrough;
|
||||
// }
|
||||
|
||||
// public async ValueTask<Option<T>> GetDataAsync(DataKey key)
|
||||
// {
|
||||
// if (_cache.TryGetValue(key, out var entry))
|
||||
// return Option<T>.Some(entry.data);
|
||||
|
||||
// var bytes = await _storage.ReadAsync(key);
|
||||
// if (!bytes.HasValue)
|
||||
// return Option<T>.None;
|
||||
|
||||
// var chunk = await _serializer.DeserializeAsync(bytes.Value);
|
||||
// if (_autoCache)
|
||||
// _cache[key] = (chunk, false);
|
||||
// return Option<T>.Some(chunk);
|
||||
// }
|
||||
|
||||
// public async ValueTask SetDataAsync(DataKey key, T chunk, bool dirty = true)
|
||||
// {
|
||||
// _cache[key] = (chunk, dirty);
|
||||
// if (_autoWriteThrough && dirty)
|
||||
// {
|
||||
// await FlushDataAsync(key, chunk);
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
|
||||
// public async ValueTask FlushDataAsync(DataKey key)
|
||||
// {
|
||||
|
||||
// if (!_cache.TryGetValue(key, out var entry) || !entry.dirty)
|
||||
// return;
|
||||
|
||||
// // var serialized = await _serializer.SerializeAsync(entry.data);
|
||||
// // await _storage.WriteAsync(key,serialized);
|
||||
// await FlushDataAsync(key, entry.data);
|
||||
// _cache[key] = (entry.data, false);
|
||||
// }
|
||||
// private async ValueTask FlushDataAsync(DataKey key, T data)
|
||||
// {
|
||||
|
||||
// var serialized = await _serializer.SerializeAsync(data);
|
||||
// await _storage.WriteAsync(key, serialized);
|
||||
// }
|
||||
|
||||
// public async ValueTask FlushAllAsync()
|
||||
// {
|
||||
// foreach (var id in _cache.Keys.ToArray())
|
||||
// await FlushDataAsync(id);
|
||||
// }
|
||||
// }
|
||||
public interface IFlushableStore<TDomainKey, TData>
|
||||
{
|
||||
ValueTask FlushAsync(
|
||||
IDataStore<TDomainKey, TData>? target,
|
||||
CancellationToken token = default);
|
||||
ValueTask FlushAsync(TDomainKey key, IDataStore<TDomainKey, TData>? target, CancellationToken token = default);
|
||||
}
|
||||
public interface ILoadedDataStore<TDomainKey, TData>
|
||||
{
|
||||
IEnumerable<(TDomainKey, TData)> GetLoadedData();
|
||||
Option<TData> TryGetData(TDomainKey key);
|
||||
}
|
||||
public sealed class MemoryDataLayer<TDomainKey, TData> : IDataStore<TDomainKey, TData>, IFlushableStore<TDomainKey, TData>, ILoadedDataStore<TDomainKey,TData> where TDomainKey : IEquatable<TDomainKey>
|
||||
{
|
||||
private readonly ConcurrentDictionary<TDomainKey, (TData data, bool dirty)> _cache = new();
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
public ValueTask<Option<TData>> ReadAsync(TDomainKey key, CancellationToken token = default)=>ValueTask.FromResult(Read(key));
|
||||
public ValueTask WriteAsync(TDomainKey key, TData value, CancellationToken token = default)
|
||||
// public ValueTask SetDataAsync(TDomainKey key, TData chunk, bool dirty = true)
|
||||
{
|
||||
_cache[key] = (value, true);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask FlushAsync(IDataStore<TDomainKey, TData>? target, CancellationToken token = default)
|
||||
{
|
||||
if (target is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var item in _cache)
|
||||
{
|
||||
if (item.Value.dirty)
|
||||
{
|
||||
await target.WriteAsync(item.Key, item.Value.data, token);
|
||||
_cache[item.Key] = (item.Value.data, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<bool> ExistsAsync(TDomainKey key, CancellationToken token = default) => ValueTask.FromResult(_cache.ContainsKey(key));
|
||||
|
||||
|
||||
public ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
_cache.TryRemove(key, out var _);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask FlushAsync(TDomainKey key, IDataStore<TDomainKey, TData> target, CancellationToken token = default)
|
||||
{
|
||||
if (_cache.TryGetValue(key, out var data))
|
||||
{
|
||||
await target.WriteAsync(key, data.data, token);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<(TDomainKey, TData)> GetLoadedData()
|
||||
{
|
||||
return _cache.Select(c=> (c.Key,c.Value.data));
|
||||
}
|
||||
|
||||
public Option<TData> TryGetData(TDomainKey key)
|
||||
{
|
||||
return _cache.GetValue(key).Map(f => f.data).ToStructOption();
|
||||
}
|
||||
|
||||
public Option<TData> Read(TDomainKey key)
|
||||
{
|
||||
if (_cache.TryGetValue(key, out var entry))
|
||||
return Option<TData>.Some(entry.data);
|
||||
return Option<TData>.None;
|
||||
}
|
||||
}
|
||||
public enum LayerPolicy
|
||||
{
|
||||
ReadOnly, // used only for reads (terrain gen)
|
||||
WriteThrough, // always updated when written
|
||||
WriteBack, // marked dirty and flushed later
|
||||
Cache // memory only, ephemeral
|
||||
}
|
||||
|
||||
public record DataLayer<TDomainKey, TData>(
|
||||
IDataStore<TDomainKey, TData> Store,
|
||||
LayerPolicy Policy,
|
||||
string Name);
|
||||
public interface ITwoTierStore<TKey, T> : IDataStore<TKey, T>, IFlushableStore<TKey, T>
|
||||
{
|
||||
IDataStore<TKey, T> Cache { get; }
|
||||
IDataStore<TKey, T> Persistent { get; }
|
||||
}
|
||||
|
||||
public sealed class TwoTierStore<TDomainKey, TData, TCache, TPersistent> : IDataStore<TDomainKey, TData>, IFlushableStore<TDomainKey, TData> where TCache : IDataStore<TDomainKey, TData>, IFlushableStore<TDomainKey, TData> where TPersistent : IDataStore<TDomainKey, TData>
|
||||
{
|
||||
|
||||
public TwoTierStore(TCache cached, TPersistent store)
|
||||
{
|
||||
Cached = cached;
|
||||
Store = store;
|
||||
}
|
||||
|
||||
public TCache Cached { get; }
|
||||
public TPersistent Store { get; }
|
||||
|
||||
public bool IsReadOnly => Cached.IsReadOnly && Store.IsReadOnly;
|
||||
|
||||
public async ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
await Cached.DeleteAsync(key, token);
|
||||
}
|
||||
|
||||
public async ValueTask<bool> ExistsAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
var exist = await Cached.ExistsAsync(key, token);
|
||||
if (exist)
|
||||
{
|
||||
return exist;
|
||||
}
|
||||
return await Store.ExistsAsync(key, token);
|
||||
}
|
||||
|
||||
public async ValueTask<Option<TData>> ReadAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
var data = await Cached.ReadAsync(key, token);
|
||||
if (data.HasValue)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
return await Store.ReadAsync(key, token);
|
||||
}
|
||||
public async ValueTask WriteAsync(TDomainKey key, TData value, CancellationToken token = default)
|
||||
{
|
||||
await Cached.WriteAsync(key, value, token);
|
||||
|
||||
}
|
||||
public async ValueTask Flush(CancellationToken token = default)
|
||||
{
|
||||
await Cached.FlushAsync(Store, token);
|
||||
}
|
||||
|
||||
public ValueTask FlushAsync(IDataStore<TDomainKey, TData>? target, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ValueTask FlushAsync(TDomainKey key, IDataStore<TDomainKey, TData> target, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
}
|
||||
public sealed class TwoTierStore<TDomainKey, TData, TCache> : IDataStore<TDomainKey, TData>, IFlushableStore<TDomainKey, TData>, ILoadedDataStore<TDomainKey,TData> where TCache : IDataStore<TDomainKey, TData>, IFlushableStore<TDomainKey, TData> , ILoadedDataStore<TDomainKey,TData>
|
||||
{
|
||||
|
||||
public TwoTierStore(TCache cached, IDataStore<TDomainKey,TData> store)
|
||||
{
|
||||
Cached = cached;
|
||||
Store = store;
|
||||
}
|
||||
|
||||
public TCache Cached { get; }
|
||||
public IDataStore<TDomainKey,TData> Store { get; }
|
||||
|
||||
public bool IsReadOnly => Cached.IsReadOnly && Store.IsReadOnly;
|
||||
|
||||
public async ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
await Cached.DeleteAsync(key, token);
|
||||
}
|
||||
|
||||
public async ValueTask<bool> ExistsAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
var exist = await Cached.ExistsAsync(key, token);
|
||||
if (exist)
|
||||
{
|
||||
return exist;
|
||||
}
|
||||
return await Store.ExistsAsync(key, token);
|
||||
}
|
||||
|
||||
public async ValueTask<Option<TData>> ReadAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
var data = await Cached.ReadAsync(key, token);
|
||||
if (data.HasValue)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
return await Store.ReadAsync(key, token);
|
||||
}
|
||||
|
||||
public async ValueTask WriteAsync(TDomainKey key, TData value, CancellationToken token = default)
|
||||
{
|
||||
await Cached.WriteAsync(key, value, token);
|
||||
|
||||
}
|
||||
public async ValueTask Flush(CancellationToken token = default)
|
||||
{
|
||||
await Cached.FlushAsync(Store, token);
|
||||
}
|
||||
|
||||
public ValueTask FlushAsync(IDataStore<TDomainKey, TData>? target, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ValueTask FlushAsync(TDomainKey key, IDataStore<TDomainKey, TData> target, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IEnumerable<(TDomainKey, TData)> GetLoadedData()
|
||||
{
|
||||
return Cached.GetLoadedData();
|
||||
}
|
||||
|
||||
public Option<TData> TryGetData(TDomainKey key)
|
||||
{
|
||||
return Cached.TryGetData(key);
|
||||
}
|
||||
public async ValueTask LoadChunkToMemory(TDomainKey key,Func<TDomainKey,TData> factory)
|
||||
{
|
||||
var s =await Store.ReadAsync(key);
|
||||
await Cached.WriteAsync(key,s.Or(()=>factory(key)));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
public sealed class LayeredDataStore<TDomainKey, TData> : IDataStore<TDomainKey, TData>, IFlushableStore<TDomainKey, TData> where TDomainKey : IEquatable<TDomainKey>
|
||||
{
|
||||
public bool IsReadOnly => _layers.All(item => item.Policy == LayerPolicy.ReadOnly);
|
||||
private readonly List<DataLayer<TDomainKey, TData>> _layers;
|
||||
public LayeredDataStore(IEnumerable<DataLayer<TDomainKey, TData>> dataStores)
|
||||
{
|
||||
_layers = dataStores.ToList();
|
||||
}
|
||||
public ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ValueTask<bool> ExistsAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ValueTask FlushAsync(IDataStore<TDomainKey, TData> target, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async ValueTask<Option<TData>> ReadAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
foreach (var item in _layers)
|
||||
{
|
||||
var data = await item.Store.ReadAsync(key, token);
|
||||
if (data.HasValue)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return Option<TData>.None;
|
||||
}
|
||||
|
||||
public async ValueTask WriteAsync(TDomainKey key, TData value, CancellationToken token = default)
|
||||
{
|
||||
if (_layers[0].Policy == LayerPolicy.ReadOnly || _layers[0].Store.IsReadOnly)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
await _layers[0].Store.WriteAsync(key,value, token);
|
||||
}
|
||||
|
||||
public ValueTask FlushAsync(TDomainKey key, IDataStore<TDomainKey, TData> target, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
}
|
||||
public readonly record struct DataKey(string Namespace,object Key);
|
||||
//TODO Replace with ChunkStorage as to sperate inmemory data and stored data
|
||||
|
||||
public interface IChunkManger<TVoxel, TChunkSize, TChunk> where TChunkSize : IChunkSize<TChunkSize> where TChunk : IVoxelChunk<TVoxel, TChunkSize>
|
||||
{
|
||||
ValueTask<TChunk> GetChunkAsync(ChunkPos<TChunkSize> chunkPos);
|
||||
ValueTask SetChunkAsync(ChunkPos<TChunkSize> chunkPos, TChunk chunk);
|
||||
Option<TChunk> TryGetChunk(ChunkPos<TChunkSize> chunkPos);
|
||||
IEnumerable<(ChunkPos<TChunkSize>,TChunk)> GetLoadedChunks();
|
||||
}
|
||||
public sealed class ChunkManger<TVoxel, TChunkSize, TChunkStore,TChunk> :IChunkManger<TVoxel,TChunkSize,TChunk> where TChunkSize : IChunkSize<TChunkSize> where TChunkStore : IDataStore<ChunkPos<TChunkSize>, TChunk>, IFlushableStore<ChunkPos<TChunkSize>, TChunk> , ILoadedDataStore<ChunkPos<TChunkSize>,TChunk> where TChunk : IVoxelChunk<TVoxel,TChunkSize>
|
||||
{
|
||||
private TChunkStore chunkStore;
|
||||
private readonly Func<TChunk> factory;
|
||||
|
||||
// public async ValueTask SetChunkAysnc(ChunkPos<TChunkSize> chunkPos, IVoxelChunk<TVoxel, TChunkSize> chunk)
|
||||
// {
|
||||
// await chunkStore.WriteAsync(chunkPos, chunk);
|
||||
// // await cachedData.WriteAsync(chunkPos, chunk);
|
||||
// }
|
||||
// public async ValueTask<Option<IVoxelChunk<TVoxel, TChunkSize>>> GetChunkAysnc(ChunkPos<TChunkSize> chunkPos)
|
||||
// {
|
||||
// var chunk = await chunkStore.ReadAsync(chunkPos);
|
||||
// // var chunk = await cachedData.ReadAsync(chunkPos);
|
||||
// return chunk;
|
||||
// if (chunk.HasValue)
|
||||
// {
|
||||
// return chunk;
|
||||
// }
|
||||
// // return await persisitenData.ReadAsync(chunkPos);
|
||||
// }
|
||||
|
||||
public ChunkManger(TChunkStore chunkStore, Func<TChunk> factory)
|
||||
{
|
||||
this.chunkStore = chunkStore;
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
// public ChunkManger(MemoryDataLayer<IVoxelChunk<TVoxel, TChunkSize>> memoryDataLayer)
|
||||
// {
|
||||
// this.memoryDataLayer = memoryDataLayer;
|
||||
// }
|
||||
// private MemoryDataLayer<IVoxelChunk<TVoxel, TChunkSize>> memoryDataLayer;
|
||||
public async ValueTask SetChunkAsync(ChunkPos<TChunkSize> chunkPos, TChunk voxelChunk)
|
||||
{
|
||||
await chunkStore.WriteAsync(chunkPos, voxelChunk);
|
||||
}
|
||||
public async ValueTask<TChunk> GetChunkAsync(ChunkPos<TChunkSize> chunkPos)
|
||||
{
|
||||
return (await chunkStore.ReadAsync(chunkPos)).Or(factory);
|
||||
}
|
||||
|
||||
public Option<TChunk> TryGetChunk(ChunkPos<TChunkSize> chunkPos)
|
||||
{
|
||||
return chunkStore.TryGetData(chunkPos);
|
||||
}
|
||||
|
||||
public IEnumerable<(ChunkPos<TChunkSize>, TChunk)> GetLoadedChunks()
|
||||
{
|
||||
return chunkStore.GetLoadedData();
|
||||
}
|
||||
// public void SetChunk(ChunkPos<TChunkSize> chunkPos, IVoxelChunk<TVoxel, TChunkSize> voxelChunk)
|
||||
// {
|
||||
// Task.Run(() => chunkStore.WriteAsync(chunkPos, voxelChunk)).GetAwaiter().GetResult();
|
||||
// }
|
||||
// public Option<IVoxelChunk<TVoxel, TChunkSize>> GetChunk(ChunkPos<TChunkSize> chunkPos)
|
||||
// {
|
||||
// return Task.Run(() => chunkStore.ReadAsync(chunkPos)).GetAwaiter().GetResult().Result;
|
||||
// }
|
||||
|
||||
}
|
||||
public interface IKeyMapper<TDomainKey, TInnerKey>
|
||||
{
|
||||
TInnerKey MapKey(TDomainKey key);
|
||||
}
|
||||
public class DataStoreMapper<TDomainKey, TData, TInnerKey>(IDataStore<TInnerKey, TData> innerStore, IKeyMapper<TDomainKey, TInnerKey> mapper) : IDataStore<TDomainKey,TData>
|
||||
{
|
||||
private readonly IDataStore<TInnerKey, TData> innerStore = innerStore;
|
||||
private readonly IKeyMapper<TDomainKey, TInnerKey> mapper = mapper;
|
||||
|
||||
public bool IsReadOnly => innerStore.IsReadOnly;
|
||||
|
||||
public ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default) => innerStore.DeleteAsync(mapper.MapKey(key), token);
|
||||
|
||||
public ValueTask<bool> ExistsAsync(TDomainKey key, CancellationToken token = default) => innerStore.ExistsAsync(mapper.MapKey(key), token);
|
||||
public ValueTask<Option<TData>> ReadAsync(TDomainKey key, CancellationToken token = default) => innerStore.ReadAsync(mapper.MapKey(key), token);
|
||||
|
||||
public ValueTask WriteAsync(TDomainKey key, TData value, CancellationToken token = default) => innerStore.WriteAsync(mapper.MapKey(key), value, token);
|
||||
}
|
||||
public class DataStoreSerializer<TDomainKey, TData,TInnerData>(IDataStore<TDomainKey, TInnerData> innerStore,IDataSerializer<TData,TInnerData> serializer) : IDataStore<TDomainKey,TData>
|
||||
{
|
||||
private readonly IDataStore<TDomainKey, TInnerData> innerStore = innerStore;
|
||||
private readonly IDataSerializer<TData,TInnerData> serializer = serializer;
|
||||
|
||||
public bool IsReadOnly => innerStore.IsReadOnly;
|
||||
|
||||
public ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default) => innerStore.DeleteAsync(key, token);
|
||||
|
||||
public ValueTask<bool> ExistsAsync(TDomainKey key, CancellationToken token = default) => innerStore.ExistsAsync(key, token);
|
||||
public async ValueTask<Option<TData>> ReadAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
var data = await innerStore.ReadAsync(key, token);
|
||||
return data.HasValue?Option<TData>.Some(await serializer.DeserializeAsync(data.Value)):Option<TData>.None;
|
||||
}
|
||||
|
||||
public async ValueTask WriteAsync(TDomainKey key, TData value, CancellationToken token = default) => await innerStore.WriteAsync(key, await serializer.SerializeAsync(value), token);
|
||||
}
|
||||
public sealed class FuncDataMapper<TDomainKey,TNewKey>(Func<TDomainKey, TNewKey> toKey) : IKeyMapper<TDomainKey,TNewKey>
|
||||
{
|
||||
private readonly Func<TDomainKey, TNewKey> toKey = toKey;
|
||||
|
||||
public TNewKey MapKey(TDomainKey key) => toKey(key);
|
||||
}
|
||||
public sealed class FuncDataSerializer<TData, TInnerData>(Func<TInnerData, TData> toData, Func<TData, TInnerData> toBytes) : IDataSerializer<TData, TInnerData>
|
||||
{
|
||||
private readonly Func<TInnerData, TData> toData = toData;
|
||||
private readonly Func<TData, TInnerData> toBytes = toBytes;
|
||||
|
||||
public ValueTask<TData> DeserializeAsync(TInnerData bytes) => ValueTask.FromResult(toData(bytes));
|
||||
|
||||
public ValueTask<TInnerData> SerializeAsync(TData value) => ValueTask.FromResult(toBytes(value));
|
||||
}
|
||||
public class DataRepository<TDomainKey, TData, TInnerKey,TInnerData> : IDataStore<TDomainKey,TData>
|
||||
{
|
||||
private readonly IDataStore<TInnerKey,TInnerData> _store;
|
||||
private readonly IKeyMapper<TDomainKey,TInnerKey> _mapper;
|
||||
private readonly IDataSerializer<TData,TInnerData> _serializer;
|
||||
|
||||
public DataRepository(IDataStore<TInnerKey,TInnerData> store, IKeyMapper<TDomainKey,TInnerKey> mapper, IDataSerializer<TData,TInnerData> serializer, bool isReadOnly = false)
|
||||
{
|
||||
_store = store;
|
||||
_mapper = mapper;
|
||||
_serializer = serializer;
|
||||
IsReadOnly = isReadOnly;
|
||||
}
|
||||
|
||||
public bool IsReadOnly { get; }
|
||||
|
||||
public ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ValueTask<bool> ExistsAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// public async ValueTask<Option<TData>> GetAsync(TDomainKey key)
|
||||
// {
|
||||
// var dataKey = _mapper.ToDataKey(key);
|
||||
// var bytes = await _store.ReadAsync(dataKey);
|
||||
// if (!bytes.HasValue) return Option<TData>.None;
|
||||
// return Option<TData>.Some(await _serializer.DeserializeAsync(bytes.Value));
|
||||
// }
|
||||
|
||||
public async ValueTask<Option<TData>> ReadAsync(TDomainKey key, CancellationToken token = default)
|
||||
{
|
||||
var dataKey = _mapper.MapKey(key);
|
||||
var bytes = await _store.ReadAsync(dataKey,token);
|
||||
if (!bytes.HasValue) return Option<TData>.None;
|
||||
return Option<TData>.Some(await _serializer.DeserializeAsync(bytes.Value));
|
||||
}
|
||||
|
||||
// public async ValueTask SetAsync(TDomainKey key, TData data)
|
||||
// {
|
||||
// var dataKey = _mapper.ToDataKey(key);
|
||||
// var bytes = await _serializer.SerializeAsync(data);
|
||||
// await _store.WriteAsync(dataKey, bytes);
|
||||
// }
|
||||
|
||||
public async ValueTask WriteAsync(TDomainKey key, TData value, CancellationToken token = default)
|
||||
{
|
||||
var dataKey = _mapper.MapKey(key);
|
||||
var bytes = await _serializer.SerializeAsync(value);
|
||||
await _store.WriteAsync(dataKey, bytes,token);
|
||||
}
|
||||
}
|
||||
public interface IDataCompressor {
|
||||
byte[] Compress(byte[] bytes);
|
||||
byte[] DeCompress(byte[] bytes);
|
||||
}
|
||||
public sealed class CompressedDataStore<TKey>(IDataStore<TKey,byte[]> inner, IDataCompressor dataCompressor) : IDataStore<TKey,byte[]>
|
||||
{
|
||||
private readonly IDataStore<TKey,byte[]> inner = inner;
|
||||
private readonly IDataCompressor dataCompressor = dataCompressor;
|
||||
|
||||
public bool IsReadOnly => inner.IsReadOnly;
|
||||
|
||||
public ValueTask DeleteAsync(TKey path, CancellationToken token = default) => inner.DeleteAsync(path, token);
|
||||
|
||||
public ValueTask<bool> ExistsAsync(TKey path, CancellationToken token = default) => inner.ExistsAsync(path, token);
|
||||
|
||||
public async ValueTask<Option<byte[]>> ReadAsync(TKey path, CancellationToken token = default)
|
||||
{
|
||||
var result = await inner.ReadAsync(path, token);
|
||||
if (!result.HasValue)
|
||||
return Option<byte[]>.None;
|
||||
return Option<byte[]>.Some(dataCompressor.DeCompress(result.Value));
|
||||
|
||||
}
|
||||
|
||||
public async ValueTask WriteAsync(TKey path, byte[] data, CancellationToken token = default)
|
||||
{
|
||||
await inner.WriteAsync(path, dataCompressor.Compress(data), token);
|
||||
}
|
||||
}public static class TwoTierStoreFactory
|
||||
{
|
||||
public static TwoTierStore<TKey, T, MemoryDataLayer<TKey, T>, TStore>
|
||||
MemoryCached<TKey, T, TStore>(TStore store, LayerPolicy policy = LayerPolicy.WriteBack)
|
||||
where TStore : IDataStore<TKey, T> where TKey : IEquatable<TKey>
|
||||
{
|
||||
var cache = new MemoryDataLayer<TKey, T>();
|
||||
return new TwoTierStore<TKey, T, MemoryDataLayer<TKey, T>, TStore>(cache, store);
|
||||
}
|
||||
|
||||
public static TwoTierStore<TKey, T, TCache, TStore>
|
||||
Create<TKey, T, TCache, TStore>(TCache cache, TStore store, LayerPolicy policy = LayerPolicy.WriteBack)
|
||||
where TCache : IDataStore<TKey, T>, IFlushableStore<TKey, T>
|
||||
where TStore : IDataStore<TKey, T>
|
||||
=> new(cache, store);
|
||||
|
||||
public static TwoTierStore<TKey, T, MemoryDataLayer<TKey, T>, TStore>
|
||||
WithMemoryCache<TKey, T, TStore>(
|
||||
this TStore store,
|
||||
LayerPolicy policy = LayerPolicy.WriteBack)
|
||||
where TKey : IEquatable<TKey>
|
||||
where TStore : IDataStore<TKey, T>
|
||||
{
|
||||
var cache = new MemoryDataLayer<TKey, T>();
|
||||
return new TwoTierStore<TKey, T, MemoryDataLayer<TKey, T>, TStore>(cache, store);
|
||||
}
|
||||
public static TwoTierStoreBuilder<TKey, T> For<TKey, T>()
|
||||
where TKey : IEquatable<TKey>
|
||||
=> new TwoTierStoreBuilder<TKey, T>();
|
||||
}
|
||||
public readonly struct TwoTierStoreBuilder<TKey, T>
|
||||
where TKey : IEquatable<TKey>
|
||||
{
|
||||
public TwoTierStore<TKey, T, MemoryDataLayer<TKey, T>, TStore>
|
||||
MemoryCached<TStore>(TStore store, LayerPolicy policy = LayerPolicy.WriteBack)
|
||||
where TStore : IDataStore<TKey, T>
|
||||
{
|
||||
var cache = new MemoryDataLayer<TKey, T>();
|
||||
return new TwoTierStore<TKey, T, MemoryDataLayer<TKey, T>, TStore>(cache, store);
|
||||
}
|
||||
public TwoTierStore<TKey, T, MemoryDataLayer<TKey, T>>
|
||||
MemoryCached(IDataStore<TKey,T> store, LayerPolicy policy = LayerPolicy.WriteBack)
|
||||
|
||||
{
|
||||
var cache = new MemoryDataLayer<TKey, T>();
|
||||
return new TwoTierStore<TKey, T, MemoryDataLayer<TKey, T>>(cache, store);
|
||||
}
|
||||
}
|
||||
1
src/voxelgame/Voxels/Worlds/VoxelSerializer.cs.uid
Normal file
1
src/voxelgame/Voxels/Worlds/VoxelSerializer.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c3rdxjvybaq0x
|
||||
1087
src/voxelgame/Voxels/Worlds/VoxelWorld.cs
Normal file
1087
src/voxelgame/Voxels/Worlds/VoxelWorld.cs
Normal file
File diff suppressed because it is too large
Load Diff
1
src/voxelgame/Voxels/Worlds/VoxelWorld.cs.uid
Normal file
1
src/voxelgame/Voxels/Worlds/VoxelWorld.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dtg4km6ikm0qf
|
||||
30
src/voxelgame/Voxels/clip_plane.gdshader
Normal file
30
src/voxelgame/Voxels/clip_plane.gdshader
Normal file
@@ -0,0 +1,30 @@
|
||||
shader_type spatial;
|
||||
uniform sampler2D _albedo : source_color;
|
||||
uniform mat4 cutplane;
|
||||
varying vec3 wvtx;
|
||||
|
||||
void vertex() {
|
||||
wvtx = (MODEL_MATRIX * vec4(VERTEX,1.0)).xyz;
|
||||
// Called for every vertex the material is visible on.
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
|
||||
vec3 planeNormal = normalize((-cutplane[1].xyz));
|
||||
float planeDistance = dot(planeNormal , cutplane[3].xyz);
|
||||
float vertexDistance = dot(planeNormal,wvtx);
|
||||
float dist = vertexDistance - planeDistance;
|
||||
if (dist < 0.0)
|
||||
{
|
||||
discard;
|
||||
}
|
||||
|
||||
|
||||
ALBEDO = texture(_albedo, UV).rgb;
|
||||
// Called for every pixel the material is visible on.
|
||||
}
|
||||
|
||||
//void light() {
|
||||
// // Called for every pixel for every light affecting the material.
|
||||
// // Uncomment to replace the default light processing function with this one.
|
||||
//}
|
||||
1
src/voxelgame/Voxels/clip_plane.gdshader.uid
Normal file
1
src/voxelgame/Voxels/clip_plane.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b0dpogulmm2p7
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace hamsterbyte.DeveloperConsole{
|
||||
using System;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class ConsoleCommandAttribute : Attribute{
|
||||
public string Prefix = string.Empty;
|
||||
public string Description = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://pducp5spu5en
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public static class DCColorTheme{
|
||||
public static readonly Dictionary<string, Color> Output = new(){
|
||||
{ "Number", Colors.White },
|
||||
{ "Symbol", Colors.White },
|
||||
{ "Method", Colors.DeepSkyBlue },
|
||||
{ "Member Variable", Colors.ForestGreen }
|
||||
};
|
||||
|
||||
public static readonly RegionColor[] Regions ={
|
||||
//Error color
|
||||
new(){ Color = Colors.DeepPink, Start = "#!", End = "!#" },
|
||||
//Context Color
|
||||
new(){ Color = Colors.Goldenrod, Start = "$", End = "::" },
|
||||
//Comment Color
|
||||
new(){ Color = Colors.PaleGreen, Start = "/*", End = "*/" }
|
||||
};
|
||||
|
||||
public static readonly Dictionary<string, string> Suggestions = new(){
|
||||
{ "Method", Colors.DeepSkyBlue.ToHtml() },
|
||||
{ "Type", Colors.ForestGreen.ToHtml() }
|
||||
};
|
||||
}
|
||||
|
||||
public struct RegionColor{
|
||||
public string Start;
|
||||
public string End;
|
||||
public Color Color;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://baxk18v2sa2l5
|
||||
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Godot;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public static partial class CommandInterpreter{
|
||||
public static (Node currentNode, Dictionary<string, MethodInfo> commands) Context =>
|
||||
(_currentNode, _commands);
|
||||
|
||||
public static Node CurrentNode => _currentNode;
|
||||
private static Node _currentNode;
|
||||
private static string _lastCommand;
|
||||
public static string LastCommand => _lastCommand;
|
||||
public static bool Busy{ get; private set; }
|
||||
|
||||
#region INITIALIZE
|
||||
|
||||
public static void Initialize(){
|
||||
bool success = TryInitialize();
|
||||
DC.Print($"Command Interpreter => {success.OKFail()}");
|
||||
if (success) DCCrosshair.Initialize();
|
||||
}
|
||||
|
||||
private static bool TryInitialize(){
|
||||
try{
|
||||
DC.SetSuppressionLevel(DC.PrintSuppression.All);
|
||||
ChangeContext("/root");
|
||||
GetStaticCommands(out _staticCommands);
|
||||
_commands = _staticCommands;
|
||||
GetInstanceCommands(out _instanceCommands, _currentNode);
|
||||
DC.SetSuppressionLevel(DC.Instance.PrintSuppression);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e){
|
||||
e.PrintToDC();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region INTERPRET
|
||||
|
||||
public static async Task Interpret(string commandString){
|
||||
try{
|
||||
if (commandString == "") return;
|
||||
_lastCommand = commandString;
|
||||
if (TryShortcuts(commandString)) return;
|
||||
if (TryMath(commandString)) return;
|
||||
ConsoleCommand command = ParseConsoleCommand(commandString);
|
||||
if (command is not null) await ExecuteConsoleCommand(command);
|
||||
}
|
||||
catch (Exception e){
|
||||
HandleDCError(e);
|
||||
}
|
||||
finally{
|
||||
Command.ResetToken();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryShortcuts(string commandString){
|
||||
for (int i = 0; i < _shortcuts.Length; i++){
|
||||
if (!commandString.StartsWith(_shortcuts[i].Character)) continue;
|
||||
_shortcuts[i].Command.Invoke(commandString);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static readonly char[] _validExpressionStartChars ={ '(', '-' };
|
||||
|
||||
private static bool TryMath(string commandString){
|
||||
if (!char.IsNumber(commandString[0]) && !_validExpressionStartChars.Contains(commandString[0])) return false;
|
||||
double result = Convert.ToDouble(new DataTable().Compute(commandString, null));
|
||||
DC.Print(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task ExecuteConsoleCommand(ConsoleCommand command){
|
||||
CommandReturnType returnType = GetCommandReturnType(command.Method);
|
||||
if ((int)returnType <= 1){
|
||||
// If not a task, execute without waiting and print result if it's not void
|
||||
ExecuteAndPrintResult(command);
|
||||
}
|
||||
else{
|
||||
// If it's a task, execute and await it, then print result if it has one
|
||||
await ExecuteAndPrintResultAsync(command, returnType);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExecuteAndPrintResult(ConsoleCommand command){
|
||||
Type returnType = command.Method.ReturnType;
|
||||
dynamic result = command.Method.Invoke(_currentNode, command.Params);
|
||||
|
||||
if (returnType != typeof(void)){
|
||||
DC.Print(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static async Task ExecuteAndPrintResultAsync(ConsoleCommand command, CommandReturnType returnType){
|
||||
DisplayServer.VSyncMode tVsync = DisplayServer.WindowGetVsyncMode();
|
||||
DisplayServer.WindowSetVsyncMode(0);
|
||||
dynamic task = command.Method.Invoke(_currentNode, command.Params);
|
||||
if (task is not null){
|
||||
DC.Print("Processing...");
|
||||
await task;
|
||||
dynamic result = task.GetType().GetProperty("Result")?.GetValue(task, null);
|
||||
if (result is not null && returnType == CommandReturnType.TaskWithResult){
|
||||
DC.Print(result);
|
||||
}
|
||||
}
|
||||
else{
|
||||
throw new DCInvalidCommandException(command.Method.Name);
|
||||
}
|
||||
DisplayServer.WindowSetVsyncMode(tVsync);
|
||||
}
|
||||
|
||||
private static ConsoleCommand ParseConsoleCommand(string commandString){
|
||||
return ConsoleCommand.FromString(commandString, _commands);
|
||||
}
|
||||
|
||||
private static void HandleDCError(Exception e){
|
||||
while (e.InnerException is not null){
|
||||
e = e.InnerException;
|
||||
}
|
||||
|
||||
e.PrintToDC();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region COMMANDS
|
||||
|
||||
private static Dictionary<string, MethodInfo> _commands = new();
|
||||
public static Dictionary<string, MethodInfo> Commands => _commands;
|
||||
public static DCDelegates.onConsoleCommandsUpdated OnGetStaticCommands;
|
||||
|
||||
private static Dictionary<string, MethodInfo> _instanceCommands = new();
|
||||
public static Dictionary<string, MethodInfo> InstanceCommands => _instanceCommands;
|
||||
public static DCDelegates.onConsoleCommandsUpdated OnGetInstanceCommands;
|
||||
|
||||
private static Dictionary<string, MethodInfo> _staticCommands = new();
|
||||
public static Dictionary<string, MethodInfo> StaticCommands => _staticCommands;
|
||||
|
||||
private static void GetStaticCommands(out Dictionary<string, MethodInfo> commands){
|
||||
commands = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(assembly => assembly.GetTypes())
|
||||
.SelectMany(type => type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)
|
||||
.Where(method => method.GetCustomAttributes(typeof(ConsoleCommandAttribute), true).Any()))
|
||||
.OrderBy(method => {
|
||||
ConsoleCommandAttribute commandAttribute = method.GetCustomAttribute<ConsoleCommandAttribute>();
|
||||
return commandAttribute?.Prefix ?? string.Empty;
|
||||
})
|
||||
.ThenBy(method => method.Name)
|
||||
.ToDictionary(method => {
|
||||
ConsoleCommandAttribute commandAttribute = method.GetCustomAttribute<ConsoleCommandAttribute>();
|
||||
string prefix = commandAttribute?.Prefix != string.Empty
|
||||
? commandAttribute?.Prefix + '.'
|
||||
: string.Empty;
|
||||
return $"{prefix}{method.Name}";
|
||||
});
|
||||
|
||||
OnGetStaticCommands?.Invoke(commands);
|
||||
}
|
||||
|
||||
private static void GetInstanceCommands(out Dictionary<string, MethodInfo> commands, object targetObject){
|
||||
commands = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(assembly => assembly.GetTypes()
|
||||
.Where(type => type == targetObject.GetType()))
|
||||
.SelectMany(type => type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(method => method.GetCustomAttributes(typeof(ConsoleCommandAttribute), true).Any()))
|
||||
.OrderBy(method => method.Name)
|
||||
.ToDictionary(method => {
|
||||
ConsoleCommandAttribute commandAttribute = method.GetCustomAttribute<ConsoleCommandAttribute>();
|
||||
string prefix = commandAttribute?.Prefix != string.Empty
|
||||
? commandAttribute?.Prefix + '.'
|
||||
: string.Empty;
|
||||
return $"{prefix}{targetObject.GetType().BaselessString()}.{method.Name}";
|
||||
});
|
||||
|
||||
OnGetInstanceCommands?.Invoke(commands);
|
||||
}
|
||||
|
||||
private static void ResetContextCommands(){
|
||||
foreach (string key in _instanceCommands.Keys){
|
||||
_commands.Remove(key);
|
||||
}
|
||||
|
||||
_instanceCommands.Clear();
|
||||
}
|
||||
|
||||
private static void MergeConsoleCommands(){
|
||||
foreach (KeyValuePair<string, MethodInfo> commandPair in _instanceCommands){
|
||||
_commands.TryAdd(commandPair.Key, commandPair.Value);
|
||||
}
|
||||
|
||||
OnConsoleCommandsUpdated?.Invoke(_commands);
|
||||
}
|
||||
|
||||
public static DCDelegates.onConsoleCommandsUpdated OnConsoleCommandsUpdated;
|
||||
|
||||
private static void UpdateContextCommands(Node selectedNode){
|
||||
ResetContextCommands();
|
||||
GetInstanceCommands(out _instanceCommands, selectedNode);
|
||||
MergeConsoleCommands();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static CommandReturnType GetCommandReturnType(MethodInfo methodInfo){
|
||||
Type returnType = methodInfo.ReturnType;
|
||||
|
||||
if (returnType == typeof(void)) return CommandReturnType.None;
|
||||
if (returnType == typeof(Task)) return CommandReturnType.Task;
|
||||
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
|
||||
return CommandReturnType.TaskWithResult;
|
||||
return CommandReturnType.Type;
|
||||
}
|
||||
|
||||
private enum CommandReturnType{
|
||||
None,
|
||||
Type,
|
||||
Task,
|
||||
TaskWithResult
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfu0pu0m1m22j
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public partial class CommandInterpreter{
|
||||
public static DCDelegates.onContextChanged OnContextChanged;
|
||||
|
||||
public static void ChangeContext(string path){
|
||||
Node selectedNode;
|
||||
if (path.StartsWith("/.")){
|
||||
ContextLevelUp(path.TrimStart('/').AsSpan(), out selectedNode);
|
||||
}
|
||||
else{
|
||||
ContextFromPath(path, out selectedNode);
|
||||
}
|
||||
|
||||
if (selectedNode is null) throw new DCException($"'{path}' does not exist.");
|
||||
|
||||
UpdateContextCommands(selectedNode);
|
||||
_currentNode = selectedNode;
|
||||
OnContextChanged?.Invoke(Context);
|
||||
}
|
||||
|
||||
private static void ContextLevelUp(ReadOnlySpan<char> trailingSpan, out Node selectedNode){
|
||||
selectedNode = _currentNode;
|
||||
for (int i = 0; i < trailingSpan.Length; i++){
|
||||
if (selectedNode is null || trailingSpan[i] != '.') break;
|
||||
Node parent = selectedNode.GetParentOrNull<Node>();
|
||||
if (parent is not null){
|
||||
selectedNode = parent;
|
||||
}
|
||||
else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ContextFromPath(string path, out Node selectedNode){
|
||||
selectedNode = DC.Instance.GetNodeOrNull(
|
||||
path.StartsWith("/root")
|
||||
? path //path is absolute
|
||||
: $"{DC.CurrentNode.GetPath()}{path}" //path is relative
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bk1byj8vgtc4e
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public partial class CommandInterpreter{
|
||||
private static void GetHelp(string commandString){
|
||||
if (commandString.Length < 2){
|
||||
Help(_commands);
|
||||
}
|
||||
else{
|
||||
if (commandString[1] == '?'){
|
||||
Help(_instanceCommands);
|
||||
}
|
||||
else{
|
||||
Help(_commands, commandString.TrimStart('?'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Help(Dictionary<string, MethodInfo> commands, string searchString = ""){
|
||||
foreach (KeyValuePair<string, MethodInfo> c in commands){
|
||||
string description = c.Value.GetCustomAttribute<ConsoleCommandAttribute>()?.Description;
|
||||
if (!c.Key.ToLower().Contains(searchString.ToLower()) && !description!.ToLower().Contains(searchString.ToLower())) continue;
|
||||
int line = DC.Instance.LastOutputLineIndex;
|
||||
ParameterInfo[] infos = c.Value.GetParameters();
|
||||
StringBuilder b = new();
|
||||
b.Append($"{c.Key}(");
|
||||
for (int i = 0; i < infos.Length; i++){
|
||||
b.Append($"{(i == 0 ? string.Empty : ", ")}{infos[i].ParameterType.BaselessString()} {infos[i].Name}");
|
||||
}
|
||||
|
||||
b.Append(')');
|
||||
string formattedString = b.ToString();
|
||||
foreach (KeyValuePair<string, string> kvp in DCExtensions.TypeReplacementStrings){
|
||||
string replace = formattedString.Replace(kvp.Key, kvp.Value);
|
||||
formattedString = replace;
|
||||
}
|
||||
|
||||
DC.Print(formattedString);
|
||||
if (description == string.Empty) continue;
|
||||
DC.IncreaseIndent();
|
||||
DC.Print($"/* {description} */");
|
||||
DC.DecreaseIndent();
|
||||
DC.Instance.CallDeferred(nameof(DC.Instance.FoldLine), line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://uajav2gns4to
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public partial class CommandInterpreter{
|
||||
private static readonly (char Character, Action<string> Command)[] _shortcuts ={
|
||||
new('/', ChangeContext),
|
||||
new('?', GetHelp)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://xdjd61gnvv5g
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public partial class DC{
|
||||
/// <summary>
|
||||
/// Quit the game
|
||||
/// </summary>
|
||||
[ConsoleCommand(Prefix = "Application", Description = "Quit the game")]
|
||||
private static void Quit(){
|
||||
Instance.GetTree().Quit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cjkfon8d3lvm4
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public static class Command {
|
||||
|
||||
|
||||
private static CancellationTokenSource _cancellationTokenSource = new();
|
||||
public static CancellationToken Token => _cancellationTokenSource.Token;
|
||||
public static DCDelegates.onCallback OnCancel;
|
||||
|
||||
[ConsoleCommand(Prefix = "Command",
|
||||
Description = "Cancel all running console commands that use the command token.")]
|
||||
private static void Cancel(){
|
||||
_cancellationTokenSource.Cancel();
|
||||
OnCancel?.InvokeAwaited();
|
||||
}
|
||||
|
||||
public static void ResetToken(){
|
||||
_cancellationTokenSource.Dispose();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqf0fhngjcpf
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public static partial class OutputCommands{
|
||||
/// <summary>
|
||||
/// Clear the output console
|
||||
/// </summary>
|
||||
[ConsoleCommand(Prefix = "Output", Description = "Clear all output and command history from the console")]
|
||||
private static void Clear(){
|
||||
DC.OnClear?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bp8n8x32b1x5w
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public class ConsoleCommand{
|
||||
public MethodInfo Method;
|
||||
public object[] Params;
|
||||
|
||||
public override string ToString() => $"{Method.Name} ({Params?.Length ?? 0})";
|
||||
|
||||
|
||||
private ConsoleCommand(MethodInfo method, object[] parameters){
|
||||
Method = method;
|
||||
Params = parameters;
|
||||
}
|
||||
|
||||
|
||||
public static ConsoleCommand FromString(string commandString, Dictionary<string, MethodInfo> commands){
|
||||
if (string.IsNullOrEmpty(commandString)){
|
||||
throw new DCException("Cannot parse an empty command string.");
|
||||
}
|
||||
|
||||
string[] decomposedCommand = DecomposeCommandString(commandString);
|
||||
|
||||
if (!commands.TryGetValue(decomposedCommand[0], out MethodInfo method)){
|
||||
throw new DCInvalidCommandException(decomposedCommand[0]);
|
||||
}
|
||||
|
||||
ParameterInfo[] commandParameterInfos = method.GetParameters();
|
||||
ValidateParameterCount(commandParameterInfos.Length, decomposedCommand.Length - 1, method.Name);
|
||||
|
||||
object[] parameters = ParseParameters(decomposedCommand, commandParameterInfos);
|
||||
|
||||
return new ConsoleCommand(method, parameters);
|
||||
}
|
||||
|
||||
private static string[] DecomposeCommandString(string commandString){
|
||||
return commandString.Split(new[]{ '(', ',', ')' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
private static void ValidateParameterCount(int expectedCount, int actualCount, string methodName){
|
||||
if (expectedCount != actualCount){
|
||||
throw new DCParameterMismatchException(methodName, expectedCount);
|
||||
}
|
||||
}
|
||||
|
||||
private static object[] ParseParameters(string[] decomposedCommand, ParameterInfo[] commandParameterInfos){
|
||||
object[] parameters = new object[commandParameterInfos.Length];
|
||||
|
||||
for (int i = 1; i < decomposedCommand.Length; i++){
|
||||
Type parameterType = commandParameterInfos[i - 1].ParameterType;
|
||||
string parameterString = decomposedCommand[i].Trim();
|
||||
|
||||
if (TryParseParameter(parameterType, parameterString, out object parsedValue)){
|
||||
parameters[i - 1] = parsedValue;
|
||||
}
|
||||
else{
|
||||
throw new DCInvalidParameterFormatException(parameterString, parameterType);
|
||||
}
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private static bool TryParseParameter(Type parameterType, string parameterString, out object parsedValue){
|
||||
if (parameterType == typeof(string)){
|
||||
parsedValue = parameterString;
|
||||
return true;
|
||||
}
|
||||
|
||||
MethodInfo parseMethod = parameterType.GetMethod("Parse", new[]{ typeof(string) });
|
||||
|
||||
if (parseMethod is not null){
|
||||
try{
|
||||
parsedValue = parseMethod.Invoke(null, new object[]{ parameterString });
|
||||
return true;
|
||||
}
|
||||
catch{
|
||||
throw new DCParseFailureException(parameterType);
|
||||
}
|
||||
}
|
||||
|
||||
parsedValue = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://lsnwo7ujgwf0
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Godot;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole{
|
||||
public partial class DC{
|
||||
/// <summary>
|
||||
/// Print paths to immediate children of context
|
||||
/// </summary>
|
||||
[ConsoleCommand(Prefix = "Context", Description = "Print a list of immediate children in the current context")]
|
||||
private static void GetChildren(){
|
||||
if (CurrentNode.GetChildCount() > 0){
|
||||
foreach (Node node in CurrentNode.GetChildren()){
|
||||
if (node.Name == "DeveloperConsole") continue;
|
||||
Print(node.GetPath());
|
||||
}
|
||||
}
|
||||
else{
|
||||
throw new DCException("Context contains no children.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print all paths to all children in context, includes internal
|
||||
/// </summary>
|
||||
[ConsoleCommand(Prefix = "Context",
|
||||
Description = "Print a list of all children in the current context. Includes internal")]
|
||||
private static void GetAllChildren(){
|
||||
if (CurrentNode.GetChildCount() > 0){
|
||||
GetChildren(CurrentNode);
|
||||
}
|
||||
else{
|
||||
throw new DCException("Context contains no children");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print all node paths in the scene tree
|
||||
/// </summary>
|
||||
[ConsoleCommand(Prefix = "Context", Description = "Print a list of all nodes in the tree")]
|
||||
private static void GetTree(){
|
||||
if (Instance.GetTree().Root.GetChildCount() != 0){
|
||||
GetChildren(Instance.GetTree().Root);
|
||||
}
|
||||
else{
|
||||
throw new DCException("Tree contains no children");
|
||||
}
|
||||
}
|
||||
|
||||
private static void GetChildren(Node targetNode){
|
||||
if (targetNode.GetChildCount() <= 0) return;
|
||||
foreach (Node node in targetNode.GetChildren()){
|
||||
if (node.GetPath().ToString()!.Contains("DeveloperConsole")) continue;
|
||||
Print(node.GetPath());
|
||||
IncreaseIndent();
|
||||
GetChildren(node);
|
||||
DecreaseIndent();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively search children of the current node for a child with a given name
|
||||
/// If found, set current node to search result
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the node. Case Sensitive</param>
|
||||
[ConsoleCommand(Prefix = "Context", Description =
|
||||
"Search recursively by name through children in the current context. Print all found nodes to console.")]
|
||||
private static void FindChild(string name){
|
||||
if (FindIn(name, CurrentNode).Count == 0)
|
||||
throw new DCException($"Node with name '{name}' does not exist in the current context.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively search entire scene tree for a node with a given name
|
||||
/// If found, set current node to search result
|
||||
/// Prefer using FindChild for performance
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the node. Case Sensitive</param>
|
||||
[ConsoleCommand(Prefix = "Context", Description =
|
||||
"Search recursively by name through all nodes in scene tree. Print all found nodes to console")]
|
||||
private static void Find(string name){
|
||||
if (FindIn(name, Instance.GetTree().Root).Count == 0)
|
||||
throw new DCException($"Node with name '{name}' does not exist in the scene tree.");
|
||||
}
|
||||
|
||||
private static List<string> FindIn(string name, Node context){
|
||||
Queue<Node> _treeQueue = new();
|
||||
List<string> foundNodes = new();
|
||||
Node node;
|
||||
for (int i = 0; i < context.GetChildren().Count; i++){
|
||||
node = context.GetChildren()[i];
|
||||
if (node.Name == "DeveloperConsole") continue;
|
||||
_treeQueue.Enqueue(node);
|
||||
}
|
||||
|
||||
while (_treeQueue.Count > 0){
|
||||
node = _treeQueue.Dequeue();
|
||||
if (node.Name.ToString()!.ToLower().Contains(name.ToLower())){
|
||||
foundNodes.Add(node.GetPath());
|
||||
}
|
||||
|
||||
foreach (Node child in node.GetChildren()){
|
||||
_treeQueue.Enqueue(child);
|
||||
}
|
||||
}
|
||||
|
||||
if (foundNodes.Count <= 0) return foundNodes;
|
||||
{
|
||||
for (int i = 0; i < foundNodes.Count; i++){
|
||||
Print($"{foundNodes[i]}");
|
||||
}
|
||||
|
||||
if (foundNodes.Count == 1){
|
||||
ChangeContext(foundNodes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return foundNodes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfl1b0g22wbck
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using Godot;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public partial class Display{
|
||||
[ConsoleCommand(Prefix = "Display", Description = "Set Vsync mode. Use GetVsyncModes() for a list of valid options. Method expects int")]
|
||||
private static void SetVsyncMode(int mode){
|
||||
DisplayServer.WindowSetVsyncMode((DisplayServer.VSyncMode)mode);
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Display", Description = "Print the current Vsync mode to the console")]
|
||||
private static void GetCurrentVsyncMode(){
|
||||
DC.Print((int)DisplayServer.WindowGetVsyncMode());
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Display", Description = "Print a list of all valid vsync mode indices and names")]
|
||||
private static void GetVsyncModes(){
|
||||
string[] modes = Enum.GetNames(typeof(DisplayServer.VSyncMode));
|
||||
for (int i = 0; i < modes.Length; i++){
|
||||
DC.Print($"{i} - {modes[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Display", Description = "Print the current mouse position")]
|
||||
private static void GetMousePosition(){
|
||||
DC.Print(DC.Root.GetViewport().GetMousePosition());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://iavcf5e0axvd
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Threading.Tasks;
|
||||
using Godot;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public static partial class DCEngine{
|
||||
#region GET
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Print current fps")]
|
||||
private static double GetFPS() => Engine.GetFramesPerSecond();
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Print number of frames drawn since init")]
|
||||
private static int GetFramesDrawn() => Engine.GetFramesDrawn();
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Print number of physics frames since init")]
|
||||
private static ulong GetPhysicsFrames() => Engine.GetPhysicsFrames();
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Print number of process frames since init")]
|
||||
private static ulong GetProcessFrames() => Engine.GetProcessFrames();
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Print current engine time scale")]
|
||||
private static double GetTimeScale() => Engine.TimeScale;
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Print max fps")]
|
||||
private static int GetMaxFPS() => Engine.MaxFps;
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Print physics jitter fix")]
|
||||
private static double GetPhysicsJitterFix() => Engine.PhysicsJitterFix;
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Print physics ticks per second")]
|
||||
private static int GetPhysicsTicksPerSecond() => Engine.PhysicsTicksPerSecond;
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Print max physics steps per frame")]
|
||||
private static int GetMaxPhysicsStepsPerFrame() => Engine.MaxPhysicsStepsPerFrame;
|
||||
|
||||
#endregion
|
||||
|
||||
#region SET
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Set the current engine time scale")]
|
||||
private static double SetTimeScale(double timeScale){
|
||||
Engine.TimeScale = timeScale;
|
||||
return GetTimeScale();
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Set the physics jitter fix")]
|
||||
private static double SetPhysicsJitterFix(double physicsJitterFix){
|
||||
Engine.PhysicsJitterFix = physicsJitterFix;
|
||||
return GetPhysicsJitterFix();
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Suppress errors from being output to Godot console")]
|
||||
private static bool SuppressGDErrorMessages(bool suppress){
|
||||
Engine.PrintErrorMessages = !suppress;
|
||||
return !Engine.PrintErrorMessages;
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Set physics ticks per second")]
|
||||
private static int SetPhysicsTicksPerSecond(int ticks){
|
||||
Engine.PhysicsTicksPerSecond = ticks;
|
||||
return GetPhysicsTicksPerSecond();
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Set max physics steps per frame")]
|
||||
private static int SetMaxPhysicsStepsPerFrame(int maxSteps){
|
||||
Engine.MaxPhysicsStepsPerFrame = maxSteps;
|
||||
return GetMaxPhysicsStepsPerFrame();
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Engine", Description = "Set max fps, a value of 0 means unlimited")]
|
||||
private static int SetMaxFPS(int maxFPS){
|
||||
Engine.MaxFps = maxFPS;
|
||||
return GetMaxFPS();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dnq1yjcsndmtg
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public static class Level{
|
||||
public static DCDelegates.onCallback OnSceneLoaded;
|
||||
private static List<ResourceInformation> _sceneInfos = new();
|
||||
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public static List<ResourceInformation> SceneInfos{
|
||||
get{
|
||||
if (_sceneInfos.Count == 0){
|
||||
InitializeScenes();
|
||||
}
|
||||
|
||||
return _sceneInfos;
|
||||
}
|
||||
}
|
||||
|
||||
private static void InitializeScenes(){
|
||||
DC.Print("Initializing Levels List...");
|
||||
Resources.GetResourceInformation(ResourcePaths.Levels, ref _sceneInfos);
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Level", Description = "Print a table of all scenes in the Levels folder")]
|
||||
private static void GetAll(){
|
||||
for (int i = 0; i < SceneInfos.Count; i++){
|
||||
DC.Print($"ID:: {i.ToString().SetWidth(20)}{SceneInfos[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Level",
|
||||
Description =
|
||||
"Load a level by index, overwriting the current context. Level.GetAll() to print a table of all levels.")]
|
||||
private static void Load(int index){
|
||||
if (DC.Mode == 1) throw new DCException("Cannot load levels from Crosshair Mode, switch to terminal.");
|
||||
try{
|
||||
PackedScene p = ResourceLoader.Load<PackedScene>(SceneInfos[index].Path);
|
||||
foreach (Node n in DC.CurrentNode.GetChildren()){
|
||||
n.QueueFree();
|
||||
}
|
||||
|
||||
DC.CurrentNode.ReplaceBy(p.Instantiate());
|
||||
DC.CurrentNode.QueueFree();
|
||||
OnSceneLoaded?.InvokeAwaited();
|
||||
}
|
||||
catch (Exception e){
|
||||
throw new DCException($"Failed to load level {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Level",
|
||||
Description = "Load a level by index into the current context. Level.GetAll() to print a table of all levels.")]
|
||||
private static void LoadAdditive(int index){
|
||||
if (DC.Mode == 1) throw new DCException("Cannot load levels from Crosshair Mode, switch to terminal.");
|
||||
try{
|
||||
PackedScene p = ResourceLoader.Load<PackedScene>(SceneInfos[index].Path);
|
||||
DC.CurrentNode.AddChild(p.Instantiate());
|
||||
OnSceneLoaded?.InvokeAwaited();
|
||||
}
|
||||
catch (Exception e){
|
||||
throw new DCException($"Failed to load level {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b2amvjqvxb3gn
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace hamsterbyte.DeveloperConsole;
|
||||
|
||||
public static class MathCommands{
|
||||
private const double DegToRad = Math.PI / 180;
|
||||
private const double RadToDeg = 180 / Math.PI;
|
||||
|
||||
[ConsoleCommand(Prefix = "Math", Description = "Print value of x raised to the power of y")]
|
||||
private static double Pow(double x, double y) => Math.Pow(x, y);
|
||||
|
||||
[ConsoleCommand(Prefix = "Math", Description = "Print the square root of a value")]
|
||||
private static double Sqrt(double val) => Math.Sqrt(val);
|
||||
|
||||
[ConsoleCommand(Prefix = "Math", Description = "Print sine of angle from degrees")]
|
||||
private static double Sin(double value) => Math.Round(Math.Sin(value * DegToRad), 3);
|
||||
|
||||
[ConsoleCommand(Prefix = "Math", Description = "Print arc sine in degrees")]
|
||||
private static double Asin(double value) => Math.Round(Math.Asin(value) * RadToDeg, 3);
|
||||
|
||||
[ConsoleCommand(Prefix = "Math", Description = "Print cosine of angle from degrees")]
|
||||
private static double Cos(double value) => Math.Round(Math.Cos(value * DegToRad), 3);
|
||||
|
||||
[ConsoleCommand(Prefix = "Math", Description = "Print arc cosine in degrees")]
|
||||
private static double Acos(double value) => Math.Round(Math.Acos(value) * RadToDeg, 3);
|
||||
|
||||
[ConsoleCommand(Prefix = "Math", Description = "Print tangent of angle from degrees")]
|
||||
private static double Tan(double value) => Math.Round(Math.Tan(value * DegToRad), 3);
|
||||
|
||||
[ConsoleCommand(Prefix = "Math", Description = "Print arc tangent in degrees")]
|
||||
private static double Atan(double value) => Math.Round(Math.Atan(value) * RadToDeg, 3);
|
||||
|
||||
[ConsoleCommand(Prefix = "Math", Description = "Print angle whose tangent is a quotient of two specified numbers")]
|
||||
private static double Atan2(double x, double y) => Math.Round(Math.Atan2(x, y), 3);
|
||||
|
||||
[ConsoleCommand(Prefix = "Math", Description = "Check if a number is prime")]
|
||||
private static Task<bool> IsPrime(long number){
|
||||
CancellationToken ct = Command.Token;
|
||||
return Task.Run(() => {
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (number == 2) return true;
|
||||
if (number < 2 || number % 2 == 0) return false;
|
||||
int squareRoot = (int)Math.Sqrt(number);
|
||||
for (long i = 3; i <= squareRoot; i += 2){
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (number % i == 0) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, ct
|
||||
);
|
||||
}
|
||||
|
||||
[ConsoleCommand(Prefix = "Math",
|
||||
Description = "Print a list of all prime numbers from start to end. Start is always greater than 2.")]
|
||||
private static async Task ListPrimes(long start, long end){
|
||||
CancellationToken ct = Command.Token;
|
||||
start = start < 2 ? 2 : start;
|
||||
DC.ShowProgress((int)(end - start));
|
||||
for (long i = start; i <= end; i++){
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (await IsPrime(i)){
|
||||
DC.Print(i);
|
||||
}
|
||||
|
||||
DC.IncrementProgress(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user