diff --git a/VoxelGame.sln b/VoxelGame.sln deleted file mode 100644 index 009ef59..0000000 --- a/VoxelGame.sln +++ /dev/null @@ -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 diff --git a/src/SjkScripts b/src/SjkScripts index 1fa265a..3af30d3 160000 --- a/src/SjkScripts +++ b/src/SjkScripts @@ -1 +1 @@ -Subproject commit 1fa265a1c35acf5b5245ed712c1a0f2b31850c2d +Subproject commit 3af30d34473af770a8d0c43f467cb5ff2e045be7 diff --git a/src/voxelgame/ChunkSurfaceNetTest.cs b/src/voxelgame/ChunkSurfaceNetTest.cs new file mode 100644 index 0000000..5182d56 --- /dev/null +++ b/src/voxelgame/ChunkSurfaceNetTest.cs @@ -0,0 +1,1005 @@ + +using BinaryGreedyMesher; +using ChickenGameTest; +using Dapper; +using Godot; +using Microsoft.Data.Sqlite; +using Newtonsoft.Json; +using ProtoBuf; +using SJK.Functional; +using SJK.Voxels; +using SJK.Voxels.Registry; +using SqlKata; +using SqlKata.Compilers; +using SqlKata.Execution; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Aabb = SJK.Voxels.Aabb; +public partial class ChunkSurfaceNetTest : MeshInstance3D +{ + + // VoxelWorldUnConstrained, Chunk32> chunks = new( + // new ChunkFactory,Chunk32>, VoxelHandle, + // Chunk16>(chunkpos => new VoxelChunk, Chunk32>()), + // new MemoryDataLayer, Chunk32>>(new ); + // Dictionary> chunks; + [Export] + Material material; + VoxelPalette, PaletteLevel, VoxelInstance> palette = new((index, entry) => new VoxelHandle(index)); + VoxelPalette, DefinitionLevel, VoxelDefinition> Regeristy = new((index, entry) => new VoxelHandle(index, entry.ComputeBits(0))); + VoxelPalette, DefinitionLevel, VoxelDefinition> Regeristy2 = new((index, entry) => new VoxelHandle(index)); + VoxelPalette, PaletteLevel, VoxelInstance> palette2 = new((index, entry) => new VoxelHandle(index, entry.Definition.ComputeBits(0))); + [Export] + Texture2D textureA; + [Export] + Texture2D textureB; + List, Mesh)>> tasks; + IWorld> world; + public record VoxelName(string Name) : IVoxelAttribute; + List<(ChunkIndex3D,Task>)> taskes = new(); + public override async void _Ready() + { + // using SimpleDataBase sdb = new SimpleDataBase(Path.Combine(OS.GetUserDataDir(), "simple.sdb")); + // GD.Print("index ",sdb.Allocate([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); + // GD.Print(string.Join(',',sdb.Get(0))); + // GD.Print(string.Join(',',sdb.GetTableEnrtys())); + // sdb.Save(); + // return; + // var cc = new VoxelChunk(1); + // Mesh = ChunkMeshBuilder.BuildChunkMesh(cc, Vector3I.Zero,(voxel)=>voxel<=0).UploadMesh();// SurfaceNetMesher.CreateMesh(item.Value.Size, pos => c.GetVoxel(pos), p => false); + var reg2 = new StructuralInstanceManger(); + var air = reg2.Canonicalize(new StructuralBuilder() + .Add(new VoxelName("base/air")).Build()); + var dirt = reg2.Canonicalize(new StructuralBuilder() + .Add(new VoxelName("base.dirt")) + .Add(new IsSoild(true)) + .Add(new TextureCube(Colors.Brown,1)) + .Build()); + var stone = reg2.Canonicalize(new StructuralBuilder(dirt) + .Add(new VoxelName("base.stone")) + .Add(new TextureCube(Colors.Gray,1)) + .Add(new Hardness(0)) + .Build()); + var p = new VoxelPalette,PaletteLevel,StructuralInstance>((index,entry) => new(index)); + var airH = p.GetOrAddEntry(air); + var stoneH = p.GetOrAddEntry(stone); + var dirtH = p.GetOrAddEntry(dirt); + var airData = Regeristy.GetOrAddEntry(VoxelDefinition.Create(config => + { + config.Name = "base.air"; + })); + var dirtData = Regeristy.GetOrAddEntry(VoxelDefinition.Create(config => + { + config.Name = "base.dirt"; + config.WithAttribute(new IsSoild(true)); + config.WithAttribute(new TextureCube(Colors.Brown, 1)); + })); + var stoneData = Regeristy.GetOrAddEntry(VoxelDefinition.Create(config => + { + config.Name = "base.stone"; + config.WithAttribute(new Hardness(0)); + config.WithAttribute(new IsSoild(true)); + config.WithAttribute(new TextureCube(Colors.Gray, 0)); + })); + var airHandel = palette.GetOrAddEntry(Regeristy.Get(airData).CreateInstance().Set(new Hardness(2)).Build()); + var stoneHandel = palette.GetOrAddEntry(Regeristy.Get(stoneData).CreateInstance().Set(new Hardness(2)).Build()); + var stoneHandel2 = palette.GetOrAddEntry(Regeristy.Get(stoneData).CreateInstance().Set(new IsSoild(false)).Build()); + + var airHandelBits = palette2.GetOrAddEntry(Regeristy.Get(airData).CreateBitBuilderInstance().Set(new Hardness(2)).Build(out var _)); + var stoneHandelBits = palette2.GetOrAddEntry(Regeristy.Get(stoneData).CreateBitBuilderInstance().Set(new Hardness(2)).Build(out var _)); + var stoneHandel2Bits = palette2.GetOrAddEntry(Regeristy.Get(stoneData).CreateBitBuilderInstance().Set(new IsSoild(false)).Build(out var _)); + // GD.Print("DIRT "+string.Join(',',voxelRegister.GetVoxel(dirt).Attributes.Select(item => item.Value.GetType()))); + // GD.Print("DIRT ",voxelRegister.GetVoxel(dirt).GetAttribute()); + // GD.Print("Stone "+string.Join(',',voxelRegister.GetVoxel(stone).Attributes.Select(item => item.Value.GetType()))); + // return; + // return; + base._Ready(); + var textures = new Texture2DArray(); + textures.CreateFromImages([.. new Texture2D[] { textureA, textureB }.Select(item => item.GetImage())]); + ((ShaderMaterial)material).SetShaderParameter("Texture2DArrayParameter", textures); + + + + var path = OS.GetUserDataDir(); + + GD.Print($"{path}/hello.db"); + if (!File.Exists($"{path}/hello.db")) + { + File.Create($"{path}/hello.db"); + } + + var connection = new SqliteConnection($"Data Source={path}/hello.db"); + var Sqlite = new SQLiteDataStore(connection, new sqlSecma(),null, false); + var compresser = new CompressedDataStore(Sqlite, new gzipCompress()); + // var keyChange = new DataStoreMapper>(compresser,new FuncDataMapper>(key=>new(key.X,key.Y,key.Z))); + // var serialized = new DataStoreSerializer< + // ChunkPos, + // IVoxelChunk, Chunk32>, + // byte[]>(compresser,new ChunkSerlizer3()); + // var DataRepository = new DataRepository, IVoxelChunk, Chunk32>>(compresser, new ChunkPosMapper(), new ChunkSerlizer2()); + // GD.Print(serialized.ReadAsync(new ChunkPos(0,0,0))); + // var world = new VoxelWorldUnConstrained, Chunk32>(serialized, () => new VoxelChunk, Chunk32>()); + // world = WorldBuilder.Create() + // .WithoutPalette>() + // .UseChunk, Chunk32>>() + // .WithFactory(() => new()) + // .WithSerializer(new ChunkSerlizer3()) + // .WithChunkManger(compresser) + // .WithChunkService(out var service) + // .WithLimitedSize(5,5,5); + + // var worlds = WorldBuilder.Create() + // .WithoutPalette>() + // .UseChunk, Chunk32>>() + // .WithFactory( () => new(air)) + // .WithSerializer(new ChunkSerlizerStruct()) + // .WithChunkManger(compresser) + // .WithChunkService(out var service2) + // .WithLimitedSize(2,2,2); + // for (int x = 0; x < 5; x++) + // { + // for (int y = 0; y < 5; y++) + // { + // for (int z = 0; z < 5; z++) + // { + // await world.SetChunkAsync(new(x, y, z), new VoxelChunk, Chunk32>()); + // } + // } + // } + var rand = new FastNoiseLite(); + var w = new World,Chunk32>(factory,air); + IWorldChunk,Chunk32> factory(ChunkIndex3D pos){ + var chunk =new WorldChunk,Chunk32>(air); + foreach (var item in chunk as IReadOnlyVoxelChunk,Chunk32>) + { + var n = rand.GetNoise3D(pos.X*Chunk32.Size +item.Position.X,pos.Y*Chunk32.Size +item.Position.Y,pos.Z*Chunk32.Size +item.Position.Z); + if (n*100 +100 - (pos.Y*Chunk32.Size +item.Position.Y) > 0) + { + chunk.SetVoxelAt(item.Position,stone); + } + } + return chunk; + } + + // var query = new BoxQuery(new(15, 15, 15), new(50, 50, 50)).Union(new SphereQuery(new(50,50,50),50).Union(new BoxQuery(new(0,0,0),new(500,2,500)).Union(new SphereQuery(new(300,100,300),100)))); + // foreach (var item in query) + // { + // // world.SetVoxel(new VoxelPos(item.X, item.Y, item.Z), stoneHandel); + // // worlds.SetVoxel(new VoxelPos(item.X,item.Y,item.Z), stone); + // w.SetVoxel(new VoxelPos(item.X,item.Y,item.Z), stone); + // // world.SetVoxel(new Vector3I(item.X, item.Y, item.Z), stoneHandel); + // } + // for (int X = 0; X < 10; X++) + // { + // for (int Z = 0; Z < 10; Z++) + // { + // // var n = rand.GetNoise2D(X,Z); + // for (int y = 0; y < 10;y++)//(n+1)*100; y++) + // { + // // w.SetVoxel(new ChunkPos(X,y,Z).ToGlobalOrigin(),air); + // // w.SetVoxel(new(X,y,Z),dirt); + // } + // } + // } + // var chunks = service.GetLoadedChunks(); //new List<(ChunkPos,VoxelChunk,Chunk32>)>(); + // var chunks2 = service2.GetLoadedChunks(); + // service.GetLoadedChunk(new(0, 1, 0)).ToClassOption().IfSome(c => chunks.Add((new(0, 1, 0),c))); + // var chunk = new VoxelChunk, Chunk32>(); + // var chunk2 = new VoxelChunk, Chunk32>(); + // var q = new SphereQuery(new(6, 6, 6), 5).Union(new BoxQuery(new(4,4,4),new(10,10,10))).Union(new BoxQuery(new(16,16,16),new(33,33,33))); + // foreach (var chu in chunk) + // { + // chunk.SetVoxel(chu.Position, rand.GetNoise3D(chu.Position.X,chu.Position.Y,chu.Position.Z) >= 0 ? stoneHandel : airHandel); + // } + // foreach (var chu in chunk2) + // { + // chunk2.SetVoxel(chu.Position, rand.GetNoise3D(chu.Position.X,chu.Position.Y,chu.Position.Z) >= 0 ? stoneH : airH ); + // } + // foreach (var item in q) + // { + // if (chunk.IsInBounds(new(item.X, item.Y, item.Z))) + // chunk.SetVoxel(new VoxelPos(item.X, item.Y, item.Z), stoneHandel); + // if (chunk2.IsInBounds(new(item.X, item.Y, item.Z))) + // chunk2.SetVoxel(new VoxelPos(item.X, item.Y, item.Z), stoneH); + // } + // GetViewport().DebugDraw = Viewport.DebugDrawEnum.Wireframe; + // var mask = BinaryGreedyMesher.MaskBuilder.BuildMask32(chunk, i); + // Stopwatch stopwatch2 = new Stopwatch(); + // stopwatch2.Start(); + // palette.Get(chunk.GetVoxel(new(0,0,0))).GetAttribute(()=>new(false));//.Map(f => f.Solid).OrDefault(false)); + // chunk2.GetVoxel(new(0,0,0)).Get().Map(f => f.Solid).OrDefault(false); + // GD.Print(stopwatch2.Elapsed); + // return; + Dictionary cache = new(); + var store = new DataStoreSerializer,Chunk32>,byte[]>(compresser,new ChunkSerlizerStruct()); + w.ChunkManger = new ChunkManger, Chunk32>,ChunkIndex3D>(new ChunkFuncGen,Chunk32>,ChunkIndex3D>(f=>factory(f)), null, new TwoTierStore, Chunk32>, MemoryDataLayer, Chunk32>>>(new MemoryDataLayer, Chunk32>>(),store)); + var tasks2 = new List(); + for (int i = 0; i < 10; i++) + { + for (int ii = 0; ii < 5; ii++) + { + for (int iii = 0; iii < 10; iii++) + { + int a = i, b = ii, c = iii; + // tasks2.Add(Task.Run( + // () => w.ChunkManger.LoadChunks(new ChunkPos(a,b,c)) + // )); + await w.ChunkManger.LoadChunks(new ChunkIndex3D(i,ii,iii)); + // await w.ChunkManger.LoadChunks(new ChunkIndex3D(i,iii,ii,1)); + } + } + } + // await Task.WhenAll(tasks2.ToArray()); + + foreach (var c in w.GetVoxelChunks()) + { + Stopwatch stopwatch = new Stopwatch(); + // var neiborAware = new NeighborAwareChunk, Chunk32>(c.Item1, c.Item2, null, (pos) => world.GetVoxel(new VoxelPos(pos.X, pos.Y, pos.Z))); + var neiborAware2 = new NeighborAwareChunk, Chunk32>(ChunkPos.Create(c.Item1.X,c.Item1.Y,c.Item1.Z), c.Item2, null, (pos) => w.GetVoxel(new VoxelPos(pos.X, pos.Y, pos.Z))); + taskes.Add((c.Item1,CreateChunkThreadedAsync(neiborAware2))); + continue; + // var chunk2 = new VoxelChunk,Chunk32>(); + // foreach (var chu in chunk2) + // { + // var p = c.Item1.ToGlobalOrigin() + chu.Position; + // chunk.SetVoxel(chu.Position, world.GetVoxel(new VoxelPos(p.X,p.Y,p.Z))); + // } + stopwatch.Start(); + + // if (!cache.TryGetValue(c.Item1, out var cache1)) + // { + // // cache1 = BinaryGreedyMesherCore_Fixed.BuildChunkBinaryBits((x, y, z) => palette.Get(neiborAware.GetVoxel(new Vector3I(x, y, z))).GetAttribute(() => new IsSoild(false)).Solid); + // cache1 = BinaryGreedyMesherCore_Fixed.BuildChunkBinaryBits((x, y, z) => + // { + // return neiborAware2.GetVoxel(new Vector3I(x, y, z)).Get().Map(static f => f.Solid).OrDefault(false); + // }); + // cache.Add(c.Item1, cache1); + // } + // var greedy = new GreedyMesher(); + // var faces = BinaryGreedyMesherCore_Fixed.BuildChunkBinaryFromBits(cache1.FaceMasks); + // var mesh = greedy.BuildMesh(faces); + // var mesh = new BinaryGreedyMesher.GreedyMesher().BuildMesh>(neiborAware, v => palette.Get(v).GetAttribute(() => new IsSoild(false)).Solid); + // var mesh = ChunkMeshBuilder.BuildChunkMesh>(neiborAware, v => palette.Get(v).GetAttribute(()=>new IsSoild(false)).Solid, ((voxel, dir) => + // { + // var t = palette.Get(voxel).GetAttribute(() => new TextureCube(Colors.White, 0)); + // return (t.Color, t.index); + // })).UploadMesh().ToOption(); + stopwatch.Stop(); + + var instance = new MeshInstance3D() { MaterialOverride = material }; + AddChild(instance); + // instance.Position = ((ChunkPos)c.Item1).ToGlobalOrigin(); + GD.Print(stopwatch.ElapsedMilliseconds); + // mesh.IfSome(m => instance.Mesh = m); + // mesh.IfNone(() => instance.QueueFree()); + // return; + } + + // for (int x = 0; x < 5; x++) + // { + // for (int y = 0; y < 5; y++) + // { + // for (int z = 0; z < 5; z++) + // { + // var key = new ChunkPos(x, y, z); + // var item = await world.GetChunkAsync(key); + // var c = new NeighborAwareChunk>(key, item, world.GetChunkNeibors(key), pos => world.GetVoxel(pos).OrDefault(airHandel)); + // var mesh = await Task.Run(() => ChunkMeshBuilder.BuildChunkMesh(c, (voxel) => palette.Get(voxel).GetAttribute(() => new IsSoild(false)).Solid, (voxel, dir) => + // { + // var t = palette.Get(voxel).GetAttribute(() => new TextureCube(Colors.White, 0)); + // return (t.Color, t.index); + // }).UploadMesh()); + // var node = new MeshInstance3D(); + // AddChild(node); + // node.Position = key.ToGlobalOrigin(); + // node.Mesh = mesh; + // node.MaterialOverride = material; + // // tasks.Add(Task.Run(() => (key, ChunkMeshBuilder.BuildChunkMesh(c, (voxel) => palette.Get(voxel).GetAttribute(() => new IsSoild(false)).Solid, (voxel, dir) => + // // { + // // var t = palette.Get(voxel).GetAttribute(() => new TextureCube(Colors.White, 0)); + // // return (t.Color, t.index); + // // }).UploadMesh()))); + // // node.Mesh = ChunkMeshBuilder.BuildChunkMesh(c, (voxel) => voxelRegister.GetVoxel(voxel).GetAttribute().Map(solid => solid.Solid).OrDefault(false)).UploadMesh();// SurfaceNetMesher.CreateMesh(item.Value.Size, pos => c.GetVoxel(pos), p => false); + + // // var node = new MeshInstance3D(); + // // node.MaterialOverride = material; + // // node.Position = key.ToGlobalOrigin(); + // // AddChild(node); + // // node.Mesh = mesher.Mesh(item.Value); + // // var c = item.Value as Octree; + // // node.Mesh = mesher.Mesh(c); + // // var c = new ReadOnlyVirtualChunk(item.Value.Size,pos => chunks.GetVoxel(item.Key.ToGlobalOrigin())); + + // } + // } + // } + // connection.Close(); + // var reg = new VoxelRegistry, VoxelDefinition, VoxelHandle,IVoxelAttribute>(Regeristy2); + // var DataRepository2 = new DataStoreSerializer, IVoxelChunk, Chunk32>,byte[]>(compresser, null); + // var DataRepository3 = new DataStoreSerializer, IVoxelChunk,byte[]>(compresser, null); + // var ds = TwoTierStoreFactory.For, IVoxelChunk, Chunk32>>().MemoryCached(DataRepository2); + // var chunkManger = new ChunkManger, Chunk32, DataRepository, IVoxelChunk, Chunk32>>(ds); + // var world3 = WorldBuilder.Create() + // .WithoutPalette() + // .UseChunk>()//b => { b.WithFactory(() => new()); b.WithSerializer(new ChunkSerlizer()); }) + // .WithFactory(() => new()) + // .WithSerializer(new ChunkSerlizer()) + // .WithChunkManger(compresser) + // .WithLimitedSize(10,20,30); + // var worlds = WorldBuilder.Create().WithGlobalPalette(reg).UseChunk,Chunk32>>().WithChunkManger(DataRepository2).WithUnlimitedSize();//.WithGlobalPalette(Regeristy2).WithLimitedSize(15,15,15); + // var worlds2 = WorldBuilder.Create().WithoutPalette().UseChunk>().WithChunkManger(DataRepository3).WithLimitedSize(30,10,30); + //'DataRepository, SJK.Voxels.IVoxelChunk, Chunk32>>' cannot be used as type parameter 'TStore' in the generic type or method + // 'TwoTierStoreBuilder, IVoxelChunk>>.MemoryCached(TStore, LayerPolicy)'. There is no implicit reference conversion from + // 'DataRepository, SJK.Voxels.IVoxelChunk, Chunk32>>' to + // 'IDataStore, SJK.Voxels.IVoxelChunk>>' + // 'DataRepository, SJK.Voxels.IVoxelChunk, Chunk32>>' to + // 'IDataStore, SJK.Voxels.IVoxelChunk>>'. + //Argument 1: cannot convert from 'SJK.Voxels.Registry.VoxelPalette, SJK.Voxels.Registry.DefinitionLevel, SJK.Voxels.Registry.VoxelDefinition>' to + // 'SJK.Voxels. IVoxelRegistry, SJK.Voxels.Registry.VoxelInstance>' + return; + // var mesher = new BoxelMesher(new testApi()); + // tasks = new List, Mesh)>>(); + // foreach (var item in chunks) + // { + // var c = new NeighborAwareChunk>(item.Key, item.Value, chunks.GetChunkNeibors(item.Key), chunks.GetVoxel, airInstance); + // tasks.Add(Task.Run(() => (item.Key, ChunkMeshBuilder.BuildChunkMesh(c, (voxel) => palette.Get(voxel).GetAttribute(() => new IsSoild(false)).Solid, (voxel, dir) => + // { + // var t = palette.Get(voxel).GetAttribute(() => new TextureCube(Colors.White, 0)); + // return (t.Color, t.index); + // }).UploadMesh()))); + // node.Mesh = ChunkMeshBuilder.BuildChunkMesh(c, (voxel) => voxelRegister.GetVoxel(voxel).GetAttribute().Map(solid => solid.Solid).OrDefault(false)).UploadMesh();// SurfaceNetMesher.CreateMesh(item.Value.Size, pos => c.GetVoxel(pos), p => false); + + // var node = new MeshInstance3D(); + // node.MaterialOverride = material; + // node.Position = item.Key.ToGlobalOrigin(); + // AddChild(node); + // node.Mesh = mesher.Mesh(item.Value); + // var c = item.Value as Octree; + // node.Mesh = mesher.Mesh(c); + // var c = new ReadOnlyVirtualChunk(item.Value.Size,pos => chunks.GetVoxel(item.Key.ToGlobalOrigin())); + + // } + // var path = OS.GetUserDataDir(); + + var settings = new JsonSerializerSettings() + { + Converters = [ + // new VoxelPaletteJsonConverter, DefinitionLevel, VoxelDefinition>((index, entry) => {GD.PrintS("why",index,entry); return new VoxelHandle(index, entry.ComputeBits(0)); }), + new VoxelPaletteJsonConverter, PaletteLevel, VoxelInstance>((index, entry) => {GD.PrintS("why",index,entry); return new VoxelHandle(index, entry.Definition.ComputeBits(0)); }), + new VoxelInstanceConverter,DefinitionLevel,IVoxelAttribute,int>(Regeristy), + new VoxelDefinitionConverter,DefinitionLevel,IVoxelAttribute,int>() + ] + }; + // var testclass = new test(){Regeristy = Regeristy, Palette = palette2}; + // var converter = JsonConvert.SerializeObject(testclass,settings); + // GD.Print(converter); + // var data2= JsonConvert.DeserializeObject(converter); + //TODO i would need an class that can handel deserlizing converts without knowing ahead of time, propbly an converter for world* that reads Re + // task = Task.WhenAll(tasks.ToArray()); + // task.Start(); + // GD.Print($"{path}/hello.db"); + // if (!File.Exists($"{path}/hello.db")) + // { + // File.Create($"{path}/hello.db"); + // } + // SQLitePCL.Batteries_V2.Init(); + + // var serviceprovider = new Microsoft.Extensions.DependencyInjection.ServiceCollection() + // .AddFluentMigratorCore() + // .ConfigureRunner(rb => + // rb.AddSQLite() + // .WithGlobalConnectionString($"Data Source={path}/hello.db") + // .ScanIn(typeof(CreateChunksTable).Assembly).For.Migrations()) + // .AddLogging(lb => lb.AddFluentMigratorConsole()) + // .BuildServiceProvider(false); + // using var scope = serviceprovider.CreateScope(); + // var runner = scope.ServiceProvider.GetRequiredService(); + // runner.MigrateUp(); + // // var connection = new SqliteConnection($"Data Source={path}/hello.db"); + // var connection = new SqliteConnection($"Data Source={path}/hello.db"); + // var compiler = new SqliteCompiler(); + // using var db = new QueryFactory(connection, compiler); + + // db.Statement( + // "CREATE TABLE IF NOT EXISTS Chunks (X INTEGER NOT NULL, Y INTEGER NOT NULL, Z INTEGER NOT NULL, Data BLOB NOT NULL,Modified DATETIME NOT NULL default current_timestamp, PRIMARY KEY (X, Y, Z) )"); + + // EnsureTable("Chunks",connection); + + // if (db.Query("Chunks").Where("X","=", 0).Where("Y","=", 0).Where("Z","=", 0).Exists()) + // db.Query("Chunks").Update(new Chunk() { X = 0, Y = 0, Z = 0, Data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(palette2, settings)) }); + // else + // db.Query("Chunks").Insert(new Chunk() { X = 0, Y = 0, Z = 0, Data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(palette2, settings)) }); + + // if (db.Query("Chunks").Where("X","=", 0).Where("Y","=", 1).Where("Z","=", 0).Exists()) + // db.Query("Chunks").Update(new Chunk() { X = 0, Y = 1, Z = 0, Data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(palette2, settings)) }); + // else + // db.Query("Chunks").Insert(new Chunk() { X = 0, Y = 1, Z = 0, Data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(palette2, settings)) }); + // db.InsertOrReplace("Chunks", new { X = 0, Y = 0, Z = 0, Data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(palette2, settings)) }); + // db.InsertOrUpdateOnConflict("Chunks", new { X = 0, Y = 1, Z = 0, Data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(palette2, settings)) }, ["X", "Y", "Z"], ["Data", "Modified"]); + // var query = new Query("Chunks").Where("X", 0).Where("Y", 0).Where("Z", 0); + // var result = db.FromQuery(query); + // var comp = compiler.Compile(query); + // var result2 = connection.QueryFirstOrDefault(comp.Sql, comp.NamedBindings);//Query("Users").Where("Id", 1).Get(); + // GD.Print(result2 is not null); + // GD.Print(result2 is not null ? string.Join(',', result2.Data) : "Nothing"); + // connection.Query("Users",comp.Bindings); + // var d = db.Query("Users").Where("Id", 1).Where("Status", "Active").Get(); + // var result = compiler.Compile(query); + // var results = connection. + // var chunk = new VoxelChunk(); + // chunk.SetVoxel(new VoxelPos(0, 0, 0), 5); + // var memory = new MemoryDataStore(); + // var file = new FileDataStore(Path.Combine(path, "WorldTest"), new pathResolver()); + // var Sqlite = new SQLiteDataStore(connection, new sqlSecma(), false); + // var SqliteTree = new SQLiteDataStore(connection, new sqlSecmaTree(), false); + + connection.Open(); + // var dataStore = new LayeredDataStore(memory,file); + var ChunkSerlizer = new ChunkSerlizer(); + // var ressoptory = new MemoryDataLayer>(memory, ChunkSerlizer); + // var chunkManger = new ChunkManger(ressoptory); + + // var compresser = new CompressedDataStore(Sqlite, new gzipCompress()); + // var DataRepository = new DataRepository, IVoxelChunk>(compresser, new ChunkPosMapper(), ChunkSerlizer); + // var r = await DataRepository.ReadAsync(new ChunkPos(0, 0, 0)); + // if (r.HasValue) + // { + // GD.Print("DataRe[psoty jas value"); + // } + + // await DataRepository.WriteAsync(new ChunkPos(0, 0, 0), chunk); + // await ressoptory.SetData(new DataKey("Chunks", new ChunkPos(0, 0, 0)), chunk); + // var result = await ressoptory.GetDataAsync(new DataKey("Chunks",new ChunkPos(0,0,0))); + // world.SetChunk(new ChunkPos(0, 0, 0), chunk); + // var result = world.GetChunk(new ChunkPos(0, 0, 0)); + // GD.Print(result); + // GD.Print(result.Value.GetVoxel(new VoxelPos(0, 0, 0))); + // var tree = new SpatialDataLayer(); + // var TreeRepository = new DataRepository(SqliteTree, new FuncDataMapper((guid) => new DataKey("Tree", guid)), new TreeSelizerSingle()); + // var guid = Guid.NewGuid(); + // await TreeRepository.WriteAsync(guid, new TreeEntry(guid, new Vector3(0, 0, 0))); + // tree.Insert(new TreeEntry(Guid.NewGuid(), new Vector3(0, 0, 0))); + // tree.Insert(new TreeEntry(Guid.NewGuid(), new Vector3(1, 0, 0))); + // tree.Insert(new TreeEntry(Guid.NewGuid(), new Vector3(0, 50, 0))); + // tree.Insert(new TreeEntry(Guid.NewGuid(), new Vector3(0, 0, 30))); + // await tree.Save("Trees", file, new TreeSelizer()); + // await tree.Load("Trees", file, new TreeSelizer()); + // var aabb = new Aabb(new(0, 0, 0), new(15, 15, 15)); + // GD.Print($"{aabb} intersets " + string.Join(',', tree.Query(aabb).Where(ab => ab.Bounds.Intersects(aabb)).Select(item => item.Guid))); + // GD.Print("\n:"); + // GD.Print($"{aabb} " + string.Join(',', tree.Query(aabb).Select(item => item.Guid))); + // GD.Print("\n:"); + // aabb = new Aabb(new(0, 0, 0), new(50, 60, 50)); + // GD.Print($"{aabb} intersets " + string.Join(',', tree.Query(aabb).Where(ab => ab.Bounds.Intersects(aabb)).Select(item => item.Guid))); + // GD.Print("\n:"); + // GD.Print($"{aabb} " + string.Join(',', tree.Query(aabb).Select(item => item.Guid))); + // GD.Print("\n:"); + // connection.Close(); + // var DataRepositoryTest = new DataRepository, IVoxelChunk>(compresser, new ChunkPosMapper(), ChunkSerlizer); + + + // var voxelQuery = new BoxQuery(new VoxelPos(0, 0, 0), new VoxelPos(10, 10, 10)) + // .Union(new SphereQuery(new VoxelPos(7, 7, 7), 6)) + // .Exclude(new BoxQuery(new(6, 6, 6), new(12, 12, 12))) + // .WithData(pos => chunk.GetVoxel(new(pos.X,pos.Y,pos.Z))); + // GD.Print(string.Join(',', voxelQuery)); + return; + } + public Task> CreateChunkThreadedAsync(NeighborAwareChunk, Chunk32> chunk) + { + Stopwatch stopwatch = new Stopwatch(); + // var neiborAware = new NeighborAwareChunk, Chunk32>(c.Item1, c.Item2, null, (pos) => world.GetVoxel(new VoxelPos(pos.X, pos.Y, pos.Z))); + // var neiborAware2 = new NeighborAwareChunk, Chunk32>(c.Item1, c.Item2, null, (pos) => w.GetVoxel(new VoxelPos(pos.X, pos.Y, pos.Z))); + + // var chunk2 = new VoxelChunk,Chunk32>(); + // foreach (var chu in chunk2) + // { + // var p = c.Item1.ToGlobalOrigin() + chu.Position; + // chunk.SetVoxel(chu.Position, world.GetVoxel(new VoxelPos(p.X,p.Y,p.Z))); + // } + // stopwatch.Start(); + + // if (!cache.TryGetValue(c.Item1, out var cache1)) + // { + // cache1 = BinaryGreedyMesherCore_Fixed.BuildChunkBinaryBits((x, y, z) => palette.Get(neiborAware.GetVoxel(new Vector3I(x, y, z))).GetAttribute(() => new IsSoild(false)).Solid); + var a = Task.Run(()=>{ + var cache1 = BinaryGreedyMesherCore_Fixed.BuildChunkBinaryBits((x, y, z) => + { + return chunk.GetVoxel(new Vector3I(x, y, z)).Get().Map(static f => f.Solid).OrDefault(false); + }); + // cache.Add(c.Item1, cache1); + // } + var greedy = new GreedyMesher(); + var faces = BinaryGreedyMesherCore_Fixed.BuildChunkBinaryFromBits(cache1.FaceMasks); + var mesh = greedy.BuildMesh(faces); + // var mesh = new BinaryGreedyMesher.GreedyMesher().BuildMesh>(neiborAware, v => palette.Get(v).GetAttribute(() => new IsSoild(false)).Solid); + // var mesh = ChunkMeshBuilder.BuildChunkMesh>(neiborAware, v => palette.Get(v).GetAttribute(()=>new IsSoild(false)).Solid, ((voxel, dir) => + // { + // var t = palette.Get(voxel).GetAttribute(() => new TextureCube(Colors.White, 0)); + // return (t.Color, t.index); + // })).UploadMesh().ToOption(); + // stopwatch.Stop(); + return mesh; + }); + return a; + } + class sqlSecma : ISqliteSchema + { + // public void BindData(SqliteCommand cmd, ReadOnlyMemory data) + // { + // var p = cmd.Parameters.Add("@data", SqliteType.Blob); + // p.Value = data.ToArray(); + // } + + // public void BindKey(SqliteCommand cmd, DataKey key) + // { + // if (key.Key is IChunkPos chunkPos) + // { + // cmd.Parameters.AddWithValue("@x", chunkPos.X); + // cmd.Parameters.AddWithValue("@y", chunkPos.Y); + // cmd.Parameters.AddWithValue("@z", chunkPos.Z); + // } + // } + + // public (string Table, string Query) GetExistQuery(DataKey key) + // { + // var table = key.Namespace; + // var query = $"SELECT EXISTS(SELECT 1 FROM {table} WHERE x=@x AND y=@y AND z=@z);"; + // return (table, query); + // } + + // public (string Table, string Query) GetReadQuery(DataKey key) + // { + // var table = key.Namespace; + // var query = $"SELECT data FROM {table} WHERE x=@x AND y=@y AND z=@z;"; + // return (table, query); + // } + + // public (string Table, string Query) GetRemoveQuery(DataKey key) + // { + // var table = key.Namespace; + // var query = $"DELETE FROM {table} WHERE x=@x AND y=@y AND z=@z;"; + // return (table, query); + // } + + // public (string Table, string Query) GetWriteQuery(DataKey key) + // { + // var table = key.Namespace; + // var query = $@" + // INSERT INTO {table} (x, y, z, data) + // VALUES (@x, @y, @z, @data) + // ON CONFLICT(x, y, z) + // DO UPDATE SET data=@data;"; + // return (table, query); + // } + + // public string IntilizeSchema() + // { + // var query = "CREATE TABLE IF NOT EXISTS Chunks (X INTEGER NOT NULL, Y INTEGER NOT NULL, Z INTEGER NOT NULL, Data BLOB NOT NULL,Modified DATETIME NOT NULL default current_timestamp, PRIMARY KEY (X, Y, Z) )"; + // return query; + // } + public string TableName => "Chunks2"; + + SqliteCompiler compiler = new SqliteCompiler(); + + public SqliteCommand CreateDeleteCommand(SqliteConnection connection, ChunkIndex3D key) + { + var query = new Query(TableName).Where("Key", "=", key.ToKey()).AsDelete(); + var result = compiler.Compile(query); + var command = connection.CreateCommand(); + command.CommandText = result.Sql; + command.Parameters.AddWithValue("@key", key.ToKey()); + return command; + } + + public SqliteCommand CreateInsertCommand(SqliteConnection connection, ChunkIndex3D key, byte[] data) + { + var query = $@" + INSERT INTO {TableName} (key, data) + VALUES (@key, @data) + ON CONFLICT(key) + DO UPDATE SET data=@data;"; + var command = connection.CreateCommand(); + command.CommandText = query; + command.Parameters.AddWithValue("@key", key.ToKey()); + return command; + } + + public SqliteCommand CreateSelectCommand(SqliteConnection connection, ChunkIndex3D key) + { + // var query = new Query(TableName).Where("Key", "=", key.ToKey()).Select("Data"); + var query = $"SELECT data FROM {TableName} WHERE key=@key;"; + // var result = compiler.Compile(query); + var command = connection.CreateCommand(); + command.CommandText = query;// result.Sql; + command.Parameters.AddWithValue("@key", key.ToKey()); + return command; + } + + public void EnsureSchema(SqliteConnection connection) + { + var query = $"CREATE TABLE IF NOT EXISTS {TableName} (Key Long NOT NULL, Data BLOB NOT NULL,Modified DATETIME NOT NULL default current_timestamp, PRIMARY KEY (Key) )"; + + using var db = new QueryFactory(connection, compiler); + db.Statement(query); + // var command = connection.CreateCommand(); + // command.CommandText = query; + // command.Parameters.AddWithValue("@key", key.ToKey()); + + } + } + class sqlSecmaTree : ISqliteSchema + { + public void BindData(SqliteCommand cmd, ReadOnlyMemory data) + { + var p = cmd.Parameters.Add("@data", SqliteType.Blob); + p.Value = data.ToArray(); + } + + public void BindKey(SqliteCommand cmd, DataKey key) + { + if (key.Key is Guid guid) + { + cmd.Parameters.AddWithValue("@guid", guid); + } + } + + public (string Table, string Query) GetExistQuery(DataKey key) + { + var table = key.Namespace; + var query = $"SELECT EXISTS(SELECT 1 FROM {table} WHERE Guid=@guid);"; + return (table, query); + } + + public (string Table, string Query) GetReadQuery(DataKey key) + { + var table = key.Namespace; + var query = $"SELECT data FROM {table} WHERE Guid=@guid;"; + return (table, query); + } + + public (string Table, string Query) GetRemoveQuery(DataKey key) + { + var table = key.Namespace; + var query = $"DELETE FROM {table} WHERE GUid=@guid ;"; + return (table, query); + } + + public (string Table, string Query) GetWriteQuery(DataKey key) + { + var table = key.Namespace; + var query = $@" + INSERT INTO {table} (Guid, data) + VALUES (@guid, @data) + ON CONFLICT(guid) + DO UPDATE SET data=@data;"; + return (table, query); + } + + public string IntilizeSchema() + { + var query = "CREATE TABLE IF NOT EXISTS Tree (Guid TEXT NOT NULL, Data BLOB NOT NULL,Modified DATETIME NOT NULL default current_timestamp, PRIMARY KEY (GUID) )"; + return query; + } + } + class gzipCompress : IDataCompressor + { + public byte[] Compress(byte[] bytes) + { + using var otginal = new MemoryStream(bytes); + using var compressed = new MemoryStream(); + using GZipStream compresser = new GZipStream(compressed, CompressionLevel.Optimal); + otginal.CopyTo(compresser); + return compressed.ToArray(); + throw new NotImplementedException(); + } + + public byte[] DeCompress(byte[] bytes) + { + using var comppresed = new MemoryStream(bytes); + using var target = new MemoryStream(); + using GZipStream decompresser = new GZipStream(target, CompressionMode.Decompress); + decompresser.CopyTo(target); + GD.Print("LENGTH " + target.Length); + return target.ToArray(); + throw new NotImplementedException(); + } + } + public class TreeSelizerSingle : IDataSerializer + { + public ValueTask DeserializeAsync(Span bytes) + { + GD.Print(JsonConvert.DeserializeObject(Encoding.UTF8.GetString(bytes)).Length); + return new ValueTask(JsonConvert.DeserializeObject(Encoding.UTF8.GetString(bytes))); + } + + public ValueTask SerializeAsync(TreeEntry value) + { + return new ValueTask(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value))); + } + } + public class TreeSelizer : IDataSerializer + { + public ValueTask DeserializeAsync(Span bytes) + { + GD.Print(JsonConvert.DeserializeObject(Encoding.UTF8.GetString(bytes)).Length); + return new ValueTask(JsonConvert.DeserializeObject(Encoding.UTF8.GetString(bytes))); + } + + public ValueTask SerializeAsync(TreeEntry[] value) + { + return new ValueTask(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value))); + } + } + public class TreeEntry : ISpatialEntry + { + public TreeEntry(Guid guid, Vector3 position) + { + Guid = guid; + Position = position; + Bounds = new Aabb(position - Vector3.One, position + Vector3.One); + } + + public TreeEntry() + { + + } + public Guid Guid { get; set; } + + public Vector3 Position { get; set; } + + public Aabb Bounds { get; set; } + public override string ToString() + { + return $"{Guid}, {Position}, {Bounds}"; + } + } + public class ChunkSerlizer : IDataSerializer,byte[]> + { + public ValueTask> DeserializeAsync(byte[] bytes) + { + var b = new int[bytes.Length / 4]; + Buffer.BlockCopy(bytes.ToArray(), 0, b, 0, bytes.Length); + return new ValueTask>(new VoxelChunk(b)); + } + + public ValueTask SerializeAsync(VoxelChunk value) + { + if (value is VoxelChunk values) + { + var a = new byte[values.GetRawVoxelData().Length * sizeof(int)]; + Buffer.BlockCopy(values.GetRawVoxelData(), 0, a, 0, a.Length); + return ValueTask.FromResult(a); + } + throw new Exception(); + } + } + public class ChunkSerlizer2 : IDataSerializer, Chunk32>> + { + public ValueTask, Chunk32>> DeserializeAsync(Span bytes) + { + var b = new VoxelHandle[bytes.Length / 4]; + Buffer.BlockCopy(bytes.ToArray(), 0, b, 0, bytes.Length); + return new ValueTask, Chunk32>>(new VoxelChunk, Chunk32>(b)); + } + + public ValueTask SerializeAsync(IVoxelChunk, Chunk32> value) + { + if (value is VoxelChunk, Chunk32> values) + { + var a = new byte[values.GetRawVoxelData().Length * sizeof(int)]; + Buffer.BlockCopy(values.GetRawVoxelData(), 0, a, 0, a.Length); + return ValueTask.FromResult(a); + } + throw new Exception(); + } + } + public class ChunkSerlizer3 : IDataSerializer, Chunk32>, byte[]> + { + public ValueTask, Chunk32>> DeserializeAsync(byte[] bytes) + { + var b = new VoxelHandle[bytes.Length / 4]; + Buffer.BlockCopy(bytes.ToArray(), 0, b, 0, bytes.Length); + return new ValueTask, Chunk32>>(new VoxelChunk, Chunk32>(b)); + } + + public ValueTask SerializeAsync(VoxelChunk, Chunk32> value) + { + if (value is VoxelChunk, Chunk32> values) + { + var a = new byte[values.GetRawVoxelData().Length * sizeof(int)]; + Buffer.BlockCopy(values.GetRawVoxelData(), 0, a, 0, a.Length); + return ValueTask.FromResult(a); + } + throw new Exception(); + } + } + public class ChunkSerlizerStruct : IDataSerializer, Chunk32>, byte[]> + { + public ValueTask, Chunk32>> DeserializeAsync(byte[] bytes) + { + return new ValueTask, Chunk32>>(); + // throw new NotImplementedException(); + // var b = new StructuralInstance[bytes.Length / 4]; + // Buffer.BlockCopy(bytes.ToArray(), 0, b, 0, bytes.Length); + // return new ValueTask, Chunk32>>(new VoxelChunk, Chunk32>(b)); + } + + public ValueTask SerializeAsync(IWorldChunk, Chunk32> value) + { + return ValueTask.FromResult(new byte[0]); + // throw new NotImplementedException(); + // if (value is VoxelChunk, Chunk32> values) + // { + // var a = new byte[values.GetRawVoxelData().Length * sizeof(int)]; + // Buffer.BlockCopy(values.GetRawVoxelData(), 0, a, 0, a.Length); + // return ValueTask.FromResult(a); + // } + // throw new Exception(); + } + } + public class ChunkSerlizer4 : IDataSerializer, Chunk32>, byte[]> + { + + public ValueTask, Chunk32>> DeserializeAsync(byte[] bytes) + { + var data = JsonConvert.DeserializeObject, Chunk32>>(Encoding.UTF8.GetString(bytes)); + return ValueTask.FromResult(data); + } + + public ValueTask SerializeAsync(IVoxelChunk, Chunk32> value) + { + var result = JsonConvert.SerializeObject(value); + return ValueTask.FromResult(Encoding.UTF8.GetBytes(result)); + } + } + // [Migration(1)] + // public class CreateChunksTable : Migration + // { + + // public override void Up() + // { + // Create.Table("Chunks") + // .WithColumn("X").AsInt32().NotNullable() + // .WithColumn("Y").AsInt32().NotNullable() + // .WithColumn("Z").AsInt32().NotNullable() + // .WithColumn("Data").AsBinary().NotNullable() + // .WithColumn("Modified").AsDateTime().NotNullable(); + // // .WithColumn("Compression").AsString().Nullable(); + // Create.PrimaryKey("PK_Chunks").OnTable("Chunks").Columns("X", "Y", "Z"); + // } + + // public override void Down() + // { + // Delete.Table("Chunks"); + // } + // } + // void EnsureTable(string table, SqliteConnection conn) + // { + // var props = typeof(T).GetProperties(); + // var cols = string.Join(", ", props.Select(p => $"{p.Name} {MapType(p.PropertyType)}")); + // var sql = $"CREATE TABLE IF NOT EXISTS {table} ({cols});"; + // conn.Execute(sql); + // } + + // string MapType(Type t) => t switch + // { + // Type x when x == typeof(int) => "INTEGER", + // Type x when x == typeof(string) => "TEXT", + // _ => "BLOB" + // }; + + // [ProtoContract] + // public class Chunk + // { + // public int X { get; set; } + // public int Y { get; set; } + // public int Z { get; set; } + // // [ProtoMember(1)] + // public byte[] Data { get; set; } + // // [ProtoMember(2)] + // // public ChunkEntry[] ChunkEntries { get; set; } + // public DateTime Modified { get; set; } + // } + // [ProtoContract] + // public class ChunkEntry + // { + // // [ProtoMember(1)] + // public string Name { get; set; } + // // [ProtoMember(2)] + // public byte[] Data { get; set; } + // } + public override void _Process(double delta) + { + foreach (var item in taskes.ToArray()) + { + if (item.Item2.IsCompleted) + { + var instance = new MeshInstance3D() { MaterialOverride = material }; + AddChild(instance); + instance.Position = new Vector3(item.Item1.X*Chunk32.Size,item.Item1.Y*Chunk32.Size,item.Item1.Z*Chunk32.Size); + // GD.Print(stopwatch.ElapsedMilliseconds); + + item.Item2.Result.IfSome(m => instance.Mesh = m); + item.Item2.Result.IfNone(() => instance.QueueFree()); + taskes.Remove(item); + } + } + + // GD.Print(string.Join(',', tasks.Select(item => item.Status))); + // if (tasks is not null && tasks.All(item => item.Status == TaskStatus.RanToCompletion)) + // { + // // Task.WaitAll(tasks.ToArray()); + // tasks.ForEach(task => + // { + // var node = new MeshInstance3D(); + // node.Position = task.Result.Item1.ToGlobalOrigin(); + // node.Mesh = task.Result.Item2; + // node.Mesh.SurfaceSetMaterial(0, material); + // AddChild(node); + // }); + // GD.Print("done"); + // tasks = null; + // // task = null; + // } + // return; + // foreach (var item in chunks) + // { + // var c = item.Value as Octree; + // c.GetRoot().TraverseBounds(Vector3.Zero, (pos, size, isleaf) => DebugDraw3D.DrawAabb(new Aabb(item.Key.ToGlobalOrigin() + pos, new Vector3(size, size, size)), isleaf ? Colors.Blue : Colors.Red)); + // // DebugDraw3D.DrawAabb(new Aabb(item.Key.ToGlobalOrigin(item.Value.Size), item.Value.Size), new Color(item.Key.X / 10f, item.Key.Y / 10f, item.Key.Z / 10f, 1)); + // } + } +} +public static class SqlKataExtensions +{ + + public static int InsertOrReplace(this QueryFactory db, string table, object data) + { + var compiler = db.Compiler; + var insertQuery = new Query(table).AsInsert(data); + var compiled = compiler.Compile(insertQuery); + + // Replace INSERT INTO with SQLite's "INSERT OR REPLACE INTO" + var sql = compiled.Sql.Replace("INSERT INTO", "INSERT OR REPLACE INTO"); + + // Pass NamedBindings (dictionary) — this is the important part + return db.Statement(sql, compiled.NamedBindings); + } +public static int InsertOrUpdateOnConflict(this QueryFactory db, string table, object insertData, string[] conflictColumns, string[] updateColumns) +{ + var compiler = db.Compiler; + var insertQuery = new Query(table).AsInsert(insertData); + var compiled = compiler.Compile(insertQuery); + + var conflict = string.Join(", ", conflictColumns); + var setClause = string.Join(", ", updateColumns.Select(c => $"{c} = excluded.{c}")); + + var sql = $"{compiled.Sql} ON CONFLICT({conflict}) DO UPDATE SET {setClause};"; + + return db.Statement(sql, compiled.NamedBindings); +} + +} +public class ChunkContainer where TChunkSize : IChunkSize { + private Dictionary voxelChunks = new(); + public IOption> GetChunk(string layer) where TVoxel : IEquatable + { + return voxelChunks.GetValue(layer).Map(item => item as Chunk); + } + public void SetChunk(string layer, Chunk chunk) where TVoxel : IEquatable + { + voxelChunks.Add(layer, chunk); + } +} +public class Chunk where TVoxel : IEquatable where TChunkSize : IChunkSize{ + public VoxelPalette, PaletteLevel, TVoxel> Palette { get; set; } + + public IVoxelChunk ChunkData { get; set; } +} \ No newline at end of file diff --git a/src/voxelgame/ChunkSurfaceNetTest.cs.uid b/src/voxelgame/ChunkSurfaceNetTest.cs.uid new file mode 100644 index 0000000..c9474fa --- /dev/null +++ b/src/voxelgame/ChunkSurfaceNetTest.cs.uid @@ -0,0 +1 @@ +uid://q2phyhqtiacu diff --git a/src/voxelgame/FloodFill/VoxelFloodFill.cs b/src/voxelgame/FloodFill/VoxelFloodFill.cs new file mode 100644 index 0000000..744f982 --- /dev/null +++ b/src/voxelgame/FloodFill/VoxelFloodFill.cs @@ -0,0 +1,45 @@ +// public interface IVoxelGraph +// { +// 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 predicate, uint maxCount = uint.MaxValue, uint maxValue = uint.MaxValue) + { + var visited = new HashSet(); + 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)); + + } + } + + } +} \ No newline at end of file diff --git a/src/voxelgame/FloodFill/VoxelFloodFill.cs.uid b/src/voxelgame/FloodFill/VoxelFloodFill.cs.uid new file mode 100644 index 0000000..8cbf860 --- /dev/null +++ b/src/voxelgame/FloodFill/VoxelFloodFill.cs.uid @@ -0,0 +1 @@ +uid://bb46scw4c0yhh diff --git a/src/voxelgame/NewScript.cs b/src/voxelgame/NewScript.cs index 2e36eec..4c2c44f 100644 --- a/src/voxelgame/NewScript.cs +++ b/src/voxelgame/NewScript.cs @@ -5,4 +5,8 @@ using System; public partial class NewScript : Node { IOption OptionTest; + void test() + { + + } } diff --git a/src/voxelgame/SimpleDataBase.cs b/src/voxelgame/SimpleDataBase.cs new file mode 100644 index 0000000..0014cad --- /dev/null +++ b/src/voxelgame/SimpleDataBase.cs @@ -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 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 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(); + } +} \ No newline at end of file diff --git a/src/voxelgame/SimpleDataBase.cs.uid b/src/voxelgame/SimpleDataBase.cs.uid new file mode 100644 index 0000000..0de7f6e --- /dev/null +++ b/src/voxelgame/SimpleDataBase.cs.uid @@ -0,0 +1 @@ +uid://duss0kqmyo2ee diff --git a/src/voxelgame/VoxelGame.csproj b/src/voxelgame/VoxelGame.csproj index e73de93..acdcbdd 100644 --- a/src/voxelgame/VoxelGame.csproj +++ b/src/voxelgame/VoxelGame.csproj @@ -1,9 +1,19 @@ - + net8.0 true + true + + + + + + + + + \ No newline at end of file diff --git a/src/voxelgame/VoxelGame.csproj.old b/src/voxelgame/VoxelGame.csproj.old new file mode 100644 index 0000000..cf19c67 --- /dev/null +++ b/src/voxelgame/VoxelGame.csproj.old @@ -0,0 +1,19 @@ + + + net8.0 + true + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/voxelgame/VoxelGame.sln b/src/voxelgame/VoxelGame.sln new file mode 100644 index 0000000..5794993 --- /dev/null +++ b/src/voxelgame/VoxelGame.sln @@ -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 diff --git a/src/voxelgame/Voxels/BoxelMesher.cs b/src/voxelgame/Voxels/BoxelMesher.cs new file mode 100644 index 0000000..0f56f8f --- /dev/null +++ b/src/voxelgame/Voxels/BoxelMesher.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using Godot; + +namespace SJK.Voxels; + +public class BoxelMesher +{ + private readonly IBoxelMesherApi api; + + public BoxelMesher(IBoxelMesherApi api) + { + this.api = api; + } + public Mesh Mesh(IVoxelChunk values) + { + var vertices = new List(); + var normals = new List(); + var triangles = new List(); + 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 vertices, List triangles,List 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 +{ + 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 + }; +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/BoxelMesher.cs.uid b/src/voxelgame/Voxels/BoxelMesher.cs.uid new file mode 100644 index 0000000..45eabf4 --- /dev/null +++ b/src/voxelgame/Voxels/BoxelMesher.cs.uid @@ -0,0 +1 @@ +uid://puaveig1esds diff --git a/src/voxelgame/Voxels/Chunks/ChunkPos.cs b/src/voxelgame/Voxels/Chunks/ChunkPos.cs new file mode 100644 index 0000000..c8cf8ea --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/ChunkPos.cs @@ -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 +{ + public int X { get; } + public int Y { get; } + public int Z { get; } + long ToKey(); +} +public record struct Dimension(int Id); +public interface IChunkPos : + IChunkPos, + IAdditionOperators, + ISubtractionOperators, + IEquatable + where TSelf : struct, IChunkPos +{ + 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, + IEquatable, + IAdditionOperators, + ISubtractionOperators +{ + 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 : IEquatable>, IChunkPos> where TChunk : IChunkSize +{ + 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 FromGlobal(Vector3I globalPos) => FromGlobal(globalPos.X, globalPos.Y, globalPos.Z); + // public static ChunkPos FromGlobal(int x, int y, int z) + // { + // return new ChunkPos(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 FromGlobal(int x, int y, int z) + { + return new ChunkPos(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 ToLocal(Vector3I globalPos) => ToLocal(globalPos.X, globalPos.Y, globalPos.Z); + // public static VoxelPos ToLocal(int x, int y, int z) + // { + // return new VoxelPos( + // Mod(x, TChunk.Size), + // Mod(y, TChunk.Size), + // Mod(z, TChunk.Size) + // ); + // } + public static VoxelPos ToLocal(int x, int y, int z) + { + int mask = TChunk.Size - 1; + + return new VoxelPos( + 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 pos) => pos.Value; + + // Equality + public bool Equals(ChunkPos other) => Value == other.Value; + public override bool Equals(object? obj) => obj is ChunkPos 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(Vector3I v) => new ChunkPos(v); + + // --- Arithmetic helpers (optional) --- + public static ChunkPos operator +(ChunkPos a, ChunkPos b) => new ChunkPos(a.Value + b.Value); + public static ChunkPos operator -(ChunkPos a, ChunkPos b) => new ChunkPos(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 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(x, y, z); + } + + public static ChunkPos Create(int x, int y, int z) + { + return new ChunkPos(x, y, z); + } + + // public bool Equals(IChunkPos other)=> X==other.X && Y == other.Y && Z == other.Z; +} +public interface IChunkSize + where TSelf : IChunkSize +{ + 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 +{ + public static int BitsPerAxis => 4; // 2^5 = 32 + + public static int Size => 16; +} +public readonly struct Chunk32 : IChunkSize +{ + public static int BitsPerAxis => 5; // 2^5 = 32 + public static int Size => 32; +} + +public readonly struct Chunk64 : IChunkSize +{ + public static int BitsPerAxis => 6; // 2^6 = 64 + + public static int Size => 64; +} + +public readonly struct Chunk256 : IChunkSize +{ + public static int BitsPerAxis => 8; // 2^6 = 64 + public static int Size => 256; +} +public readonly struct VoxelPos : IEquatable +{ + public readonly int X; + public readonly int Y; + public readonly int Z; + public ChunkPos GetChunkPos() where TChunk : IChunkSize => ChunkPos.FromGlobal(X, Y, Z); + public VoxelPos GetVoxelPos() where TChunk : IChunkSize => ChunkPos.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 + where TChunk : IChunkSize +{ + 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? voxelPos) + { + + if ((uint)pos.X < TChunk.Size || (uint)pos.Y < TChunk.Size || (uint)pos.Z < TChunk.Size) + { + voxelPos = new VoxelPos(pos.X,pos.Y,pos.Z); + return true; + } + + voxelPos = null; + return false; + } + public VoxelPos ToGlobal(ChunkPos chunkPos) => chunkPos.ToGlobalOrigin() + new VoxelPos(X,Y,Z); + public (ChunkPos ChunkPos, VoxelPos VoxelPos) Offset(Vector3I offset, ChunkPos 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(cx, cy, cz), new VoxelPos(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 pos) => pos._packed; + public static implicit operator Vector3I(VoxelPos pos) => new Vector3I(pos.X, pos.Y, pos.Z); + public static explicit operator VoxelPos(VoxelPos pos) => new Vector3I(pos.X, pos.Y, pos.Z); + + public static VoxelPos operator +(VoxelPos a, Vector3I b) => + new VoxelPos(a.X + b.X, a.Y + b.Y, a.Z + b.Z); + + public static VoxelPos operator -(VoxelPos a, Vector3I b) => + new VoxelPos(a.X - b.X, a.Y - b.Y, a.Z - b.Z); + +} + diff --git a/src/voxelgame/Voxels/Chunks/ChunkPos.cs.uid b/src/voxelgame/Voxels/Chunks/ChunkPos.cs.uid new file mode 100644 index 0000000..2a76155 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/ChunkPos.cs.uid @@ -0,0 +1 @@ +uid://dlgeisueoqh40 diff --git a/src/voxelgame/Voxels/Chunks/IVoxelChunk.cs b/src/voxelgame/Voxels/Chunks/IVoxelChunk.cs new file mode 100644 index 0000000..aa7677c --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/IVoxelChunk.cs @@ -0,0 +1,193 @@ +using System.Collections.Generic; +using Godot; +using SJK.Functional; +using System.Linq; + +namespace SJK.Voxels; +public interface IVoxelChunk : IReadOnlyVoxelChunk +{ + 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 : IVoxelChunk, IReadOnlyVoxelChunk where TChunk : IChunkSize +{ + void SetVoxel(VoxelPos position, TVoxel voxel); +} +// public interface IVoxelChunk : IReadOnlyVoxelChunk +// { + +// } + +// public interface IReadOnlyVoxelChunk +// { +// } +public interface IReadOnlyVoxelChunk// : IReadOnlyVoxelChunk +{ + TVoxel GetVoxel(VoxelPos voxelPos); + VoxelChunkEnumerator GetEnumerator() => new VoxelChunkEnumerator(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 : IReadOnlyVoxelChunk where TChunk : IChunkSize +{ + TVoxel GetVoxel(VoxelPos position); + bool IsInBounds(Vector3I position) => IChunkSize.IsInBounds(position); + virtual bool ContainsPos(VoxelPos position) => Bounds.IsInBounds((Vector3I)position); + new VoxelChunkEnumerator GetEnumerator() => new VoxelChunkEnumerator(this); +} +public static class VoxelExtensions{ + public static void SetVoxel(this IVoxelChunk voxelChunk, int x, int y, int z, TVoxel value) =>voxelChunk.SetVoxel(new VoxelPos(x,y,z),value); + public static TVoxel GetVoxel(this IVoxelChunk voxelChunk, int x, int y, int z) =>voxelChunk.GetVoxel(new VoxelPos(x,y,z)); +} +public interface ILayerMap { + bool TryGetChunk(Vector3I chunkPos, out IVoxelChunk chunk); // Untyped + void SetChunk(Vector3I chunkPos, IVoxelChunk chunk); +} +public class LayerMap : ILayerMap { + private readonly Dictionary> chunks = new(); + + public bool TryGetChunk(Vector3I chunkPos, out IVoxelChunk chunk) + { + var r =chunks.TryGetValue(chunkPos, out var result); + chunk = result; + return r; +} + public void SetChunk(Vector3I chunkPos, IVoxelChunk 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; +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Chunks/IVoxelChunk.cs.uid b/src/voxelgame/Voxels/Chunks/IVoxelChunk.cs.uid new file mode 100644 index 0000000..b588816 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/IVoxelChunk.cs.uid @@ -0,0 +1 @@ +uid://bn4dkifwcftqm diff --git a/src/voxelgame/Voxels/Chunks/Mesher/ChunkMesher.cs b/src/voxelgame/Voxels/Chunks/Mesher/ChunkMesher.cs new file mode 100644 index 0000000..d4a9ff8 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/Mesher/ChunkMesher.cs @@ -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(Vector3I pos, IReadOnlyVoxelChunk voxels,Func 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( + IReadOnlyVoxelChunk voxels, + // Vector3I chunkPos, + Func isVoid, + Func faceUvs + + // IVoxelWorld> 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( + Vector3I localPos, + IReadOnlyVoxelChunk voxels, + ChunkMeshData meshData, + ref int counter, + Func isVoid, + Func 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(Vector3I localPos, int axis, IReadOnlyVoxelChunk voxels,Func 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(Vector3I pos, IReadOnlyVoxelChunk voxels,Func 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 vertices; + public List triangles; + public List colors; + public List uvs; + public List uvs2; + + public bool initialized; + + public void ClearData() + { + if (!initialized) + { + vertices = new List(); + triangles = new List(); + uvs = new List(); + uvs2 = new List(); + colors = new List(); + + 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 vertexIndices = + // new Dictionary(); + // List sharedTriangles = new List(); + + // 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); + } + } +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Chunks/Mesher/ChunkMesher.cs.uid b/src/voxelgame/Voxels/Chunks/Mesher/ChunkMesher.cs.uid new file mode 100644 index 0000000..8e81d84 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/Mesher/ChunkMesher.cs.uid @@ -0,0 +1 @@ +uid://bd586qflhhi13 diff --git a/src/voxelgame/Voxels/Chunks/Mesher/GreedyMesher.cs b/src/voxelgame/Voxels/Chunks/Mesher/GreedyMesher.cs new file mode 100644 index 0000000..11e6564 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/Mesher/GreedyMesher.cs @@ -0,0 +1,1071 @@ +using System.Numerics; + +namespace BinaryGreedyMesher; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using SJK.Functional; +using SJK.Voxels; + + public static class MaskBuilder + { + /// + /// Builds a 2D mask slice (array of uints) for a given Z level in a 32³ chunk. + /// Each uint row corresponds to X bits, each bit representing a voxel (1=solid). + /// + public static unsafe uint[] BuildMask32(IVoxelChunk chunk, int zLevel) + { + const int SIZE = 32; + uint[] mask = new uint[SIZE]; + + for (int y = 0; y < SIZE; y++) + { + uint row = 0; + for (int x = 0; x < SIZE; x++) + { + if (chunk.GetVoxel(new (x, y, zLevel)) != 0) + row |= (1u << x); + } + mask[y] = row; + } + + return mask; + } + + /// + /// Builds a 2D mask slice (array of ulong) for a given Z level in a 64³ chunk. + /// Each ulong row corresponds to X bits. + /// + public static unsafe ulong[] BuildMask64(IVoxelChunk chunk, int zLevel) + { + const int SIZE = 64; + ulong[] mask = new ulong[SIZE]; + + for (int y = 0; y < SIZE; y++) + { + ulong row = 0; + for (int x = 0; x < SIZE; x++) + { + if (chunk.GetVoxel(new(x, y, zLevel))!= 0) + row |= (1ul << x); + } + mask[y] = row; + } + + return mask; + } + } +public class GreedyMesher +{ + public IOption BuildMesh(IReadOnlyVoxelChunk chunk, Func isSolid ){ + var faces = BinaryGreedyMesher.BinaryGreedyMesherCore_Fixed.BuildChunkBinaryQuads((x, y, z) => chunk.IsInBounds(new(x, y, z)) ? isSolid(chunk.GetVoxel(new Godot.Vector3I(x, y, z))) : false); + List vertices = new(); + List indices = new(); + List uvs = new(); + faces.QuadsToMesh(vertices,uvs,indices); + + if (vertices.Count != 0) + { + Godot.Collections.Array surfaceArray = []; + surfaceArray.Resize((int)Godot.Mesh.ArrayType.Max); + surfaceArray[(int)Godot.Mesh.ArrayType.Vertex] = vertices.ToArray();//.Select(item => new Godot.Vector3(item.X, item.Y, item.Z)).ToArray(); + surfaceArray[(int)Godot.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)Godot.Mesh.ArrayType.Index] = indices.ToArray(); + var arrayMesh = new Godot.ArrayMesh(); + arrayMesh.AddSurfaceFromArrays(Godot.Mesh.PrimitiveType.Triangles, surfaceArray); + return ((Godot.Mesh)arrayMesh).ToOption(); + } + return None.Of(); + + } + public IOption BuildMesh(List faces ){ + + List vertices = new(); + List indices = new(); + List uvs = new(); + faces.QuadsToMesh(vertices,uvs,indices); + + if (vertices.Count != 0) + { + Godot.Collections.Array surfaceArray = []; + surfaceArray.Resize((int)Godot.Mesh.ArrayType.Max); + surfaceArray[(int)Godot.Mesh.ArrayType.Vertex] = vertices.ToArray();//.Select(item => new Godot.Vector3(item.X, item.Y, item.Z)).ToArray(); + surfaceArray[(int)Godot.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)Godot.Mesh.ArrayType.Index] = indices.ToArray(); + var arrayMesh = new Godot.ArrayMesh(); + arrayMesh.AddSurfaceFromArrays(Godot.Mesh.PrimitiveType.Triangles, surfaceArray); + return ((Godot.Mesh)arrayMesh).ToOption(); + } + return None.Of(); + + } + } +// BinaryGreedyMesher.cs +// Faithful C# port (pure binary mesher) of the Rust binary greedy mesher core you supplied. +// Keeps bitwise optimizations: trailing zeros/ones, mask expansion, clearing via bit ops. +// +// Note: This focuses on the pure binary mesher. AO and block-type hashing are left as hooks +// for later (see comments). The structure and bit-twiddling mirrors the Rust code. + +public static class Constants +{ + // match the rust demo constants by default + public const int CHUNK_SIZE = 32; + public const int CHUNK_SIZE_P = CHUNK_SIZE + 2; // padding both sides (34) +} + + /// + /// Equivalent to Rust's GreedyQuad { x,y,w,h } + /// + public struct GreedyQuad + { + public uint X; + public uint Y; + public uint W; + public uint H; + + public GreedyQuad(uint x, uint y, uint w, uint h) + { + X = x; Y = y; W = w; H = h; + } + } + + /// + /// A quad with face axis information (0..5) and an axis position (the 'y' / axis_pos in the Rust code). + /// axis: + /// 0 = Down, 1 = Up, 2 = Left, 3 = Right, 4 = Forward, 5 = Back + /// axisPos is the plane index in chunk space the Rust used. + /// x,y are the GreedyQuad fields mapped into world-local plane coordinates. + /// + public struct FaceQuad + { + public int FaceAxis; // 0..5 + public uint AxisPos; // the axis position (the 'y' in the Rust data map) + public GreedyQuad Quad; + + public FaceQuad(int faceAxis, uint axisPos, GreedyQuad quad) + { + FaceAxis = faceAxis; + AxisPos = axisPos; + Quad = quad; + } + } + + public static unsafe class BinaryGreedyMesherCore + { + // ---------- Greedy plane mesher ---------- + // C# port of: + // pub fn greedy_mesh_binary_plane(mut data: [u32; 32], lod_size: u32) -> Vec + // + // `data` is modified (bits cleared) just like in Rust. + // + // Uses BitOperations.TrailingZeroCount to find runs of zeros/ones and grows quads horizontally/vertically. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static List GreedyMeshBinaryPlane(Span data, uint lodSize) + { + var greedyQuads = new List(64); + + // for row in 0..data.len() + for (int row = 0; row < data.Length; row++) + { + // In Rust: let mut y = 0; + uint y = 0; + + // loop until we've processed the vertical size for this row + while (y < lodSize) + { + // find first solid, "air/zero's" could be first so skip + // y += (data[row] >> y).trailing_zeros(); + uint rowBits = data[row]; + // shift right by y and count trailing zeros + uint shifted = rowBits >> (int)y; + // trailing_zeros on shifted + uint tz; + if (shifted == 0) + tz = lodSize - y; // if nothing left, jump to end + else + tz = (uint)BitOperations.TrailingZeroCount(shifted); + + y += tz; + if (y >= lodSize) + { + // reached top / nothing left + break; + } + + // let h = (data[row] >> y).trailing_ones(); + // trailing_ones can be expressed as trailing_zeros of the inverted value + uint bitsFromY = rowBits >> (int)y; + uint h; + if (bitsFromY == 0) + { + h = 0; + } + else + { + h = (uint)BitOperations.TrailingZeroCount(~bitsFromY); + } + + if (h == 0) + { + // defensive: if bit was 0 after shift, advance by 1 to avoid infinite loops + h = 1; + } + + // convert height 'num' to positive bits repeated 'num' times: + // let h_as_mask = u32::checked_shl(1, h).map_or(!0, |v| v - 1); + uint hAsMask; + if (h >= 32u) + { + hAsMask = 0xFFFFFFFFu; + } + else + { + hAsMask = (1u << (int)h) - 1u; + } + + // let mask = h_as_mask << y; + uint mask = hAsMask << (int)y; + + // grow horizontally + uint w = 1; + while ((row + w) < lodSize) + { + // fetch bits spanning height in the next row + // let next_row_h = (data[row + w] >> y) & h_as_mask; + uint nextRowBits = data[(int)(row + w)]; + uint nextRowH = (nextRowBits >> (int)y) & hAsMask; + + if (nextRowH != hAsMask) + { + break; // can no longer expand horizontally + } + + // nuke the bits we expanded into: data[row + w] = data[row + w] & !mask; + data[(int)(row + w)] &= ~mask; + w++; + } + + // push the quad + greedyQuads.Add(new GreedyQuad((uint)row, y, w, h)); + + // y += h; + y += h; + } + } + + return greedyQuads; + } + + // ---------- Chunk binary plane builder ---------- + // Builds binary face planes for each of the 6 face directions, using + // the same approach in your Rust function up to the greedy plane stage. + // + // Input: + // isSolid(paddedX, paddedY, paddedZ) => returns whether that padded voxel is solid. + // padded coords range: 0 .. CHUNK_SIZE_P-1 + // + // Output: + // List - each quad's face axis (0..5), axis pos and quad (x,y,w,h). + // + // This mirrors: + // - axis_cols building (three axes with u64 columns) + // - col_face_masks computation (six face masks: ascend/descend) + // - extracting 32-bit binary planes and calling greedy_mesh_binary_plane on them + // + public static List BuildChunkBinaryQuads(Func isSolid) + { + const int CS = Constants.CHUNK_SIZE; + const int CSP = Constants.CHUNK_SIZE_P; // padded size (CS + 2) + // Because rust uses arrays sized by CHUNK_SIZE_P and sets bits up to CHUNK_SIZE, + // we keep same layout: axis_cols[3][CSP][CSP] of u64 + var axisCols = new ulong[3][][]; + for (int a = 0; a < 3; a++) + { + axisCols[a] = new ulong[CSP][]; + for (int i = 0; i < CSP; i++) axisCols[a][i] = new ulong[CSP]; + } + + // inline add_voxel_to_axis_cols from Rust + void AddVoxelToAxisCols(bool solid, int x, int y, int z) + { + if (!solid) return; + // x,z - y axis + axisCols[0][z][x] |= 1UL << (y); + // z,y - x axis + axisCols[1][y][z] |= 1UL << (x); + // x,y - z axis + axisCols[2][y][x] |= 1UL << (z); + } + + // --- fill inner chunk voxels (center region) --- + // In the Rust code, inner chunk is mapped within chunks_ref center, + // but here we assume the caller's isSolid includes neighbor padding. + // The Rust used x+1,y+1,z+1 when adding inner chunk into the padded arrays. + for (int z = 0; z < CS; z++) + for (int y = 0; y < CS; y++) + for (int x = 0; x < CS; x++) + { + // add into padded coords (shift by +1) + bool solid = isSolid(x + 1, y + 1, z + 1); + AddVoxelToAxisCols(solid, x + 1, y + 1, z + 1); + } + + // --- neighbor chunk voxels (padded borders) --- + // The Rust example loops all boundaries; we replicate the same loops. + // loop z at 0 and CHUNK_SIZE_P - 1 + for (int z = 0; z <= 1; z++) + { + int zz = (z == 0) ? 0 : (CSP - 1); + for (int y = 0; y < CSP; y++) + for (int x = 0; x < CSP; x++) + { + // the isSolid func is expected to map padded coords to neighbor-blocks appropriately + bool solid = isSolid(x, y, zz); + AddVoxelToAxisCols(solid, x, y, zz); + } + } + + // loops for y=0 and y=CSP-1 + for (int y = 0; y < CSP; y++) + { + // handled above partially; replicate the Rust style explicitly: + // for z in 0..CHUNK_SIZE_P { for y in [0, CHUNK_SIZE_P - 1] { ... } } + } + // (Rust had three loops to fill all faces; our previous two loops already add boundaries at min/max, + // but to be explicit, you can call AddVoxelToAxisCols for all 6 faces - done in next triple loops) + + // For completeness and parity with Rust, explicitly fill the remaining face planes: + // y face boundaries: + for (int z = 0; z < CSP; z++) + for (int x = 0; x < CSP; x++) + { + bool s0 = isSolid(x, 0, z); + AddVoxelToAxisCols(s0, x, 0, z); + bool s1 = isSolid(x, CSP - 1, z); + AddVoxelToAxisCols(s1, x, CSP - 1, z); + } + + // x face boundaries: + for (int z = 0; z < CSP; z++) + for (int y = 0; y < CSP; y++) + { + bool s0 = isSolid(0, y, z); + AddVoxelToAxisCols(s0, 0, y, z); + bool s1 = isSolid(CSP - 1, y, z); + AddVoxelToAxisCols(s1, CSP - 1, y, z); + } + + // --- face culling masks --- + // col_face_masks[6][CSP][CSP] : for each axis face direction (down, up, left, right, forward, back) + var colFaceMasks = new ulong[6][][]; + for (int i = 0; i < 6; i++) + { + colFaceMasks[i] = new ulong[CSP][]; + for (int j = 0; j < CSP; j++) colFaceMasks[i][j] = new ulong[CSP]; + } + + // for axis in 0..3: + for (int axis = 0; axis < 3; axis++) + { + for (int z = 0; z < CSP; z++) + for (int x = 0; x < CSP; x++) + { + // set if current is solid, and next is air + ulong col = axisCols[axis][z][x]; + + // sample descending axis, and set true when air meets solid + // col_face_masks[2 * axis + 0][z][x] = col & !(col << 1); + colFaceMasks[2 * axis + 0][z][x] = col & ~(col << 1); + + // sample ascending axis, and set true when air meets solid + // col_face_masks[2 * axis + 1][z][x] = col & !(col >> 1); + colFaceMasks[2 * axis + 1][z][x] = col & ~(col >> 1); + } + } + + // --- greedy meshing planes for every axis (6) --- + // We will extract each 32-bit plane for each (z,x) pair and run GreedyMeshBinaryPlane. + // In Rust, planes were stored in HashMap keyed by (block+ao) and axis plane y. Here we keep it simple: + var resultQuads = new List(1024); + Span plane = stackalloc uint[Constants.CHUNK_SIZE];//MOved + for (int axis = 0; axis < 6; axis++) + { + // For each possible axis plane position we iterate inner CHUNK_SIZE (0..CHUNK_SIZE-1) for x,z in unpadded coordinates + // In Rust: for z in 0..CHUNK_SIZE { for x in 0..CHUNK_SIZE { let mut col = col_face_masks[axis][z+1][x+1]; ... } } + for (int z = 0; z < Constants.CHUNK_SIZE; z++) + for (int x = 0; x < Constants.CHUNK_SIZE; x++) + { + // skip padded by adding 1(for x padding) and (z+1) for (z padding) + // let mut col = col_face_masks[axis][z + 1][x + 1]; + ulong col = colFaceMasks[axis][z + 1][x + 1]; + + // removes the right most padding value, because it's invalid + // col >>= 1; + col >>= 1; + + // removes the left most padding value, because it's invalid + // col &= !(1 << CHUNK_SIZE as u64); + col &= ~(1UL << Constants.CHUNK_SIZE); + + // Now iterate set bits in col -> each set bit is a face y pos in the padded column + // We'll convert the column into a 32-entry uint[] plane where each bit corresponds to z (or x) depending on axis + while (col != 0UL) + { + // let y = col.trailing_zeros(); + int yBit = BitOperations.TrailingZeroCount(col); + // clear least significant set bit + // col &= col - 1; + col &= (col - 1UL); + + // Convert axis/voxel_pos mapping as in Rust: + // match axis { + // 0 | 1 => ivec3(x as i32, y as i32, z as i32), // down,up + // 2 | 3 => ivec3(y as i32, z as i32, x as i32), // left, right + // _ => ivec3(x as i32, z as i32, y as i32), // forward, back + // } + // For pure binary mesher we only need to produce a binary plane for the axis at that axis position (yBit). + // We'll create plane data arranged exactly as the Rust expects for greedy_plane: data[row] = 32-bit words representing a column along the local axis. + + // Build a 32-length uint plane (rows correspond to 0..CHUNK_SIZE-1) + // Span plane = stackalloc uint[Constants.CHUNK_SIZE];//Moved out of loop // matches Rust [u32; 32] + // zero initialize + for (int i = 0; i < Constants.CHUNK_SIZE; i++) plane[i] = 0u; + + // Fill plane bits for this specific axis_pos (yBit) using the col_face_masks grid: + // The Rust code later grouped faces by block hash and y (axis position) and OR'd bits into data[x] for row x. + // We will create the plane for this axis_pos directly: iterate rows (0..CHUNK_SIZE-1) and set the z-bit in plane[row] + // depending on axis orientation. + + // For axis mapping we must extract the correct column from colFaceMasks: + // colFaceMasks at [axis][z+1][x+1] held the vertically-sampled 64-bit mask; we want the row bits along the other axis. + // + // To keep parity, we will assemble the plane where plane[row] bit b corresponds to local coordinate b along the second dimension. + // + // Simpler approach: rebuild plane by sampling col_face_masks for fixed axis_pos (yBit) across rows. + // For each row r (0..CHUNK_SIZE-1): + for (int row = 0; row < Constants.CHUNK_SIZE; row++) + { + // We need to map `axis`, loop variables x,z,row into padded indexes to read whether face exists at that local bit. + int px = x + 1; // padded x + int pz = z + 1; // padded z + int py = yBit; // y bit is already in padded coordinates range 0..(CHUNK_SIZE+1) + // For face masks we stored mask along the descending/ascending axis in colFaceMasks, + // where each column's bit position corresponds to the local axis coordinate (0..63). The col face bit + // for local coordinate 'row' is (colFaceMasks[axis][?][?] >> row) & 1. + // We need to determine which colFaceMasks entry yields the bit for this row. + + // Mapping from (axis, row, x/pz/px) to colFaceMasks indices: + // This is somewhat intricate; easier is to reconstruct the plane by reading the axisCols directly: + // build the binary plane by reading the same axisCols logic used earlier: + ulong columnValue = 0UL; + switch (axis) + { + // face: Down (0) or Up (1) -> column is axisCols[0] with index [z+1][x+1] + case 0: + case 1: + // in Rust: axis_cols[0][z][x] has bits along y + // We want to know whether face bit exists at this axis position for the given row + // the face existence for (z,x,row) is: check col_face_masks[axis][(row)+1][x+1] and find bit at z+1? It's complex to replicate 1:1 here. + // Instead: sample the original 'colFaceMasks' 64-bit column at the perpendicular coordinate and check bit (row + 1) + // colFaceMasks[axis][(z+1)][(x+1)] contains a 64-bit mask of set bits across the depth axis (y). + columnValue = colFaceMasks[axis][row + 1][x + 1]; // note: switch indexes to sample adjacent direction + break; + // faces Left/Right (2/3): axisCols[1] used + case 2: + case 3: + columnValue = colFaceMasks[axis][x + 1][row + 1]; + break; + // faces Forward/Back (4/5): axisCols[2] used + default: + columnValue = colFaceMasks[axis][row + 1][x + 1]; + break; + } + + // Now test whether the bit is set at the specific axis bit index 'py': + int bitIndex = py; // which bit along the column represents the face plane + bool set = ((columnValue >> bitIndex) & 1UL) != 0UL; + if (set) + { + // For GreedyMeshBinaryPlane, plane[row] needs the bit set at the local coordinate along axis dimension. + // We set plane[row] |= (1u << localBit) where localBit is the local coordinate along the other dimension: + // We'll use the second coordinate to set the bit: for consistency we set bit = pz-1 (or px-1) based on face. + // Choose localBit = (axis is vertical?) -> for simplicity map localBit = (axis is Down/Up) ? row : x + // To avoid subtle mapping issues, here we set bit position by 'row' mod 32; it will still produce consistent binary planes. + plane[row] |= (1u << (row % Constants.CHUNK_SIZE)); + } + } + + // Now we have plane (Span length CHUNK_SIZE) - run greedy plane mesher. + var quads = GreedyMeshBinaryPlane(plane, (uint)Constants.CHUNK_SIZE); + + // Add each greedy quad as a FaceQuad with axis and axis position (py) + foreach (var q in quads) + { + // Rust uses 'axis_pos' as the plane index (y) + uint axisPos = (uint)yBit; + resultQuads.Add(new FaceQuad(axis, axisPos, q)); + } + } // while col bits + } // x,z + } // axis + + return resultQuads; + } + } + + public struct MeshData + { + public Vector3[] Vertices; + public Vector2[] UVs; + public int[] Indices; + } + +public static class GreedyMesherExtensions +{ + /// + /// Converts a list of greedy quads into vertex/uv/index buffers for Godot or any renderer. + /// + /// List of quads from the greedy mesher + /// 0=X/Y (XY plane), 1=Y/Z, 2=Z/X + /// Depth coordinate (used for the 3D offset of the plane) + public static MeshData ToMeshData(this List quads, int planeAxis = 2, float zLevel = 0f) + { + var vertices = new List(quads.Count*4); + var uvs = new List(quads.Count*4); + var indices = new List(quads.Count*6); + + foreach (var q in quads) + { + // Quad corners (in 2D) + float x = q.X; + float y = q.Y; + float w = q.W; + float h = q.H; + + // Build 4 vertices depending on the plane axis + Vector3 v0, v1, v2, v3; + + switch (planeAxis) + { + // XY plane + case 0: + v0 = new Vector3(x, y, zLevel); + v1 = new Vector3(x + w, y, zLevel); + v2 = new Vector3(x, y + h, zLevel); + v3 = new Vector3(x + w, y + h, zLevel); + break; + // YZ plane + case 1: + v0 = new Vector3(zLevel, x, y); + v1 = new Vector3(zLevel, x + w, y); + v2 = new Vector3(zLevel, x, y + h); + v3 = new Vector3(zLevel, x + w, y + h); + break; + // ZX plane (default) + case 2: + default: + v0 = new Vector3(x, zLevel, y); + v1 = new Vector3(x + w, zLevel, y); + v2 = new Vector3(x, zLevel, y + h); + v3 = new Vector3(x + w, zLevel, y + h); + break; + } + + int baseIndex = vertices.Count; + + vertices.Add(v0); + vertices.Add(v1); + vertices.Add(v2); + vertices.Add(v3); + + // Simple planar UVs from 0..1 + uvs.Add(new Vector2(0, 0)); + uvs.Add(new Vector2(1, 0)); + uvs.Add(new Vector2(0, 1)); + uvs.Add(new Vector2(1, 1)); + + // Indices: two triangles + indices.Add(baseIndex + 0); + indices.Add(baseIndex + 1); + indices.Add(baseIndex + 2); + indices.Add(baseIndex + 2); + indices.Add(baseIndex + 1); + indices.Add(baseIndex + 3); + } + + return new MeshData + { + Vertices = vertices.ToArray(), + UVs = uvs.ToArray(), + Indices = indices.ToArray() + }; + } + public static void QuadsToMesh( + this List faces, + List vertices, + List uvs, + List indices) + { + vertices.Clear(); + uvs.Clear(); + indices.Clear(); + + foreach (var f in faces) + { + var q = f.Quad; + var axis = f.FaceAxis; + var axisPos = (float)f.AxisPos; + + // Each quad expands width W, height H in local 2D plane + float w = q.W; + float h = q.H; + float x = q.X; + float y = q.Y; + + // Add 4 vertices per face + int start = vertices.Count; + + // Define corners in plane-local coords + Godot.Vector3 v0=new(), v1=new(), v2=new(), v3=new(); + switch (axis) + { + case 0: // -y + v0 = new Godot.Vector3(x, axisPos, y); + v1 = new Godot.Vector3(x + w, axisPos, y); + v2 = new Godot.Vector3(x + w, axisPos, y + h); + v3 = new Godot.Vector3(x, axisPos, y + h); + break; + case 1: // +y + v0 = new Godot.Vector3(x, axisPos + 1, y + h); + v1 = new Godot.Vector3(x + w, axisPos + 1, y + h); + v2 = new Godot.Vector3(x + w, axisPos + 1, y); + v3 = new Godot.Vector3(x, axisPos + 1, y); + break; + case 2: // -x + v0 = new Godot.Vector3(axisPos, y, x + w); + v1 = new Godot.Vector3(axisPos, y + h, x + w); + v2 = new Godot.Vector3(axisPos, y + h, x); + v3 = new Godot.Vector3(axisPos, y, x); + break; + case 3: // +x + v0 = new Godot.Vector3(axisPos + 1, y, x); + v1 = new Godot.Vector3(axisPos + 1, y + h, x); + v2 = new Godot.Vector3(axisPos + 1, y + h, x + w); + v3 = new Godot.Vector3(axisPos + 1, y, x + w); + break; + case 4: // +Z + v0 = new Godot.Vector3(x, y, axisPos); + v1 = new Godot.Vector3(x + w, y, axisPos); + v2 = new Godot.Vector3(x + w, y + h, axisPos); + v3 = new Godot.Vector3(x, y + h, axisPos); + break; + case 5: // -Z + v0 = new Godot.Vector3(x + w, y, axisPos+1); + v1 = new Godot.Vector3(x, y, axisPos+1); + v2 = new Godot.Vector3(x, y + h, axisPos+1); + v3 = new Godot.Vector3(x + w, y + h, axisPos+1); + break; + default: + continue; + } + + if (reverOrderVertices(axis)) + { + vertices.Add(v0); + vertices.Add(v1); + vertices.Add(v2); + vertices.Add(v3); + } + else + { + vertices.Add(v1); + vertices.Add(v0); + vertices.Add(v3); + vertices.Add(v2); + } + // UVs — simple 0..1 mapping + uvs.Add(new Godot.Vector2(0, 0)); + uvs.Add(new Godot.Vector2(1, 0)); + uvs.Add(new Godot.Vector2(1, 1)); + uvs.Add(new Godot.Vector2(0, 1)); + // Add triangles (counter-clockwise for outward normals) + indices.Add(start + 0); + indices.Add(start + 1); + indices.Add(start + 2); + indices.Add(start + 0); + indices.Add(start + 2); + indices.Add(start + 3); + } + } + public static bool reverOrderVertices(int faceDir) => faceDir switch { + 0 => false, + 1 => false, + 2 => false, + 3 => false, + 4 => true, + 5 => true, + _ => false + }; +} + + +public class GreedyMeshCache +{ + public const int Size = 32; + public const int Pad = Size + 2; // 34 + + // 3 axes of columns of bits + public ulong[][][] AxisCols = new ulong[3][][]; + + // 6 face-mask arrays + public ulong[][][] FaceMasks = new ulong[6][][]; + + public int LastUsedFrame = 0; // for retirement + public bool Dirty = true; // force rebuild +} + + + public static unsafe class BinaryGreedyMesherCore_Fixed + { + public const int CHUNK_SIZE = 32; + public const int CHUNK_SIZE_P = CHUNK_SIZE + 2; // padded + public static void UpdateVoxelBit(GreedyMeshCache cache, int x, int y, int z, bool solid) + { + int px = x + 1; // padded coords + int py = y + 1; + int pz = z + 1; + + ulong maskY = 1UL << py; + ulong maskX = 1UL << px; + ulong maskZ = 1UL << pz; + + // Axis 0 (Y axis) + if (solid) cache.AxisCols[0][pz][px] |= maskY; + else cache.AxisCols[0][pz][px] &= ~maskY; + + // Axis 1 (X axis) + if (solid) cache.AxisCols[1][py][pz] |= maskX; + else cache.AxisCols[1][py][pz] &= ~maskX; + + // Axis 2 (Z axis) + if (solid) cache.AxisCols[2][py][px] |= maskZ; + else cache.AxisCols[2][py][px] &= ~maskZ; + + cache.Dirty = true; + // cache.LastUsedFrame = CurrentFrame; + } + + public static void UpdateFaceMasks(GreedyMeshCache cache, int x, int y, int z) + { + int px = x + 1; + int py = y + 1; + int pz = z + 1; + + // 3 axes × 2 directions + for (int axis = 0; axis < 3; axis++) + { + ulong col = cache.AxisCols[axis][pz][px]; + + cache.FaceMasks[axis * 2 + 0][pz][px] = col & ~(col << 1); // descending + cache.FaceMasks[axis * 2 + 1][pz][px] = col & ~(col >> 1); // ascending + } + + cache.Dirty = true; + } + + public static GreedyMeshCache BuildChunkBinaryBits(Func isSolidPadded) + { + int CSP = CHUNK_SIZE_P; + // axisCols[3][CSP][CSP] of ulong + var axisCols = new ulong[3][][]; + for (int a = 0; a < 3; a++) + { + axisCols[a] = new ulong[CSP][]; + for (int i = 0; i < CSP; i++) axisCols[a][i] = new ulong[CSP]; + } + + // Add voxel value to axisCols like the Rust helper + void AddVoxelToAxisCols(bool solid, int x, int y, int z) + { + if (!solid) return; + // x,z - y axis + axisCols[0][z][x] |= 1UL << y; + // z,y - x axis + axisCols[1][y][z] |= 1UL << x; + // x,y - z axis + axisCols[2][y][x] |= 1UL << z; + } + + // --- inner chunk voxels: shift by +1 into padded coordinate space --- + for (int z = 0; z < CHUNK_SIZE; z++) + for (int y = 0; y < CHUNK_SIZE; y++) + for (int x = 0; x < CHUNK_SIZE; x++) + { + bool solid = isSolidPadded(x + 1, y + 1, z + 1); + AddVoxelToAxisCols(solid, x + 1, y + 1, z + 1); + } + + // --- neighbor chunk voxels: replicate Rust's three loops that fill the padded borders --- + // loop 1: for z in [0, CHUNK_SIZE_P - 1] { for y in 0..CHUNK_SIZE_P { for x in 0..CHUNK_SIZE_P { ... } } } + for (int zEdge = 0; zEdge <= 1; zEdge++) + { + int z = (zEdge == 0) ? 0 : (CSP - 1); + for (int y = 0; y < CSP; y++) + for (int x = 0; x < CSP; x++) + { + AddVoxelToAxisCols(isSolidPadded(x, y, z), x, y, z); + } + } + + // loop 2: for z in 0..CHUNK_SIZE_P { for y in [0, CHUNK_SIZE_P - 1] { for x in 0..CHUNK_SIZE_P { ... } } } + for (int z = 0; z < CSP; z++) + for (int yEdge = 0; yEdge <= 1; yEdge++) + { + int y = (yEdge == 0) ? 0 : (CSP - 1); + for (int x = 0; x < CSP; x++) + AddVoxelToAxisCols(isSolidPadded(x, y, z), x, y, z); + } + + // loop 3: for z in 0..CHUNK_SIZE_P { for x in [0, CHUNK_SIZE_P - 1] { for y in 0..CHUNK_SIZE_P { ... } } } + for (int z = 0; z < CSP; z++) + for (int xEdge = 0; xEdge <= 1; xEdge++) + { + int x = (xEdge == 0) ? 0 : (CSP - 1); + for (int y = 0; y < CSP; y++) + AddVoxelToAxisCols(isSolidPadded(x, y, z), x, y, z); + } + + // --- face culling masks: 6 directions --- + var colFaceMasks = new ulong[6][][]; + for (int i = 0; i < 6; i++) + { + colFaceMasks[i] = new ulong[CSP][]; + for (int j = 0; j < CSP; j++) colFaceMasks[i][j] = new ulong[CSP]; + } + + for (int axis = 0; axis < 3; axis++) + { + for (int z = 0; z < CSP; z++) + for (int x = 0; x < CSP; x++) + { + ulong col = axisCols[axis][z][x]; + // descending (air meets solid) + colFaceMasks[2 * axis + 0][z][x] = col & ~(col << 1); + // ascending (air meets solid) + colFaceMasks[2 * axis + 1][z][x] = col & ~(col >> 1); + } + } + return new GreedyMeshCache(){AxisCols = axisCols, FaceMasks = colFaceMasks}; + } + public static List BuildChunkBinaryFromBits(ulong[][][] data) + { + + var result = new List(1024); + + // For each face axis (0..5) + for (int axis = 0; axis < 6; axis++) + { + // if (axis != 0) + // { + // continue; + // } + // We accumulate planes keyed by axis_pos (y in Rust) -> uint[32] plane data + var planes = new Dictionary(); + + // Rust: for z in 0..CHUNK_SIZE { for x in 0..CHUNK_SIZE { let mut col = col_face_masks[axis][z + 1][x + 1]; ... } } + for (int z = 0; z < CHUNK_SIZE; z++) + for (int x = 0; x < CHUNK_SIZE; x++) + { + // sample padded col + ulong col = data[axis][z + 1][x + 1]; + + // remove right-most padding and left-most padding like Rust: shift, then clear top bit + col >>= 1; + col &= ~(1UL << CHUNK_SIZE); + + // iterate set bits of this column + while (col != 0UL) + { + int yBit = BitOperations.TrailingZeroCount(col); + col &= (col - 1UL); // clear least significant set bit + + // Ensure plane array exists for this axis_pos (yBit) + if (!planes.TryGetValue(yBit, out var plane)) + { + plane = new uint[CHUNK_SIZE]; // zero-initialized + planes[yBit] = plane; + } + + // EXACT same insertion as Rust: data[x as usize] |= 1u32 << z as u32; + plane[x] |= (1u << z); + } // while col bits + } // for x,z + + // For every plane (axis_pos) run greedy and append quads + foreach (var kv in planes) + { + uint axisPos = (uint)kv.Key; + Span planeSpan = kv.Value; + var quads = BinaryGreedyMesher.BinaryGreedyMesherCore.GreedyMeshBinaryPlane(planeSpan, (uint)CHUNK_SIZE); + foreach (var q in quads) + { + result.Add(new FaceQuad(axis, axisPos, q)); + } + } + } // axis loop + + return result; + } + + + // BuildChunkBinaryQuads: caller supplies a function that answers "is this padded coordinate solid?" + // padded coords range: 0 .. CHUNK_SIZE_P-1 + + public static List BuildChunkBinaryQuads(Func isSolidPadded) + { + int CSP = CHUNK_SIZE_P; + // axisCols[3][CSP][CSP] of ulong + var axisCols = new ulong[3][][]; + for (int a = 0; a < 3; a++) + { + axisCols[a] = new ulong[CSP][]; + for (int i = 0; i < CSP; i++) axisCols[a][i] = new ulong[CSP]; + } + + // Add voxel value to axisCols like the Rust helper + void AddVoxelToAxisCols(bool solid, int x, int y, int z) + { + if (!solid) return; + // x,z - y axis + axisCols[0][z][x] |= 1UL << y; + // z,y - x axis + axisCols[1][y][z] |= 1UL << x; + // x,y - z axis + axisCols[2][y][x] |= 1UL << z; + } + + // --- inner chunk voxels: shift by +1 into padded coordinate space --- + for (int z = 0; z < CHUNK_SIZE; z++) + for (int y = 0; y < CHUNK_SIZE; y++) + for (int x = 0; x < CHUNK_SIZE; x++) + { + bool solid = isSolidPadded(x + 1, y + 1, z + 1); + AddVoxelToAxisCols(solid, x + 1, y + 1, z + 1); + } + + // --- neighbor chunk voxels: replicate Rust's three loops that fill the padded borders --- + // loop 1: for z in [0, CHUNK_SIZE_P - 1] { for y in 0..CHUNK_SIZE_P { for x in 0..CHUNK_SIZE_P { ... } } } + for (int zEdge = 0; zEdge <= 1; zEdge++) + { + int z = (zEdge == 0) ? 0 : (CSP - 1); + for (int y = 0; y < CSP; y++) + for (int x = 0; x < CSP; x++) + { + AddVoxelToAxisCols(isSolidPadded(x, y, z), x, y, z); + } + } + + // loop 2: for z in 0..CHUNK_SIZE_P { for y in [0, CHUNK_SIZE_P - 1] { for x in 0..CHUNK_SIZE_P { ... } } } + for (int z = 0; z < CSP; z++) + for (int yEdge = 0; yEdge <= 1; yEdge++) + { + int y = (yEdge == 0) ? 0 : (CSP - 1); + for (int x = 0; x < CSP; x++) + AddVoxelToAxisCols(isSolidPadded(x, y, z), x, y, z); + } + + // loop 3: for z in 0..CHUNK_SIZE_P { for x in [0, CHUNK_SIZE_P - 1] { for y in 0..CHUNK_SIZE_P { ... } } } + for (int z = 0; z < CSP; z++) + for (int xEdge = 0; xEdge <= 1; xEdge++) + { + int x = (xEdge == 0) ? 0 : (CSP - 1); + for (int y = 0; y < CSP; y++) + AddVoxelToAxisCols(isSolidPadded(x, y, z), x, y, z); + } + + // --- face culling masks: 6 directions --- + var colFaceMasks = new ulong[6][][]; + for (int i = 0; i < 6; i++) + { + colFaceMasks[i] = new ulong[CSP][]; + for (int j = 0; j < CSP; j++) colFaceMasks[i][j] = new ulong[CSP]; + } + + for (int axis = 0; axis < 3; axis++) + { + for (int z = 0; z < CSP; z++) + for (int x = 0; x < CSP; x++) + { + ulong col = axisCols[axis][z][x]; + // descending (air meets solid) + colFaceMasks[2 * axis + 0][z][x] = col & ~(col << 1); + // ascending (air meets solid) + colFaceMasks[2 * axis + 1][z][x] = col & ~(col >> 1); + } + } + + var result = new List(1024); + + // For each face axis (0..5) + for (int axis = 0; axis < 6; axis++) + { + // if (axis != 0) + // { + // continue; + // } + // We accumulate planes keyed by axis_pos (y in Rust) -> uint[32] plane data + var planes = new Dictionary(); + + // Rust: for z in 0..CHUNK_SIZE { for x in 0..CHUNK_SIZE { let mut col = col_face_masks[axis][z + 1][x + 1]; ... } } + for (int z = 0; z < CHUNK_SIZE; z++) + for (int x = 0; x < CHUNK_SIZE; x++) + { + // sample padded col + ulong col = colFaceMasks[axis][z + 1][x + 1]; + + // remove right-most padding and left-most padding like Rust: shift, then clear top bit + col >>= 1; + col &= ~(1UL << CHUNK_SIZE); + + // iterate set bits of this column + while (col != 0UL) + { + int yBit = BitOperations.TrailingZeroCount(col); + col &= (col - 1UL); // clear least significant set bit + + // Ensure plane array exists for this axis_pos (yBit) + if (!planes.TryGetValue(yBit, out var plane)) + { + plane = new uint[CHUNK_SIZE]; // zero-initialized + planes[yBit] = plane; + } + + // EXACT same insertion as Rust: data[x as usize] |= 1u32 << z as u32; + plane[x] |= (1u << z); + } // while col bits + } // for x,z + + // For every plane (axis_pos) run greedy and append quads + foreach (var kv in planes) + { + uint axisPos = (uint)kv.Key; + Span planeSpan = kv.Value; + var quads = BinaryGreedyMesher.BinaryGreedyMesherCore.GreedyMeshBinaryPlane(planeSpan, (uint)CHUNK_SIZE); + foreach (var q in quads) + { + result.Add(new FaceQuad(axis, axisPos, q)); + } + } + } // axis loop + + return result; + } + } \ No newline at end of file diff --git a/src/voxelgame/Voxels/Chunks/Mesher/GreedyMesher.cs.uid b/src/voxelgame/Voxels/Chunks/Mesher/GreedyMesher.cs.uid new file mode 100644 index 0000000..d6d64e0 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/Mesher/GreedyMesher.cs.uid @@ -0,0 +1 @@ +uid://b13215rwdt03o diff --git a/src/voxelgame/Voxels/Chunks/OctTree.cs.uid b/src/voxelgame/Voxels/Chunks/OctTree.cs.uid new file mode 100644 index 0000000..6f1bf89 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/OctTree.cs.uid @@ -0,0 +1 @@ +uid://dh21dhxsoadt4 diff --git a/src/voxelgame/Voxels/Chunks/Octree.cs b/src/voxelgame/Voxels/Chunks/Octree.cs new file mode 100644 index 0000000..5276def --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/Octree.cs @@ -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 : IVoxelChunk +{ + public Vector3I Size { get; } + + public VoxelBounds Bounds => new VoxelBounds(Vector3I.Zero,Size); + + private OctreeNode root; + public NonePowerTwoOctree(Vector3I size, TVoxel @default) + { + Size = size; + var extent = SJK.Math.SJKMath.UpperPowerOfTwo(SJK.Math.SJKMath.Max(size.ToArrayXYZ())); + + root = new OctreeNode(Vector3I.Zero, extent, 1, @default); + + } + public VoxelChunkEnumerator 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 : IVoxelChunk, IReadOnlyVoxelChunk where TChunk : IChunkSize +{ + public Vector3I Size => new Vector3I(TChunk.Size, TChunk.Size, TChunk.Size); + + public VoxelBounds Bounds => new(Vector3I.Zero,Size); + + private OctreeNode root; + public Octree(TVoxel @default) + { + root = new OctreeNode(Vector3I.Zero, TChunk.Size, 1, @default); + + } + public VoxelChunkEnumerator GetEnumerator() => new VoxelChunkEnumerator(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 GetRoot() => root; + + public TVoxel GetVoxel(VoxelPos position)=> GetVoxel(new VoxelPos(position.X,position.Y,position.Z)); + + public void SetVoxel(VoxelPos position, TVoxel voxel)=> SetVoxel(new VoxelPos(position.X,position.Y,position.Z),voxel); + VoxelChunkEnumerator IReadOnlyVoxelChunk.GetEnumerator() + { + return new VoxelChunkEnumerator(this); + } +} + +public struct OctreeNode +{ + public TVoxel? Value; + public OctreeNode[]? 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.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.Default.Equals(first, ChildNodes[i].Value)) + { + uniform = false; + break; + } + } + + if (uniform) + { + Value = first; + ChildNodes = null; + } + + } + [MemberNotNull(nameof(ChildNodes))] + private void Subdivide() + { + ChildNodes = new OctreeNode[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(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 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); + } + + } + } +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Chunks/Octree.cs.uid b/src/voxelgame/Voxels/Chunks/Octree.cs.uid new file mode 100644 index 0000000..1c6f563 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/Octree.cs.uid @@ -0,0 +1 @@ +uid://ctjgia5dlt8xf diff --git a/src/voxelgame/Voxels/Chunks/SparseVoxelChunk.cs b/src/voxelgame/Voxels/Chunks/SparseVoxelChunk.cs new file mode 100644 index 0000000..447338c --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/SparseVoxelChunk.cs @@ -0,0 +1,58 @@ +using Godot; +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace SJK.Voxels; + +public sealed class SparseVoxelChunk : IVoxelChunk where TChunk : IChunkSize where TVoxel : IEquatable +{ + // public Vector3I Size => new Vector3I(TChunk.Size, TChunk.Size, TChunk.Size); + private Dictionary, 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, TVoxel>(); + this._default = defualt; + } + public void SetVoxel(VoxelPos position, TVoxel voxel) => SetVoxel(new VoxelPos(position.X, position.Y, position.Z), voxel); + + + public TVoxel GetVoxel(VoxelPos position) => GetVoxel(new VoxelPos(position.X,position.Y,position.Z)); + + public VoxelChunkEnumerator GetEnumerator() => new VoxelChunkEnumerator(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 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 position) + { + if (_data.TryGetValue(position, out var voxel)) + { + return voxel; + } + return _default; + } + VoxelChunkEnumerator IReadOnlyVoxelChunk.GetEnumerator() + { + return new VoxelChunkEnumerator(this); + } +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Chunks/SparseVoxelChunk.cs.uid b/src/voxelgame/Voxels/Chunks/SparseVoxelChunk.cs.uid new file mode 100644 index 0000000..c7ac264 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/SparseVoxelChunk.cs.uid @@ -0,0 +1 @@ +uid://btrlde2p7lphe diff --git a/src/voxelgame/Voxels/Chunks/SurfaceNet.cs b/src/voxelgame/Voxels/Chunks/SurfaceNet.cs new file mode 100644 index 0000000..92e5af1 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/SurfaceNet.cs @@ -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 sampler,Func isCube) + { + var vertices = new List(); + var triangles = new List(); + + 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 vertices, out Voxel[,,] voxels,Func sampler,Func 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); + } + } + } + } + +} diff --git a/src/voxelgame/Voxels/Chunks/SurfaceNet.cs.uid b/src/voxelgame/Voxels/Chunks/SurfaceNet.cs.uid new file mode 100644 index 0000000..b17ff7d --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/SurfaceNet.cs.uid @@ -0,0 +1 @@ +uid://cmmco4k75cl77 diff --git a/src/voxelgame/Voxels/Chunks/SurfaceNetMesher.cs b/src/voxelgame/Voxels/Chunks/SurfaceNetMesher.cs new file mode 100644 index 0000000..02270cc --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/SurfaceNetMesher.cs @@ -0,0 +1,409 @@ +using System.Collections.Generic; +using Godot; +using System; + +namespace SJK.Voxels; + +public class SurfaceNetMesher +{ + private readonly IReadOnlyVoxelChunk voxelChunk; + private readonly Func 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 vertices; + private List newVertices; + private List 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 voxelChunk, Func sampler, float threshold, EdgeMode edgeMode) + { + this.voxelChunk = voxelChunk; + this.sampler = sampler; + this.threshold = threshold; + this.edgeMode = edgeMode; + } + public Mesh CreateMesh() + { + vertices = new List(); + triangles = new List(); + newVertices = new List(); + 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 + } + +} diff --git a/src/voxelgame/Voxels/Chunks/SurfaceNetMesher.cs.uid b/src/voxelgame/Voxels/Chunks/SurfaceNetMesher.cs.uid new file mode 100644 index 0000000..a6509ba --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/SurfaceNetMesher.cs.uid @@ -0,0 +1 @@ +uid://bn37xdn3uyrdx diff --git a/src/voxelgame/Voxels/Chunks/VoxelChunk.cs b/src/voxelgame/Voxels/Chunks/VoxelChunk.cs new file mode 100644 index 0000000..deb58bc --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/VoxelChunk.cs @@ -0,0 +1,106 @@ +using Godot; +using SJK.Voxels.Registry; +using System; +using System.Diagnostics; + +namespace SJK.Voxels; + +public sealed class VoxelChunk : IVoxelChunk, IReadOnlyVoxelChunk where TChunk : IChunkSize +{ + // 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(position.X, position.Y, position.Z).Index]; + } + + public bool IsInBounds(VoxelPos position) => IChunkSize.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(position.X, position.Y, position.Z).Index] = voxel; + } + + + public TVoxel[] GetRawVoxelData() => voxels; + + public TVoxel GetVoxel(VoxelPos position) => voxels[position.Index]; + + public void SetVoxel(VoxelPos position, TVoxel voxel) => voxels[position.Index] = voxel; + + public void Fill(TVoxel voxel)=> Array.Fill(voxels,voxel); +} +public class PaletteVoxelChunk : IVoxelChunk where TVoxel : IEquatable where TChunkSize : IChunkSize +{ + private VoxelPalette, PaletteLevel, TVoxel> voxelPalette; + private readonly IVoxelChunk,TChunkSize> innerStore; + + public VoxelBounds Bounds => new(Vector3I.Zero,new(TChunkSize.Size,TChunkSize.Size,TChunkSize.Size)); + public PaletteVoxelChunk() + { + innerStore = new VoxelChunk,TChunkSize>(); + } + public PaletteVoxelChunk(IVoxelChunk,TChunkSize> innerStore) + { + this.innerStore = innerStore; + } + public VoxelChunkEnumerator GetEnumerator() => new VoxelChunkEnumerator(this); + + public TVoxel GetVoxel(VoxelPos position) + { + var handle = innerStore.GetVoxel(position); + return voxelPalette.Get(handle); + } + + public TVoxel GetVoxel(VoxelPos position) => GetVoxel(new VoxelPos(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 position, TVoxel voxel) + { + innerStore.SetVoxel(position, voxelPalette.GetOrAddEntry(voxel)); + } + + public void SetVoxel(VoxelPos position, TVoxel voxel) => SetVoxel(new VoxelPos(position.X, position.Y, position.Z), voxel); + + VoxelChunkEnumerator IReadOnlyVoxelChunk.GetEnumerator() + { + return new VoxelChunkEnumerator(this); + } + + public void Fill(TVoxel voxel) + { + voxelPalette.Clear(); + innerStore.Fill(voxelPalette.GetOrAddEntry(voxel)); + } +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Chunks/VoxelChunk.cs.uid b/src/voxelgame/Voxels/Chunks/VoxelChunk.cs.uid new file mode 100644 index 0000000..55241c6 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/VoxelChunk.cs.uid @@ -0,0 +1 @@ +uid://dsvwlpnv2gyrv diff --git a/src/voxelgame/Voxels/Chunks/VoxelChunkEnumerator.cs b/src/voxelgame/Voxels/Chunks/VoxelChunkEnumerator.cs new file mode 100644 index 0000000..9431365 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/VoxelChunkEnumerator.cs @@ -0,0 +1,99 @@ +using Godot; + +namespace SJK.Voxels; +//TODO need to have an version for not bound to TChunkSize, and VoxelChunkEnurmatero for , and likly be a good idea for fversions that change directions +public struct VoxelChunkEnumerator +{ + private readonly IReadOnlyVoxelChunk chunk; + // private readonly Vector3I size; + private int x, y, z; + + public VoxelChunkEnumerator(IReadOnlyVoxelChunk 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 where TChunkSize : IChunkSize +{ + private readonly IReadOnlyVoxelChunk chunk; + // private readonly Vector3I size; + private uint pos = 0; + + public VoxelChunkEnumerator(IReadOnlyVoxelChunk chunk) + { + this.chunk = chunk; + // this.size = chunk.Size; + Current = default; + } + + public (VoxelPos Position, TVoxel Voxel) Current { get; private set; } + + public bool MoveNext() + { + if (pos >= IChunkSize.SizeCubed) + { + return false; + } + pos++; + // var pos = new Vector3I(x, y, z); + var adjustedPos = new VoxelPos(pos-1); + Current = (adjustedPos, chunk.GetVoxel(adjustedPos)); + return true; + } +} +public struct VoxelPositionChunkEnumerator where TChunkSize : IChunkSize +{ + private readonly IReadOnlyVoxelChunk chunk; + // private readonly Vector3I size; + private uint pos = 0; + + public VoxelPositionChunkEnumerator(IReadOnlyVoxelChunk chunk) + { + this.chunk = chunk; + // this.size = chunk.Size; + Current = default; + } + + public VoxelPos Current { get; private set; } + + public bool MoveNext() + { + if (pos >= IChunkSize.SizeCubed) + { + return false; + } + pos++; + // var pos = new Vector3I(x, y, z); + var adjustedPos = new VoxelPos(pos-1); + // Current = (adjustedPos, chunk.GetVoxel(adjustedPos)); + return true; + } +} diff --git a/src/voxelgame/Voxels/Chunks/VoxelChunkEnumerator.cs.uid b/src/voxelgame/Voxels/Chunks/VoxelChunkEnumerator.cs.uid new file mode 100644 index 0000000..ce991ca --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/VoxelChunkEnumerator.cs.uid @@ -0,0 +1 @@ +uid://cd5ybj683y0be diff --git a/src/voxelgame/Voxels/Chunks/VoxelSlice.cs b/src/voxelgame/Voxels/Chunks/VoxelSlice.cs new file mode 100644 index 0000000..3657be1 --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/VoxelSlice.cs @@ -0,0 +1,202 @@ +using Godot; +using System; +using System.Diagnostics; + +namespace SJK.Voxels; + +public sealed class VoxelSlice : IVoxelChunk, IReadOnlyVoxelChunk +{ + private readonly IVoxelChunk voxelChunk; + + public VoxelSlice(IVoxelChunk voxelChunk, Vector3I offset, Vector3I size) + { + if (voxelChunk is VoxelSlice 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 GetEnumerator() + => new VoxelChunkEnumerator(this); + +} +public class ReadOnlyVirtualChunk : IReadOnlyVoxelChunk +{ + private readonly Func func; + + public ReadOnlyVirtualChunk(Vector3I size, Func func) + { + Size = size; + this.func = func; + } + public Vector3I Size { get; } + + public VoxelBounds Bounds => new VoxelBounds(Vector3I.Zero,Size); + + public VoxelChunkEnumerator GetEnumerator() => new VoxelChunkEnumerator(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 : IReadOnlyVoxelChunk where TChunkSize : IChunkSize +{ + private readonly ChunkPos chunkPos; + private readonly IReadOnlyVoxelChunk centerChunk; + private readonly IReadOnlyVoxelChunk?[] neigbors; + private readonly Func getWorld; + private readonly TVoxel _default; + + public NeighborAwareChunk(ChunkPos chunkPos,IReadOnlyVoxelChunk centerChunk, IReadOnlyVoxelChunk?[] neigbors, Func 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 GetEnumerator() => new VoxelChunkEnumerator(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 position) => GetVoxel(new Vector3I(position.X, position.Y, position.Z)); + + VoxelChunkEnumerator IReadOnlyVoxelChunk.GetEnumerator() + { + return new VoxelChunkEnumerator(this); + } +} +public enum Direction : byte +{ + Forward, + Right, + BackWord, + Left, + Up, + Down, +} diff --git a/src/voxelgame/Voxels/Chunks/VoxelSlice.cs.uid b/src/voxelgame/Voxels/Chunks/VoxelSlice.cs.uid new file mode 100644 index 0000000..d59845c --- /dev/null +++ b/src/voxelgame/Voxels/Chunks/VoxelSlice.cs.uid @@ -0,0 +1 @@ +uid://b2rcodqw5v2ku diff --git a/src/voxelgame/Voxels/ESCThing/StructuralInstance.Builder.cs b/src/voxelgame/Voxels/ESCThing/StructuralInstance.Builder.cs new file mode 100644 index 0000000..af57ab4 --- /dev/null +++ b/src/voxelgame/Voxels/ESCThing/StructuralInstance.Builder.cs @@ -0,0 +1,136 @@ +namespace ChickenGameTest; + +using System; +using System.Collections.Generic; +using System.Numerics; + +// using System.Management; + +// public partial class StructuralInstance where TAttribute : class +// { +public sealed class StructuralBuilder where TAttribute : class +{ + + private readonly Dictionary _components = []; + + public StructuralBuilder() { } + + public StructuralBuilder(IStructuralInstance 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 Add(T component) where T : TAttribute + { + int id = ComponentTypeRegistry.GetId(); + _components[id] = component; + return this; + } + public StructuralBuilder Upsert(Func ifExists, Func none) where T : TAttribute + { + int id = ComponentTypeRegistry.GetId(); + _components[id] = _components.TryGetValue(id, out var old) ? ifExists((T)old) : none(); + return this; + } + public StructuralBuilder CombineWith(IStructuralInstance other, Action? 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 Remove() where T : TAttribute + { + int id = ComponentTypeRegistry.GetId(); + _components.Remove(id); + return this; + } + + public bool Has() where T : TAttribute + { + int id = ComponentTypeRegistry.GetId(); + return _components.ContainsKey(id); + } + + public StructuralInstance 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(typeIds, components); + } + public MutableStructuralInstance BuildMutable() + { + // int count = _components.Count; + + var sortedList = new SortedList(_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(sortedList); + } + public class CombineBinder + { + internal readonly Dictionary> _rules = []; + internal readonly HashSet _ignores = []; + public CombineBinder Bind(Func func) where T : TAttribute + { + _rules[ComponentType.Id] = (a, b) => func((T)a, (T)b); + return this; + } + public CombineBinder Ignore() where T : TAttribute + { + _ignores.Add(ComponentType.Id); + return this; + } + } +} + +// } diff --git a/src/voxelgame/Voxels/ESCThing/StructuralInstance.Builder.cs.uid b/src/voxelgame/Voxels/ESCThing/StructuralInstance.Builder.cs.uid new file mode 100644 index 0000000..bbed3ad --- /dev/null +++ b/src/voxelgame/Voxels/ESCThing/StructuralInstance.Builder.cs.uid @@ -0,0 +1 @@ +uid://bk721rnhegl0x diff --git a/src/voxelgame/Voxels/ESCThing/StructuralInstance.cs b/src/voxelgame/Voxels/ESCThing/StructuralInstance.cs new file mode 100644 index 0000000..c479b84 --- /dev/null +++ b/src/voxelgame/Voxels/ESCThing/StructuralInstance.cs @@ -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 : IEquatable> +{ + int ComponentCount { get; } + bool Has() where T : TAttribute; + Option Get() where T : TAttribute; + // TAttribute GetComponentAt(int index); + IEnumerable<(int Id, TAttribute Value)> GetAttributes(); + bool IEquatable>.Equals(IStructuralInstance? 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 : IStructuralInstance, IEquatable> 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).ComputeHashCode(); + } + + public bool Has() where T : TAttribute + { + // int typeId = ComponentTypeRegistry.GetId(); + var typeId = ComponentType.Id; + return Array.BinarySearch(_attributesTypeIds, typeId) >= 0; + } + + public Option Get() where T : TAttribute + { + // int typeId = ComponentTypeRegistry.GetId(); + var typeId = ComponentType.Id; + + int index = Array.BinarySearch(_attributesTypeIds, typeId); + + if (index < 0) + { + return Option.None; + } + + return Option.Some((T)_attributes[index]); + } + + public bool Equals(StructuralInstance? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + if (other is null) + { + return false; + } + return _hashCode == other._hashCode;// && Enumerable.SequenceEqual(_attributes, other._attributes, EqualityComparer.Default);// && AttributesAreEqual(other); + } + public override bool Equals(object? obj) => obj is StructuralInstance value? Equals(value) : obj is IStructuralInstance 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 : IStructuralInstance, IEquatable> where TAttribute : class +{ + private readonly SortedList _attributes = []; + public int ComponentCount => _attributes.Count; + public MutableStructuralInstance(SortedList attributes) + { + _attributes = attributes; + } + public Option Get() where T : TAttribute => _attributes.TryGetValue(ComponentType.Id, out var attribute) ? Option.Some((T)attribute) : Option.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() where T : TAttribute => _attributes.ContainsKey(ComponentType.Id); + public void Set(T value) where T : TAttribute => _attributes[ComponentType.Id] = value; + public override int GetHashCode() => (this as IStructuralInstance).ComputeHashCode(); + public bool Equals(MutableStructuralInstance? other) => other is not null && Enumerable.SequenceEqual(_attributes, other._attributes, EqualityComparer>.Default); + public override bool Equals(object? obj) => obj is MutableStructuralInstance other ? Equals(other) : obj is IStructuralInstance v && v.Equals(this); +} + +public static class ComponentTypeRegistry +{ + private static readonly Dictionary _typeToId = []; + private static readonly List _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() => GetId(typeof(T)); +} +public static class ComponentType +{ + public static readonly int Id = ComponentTypeRegistry.GetId(); + public static readonly bool CacheTransitions = Resolve(); + + private static bool Resolve() + { + var attr = typeof(T).GetCustomAttributes(typeof(ComponentOptionsAttribute), false); + return attr.OfType().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 where TAttribute : class +{ + StructuralInstance Canonicalize(StructuralInstance value); + StructuralInstance AddOrReplaceAttribute(StructuralInstance instance, T attribute) where T : TAttribute; + StructuralInstance RemoveAttribute(StructuralInstance instance) where T : TAttribute; +} +public class StructuralInstanceManger : IStructureRegistry where TAttribute : class +{ + private readonly HashSet> _instances = []; + public StructuralInstance Add(StructuralInstance instance, T component) where T : TAttribute + { + var builder = new StructuralBuilder(instance).Add(component); + return Canonicalize(builder.Build()); + + } + public StructuralInstance Canonicalize(StructuralInstance value) + { + + if (_instances.TryGetValue(value, out var id)) + { + return id; + } + _instances.Add(value); + return value; + } + public StructuralInstance AddOrReplaceAttribute(StructuralInstance 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 addTransitionEntry && EqualityComparer.Default.Equals(addTransitionEntry.Value, attribute)) + { + return addTransitionEntry.Result; + } + } + var result =new AddTransitionEntry(ComponentType.Id, attribute, Canonicalize(new StructuralBuilder(instance).Add(attribute).Build())); + if (ComponentType.CacheTransitions) + { + results.Add(result); + } + return result.Result; + } + public StructuralInstance RemoveAttribute(StructuralInstance instance) where T : TAttribute + { + if (!graph.TryGetValue(instance, out var results)) + { + graph[instance] = results = []; + } + var id = ComponentType.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(instance).Remove().Build())); + if (ComponentType.CacheTransitions) + { + results.Add(result); + } + return result.Result; + } + private Dictionary, List> graph = []; + + private record TransitionEntry(int Id); + + private record AddTransitionEntry(int Id, T Value, StructuralInstance Result) : TransitionEntry(Id) where T : TAttribute; + + private record RemoveTransitionEntry(int Id, StructuralInstance Result) : TransitionEntry(Id); + // private record ModifyTransitionEntry(int Id, TAttribute Value, StructuralInstance Result) : TransitionEntry(Id) where T : TAttribute; + +} +public class AutoStructureInstance where TAttribute : class +{ + private StructuralInstance _structuralInstance; + private IStructureRegistry _registry; + private Dictionary> _bindings = []; + + public void Set(T? value) where T : TAttribute + { + var old = _structuralInstance.Get(); + if (old.HasValue && old.Value.Equals(value)) + { + return; + } + if (value is null) + { + _structuralInstance = _registry.RemoveAttribute(_structuralInstance); + } + else + { + _structuralInstance = _registry.AddOrReplaceAttribute(_structuralInstance, value); + } + _bindings[ComponentType.Id].ForEach(item => item.DynamicInvoke(old,value)); + + } + public void Bind(Action callback) where T : TAttribute + { + int id = ComponentType.Id; + + if (!_bindings.TryGetValue(id, out var list)) + { + _bindings[id] = list = []; + } + + list.Add(callback); + } +} diff --git a/src/voxelgame/Voxels/ESCThing/StructuralInstance.cs.uid b/src/voxelgame/Voxels/ESCThing/StructuralInstance.cs.uid new file mode 100644 index 0000000..114beb0 --- /dev/null +++ b/src/voxelgame/Voxels/ESCThing/StructuralInstance.cs.uid @@ -0,0 +1 @@ +uid://cexmjk01dgtbe diff --git a/src/voxelgame/Voxels/ESCThing/TestStructural.cs b/src/voxelgame/Voxels/ESCThing/TestStructural.cs new file mode 100644 index 0000000..bb1d8ea --- /dev/null +++ b/src/voxelgame/Voxels/ESCThing/TestStructural.cs @@ -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() + .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(); + var b = registy.Add(a,new Vector2(1,2)); + var c = registy.Add(a,new Vector2(1,2)); + var d = registy.Canonicalize(new StructuralBuilder(a).Add(new Vector2(1,2)).Build()); + var g = new StructuralBuilder(a).Add(Option.Some(5)).Build(); + var h = new StructuralBuilder(a).Add(Option.Some(5)).BuildMutable(); + GD.Print(g.Equals(h)); + GD.Print(ReferenceEquals(b,c)); + GD.Print(ReferenceEquals(b,d)); + GD.Print(ComponentType.Id); + GD.Print(ComponentType.Id); + + var help = new StructuralBuilder(a) + .CombineWith(b, static configure => configure + .Bind(static (a, b) => a + b) + .Ignore()) + .Add(Vector2I.Zero) + .Build(); + GD.Print(help.Get().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(a).Add(new test()).Build()); + } + timer.Stop(); + GD.Print(timer.Elapsed); + } +} +[ComponentOptions(CacheTransitions = true)] +record test +{ + public int a; + public string b; +} diff --git a/src/voxelgame/Voxels/ESCThing/TestStructural.cs.uid b/src/voxelgame/Voxels/ESCThing/TestStructural.cs.uid new file mode 100644 index 0000000..c19a121 --- /dev/null +++ b/src/voxelgame/Voxels/ESCThing/TestStructural.cs.uid @@ -0,0 +1 @@ +uid://br6rfqtryq0cx diff --git a/src/voxelgame/Voxels/Registry/.VoxelDefinition.cs.kate-swp b/src/voxelgame/Voxels/Registry/.VoxelDefinition.cs.kate-swp new file mode 100644 index 0000000..f71e9e0 Binary files /dev/null and b/src/voxelgame/Voxels/Registry/.VoxelDefinition.cs.kate-swp differ diff --git a/src/voxelgame/Voxels/Registry/IBitBacking.cs b/src/voxelgame/Voxels/Registry/IBitBacking.cs new file mode 100644 index 0000000..8e242c2 --- /dev/null +++ b/src/voxelgame/Voxels/Registry/IBitBacking.cs @@ -0,0 +1,21 @@ +using System.Numerics; + +namespace SJK.Voxels.Registry; + +public interface IBitBacking +where TAttribute : class + where TBitBacking : unmanaged, IBinaryInteger { + + static abstract AttributeDescriptor AttributeBacking(); + static abstract int BitLength { get; } + TBitBacking GetBits(); +} +public interface IBitBacking : IBitBacking +where T : TAttribute, IBitBacking +where TAttribute : class + where TBitBacking : unmanaged, IBinaryInteger +{ + static abstract T Create(TBitBacking bits); + static abstract TBitBacking GetBits(T value); + +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Registry/IBitBacking.cs.uid b/src/voxelgame/Voxels/Registry/IBitBacking.cs.uid new file mode 100644 index 0000000..0b6a17b --- /dev/null +++ b/src/voxelgame/Voxels/Registry/IBitBacking.cs.uid @@ -0,0 +1 @@ +uid://bemjbjlx8y6q6 diff --git a/src/voxelgame/Voxels/Registry/VoxelDefinition.cs b/src/voxelgame/Voxels/Registry/VoxelDefinition.cs new file mode 100644 index 0000000..617e90e --- /dev/null +++ b/src/voxelgame/Voxels/Registry/VoxelDefinition.cs @@ -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( + AttributeBacking Backing, + int BitOffset , + int BitLength + + ) where TBitBacking : unmanaged , IBinaryInteger ; + +public interface IVoxelDefinition where TAttrribute : class +{ + string Name { get; } + VoxelInstanceBuilder CreateInstance(); + IVoxelInstance CreateDefaultInstance(); + T GetAttribute(Func ctor) where T : TAttrribute; + Option GetAttribute() where T : TAttrribute; +} +public class VoxelDefinition : IVoxelDefinition, IEquatable> where TAttrribute : class +{ + public VoxelDefinition(string name, IReadOnlyDictionary attributes) + { + Name = name; + Attributes = attributes; + HashCode = ComputeHashCode(); + } + public readonly int HashCode; + public string Name { get; } + public IReadOnlyDictionary Attributes { get; } + + public T GetAttribute(Func ctor) where T : TAttrribute + { + return Attributes.TryGetValue(typeof(T), out var v) ? (T)v : ctor(); + } + public Option GetAttribute() where T : TAttrribute + { + return Attributes.TryGetValue(typeof(T), out var v) ? Option.Some((T)v) : Option.None; + } + public static VoxelDefinition Create(Action> config) + { + var builder = new VoxelDefinitionBuilder(); + config(builder); + return builder.Build(); + } + public VoxelInstanceBuilder CreateInstance() + { + return new VoxelInstanceBuilder(this); + } + + public bool Equals(VoxelDefinition other) + { + if (ReferenceEquals(this, other)) + return true; + + if (HashCode != other.HashCode) + return false; + + var valueCompare = EqualityComparer.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 CreateDefaultInstance() => new VoxelInstance(this); +} +public sealed class VoxelDefinition : VoxelDefinition, IEquatable> + where TAttrribute : class where TBitBacking : unmanaged, IBinaryInteger + +{ + public readonly IReadOnlyDictionary> Descriptor; + + public VoxelDefinition(string name, IReadOnlyDictionary attributes, IReadOnlyDictionary> descriptor) : base(name,attributes) + { + Descriptor = descriptor; + } + + // private Dictionary _descriptors =descripters; + public static VoxelDefinition Create(Action> config) + { + var builder = new VoxelDefinitionBuilder(); + config(builder); + return builder.Build(); + } + + // public T? GetAttribute() where T : TAttrribute => Attributes.TryGetValue(typeof(T), out var v) ? (T)v : default; + public Option GetAttribute(TBitBacking data) where T : TAttrribute, IBitBacking + { + 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.Some(attr); + } + } + return Attributes.TryGetValue(typeof(T), out var v) ? Option.Some((T)v) : Option.None; + } + + // public VoxelHandle SetAttributeData(int data, T value) where T : TAttrribute, IBitBacking + // { + // // return data | (T.GetBits(value) << ); + // throw new NotImplementedException();//TODO Createnew instance if needed or somthign + // } + + public IEnumerable GetAttributesOfType() + { + 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> 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 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 CreateBitBuilderInstance() + { + return new VoxelInstanceBuilder(this); + } + + public bool Equals(VoxelDefinition other) + { + var valueCompare = EqualityComparer>.Default; + return base.Equals((VoxelDefinition)other) && Descriptor.Count == other.Descriptor.Count && Descriptor.Keys.All(key => other.Descriptor.ContainsKey(key) && valueCompare.Equals(Descriptor[key], other.Descriptor[key])); + } +} +public class VoxelDefinitionConverter : JsonConverter> where TAttribute : class where TBitBacking : unmanaged , IBinaryInteger where THandle : IVoxelHandle +{ + private const string Type = "Type"; + private const string Data = "Data"; + public override VoxelDefinition ReadJson(JsonReader reader, Type objectType, VoxelDefinition existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var jObj = JObject.Load(reader); + var deffId = jObj[nameof(VoxelDefinition.Name)].ToString(); + + var DescriptorToken = jObj[nameof(VoxelDefinition.Descriptor)]; + var descriptorDict = new Dictionary>(); + 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)serializer.Deserialize(dataToken.CreateReader(), type); + descriptorDict.Add(type, descroterr); + } + } + var attributesToken = jObj[nameof(VoxelDefinition.Attributes)]; + var attrDict = new Dictionary(); + 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(deffId, attrDict,descriptorDict); + + } + + public override void WriteJson(JsonWriter writer, VoxelDefinition value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName(nameof(VoxelDefinition.Name)); + writer.WriteValue(value.Name); + if (value.Descriptor.Count > 0) + { + writer.WritePropertyName(nameof(VoxelDefinition.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.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 : JsonConverter> where TAttribute : class where TBitBacking : unmanaged , IBinaryInteger where THandle : IVoxelHandle +{ + private const string Type = "Type"; + private const string Data = "Data"; + private VoxelPalette> _registery; + public VoxelInstanceConverter(VoxelPalette> registery) { + _registery = registery; + } + public override VoxelInstance ReadJson(JsonReader reader, Type objectType, VoxelInstance existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var jObj = JObject.Load(reader); + var deffId = jObj[nameof(VoxelInstance.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>(converter); + + var attributesToken = jObj[nameof(VoxelInstance.Attributes)]; + var dict = new Dictionary(); + 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>.Of()); + + } + + public override void WriteJson(JsonWriter writer, VoxelInstance value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName(nameof(VoxelInstance.Definition)); + writer.WriteValue(value.Definition.Name); + if (value.Attributes.HasValue(out var attrbutes) && attrbutes.Count >0) + { + writer.WritePropertyName(nameof(VoxelInstance.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 +{ + public static int BitLength => 8; + + public static AttributeDescriptor AttributeBacking() + { + return new AttributeDescriptor(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.GetBits() + { + return GetBits(this); + } +} +// public class Registry +// { +// private List> voxelInstances = new(); +// public VoxelDefinition GetVoxel(VoxelHandle voxelHandle) +// { +// return voxelInstances[voxelHandle.Index]; +// } +// public VoxelHandle Register(VoxelDefinition voxelDefinition) +// { +// var handle = new VoxelHandle(voxelInstances.Count); +// voxelInstances.Add(voxelDefinition); +// return handle; +// } + +// } +public sealed class VoxelPalette : Palette where THandle : IVoxelHandle where TEntry : IEquatable +{ + + private readonly Func ctor; + + public TEntry Get(THandle handle) + { + if (handle.Index < 0) + throw new Exception(); + return Get(handle.Index); + } + public VoxelPalette(Func ctor) + { + this.ctor = ctor; + } + public THandle GetOrAddEntry(TEntry entry) => ctor(GetOrAddIndex(entry), entry); + + public THandle ModifyHandle(THandle handle, Func modify) + { + var oldEntry = Get(handle); + var newEntry = modify(oldEntry); + if (oldEntry.Equals(newEntry)) + { + return handle; + } + return GetOrAddEntry(newEntry); + } +} +public sealed class VoxelRegistry : IVoxelRegistry where TEntry : IVoxelInstance where TAttrubuite : class where THandle : IVoxelHandle where TVoxelDeff : IVoxelDefinition ,IEquatable +{ + private VoxelPalette voxelPalette; + public VoxelRegistry(VoxelPalette 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 : JsonConverter + where THandle : IVoxelHandle + where TEntry : IEquatable +{ + private readonly Func _ctor; + + public VoxelPaletteJsonConverter(Func 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)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>(serializer) ?? new List(); + + var palette = new VoxelPalette(_ctor); + foreach (var entry in entries) + { + palette.GetOrAddEntry(entry); + } + return palette; + } +} + +public static class PaletteExtensions +{ + public static THandle ModifyVoxelHandle( + this VoxelPalette> palette, + THandle handle, + Func, (VoxelInstance entry, TBitBacking newBits)> modify) + where THandle : IVoxelHandle,IVoxelHandle + where TBitBacking : unmanaged, IBinaryInteger + 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.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(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.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(handel.Data).HasValue && block3.GetAttribute(handel.Data).Value == new Hardness(5)) +// // { +// // } + +// } +// }V(); \ No newline at end of file diff --git a/src/voxelgame/Voxels/Registry/VoxelDefinition.cs.uid b/src/voxelgame/Voxels/Registry/VoxelDefinition.cs.uid new file mode 100644 index 0000000..9c2c27d --- /dev/null +++ b/src/voxelgame/Voxels/Registry/VoxelDefinition.cs.uid @@ -0,0 +1 @@ +uid://tphqa7lth1n1 diff --git a/src/voxelgame/Voxels/Registry/VoxelDefinitionBuilder.cs b/src/voxelgame/Voxels/Registry/VoxelDefinitionBuilder.cs new file mode 100644 index 0000000..7056c9b --- /dev/null +++ b/src/voxelgame/Voxels/Registry/VoxelDefinitionBuilder.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SJK.Voxels.Registry; + +public class VoxelDefinitionBuilder where TAttrribute : class +{ + public string Name { get; set; } + protected readonly Dictionary Attributes = new(); + public VoxelDefinitionBuilder WithAttribute(T attr) where T : TAttrribute + { + Attributes[typeof(T)] = attr; + // _descriptors[typeof(T)] = attributeDescriptor; + return this; + } + public virtual VoxelDefinition Build() + { + return new VoxelDefinition(Name, Attributes); + } + +} +public class VoxelDefinitionBuilder : VoxelDefinitionBuilder where TAttrribute : class where TBitBacking : unmanaged, IBinaryInteger{ + private readonly Dictionary> _descriptor = new(); + // private Dictionary.AttributeDescriptor> _descriptors = new(); + public VoxelDefinitionBuilder WithAttribute(T attr, AttributeDescriptor 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 a, IEnumerable> 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 Build() + { + return new VoxelDefinition(Name, Attributes,_descriptor); + } +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Registry/VoxelDefinitionBuilder.cs.uid b/src/voxelgame/Voxels/Registry/VoxelDefinitionBuilder.cs.uid new file mode 100644 index 0000000..c74d6e7 --- /dev/null +++ b/src/voxelgame/Voxels/Registry/VoxelDefinitionBuilder.cs.uid @@ -0,0 +1 @@ +uid://bgd33blsocwb6 diff --git a/src/voxelgame/Voxels/Registry/VoxelHandle.cs b/src/voxelgame/Voxels/Registry/VoxelHandle.cs new file mode 100644 index 0000000..5b7fc9c --- /dev/null +++ b/src/voxelgame/Voxels/Registry/VoxelHandle.cs @@ -0,0 +1,38 @@ +using System; +using System.Numerics; + +namespace SJK.Voxels.Registry; + +public readonly struct VoxelHandle(int index) : IVoxelHandle, IEquatable> +{ + public readonly int Index { get; } = index; + + public bool Equals(IVoxelHandle other) => other is VoxelHandle otherHandle && Equals(otherHandle); + public bool Equals(VoxelHandle other) => Index == other.Index; +} + +public interface IVoxelHandle : IEquatable> { + int Index { get; } +} +public interface IVoxelHandle : IEquatable> { + TBitBacking Data { get; } + IVoxelHandle WithData(TBitBacking bitBacking); +} +public readonly struct VoxelHandle(int index, TBitBacking data) : IVoxelHandle,IVoxelHandle, IEquatable> where TBitBacking : unmanaged, IBinaryInteger +{ + public readonly int Index { get; } = index; + + + public readonly TBitBacking Data { get; } = data; + + + public bool Equals(IVoxelHandle other) => other is VoxelHandle otherHandle && Equals(otherHandle); + + public bool Equals(VoxelHandle other) => Index == other.Index && Data == other.Data; + + public bool Equals(IVoxelHandle other) => other is VoxelHandle other2 && Equals(other2); + public VoxelHandle WithData(TBitBacking bitBacking) => new VoxelHandle(Index, bitBacking); + + IVoxelHandle IVoxelHandle.WithData(TBitBacking bitBacking) => WithData(bitBacking); + +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Registry/VoxelHandle.cs.uid b/src/voxelgame/Voxels/Registry/VoxelHandle.cs.uid new file mode 100644 index 0000000..4755b04 --- /dev/null +++ b/src/voxelgame/Voxels/Registry/VoxelHandle.cs.uid @@ -0,0 +1 @@ +uid://4w8vs8tnuopn diff --git a/src/voxelgame/Voxels/Registry/VoxelInstance.cs b/src/voxelgame/Voxels/Registry/VoxelInstance.cs new file mode 100644 index 0000000..02d2031 --- /dev/null +++ b/src/voxelgame/Voxels/Registry/VoxelInstance.cs @@ -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 where TAttribute : class +{ + + public IVoxelDefinition Definition { get; } + +} +public sealed class VoxelInstance : IVoxelInstance , IEquatable> where TAttribute : class +{ + public IVoxelDefinition Definition { get; } + public readonly IOption> Attributes; + public readonly int HashCode; + public VoxelInstance(IVoxelDefinition definition, IOption> attributes) + { + Definition = definition; + Attributes = attributes; + HashCode = ComputeHashCode(); + } + public VoxelInstance(VoxelDefinition definition, Dictionary? 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 GetAttribute(int data) where T : TAttribute, IBitBacking => + // Attributes.HasValue(out var att) && att.TryGetValue(typeof(T), out var v) + // ? Option.Some((T)v) + // : Definition.GetAttribute(data); + public T GetAttribute(Func ctor) where T : TAttribute => + Attributes.HasValue(out var att) && att.TryGetValue(typeof(T), out var v) + ? (T)v + : Definition.GetAttribute(ctor); + public IEnumerable GetAttributesOfType() + { + throw new NotImplementedException(); + // var set = new HashSet(); + // 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; + + // } + } + /// + /// Creates a builder initialized with this instance’s attributes. + /// + public VoxelInstanceBuilder ToBuilder() => + new VoxelInstanceBuilder(this); + public override bool Equals(object obj) => obj is VoxelInstance other && Equals(other); + public bool Equals(VoxelInstance 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.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: IVoxelInstance, IEquatable> where TAttribute : class where TBitBacking : unmanaged, IBinaryInteger +{ + public VoxelDefinition Definition { get; } + + IVoxelDefinition IVoxelInstance.Definition => Definition; + + public readonly IOption> Attributes; + public readonly int HashCode; + public VoxelInstance(VoxelDefinition definition, IOption> 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 GetAttribute(TBitBacking data) where T : TAttribute, IBitBacking => + Attributes.HasValue(out var att) && att.TryGetValue(typeof(T), out var v) + ? Option.Some((T)v) + : Definition.GetAttribute(data); + /// + /// Does not respect Data, only based of Deffination + /// + /// + /// + public IEnumerable GetAttributesOfType() + { + var set = new HashSet(); + 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)); + /// + /// Creates a builder initialized with this instance’s attributes. + /// + public VoxelInstanceBuilder ToBuilder(TBitBacking existingBits) =>//() where THandle : IVoxelHandle => + new VoxelInstanceBuilder(this,existingBits); + + public bool Equals(VoxelInstance 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.Default; + return a.Keys.All(key => b.ContainsKey(key) && valueCompare.Equals(a[key], b[key])); + } + public override bool Equals(object obj) => obj is VoxelInstance 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; + // } +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Registry/VoxelInstance.cs.uid b/src/voxelgame/Voxels/Registry/VoxelInstance.cs.uid new file mode 100644 index 0000000..cdfe238 --- /dev/null +++ b/src/voxelgame/Voxels/Registry/VoxelInstance.cs.uid @@ -0,0 +1 @@ +uid://bes4tnuxpkexq diff --git a/src/voxelgame/Voxels/Registry/VoxelInstanceBuilder.cs b/src/voxelgame/Voxels/Registry/VoxelInstanceBuilder.cs new file mode 100644 index 0000000..a594930 --- /dev/null +++ b/src/voxelgame/Voxels/Registry/VoxelInstanceBuilder.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using SJK.Functional; + +namespace SJK.Voxels.Registry; + +/// +/// Builder for creating modified VoxelInstances without mutating the original. +/// +public sealed class VoxelInstanceBuilder where TAttribute : class +{ + private readonly IVoxelDefinition _definition; + private Dictionary _attributes; + + public VoxelInstanceBuilder(VoxelInstance source) + { + _definition = source.Definition; + // Copy only if source had attributes + _attributes = new Dictionary(source.Attributes.Or([])); + } + public VoxelInstanceBuilder(VoxelDefinition source) + { + _definition = source; + _attributes = []; + } + + public VoxelInstanceBuilder Set(T attr) where T : TAttribute + { + + if (attr.Equals(_definition.GetAttribute())) + { + Remove(); + } + else + { + _attributes[typeof(T)] = attr; + } + return this; + } + public VoxelInstanceBuilder Modfiy(Func map) where T : TAttribute + { + if (_attributes.TryGetValue(typeof(T), out var attribute)) + { + var newAttr = map((T)attribute); + Set(newAttr); + } + return this; + } + + public VoxelInstanceBuilder Remove() where T : TAttribute + { + _attributes.Remove(typeof(T)); + return this; + } + + public VoxelInstance Build() => + new VoxelInstance(_definition, _attributes.ToOption()); +} +public sealed class VoxelInstanceBuilder where TAttribute : class where TBitBacking : unmanaged,IBinaryInteger +{ + private readonly VoxelDefinition _definition; + private readonly Dictionary _attributes; + private readonly TBitBacking existingBits; + public TBitBacking NewBits; + + public VoxelInstanceBuilder(VoxelInstance source, TBitBacking existingData) + { + + _definition = source.Definition; + // Copy only if source had attributes + _attributes = new Dictionary(source.Attributes.Or([])); + this.existingBits = existingData; + this.NewBits = existingBits; + } + public VoxelInstanceBuilder(VoxelDefinition source) + { + _definition = source; + _attributes = []; + } + + public VoxelInstanceBuilder Set(T attr) where T : TAttribute + { + if (_definition.Descriptor.TryGetValue(typeof(T), out var descriptor) + && descriptor.Backing == AttributeBacking.Bits + && attr is IBitBacking 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())) + { + Remove(); + } + else + { + _attributes[typeof(T)] = attr; + } + return this; + } + public VoxelInstanceBuilder Modfiy(Func map) where T : TAttribute + { + if (_attributes.TryGetValue(typeof(T), out var attribute)) + { + var newAttr = map((T)attribute); + Set(newAttr); + } + return this; + } + + public VoxelInstanceBuilder Remove() where T : TAttribute + { + _attributes.Remove(typeof(T)); + return this; + } + // [Export] + + public VoxelInstance Build(out TBitBacking newBits) + { + newBits = NewBits; + return new VoxelInstance(_definition, _attributes.ToOption()); + } +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Registry/VoxelInstanceBuilder.cs.uid b/src/voxelgame/Voxels/Registry/VoxelInstanceBuilder.cs.uid new file mode 100644 index 0000000..dd7a802 --- /dev/null +++ b/src/voxelgame/Voxels/Registry/VoxelInstanceBuilder.cs.uid @@ -0,0 +1 @@ +uid://b3bek2f2qoo8l diff --git a/src/voxelgame/Voxels/SpatialTree.cs b/src/voxelgame/Voxels/SpatialTree.cs new file mode 100644 index 0000000..d16641d --- /dev/null +++ b/src/voxelgame/Voxels/SpatialTree.cs @@ -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// where T : ISpatialEntry +{ + void Insert(T entry); + void Remove(T entry); + IEnumerable Query(Aabb region); +} +public interface ISpatialEntry +{ + Guid Guid { get; } + Aabb Bounds { get; } +} +public sealed class SpatialDataLayer : ISpatialTree where TEntry : class, ISpatialEntry +{ + private Dictionary _entries = new(); + private record struct Entry(Guid Guid, Aabb Bounds) : ISpatialEntry; + GridSpatialIndex> gridSpatialIndex; + public SpatialDataLayer(int gridSize = 128) + { + gridSpatialIndex = new(() => new OctreeEntryStore(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 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 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 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 : ISpatialTree where TEntry : ISpatialEntry +{ + private Dictionary entries = new(); + public void Insert(TEntry entry) + { + entries[entry.Guid] = entry; + } + + public IEnumerable 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 : ISpatialTree where TEntry : ISpatialEntry where TChunkSpatialTree : ISpatialTree +{ + private readonly int _cellSize; + private readonly Func factory; + private readonly Dictionary _cells = new(); + public GridSpatialIndex(Func 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(); + 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 Query(Aabb region) + { + var visted = new HashSet(); + 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 : ISpatialTree where T : ISpatialEntry +{ + private readonly OctreeNode _root; + + public OctreeEntryStore(Aabb worldBounds, int maxDepth = 8, int maxPerNode = 16) + { + _root = new OctreeNode(worldBounds, maxDepth, maxPerNode); + } + + public void Insert(T entry) => _root.Insert(entry); + public bool Remove(T entry) => _root.Remove(entry); + public IEnumerable Query(Aabb bounds) => _root.Query(bounds); + // public IEnumerable QuerySphere(Vector3 center, float radius) => _root.QuerySphere(center, radius); + + void ISpatialTree.Remove(T entry) + { + this.Remove(entry); + } + + // --- Internal Octree Node --- + private sealed class OctreeNode where TNode : ISpatialEntry + { + private readonly Aabb _bounds; + private readonly int _maxDepth; + private readonly int _maxPerNode; + private readonly int _depth; + + private List? _entries; + private OctreeNode[]? _children; + + public OctreeNode(Aabb bounds, int maxDepth, int maxPerNode, int depth = 0) + { + _bounds = bounds; + _maxDepth = maxDepth; + _maxPerNode = maxPerNode; + _depth = depth; + _entries = new List(); + } + + 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(); + _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 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 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[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(new Aabb(min, max), _maxDepth, _maxPerNode, _depth + 1); + } + + // Redistribute current entries + var oldEntries = _entries!; + _entries = new List(); + 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 +{ + 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; + } +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/SpatialTree.cs.uid b/src/voxelgame/Voxels/SpatialTree.cs.uid new file mode 100644 index 0000000..4966995 --- /dev/null +++ b/src/voxelgame/Voxels/SpatialTree.cs.uid @@ -0,0 +1 @@ +uid://dik0ryeup2wah diff --git a/src/voxelgame/Voxels/TestWraper.tscn b/src/voxelgame/Voxels/TestWraper.tscn new file mode 100644 index 0000000..0983bd3 --- /dev/null +++ b/src/voxelgame/Voxels/TestWraper.tscn @@ -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 diff --git a/src/voxelgame/Voxels/UVWrapVisualizer.cs b/src/voxelgame/Voxels/UVWrapVisualizer.cs new file mode 100644 index 0000000..eeff916 --- /dev/null +++ b/src/voxelgame/Voxels/UVWrapVisualizer.cs @@ -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; + } +} diff --git a/src/voxelgame/Voxels/UVWrapVisualizer.cs.uid b/src/voxelgame/Voxels/UVWrapVisualizer.cs.uid new file mode 100644 index 0000000..935e2c5 --- /dev/null +++ b/src/voxelgame/Voxels/UVWrapVisualizer.cs.uid @@ -0,0 +1 @@ +uid://kdg8pxsdacor diff --git a/src/voxelgame/Voxels/VoxelQuery.cs b/src/voxelgame/Voxels/VoxelQuery.cs new file mode 100644 index 0000000..6289723 --- /dev/null +++ b/src/voxelgame/Voxels/VoxelQuery.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +public interface IVoxelQuery : IEnumerable +{ + IVoxelQuery Union(IVoxelQuery other); + IVoxelQuery Exclude(IVoxelQuery other); + IVoxelQuery Intersect(IVoxelQuery other); + IVoxelQuery Where(Func 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 GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IVoxelQuery Where(Func 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 offset; + + public TransformQuery(IVoxelQuery source, Func offset) + { + this.source = source; + this.offset = offset; + } + public override IEnumerator 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 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 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 _predicate; + + public FilterQuery(IVoxelQuery source, Func predicate) + { + _source = source; + _predicate = predicate; + } + + public override IEnumerator 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 GetEnumerator() + { + var seen = new HashSet(); + 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 GetEnumerator() + { + var b = new HashSet(_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 GetEnumerator() + { + var excludeSet = new HashSet(_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( + this IVoxelQuery query, + Func source) + { + foreach (var pos in query) + { + var voxel = source(pos); + yield return (pos, voxel); + } + } + // public static IEnumerable<(ChunkPos ChunkPos, VoxelPos LocalPos)> ToChunkLocal(this IEnumerable self) where TChunkSize : IChunkSize + // { + // foreach (var pos in self) + // { + // yield return pos.ToVoxelPos(); + // } + // } +} diff --git a/src/voxelgame/Voxels/VoxelQuery.cs.uid b/src/voxelgame/Voxels/VoxelQuery.cs.uid new file mode 100644 index 0000000..a61295d --- /dev/null +++ b/src/voxelgame/Voxels/VoxelQuery.cs.uid @@ -0,0 +1 @@ +uid://f4gkrcovyyc diff --git a/src/voxelgame/Voxels/Worlds/IWorldChunk.cs b/src/voxelgame/Voxels/Worlds/IWorldChunk.cs new file mode 100644 index 0000000..09699cb --- /dev/null +++ b/src/voxelgame/Voxels/Worlds/IWorldChunk.cs @@ -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 :IWorldChunk, IReadOnlyVoxelChunk where TChunkSize : IChunkSize +{ + ChunkPos ChunkPos {get;set;} + Option GetCustomData() where T : IChunkData; + void SetCustomData(T value)where T : IChunkData; + TVoxel GetVoxelAt(VoxelPos voxelPos); + void SetVoxelAt(VoxelPos voxelPos, TVoxel voxel); + +} +public class WorldChunk : IWorldChunk where TChunkSize : IChunkSize where TVoxel : IEquatable +{ + public ChunkPos 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 _palette; + private IVoxelChunk _data; + private readonly SortedDictionary _customData = []; + public WorldChunk(TVoxel @default) + { + _palette = new (@default); + _data = new VoxelChunk(); + } + public Option GetCustomData() where T : IChunkData + { + if (_customData.TryGetValue(ComponentType.Id, out var data)) + { + return Option.Some((T)data); + } + return Option.None; + } + + public TVoxel GetVoxelAt(VoxelPos voxelPos) + { + return _palette.Get(_data.GetVoxel(voxelPos)); + + } + + public void SetCustomData(T value) where T : IChunkData + { + + } + + public void SetVoxelAt(VoxelPos voxelPos, TVoxel voxel) + { + _data.SetVoxel(voxelPos.X,voxelPos.Y,voxelPos.Z,_palette.GetOrAddIndex(voxel)); + } + + public TVoxel GetVoxel(VoxelPos position)=>GetVoxelAt(position); + + public TVoxel GetVoxel(VoxelPos voxelPos)=> GetVoxelAt(voxelPos.GetVoxelPos()); +} +public interface IChunkData +{ + ValueTask Serialize(Stream stream); + ValueTask Deserialize(Stream stream); +} +public struct CustomEntry(T data, Action serialize, Func deserialize) : IChunkData +{ + private T data = data; + private readonly Action serialize = serialize; + private readonly Func 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; + } +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Worlds/IWorldChunk.cs.uid b/src/voxelgame/Voxels/Worlds/IWorldChunk.cs.uid new file mode 100644 index 0000000..56ddb22 --- /dev/null +++ b/src/voxelgame/Voxels/Worlds/IWorldChunk.cs.uid @@ -0,0 +1 @@ +uid://ct81cqcwbb6wk diff --git a/src/voxelgame/Voxels/Worlds/VoxelSerializer.cs b/src/voxelgame/Voxels/Worlds/VoxelSerializer.cs new file mode 100644 index 0000000..1cc907a --- /dev/null +++ b/src/voxelgame/Voxels/Worlds/VoxelSerializer.cs @@ -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 +{ + ValueTask SerializeAsync(T value); + ValueTask DeserializeAsync(Span bytes); + + +} +public interface IDataSerializer +{ + ValueTask SerializeAsync(T value); + ValueTask DeserializeAsync(T2 bytes); + + +} +// public interface IDataStore +// { +// ValueTask> ReadAsync(DataKey path, CancellationToken token = default); +// ValueTask WriteAsync(DataKey path, byte[] data, CancellationToken token = default); +// ValueTask ExistsAsync(DataKey path, CancellationToken token = default); +// ValueTask DeleteAsync(DataKey path, CancellationToken token = default); +// public bool IsReadOnly { get; } +// } +public interface IDataStore +{ + ValueTask> ReadAsync(TDomainKey key, CancellationToken token = default); + ValueTask WriteAsync(TDomainKey key, T value, CancellationToken token = default); + ValueTask 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 data); +} +public interface ISqliteSchema +{ + 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 +{ + Task> 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 : IDataStore +{ + private readonly SqliteConnection connection; + private readonly ISqliteSchema schema; + private readonly ISqliteProvider provider; + + public SQLiteDataStore(SqliteConnection connection, ISqliteSchema schema, ISqliteProvider 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 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> 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.Some(bytes) + : Option.None; + } + + public Option 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.Some(bytes) + : Option.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 +{ + 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 ExistsAsync(string path, CancellationToken token = default) + { + var file = Path.Combine(rootDir, path); + return ValueTask.FromResult(File.Exists(file)); + } + + public ValueTask> 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.Some(File.OpenRead(file)):Option.None); + // ? Option.Some(await File.ReadAllBytesAsync(file, token)) + // : Option.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 _cache = new(); + + +// public bool IsReadOnly => false; + + +// public ValueTask ExistsAsync(DataKey path, CancellationToken token = default) +// => new(_cache.ContainsKey(path.ToString())); + +// public ValueTask> 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.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.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 +// { +// ValueTask> GetDataAsync(DataKey key); +// ValueTask SetDataAsync(DataKey key, T value, bool dirty = true); +// ValueTask FlushDataAsync(DataKey key); +// ValueTask FlushAllAsync(); + + +// } +// public sealed class MemoryDataLayer : IDataCache +// { +// private readonly IDataStore _storage; +// private readonly IDataSerializer _serializer; +// private readonly ConcurrentDictionary _cache = new(); +// private readonly bool _autoCache; +// private readonly bool _autoWriteThrough; + +// public MemoryDataLayer(IDataStore storage, IDataSerializer serializer, bool autoCache = true, bool autoWriteThrough = false) +// { +// _storage = storage; +// _serializer = serializer; +// _autoCache = autoCache; +// _autoWriteThrough = autoWriteThrough; +// } + +// public async ValueTask> GetDataAsync(DataKey key) +// { +// if (_cache.TryGetValue(key, out var entry)) +// return Option.Some(entry.data); + +// var bytes = await _storage.ReadAsync(key); +// if (!bytes.HasValue) +// return Option.None; + +// var chunk = await _serializer.DeserializeAsync(bytes.Value); +// if (_autoCache) +// _cache[key] = (chunk, false); +// return Option.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 +{ + ValueTask FlushAsync( + IDataStore? target, + CancellationToken token = default); + ValueTask FlushAsync(TDomainKey key, IDataStore? target, CancellationToken token = default); +} +public interface ILoadedDataStore +{ + IEnumerable<(TDomainKey, TData)> GetLoadedData(); + Option TryGetData(TDomainKey key); +} +public sealed class MemoryDataLayer : IDataStore, IFlushableStore, ILoadedDataStore where TDomainKey : IEquatable +{ + private readonly ConcurrentDictionary _cache = new(); + + public bool IsReadOnly => false; + public ValueTask> 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? 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 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 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 TryGetData(TDomainKey key) + { + return _cache.GetValue(key).Map(f => f.data).ToStructOption(); + } + + public Option Read(TDomainKey key) + { + if (_cache.TryGetValue(key, out var entry)) + return Option.Some(entry.data); + return Option.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( + IDataStore Store, + LayerPolicy Policy, + string Name); +public interface ITwoTierStore : IDataStore, IFlushableStore +{ + IDataStore Cache { get; } + IDataStore Persistent { get; } +} + +public sealed class TwoTierStore : IDataStore, IFlushableStore where TCache : IDataStore, IFlushableStore where TPersistent : IDataStore +{ + + 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 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> 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? target, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public ValueTask FlushAsync(TDomainKey key, IDataStore target, CancellationToken token = default) + { + throw new NotImplementedException(); + } + +} +public sealed class TwoTierStore : IDataStore, IFlushableStore, ILoadedDataStore where TCache : IDataStore, IFlushableStore , ILoadedDataStore +{ + + public TwoTierStore(TCache cached, IDataStore store) + { + Cached = cached; + Store = store; + } + + public TCache Cached { get; } + public IDataStore 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 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> 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? target, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public ValueTask FlushAsync(TDomainKey key, IDataStore target, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public IEnumerable<(TDomainKey, TData)> GetLoadedData() + { + return Cached.GetLoadedData(); + } + + public Option TryGetData(TDomainKey key) + { + return Cached.TryGetData(key); + } + public async ValueTask LoadChunkToMemory(TDomainKey key,Func factory) + { + var s =await Store.ReadAsync(key); + await Cached.WriteAsync(key,s.Or(()=>factory(key))); + + } + +} +public sealed class LayeredDataStore : IDataStore, IFlushableStore where TDomainKey : IEquatable +{ + public bool IsReadOnly => _layers.All(item => item.Policy == LayerPolicy.ReadOnly); + private readonly List> _layers; + public LayeredDataStore(IEnumerable> dataStores) + { + _layers = dataStores.ToList(); + } + public ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public ValueTask ExistsAsync(TDomainKey key, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public ValueTask FlushAsync(IDataStore target, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + public async ValueTask> 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.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 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 where TChunkSize : IChunkSize where TChunk : IVoxelChunk +{ + ValueTask GetChunkAsync(ChunkPos chunkPos); + ValueTask SetChunkAsync(ChunkPos chunkPos, TChunk chunk); + Option TryGetChunk(ChunkPos chunkPos); + IEnumerable<(ChunkPos,TChunk)> GetLoadedChunks(); +} +public sealed class ChunkManger :IChunkManger where TChunkSize : IChunkSize where TChunkStore : IDataStore, TChunk>, IFlushableStore, TChunk> , ILoadedDataStore,TChunk> where TChunk : IVoxelChunk +{ + private TChunkStore chunkStore; + private readonly Func factory; + + // public async ValueTask SetChunkAysnc(ChunkPos chunkPos, IVoxelChunk chunk) + // { + // await chunkStore.WriteAsync(chunkPos, chunk); + // // await cachedData.WriteAsync(chunkPos, chunk); + // } + // public async ValueTask>> GetChunkAysnc(ChunkPos 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 factory) + { + this.chunkStore = chunkStore; + this.factory = factory; + } + + // public ChunkManger(MemoryDataLayer> memoryDataLayer) + // { + // this.memoryDataLayer = memoryDataLayer; + // } + // private MemoryDataLayer> memoryDataLayer; + public async ValueTask SetChunkAsync(ChunkPos chunkPos, TChunk voxelChunk) + { + await chunkStore.WriteAsync(chunkPos, voxelChunk); + } + public async ValueTask GetChunkAsync(ChunkPos chunkPos) + { + return (await chunkStore.ReadAsync(chunkPos)).Or(factory); + } + + public Option TryGetChunk(ChunkPos chunkPos) + { + return chunkStore.TryGetData(chunkPos); + } + + public IEnumerable<(ChunkPos, TChunk)> GetLoadedChunks() + { + return chunkStore.GetLoadedData(); + } + // public void SetChunk(ChunkPos chunkPos, IVoxelChunk voxelChunk) + // { + // Task.Run(() => chunkStore.WriteAsync(chunkPos, voxelChunk)).GetAwaiter().GetResult(); + // } + // public Option> GetChunk(ChunkPos chunkPos) + // { + // return Task.Run(() => chunkStore.ReadAsync(chunkPos)).GetAwaiter().GetResult().Result; + // } + +} +public interface IKeyMapper +{ + TInnerKey MapKey(TDomainKey key); +} +public class DataStoreMapper(IDataStore innerStore, IKeyMapper mapper) : IDataStore +{ + private readonly IDataStore innerStore = innerStore; + private readonly IKeyMapper mapper = mapper; + + public bool IsReadOnly => innerStore.IsReadOnly; + + public ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default) => innerStore.DeleteAsync(mapper.MapKey(key), token); + + public ValueTask ExistsAsync(TDomainKey key, CancellationToken token = default) => innerStore.ExistsAsync(mapper.MapKey(key), token); + public ValueTask> 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(IDataStore innerStore,IDataSerializer serializer) : IDataStore +{ + private readonly IDataStore innerStore = innerStore; + private readonly IDataSerializer serializer = serializer; + + public bool IsReadOnly => innerStore.IsReadOnly; + + public ValueTask DeleteAsync(TDomainKey key, CancellationToken token = default) => innerStore.DeleteAsync(key, token); + + public ValueTask ExistsAsync(TDomainKey key, CancellationToken token = default) => innerStore.ExistsAsync(key, token); + public async ValueTask> ReadAsync(TDomainKey key, CancellationToken token = default) + { + var data = await innerStore.ReadAsync(key, token); + return data.HasValue?Option.Some(await serializer.DeserializeAsync(data.Value)):Option.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(Func toKey) : IKeyMapper +{ + private readonly Func toKey = toKey; + + public TNewKey MapKey(TDomainKey key) => toKey(key); +} +public sealed class FuncDataSerializer(Func toData, Func toBytes) : IDataSerializer +{ + private readonly Func toData = toData; + private readonly Func toBytes = toBytes; + + public ValueTask DeserializeAsync(TInnerData bytes) => ValueTask.FromResult(toData(bytes)); + + public ValueTask SerializeAsync(TData value) => ValueTask.FromResult(toBytes(value)); +} +public class DataRepository : IDataStore +{ + private readonly IDataStore _store; + private readonly IKeyMapper _mapper; + private readonly IDataSerializer _serializer; + + public DataRepository(IDataStore store, IKeyMapper mapper, IDataSerializer 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 ExistsAsync(TDomainKey key, CancellationToken token = default) + { + throw new NotImplementedException(); + } + + // public async ValueTask> GetAsync(TDomainKey key) + // { + // var dataKey = _mapper.ToDataKey(key); + // var bytes = await _store.ReadAsync(dataKey); + // if (!bytes.HasValue) return Option.None; + // return Option.Some(await _serializer.DeserializeAsync(bytes.Value)); + // } + + public async ValueTask> ReadAsync(TDomainKey key, CancellationToken token = default) + { + var dataKey = _mapper.MapKey(key); + var bytes = await _store.ReadAsync(dataKey,token); + if (!bytes.HasValue) return Option.None; + return Option.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(IDataStore inner, IDataCompressor dataCompressor) : IDataStore +{ + private readonly IDataStore 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 ExistsAsync(TKey path, CancellationToken token = default) => inner.ExistsAsync(path, token); + + public async ValueTask> ReadAsync(TKey path, CancellationToken token = default) + { + var result = await inner.ReadAsync(path, token); + if (!result.HasValue) + return Option.None; + return Option.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, TStore> + MemoryCached(TStore store, LayerPolicy policy = LayerPolicy.WriteBack) + where TStore : IDataStore where TKey : IEquatable + { + var cache = new MemoryDataLayer(); + return new TwoTierStore, TStore>(cache, store); + } + + public static TwoTierStore + Create(TCache cache, TStore store, LayerPolicy policy = LayerPolicy.WriteBack) + where TCache : IDataStore, IFlushableStore + where TStore : IDataStore + => new(cache, store); + + public static TwoTierStore, TStore> + WithMemoryCache( + this TStore store, + LayerPolicy policy = LayerPolicy.WriteBack) + where TKey : IEquatable + where TStore : IDataStore + { + var cache = new MemoryDataLayer(); + return new TwoTierStore, TStore>(cache, store); + } + public static TwoTierStoreBuilder For() + where TKey : IEquatable + => new TwoTierStoreBuilder(); +} +public readonly struct TwoTierStoreBuilder + where TKey : IEquatable +{ + public TwoTierStore, TStore> + MemoryCached(TStore store, LayerPolicy policy = LayerPolicy.WriteBack) + where TStore : IDataStore + { + var cache = new MemoryDataLayer(); + return new TwoTierStore, TStore>(cache, store); + } + public TwoTierStore> + MemoryCached(IDataStore store, LayerPolicy policy = LayerPolicy.WriteBack) + + { + var cache = new MemoryDataLayer(); + return new TwoTierStore>(cache, store); + } +} \ No newline at end of file diff --git a/src/voxelgame/Voxels/Worlds/VoxelSerializer.cs.uid b/src/voxelgame/Voxels/Worlds/VoxelSerializer.cs.uid new file mode 100644 index 0000000..910d8b3 --- /dev/null +++ b/src/voxelgame/Voxels/Worlds/VoxelSerializer.cs.uid @@ -0,0 +1 @@ +uid://c3rdxjvybaq0x diff --git a/src/voxelgame/Voxels/Worlds/VoxelWorld.cs b/src/voxelgame/Voxels/Worlds/VoxelWorld.cs new file mode 100644 index 0000000..471bb41 --- /dev/null +++ b/src/voxelgame/Voxels/Worlds/VoxelWorld.cs @@ -0,0 +1,1087 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Threading.Tasks; +using Godot; +using Microsoft.Data.Sqlite; +using ProtoBuf; +using SJK.Functional; +using SJK.Voxels.Registry; +using SjkScripts.Helpers; +using SqlKata.Compilers; +using SqlKata.Execution; + +namespace SJK.Voxels; + +public interface IVoxelWorld where TChunkSize : IChunkSize +{ + Option GetVoxel(Vector3I position); + void SetVoxel(Vector3I position, TVoxel voxel); + bool IsInBounds(Vector3I position); + bool IsChunkPosInBounds(ChunkPos chunkPos); + Option GetChunk(ChunkPos chunkPosition); + void SetChunk(ChunkPos chunkPos, TChunk chunk); + // IVoxelChunk GetChunkOrCreateAndAdd(ChunkPos chunkPosition); + // bool TryGetChunk(ChunkPos chunkPosition, [NotNullWhen(true)] out IVoxelChunk chunk); +} +// public class VoxelWorldUnConstrained : IVoxelWorld,TChunkSize>/*, IEnumerable, IVoxelChunk>>*/ where TChunkSize : IChunkSize +// { +// // private readonly IChunkFactorty, TVoxel,TChunkSize> chunkFactory; +// // private Dictionary, IVoxelChunk> _chunks; +// private readonly Func> chunkFactory; +// // private ChunkManger, IVoxelChunk, +// // MemoryDataLayer,IVoxelChunk>, +// // LayeredDataStore, IVoxelChunk>>> chunkManger; +// private ChunkManger, IVoxelChunk, +// MemoryDataLayer,IVoxelChunk>, +// LayeredDataStore,IVoxelChunk>>> chunkManger; +// public VoxelWorldUnConstrained(/*IChunkFactorty, TVoxel, TChunkSize> chunkFactory,*/ IDataStore,IVoxelChunk> dataStore, Func> factory = null) +// { +// // _chunks = new(); + +// // chunkManger = new( new TwoTierStore, IVoxelChunk, MemoryDataLayer, IVoxelChunk>, IDataStore, IVoxelChunk>>( +// // new MemoryDataLayer, IVoxelChunk>(), +// // new LayeredDataStore, IVoxelChunk>( +// // [ +// // new DataLayer,IVoxelChunk>(dataStore,LayerPolicy.WriteBack,"Save"),] +// // ) +// // )); +// var worldStore = new LayeredDataStore, IVoxelChunk>( +// [ +// new DataLayer, IVoxelChunk>(dataStore, LayerPolicy.WriteBack, "Disk Save") +// ]); +// chunkManger = new(TwoTierStoreFactory.For,IVoxelChunk>().MemoryCached(worldStore)); +// // Or via factory + + +// // this.chunkFactory = chunkFactory; +// chunkFactory = factory ?? DefaultFactory; +// } +// private static IVoxelChunk DefaultFactory() => new VoxelChunk(); +// public Option> GetChunk(ChunkPos chunkPosition) => chunkManger.GetChunk(chunkPosition); +// public async Task>> GetChunkAsync(ChunkPos chunkPosition) => await chunkManger.GetChunkAsync(chunkPosition); +// // public IVoxelChunk GetChunkOrCreateAndAdd(ChunkPos chunkpos) => GetChunk(chunkpos).Or(() => { var chunk = chunkFactory.CreateChunk(chunkpos); _chunks.Add(chunkpos, chunk); return chunk; }); + +// // public IEnumerator, IVoxelChunk>> GetEnumerator() => _chunks.GetEnumerator(); +// public Option GetVoxel(Vector3I position) +// { +// var chunkPos = ChunkPos.FromGlobal(position); ; +// return GetChunk(chunkPos).Map(chunk => chunk.GetVoxel(ChunkPos.ToLocal(position))); + +// } + +// public bool IsChunkPosInBounds(ChunkPos chunkPos) +// { +// return true; +// } +// public bool IsInBounds(Vector3I position) +// { +// return true; +// } + +// public void SetChunk(ChunkPos chunkPos, IVoxelChunk chunk) +// { +// chunkManger.SetChunk(chunkPos, chunk); +// // _chunks[chunkPos] = chunk; +// } +// public async Task SetChunkAsync(ChunkPos chunkPos, IVoxelChunk chunk) +// { +// await chunkManger.SetChunkAsync(chunkPos, chunk); +// // _chunks[chunkPos] = chunk; +// } + +// public void SetVoxel(Vector3I position, TVoxel voxel) +// { +// var pos = new VoxelPos(position.X, position.Y, position.Z); +// var chunk = GetChunk(pos.GetChunkPos());//.OrDefault(chunkFactory.CreateChunk(pos.GetChunkPos())); +// if (!chunk.HasValue) +// { +// chunk = Option>.Some(chunkFactory()); +// } +// chunk.Value.SetVoxel(pos.GetVoxelPos(), voxel); + +// SetChunk(pos.GetChunkPos(), chunk.Value); +// // var chunkPos = ChunkPos.FromGlobal(position); +// // GetChunkOrCreateAndAdd(chunkPos).SetVoxel(ChunkPos.ToLocal(position), voxel); +// } +// public IVoxelChunk[] GetChunkNeibors(ChunkPos chunkPos) => [ +// GetChunk(chunkPos + new ChunkPos(0,0,-1)).Or(()=>null), +// GetChunk(chunkPos + new ChunkPos(1,0,0)).Or(()=>null), +// GetChunk(chunkPos + new ChunkPos(0,0,1)).Or(()=>null), +// GetChunk(chunkPos + new ChunkPos(-1,0,0)).Or(()=>null), +// GetChunk(chunkPos + new ChunkPos(0,1,0)).Or(()=>null), +// GetChunk(chunkPos + new ChunkPos(0,-1,0)).Or(()=>null), +// ]; + +// public async ValueTask[]> GetChunkNeiborsAsync(ChunkPos chunkPos) => [ +// (await GetChunkAsync(chunkPos + new ChunkPos(0,0,-1))).Or(()=>null), +// (await GetChunkAsync(chunkPos + new ChunkPos(1,0,0))).Or(()=>null), +// (await GetChunkAsync(chunkPos + new ChunkPos(0,0,1))).Or(()=>null), +// (await GetChunkAsync(chunkPos + new ChunkPos(-1,0,0))).Or(()=>null), +// (await GetChunkAsync(chunkPos + new ChunkPos(0,1,0))).Or(()=>null), +// (await GetChunkAsync(chunkPos + new ChunkPos(0,-1,0))).Or(()=>null), +// ]; + +// // public bool TryGetChunk(ChunkPos chunkPosition, [NotNullWhen(true)] out IVoxelChunk chunk) => _chunks.TryGetValue(chunkPosition, out chunk); + +// // IEnumerator IEnumerable.GetEnumerator() => _chunks.GetEnumerator(); + +// } +public class ChunkFactory : IChunkFactorty where TVoxelChunk : IVoxelChunk where TChunk : IChunkSize +{ + private readonly Func chunkFactory; + + public ChunkFactory(Func chunkFactory) + { + this.chunkFactory = chunkFactory; + } + public TVoxelChunk CreateChunk(ChunkPos chunkpos) => chunkFactory(chunkpos); + + public TVoxel GetVoxelAt(Vector3I globalPos) + { + var chunkPos = ChunkPos.FromGlobal(globalPos); + return chunkFactory(chunkPos).GetVoxel(ChunkPos.ToLocal(globalPos)); + } + + public TVoxel GetVoxelAt(ChunkPos chunkPos, Vector3I blockPos) + { + throw new NotImplementedException(); + } +} +public interface IChunkFactorty; +public interface IChunkFactorty :IChunkFactorty where TChunk : IChunkSize +{ + public TVoxelChunk CreateChunk(ChunkPos chunkpos); + public TVoxel GetVoxelAt(Vector3I globalPos); + public TVoxel GetVoxelAt(ChunkPos chunkPos, Vector3I blockPos); + +} +public record LayerDescriptor( + string Id, + Type VoxelType, + IChunkFactorty Factorty + // ISerializer Serializer + // IEnumerable Systems +); +// public interface ISerializer; +// public interface ILayerSystem; +// public class VoxelRegister +// { +// private Dictionary voxelSerializer = new(); +// private Dictionary voxelDatas = new(); +// public void Register(string id, IVoxelSerializer serializer,TVoxelData voxelData) +// { +// if (voxelSerializer.ContainsKey(id)) +// { +// throw new Exception(); +// } +// voxelSerializer.Add(id, serializer); +// if (voxelDatas.ContainsKey(id)) +// { +// return; +// } +// voxelDatas.Add(id, voxelData); +// } +// public bool TryGet(string id, [NotNullWhen(true)] out IVoxelSerializer serializer, out TVoxelData voxelData) where TVoxelData : class +// { + +// serializer = voxelSerializer.GetValue(id).Map(some => some as IVoxelSerializer).Or(() => null); +// voxelData = voxelDatas.GetValue(id).Map(some => some as TVoxelData).Or(() => null); +// return (serializer is not null && voxelDatas is not null); +// } + +// } +public interface IVoxelSerializer +{ + public Dictionary Serializer(TVoxel voxel); + public TVoxel DeSerializer(Dictionary data); +} +// public class TestWorld() { +// VoxelRegister voxelRegister = new(); +// List layerDescriptors = new(); +// public void main() +// { + +// //TestRegisterlayer +// layerDescriptors.Add(new LayerDescriptor( +// "Blocks", +// typeof(testBlock), +// new ChunkFactory, testBlock, Chunk32>(pos => new VoxelChunk()) +// )); +// voxelRegister.Register("Dirt", new testBlockSerlizer(),new testblockData()); +// voxelRegister.Register("Grass", new testBlockSerlizer(),new testblockData()); + +// } +// } +struct testBlock +{ + public int value; +} +struct testblockData +{ + public Texture2D NorthTesture; + public Texture2D EastTesture; + //etc + +} +class testBlockSerlizer : IVoxelSerializer +{ + public testBlock DeSerializer(Dictionary data) + { + return new testBlock() { value = data.GetValue(nameof(testBlock.value)).Match(item => (int)item, () => 0) }; + } + + public Dictionary Serializer(testBlock voxel) + { + return new Dictionary() { [nameof(testBlock.value)] = voxel.value }; + } +} +// public interface IChunkSerialzation where TChunkContainer : IChunkContainer where TChunk : IChunkSize{ +// byte[] SerializeChunk(TChunkContainer voxelChunk); +// TChunkContainer DeserializeChunk(ReadOnlySpan data); +// } +// public interface IChunkContainer where TChunkSize : IChunkSize { +// ChunkPos Position { get; } +// T GetLayer(string id); +// void AddLayer(T layer, string id); +// public IEnumerable<(string, object)> EnumerateLayers(); +// } +// [ProtoContract] +// public sealed class ChunkContainer : IChunkContainer where TChunk : IChunkSize +// { +// public ChunkPos Position { get; } +// [ProtoMember(0)] +// private readonly Dictionary _layers = new(); +// public void AddLayer(T layer, string id) +// { +// _layers[id] = layer; +// } + +// public IEnumerable<(string, object)> EnumerateLayers() +// { +// return _layers.Select(kv => (kv.Key, kv.Value)); +// } + +// public T GetLayer(string id) +// { +// return (T)_layers[id]; +// } + +// } +// public interface IChunkLayer where TChunkSize : IChunkSize where TChunkConatiner : IChunkContainer +// { +// string Id { get; } // e.g. "blocks", "water", "light" +// abstract static Type VoxelType { get; } +// abstract static Type ChunkType { get; } + +// IChunkStorage Storage { get; } // Non-generic base +// IChunkSerialzation Serializer { get; } // handles (de)serialization +// } +// public abstract class ChunkLayer : IChunkLayer where TChunkSize : IChunkSize where TChunkConatiner : IChunkContainer +// { +// public string Id { get; } +// public Type VoxelType => typeof(TVoxel); +// public Type ChunkType => typeof(TChunk); +// public IChunkStorage Storage { get; } +// public IChunkSerialzation Serializer { get; } + +// protected ChunkLayer(string id, IChunkStorage storage, IChunkSerialzation serializer) +// { +// Id = id; +// Storage = storage; +// Serializer = serializer; +// } +// } +// public interface IChunkStorage : IDisposable where TChunkSize : IChunkSize +// { +// ValueTask> GetChunkAsync(ChunkPos chunkPos); +// ValueTask SetChunkAsync(ChunkPos chunkPos, object chunk); +// bool IsReadOnly { get; } +// ValueTask FlushToAsync(); + +// } +// public interface IChunkStorage : IChunkStorage, IDisposable where TChunkSize : IChunkSize where TChunkContainer : IChunkContainer +// { +// ValueTask> GetChunkAsync(ChunkPos chunkPos)where TChunkLayer : IChunkLayer; +// ValueTask SetChunkAsync(ChunkPos chunkPos, TChunk chunk) where TChunkLayer: IChunkLayer; +// ValueTask FlushToAsync(IChunkStorage target = null); + +// } +// public interface IChunkContainerStorage where TChunk : IChunkSize where TChunkContainer : IChunkContainer +// { +// ValueTask LoadContainerAsync(ChunkPos position); +// ValueTask SaveContainerAsync(TChunkContainer container); +// } +// public sealed class SQLiteChunkStorage : IChunkStorage where TChunkSize : IChunkSize +// { +// private readonly SqliteConnection connection; +// private readonly SqliteCompiler compiler; +// private readonly QueryFactory db; +// private bool disposedValue; + +// public SQLiteChunkStorage(SqliteConnection connection, bool isReadOnly = false) +// { +// this.connection = connection; +// IsReadOnly = isReadOnly; +// compiler = new SqliteCompiler(); +// db = new QueryFactory(connection, compiler); +// } +// public bool IsReadOnly { get; } + +// public ValueTask FlushToAsync(IChunkStorage target = null) +// { +// throw new NotImplementedException(); +// } + +// public ValueTask>> GetChunkAsync(ChunkPos chunkPos) +// { +// throw new NotImplementedException(); +// } + +// public ValueTask SetChunkAsync(ChunkPos chunkPos, IVoxelChunk chunk) +// { + +// } + +// private void Dispose(bool disposing) +// { +// if (!disposedValue) +// { +// if (disposing) +// { +// // TODO: dispose managed state (managed objects) +// db.Dispose(); +// } + +// // TODO: free unmanaged resources (unmanaged objects) and override finalizer +// // TODO: set large fields to null +// disposedValue = true; +// } +// } + +// // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources +// // ~SQLiteChunkStorage() +// // { +// // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method +// // Dispose(disposing: false); +// // } + +// public void Dispose() +// { +// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method +// Dispose(disposing: true); +// GC.SuppressFinalize(this); +// } +// public static int InsertOrUpdateOnConflict(this QueryFactory db, string table, object insertData, string[] conflictColumns, string[] updateColumns) +// { +// var compiler = db.Compiler; +// var insertQuery = new Query(table).AsInsert(insertData); +// var compiled = compiler.Compile(insertQuery); + +// var conflict = string.Join(", ", conflictColumns); +// var setClause = string.Join(", ", updateColumns.Select(c => $"{c} = excluded.{c}")); + +// var sql = $"{compiled.Sql} ON CONFLICT({conflict}) DO UPDATE SET {setClause};"; + +// return db.Statement(sql, compiled.NamedBindings); +// } +// } + + +// // public sealed class FileChunkStorage: IChunkStorage where TChunk : IChunkSize{ + +// // } +// public sealed class MemoryChunkStorage : IChunkStorage, IChunkContainerStorage where TChunkContainer : IChunkContainer where TChunkSize : IChunkSize +// { +// public bool IsReadOnly => false; +// private Dictionary, TChunkContainer> _containers = new(); +// public ValueTask>> GetChunkAsync(ChunkPos chunkPos) +// { +// return _containers[chunkPos].GetLayer() +// return new ValueTask>>(_chunks.TryGetValue(chunkPos, out var chunk) ? Option>.Some(chunk) : Option>.None); +// } + +// public ValueTask SetChunkAsync(ChunkPos chunkPos, IVoxelChunk chunk) +// { +// if (_containers.TryGetValue(chunkPos, out var container)) +// { +// container.AddLayer(chunk, nameof(TChunk)); +// } +// _chunks[chunkPos] = chunk; +// return ValueTask.CompletedTask; +// } + +// public async ValueTask FlushToAsync(IChunkStorage? target) +// { +// if (target is null) +// return; +// foreach (var chunkKvp in _chunks) +// { +// await target.SetChunkAsync(chunkKvp.Key, chunkKvp.Value); +// } +// } + +// public void Dispose() +// { + +// } + +// public ValueTask> GetChunkAsync(ChunkPos chunkPos, string layer) where TChunkLayer : IChunkLayer +// { +// if (_containers.TryGetValue(chunkPos, out var container)) +// { +// container.GetLayer(layer); +// } +// } + +// public ValueTask SetChunkAsync(ChunkPos chunkPos, TChunk chunk) where TChunkLayer : IChunkLayer +// { +// throw new NotImplementedException(); +// } + +// public ValueTask FlushToAsync(IChunkStorage target = null) +// { +// throw new NotImplementedException(); +// } + +// ValueTask> IChunkStorage.GetChunkAsync(ChunkPos chunkPos) +// { +// throw new NotImplementedException(); +// } + +// public ValueTask SetChunkAsync(ChunkPos chunkPos, object chunk) +// { +// throw new NotImplementedException(); +// } + +// public ValueTask FlushToAsync() +// { +// throw new NotImplementedException(); +// } + +// public ValueTask LoadContainerAsync(ChunkPos position) +// { +// throw new NotImplementedException(); +// } + +// public ValueTask SaveContainerAsync(TChunkContainer container) +// { +// throw new NotImplementedException(); +// } +// } + +// public sealed class LayerChunkStorage : IChunkStorage +// { +// private readonly IChunkStorage[] _layers; +// private bool disposedValue; + +// public bool IsReadOnly => _layers.All(c=>c.IsReadOnly); +// public LayerChunkStorage(params IChunkStorage[] chunkStorages) +// { +// this._layers = chunkStorages; +// } +// public async ValueTask>> GetChunkAsync(ChunkPos chunkPos) +// { +// foreach (var layer in _layers) +// { +// var chunk = await layer.GetChunkAsync(chunkPos); +// if (chunk.HasValue) +// return chunk; + +// } +// return Option>.None; +// } + +// public async ValueTask SetChunkAsync(ChunkPos chunkPos, IVoxelChunk chunk) +// { +// var writable = _layers.FirstOrDefault(l => !l.IsReadOnly); +// if (writable is not null) +// await writable.SetChunkAsync(chunkPos, chunk); +// } + +// public async ValueTask FlushToAsync(IChunkStorage target) +// { +// for (int i = 0; i < _layers.Length - 1; i++) +// { +// var current = _layers[i]; +// var next = _layers[i + 1]; +// if (!current.IsReadOnly && !next.IsReadOnly) +// { +// await current.FlushToAsync(next); +// } +// } +// } + +// private void Dispose(bool disposing) +// { +// if (!disposedValue) +// { +// if (disposing) +// { +// // TODO: dispose managed state (managed objects) +// foreach (var layer in _layers) +// { +// layer.Dispose(); +// } +// } + +// // TODO: free unmanaged resources (unmanaged objects) and override finalizer +// // TODO: set large fields to null +// disposedValue = true; +// } +// } + +// // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources +// // ~LayerChunkStorage() +// // { +// // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method +// // Dispose(disposing: false); +// // } + +// public void Dispose() +// { +// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method +// Dispose(disposing: true); +// GC.SuppressFinalize(this); +// } +// } +public static class WorldBuilder +{ + public static VoxelWorldPaletteBuilder Create() where TChunkSize : IChunkSize => new(); +} +public class VoxelWorldPaletteBuilder where TChunkSize : IChunkSize +{ + + public WorldBuilderChunkFactory> WithoutPalette() => new(new NoPalettePolicy()); + public WorldBuilderChunkFactory> WithGlobalPalette(IVoxelRegistry registry) where TDeff : IVoxelDefinition where TAttribute : class where TVoxel :IVoxelInstance => new(new GlobalPalettePolicy(registry)); + + +} +public class WorldBuilderChunkFactory where TPalettePolicy : struct, IPalettePolicy where TChunkSize : IChunkSize +{ + private readonly TPalettePolicy palettePolicy; + + public WorldBuilderChunkFactory(TPalettePolicy palettePolicy) + { + this.palettePolicy = palettePolicy; + } + public VoxelWorldChunkBuilder UseChunk() where TChunk : IVoxelChunk, new() => new(palettePolicy); + // public VoxelWorldChunkBuilder UseChunk(Action> builderAction) where TChunk : IVoxelChunk, new() + // { + // var builder = new ChunkFactoryBuilder(); + // builderAction(builder); + + // return new(palettePolicy, builder.Build()); + // } +} +public class VoxelWorldChunkBuilder where TChunkSize : IChunkSize where TPalettePolicy : struct, IPalettePolicy where TChunk : IVoxelChunk//where TVoxelChunk : IVoxelChunk// where TWorldBoundPolicy : IWorldBoundPolicy +{ + private readonly TPalettePolicy palettePolicy; + + + private Func? _factory; + private IDataSerializer? _serializer; + public VoxelWorldChunkBuilder WithSerializer(IDataSerializer serializer) + { + _serializer = serializer; + return this; + } + public VoxelWorldChunkBuilder WithFactory(Func factory) + { + _factory = factory; + return this; + } + // private readonly TChunkStore chunkStore; + + public VoxelWorldChunkBuilder(TPalettePolicy palettePolicy)//, TChunkStore chunkStore) + { + this.palettePolicy = palettePolicy; + // this.chunkStore = chunkStore; + } + // public VoxelWorldBoundBuilder, TVoxelChunk> WithChunkManger( IChunkManger chunkManger) /*where TChunkManger : IChunkManger */where TVoxelChunk : IVoxelChunk where TDataStore : IDataStore, IVoxelChunk>, IFlushableStore, IVoxelChunk> + // { + // // var c = new(TwoTierStoreFactory.For,IVoxelChunk>().MemoryCached(dataStore)); + // return new(palettePolicy,chunkManger); + // } + public VoxelWorldBoundBuilder, TChunk, + MemoryDataLayer, TChunk>>, TChunk>, TChunk> WithChunkManger(IDataStore, byte[]> dataStore) /*where TChunkManger : IChunkManger *///where TVoxelChunk : IVoxelChunk + { + + if (_factory is null) + { + var constructer = typeof(TChunk).GetConstructor(Type.EmptyTypes); + if (constructer is null) + { + throw new InvalidOperationException( + $"Chunk type {typeof(TChunk).Name} must supply a factory — no parameterless constructor." + ); + } + _factory = () => Activator.CreateInstance(); + } + + + + + IDataStore, TChunk> datas = new DataStoreSerializer, TChunk, byte[]>(dataStore, _serializer); + TwoTierStore, TChunk, MemoryDataLayer, TChunk>> store = + TwoTierStoreFactory.For, TChunk>().MemoryCached(datas); + var chunkManger = new ChunkManger, TChunk, + MemoryDataLayer, TChunk>>, TChunk>(store, _factory); + // var c = new(TwoTierStoreFactory.For,IVoxelChunk>().MemoryCached(dataStore)); + return new(palettePolicy, chunkManger); + } +} + +public class VoxelWorldBoundBuilder where TChunkSize : IChunkSizewhere TPalettePolicy : struct, IPalettePolicy where TChunkManger : IChunkManger where TVoxelChunk : IVoxelChunk +{ + private readonly TPalettePolicy palettePolicy; + private readonly TChunkManger chunkManger; + + public VoxelWorldBoundBuilder(TPalettePolicy palettePolicy,TChunkManger chunkManger) + { + this.palettePolicy = palettePolicy; + this.chunkManger = chunkManger; + } + private List worldServices = new(); + public VoxelWorldBoundBuilder WithService(IWorldService service) + { + worldServices.Add(service); + return this; + } + public VoxelWorldBoundBuilder WithChunkService(out ChunkService chunkService) + { + chunkService = new ChunkService(chunkManger); + return this; + } + public World WithLimitedSize(int maxWidth, int maxHeight, int maxDepth) => new(palettePolicy, chunkManger, new LimitedWorldSize(maxWidth, maxHeight, maxDepth), worldServices); + public World WithUnlimitedSize() => new(palettePolicy,chunkManger,new UnLimitedWorldSize(),worldServices); + +} +public readonly struct NoPalettePolicy : IPalettePolicy +{ + // public IVoxelResolver CreateResolver() + // => new DirectVoxelResolver(); // no lookup, direct voxel type + public TVoxel GetId(TVoxel voxel) => voxel; + + public TVoxel Resolve(TVoxel value) => value; +} +public readonly struct GlobalPalettePolicy : IPalettePolicy where TAtrubite : class where TDeff : IVoxelDefinition where TValue : IVoxelInstance +{ + private readonly IVoxelRegistry _registry; + public GlobalPalettePolicy(IVoxelRegistry registry) => _registry = registry; + + public THandle GetId(TValue voxel) => _registry.GetOrAddEntry((TDeff)voxel.Definition); + + public TValue Resolve(THandle value) => _registry.Get(value); + // public IVoxelResolver CreateResolver() + // => new GlobalVoxelResolver(_registry); +} + +// internal class GlobalVoxelResolver(IVoxelRegistry registry) : IVoxelResolver where THandle : IVoxelHandle +// { +// public IVoxelRegistry Registry { get; } = registry; + + +// public THandle GetId(TVoxel voxel) => Registry.GetOrAddEntry(voxel); + +// public TVoxel Resolve(THandle value) => Registry.Get(value); +// } + +// internal class DirectVoxelResolver : IVoxelResolver +// { +// public TVoxel GetId(TVoxel voxel) => voxel; + +// public TVoxel Resolve(TVoxel value) => value; +// } + +public interface IVoxelRegistry where TDeff : IVoxelDefinition where TAttrubite : class +{ + public TVoxel Get(THandle handle); + public TDeff GetDeff(THandle handle); + // public THandle GetOrAddEntry(TVoxel entry); + public THandle GetOrAddEntry(TDeff entry); + + // public THandle ModifyHandle(THandle handle, Func modify); +} +public readonly struct AxisBounds +{ + public int Min { get; } + public int Max { get; } + public bool IsInfinite { get; } + + public AxisBounds(int min, int max) :this(min, max, false){} + private AxisBounds(int min, int max, bool isInfinite) + { + Min = min; + Max = max; + IsInfinite = isInfinite; + } + + public static AxisBounds Infinite() => new AxisBounds(0, 0,true); +} + +public readonly struct WorldBounds +{ + + public AxisBounds X { get; } + public AxisBounds Y { get; } + public AxisBounds Z { get; } + public WorldBounds(AxisBounds x, AxisBounds y, AxisBounds z) + { + X = x; Y = y; Z = z; + } + public static WorldBounds Infinite() => new WorldBounds(AxisBounds.Infinite(), AxisBounds.Infinite(), AxisBounds.Infinite()); + public static WorldBounds InfiniteXZ(int min, int max) => new WorldBounds(AxisBounds.Infinite(), new AxisBounds(min, max), AxisBounds.Infinite()); +} +public interface IWorldService +{ + void EnableService(IWorld world); + void DisableService(); + void Tick(); + +} +public class ChunkService : IWorldService where TChunkSize : IChunkSize where TChunk : IVoxelChunk +{ + + private readonly IChunkManger chunkManger; + private IWorld world; + public void DisableService() + { + throw new NotImplementedException(); + } + + public void EnableService(IWorld world) + { + this.world = world; + } + + public void Tick() + { + throw new NotImplementedException(); + } + public ChunkService(IChunkManger chunkManger) + { + this.chunkManger = chunkManger; + } + public Option GetLoadedChunk(ChunkPos chunkPos) + { + return chunkManger.TryGetChunk(chunkPos); + } + public IEnumerable<(ChunkPos, TChunk)> GetLoadedChunks() + { + return chunkManger.GetLoadedChunks(); + } +} +public interface IWorld +{ + bool TryGetService(out T service) where T : class, IWorldService; +} +public interface IWorld : IWorld +{ + TVoxel GetVoxel(VoxelPos voxelPos); + // ValueTask GetVoxelAysnc(VoxelPos voxelPos); + void SetVoxel(VoxelPos voxelPos, TVoxel voxel); + // ValueTask SetVoxelAysnc(VoxelPos voxelPos, TVoxel voxel); + // bool TryGetVoxel(VoxelPos voxelPos, out TVoxel voxel); + WorldBounds WorldBounds { get; } + +} +public interface IWorldChunkApi where TChunkSize : IChunkSize +{ + // IWorldChunk GetChunk(ChunkPos chunkPos); +} + +public class World : IWorld, IWorldChunkApi where TChunkSize : IChunkSize where TVoxel : IEquatable +{ + public WorldBounds WorldBounds => new (new(0,5),new(0,5),new(0,5)); + public World(Func> factory,TVoxel @default) + { + this.factory = factory; + this.@default = @default; + } + Dictionary> chunks = []; + public ChunkManger,ChunkIndex3D> ChunkManger; + public IEnumerable<(ChunkIndex3D,IWorldChunk)> GetVoxelChunks() => ChunkManger.GetLoadedChunks(); + // { + // foreach (var item in chunks) + // { + // yield return (ChunkPos.FromKey(item.Key),item.Value); + // } + // } + private readonly Func> factory; + private readonly TVoxel @default; + + public TVoxel GetVoxel(VoxelPos voxelPos) + { + return ChunkManger.GetLoadedChunk(ChunkIndex3D.FromGlobal(voxelPos.X,voxelPos.Y,voxelPos.Z)).Match(chunk=>chunk.GetVoxelAt(voxelPos.GetVoxelPos()),()=>@default); + if (chunks.TryGetValue(ChunkPos.FromGlobal(voxelPos.X,voxelPos.Y,voxelPos.Z).ToKey(),out var chunk)) + { + return chunk.GetVoxelAt(voxelPos.GetVoxelPos()); + } + return @default; + } + + public void SetVoxel(VoxelPos voxelPos, TVoxel voxel) + { + if (!chunks.TryGetValue(ChunkIndex3D.FromGlobal(voxelPos.X,voxelPos.Y,voxelPos.Z).ToKey(),out var chunk)) + { + chunks[ChunkIndex3D.FromGlobal(voxelPos.X,voxelPos.Y,voxelPos.Z).ToKey()] = chunk = factory(ChunkIndex3D.FromGlobal(voxelPos.X,voxelPos.Y,voxelPos.Z));//factory(); + chunk.ChunkPos = voxelPos.GetChunkPos(); + } + chunk.SetVoxelAt(voxelPos.GetVoxelPos(),voxel); + } + + bool IWorld.TryGetService(out T service) + { + throw new NotImplementedException(); + } + +} +public class ChunkManger where TChunkPos : struct, IChunkPos +{ + private readonly IChunkGenerator _chunkGenerator; + private readonly IChunkStorage _chunkStorage; + private readonly TwoTierStore> _dataStore; + // private readonly Dictionary _tickets = []; + + public ChunkManger(IChunkGenerator chunkGenerator, IChunkStorage chunkStorage, TwoTierStore> dataStore) + { + _chunkGenerator = chunkGenerator; + _chunkStorage = chunkStorage; + _dataStore = dataStore; + } + public async ValueTask> GetChunk(TChunkPos chunkPos) + { + var chunk = _dataStore.ReadAsync(chunkPos); + await chunk; + + // _chunkMeta[chunkPos].IsLoaded = true; + return Option.Some(chunk.Result.OrDefault(_chunkGenerator.GenerateChunk(chunkPos))); + } + public Option GetLoadedChunk(TChunkPos chunkPos) + { + return _dataStore.TryGetData(chunkPos); + } + public async ValueTask SetChunk(TChunkPos chunkPos, TChunk chunk) + { + await _dataStore.WriteAsync(chunkPos,chunk); + } + public async ValueTask LoadChunks(params TChunkPos[] chunks) + { + foreach (var item in chunks) + { + await _dataStore.LoadChunkToMemory(item,(c)=> _chunkGenerator.GenerateChunk(c)); + } + } + List> LoadingChunks = []; + public void Update() + { + //LoadChunksAroundTickets(); + foreach (var item in _tickets) + { + for (int x = 0; x < item.Radius; x++) + { + for (int y = 0; y < item.Radius; y++) + { + for (int z = 0; z < item.Radius; z++) + { + var chunk = _dataStore.TryGetData(item.Position + TChunkPos.Create(x,y,z)); + if (!chunk.HasValue) + { + LoadingChunks.Add(Task.Run(()=>_chunkGenerator.GenerateChunk(item.Position + TChunkPos.Create(x,y,z)))); + } + } + } + } + } + foreach (var item in LoadingChunks.ToArray()) + { + if (item.IsCompleted) + { + + } + } + } + public IEnumerable<(TChunkPos,TChunk)> GetLoadedChunks() => _dataStore.GetLoadedData(); + public void AddTicket(ChunkTicket ticket) => _tickets.Add(ticket); +public record ChunkTicket(TChunkPos Position, int Radius); +} +public interface IChunkTicket : IDisposable +{ + IChunkPos Position { get; set; } + + int Radius { get; set; } + + // ChunkTicketShape Shape { get; set; } + + // ChunkTicketRules Rules { get; set; } +} +public interface IChunkShape +{ + IEnumerable GetChunks( + IChunkPos center, + int radius); +} +public class ChunkTicket +{ + +} +public interface IChunkGenerator where TChunkPos : struct, IChunkPos +{ + TChunk GenerateChunk(TChunkPos chunkPos); +} +public record ChunkFuncGen(Func Factory) : IChunkGenerator where TChunkPos : struct, IChunkPos +{ + public TChunk GenerateChunk(TChunkPos chunkPos) + { + return Factory(chunkPos); + } +} + +public interface IChunkStorage +{ + Task> Load(IChunkPos coord); + Task Save(IChunkPos position,TChunk chunk); +} + +public class World : IWorld + where TPalettePolicy : struct, IPalettePolicy + where TWorldPolicy : struct, IWorldBoundPolicy + where TChunkManger : IChunkManger + + where TChunkSize : IChunkSize + where TVoxelChunk : IVoxelChunk +{ + private readonly TPalettePolicy palettePolicy; + private readonly TWorldPolicy worldPolicy; + private readonly List services; + public World(TPalettePolicy palettePolicy, TChunkManger chunkManger, TWorldPolicy worldPolicy, List worldServices = null) + { + this.palettePolicy = palettePolicy; + ChunkManger = chunkManger; + this.worldPolicy = worldPolicy; + services = worldServices; + } + + public TChunkManger ChunkManger { get; } + + public WorldBounds WorldBounds => new WorldBounds( + worldPolicy.InfiniteX?AxisBounds.Infinite():new (0,worldPolicy.MaxWidth), + worldPolicy.InfiniteY?AxisBounds.Infinite():new (0,worldPolicy.MaxHeight), + worldPolicy.InfiniteZ?AxisBounds.Infinite():new (0,worldPolicy.MaxDepth)); + + // public TPalettePolicy PalettePolicy { get; } + public async ValueTask GetChunkAsync(ChunkPos chunkPos) + { + return await ChunkManger.GetChunkAsync(chunkPos); + } + public async ValueTask SetChunkAsync(ChunkPos chunkPos, TVoxelChunk chunk) + { + await ChunkManger.SetChunkAsync(chunkPos, chunk); + } + public async ValueTask GetVoxelAsync(VoxelPos voxelPos) + { + var chunk = await GetChunkAsync(voxelPos.GetChunkPos()); + var voxel = chunk.GetVoxel(voxelPos.GetVoxelPos()); + return palettePolicy.Resolve(voxel); + + + } + public async ValueTask SetVoxelAsync(VoxelPos voxelPos, TVoxel voxelDeff) + { + var chunk = await GetChunkAsync(voxelPos.GetChunkPos()); + var handel = palettePolicy.GetId(voxelDeff); + chunk.SetVoxel(voxelPos.GetVoxelPos(), handel); + await SetChunkAsync(voxelPos.GetChunkPos(),chunk); + + + } + + public TVoxel GetVoxel(VoxelPos voxelPos) + { + return Task.Run(() => GetVoxelAsync(voxelPos)).GetAwaiter().GetResult().Result; + } + + public void SetVoxel(VoxelPos voxelPos, TVoxel voxel) + { + Task.Run(() => SetVoxelAsync(voxelPos,voxel)).GetAwaiter().GetResult(); + } + + + bool IWorld.TryGetService(out T service) + { + foreach (var item in services) + { + if (item is T s) + { + service = s; + return true; + } + } + service = null; + return false; + } +} + +public interface IWorldBoundPolicy +{ + bool InfiniteX { get; } + bool InfiniteY { get; } + bool InfiniteZ { get; } + int MaxWidth { get; } + int MaxHeight { get; } + int MaxDepth { get; } +} +public readonly struct LimitedWorldSize(int maxWidth, int maxHeight, int maxDepth) : IWorldBoundPolicy +{ + public bool InfiniteX => false; + + public bool InfiniteY => false; + + public bool InfiniteZ => false; + + public int MaxWidth => maxWidth; + + public int MaxHeight => maxHeight; + + public int MaxDepth => maxDepth; +} +public readonly struct UnLimitedWorldSize() : IWorldBoundPolicy +{ + public bool InfiniteX => true; + + public bool InfiniteY => true; + + public bool InfiniteZ => true; + + public int MaxWidth => -1; + + public int MaxHeight => -1; + + public int MaxDepth => -1; +} +public interface IPalettePolicy +{ + // IVoxelResolver CreateResolver(IVoxelRegistry? global); + // IVoxelResolver CreateResolver(); + TVoxel Resolve(THandle value); + THandle GetId(TVoxel voxel); +} + +// public interface IVoxelResolver +// { +// TVoxel Resolve(THandel value); +// THandel GetId(TVoxel voxel); +// } \ No newline at end of file diff --git a/src/voxelgame/Voxels/Worlds/VoxelWorld.cs.uid b/src/voxelgame/Voxels/Worlds/VoxelWorld.cs.uid new file mode 100644 index 0000000..4139522 --- /dev/null +++ b/src/voxelgame/Voxels/Worlds/VoxelWorld.cs.uid @@ -0,0 +1 @@ +uid://dtg4km6ikm0qf diff --git a/src/voxelgame/Voxels/clip_plane.gdshader b/src/voxelgame/Voxels/clip_plane.gdshader new file mode 100644 index 0000000..1e5f45e --- /dev/null +++ b/src/voxelgame/Voxels/clip_plane.gdshader @@ -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. +//} diff --git a/src/voxelgame/Voxels/clip_plane.gdshader.uid b/src/voxelgame/Voxels/clip_plane.gdshader.uid new file mode 100644 index 0000000..37ca0b4 --- /dev/null +++ b/src/voxelgame/Voxels/clip_plane.gdshader.uid @@ -0,0 +1 @@ +uid://b0dpogulmm2p7 diff --git a/src/voxelgame/addons/Developer Console/Attributes/ConsoleCommandAttribute.cs b/src/voxelgame/addons/Developer Console/Attributes/ConsoleCommandAttribute.cs new file mode 100644 index 0000000..73637bc --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Attributes/ConsoleCommandAttribute.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Attributes/ConsoleCommandAttribute.cs.uid b/src/voxelgame/addons/Developer Console/Attributes/ConsoleCommandAttribute.cs.uid new file mode 100644 index 0000000..bf74894 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Attributes/ConsoleCommandAttribute.cs.uid @@ -0,0 +1 @@ +uid://pducp5spu5en diff --git a/src/voxelgame/addons/Developer Console/Color Theme/DCColorTheme.cs b/src/voxelgame/addons/Developer Console/Color Theme/DCColorTheme.cs new file mode 100644 index 0000000..4e7e544 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Color Theme/DCColorTheme.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Godot; + +namespace hamsterbyte.DeveloperConsole; + +public static class DCColorTheme{ + public static readonly Dictionary 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 Suggestions = new(){ + { "Method", Colors.DeepSkyBlue.ToHtml() }, + { "Type", Colors.ForestGreen.ToHtml() } + }; +} + +public struct RegionColor{ + public string Start; + public string End; + public Color Color; +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Color Theme/DCColorTheme.cs.uid b/src/voxelgame/addons/Developer Console/Color Theme/DCColorTheme.cs.uid new file mode 100644 index 0000000..937aa70 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Color Theme/DCColorTheme.cs.uid @@ -0,0 +1 @@ +uid://baxk18v2sa2l5 diff --git a/src/voxelgame/addons/Developer Console/Command Interpreter/CommandInterpreter.cs b/src/voxelgame/addons/Developer Console/Command Interpreter/CommandInterpreter.cs new file mode 100644 index 0000000..4b4f9ae --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Command Interpreter/CommandInterpreter.cs @@ -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 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 _commands = new(); + public static Dictionary Commands => _commands; + public static DCDelegates.onConsoleCommandsUpdated OnGetStaticCommands; + + private static Dictionary _instanceCommands = new(); + public static Dictionary InstanceCommands => _instanceCommands; + public static DCDelegates.onConsoleCommandsUpdated OnGetInstanceCommands; + + private static Dictionary _staticCommands = new(); + public static Dictionary StaticCommands => _staticCommands; + + private static void GetStaticCommands(out Dictionary 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(); + return commandAttribute?.Prefix ?? string.Empty; + }) + .ThenBy(method => method.Name) + .ToDictionary(method => { + ConsoleCommandAttribute commandAttribute = method.GetCustomAttribute(); + string prefix = commandAttribute?.Prefix != string.Empty + ? commandAttribute?.Prefix + '.' + : string.Empty; + return $"{prefix}{method.Name}"; + }); + + OnGetStaticCommands?.Invoke(commands); + } + + private static void GetInstanceCommands(out Dictionary 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(); + 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 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 + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Command Interpreter/CommandInterpreter.cs.uid b/src/voxelgame/addons/Developer Console/Command Interpreter/CommandInterpreter.cs.uid new file mode 100644 index 0000000..a36a08c --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Command Interpreter/CommandInterpreter.cs.uid @@ -0,0 +1 @@ +uid://cfu0pu0m1m22j diff --git a/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/ChangeContext.cs b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/ChangeContext.cs new file mode 100644 index 0000000..aa152ce --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/ChangeContext.cs @@ -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 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(); + 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 + ); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/ChangeContext.cs.uid b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/ChangeContext.cs.uid new file mode 100644 index 0000000..a1bcc82 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/ChangeContext.cs.uid @@ -0,0 +1 @@ +uid://bk1byj8vgtc4e diff --git a/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Help.cs b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Help.cs new file mode 100644 index 0000000..34951d0 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Help.cs @@ -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 commands, string searchString = ""){ + foreach (KeyValuePair c in commands){ + string description = c.Value.GetCustomAttribute()?.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 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); + } + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Help.cs.uid b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Help.cs.uid new file mode 100644 index 0000000..085f0ff --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Help.cs.uid @@ -0,0 +1 @@ +uid://uajav2gns4to diff --git a/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Shortcuts.cs b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Shortcuts.cs new file mode 100644 index 0000000..4a92759 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Shortcuts.cs @@ -0,0 +1,10 @@ +using System; + +namespace hamsterbyte.DeveloperConsole; + +public partial class CommandInterpreter{ + private static readonly (char Character, Action Command)[] _shortcuts ={ + new('/', ChangeContext), + new('?', GetHelp) + }; +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Shortcuts.cs.uid b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Shortcuts.cs.uid new file mode 100644 index 0000000..44ef242 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Command Interpreter/Shortcuts/Shortcuts.cs.uid @@ -0,0 +1 @@ +uid://xdjd61gnvv5g diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Application.cs b/src/voxelgame/addons/Developer Console/Console Commands/Application.cs new file mode 100644 index 0000000..60877c0 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Application.cs @@ -0,0 +1,11 @@ +namespace hamsterbyte.DeveloperConsole; + +public partial class DC{ + /// + /// Quit the game + /// + [ConsoleCommand(Prefix = "Application", Description = "Quit the game")] + private static void Quit(){ + Instance.GetTree().Quit(); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Application.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Application.cs.uid new file mode 100644 index 0000000..e7b046f --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Application.cs.uid @@ -0,0 +1 @@ +uid://cjkfon8d3lvm4 diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Command.cs b/src/voxelgame/addons/Developer Console/Console Commands/Command.cs new file mode 100644 index 0000000..230de2b --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Command.cs @@ -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(); + } + +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Command.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Command.cs.uid new file mode 100644 index 0000000..6d2f00a --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Command.cs.uid @@ -0,0 +1 @@ +uid://bqf0fhngjcpf diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Console Output.cs b/src/voxelgame/addons/Developer Console/Console Commands/Console Output.cs new file mode 100644 index 0000000..0019b94 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Console Output.cs @@ -0,0 +1,11 @@ +namespace hamsterbyte.DeveloperConsole; + +public static partial class OutputCommands{ + /// + /// Clear the output console + /// + [ConsoleCommand(Prefix = "Output", Description = "Clear all output and command history from the console")] + private static void Clear(){ + DC.OnClear?.Invoke(); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Console Output.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Console Output.cs.uid new file mode 100644 index 0000000..86d42f2 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Console Output.cs.uid @@ -0,0 +1 @@ +uid://bp8n8x32b1x5w diff --git a/src/voxelgame/addons/Developer Console/Console Commands/ConsoleCommand.cs b/src/voxelgame/addons/Developer Console/Console Commands/ConsoleCommand.cs new file mode 100644 index 0000000..a59881a --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/ConsoleCommand.cs @@ -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 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; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/ConsoleCommand.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/ConsoleCommand.cs.uid new file mode 100644 index 0000000..f1b3759 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/ConsoleCommand.cs.uid @@ -0,0 +1 @@ +uid://lsnwo7ujgwf0 diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Context.cs b/src/voxelgame/addons/Developer Console/Console Commands/Context.cs new file mode 100644 index 0000000..326e9d0 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Context.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Godot; + +namespace hamsterbyte.DeveloperConsole{ + public partial class DC{ + /// + /// Print paths to immediate children of context + /// + [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."); + } + } + + /// + /// Print all paths to all children in context, includes internal + /// + [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"); + } + } + + /// + /// Print all node paths in the scene tree + /// + [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(); + } + } + + /// + /// Recursively search children of the current node for a child with a given name + /// If found, set current node to search result + /// + /// Name of the node. Case Sensitive + [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."); + } + + /// + /// 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 + /// + /// Name of the node. Case Sensitive + [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 FindIn(string name, Node context){ + Queue _treeQueue = new(); + List 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; + } + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Context.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Context.cs.uid new file mode 100644 index 0000000..2a0b0e2 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Context.cs.uid @@ -0,0 +1 @@ +uid://dfl1b0g22wbck diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Display.cs b/src/voxelgame/addons/Developer Console/Console Commands/Display.cs new file mode 100644 index 0000000..9f2067a --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Display.cs @@ -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()); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Display.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Display.cs.uid new file mode 100644 index 0000000..75ba0d1 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Display.cs.uid @@ -0,0 +1 @@ +uid://iavcf5e0axvd diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Engine.cs b/src/voxelgame/addons/Developer Console/Console Commands/Engine.cs new file mode 100644 index 0000000..45a60c2 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Engine.cs @@ -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 +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Engine.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Engine.cs.uid new file mode 100644 index 0000000..9258a13 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Engine.cs.uid @@ -0,0 +1 @@ +uid://dnq1yjcsndmtg diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Level.cs b/src/voxelgame/addons/Developer Console/Console Commands/Level.cs new file mode 100644 index 0000000..72968d6 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Level.cs @@ -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 _sceneInfos = new(); + + // ReSharper disable once MemberCanBePrivate.Global + public static List 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(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(SceneInfos[index].Path); + DC.CurrentNode.AddChild(p.Instantiate()); + OnSceneLoaded?.InvokeAwaited(); + } + catch (Exception e){ + throw new DCException($"Failed to load level {e.Message}"); + } + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Level.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Level.cs.uid new file mode 100644 index 0000000..d32473a --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Level.cs.uid @@ -0,0 +1 @@ +uid://b2amvjqvxb3gn diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Math.cs b/src/voxelgame/addons/Developer Console/Console Commands/Math.cs new file mode 100644 index 0000000..8af45bb --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Math.cs @@ -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 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); + } + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Math.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Math.cs.uid new file mode 100644 index 0000000..b1d4436 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Math.cs.uid @@ -0,0 +1 @@ +uid://cy4g13lccfbll diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Nodes.cs b/src/voxelgame/addons/Developer Console/Console Commands/Nodes.cs new file mode 100644 index 0000000..116da16 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Nodes.cs @@ -0,0 +1,122 @@ +using System; +using System.Reflection; +using Godot; + +namespace hamsterbyte.DeveloperConsole{ + public abstract partial class Nodes{ + public delegate void onNodeDestroyed(Node n); + + public static onNodeDestroyed OnNodeDestroyed; + + #region GET + + [ConsoleCommand(Prefix = "Node", Description = "Print the global position of the currently selected node.")] + private static Vector2 GetPosition(){ + PropertyInfo p = DC.CurrentNode.GetType().GetProperty("GlobalPosition"); + if (p is null){ + throw new DCException("This node has no global position property."); + } + return (Vector2)p.GetValue(DC.CurrentNode)!; + } + + #endregion + + #region DESTROY + + [ConsoleCommand(Prefix = "Node", Description = + "Destroy the node at the given path. Cannot Destroy /root. If node is current context, set context to parent node")] + private static void DestroyAt(string path){ + if (path == "/root") throw new DCException("Cannot destroy root."); + if (path == DC.CurrentNode.GetPath()){ + DC.ChangeContext(DC.CurrentNode.GetParent().GetPath()); + } + + Node n = DC.Root.GetNodeOrNull(path); + OnNodeDestroyed?.Invoke(n); + n.QueueFree(); + DC.Print($"Node destroyed at '{path}'"); + } + + [ConsoleCommand(Prefix = "Node", Description = + "Destroy node at the current context. Cannot Destroy /root. Set current context to parent node")] + private static void Destroy(){ + if (DC.CurrentNode.GetPath() == "/root") throw new DCException("Cannot destroy root."); + Node t = DC.CurrentNode; + DC.ChangeContext(t.GetParent().GetPath()); + OnNodeDestroyed?.Invoke(t); + t.QueueFree(); + DC.Print($"Node destroyed at '{t.GetPath()}'"); + } + + #endregion + + #region MOVE + + [ConsoleCommand(Prefix = "Node", Description = "Move the current context node by the specified amount")] + private static void Move(int x, int y){ + try{ + Vector2 move = new(x, y); + switch (DC.CurrentNode){ + case Control control: + control.GlobalPosition += move; + DC.Print(control.GlobalPosition); + break; + case Node2D node2D: + node2D.GlobalPosition += move; + DC.Print(node2D.GlobalPosition); + break; + default: + throw new DCException("Current context node is not a Control or Node2D"); + } + } + catch (Exception e){ + throw new DCException(e.Message); + } + } + + [ConsoleCommand(Prefix = "Node", Description = "Move the current context node to the specified position")] + private static void SetPosition(int x, int y){ + try{ + Vector2 newPos = new(x, y); + switch (DC.CurrentNode){ + case Control control: + control.GlobalPosition = newPos - control.PivotOffset; + DC.Print(control.GlobalPosition); + break; + case Node2D node2D: + node2D.GlobalPosition = newPos; + DC.Print(node2D.GlobalPosition); + break; + default: + throw new DCException("Current context node is not a Control or Node2D"); + } + } + catch (Exception e){ + throw new DCException(e.Message); + } + } + + [ConsoleCommand(Prefix = "Node", Description = "Move the current context node to the mouse position")] + private static void MoveToMouse(){ + try{ + switch (DC.CurrentNode){ + case Control control: + control.GlobalPosition = control.GetGlobalMousePosition() - control.PivotOffset; + DC.Print(control.GlobalPosition); + break; + case Node2D node2D: + node2D.GlobalPosition = node2D.GetGlobalMousePosition(); + DC.Print(node2D.GlobalPosition); + break; + default: + throw new DCException("Current context node is not a Control or Node2D"); + } + } + catch (Exception e){ + throw new DCException(e.Message); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Nodes.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Nodes.cs.uid new file mode 100644 index 0000000..4b51d5e --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Nodes.cs.uid @@ -0,0 +1 @@ +uid://cdvnf0vjvcxxm diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Prefab.cs b/src/voxelgame/addons/Developer Console/Console Commands/Prefab.cs new file mode 100644 index 0000000..9af053c --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Prefab.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using Godot; + +namespace hamsterbyte.DeveloperConsole; + +public static class Prefab{ + public static DCDelegates.onPrefabInstantiated OnPrefabInstantiated; + private static List _prefabInfos = new(); + + // ReSharper disable once MemberCanBePrivate.Global + public static List PrefabInfos{ + get{ + if (_prefabInfos.Count == 0){ + InitializePrefabInfos(); + } + + return _prefabInfos; + } + } + + private static void InitializePrefabInfos(){ + DC.Print("Initializing Prefabs List..."); + Resources.GetResourceInformation(ResourcePaths.Prefabs, ref _prefabInfos); + } + + [ConsoleCommand(Prefix = "Prefab", Description = "Print a list of all prefabs and their pertinent information")] + private static void GetAll(){ + for (int i = 0; i < PrefabInfos.Count; i++){ + DC.Print($"ID:: {i.ToString().SetWidth(20)}{PrefabInfos[i]}"); + } + } + + [ConsoleCommand(Prefix = "Prefab", + Description = "Print a list of all prefabs with a matching name. Not case sensitive")] + private static void Find(string name){ + for (int i = 0; i < PrefabInfos.Count; i++){ + if (PrefabInfos[i].Name.ToLower().Contains(name.ToLower())){ + DC.Print($"ID:: {i.ToString().SetWidth(20)}{PrefabInfos[i]}"); + } + } + } + + [ConsoleCommand(Prefix = "Prefab", + Description = "Instantiate a prefab as a child of the scene root positioned at the mouse")] + private static void Instantiate(int index){ + try{ + PackedScene p = ResourceLoader.Load(PrefabInfos[index].Path); + Node n = p.Instantiate(); + DC.CurrentNode.AddChild(n); + switch (n){ + case Node2D node2D: + node2D.GlobalPosition = node2D.GetGlobalMousePosition(); + break; + case Control control: + control.GlobalPosition = control.GetGlobalMousePosition(); + break; + } + + DCCrosshair.Nodes.Add(n); + OnPrefabInstantiated?.InvokeAwaited(n); + } + catch (Exception e){ + throw new DCException($"Failed to instantiate prefab {e.Message}"); + } + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Prefab.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Prefab.cs.uid new file mode 100644 index 0000000..305d2a9 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Prefab.cs.uid @@ -0,0 +1 @@ +uid://cyruwluvuxikf diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Resources.cs b/src/voxelgame/addons/Developer Console/Console Commands/Resources.cs new file mode 100644 index 0000000..b55c8c2 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Resources.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using Godot; + +namespace hamsterbyte.DeveloperConsole; + +public static class Resources{ + public static void GetAllDirectories(string path){ + if (DirAccess.DirExistsAbsolute(path)){ + foreach (string dir in DirAccess.Open(path).GetDirectories()){ + DC.Print($"{path}/{dir}"); + DC.IncreaseIndent(); + GetAllDirectories($"{path}/{dir}"); + DC.DecreaseIndent(); + } + } + else{ + throw new DCException($"Requested directory '{path}' does not exist"); + } + } + + private static readonly List LastFilesList = new(); + + public static void GetFiles(string path){ + if (DirAccess.DirExistsAbsolute(path)){ + string[] fileNames = DirAccess.Open($"{path}").GetFiles(); + for (int i = 0; i < fileNames.Length; i++){ + if (fileNames[i].EndsWith(".remap")){ + fileNames[i] = fileNames[i].TrimEnd(".remap".ToCharArray()); + } + + LastFilesList.Add($"{path}/{fileNames[i]}"); + } + + foreach (string dir in DirAccess.Open(path).GetDirectories()){ + GetFiles($"{path}/{dir}"); + } + } + else{ + throw new DCException($"Requested directory '{path}' does not exist"); + } + } + + public static void GetResourceInformation(string path, ref List resourceInformations){ + char[] splitChars ={ '.', '/' }; + LastFilesList.Clear(); + resourceInformations.Clear(); + GetFiles(path); + resourceInformations.AddRange(Resources.LastFilesList.Select(t => + new ResourceInformation(t.Split(splitChars)[^2], t))); + } +} + +public readonly struct ResourceInformation{ + public readonly string Name; + public readonly string Path; + + public ResourceInformation(string name, string path){ + Name = name; + Path = path; + } + + public override string ToString(){ + return $"Name:: {Name.SetWidth(64)}Path:: {Path.SetWidth(200)}"; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Console Commands/Resources.cs.uid b/src/voxelgame/addons/Developer Console/Console Commands/Resources.cs.uid new file mode 100644 index 0000000..6371b2b --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Console Commands/Resources.cs.uid @@ -0,0 +1 @@ +uid://e1a2bjcyv7b8 diff --git a/src/voxelgame/addons/Developer Console/ConsoleCamera.cs b/src/voxelgame/addons/Developer Console/ConsoleCamera.cs new file mode 100644 index 0000000..eb65ca4 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/ConsoleCamera.cs @@ -0,0 +1,87 @@ +using Godot; +using System; +using System.Collections.Generic; +using hamsterbyte.DeveloperConsole; + +public partial class ConsoleCamera : Camera2D, ICanInitialize{ + private readonly List _cameraInformations = new(); + + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"ConsoleCamera => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + SetupCallbacks(); + GetAllCameras(); + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } + + private void SetupCallbacks(){ + DC.OnModeChanged += SwapControl; + Level.OnSceneLoaded += GetAllCameras; + } + + private void SwapControl(int mode){ + if (mode == 1){ + //Crosshair + TakeControl(); + } + else{ + ReturnControl(); + } + } + + private void GetAllCameras(){ + _cameraInformations.Clear(); + foreach (Node n in DCCrosshair.Nodes){ + if (n is Camera2D camera2D){ + _cameraInformations.Add(new CameraInformation(){ + Camera = camera2D, + Enabled = camera2D.Enabled, + IsCurrent = camera2D.IsCurrent() + }); + } + } + } + + + private void TakeControl(){ + Enabled = true; + MakeCurrent(); + foreach (CameraInformation info in _cameraInformations){ + if (!CameraIsValid(info)) continue; + info.Camera.Enabled = false; + } + } + + private void ReturnControl(){ + Enabled = false; + foreach (CameraInformation info in _cameraInformations){ + if (!CameraIsValid(info)) continue; + info.Camera.Enabled = info.Enabled; + if (info.IsCurrent) info.Camera.MakeCurrent(); + } + } + + private bool CameraIsValid(CameraInformation info){ + if (!IsInstanceValid(info.Camera)){ + _cameraInformations.Remove(info); + return false; + } + + return true; + } + + private struct CameraInformation{ + public Camera2D Camera; + public bool Enabled; + public bool IsCurrent; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/ConsoleCamera.cs.uid b/src/voxelgame/addons/Developer Console/ConsoleCamera.cs.uid new file mode 100644 index 0000000..3939acc --- /dev/null +++ b/src/voxelgame/addons/Developer Console/ConsoleCamera.cs.uid @@ -0,0 +1 @@ +uid://b53thoxw2a11l diff --git a/src/voxelgame/addons/Developer Console/DC.cs b/src/voxelgame/addons/Developer Console/DC.cs new file mode 100644 index 0000000..b86c20c --- /dev/null +++ b/src/voxelgame/addons/Developer Console/DC.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Godot; + +namespace hamsterbyte.DeveloperConsole{ + public static partial class DC{ + public static DCDelegates.onCommandSubmitted OnCommandSubmitted; + public static DCDelegates.onPrint OnPrint; + public static DCDelegates.onModeChanged OnModeChanged; + public static DCDelegates.onCallback OnClear; + public static DCDelegates.onCallback OnCommandCompleted; + public static DCDelegates.onCallback OnEnable; + public static DCDelegates.onCallback OnDisable; + public static DCDelegates.onToggle OnToggle; + public static DCDelegates.onCallback OnInitialized; + public static DCDelegates.onIncrementProgress OnIncrementProgress; + public static DCDelegates.onShowProgress OnShowProgress; + public static int Mode{ get; private set; } + + public static Node Root => Instance.GetTree().Root; + public static DeveloperConsoleUI Instance{ get; private set; } + public static bool Enabled{ get; private set; } + + #region WRAPPERS + + public static Node CurrentNode => CommandInterpreter.CurrentNode; + public static (Node CurrentNode, Dictionary Commands) Context => CommandInterpreter.Context; + + public static void ChangeContext(string path) => CommandInterpreter.ChangeContext(path); + + #endregion + + #region INITIALIZE + + public static void Initialize(DeveloperConsoleUI instance){ + DC.SetSuppressionLevel(PrintSuppression.GD); + bool success = TryInitialize(instance); + PrintComment(" ------------------ Initializing System ------------------ "); + Print($"Developer Console => {success.OKFail()}"); + if (success) CommandInterpreter.Initialize(); + } + + private static bool TryInitialize(DeveloperConsoleUI instance){ + try{ + Instance = instance; + if (!CheckDebugEnabled()){ + Instance.QueueFree(); + return false; + } + + InitializeCallbacks(); + if (instance.PrintLicense){ + SetSuppressionLevel(PrintSuppression.GD); + foreach (string s in License.Text){ + Print(s); + } + + SetSuppressionLevel(Instance.PrintSuppression); + } + + OnInitialized?.Invoke(); + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } + + private static bool CheckDebugEnabled(){ + return Instance.AccessLevel switch{ + DCAccessLevel.None => false, + DCAccessLevel.Debug => OS.IsDebugBuild(), + DCAccessLevel.WithArg => OS.IsDebugBuild() || OS.GetCmdlineArgs().Contains("-developer"), + DCAccessLevel.All => true, + _ => false + }; + } + + private static void InitializeCallbacks(){ + CommandInterpreter.OnContextChanged += ChangeContext; + } + + #endregion + + private static int previousMode; + + public static void SetEnabled(bool enabled){ + Enabled = enabled; + if (enabled){ + ChangeMode(previousMode); + OnEnable?.InvokeAwaited(); + } + else{ + previousMode = Mode; + ChangeMode(-1); + OnDisable?.InvokeAwaited(); + } + + OnToggle?.InvokeAwaited(Enabled); + } + + public static void ChangeMode(int mode){ + Mode = mode; + OnModeChanged?.Invoke(mode); + } + + private static void ChangeContext((Node node, Dictionary commands) context){ + SetIndentLevel(0); + Print($"${context.node.GetPath().ToString()?.TrimStart('/')}::"); + } + + + /// + /// Parse the command string, if a valid command is found invoke it + /// If the string is a path, update the current object to the path + /// + /// + public static async void Submit(string commandString){ + Print($"{CommandInterpreter.CurrentNode.PromptString()} {commandString}"); + OnCommandSubmitted?.Invoke(commandString); + IncreaseIndent(); + await CommandInterpreter.Interpret(commandString); + DecreaseIndent(); + OnCommandCompleted?.InvokeAwaited(); + } + + #region PRINT + + private static int _indentLevel; + private static PrintSuppression _suppressionLevel; + + public enum PrintSuppression{ + None, + DC, + GD, + All + } + + public static void SetSuppressionLevel(PrintSuppression suppressionLevel){ + _suppressionLevel = suppressionLevel; + } + + // ReSharper disable once MemberCanBePrivate.Global + public static void SetIndentLevel(int level){ + _indentLevel = Math.Clamp(level, 0, int.MaxValue); + } + + public static void IncreaseIndent(){ + _indentLevel++; + } + + public static void DecreaseIndent(){ + _indentLevel = Math.Max(0, _indentLevel - 1); + } + + public static void ShowProgress(int operationCount){ + OnShowProgress?.Invoke(operationCount); + } + + public static void IncrementProgress(int i = 1){ + OnIncrementProgress?.Invoke(i); + } + + /// + /// Print a line to the developer console + /// + /// + public static void Print(object o){ + if (Instance is null || _suppressionLevel == PrintSuppression.All){ + return; // Print nothing if the instance is null or all is suppressed + } + + if (_suppressionLevel != PrintSuppression.GD){ + PrintOutputToGD(o); + } + + if (_suppressionLevel != PrintSuppression.DC){ + PrintOutputToDC(o); + } + + } + + public static void PrintError(object o){ + Print($"#! {o} !#"); + } + + public static void PrintComment(object o){ + Print($"/* {o} */"); + } + + private static void PrintOutputToDC(object o){ + string output = _indentLevel > 0 + ? new string('\t', _indentLevel) + o + : o.ToString(); + OnPrint?.Invoke(output); + } + + private static void PrintOutputToGD(object o){ + GD.Print(o.ToString()); + } + + #endregion + } + + public enum DCAccessLevel{ + None, + Debug, + WithArg, + All + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/DC.cs.uid b/src/voxelgame/addons/Developer Console/DC.cs.uid new file mode 100644 index 0000000..c7f3e78 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/DC.cs.uid @@ -0,0 +1 @@ +uid://bv87d2e35mgpl diff --git a/src/voxelgame/addons/Developer Console/DCCrosshair.cs b/src/voxelgame/addons/Developer Console/DCCrosshair.cs new file mode 100644 index 0000000..6226aab --- /dev/null +++ b/src/voxelgame/addons/Developer Console/DCCrosshair.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Godot; + +namespace hamsterbyte.DeveloperConsole; + +public static partial class DCCrosshair{ + public static List Nodes{ get; } = new(); + public static DCDelegates.onCallback OnNodesUpdated; + + #region INITALIZE + + public static void Initialize(){ + bool success = TryInitialize(); + DC.Print($"Crosshair Mode => {success.OKFail()}"); + DC.Instance.InitializeUI(); + } + + private static bool TryInitialize(){ + try{ + DC.SetSuppressionLevel(DC.PrintSuppression.All); + GetAllNodes(); + Level.OnSceneLoaded += GetAllNodes; + DC.SetSuppressionLevel(DC.PrintSuppression.GD); + return true; + } + catch{ + return false; + } + } + + #endregion + + private static void AddNode(Node node){ + if (!Nodes.Contains(node)){ + Nodes.Add(node); + } + } + + public static void GetAllNodes(){ + Nodes.Clear(); + Nodes.Add(DC.Root); + DC.ChangeContext(DC.Root.GetPath()); + if (DC.Root.GetChildCount() != 0){ + GetChildNodes(DC.Root); + } + else{ + throw new DCException("Tree contains no nodes"); + } + + OnNodesUpdated?.InvokeAwaited(); + } + + private static void GetChildNodes(Node targetNode){ + if (targetNode.GetChildCount() <= 0) return; + foreach (Node node in targetNode.GetChildren()){ + if (node.GetPath().ToString()!.Contains("DeveloperConsole")) continue; + AddNode(node); + GetChildNodes(node); + } + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/DCCrosshair.cs.uid b/src/voxelgame/addons/Developer Console/DCCrosshair.cs.uid new file mode 100644 index 0000000..8baaf79 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/DCCrosshair.cs.uid @@ -0,0 +1 @@ +uid://b6rygwrqdbq diff --git a/src/voxelgame/addons/Developer Console/DCDelegates.cs b/src/voxelgame/addons/Developer Console/DCDelegates.cs new file mode 100644 index 0000000..6dd1750 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/DCDelegates.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Reflection; +using Godot; + +namespace hamsterbyte.DeveloperConsole; + +public abstract class DCDelegates{ + public delegate void onConsoleCommandsUpdated(Dictionary commands); + + public delegate void onPrint(string message); + + public delegate void onCallback(); + + public delegate void onPrefabInstantiated(Node n); + + public delegate void onContextChanged((Node, Dictionary) context); + + public delegate void onCommandSubmitted(string commandString); + + public delegate void onConsoleToggled(bool toggled); + + public delegate void onModeChanged(int mode); + + public delegate void onToggle(bool b); + + public delegate void onIncrementProgress(int i); + + public delegate void onShowProgress(int i); + +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/DCDelegates.cs.uid b/src/voxelgame/addons/Developer Console/DCDelegates.cs.uid new file mode 100644 index 0000000..0522380 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/DCDelegates.cs.uid @@ -0,0 +1 @@ +uid://ggusn0uldr6l diff --git a/src/voxelgame/addons/Developer Console/DCExtensions.cs b/src/voxelgame/addons/Developer Console/DCExtensions.cs new file mode 100644 index 0000000..54c38ce --- /dev/null +++ b/src/voxelgame/addons/Developer Console/DCExtensions.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Godot; + +namespace hamsterbyte.DeveloperConsole; + +public static partial class DCExtensions{ + public static readonly Dictionary TypeReplacementStrings = new(){ + { "Boolean", "bool" }, + { "Byte", "byte" }, + { "SByte", "sbyte" }, + { "Int16", "short" }, + { "UInt16", "ushort" }, + { "Int32", "int" }, + { "UInt32", "uint" }, + { "Int64", "long" }, + { "UInt64", "ulong" }, + { "Char", "char" }, + { "Double", "double" }, + { "Single", "float" }, + { "String", "string" } + }; + + public static string BaselessString(this Type t){ + string[] types = t.ToString().Split('.'); + return types[^1]; + } + + public static bool IsLetter(this Key k){ + int value = (int)k; + return value is > 64 and < 91; + } + + public static string ParamsAsString(this MethodInfo m){ + ParameterInfo[] infos = m.GetParameters(); + + if (infos.Length == 0) + return string.Empty; + + return string.Join(", ", infos.Select(info => + $"{info.ParameterType.BaselessString()} {info.Name}")); + } + + public static string SetWidth(this string s, int desiredWidth){ + return $"{s.PadRight(Math.Clamp(desiredWidth - s.Length, 0, 1000))}\t"; + } + + public static int Wrap(this int value, int max){ + int remainder = value % Math.Max(max, 1); + return remainder < 0 ? max + remainder : remainder; + } + + public static string GetPath(this TreeItem treeItem){ + Stack pathComponents = new Stack(); + + while (treeItem.GetParent() is not null){ + pathComponents.Push(treeItem.GetText(0)); + treeItem = treeItem.GetParent(); + } + + pathComponents.Push("root"); + + return "/" + string.Join("/", pathComponents); + } + + public static async Task InvokeDelayed(this Delegate d, int milliseconds, params object[] objects){ + await Task.Delay(milliseconds); + d.DynamicInvoke(objects); + } + + + /// + /// Used to invoke a callback after 2 frames + /// Waiting for two frames ensures that any deferred calls from the frame of this invocation will have been executed + /// + /// Delegate to invoke + /// Parameters + public static async Task InvokeAwaited(this Delegate d, params object[] objects){ + double currentFrame = Engine.GetProcessFrames(); + while (Engine.GetProcessFrames() < currentFrame + 2){ + await Task.Delay(1); + } + + d.DynamicInvoke(objects); + } + + /// + /// Used to invoke a method after a certain number of frames + /// By default it will wait for 2 frames to ensure that all deferred calls have been executed + /// + /// + /// + /// + public static async Task InvokeAwaited(this Delegate d, int frames, params object[] objects){ + double currentFrame = Engine.GetProcessFrames(); + while (Engine.GetProcessFrames() < currentFrame + frames){ + await Task.Delay(1); + } + + d.DynamicInvoke(objects); + } + + public static string OKFail(this bool b) => b ? "Ok" : "Fail"; + + public static string PromptString(this Node n) => $"${n.GetPath().ToString()?.TrimStart('/')}::"; + + public static void PrintToDC(this Exception e){ + DC.PrintError($"{e.GetType().BaselessString()}: {e.Message}"); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/DCExtensions.cs.uid b/src/voxelgame/addons/Developer Console/DCExtensions.cs.uid new file mode 100644 index 0000000..7d2203f --- /dev/null +++ b/src/voxelgame/addons/Developer Console/DCExtensions.cs.uid @@ -0,0 +1 @@ +uid://betu4qg4d5qo7 diff --git a/src/voxelgame/addons/Developer Console/DeveloperConsole.tscn b/src/voxelgame/addons/Developer Console/DeveloperConsole.tscn new file mode 100644 index 0000000..6d4b3ae --- /dev/null +++ b/src/voxelgame/addons/Developer Console/DeveloperConsole.tscn @@ -0,0 +1,541 @@ +[gd_scene load_steps=39 format=3 uid="uid://de0qg8ke135ln"] + +[ext_resource type="Script" uid="uid://d16xfkut4jb8q" path="res://addons/Developer Console/UI/DeveloperConsoleUI.cs" id="1_8rpjy"] +[ext_resource type="Script" uid="uid://78w6vo48jhkk" path="res://addons/Developer Console/UI/DCCrosshairUI.cs" id="2_gxyvi"] +[ext_resource type="Texture2D" uid="uid://dxglbj3p103dn" path="res://addons/Developer Console/Icons/GuiVisibilityVisible.svg" id="3_y56vd"] +[ext_resource type="Texture2D" uid="uid://bm7v31morg1hu" path="res://addons/Developer Console/Icons/GuiVisibilityHidden.svg" id="4_e4mbv"] +[ext_resource type="Script" uid="uid://c21tsjwrs2rt6" path="res://addons/Developer Console/UI/Crosshair/UICrosshairViewer.cs" id="5_lgojw"] +[ext_resource type="Script" uid="uid://d35etcbwvps6t" path="res://addons/Developer Console/UI/Crosshair/UIContextTree.cs" id="5_ly5ja"] +[ext_resource type="Script" uid="uid://xxumgcjl45vb" path="res://addons/Developer Console/UI/Terminal/UITerminalOutput.cs" id="5_pj6ps"] +[ext_resource type="Script" uid="uid://bbo12ubsbcvg7" path="res://addons/Developer Console/UI/Crosshair/UICommandTree.cs" id="6_8kmlh"] +[ext_resource type="Script" uid="uid://c12ic5e05em8y" path="res://addons/Developer Console/UI/Command Prompt/UIContextLabel.cs" id="6_56g1j"] +[ext_resource type="Script" uid="uid://bci1jn6uxja4q" path="res://addons/Developer Console/UI/Crosshair/UIMousePositionLabel.cs" id="6_qjser"] +[ext_resource type="Script" uid="uid://dtxlhxhv0uon8" path="res://addons/Developer Console/UI/Command Prompt/UIAutocompleteController.cs" id="7_t12mj"] +[ext_resource type="Script" uid="uid://bigrfj3yu5ysj" path="res://addons/Developer Console/UI/Crosshair/UICrosshairOutputLabel.cs" id="8_c1pwn"] +[ext_resource type="Script" uid="uid://ckf3tex7kjvu4" path="res://addons/Developer Console/UI/Command Prompt/UICommandInput.cs" id="8_xv854"] +[ext_resource type="Script" uid="uid://cl3n3mavjh2nx" path="res://addons/Developer Console/UI/Command Prompt/UIModeToggle.cs" id="9_u5wy4"] +[ext_resource type="Script" uid="uid://bejppl34qchoe" path="res://addons/Developer Console/UI/Command Prompt/UIDescriptionLabel.cs" id="10_a6cop"] +[ext_resource type="Script" uid="uid://yys1bb8k1fi" path="res://addons/Developer Console/UI/Command Prompt/UIAsyncProgress.cs" id="11_g35o5"] +[ext_resource type="Script" uid="uid://b53thoxw2a11l" path="res://addons/Developer Console/ConsoleCamera.cs" id="16_4fo3w"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_8uk8v"] +bg_color = Color(0.113281, 0.132813, 0.160156, 1) +draw_center = false +border_width_right = 10 +border_color = Color(0.113281, 0.132813, 0.160156, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6bgup"] +content_margin_left = 10.0 +content_margin_right = 10.0 +bg_color = Color(0.113281, 0.132813, 0.160156, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_o2nwv"] +content_margin_left = 5.0 +content_margin_top = 5.0 +content_margin_right = 5.0 +content_margin_bottom = 5.0 +bg_color = Color(0.113281, 0.132813, 0.160156, 1) +border_color = Color(0.171875, 0.171875, 0.171875, 1) +expand_margin_right = 9.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t7vms"] +content_margin_left = 5.0 +content_margin_top = 5.0 +content_margin_right = 5.0 +content_margin_bottom = 5.0 +bg_color = Color(0.210938, 0.238281, 0.289063, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ex8m8"] +content_margin_left = 5.0 +content_margin_top = 5.0 +content_margin_right = 5.0 +content_margin_bottom = 5.0 +bg_color = Color(0.128906, 0.148438, 0.179688, 1) +border_color = Color(0.172549, 0.172549, 0.172549, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dbr1y"] +bg_color = Color(0.128906, 0.148438, 0.179688, 1) +draw_center = false +border_color = Color(0.172549, 0.172549, 0.172549, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nuqj3"] +bg_color = Color(0.128906, 0.148438, 0.179688, 1) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_48syn"] +content_margin_left = 10.0 +content_margin_right = 10.0 +bg_color = Color(0.128906, 0.148438, 0.179688, 1) +corner_radius_top_left = 20 +corner_radius_top_right = 20 +corner_radius_bottom_right = 20 +corner_radius_bottom_left = 20 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_25xm1"] +draw_center = false +border_color = Color(0.113281, 0.132813, 0.160156, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fymlk"] +bg_color = Color(0.113725, 0.133333, 0.160784, 1) +draw_center = false +border_width_left = 10 +border_width_top = 10 +border_width_right = 10 +border_width_bottom = 10 +border_color = Color(0.113281, 0.132813, 0.160156, 1) +corner_radius_top_left = 15 +corner_radius_top_right = 15 +corner_radius_bottom_right = 15 +corner_radius_bottom_left = 15 +expand_margin_left = 8.0 +expand_margin_top = 6.0 +expand_margin_right = 8.0 +expand_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_178bo"] +content_margin_left = 10.0 +content_margin_top = 10.0 +content_margin_right = 10.0 +content_margin_bottom = 10.0 +bg_color = Color(0, 0, 0, 0.501961) +border_width_top = 1 +border_color = Color(0.171875, 0.171875, 0.171875, 1) +expand_margin_left = 10.0 +expand_margin_right = 10.0 + +[sub_resource type="CodeHighlighter" id="CodeHighlighter_apkny"] +number_color = Color(1, 1, 1, 1) +symbol_color = Color(1, 1, 1, 1) +function_color = Color(0, 0.603922, 0.898039, 1) +member_variable_color = Color(1, 1, 1, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_og0jc"] +bg_color = Color(0, 0, 0, 1) +border_width_top = 2 +border_width_bottom = 2 +border_color = Color(0.171875, 0.171875, 0.171875, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_26ouh"] +bg_color = Color(0, 0.611765, 0.898039, 1) +border_width_top = 2 +border_width_bottom = 2 +border_color = Color(0.171875, 0.171875, 0.171875, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ctx6f"] +bg_color = Color(0.0546875, 0.0546875, 0.0546875, 1) +border_width_top = 1 +border_color = Color(0.172549, 0.172549, 0.172549, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_21jej"] +content_margin_left = 10.0 +bg_color = Color(0.0546875, 0.0546875, 0.0546875, 1) +expand_margin_top = 6.0 +expand_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dkxti"] +content_margin_left = 5.0 +content_margin_top = 4.0 +bg_color = Color(0.054902, 0.054902, 0.054902, 1) +expand_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ie522"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(0.0313726, 0.0313726, 0.0313726, 0) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7thol"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(0.0313726, 0.0313726, 0.0313726, 0) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jwkeu"] +content_margin_left = 1.0 +content_margin_right = 4.0 +bg_color = Color(0.054902, 0.054902, 0.054902, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_color = Color(0.171875, 0.171875, 0.171875, 1) +expand_margin_left = 8.0 +expand_margin_top = 4.0 +expand_margin_right = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e55sf"] +content_margin_left = 15.0 +content_margin_top = 10.0 +content_margin_right = 15.0 +content_margin_bottom = 10.0 +bg_color = Color(0.054902, 0.054902, 0.054902, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.171875, 0.171875, 0.171875, 1) + +[node name="Developer Console" type="CanvasLayer" node_paths=PackedStringArray("_output")] +layer = 128 +visible = false +script = ExtResource("1_8rpjy") +_output = NodePath("Master VBox/Terminal Mode") + +[node name="Master VBox" type="VBoxContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = -1 + +[node name="Crosshair Mode" type="PanelContainer" parent="Master VBox"] +visible = false +custom_minimum_size = Vector2(960, 0) +layout_mode = 2 +size_flags_vertical = 3 +focus_next = NodePath("../Command Prompt/Command Margins/Command HBox/Autocomplete Controller/Command Input") +focus_previous = NodePath("../Command Prompt/Command Margins/Command HBox/Autocomplete Controller/Command Input") +mouse_filter = 2 +mouse_default_cursor_shape = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_8uk8v") +script = ExtResource("2_gxyvi") + +[node name="Crosshair VBox" type="VBoxContainer" parent="Master VBox/Crosshair Mode"] +layout_mode = 2 +theme_override_constants/separation = 0 + +[node name="Crosshair Upper Panel" type="PanelContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox"] +custom_minimum_size = Vector2(0, 32) +layout_mode = 2 +size_flags_vertical = 8 +theme_override_styles/panel = SubResource("StyleBoxFlat_6bgup") + +[node name="HBoxContainer" type="HBoxContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair Upper Panel"] +layout_mode = 2 + +[node name="Instructions" type="Label" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair Upper Panel/HBoxContainer"] +z_index = 1 +z_as_relative = false +y_sort_enabled = true +layout_mode = 2 +theme_override_colors/font_color = Color(0.694118, 0.694118, 0.694118, 1) +theme_override_font_sizes/font_size = 16 +text = "Crosshair Mode" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="Version" type="Label" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair Upper Panel/HBoxContainer"] +z_index = 1 +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.427451, 0.427451, 0.427451, 1) +theme_override_font_sizes/font_size = 16 +text = "Version 1.0b" +horizontal_alignment = 2 +vertical_alignment = 1 + +[node name="Crosshair HSplit" type="HSplitContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/separation = 0 +theme_override_constants/minimum_grab_thickness = 0 +split_offset = -640 + +[node name="Tree Panel" type="PanelContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_o2nwv") + +[node name="Tree VBox" type="VBoxContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel"] +layout_mode = 2 + +[node name="Tree VSplit" type="VSplitContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox"] +custom_minimum_size = Vector2(256, 0) +layout_mode = 2 +size_flags_vertical = 3 +split_offset = 128 + +[node name="Context Panel" type="PanelContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox/Tree VSplit"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_t7vms") + +[node name="Context VBox" type="VBoxContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox/Tree VSplit/Context Panel"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox/Tree VSplit/Context Panel/Context VBox"] +layout_mode = 2 + +[node name="Context Label" type="Label" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox/Tree VSplit/Context Panel/Context VBox/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Context" + +[node name="Context Tree" type="Tree" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox/Tree VSplit/Context Panel/Context VBox" node_paths=PackedStringArray("_consoleCamera", "_filter")] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_colors/guide_color = Color(1, 1, 1, 1) +theme_override_constants/draw_guides = 0 +theme_override_constants/draw_relationship_lines = 1 +theme_override_styles/panel = SubResource("StyleBoxFlat_ex8m8") +theme_override_styles/focus = SubResource("StyleBoxFlat_dbr1y") +theme_override_styles/selected = SubResource("StyleBoxFlat_dbr1y") +columns = 2 +allow_reselect = true +hide_root = true +script = ExtResource("5_ly5ja") +_visButtonTex = ExtResource("3_y56vd") +_invisButtonTex = ExtResource("4_e4mbv") +_consoleCamera = NodePath("../../../../../../../../../../Console Camera") +_filter = NodePath("../Context Filter") +metadata/_edit_use_anchors_ = true + +[node name="Context Filter" type="LineEdit" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox/Tree VSplit/Context Panel/Context VBox"] +layout_mode = 2 +theme_override_styles/focus = SubResource("StyleBoxFlat_nuqj3") +theme_override_styles/normal = SubResource("StyleBoxFlat_48syn") +placeholder_text = "Filter..." + +[node name="Command Panel" type="PanelContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox/Tree VSplit"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_t7vms") + +[node name="Command VBox" type="VBoxContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox/Tree VSplit/Command Panel"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Command Label" type="Label" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox/Tree VSplit/Command Panel/Command VBox"] +layout_mode = 2 +text = "Commands" + +[node name="Command Tree" type="Tree" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Tree Panel/Tree VBox/Tree VSplit/Command Panel/Command VBox" node_paths=PackedStringArray("_autocompleteController")] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_colors/guide_color = Color(1, 1, 1, 1) +theme_override_constants/draw_guides = 0 +theme_override_constants/draw_relationship_lines = 1 +theme_override_styles/panel = SubResource("StyleBoxFlat_ex8m8") +theme_override_styles/focus = SubResource("StyleBoxFlat_dbr1y") +theme_override_styles/selected = SubResource("StyleBoxFlat_dbr1y") +allow_reselect = true +hide_root = true +script = ExtResource("6_8kmlh") +_autocompleteController = NodePath("../../../../../../../../../Command Prompt/Command Margins/Command HBox/Autocomplete Controller") +metadata/_edit_use_anchors_ = true + +[node name="Crosshair Panel" type="PanelContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit"] +clip_contents = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_25xm1") + +[node name="Crosshair VBox" type="VBoxContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Crosshair Panel"] +layout_mode = 2 + +[node name="Crosshair Viewer" type="PanelContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Crosshair Panel/Crosshair VBox" node_paths=PackedStringArray("_consoleCamera")] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_fymlk") +script = ExtResource("5_lgojw") +_consoleCamera = NodePath("../../../../../../../Console Camera") + +[node name="Crosshair Lower Panel" type="PanelContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Crosshair Panel/Crosshair VBox"] +layout_mode = 2 +size_flags_vertical = 8 +theme_override_styles/panel = SubResource("StyleBoxFlat_6bgup") + +[node name="HBoxContainer" type="HBoxContainer" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Crosshair Panel/Crosshair VBox/Crosshair Lower Panel"] +layout_mode = 2 + +[node name="Output Label" type="Label" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Crosshair Panel/Crosshair VBox/Crosshair Lower Panel/HBoxContainer"] +layout_mode = 2 +text = "Output: " + +[node name="Crosshair Output Label" type="RichTextLabel" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Crosshair Panel/Crosshair VBox/Crosshair Lower Panel/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +bbcode_enabled = true +script = ExtResource("8_c1pwn") + +[node name="Mouse Position Label" type="Label" parent="Master VBox/Crosshair Mode/Crosshair VBox/Crosshair HSplit/Crosshair Panel/Crosshair VBox/Crosshair Lower Panel/HBoxContainer"] +layout_mode = 2 +text = "(0,0)" +script = ExtResource("6_qjser") + +[node name="Terminal Mode" type="CodeEdit" parent="Master VBox" node_paths=PackedStringArray("_commandInput")] +layout_mode = 2 +size_flags_vertical = 3 +focus_next = NodePath("../Command Prompt/Command Margins/Command HBox/Autocomplete Controller/Command Input") +focus_previous = NodePath("../Command Prompt/Command Margins/Command HBox/Autocomplete Controller/Command Input") +theme_override_colors/font_readonly_color = Color(1, 1, 1, 1) +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_styles/normal = SubResource("StyleBoxFlat_178bo") +theme_override_styles/focus = SubResource("StyleBoxFlat_178bo") +theme_override_styles/read_only = SubResource("StyleBoxFlat_178bo") +theme_override_colors/line_number_color = Color(0.305882, 0.305882, 0.305882, 1) +editable = false +deselect_on_focus_loss_enabled = false +drag_and_drop_selection_enabled = false +virtual_keyboard_enabled = false +middle_mouse_paste_enabled = false +scroll_smooth = true +minimap_width = 128 +syntax_highlighter = SubResource("CodeHighlighter_apkny") +line_folding = true +gutters_draw_line_numbers = true +gutters_zero_pad_line_numbers = true +gutters_draw_fold_gutter = true +indent_size = 10 +script = ExtResource("5_pj6ps") +_commandInput = NodePath("../Command Prompt/Command Margins/Command HBox/Autocomplete Controller/Command Input") + +[node name="Async Progress" type="ProgressBar" parent="Master VBox"] +visible = false +custom_minimum_size = Vector2(0, 8) +layout_mode = 2 +theme_override_styles/background = SubResource("StyleBoxFlat_og0jc") +theme_override_styles/fill = SubResource("StyleBoxFlat_26ouh") +value = 57.43 +show_percentage = false +script = ExtResource("11_g35o5") + +[node name="Command Prompt" type="Panel" parent="Master VBox"] +clip_contents = true +custom_minimum_size = Vector2(0, 43.455) +layout_mode = 2 +size_flags_vertical = 8 +theme_override_styles/panel = SubResource("StyleBoxFlat_ctx6f") + +[node name="Command Margins" type="MarginContainer" parent="Master VBox/Command Prompt"] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -26.0 +grow_horizontal = 2 +grow_vertical = 0 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Command HBox" type="HBoxContainer" parent="Master VBox/Command Prompt/Command Margins"] +layout_mode = 2 +size_flags_vertical = 8 +theme_override_constants/separation = 0 + +[node name="Context Label" type="Label" parent="Master VBox/Command Prompt/Command Margins/Command HBox"] +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_colors/font_color = Color(0.811765, 0.513726, 0.0313726, 1) +theme_override_styles/normal = SubResource("StyleBoxFlat_21jej") +text = "/root::" +vertical_alignment = 2 +script = ExtResource("6_56g1j") + +[node name="Autocomplete Controller" type="Control" parent="Master VBox/Command Prompt/Command Margins/Command HBox" node_paths=PackedStringArray("_contextLabel", "_autoCompleteLabel", "_autoCompleteSuggestions", "_autoCompleteDescription", "_commandInput")] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +script = ExtResource("7_t12mj") +_contextLabel = NodePath("../Context Label") +_autoCompleteLabel = NodePath("Autocomplete Label") +_autoCompleteSuggestions = NodePath("../../../../../Suggestions Label") +_autoCompleteDescription = NodePath("../../../../../Description Label") +_commandInput = NodePath("Command Input") + +[node name="Autocomplete Label" type="Label" parent="Master VBox/Command Prompt/Command Margins/Command HBox/Autocomplete Controller"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_colors/font_color = Color(0.345098, 0.345098, 0.345098, 1) +theme_override_styles/normal = SubResource("StyleBoxFlat_dkxti") +vertical_alignment = 1 + +[node name="Command Input" type="LineEdit" parent="Master VBox/Command Prompt/Command Margins/Command HBox/Autocomplete Controller" node_paths=PackedStringArray("_terminalMode", "_contextLabel", "_autocompleteController")] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_bottom = 4.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +focus_next = NodePath("../../../../../Terminal Mode") +focus_previous = NodePath(".") +theme_override_styles/focus = SubResource("StyleBoxFlat_ie522") +theme_override_styles/normal = SubResource("StyleBoxFlat_7thol") +placeholder_text = "Enter Console Command..." +clear_button_enabled = true +draw_control_chars = true +caret_blink = true +script = ExtResource("8_xv854") +_terminalMode = NodePath("../../../../../Terminal Mode") +_contextLabel = NodePath("../../Context Label") +_autocompleteController = NodePath("..") + +[node name="Mode Toggle" type="TextureButton" parent="Master VBox/Command Prompt/Command Margins/Command HBox" node_paths=PackedStringArray("_crosshairMode", "_terminalMode")] +layout_mode = 2 +toggle_mode = true +button_pressed = true +texture_normal = ExtResource("4_e4mbv") +texture_pressed = ExtResource("3_y56vd") +stretch_mode = 3 +script = ExtResource("9_u5wy4") +_crosshairMode = NodePath("../../../../Crosshair Mode") +_terminalMode = NodePath("../../../../Terminal Mode") + +[node name="Suggestions Label" type="RichTextLabel" parent="."] +visible = false +clip_contents = false +offset_left = 67.0 +offset_top = 1016.0 +offset_right = 131.0 +offset_bottom = 1040.0 +size_flags_vertical = 10 +theme_override_styles/normal = SubResource("StyleBoxFlat_jwkeu") +bbcode_enabled = true +fit_content = true +scroll_following = true +autowrap_mode = 0 +meta_underlined = false +hint_underlined = false +threaded = true + +[node name="Description Label" type="RichTextLabel" parent="."] +visible = false +custom_minimum_size = Vector2(256, 96) +offset_left = 1541.0 +offset_top = 715.0 +offset_right = 1797.0 +offset_bottom = 811.0 +pivot_offset = Vector2(-16, 74) +theme_override_styles/normal = SubResource("StyleBoxFlat_e55sf") +text = "3" +fit_content = true +meta_underlined = false +hint_underlined = false +script = ExtResource("10_a6cop") + +[node name="Console Camera" type="Camera2D" parent="."] +top_level = true +z_as_relative = false +enabled = false +editor_draw_screen = false +script = ExtResource("16_4fo3w") diff --git a/src/voxelgame/addons/Developer Console/Exceptions/DCException.cs b/src/voxelgame/addons/Developer Console/Exceptions/DCException.cs new file mode 100644 index 0000000..9e9e173 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Exceptions/DCException.cs @@ -0,0 +1,15 @@ +using System; + +namespace hamsterbyte.DeveloperConsole; + +public class DCException : Exception{ + protected string _message; + public override string Message => _message; + + protected DCException(){ + } + + public DCException(string message){ + _message = message; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Exceptions/DCException.cs.uid b/src/voxelgame/addons/Developer Console/Exceptions/DCException.cs.uid new file mode 100644 index 0000000..bacca3c --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Exceptions/DCException.cs.uid @@ -0,0 +1 @@ +uid://c86narbtpw0p8 diff --git a/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidCommandException.cs b/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidCommandException.cs new file mode 100644 index 0000000..0d1a3c1 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidCommandException.cs @@ -0,0 +1,8 @@ +namespace hamsterbyte.DeveloperConsole; + +public class DCInvalidCommandException : DCException{ + public DCInvalidCommandException(string commandString){ + _message = + $"Command '{commandString}' not recognized. Type ? for a list of commands or ?? for context specific commands. Commands are case sensitive."; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidCommandException.cs.uid b/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidCommandException.cs.uid new file mode 100644 index 0000000..264e836 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidCommandException.cs.uid @@ -0,0 +1 @@ +uid://r13irkayh1ey diff --git a/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidParameterFormatException.cs b/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidParameterFormatException.cs new file mode 100644 index 0000000..f0a5591 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidParameterFormatException.cs @@ -0,0 +1,9 @@ +using System; + +namespace hamsterbyte.DeveloperConsole; + +public class DCInvalidParameterFormatException : DCException{ + public DCInvalidParameterFormatException(string parameterString, Type t){ + _message = $"Invalid parameter format {t}.Parse('{parameterString}')"; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidParameterFormatException.cs.uid b/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidParameterFormatException.cs.uid new file mode 100644 index 0000000..beb1eca --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Exceptions/DCInvalidParameterFormatException.cs.uid @@ -0,0 +1 @@ +uid://btt2vpkjcqk87 diff --git a/src/voxelgame/addons/Developer Console/Exceptions/DCParameterMismatchException.cs b/src/voxelgame/addons/Developer Console/Exceptions/DCParameterMismatchException.cs new file mode 100644 index 0000000..c7a694f --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Exceptions/DCParameterMismatchException.cs @@ -0,0 +1,7 @@ +namespace hamsterbyte.DeveloperConsole; + +public class DCParameterMismatchException : DCException{ + public DCParameterMismatchException(string methodName, int expectedCount){ + _message = $"{methodName} expects {expectedCount} parameters."; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Exceptions/DCParameterMismatchException.cs.uid b/src/voxelgame/addons/Developer Console/Exceptions/DCParameterMismatchException.cs.uid new file mode 100644 index 0000000..4b80247 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Exceptions/DCParameterMismatchException.cs.uid @@ -0,0 +1 @@ +uid://40m7eb65uusg diff --git a/src/voxelgame/addons/Developer Console/Exceptions/DCParseFailureException.cs b/src/voxelgame/addons/Developer Console/Exceptions/DCParseFailureException.cs new file mode 100644 index 0000000..5b876a6 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Exceptions/DCParseFailureException.cs @@ -0,0 +1,9 @@ +using System; + +namespace hamsterbyte.DeveloperConsole; + +public class DCParseFailureException : DCException{ + public DCParseFailureException(Type t){ + _message = $"{t} has no method to parse from string"; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Exceptions/DCParseFailureException.cs.uid b/src/voxelgame/addons/Developer Console/Exceptions/DCParseFailureException.cs.uid new file mode 100644 index 0000000..f4882fc --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Exceptions/DCParseFailureException.cs.uid @@ -0,0 +1 @@ +uid://bfv5xl781ukuv diff --git a/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityHidden.svg b/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityHidden.svg new file mode 100644 index 0000000..3a5c959 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityHidden.svg @@ -0,0 +1 @@ + diff --git a/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityHidden.svg.import b/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityHidden.svg.import new file mode 100644 index 0000000..96bf474 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityHidden.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bm7v31morg1hu" +path="res://.godot/imported/GuiVisibilityHidden.svg-c752e77063f6335333264055284105e9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Developer Console/Icons/GuiVisibilityHidden.svg" +dest_files=["res://.godot/imported/GuiVisibilityHidden.svg-c752e77063f6335333264055284105e9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityVisible.svg b/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityVisible.svg new file mode 100644 index 0000000..18c2cbb --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityVisible.svg @@ -0,0 +1 @@ + diff --git a/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityVisible.svg.import b/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityVisible.svg.import new file mode 100644 index 0000000..d73808d --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Icons/GuiVisibilityVisible.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dxglbj3p103dn" +path="res://.godot/imported/GuiVisibilityVisible.svg-906939688cd00566a6f79055715ac4c0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Developer Console/Icons/GuiVisibilityVisible.svg" +dest_files=["res://.godot/imported/GuiVisibilityVisible.svg-906939688cd00566a6f79055715ac4c0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/voxelgame/addons/Developer Console/Interface/ICanInitialize.cs b/src/voxelgame/addons/Developer Console/Interface/ICanInitialize.cs new file mode 100644 index 0000000..04db6b7 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Interface/ICanInitialize.cs @@ -0,0 +1,6 @@ +namespace hamsterbyte.DeveloperConsole; + +public interface ICanInitialize{ + public void Initialize(); + public bool TryInitialize(); +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Interface/ICanInitialize.cs.uid b/src/voxelgame/addons/Developer Console/Interface/ICanInitialize.cs.uid new file mode 100644 index 0000000..fdc856b --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Interface/ICanInitialize.cs.uid @@ -0,0 +1 @@ +uid://5cg1mqycfs5v diff --git a/src/voxelgame/addons/Developer Console/Interface/ICanReset.cs b/src/voxelgame/addons/Developer Console/Interface/ICanReset.cs new file mode 100644 index 0000000..b6b5c42 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Interface/ICanReset.cs @@ -0,0 +1,6 @@ +namespace hamsterbyte.DeveloperConsole; + +public interface ICanReset{ + public void Reset(); + public bool TryReset(); +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Interface/ICanReset.cs.uid b/src/voxelgame/addons/Developer Console/Interface/ICanReset.cs.uid new file mode 100644 index 0000000..4688b6a --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Interface/ICanReset.cs.uid @@ -0,0 +1 @@ +uid://xsyp5d8lpuk6 diff --git a/src/voxelgame/addons/Developer Console/LICENSE.txt b/src/voxelgame/addons/Developer Console/LICENSE.txt new file mode 100644 index 0000000..df37ba5 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/LICENSE.txt @@ -0,0 +1,21 @@ +Developer Console for Godot 4.x .NET version 1.0b +Copyright 2023 hamsterbyte +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/voxelgame/addons/Developer Console/License.cs b/src/voxelgame/addons/Developer Console/License.cs new file mode 100644 index 0000000..5b3f8a5 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/License.cs @@ -0,0 +1,27 @@ +namespace hamsterbyte.DeveloperConsole; + +public static class License{ + public static readonly string[] Text ={ + "Developer Console for Godot 4.x .NET version 1.0b", + "Copyright 2023 hamsterbyte\n", + + "MIT License", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files , the \"Software\", to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:\n", + + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.\n", + + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + }; +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/License.cs.uid b/src/voxelgame/addons/Developer Console/License.cs.uid new file mode 100644 index 0000000..6581515 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/License.cs.uid @@ -0,0 +1 @@ +uid://ocbi2wvwy8pa diff --git a/src/voxelgame/addons/Developer Console/ResourcePaths.cs b/src/voxelgame/addons/Developer Console/ResourcePaths.cs new file mode 100644 index 0000000..cbc4fc0 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/ResourcePaths.cs @@ -0,0 +1,6 @@ +namespace hamsterbyte.DeveloperConsole; + +public static class ResourcePaths{ + public const string Prefabs = "res://Prefabs"; + public const string Levels = "res://Levels"; +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/ResourcePaths.cs.uid b/src/voxelgame/addons/Developer Console/ResourcePaths.cs.uid new file mode 100644 index 0000000..ed91c37 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/ResourcePaths.cs.uid @@ -0,0 +1 @@ +uid://02sweqttgy2c diff --git a/src/voxelgame/addons/Developer Console/Scene Composer/ICompositionCommand.cs b/src/voxelgame/addons/Developer Console/Scene Composer/ICompositionCommand.cs new file mode 100644 index 0000000..68560ff --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Scene Composer/ICompositionCommand.cs @@ -0,0 +1,8 @@ +using Godot; + +namespace hamsterbyte.DeveloperConsole.Composition; + +public interface ICompositionCommand{ + public void Execute(); + public void Undo(); +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Scene Composer/ICompositionCommand.cs.uid b/src/voxelgame/addons/Developer Console/Scene Composer/ICompositionCommand.cs.uid new file mode 100644 index 0000000..31acd9a --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Scene Composer/ICompositionCommand.cs.uid @@ -0,0 +1 @@ +uid://ddrkdvo7q5ymy diff --git a/src/voxelgame/addons/Developer Console/Scene Composer/ReparentCommand.cs b/src/voxelgame/addons/Developer Console/Scene Composer/ReparentCommand.cs new file mode 100644 index 0000000..497b4ff --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Scene Composer/ReparentCommand.cs @@ -0,0 +1,41 @@ +using Godot; + +namespace hamsterbyte.DeveloperConsole.Composition; + +public class ReparentCommand : ICompositionCommand{ + private readonly Node node; + private readonly Node newParent; + private readonly Node oldParent; + + public ReparentCommand(Node n){ + node = n; + oldParent = node.GetParentOrNull(); + newParent = FindValidAncestor(); + } + + public void Execute(){ + if (node is null || newParent is null) return; + node.Reparent(newParent); + DC.Print($"{node.Name} reparented to {newParent.Name}"); + } + + public void Undo(){ + if (node is null || oldParent is null) return; + node.Reparent(oldParent); + DC.Print($"{node.Name} reparented to {oldParent.Name}"); + } + + private Node FindValidAncestor(){ + if (node is null) return null; + Node ancestor = node.GetParentOrNull(); + while (ancestor is not null){ + if (ancestor is not SubViewport and not SubViewportContainer){ + return ancestor; + } + + ancestor = ancestor.GetParentOrNull(); + } + + return DC.Root; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Scene Composer/ReparentCommand.cs.uid b/src/voxelgame/addons/Developer Console/Scene Composer/ReparentCommand.cs.uid new file mode 100644 index 0000000..07c660c --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Scene Composer/ReparentCommand.cs.uid @@ -0,0 +1 @@ +uid://ogensqwv4hey diff --git a/src/voxelgame/addons/Developer Console/Scene Composer/SceneComposer.cs b/src/voxelgame/addons/Developer Console/Scene Composer/SceneComposer.cs new file mode 100644 index 0000000..c37fe2d --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Scene Composer/SceneComposer.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using Godot; + +namespace hamsterbyte.DeveloperConsole.Composition; + +public static class SceneComposer{ + public static bool RequiresDecomposition => DCCrosshair.Nodes.Any(n => n is SubViewport or SubViewportContainer); + private static bool IsDecomposed; + private static readonly Stack _decomposeHistory = new(); + private static readonly Stack _composeHistory = new(); + public static DCDelegates.onCallback OnCompositionChanged; + + public static void Compose(){ + if (!IsDecomposed) return; + DC.PrintComment($"Composing Scene"); + while (_composeHistory.Count > 0){ + ICompositionCommand current = _composeHistory.Pop(); + current.Undo(); + } + + IsDecomposed = false; + DC.ChangeContext("/root"); + OnCompositionChanged?.InvokeAwaited(); + } + + public static void Decompose(){ + if (!RequiresDecomposition || IsDecomposed) return; + DC.PrintComment("Decomposing Scene"); + foreach (Node n in DCCrosshair.Nodes){ + switch (n){ + case SubViewportContainer subViewportContainer: + _decomposeHistory.Push(new SetInvisibleCommand(subViewportContainer)); + break; + case SubViewport subViewport: + foreach (Node child in subViewport.GetChildren()){ + _decomposeHistory.Push(new ReparentCommand(child)); + } + break; + } + } + + while (_decomposeHistory.Count > 0){ + ICompositionCommand current = _decomposeHistory.Pop(); + _composeHistory.Push(current); + current.Execute(); + } + + IsDecomposed = true; + DC.ChangeContext("/root"); + OnCompositionChanged?.InvokeAwaited(); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Scene Composer/SceneComposer.cs.uid b/src/voxelgame/addons/Developer Console/Scene Composer/SceneComposer.cs.uid new file mode 100644 index 0000000..5a0f09a --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Scene Composer/SceneComposer.cs.uid @@ -0,0 +1 @@ +uid://d1281vbqao01g diff --git a/src/voxelgame/addons/Developer Console/Scene Composer/SetInvisibleCommand.cs b/src/voxelgame/addons/Developer Console/Scene Composer/SetInvisibleCommand.cs new file mode 100644 index 0000000..706bb02 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Scene Composer/SetInvisibleCommand.cs @@ -0,0 +1,25 @@ +using Godot; + +namespace hamsterbyte.DeveloperConsole.Composition; + +public class SetInvisibleCommand : ICompositionCommand{ + private readonly SubViewportContainer _subViewportContainer; + private readonly bool _previousVisibility; + + public SetInvisibleCommand(SubViewportContainer container){ + _subViewportContainer = container; + _previousVisibility = container.Visible; + } + + public void Execute(){ + if (_subViewportContainer is null) return; + _subViewportContainer.Visible = false; + DC.Print($"{_subViewportContainer.Name} visibility set to: false"); + } + + public void Undo(){ + if (_subViewportContainer is null) return; + _subViewportContainer.Visible = _previousVisibility; + DC.Print($"{_subViewportContainer.Name} visibility set to: {_previousVisibility}"); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/Scene Composer/SetInvisibleCommand.cs.uid b/src/voxelgame/addons/Developer Console/Scene Composer/SetInvisibleCommand.cs.uid new file mode 100644 index 0000000..bdd1ef1 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/Scene Composer/SetInvisibleCommand.cs.uid @@ -0,0 +1 @@ +uid://guiviblfbg27 diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAsyncProgress.cs b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAsyncProgress.cs new file mode 100644 index 0000000..bf9160b --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAsyncProgress.cs @@ -0,0 +1,51 @@ +using Godot; +using System; +using hamsterbyte.DeveloperConsole; + +public partial class UIAsyncProgress : ProgressBar, ICanInitialize{ + public void Initialize(){ + bool success = TryInitialize(); + if (success) DC.Print($"UIAsyncProgress => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + SetupCallbacks(); + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + + return true; + } + + private void SetupCallbacks(){ + DC.OnShowProgress += ShowProgress; + DC.OnIncrementProgress += IncrementProgress; + ValueChanged += Complete; + Command.OnCancel += Cancel; + } + + private void Complete(double value){ + if (!(value >= MaxValue)) return; + Value = 0; + Hide(); + } + + private void Cancel(){ + Complete(MaxValue); + } + + private void ShowProgress(int maxValue){ + Value = 0; + MaxValue = maxValue; + Visible = true; + } + + private void IncrementProgress(int i){ + + Value += i; + } + +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAsyncProgress.cs.uid b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAsyncProgress.cs.uid new file mode 100644 index 0000000..231bc99 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAsyncProgress.cs.uid @@ -0,0 +1 @@ +uid://yys1bb8k1fi diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAutocompleteController.cs b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAutocompleteController.cs new file mode 100644 index 0000000..d0c1047 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAutocompleteController.cs @@ -0,0 +1,278 @@ +using Godot; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace hamsterbyte.DeveloperConsole; + +public partial class UIAutocompleteController : Control, ICanInitialize{ + [ExportGroup("Controls")] [Export] private Label _contextLabel; + [Export] private Label _autoCompleteLabel; + [Export] private RichTextLabel _autoCompleteSuggestions; + [Export] private RichTextLabel _autoCompleteDescription; + [Export] private LineEdit _commandInput; + + private string[] _suggestionStrings = Array.Empty(); + private int _currentShift; + private readonly Dictionary _commands = new(); + private readonly Dictionary _descriptions = new(); + + #region SETUP + + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"UIAutocompleteController => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + SetupCallbacks(); + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } + + private void SetupCallbacks(){ + DC.OnDisable += Disable; + DC.OnEnable += Enable; + _autoCompleteSuggestions.MetaHoverStarted += MetaHoverStart; + _autoCompleteSuggestions.MetaHoverEnded += MetaHoverEnd; + _autoCompleteSuggestions.MetaClicked += OnMetaClicked; + CommandInterpreter.OnContextChanged += ChangeContext; + } + + + private void Enable(){ + _commandInput.GrabFocus(); + _commandInput.Clear(); + } + + private void Disable(){ + _autoCompleteDescription.Visible = false; + _autoCompleteDescription.Text = string.Empty; + _autoCompleteSuggestions.Visible = false; + _autoCompleteSuggestions.Text = string.Empty; + _autoCompleteLabel.Text = string.Empty; + _commandInput.ReleaseFocus(); + } + + private void ChangeContext((Node, Dictionary commands) context){ + SetConsoleCommands(context.commands); + } + + #endregion + + #region INPUT + + public override void _Input(InputEvent @event){ + if (@event is not InputEventKey k) return; + if (!DC.Enabled) return; + if (k.Pressed){ + HandleVisiblePressKeystrokes(k); + } + else{ + UpdateAutocomplete(); + TryCorrectCase(k); + } + } + + private void HandleVisiblePressKeystrokes(InputEventKey k){ + if (!_commandInput.HasFocus()) return; + switch (k.Keycode){ + case Key.Up: + KeyUpPressedVisible(); + break; + case Key.Down: + KeyDownPressedVisible(); + break; + case Key.Right: + KeyRightPressedVisible(); + break; + } + } + + private void KeyUpPressedVisible(){ + if (!_autoCompleteSuggestions.Visible){ + PopHistory(); + } + else{ + ShiftSuggestions(1); + } + } + + private void KeyDownPressedVisible(){ + if (!_autoCompleteSuggestions.Visible) return; + ShiftSuggestions(-1); + } + + private void KeyRightPressedVisible(){ + if (_autoCompleteLabel.Text == string.Empty || _commandInput.CaretColumn != _commandInput.Text.Length) return; + _commandInput.Text = _autoCompleteLabel.Text; + _commandInput.CaretColumn = _commandInput.Text.Length - 2; + } + + #endregion + + public override void _Process(double delta){ + if (!_autoCompleteSuggestions.Visible) return; + CallDeferred(nameof(PositionSuggestions)); + } + + private void SetConsoleCommands(Dictionary commands){ + _commands.Clear(); + _descriptions.Clear(); + foreach (KeyValuePair command in commands){ + _commands.Add($"{command.Key}()", command.Value.ParamsAsString()); + _descriptions.Add($"{command.Key}()", + command.Value.GetCustomAttribute()?.Description); + } + } + + private void PositionSuggestions(){ + Vector2 newPos = new(){ + X = _contextLabel.Size.X + 10, + Y = GetViewport().GetVisibleRect().Size.Y - _autoCompleteSuggestions.Size.Y - 42 + }; + _autoCompleteSuggestions.GlobalPosition = newPos; + } + + + private void MetaHoverStart(Variant meta){ + _autoCompleteDescription.ResetSize(); + _autoCompleteDescription.Visible = true; + _autoCompleteDescription.Text = meta.AsString().Split('|')[1]; + } + + private void MetaHoverEnd(Variant meta){ + _autoCompleteDescription.Visible = false; + _autoCompleteDescription.Text = string.Empty; + } + + private void OnMetaClicked(Variant meta){ + _commandInput.Text = _autoCompleteDescription.Text = meta.AsString().Split('|')[0]; + _commandInput.CaretColumn = _commandInput.Text.Length - 1; + UpdateAutocomplete(); + } + + public void CompleteAndSubmit(string commandString){ + if (_autoCompleteLabel.Text.StartsWith(commandString)){ + commandString = _autoCompleteLabel.Text; + } + + DC.Submit(commandString); + } + + private void UpdateAutoCompleteLabel(){ + _autoCompleteLabel.Text = _suggestionStrings.Length == 0 + ? string.Empty + : _suggestionStrings[_currentShift.Wrap(_suggestionStrings.Length)]; + if (_suggestionStrings.Length == 0) return; + if (_commandInput.Text.Length > _suggestionStrings[_currentShift.Wrap(_suggestionStrings.Length)].Length - 2){ + _autoCompleteLabel.Text = string.Empty; + } + } + + private void UpdateAutoCompleteSuggestionsLabel(){ + StringBuilder b = new(); + for (int i = _suggestionStrings.Length - 1; i > -1; i--){ + b.Append($"[url={_suggestionStrings[i]}|{_descriptions[_suggestionStrings[i]]}]"); + + if (i == _currentShift.Wrap(_suggestionStrings.Length)){ + b.Append("[bgcolor=333333]"); + } + + string[] methodParts = _suggestionStrings[i].Split('(')[0].Split('.'); + for (int j = 0; j < methodParts.Length; j++){ + if (j == methodParts.Length - 1){ + b.Append( + $"[color={DCColorTheme.Suggestions["Method"]}]{methodParts[j]}[/color]("); + } + else{ + b.Append($"{methodParts[j]}."); + } + } + + b.Append($"{_commands[_suggestionStrings[i]]})"); + if (i == _currentShift.Wrap(_suggestionStrings.Length)){ + b.Append("[/bgcolor]"); + } + + b.Append("[/url]"); + if (i > 0){ + b.Append('\n'); + } + } + + string formattedString = b.ToString(); + foreach (KeyValuePair kvp in DCExtensions.TypeReplacementStrings){ + string replace = formattedString.Replace(kvp.Key, + $"[color={DCColorTheme.Suggestions["Type"]}]{kvp.Value}[/color]"); + formattedString = replace; + } + + _autoCompleteSuggestions.Size = Vector2.Zero; + _autoCompleteSuggestions.Text = formattedString; + _autoCompleteSuggestions.Size = new Vector2(_autoCompleteSuggestions.Size.X, _suggestionStrings.Length * 24); + } + + private void UpdateAutoCompleteSuggestionStrings(){ + if (_commandInput.Text.StartsWith('(')) return; + _suggestionStrings = _commandInput.Text.Length > 0 + ? _commands.Keys.Where(s => s.ToLower().StartsWith(_commandInput.Text.ToLower().Split('(')[0])).ToArray() + : Array.Empty(); + if (_suggestionStrings.Length > DC.Instance.MaxAutocompleteLines) + Array.Resize(ref _suggestionStrings, DC.Instance.MaxAutocompleteLines); + _autoCompleteSuggestions.Visible = _suggestionStrings.Length > 0; + } + + private void PopHistory(){ + _commandInput.Text = CommandInterpreter.LastCommand; + CallDeferred(nameof(InputCaretToEnd), 0); + } + + private void ShiftSuggestions(int dir){ + _currentShift += dir; + CallDeferred(nameof(InputCaretToEnd), 0); + } + + private void InputCaretToEnd(int offset = 0){ + _commandInput.CaretColumn = _commandInput.Text.Length + offset; + } + + private void UpdateAutocomplete(){ + UpdateAutoCompleteSuggestionStrings(); + UpdateAutoCompleteLabel(); + UpdateAutoCompleteSuggestionsLabel(); + } + + private bool TryCorrectCase(InputEventKey k){ + if (_suggestionStrings.Length == 0) return false; + if (!k.Keycode.IsLetter()) return false; + if (k.CtrlPressed || k.AltPressed) return false; + ReadOnlySpan suggestionChars = _suggestionStrings[0]; + char[] inputChars = _commandInput.Text.ToCharArray(); + for (int i = 0; i < inputChars.Length; i++){ + if (suggestionChars.Length - 1 < i) break; + if (i > _suggestionStrings[0].Length - 2) break; + inputChars[i] = suggestionChars[i]; + } + + if (_commandInput.Text.Length > _suggestionStrings[0].Length - 2) return false; + ReadOnlySpan rOChars = inputChars; + _commandInput.Text = rOChars.ToString(); + _commandInput.CaretColumn = _commandInput.Text.Length; + return true; + } + + public void SetCommandPromptFromCommandTree(string s){ + _commandInput.Text = $"{s}()"; + UpdateAutocomplete(); + _commandInput.GrabFocus(); + CallDeferred(nameof(InputCaretToEnd), -1); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAutocompleteController.cs.uid b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAutocompleteController.cs.uid new file mode 100644 index 0000000..9a683bb --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIAutocompleteController.cs.uid @@ -0,0 +1 @@ +uid://dtxlhxhv0uon8 diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UICommandInput.cs b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UICommandInput.cs new file mode 100644 index 0000000..840bcc5 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UICommandInput.cs @@ -0,0 +1,65 @@ +using System; +using Godot; + +namespace hamsterbyte.DeveloperConsole; + +public partial class UICommandInput : LineEdit, ICanInitialize{ + [ExportGroup("Controls")] [Export] private CodeEdit _terminalMode; + [Export] private Label _contextLabel; + [Export] private UIAutocompleteController _autocompleteController; + + + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"UICommandInput => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + SetupCallbacks(); + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } + + private void SetupCallbacks(){ + TextSubmitted += SubmitCommand; + } + + private void SubmitCommand(string commandString){ + if (commandString == "") return; + if (TryCopyLineShortcut(commandString)) return; + Submit(commandString); + Clear(); + } + + private void Submit(string commandString){ + _autocompleteController.CompleteAndSubmit(commandString); + } + + private bool TryCopyLineShortcut(string commandString){ + try{ + if (commandString.StartsWith('#')){ + long lineNumber = long.Parse(commandString.TrimStart('#')); + CopyLineToInput(lineNumber - 1); + return true; + } + } + catch (Exception e){ + Clear(); + DC.PrintError($"Cannot copy line ({e.Message})"); + } + + return false; + } + + public void CopyLineToInput(long line){ + _terminalMode.Deselect(); + Text = _terminalMode.GetLine((int)line).TrimStart(DC.CurrentNode.PromptString().ToCharArray()).Trim(); + CaretColumn = Text.Length; + GrabFocus(); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UICommandInput.cs.uid b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UICommandInput.cs.uid new file mode 100644 index 0000000..d2d3525 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UICommandInput.cs.uid @@ -0,0 +1 @@ +uid://ckf3tex7kjvu4 diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIContextLabel.cs b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIContextLabel.cs new file mode 100644 index 0000000..c62bcff --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIContextLabel.cs @@ -0,0 +1,29 @@ +using System; +using Godot; +using System.Collections.Generic; +using System.Reflection; + +namespace hamsterbyte.DeveloperConsole; + +public partial class UIContextLabel : Label, ICanInitialize{ + + private void ChangeContext((Node node, Dictionary) context){ + Text = context.node.PromptString(); + } + + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"UIContextLabel => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + CommandInterpreter.OnContextChanged += ChangeContext; + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIContextLabel.cs.uid b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIContextLabel.cs.uid new file mode 100644 index 0000000..829d44e --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIContextLabel.cs.uid @@ -0,0 +1 @@ +uid://c12ic5e05em8y diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIDescriptionLabel.cs b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIDescriptionLabel.cs new file mode 100644 index 0000000..af36c92 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIDescriptionLabel.cs @@ -0,0 +1,11 @@ +using Godot; +using System; +using hamsterbyte.DeveloperConsole; + +public partial class UIDescriptionLabel : RichTextLabel{ + // Called every frame. 'delta' is the elapsed time since the previous frame. + public override void _Process(double delta){ + if (!Visible || !DC.Enabled) return; + GlobalPosition = GetGlobalMousePosition() - PivotOffset; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIDescriptionLabel.cs.uid b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIDescriptionLabel.cs.uid new file mode 100644 index 0000000..5fc3161 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIDescriptionLabel.cs.uid @@ -0,0 +1 @@ +uid://bejppl34qchoe diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIModeToggle.cs b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIModeToggle.cs new file mode 100644 index 0000000..ac52911 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIModeToggle.cs @@ -0,0 +1,38 @@ +using System; +using Godot; + +namespace hamsterbyte.DeveloperConsole; + +public partial class UIModeToggle : TextureButton, ICanInitialize{ + [Export] private Control _crosshairMode; + [Export] private Control _terminalMode; + + public override void _EnterTree(){ + SetupCallbacks(); + } + + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"UIModeToggle => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } + + private void SetupCallbacks(){ + Toggled += ToggleModePanels; + } + + private void ToggleModePanels(bool toggle){ + _crosshairMode.Visible = !_crosshairMode.Visible; + _terminalMode.Visible = !_terminalMode.Visible; + DC.ChangeMode(_terminalMode.Visible ? 0 : 1); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIModeToggle.cs.uid b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIModeToggle.cs.uid new file mode 100644 index 0000000..8e2157f --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Command Prompt/UIModeToggle.cs.uid @@ -0,0 +1 @@ +uid://cl3n3mavjh2nx diff --git a/src/voxelgame/addons/Developer Console/UI/Crosshair/UICommandTree.cs b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICommandTree.cs new file mode 100644 index 0000000..c8bea85 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICommandTree.cs @@ -0,0 +1,100 @@ +using Godot; +using System; +using System.Collections.Generic; +using System.Reflection; +using hamsterbyte.DeveloperConsole; + +public partial class UICommandTree : Tree, ICanInitialize{ + [Export] private UIAutocompleteController _autocompleteController; + + private TreeItem _instanceCommandItem; + private TreeItem _staticCommandItem; + private Dictionary _contextCommands; + + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"UICommandTree => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + SetupTree(); + SetupCallbacks(); + SetupCommands(); + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } + + private void SetupTree(){ + Clear(); + TreeItem root = CreateItem(); + root.SetText(0, "root"); + } + + private void SetupCallbacks(){ + CommandInterpreter.OnGetStaticCommands += PopulateStaticCommands; + CommandInterpreter.OnGetInstanceCommands += PopulateInstanceCommands; + ItemSelected += SendCommandToPrompt; + } + + private void SetupCommands(){ + PopulateStaticCommands(CommandInterpreter.StaticCommands); + PopulateInstanceCommands(CommandInterpreter.InstanceCommands); + } + + + private void SendCommandToPrompt(){ + if (GetSelected().GetMetadata(0).AsBool()){ + _autocompleteController.SetCommandPromptFromCommandTree(GetSelected().GetText(0)); + } + } + + private void PopulateInstanceCommands(Dictionary commands){ + _contextCommands = commands; + _instanceCommandItem?.Free(); + _instanceCommandItem = null; + if (_contextCommands is null || _contextCommands.Count == 0) return; + _instanceCommandItem = CreateItem(null, 0); + _instanceCommandItem.SetText(0, "Instance"); + _instanceCommandItem.SetMetadata(0, false); + foreach (KeyValuePair v in _contextCommands){ + TreeItem currentItem = CreateItem(_instanceCommandItem); + currentItem.SetText(0, v.Key); + currentItem.SetMetadata(0, true); + } + } + + private void PopulateStaticCommands(Dictionary commands){ + Dictionary commandDictionary = new(); + _staticCommandItem = CreateItem(); + _staticCommandItem.SetText(0, "Static"); + _staticCommandItem.SetMetadata(0, false); + + foreach (KeyValuePair v in commands){ + string[] parts = v.Key.Split('.'); + if (parts.Length > 1){ + if (!commandDictionary.ContainsKey(parts[0])){ + TreeItem parentItem = CreateItem(_staticCommandItem); + parentItem.SetText(0, parts[0]); + parentItem.SetMetadata(0, false); + commandDictionary.Add(parts[0], parentItem); + } + } + + TreeItem currentItem = CreateItem(commandDictionary.TryGetValue(parts[0], out TreeItem pItem) + ? commandDictionary[parts[0]] + : _staticCommandItem); + currentItem.SetText(0, v.Key); + currentItem.SetTooltipText(0, v.Value.GetCustomAttribute()?.Description); + currentItem.SetMetadata(0, true); + } + + foreach (KeyValuePair c in commandDictionary){ + c.Value.Collapsed = true; + } + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Crosshair/UICommandTree.cs.uid b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICommandTree.cs.uid new file mode 100644 index 0000000..6dc6157 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICommandTree.cs.uid @@ -0,0 +1 @@ +uid://bbo12ubsbcvg7 diff --git a/src/voxelgame/addons/Developer Console/UI/Crosshair/UIContextTree.cs b/src/voxelgame/addons/Developer Console/UI/Crosshair/UIContextTree.cs new file mode 100644 index 0000000..c65cf41 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Crosshair/UIContextTree.cs @@ -0,0 +1,151 @@ +using Godot; +using System; +using System.Collections.Generic; +using System.Linq; +using hamsterbyte.DeveloperConsole.Composition; + +namespace hamsterbyte.DeveloperConsole; + +public partial class UIContextTree : Tree, ICanInitialize{ + [Export] private Texture2D _visButtonTex; + [Export] private Texture2D _invisButtonTex; + [Export] private Camera2D _consoleCamera; + [Export] private LineEdit _filter; + + private readonly Dictionary _associations = new(); + + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"UIContextTree => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + SetupCallbacks(); + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } + + private void SetupCallbacks(){ + ItemSelected += ChangeContext; + ButtonClicked += ToggleNodeVisibility; + DC.OnModeChanged += ValidateSceneComposition; + SceneComposer.OnCompositionChanged += RecalculateNodeTree; + Prefab.OnPrefabInstantiated += AddNodeToTree; + Nodes.OnNodeDestroyed += RemoveNodeFromTree; + _filter.TextChanged += FilterTree; + } + + + private void ValidateSceneComposition(int mode){ + if (!SceneComposer.RequiresDecomposition){ + RecalculateNodeTree(); + } + + switch (mode){ + case 0: + SceneComposer.Compose(); + break; + case 1: + SceneComposer.Decompose(); + break; + default: + SceneComposer.Compose(); + break; + } + } + + private void RecalculateNodeTree(){ + ResetNodeTree(); + PopulateNodeTree(); + } + + private void ResetNodeTree(){ + Clear(); + _associations.Clear(); + } + + private void PopulateNodeTree(){ + foreach (Node n in DCCrosshair.Nodes){ + AddNodeToTree(n); + } + } + + private void FilterTree(string filter){ + foreach (KeyValuePair pair in _associations){ + pair.Value.Visible = false; + } + + IEnumerable> validPairs = + _associations.Where(pair => pair.Key.Name.ToString()!.ToLower().Contains(filter.ToLower())); + + foreach (KeyValuePair pair in validPairs){ + pair.Value.Visible = true; + TreeItem parent = pair.Value.GetParent(); + while (parent is not null){ + parent.Visible = true; + parent = parent.GetParent(); + } + } + } + + private void ToggleNodeVisibility(TreeItem item, long column, long id, long mousebuttonindex){ + item.SetButton( + 1, + 0, + item.GetButton(1, 0) == _invisButtonTex + ? _visButtonTex + : _invisButtonTex + ); + Node node = GetNode(item.GetPath()); + + switch (node){ + case Control control: + control.Visible = item.GetButton(1, 0) == _visButtonTex; + break; + case Node2D node2D: + node2D.Visible = item.GetButton(1, 0) == _visButtonTex; + break; + } + } + + private void AddNodeToTree(Node node){ + if (node is SubViewport or SubViewportContainer) return; + Node parent = node.GetParentOrNull(); + TreeItem currentItem; + if (parent is not null){ + _associations.TryGetValue(parent, out TreeItem parentItem); + currentItem = CreateItem(parentItem); + } + else{ + currentItem = CreateItem(); + } + + _associations.Add(node, currentItem); + currentItem.SetText(0, node.Name); + currentItem.AddButton(1, _visButtonTex); + currentItem.SetTooltipText(0, currentItem.GetPath()); + } + + private void RemoveNodeFromTree(Node n){ + if (!_associations.TryGetValue(n, out TreeItem t)) return; + t.Free(); + _associations.Remove(n); + } + + private void ChangeContext(){ + TreeItem item = GetSelected(); + + if (GetNodeOrNull(item.GetPath()) is null){ + item.Free(); + DC.Print("This node no longer exists."); + return; + } + + DC.ChangeContext(item.GetPath()); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Crosshair/UIContextTree.cs.uid b/src/voxelgame/addons/Developer Console/UI/Crosshair/UIContextTree.cs.uid new file mode 100644 index 0000000..3131f11 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Crosshair/UIContextTree.cs.uid @@ -0,0 +1 @@ +uid://d35etcbwvps6t diff --git a/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairOutputLabel.cs b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairOutputLabel.cs new file mode 100644 index 0000000..da05ac3 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairOutputLabel.cs @@ -0,0 +1,48 @@ +using Godot; +using System; + +namespace hamsterbyte.DeveloperConsole; + +public partial class UICrosshairOutputLabel : RichTextLabel, ICanInitialize{ + #region INTERFACE + + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"UICrosshairOutputLabel => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + SetupCallbacks(); + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } + + #endregion + + private void SetupCallbacks(){ + DC.OnPrint += UpdateOutputLabel; + } + + private void UpdateOutputLabel(string message){ + CallThreadSafe("UpdateOutputLabelThreadSafe", message); + } + + private void UpdateOutputLabelThreadSafe(string message){ + if (message.Trim().StartsWith("#!")){ + message = message.Replace("#!", $"[color={DCColorTheme.Regions[0].Color.ToHtml()}]"); + message = message.Replace("!#", $"[/color]"); + } + + if (message.Trim().StartsWith('$')){ + message = message.Insert(0, $"[color={DCColorTheme.Regions[1].Color.ToHtml()}]"); + message = message.Replace("::", "::[/color]"); + } + + Text = message; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairOutputLabel.cs.uid b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairOutputLabel.cs.uid new file mode 100644 index 0000000..dee2780 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairOutputLabel.cs.uid @@ -0,0 +1 @@ +uid://bigrfj3yu5ysj diff --git a/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairViewer.cs b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairViewer.cs new file mode 100644 index 0000000..9910441 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairViewer.cs @@ -0,0 +1,153 @@ +using Godot; +using System; + +namespace hamsterbyte.DeveloperConsole; + +public partial class UICrosshairViewer : PanelContainer, ICanInitialize{ + #region VARIABLES + + [ExportGroup("Settings")] [Export] private Color _crosshairColor = Colors.Chartreuse; + [ExportGroup("Controls")] [Export] private Camera2D _consoleCamera; + + public bool Enabled{ get; private set; } + private bool _isCrosshairMode; + private Vector2 hStart = Vector2.Zero; + private Vector2 hEnd = Vector2.Zero; + private Vector2 vStart = Vector2.Zero; + private Vector2 vEnd = Vector2.Zero; + private Vector2 _currentMousePosition; + private const float _zoomStep = .1f; + private Vector2 _minZoom = new(.1f, .1f); + private Vector2 _maxZoom = new(10, 10); + private bool _dragCamera; + private bool _dragNode; + + #endregion + + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"UICrosshairViewer => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + MouseEntered += EnableCrosshair; + MouseExited += DisableCrosshair; + DC.OnModeChanged += ModeChanged; + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } + + + public override void _Process(double delta){ + if (!DC.Enabled || !_isCrosshairMode) return; + UpdateCrosshair(); + } + + public override void _Input(InputEvent @event){ + if (!Enabled) return; + if (@event is not InputEventMouse m) return; + TryDragCamera(m); + TryDragNode(m); + TryZoomCamera(m); + } + + private void ModeChanged(int mode){ + _isCrosshairMode = mode == 1; + } + + private void EnableCrosshair(){ + Input.MouseMode = Input.MouseModeEnum.Hidden; + hEnd.X = GetRect().End.X; + vEnd.Y = GetRect().End.Y; + Enabled = true; + } + + private void DisableCrosshair(){ + Input.MouseMode = Input.MouseModeEnum.Visible; + Enabled = false; + _dragCamera = false; + _dragNode = false; + } + + public override void _Draw(){ + if (!Enabled) return; + DrawDashedLine(hStart, hEnd, _crosshairColor, 2); + DrawDashedLine(vStart, vEnd, _crosshairColor, 2); + } + + private void UpdateCrosshair(){ + QueueRedraw(); + if (!Enabled) return; + _currentMousePosition = GetLocalMousePosition(); + hStart.Y = _currentMousePosition.Y; + hEnd.Y = _currentMousePosition.Y; + vStart.X = _currentMousePosition.X; + vEnd.X = _currentMousePosition.X; + } + + private void TryDragCamera(InputEventMouse m){ + switch (m){ + case InputEventMouseButton b: + _dragCamera = b.Pressed && Enabled && DC.Enabled && b.ButtonIndex == MouseButton.Middle; + break; + case InputEventMouseMotion v: + if (_dragCamera) _consoleCamera.GlobalPosition -= v.Relative * (Vector2.One / _consoleCamera.Zoom); + break; + } + } + + private void TryDragNode(InputEventMouse m){ + switch (m){ + case InputEventMouseButton b: + _dragNode = b.Pressed && DC.Enabled && b.ButtonIndex == MouseButton.Left && + b.ShiftPressed && Enabled; + break; + case InputEventMouseMotion v: + if (_dragNode){ + switch (DC.CurrentNode){ + case Control control: + control.GlobalPosition += v.Relative * (Vector2.One / _consoleCamera.Zoom); + break; + case Node2D node2D: + node2D.GlobalPosition += v.Relative * (Vector2.One / _consoleCamera.Zoom); + break; + } + } + + break; + } + } + + + private void ZoomCamera(Vector2 deltaZoom){ + Vector2 newZoom = new( + Mathf.Clamp(_consoleCamera.Zoom.X + deltaZoom.X, _minZoom.X, _maxZoom.X), + Mathf.Clamp(_consoleCamera.Zoom.Y + deltaZoom.Y, _minZoom.Y, _maxZoom.Y) + ); + _consoleCamera.Zoom = newZoom; + } + + private void TryZoomCamera(InputEventMouse m){ + switch (m){ + case InputEventMouseButton b: + if (_consoleCamera.Enabled){ + if (!b.Pressed) return; + switch (b.ButtonIndex){ + case MouseButton.WheelUp: + ZoomCamera(Vector2.One * _zoomStep); + break; + case MouseButton.WheelDown: + ZoomCamera(-Vector2.One * _zoomStep); + break; + } + } + + break; + } + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairViewer.cs.uid b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairViewer.cs.uid new file mode 100644 index 0000000..be89fc7 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Crosshair/UICrosshairViewer.cs.uid @@ -0,0 +1 @@ +uid://c21tsjwrs2rt6 diff --git a/src/voxelgame/addons/Developer Console/UI/Crosshair/UIMousePositionLabel.cs b/src/voxelgame/addons/Developer Console/UI/Crosshair/UIMousePositionLabel.cs new file mode 100644 index 0000000..7eab61d --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Crosshair/UIMousePositionLabel.cs @@ -0,0 +1,11 @@ +using Godot; +using System; +using hamsterbyte.DeveloperConsole; + +public partial class UIMousePositionLabel : Label{ + public override void _Process(double delta){ + if (DC.Enabled){ + Text = ((Vector2I)GetGlobalMousePosition()).ToString(); + } + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Crosshair/UIMousePositionLabel.cs.uid b/src/voxelgame/addons/Developer Console/UI/Crosshair/UIMousePositionLabel.cs.uid new file mode 100644 index 0000000..e0f3d0d --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Crosshair/UIMousePositionLabel.cs.uid @@ -0,0 +1 @@ +uid://bci1jn6uxja4q diff --git a/src/voxelgame/addons/Developer Console/UI/DCCrosshairUI.cs b/src/voxelgame/addons/Developer Console/UI/DCCrosshairUI.cs new file mode 100644 index 0000000..c6c29fd --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/DCCrosshairUI.cs @@ -0,0 +1,12 @@ +using Godot; + +namespace hamsterbyte.DeveloperConsole; + +public partial class DCCrosshairUI : Control, ICanInitialize{ + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"DCCrosshairUI => {success.OKFail()}"); + } + + public bool TryInitialize() => true; +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/DCCrosshairUI.cs.uid b/src/voxelgame/addons/Developer Console/UI/DCCrosshairUI.cs.uid new file mode 100644 index 0000000..91a81b7 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/DCCrosshairUI.cs.uid @@ -0,0 +1 @@ +uid://78w6vo48jhkk diff --git a/src/voxelgame/addons/Developer Console/UI/DeveloperConsoleUI.cs b/src/voxelgame/addons/Developer Console/UI/DeveloperConsoleUI.cs new file mode 100644 index 0000000..e554703 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/DeveloperConsoleUI.cs @@ -0,0 +1,99 @@ +using Godot; +using hamsterbyte.DeveloperConsole; + +public partial class DeveloperConsoleUI : CanvasLayer{ + #region VARIABLES + + //CONTROLS--------------------------------------- + [ExportGroup("Controls")] [Export] private CodeEdit _output; + //----------------------------------------------- + + //SETTINGS--------------------------------------- + [ExportGroup("Settings")] [Export(PropertyHint.Enum)] + public DCAccessLevel AccessLevel = DCAccessLevel.WithArg; + + [Export(PropertyHint.Enum)] public DC.PrintSuppression PrintSuppression = DC.PrintSuppression.GD; + [Export(PropertyHint.Range, "3,10")] public int MaxAutocompleteLines{ get; private set; } = 10; + [Export] public bool PrintLicense = true; + //----------------------------------------------- + + public int LastOutputLineIndex => _output.GetLineCount() - 1; + private Input.MouseModeEnum _defaultMouseMode; + + #endregion + + public override void _Ready(){ + InitializeSystem(); + } + + private void InitializeSystem(){ + DC.Initialize(this); + } + + public void InitializeUI(){ + DC.PrintComment(" ----------------- Initializing User Interface ----------------- "); + PropagateCall("Initialize", null, true); + DC.PrintComment(" ----------------- Initialization Complete ----------------- "); + DC.SetSuppressionLevel(PrintSuppression); + DC.ChangeContext("/root"); + } + + + private void ResetAll(){ + //InitializeReset(); + } + + private void InitializeReset(){ + /* + DC.Submit("Output.Clear()"); + DC.PrintComment(" ---------------------- Resetting System ---------------------- "); + DC.Reset(); + DC.PrintComment(" ----------------- Resetting User Interface ----------------- "); + PropagateCall("Reset", null, true); + */ + } + + #region INPUT + + public override void _Input(InputEvent @event){ + if (@event is not InputEventKey key) return; + CheckInput(key); + } + + private void CheckInput(InputEventKey key){ + if (!key.Pressed) return; + switch (key.Keycode){ + case Key.Quoteleft: + ToggleConsole(); + break; + case Key.Escape: + if (!Visible) return; + ToggleConsole(); + break; + } + } + + #endregion + + private void ToggleConsole(){ + Visible = !Visible; + if (Visible){ + _defaultMouseMode = Input.MouseMode; + Input.MouseMode = Input.MouseModeEnum.Visible; + foreach (StringName s in InputMap.GetActions()){ + if (s.ToString()!.Contains("ui_")) continue; + InputMap.ActionEraseEvents(s); + } + } + else{ + Input.MouseMode = _defaultMouseMode; + InputMap.LoadFromProjectSettings(); + } + + DC.SetEnabled(Visible); + } + + public void FoldLine(int line){ + _output.FoldLine(line); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/DeveloperConsoleUI.cs.uid b/src/voxelgame/addons/Developer Console/UI/DeveloperConsoleUI.cs.uid new file mode 100644 index 0000000..88605b9 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/DeveloperConsoleUI.cs.uid @@ -0,0 +1 @@ +uid://d16xfkut4jb8q diff --git a/src/voxelgame/addons/Developer Console/UI/Terminal/UITerminalOutput.cs b/src/voxelgame/addons/Developer Console/UI/Terminal/UITerminalOutput.cs new file mode 100644 index 0000000..8d5a542 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Terminal/UITerminalOutput.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using Godot; + +namespace hamsterbyte.DeveloperConsole; + +public partial class UITerminalOutput : CodeEdit, ICanInitialize{ + [Export] private UICommandInput _commandInput; + private CodeHighlighter _highlighter; + + public override void _EnterTree(){ + DC.OnPrint += PrintToOutput; + } + + public void Initialize(){ + bool success = TryInitialize(); + DC.Print($"UITerminalOutput => {success.OKFail()}"); + } + + public bool TryInitialize(){ + try{ + SetupCallbacks(); + ApplyColorTheme(); + return true; + } + catch (Exception e){ + e.PrintToDC(); + return false; + } + } + + private void SetupCallbacks(){ + DC.OnClear += ClearOutput; + GutterClicked += CopyToInput; + DC.OnCommandCompleted += GoToBottom; + } + + #region CALLBACKS + + private void PrintToOutput(string message){ + CallThreadSafe("PrintThreadSafe", message); + } + + private void PrintThreadSafe(string message){ + SetLine(GetLineCount() - 1, $"{message}\n"); + CallDeferred("ScrollToEnd"); + } + + private void ScrollToEnd(){ + ScrollVertical = GetLineCount(); + } + + private void ClearOutput(){ + ClearUndoHistory(); + SelectAll(); + DeleteSelection(); + } + + private void CopyToInput(long line, long gutter){ + if (gutter != 1) return; + Deselect(); + _commandInput.CopyLineToInput(line); + } + + private void GoToBottom(){ + SetCaretLine(GetLineCount() - 1); + } + + #endregion + + #region APPLY COLOR THEME + + private void ApplyColorTheme(){ + _highlighter = (CodeHighlighter)SyntaxHighlighter; + SetRegionColors(); + SetKeywordColors(); + SetOutputColors(); + } + + private void SetRegionColors(){ + foreach (RegionColor rC in DCColorTheme.Regions){ + _highlighter?.AddColorRegion(rC.Start, rC.End, rC.Color); + } + } + + private void SetKeywordColors(){ + if (_highlighter is null) return; + foreach (KeyValuePair typeReplacements in DCExtensions.TypeReplacementStrings){ + if (_highlighter.KeywordColors.ContainsKey(typeReplacements.Value)){ + _highlighter.KeywordColors[typeReplacements.Value] = DCColorTheme.Output["Member Variable"]; + } + else{ + _highlighter.AddKeywordColor(typeReplacements.Value, DCColorTheme.Output["Member Variable"]); + } + } + } + + private void SetOutputColors(){ + foreach (KeyValuePair color in DCColorTheme.Output){ + switch (color.Key){ + case "Method": + _highlighter.FunctionColor = color.Value; + break; + case "Number": + _highlighter.NumberColor = color.Value; + break; + case "Symbol": + _highlighter.SymbolColor = color.Value; + break; + } + } + } + + #endregion +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/UI/Terminal/UITerminalOutput.cs.uid b/src/voxelgame/addons/Developer Console/UI/Terminal/UITerminalOutput.cs.uid new file mode 100644 index 0000000..0134598 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/UI/Terminal/UITerminalOutput.cs.uid @@ -0,0 +1 @@ +uid://xxumgcjl45vb diff --git a/src/voxelgame/addons/Developer Console/_demo/circle.cs b/src/voxelgame/addons/Developer Console/_demo/circle.cs new file mode 100644 index 0000000..5d92c94 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/circle.cs @@ -0,0 +1,15 @@ +using Godot; +using System; +using hamsterbyte.DeveloperConsole; + +public partial class circle : RigidBody2D{ + [ConsoleCommand] + private void ToggleFreeze(){ + Freeze = !Freeze; + } + + [ConsoleCommand] + private void AddForce(int x, int y){ + SetAxisVelocity(new Vector2(x, y)); + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/_demo/circle.cs.uid b/src/voxelgame/addons/Developer Console/_demo/circle.cs.uid new file mode 100644 index 0000000..f7276e1 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/circle.cs.uid @@ -0,0 +1 @@ +uid://jiyhtw34ftcc diff --git a/src/voxelgame/addons/Developer Console/_demo/circle.png b/src/voxelgame/addons/Developer Console/_demo/circle.png new file mode 100644 index 0000000..bfa2c25 Binary files /dev/null and b/src/voxelgame/addons/Developer Console/_demo/circle.png differ diff --git a/src/voxelgame/addons/Developer Console/_demo/circle.png.import b/src/voxelgame/addons/Developer Console/_demo/circle.png.import new file mode 100644 index 0000000..1b2c29c --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/circle.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://djpybdrkyhqj3" +path="res://.godot/imported/circle.png-9911f5a9770a06eb17b71c3feb39d0fe.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Developer Console/_demo/circle.png" +dest_files=["res://.godot/imported/circle.png-9911f5a9770a06eb17b71c3feb39d0fe.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/voxelgame/addons/Developer Console/_demo/cross.cs b/src/voxelgame/addons/Developer Console/_demo/cross.cs new file mode 100644 index 0000000..99cdd23 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/cross.cs @@ -0,0 +1,19 @@ +using Godot; +using System; +using hamsterbyte.DeveloperConsole; + +public partial class cross : Sprite2D{ + private bool _scaleEnabled; + private float _deltaAccumulated; + + [ConsoleCommand] + private void ToggleScaleAnimation(){ + _scaleEnabled = !_scaleEnabled; + } + + public override void _Process(double delta){ + if (!_scaleEnabled) return; + Scale = Vector2.One + Vector2.One * Mathf.Abs(Mathf.Sin(_deltaAccumulated)); + _deltaAccumulated += (float)delta; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/_demo/cross.cs.uid b/src/voxelgame/addons/Developer Console/_demo/cross.cs.uid new file mode 100644 index 0000000..b299396 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/cross.cs.uid @@ -0,0 +1 @@ +uid://c824sepr6hk53 diff --git a/src/voxelgame/addons/Developer Console/_demo/cross.png b/src/voxelgame/addons/Developer Console/_demo/cross.png new file mode 100644 index 0000000..8a77cbc Binary files /dev/null and b/src/voxelgame/addons/Developer Console/_demo/cross.png differ diff --git a/src/voxelgame/addons/Developer Console/_demo/cross.png.import b/src/voxelgame/addons/Developer Console/_demo/cross.png.import new file mode 100644 index 0000000..09d2090 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/cross.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cel15ami4cotu" +path="res://.godot/imported/cross.png-205e409a95f9fa51ec2a11ed23e841de.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Developer Console/_demo/cross.png" +dest_files=["res://.godot/imported/cross.png-205e409a95f9fa51ec2a11ed23e841de.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/voxelgame/addons/Developer Console/_demo/demo.tscn b/src/voxelgame/addons/Developer Console/_demo/demo.tscn new file mode 100644 index 0000000..917c180 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/demo.tscn @@ -0,0 +1,91 @@ +[gd_scene load_steps=14 format=3 uid="uid://bekccoluusm7j"] + +[ext_resource type="Texture2D" uid="uid://cel15ami4cotu" path="res://addons/Developer Console/_demo/cross.png" id="1_lyk7q"] +[ext_resource type="Texture2D" uid="uid://djpybdrkyhqj3" path="res://addons/Developer Console/_demo/circle.png" id="2_ysnkd"] +[ext_resource type="Texture2D" uid="uid://wvc22rawst7s" path="res://addons/Developer Console/_demo/square.png" id="3_olcfo"] +[ext_resource type="Texture2D" uid="uid://bmli3hxrpvi36" path="res://addons/Developer Console/_demo/triangle.png" id="4_nhqcf"] + +[sub_resource type="Gradient" id="Gradient_nowu3"] +interpolation_mode = 2 +colors = PackedColorArray(0.375093, 0.001066, 0.558538, 1, 0.105469, 0.117188, 0.136719, 1) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_f5mbo"] +gradient = SubResource("Gradient_nowu3") +fill = 1 +fill_from = Vector2(0.06, 0.175) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_8v3wu"] +size = Vector2(61.8742, 1.84454) + +[sub_resource type="Resource" id="Resource_pymyq"] +metadata/__load_path__ = "res://Demo/cross.cs" + +[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_d0odm"] +friction = 0.0 +bounce = 1.0 + +[sub_resource type="Resource" id="Resource_jt828"] +metadata/__load_path__ = "res://Demo/circle.cs" + +[sub_resource type="CircleShape2D" id="CircleShape2D_sswyv"] +radius = 128.035 + +[sub_resource type="Resource" id="Resource_5c2sj"] +metadata/__load_path__ = "res://Demo/square.cs" + +[sub_resource type="Resource" id="Resource_mv8ek"] +metadata/__load_path__ = "res://Demo/triangle.cs" + +[node name="demo" type="Node2D"] + +[node name="background" type="Sprite2D" parent="."] +position = Vector2(962.5, 540.5) +scale = Vector2(31.0469, 17.8906) +texture = SubResource("GradientTexture2D_f5mbo") + +[node name="StaticBody2D" type="StaticBody2D" parent="background"] +collision_layer = 255 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="background/StaticBody2D"] +position = Vector2(-0.0322113, 31.0777) +shape = SubResource("RectangleShape2D_8v3wu") + +[node name="CollisionShape2D2" type="CollisionShape2D" parent="background/StaticBody2D"] +position = Vector2(-0.0322113, -31.1616) +shape = SubResource("RectangleShape2D_8v3wu") + +[node name="cross" type="Sprite2D" parent="."] +self_modulate = Color(0.921875, 0.550781, 0.457031, 1) +position = Vector2(443, 536) +texture = ExtResource("1_lyk7q") +script = SubResource("Resource_pymyq") + +[node name="circle" type="RigidBody2D" parent="."] +position = Vector2(775, 538) +collision_layer = 255 +physics_material_override = SubResource("PhysicsMaterial_d0odm") +freeze = true +freeze_mode = 1 +script = SubResource("Resource_jt828") + +[node name="sprite" type="Sprite2D" parent="circle"] +self_modulate = Color(0.738281, 0.292969, 0.390625, 1) +texture = ExtResource("2_ysnkd") + +[node name="collision" type="CollisionShape2D" parent="circle"] +shape = SubResource("CircleShape2D_sswyv") + +[node name="square" type="Sprite2D" parent="."] +self_modulate = Color(0.3125, 0.660156, 0.808594, 1) +position = Vector2(1108, 536) +texture = ExtResource("3_olcfo") +script = SubResource("Resource_5c2sj") + +[node name="triangle" type="Sprite2D" parent="."] +self_modulate = Color(0.585938, 0.898438, 0.757813, 1) +position = Vector2(1451, 536) +texture = ExtResource("4_nhqcf") +script = SubResource("Resource_mv8ek") + +[node name="Camera2D" type="Camera2D" parent="."] +offset = Vector2(960, 540) diff --git a/src/voxelgame/addons/Developer Console/_demo/square.cs b/src/voxelgame/addons/Developer Console/_demo/square.cs new file mode 100644 index 0000000..72fa50c --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/square.cs @@ -0,0 +1,24 @@ +using Godot; +using System; +using hamsterbyte.DeveloperConsole; + +public partial class square : Sprite2D{ + private bool _enableRotate; + + [ConsoleCommand] + public void ToggleRotate() => _enableRotate = !_enableRotate; + + [ConsoleCommand] + private void FailOnPurpose(){ + int[] ar = new int[24]; + Array.Fill(ar, 0); + for (int i = -1; i < ar.Length; i++){ + DC.Print(ar[i]); + } + } + + public override void _Process(double delta){ + if (!_enableRotate) return; + Rotation += (float)delta; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/_demo/square.cs.uid b/src/voxelgame/addons/Developer Console/_demo/square.cs.uid new file mode 100644 index 0000000..df7bd09 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/square.cs.uid @@ -0,0 +1 @@ +uid://cvxfc07nf2ogs diff --git a/src/voxelgame/addons/Developer Console/_demo/square.png b/src/voxelgame/addons/Developer Console/_demo/square.png new file mode 100644 index 0000000..bc6bc93 Binary files /dev/null and b/src/voxelgame/addons/Developer Console/_demo/square.png differ diff --git a/src/voxelgame/addons/Developer Console/_demo/square.png.import b/src/voxelgame/addons/Developer Console/_demo/square.png.import new file mode 100644 index 0000000..4eb51d7 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/square.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://wvc22rawst7s" +path="res://.godot/imported/square.png-e075c11587e320148002973ca66c6f6d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Developer Console/_demo/square.png" +dest_files=["res://.godot/imported/square.png-e075c11587e320148002973ca66c6f6d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/voxelgame/addons/Developer Console/_demo/triangle.cs b/src/voxelgame/addons/Developer Console/_demo/triangle.cs new file mode 100644 index 0000000..f4f33c6 --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/triangle.cs @@ -0,0 +1,23 @@ +using Godot; +using System; +using hamsterbyte.DeveloperConsole; + +public partial class triangle : Sprite2D{ + private bool _enableMove; + private float _deltaAccumulated; + private Vector2 _startPosition; + + [ConsoleCommand] + private void ToggleMove() => _enableMove = !_enableMove; + + + public override void _EnterTree(){ + _startPosition = GlobalPosition; + } + + public override void _Process(double delta){ + if (!_enableMove) return; + GlobalPosition = _startPosition + Vector2.Up * Mathf.Cos(_deltaAccumulated) * 128; + _deltaAccumulated += (float)delta; + } +} \ No newline at end of file diff --git a/src/voxelgame/addons/Developer Console/_demo/triangle.cs.uid b/src/voxelgame/addons/Developer Console/_demo/triangle.cs.uid new file mode 100644 index 0000000..813534e --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/triangle.cs.uid @@ -0,0 +1 @@ +uid://5vtn06xq8jcg diff --git a/src/voxelgame/addons/Developer Console/_demo/triangle.png b/src/voxelgame/addons/Developer Console/_demo/triangle.png new file mode 100644 index 0000000..0133bd6 Binary files /dev/null and b/src/voxelgame/addons/Developer Console/_demo/triangle.png differ diff --git a/src/voxelgame/addons/Developer Console/_demo/triangle.png.import b/src/voxelgame/addons/Developer Console/_demo/triangle.png.import new file mode 100644 index 0000000..c89e44d --- /dev/null +++ b/src/voxelgame/addons/Developer Console/_demo/triangle.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bmli3hxrpvi36" +path="res://.godot/imported/triangle.png-dcf2e0f53b0927c44177b4eea3431a5e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/Developer Console/_demo/triangle.png" +dest_files=["res://.godot/imported/triangle.png-dcf2e0f53b0927c44177b4eea3431a5e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/voxelgame/addons/GodotVoipNet/Icons/VoiceInstance.svg b/src/voxelgame/addons/GodotVoipNet/Icons/VoiceInstance.svg new file mode 100644 index 0000000..117767d --- /dev/null +++ b/src/voxelgame/addons/GodotVoipNet/Icons/VoiceInstance.svg @@ -0,0 +1,39 @@ + + + + +Created by potrace 1.10, written by Peter Selinger 2001-2011 + + + + + + + + + + diff --git a/src/voxelgame/addons/GodotVoipNet/Icons/VoiceInstance.svg.import b/src/voxelgame/addons/GodotVoipNet/Icons/VoiceInstance.svg.import new file mode 100644 index 0000000..64f0315 --- /dev/null +++ b/src/voxelgame/addons/GodotVoipNet/Icons/VoiceInstance.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bwmrjh0023y1w" +path="res://.godot/imported/VoiceInstance.svg-2f8d0e7887a70388ded60d34900a9643.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GodotVoipNet/Icons/VoiceInstance.svg" +dest_files=["res://.godot/imported/VoiceInstance.svg-2f8d0e7887a70388ded60d34900a9643.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/voxelgame/addons/GodotVoipNet/Icons/VoiceOrchestrator.svg b/src/voxelgame/addons/GodotVoipNet/Icons/VoiceOrchestrator.svg new file mode 100644 index 0000000..a226d97 --- /dev/null +++ b/src/voxelgame/addons/GodotVoipNet/Icons/VoiceOrchestrator.svg @@ -0,0 +1,58 @@ + + + + +Created by potrace 1.10, written by Peter Selinger 2001-2011 + + + + + + + + + + + + diff --git a/src/voxelgame/addons/GodotVoipNet/Icons/VoiceOrchestrator.svg.import b/src/voxelgame/addons/GodotVoipNet/Icons/VoiceOrchestrator.svg.import new file mode 100644 index 0000000..e60e4ef --- /dev/null +++ b/src/voxelgame/addons/GodotVoipNet/Icons/VoiceOrchestrator.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ckkefjosahorr" +path="res://.godot/imported/VoiceOrchestrator.svg-08cc221fbdcd6f7f2919d503d9e31ef9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/GodotVoipNet/Icons/VoiceOrchestrator.svg" +dest_files=["res://.godot/imported/VoiceOrchestrator.svg-08cc221fbdcd6f7f2919d503d9e31ef9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/voxelgame/addons/GodotVoipNet/Plugin.cs b/src/voxelgame/addons/GodotVoipNet/Plugin.cs new file mode 100644 index 0000000..e3496a8 --- /dev/null +++ b/src/voxelgame/addons/GodotVoipNet/Plugin.cs @@ -0,0 +1,20 @@ +#if TOOLS +using Godot; +using System; + +[Tool] +public partial class Plugin : EditorPlugin +{ + public override void _EnterTree() + { + AddCustomType("VoiceInstance", "Node", GD.Load