From 6a68953c9a59c6b96fc30f2b22accb42c112a310 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Mon, 1 Jun 2026 10:29:01 +0200 Subject: [PATCH] feat: World.SetBlock writes through to IChunkModificationStore --- Core/Game.cs | 5 +- VoxelEngine.Tests/WorldTests.cs | 88 +++++++++++++++++++++++++++++++++ World/IWorld.cs | 15 ++++-- World/World.cs | 81 +++++++++++++++++------------- 4 files changed, 149 insertions(+), 40 deletions(-) create mode 100644 VoxelEngine.Tests/WorldTests.cs diff --git a/Core/Game.cs b/Core/Game.cs index 6620d63..f6ea81a 100644 --- a/Core/Game.cs +++ b/Core/Game.cs @@ -6,6 +6,7 @@ using OpenTK.Windowing.GraphicsLibraryFramework; using VoxelEngine.Rendering; using VoxelEngine.World.Blocks; using VoxelEngine.World.Chunk; +using VoxelEngine.World.Persistence; using VoxelEngine.World.WorldGen; namespace VoxelEngine.Core; @@ -15,7 +16,7 @@ public sealed class Game : GameWindow private const float MoveSpeed = 8f; // units per second private const float Sensitivity = 0.15f; // degrees per pixel private const int Seed = 1337; - private const bool UseSeed = false; + private const bool UseSeed = false; private readonly BlockRegistry _registry = new(); private Shader _shader = null!; @@ -40,7 +41,7 @@ public sealed class Game : GameWindow this._shader = new Shader("Assets/shaders/chunk.vert", "Assets/shaders/chunk.frag"); this._texture = new Texture("Assets/textures/VoxelShadeAtlas.png"); - this._world = new World.World(this._registry); + this._world = new World.World(this._registry, new InMemoryChunkModificationStore()); int seed = Game.UseSeed ? Game.Seed : new Random().Next(); diff --git a/VoxelEngine.Tests/WorldTests.cs b/VoxelEngine.Tests/WorldTests.cs new file mode 100644 index 0000000..44fad47 --- /dev/null +++ b/VoxelEngine.Tests/WorldTests.cs @@ -0,0 +1,88 @@ +using OpenTK.Mathematics; +using VoxelEngine.World; +using VoxelEngine.World.Blocks; +using VoxelEngine.World.Persistence; +using Xunit; +using VoxelChunk = VoxelEngine.World.Chunk.Chunk; + +namespace VoxelEngine.Tests; + +public class WorldTests +{ + private static (IWorld, BlockRegistry, IChunkModificationStore) Create() + { + var registry = new BlockRegistry(); + var store = new InMemoryChunkModificationStore(); + IWorld world = new World.World(registry, store); + return (world, registry, store); + } + + [Fact] + public void SetBlock_LoadedChunk_WritesBlockAndRecordsDiff() + { + var (world, registry, store) = Create(); + var chunkPos = new Vector3i(0, 0, 0); + world.SetChunk(chunkPos, new VoxelChunk()); + + bool result = world.SetBlock(2, 3, 4, registry.Stone.Id); + + Assert.True(result); + Assert.Equal(registry.Stone.Id, world.GetBlock(2, 3, 4).Id); + Assert.True(store.HasDiff(chunkPos)); + Assert.True(store.TryGetDiff(chunkPos, out var diff)); + int expectedIndex = (2 * VoxelChunk.Size + 3) * VoxelChunk.Size + 4; + Assert.Equal(registry.Stone.Id, diff![expectedIndex]); + } + + [Fact] + public void SetBlock_UnloadedChunk_RecordsDiffButReturnsFalse() + { + var (world, registry, store) = Create(); + var chunkPos = new Vector3i(5, 0, 0); + + bool result = world.SetBlock(5 * VoxelChunk.Size, 0, 0, registry.Stone.Id); + + Assert.False(result); + Assert.True(store.HasDiff(chunkPos)); + } + + [Fact] + public void GetBlock_UnloadedChunk_ReturnsAir() + { + var (world, registry, _) = Create(); + Assert.Same(registry.Air, world.GetBlock(0, 0, 0)); + } + + [Fact] + public void SetBlock_NegativeCoords_FloorsToCorrectChunk() + { + var (world, registry, store) = Create(); + var chunkPos = new Vector3i(-1, 0, 0); + world.SetChunk(chunkPos, new VoxelChunk()); + + bool result = world.SetBlock(-1, 0, 0, registry.Stone.Id); + + Assert.True(result); + Assert.Equal(registry.Stone.Id, world.GetBlock(-1, 0, 0).Id); + Assert.True(store.HasDiff(chunkPos)); + } + + [Fact] + public void RemoveChunk_RemovesAndGetBlockReturnsAir() + { + var (world, registry, _) = Create(); + var chunkPos = new Vector3i(0, 0, 0); + world.SetChunk(chunkPos, new VoxelChunk()); + world.RemoveChunk(chunkPos); + + Assert.Null(world.GetChunk(chunkPos)); + Assert.Same(registry.Air, world.GetBlock(0, 0, 0)); + } + + [Fact] + public void ModificationStore_AccessorReturnsConstructorInjectedStore() + { + var (world, _, store) = Create(); + Assert.Same(store, world.ModificationStore); + } +} diff --git a/World/IWorld.cs b/World/IWorld.cs index f286a70..7d18076 100644 --- a/World/IWorld.cs +++ b/World/IWorld.cs @@ -1,11 +1,16 @@ -using OpenTK.Mathematics; +using OpenTK.Mathematics; using VoxelEngine.World.Blocks; +using VoxelEngine.World.Persistence; namespace VoxelEngine.World; public interface IWorld { - public void SetChunk(Vector3i chunkPos, Chunk.Chunk chunk); - public Chunk.Chunk? GetChunk(Vector3i chunkPos); - public IBlock GetBlock(int worldX, int worldY, int worldZ); -} \ No newline at end of file + IChunkModificationStore ModificationStore { get; } + + void SetChunk(Vector3i chunkPos, Chunk.Chunk chunk); + void RemoveChunk(Vector3i chunkPos); + Chunk.Chunk? GetChunk(Vector3i chunkPos); + IBlock GetBlock(int worldX, int worldY, int worldZ); + bool SetBlock(int worldX, int worldY, int worldZ, ushort blockId); +} diff --git a/World/World.cs b/World/World.cs index cc44063..938a929 100644 --- a/World/World.cs +++ b/World/World.cs @@ -1,6 +1,7 @@ -using System.Collections; +using System.Collections; using OpenTK.Mathematics; using VoxelEngine.World.Blocks; +using VoxelEngine.World.Persistence; namespace VoxelEngine.World; @@ -9,55 +10,69 @@ public sealed class World : IWorld, IEnumerable _chunks = new(); private readonly BlockRegistry _registry; - public World(BlockRegistry registry) + public IChunkModificationStore ModificationStore { get; } + + public World(BlockRegistry registry, IChunkModificationStore modStore) { this._registry = registry; - } - - public void SetChunk(Vector3i chunkPos, Chunk.Chunk chunk) - { - this._chunks[chunkPos] = chunk; + this.ModificationStore = modStore; } - public Chunk.Chunk? GetChunk(Vector3i chunkPos) - { - return this._chunks.GetValueOrDefault(chunkPos); - } + public void SetChunk(Vector3i chunkPos, Chunk.Chunk chunk) => this._chunks[chunkPos] = chunk; + + public void RemoveChunk(Vector3i chunkPos) => this._chunks.Remove(chunkPos); + + public Chunk.Chunk? GetChunk(Vector3i chunkPos) => this._chunks.GetValueOrDefault(chunkPos); public IBlock GetBlock(int worldX, int worldY, int worldZ) { Vector3i chunkPos = new( World.FloorDiv(worldX, Chunk.Chunk.Size), World.FloorDiv(worldY, Chunk.Chunk.Size), - World.FloorDiv(worldZ, Chunk.Chunk.Size) - ); - + World.FloorDiv(worldZ, Chunk.Chunk.Size)); + Chunk.Chunk? chunk = this.GetChunk(chunkPos); - if (chunk == null) - { - return this._registry.Air; - } - - int localX = worldX - chunkPos.X * Chunk.Chunk.Size; - int localY = worldY - chunkPos.Y * Chunk.Chunk.Size; - int localZ = worldZ - chunkPos.Z * Chunk.Chunk.Size; - + if (chunk == null) return this._registry.Air; + + int localX = worldX - chunkPos.X * Chunk.Chunk.Size; + int localY = worldY - chunkPos.Y * Chunk.Chunk.Size; + int localZ = worldZ - chunkPos.Z * Chunk.Chunk.Size; return this._registry.Get(chunk[localX, localY, localZ]); } + public bool SetBlock(int worldX, int worldY, int worldZ, ushort blockId) + { + Vector3i chunkPos = new( + World.FloorDiv(worldX, Chunk.Chunk.Size), + World.FloorDiv(worldY, Chunk.Chunk.Size), + World.FloorDiv(worldZ, Chunk.Chunk.Size)); + + Chunk.Chunk? chunk = this.GetChunk(chunkPos); + + int localX = worldX - chunkPos.X * Chunk.Chunk.Size; + int localY = worldY - chunkPos.Y * Chunk.Chunk.Size; + int localZ = worldZ - chunkPos.Z * Chunk.Chunk.Size; + int localIndex = (localX * Chunk.Chunk.Size + localY) * Chunk.Chunk.Size + localZ; + + // For a loaded chunk, no-op writes shouldn't pollute the diff store. + if (chunk != null && chunk[localX, localY, localZ] == blockId) return true; + + this.ModificationStore.RecordEdit(chunkPos, localIndex, blockId); + + if (chunk == null) return false; + + chunk[localX, localY, localZ] = blockId; + return true; + } + private static int FloorDiv(int value, int size) { - int quotient = value / size; - if (value % size != 0 && value < 0) - { - quotient--; - } - return quotient; + int q = value / size; + if (value % size != 0 && value < 0) q--; + return q; } - public IEnumerator> GetEnumerator() - => this._chunks.GetEnumerator(); + public IEnumerator> GetEnumerator() => this._chunks.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - -} \ No newline at end of file +}