From 89dfdeafad1b70b6a609ac5b0f268eedd0c3a9a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:04:21 +0000 Subject: [PATCH 1/6] Initial plan From c36cf6eda42a75524ec59fae777c3b6416337841 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:16:44 +0000 Subject: [PATCH 2/6] Add ITerrainGenerator with GenerationUnit fork support for cross-chunk modifications Co-authored-by: CoPokBl <59753822+CoPokBl@users.noreply.github.com> --- .../Server/Terrain/ChunkGenerationModifier.cs | 76 +++++ .../Server/Terrain/GenerationUnit.cs | 165 +++++++++++ .../Server/Terrain/IGenerationModifier.cs | 34 +++ .../Server/Terrain/IGenerationUnit.cs | 40 +++ .../Server/Terrain/ITerrainGenerator.cs | 14 + .../Server/Terrain/LambdaTerrainGenerator.cs | 37 +++ .../Providers/GeneratorTerrainProvider.cs | 72 +++++ Tests/GenerationUnitTest.cs | 259 ++++++++++++++++++ 8 files changed, 697 insertions(+) create mode 100644 Minecraft/Implementations/Server/Terrain/ChunkGenerationModifier.cs create mode 100644 Minecraft/Implementations/Server/Terrain/GenerationUnit.cs create mode 100644 Minecraft/Implementations/Server/Terrain/IGenerationModifier.cs create mode 100644 Minecraft/Implementations/Server/Terrain/IGenerationUnit.cs create mode 100644 Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs create mode 100644 Minecraft/Implementations/Server/Terrain/LambdaTerrainGenerator.cs create mode 100644 Minecraft/Implementations/Server/Terrain/Providers/GeneratorTerrainProvider.cs create mode 100644 Tests/GenerationUnitTest.cs diff --git a/Minecraft/Implementations/Server/Terrain/ChunkGenerationModifier.cs b/Minecraft/Implementations/Server/Terrain/ChunkGenerationModifier.cs new file mode 100644 index 00000000..65b0bb24 --- /dev/null +++ b/Minecraft/Implementations/Server/Terrain/ChunkGenerationModifier.cs @@ -0,0 +1,76 @@ +using Minecraft.Data.Blocks; +using Minecraft.Schemas.Chunks; +using Minecraft.Schemas.Vec; + +namespace Minecraft.Implementations.Server.Terrain; + +/// +/// A modifier that modifies blocks within a single chunk. +/// +internal class ChunkGenerationModifier : IGenerationModifier { + private readonly ChunkData _chunk; + private readonly int _chunkAbsoluteX; + private readonly int _chunkAbsoluteZ; + private readonly int _minY; + private readonly int _maxY; + + public ChunkGenerationModifier(ChunkData chunk, int minY) { + _chunk = chunk; + _chunkAbsoluteX = chunk.ChunkX * ChunkSection.Size; + _chunkAbsoluteZ = chunk.ChunkZ * ChunkSection.Size; + _minY = minY; + _maxY = minY + chunk.WorldHeight; + } + + public void SetBlock(Vec3 position, IBlock block) { + // Check if the position is within this chunk + if (!IsInChunk(position)) { + return; + } + + // Convert to chunk-local coordinates + int localX = (position.X % ChunkSection.Size + ChunkSection.Size) % ChunkSection.Size; + int localZ = (position.Z % ChunkSection.Size + ChunkSection.Size) % ChunkSection.Size; + int localY = position.Y - _minY; + + if (localY < 0 || localY >= _chunk.WorldHeight) { + return; + } + + _chunk.SetBlock(localX, localY, localZ, block.StateId); + } + + public void Fill(Vec3 start, Vec3 end, IBlock block) { + // Clamp to chunk boundaries + int startX = Math.Max(start.X, _chunkAbsoluteX); + int startZ = Math.Max(start.Z, _chunkAbsoluteZ); + int startY = Math.Max(start.Y, _minY); + + int endX = Math.Min(end.X, _chunkAbsoluteX + ChunkSection.Size); + int endZ = Math.Min(end.Z, _chunkAbsoluteZ + ChunkSection.Size); + int endY = Math.Min(end.Y, _maxY); + + for (int x = startX; x < endX; x++) { + for (int y = startY; y < endY; y++) { + for (int z = startZ; z < endZ; z++) { + SetBlock(new Vec3(x, y, z), block); + } + } + } + } + + public void FillHeight(int minY, int maxY, IBlock block) { + Fill( + new Vec3(_chunkAbsoluteX, minY, _chunkAbsoluteZ), + new Vec3(_chunkAbsoluteX + ChunkSection.Size, maxY, _chunkAbsoluteZ + ChunkSection.Size), + block + ); + } + + private bool IsInChunk(Vec3 position) { + int localX = position.X - _chunkAbsoluteX; + int localZ = position.Z - _chunkAbsoluteZ; + return localX >= 0 && localX < ChunkSection.Size && + localZ >= 0 && localZ < ChunkSection.Size; + } +} diff --git a/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs b/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs new file mode 100644 index 00000000..a80f0b17 --- /dev/null +++ b/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs @@ -0,0 +1,165 @@ +using System.Collections.Concurrent; +using Minecraft.Data.Blocks; +using Minecraft.Schemas.Chunks; +using Minecraft.Schemas.Vec; + +namespace Minecraft.Implementations.Server.Terrain; + +/// +/// Represents a generation unit for a single chunk. +/// Provides methods to modify blocks and fork for cross-boundary modifications. +/// +public class GenerationUnit : IGenerationUnit { + private readonly ChunkData _chunk; + private readonly int _minY; + private readonly IGenerationModifier _modifier; + private readonly ConcurrentDictionary, List>> _pendingForkModifications; + + /// + /// Creates a new GenerationUnit for a single chunk. + /// + /// The chunk data to modify. + /// The minimum Y coordinate of the world (e.g., -64 for vanilla overworld). + /// Shared dictionary for storing pending fork modifications across chunks. + public GenerationUnit(ChunkData chunk, int minY, ConcurrentDictionary, List>> pendingForkModifications) { + _chunk = chunk; + _minY = minY; + _modifier = new ChunkGenerationModifier(chunk, minY); + _pendingForkModifications = pendingForkModifications; + } + + /// + public Vec3 AbsoluteStart() { + return new Vec3( + _chunk.ChunkX * ChunkSection.Size, + _minY, + _chunk.ChunkZ * ChunkSection.Size + ); + } + + /// + public Vec3 AbsoluteEnd() { + return new Vec3( + (_chunk.ChunkX + 1) * ChunkSection.Size, + _minY + _chunk.WorldHeight, + (_chunk.ChunkZ + 1) * ChunkSection.Size + ); + } + + /// + public IGenerationModifier Modifier() { + return _modifier; + } + + /// + public IGenerationUnit Fork(Vec3 start, Vec3 end) { + return new ForkedGenerationUnit(start, end, _minY, _chunk.WorldHeight, _chunk, _pendingForkModifications); + } +} + +/// +/// A forked generation unit that can span across multiple chunks. +/// Modifications are queued and applied when chunks are generated. +/// +public class ForkedGenerationUnit : IGenerationUnit { + private readonly Vec3 _start; + private readonly Vec3 _end; + private readonly int _minY; + private readonly int _worldHeight; + private readonly ChunkData _originChunk; + private readonly ConcurrentDictionary, List>> _pendingModifications; + private readonly ForkedGenerationModifier _modifier; + + internal ForkedGenerationUnit( + Vec3 start, + Vec3 end, + int minY, + int worldHeight, + ChunkData originChunk, + ConcurrentDictionary, List>> pendingModifications) { + _start = start; + _end = end; + _minY = minY; + _worldHeight = worldHeight; + _originChunk = originChunk; + _pendingModifications = pendingModifications; + _modifier = new ForkedGenerationModifier(this); + } + + /// + public Vec3 AbsoluteStart() => _start; + + /// + public Vec3 AbsoluteEnd() => _end; + + /// + public IGenerationModifier Modifier() => _modifier; + + /// + public IGenerationUnit Fork(Vec3 start, Vec3 end) { + // Nested forks reuse the same pending modifications dictionary + return new ForkedGenerationUnit(start, end, _minY, _worldHeight, _originChunk, _pendingModifications); + } + + /// + /// Applies a modification to all affected chunks. + /// If the chunk is the origin chunk, it applies immediately. + /// Otherwise, it queues the modification for later application. + /// + internal void ApplyModification(Action modification) { + // Calculate which chunks are affected + int startChunkX = (int)Math.Floor((double)_start.X / ChunkSection.Size); + int startChunkZ = (int)Math.Floor((double)_start.Z / ChunkSection.Size); + int endChunkX = (int)Math.Ceiling((double)_end.X / ChunkSection.Size); + int endChunkZ = (int)Math.Ceiling((double)_end.Z / ChunkSection.Size); + + for (int cx = startChunkX; cx < endChunkX; cx++) { + for (int cz = startChunkZ; cz < endChunkZ; cz++) { + Vec2 chunkPos = new(cx, cz); + + if (cx == _originChunk.ChunkX && cz == _originChunk.ChunkZ) { + // Apply immediately to the origin chunk + modification(_originChunk, _minY); + } + else { + // Queue for later application + List> actions = _pendingModifications.GetOrAdd(chunkPos, _ => []); + lock (actions) { + actions.Add(chunk => modification(chunk, _minY)); + } + } + } + } + } +} + +/// +/// A modifier for forked generation units that applies changes across multiple chunks. +/// +internal class ForkedGenerationModifier : IGenerationModifier { + private readonly ForkedGenerationUnit _unit; + + public ForkedGenerationModifier(ForkedGenerationUnit unit) { + _unit = unit; + } + + public void SetBlock(Vec3 position, IBlock block) { + _unit.ApplyModification((chunk, minY) => { + ChunkGenerationModifier modifier = new(chunk, minY); + modifier.SetBlock(position, block); + }); + } + + public void Fill(Vec3 start, Vec3 end, IBlock block) { + _unit.ApplyModification((chunk, minY) => { + ChunkGenerationModifier modifier = new(chunk, minY); + modifier.Fill(start, end, block); + }); + } + + public void FillHeight(int minY, int maxY, IBlock block) { + Vec3 start = _unit.AbsoluteStart(); + Vec3 end = _unit.AbsoluteEnd(); + Fill(new Vec3(start.X, minY, start.Z), new Vec3(end.X, maxY, end.Z), block); + } +} diff --git a/Minecraft/Implementations/Server/Terrain/IGenerationModifier.cs b/Minecraft/Implementations/Server/Terrain/IGenerationModifier.cs new file mode 100644 index 00000000..ce437c43 --- /dev/null +++ b/Minecraft/Implementations/Server/Terrain/IGenerationModifier.cs @@ -0,0 +1,34 @@ +using Minecraft.Data.Blocks; +using Minecraft.Schemas.Vec; + +namespace Minecraft.Implementations.Server.Terrain; + +/// +/// Interface for modifying blocks during terrain generation. +/// Provides methods to set blocks, fill areas, and perform other block modifications. +/// +public interface IGenerationModifier { + /// + /// Sets a single block at the specified position. + /// + /// The absolute position of the block. + /// The block to set. + void SetBlock(Vec3 position, IBlock block); + + /// + /// Fills a rectangular area with the specified block. + /// + /// The starting corner of the area (inclusive). + /// The ending corner of the area (exclusive). + /// The block to fill with. + void Fill(Vec3 start, Vec3 end, IBlock block); + + /// + /// Fills blocks at the specified Y-height range with the given block. + /// This fills all X/Z coordinates within the generation unit's bounds. + /// + /// The minimum Y coordinate (inclusive). + /// The maximum Y coordinate (exclusive). + /// The block to fill with. + void FillHeight(int minY, int maxY, IBlock block); +} diff --git a/Minecraft/Implementations/Server/Terrain/IGenerationUnit.cs b/Minecraft/Implementations/Server/Terrain/IGenerationUnit.cs new file mode 100644 index 00000000..058acce4 --- /dev/null +++ b/Minecraft/Implementations/Server/Terrain/IGenerationUnit.cs @@ -0,0 +1,40 @@ +using Minecraft.Schemas.Vec; + +namespace Minecraft.Implementations.Server.Terrain; + +/// +/// Represents a unit of terrain generation. +/// A GenerationUnit typically corresponds to a chunk or a region of chunks. +/// It provides methods to modify blocks within its boundaries and to create +/// forked units that can span across multiple chunk boundaries. +/// +public interface IGenerationUnit { + /// + /// Gets the absolute start position (minimum corner) of this generation unit. + /// + /// The absolute start position. + Vec3 AbsoluteStart(); + + /// + /// Gets the absolute end position (maximum corner, exclusive) of this generation unit. + /// + /// The absolute end position. + Vec3 AbsoluteEnd(); + + /// + /// Gets the modifier for this generation unit. + /// Use this to place blocks within the unit's boundaries. + /// + /// The generation modifier for this unit. + IGenerationModifier Modifier(); + + /// + /// Creates a forked generation unit that can span beyond the boundaries of this unit. + /// Forked units are useful for placing structures that cross chunk boundaries. + /// The modifications made to a forked unit will be applied when all relevant chunks are loaded. + /// + /// The absolute start position of the fork. + /// The absolute end position of the fork (exclusive). + /// A new generation unit representing the forked area. + IGenerationUnit Fork(Vec3 start, Vec3 end); +} diff --git a/Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs b/Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs new file mode 100644 index 00000000..222952fa --- /dev/null +++ b/Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs @@ -0,0 +1,14 @@ +namespace Minecraft.Implementations.Server.Terrain; + +/// +/// Interface for terrain generators that work with GenerationUnits. +/// Similar to but provides a higher-level API +/// with support for cross-chunk modifications through forking. +/// +public interface ITerrainGenerator { + /// + /// Generates terrain for the given generation unit. + /// + /// The generation unit to generate terrain for. + void Generate(IGenerationUnit unit); +} diff --git a/Minecraft/Implementations/Server/Terrain/LambdaTerrainGenerator.cs b/Minecraft/Implementations/Server/Terrain/LambdaTerrainGenerator.cs new file mode 100644 index 00000000..cc42f8dd --- /dev/null +++ b/Minecraft/Implementations/Server/Terrain/LambdaTerrainGenerator.cs @@ -0,0 +1,37 @@ +namespace Minecraft.Implementations.Server.Terrain; + +/// +/// A terrain generator that uses a lambda/delegate for generation. +/// This provides a convenient way to create terrain generators without +/// implementing the full interface. +/// +/// +/// +/// var generator = new LambdaTerrainGenerator(unit => { +/// var start = unit.AbsoluteStart(); +/// +/// // Create a snow carpet +/// unit.Modifier().FillHeight(-64, -60, Block.Snow); +/// +/// // Fork to add a tall structure +/// var fork = unit.Fork(start, start + new Vec3<int>(16, 32, 16)); +/// fork.Modifier().Fill(start, start + new Vec3<int>(3, 19, 3), Block.PowderSnow); +/// }); +/// +/// +public class LambdaTerrainGenerator : ITerrainGenerator { + private readonly Action _generator; + + /// + /// Creates a new LambdaTerrainGenerator with the specified generation action. + /// + /// The action that generates terrain for a generation unit. + public LambdaTerrainGenerator(Action generator) { + _generator = generator; + } + + /// + public void Generate(IGenerationUnit unit) { + _generator(unit); + } +} diff --git a/Minecraft/Implementations/Server/Terrain/Providers/GeneratorTerrainProvider.cs b/Minecraft/Implementations/Server/Terrain/Providers/GeneratorTerrainProvider.cs new file mode 100644 index 00000000..173db664 --- /dev/null +++ b/Minecraft/Implementations/Server/Terrain/Providers/GeneratorTerrainProvider.cs @@ -0,0 +1,72 @@ +using System.Collections.Concurrent; +using Minecraft.Schemas.Chunks; +using Minecraft.Schemas.Vec; + +namespace Minecraft.Implementations.Server.Terrain.Providers; + +/// +/// A terrain provider that wraps an . +/// This allows using the higher-level GenerationUnit API with the existing +/// terrain provider infrastructure. +/// +/// +/// This provider handles pending fork modifications by applying them when chunks are generated. +/// Fork modifications that target chunks not yet generated are stored and applied later. +/// +public class GeneratorTerrainProvider : ITerrainProvider { + private readonly ITerrainGenerator _generator; + private readonly int _minY; + private readonly ConcurrentDictionary, List>> _pendingForkModifications = new(); + + /// + /// Creates a new GeneratorTerrainProvider. + /// + /// The terrain generator to use. + /// The minimum Y coordinate of the world (default: -64 for vanilla overworld). + public GeneratorTerrainProvider(ITerrainGenerator generator, int minY = -64) { + _generator = generator; + _minY = minY; + } + + /// + /// Creates a new GeneratorTerrainProvider with a lambda generator. + /// + /// The generation action. + /// The minimum Y coordinate of the world (default: -64 for vanilla overworld). + public GeneratorTerrainProvider(Action generator, int minY = -64) + : this(new LambdaTerrainGenerator(generator), minY) { + } + + /// + public void GetChunk(ref ChunkData chunk) { + Vec2 chunkPos = new(chunk.ChunkX, chunk.ChunkZ); + + // Create a generation unit for this chunk + GenerationUnit unit = new(chunk, _minY, _pendingForkModifications); + + // Run the generator + _generator.Generate(unit); + + // Apply any pending fork modifications for this chunk + ApplyPendingModifications(chunk, chunkPos); + } + + /// + public void GetChunks(int start, int count, ChunkData[] chunks) { + for (int i = start; i < start + count; i++) { + GetChunk(ref chunks[i]); + } + } + + private void ApplyPendingModifications(ChunkData chunk, Vec2 chunkPos) { + if (!_pendingForkModifications.TryRemove(chunkPos, out List>? actions)) { + return; + } + + lock (actions) { + foreach (Action action in actions) { + action(chunk); + } + } + } +} diff --git a/Tests/GenerationUnitTest.cs b/Tests/GenerationUnitTest.cs new file mode 100644 index 00000000..34c721c0 --- /dev/null +++ b/Tests/GenerationUnitTest.cs @@ -0,0 +1,259 @@ +using System.Collections.Concurrent; +using Minecraft.Data.Generated; +using Minecraft.Implementations.Server.Terrain; +using Minecraft.Implementations.Server.Terrain.Providers; +using Minecraft.Schemas.Chunks; +using Minecraft.Schemas.Vec; + +namespace Tests; + +public class GenerationUnitTest { + + [Test] + public void GenerationUnit_AbsoluteStart_ReturnsCorrectPosition() { + // Arrange + ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 2, + ChunkZ = 3 + }; + ConcurrentDictionary, List>> pending = new(); + GenerationUnit unit = new(chunk, -64, pending); + + // Act + Vec3 start = unit.AbsoluteStart(); + + // Assert + Assert.That(start.X, Is.EqualTo(32)); // 2 * 16 + Assert.That(start.Y, Is.EqualTo(-64)); + Assert.That(start.Z, Is.EqualTo(48)); // 3 * 16 + } + + [Test] + public void GenerationUnit_AbsoluteEnd_ReturnsCorrectPosition() { + // Arrange + ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 2, + ChunkZ = 3 + }; + ConcurrentDictionary, List>> pending = new(); + GenerationUnit unit = new(chunk, -64, pending); + + // Act + Vec3 end = unit.AbsoluteEnd(); + + // Assert + Assert.That(end.X, Is.EqualTo(48)); // (2 + 1) * 16 + Assert.That(end.Y, Is.EqualTo(-64 + ChunkData.VanillaOverworldHeight)); + Assert.That(end.Z, Is.EqualTo(64)); // (3 + 1) * 16 + } + + [Test] + public void GenerationUnit_Modifier_SetBlock_SetsBlockInChunk() { + // Arrange + ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 0, + ChunkZ = 0 + }; + ConcurrentDictionary, List>> pending = new(); + GenerationUnit unit = new(chunk, -64, pending); + + // Act - Set a block at absolute position (5, 0, 7) + // Local position in chunk would be (5, 64, 7) - Y=0 absolute is Y=64 in chunk coords (0-based from minY=-64) + unit.Modifier().SetBlock(new Vec3(5, 0, 7), Block.Stone); + + // Assert - Check the block at local position + Assert.That(chunk.GetBlock(5, 64, 7), Is.EqualTo(Block.Stone.StateId)); + } + + [Test] + public void GenerationUnit_Modifier_FillHeight_FillsBlocksAtHeight() { + // Arrange + ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 0, + ChunkZ = 0 + }; + ConcurrentDictionary, List>> pending = new(); + GenerationUnit unit = new(chunk, -64, pending); + + // Act - Fill from Y=-64 to Y=-60 (4 layers) + unit.Modifier().FillHeight(-64, -60, Block.Bedrock); + + // Assert - Check some blocks in the filled area + // Y=-64 is local Y=0, Y=-60 is local Y=4 + Assert.That(chunk.GetBlock(0, 0, 0), Is.EqualTo(Block.Bedrock.StateId)); + Assert.That(chunk.GetBlock(15, 0, 15), Is.EqualTo(Block.Bedrock.StateId)); + Assert.That(chunk.GetBlock(8, 3, 8), Is.EqualTo(Block.Bedrock.StateId)); + // Y=4 should not be filled (exclusive end) + Assert.That(chunk.GetBlock(0, 4, 0), Is.Not.EqualTo(Block.Bedrock.StateId)); + } + + [Test] + public void GenerationUnit_Modifier_Fill_FillsArea() { + // Arrange + ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 0, + ChunkZ = 0 + }; + ConcurrentDictionary, List>> pending = new(); + GenerationUnit unit = new(chunk, -64, pending); + + // Act - Fill a 3x3x3 area starting at (2, -62, 2) = local (2, 2, 2) + unit.Modifier().Fill( + new Vec3(2, -62, 2), + new Vec3(5, -59, 5), + Block.Cobblestone + ); + + // Assert - Check blocks inside the filled area + Assert.That(chunk.GetBlock(2, 2, 2), Is.EqualTo(Block.Cobblestone.StateId)); + Assert.That(chunk.GetBlock(4, 4, 4), Is.EqualTo(Block.Cobblestone.StateId)); + // Check block outside the filled area + Assert.That(chunk.GetBlock(1, 2, 2), Is.Not.EqualTo(Block.Cobblestone.StateId)); + Assert.That(chunk.GetBlock(5, 2, 2), Is.Not.EqualTo(Block.Cobblestone.StateId)); // exclusive end + } + + [Test] + public void GenerationUnit_Fork_CreatesForkWithCorrectBounds() { + // Arrange + ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 0, + ChunkZ = 0 + }; + ConcurrentDictionary, List>> pending = new(); + GenerationUnit unit = new(chunk, -64, pending); + Vec3 start = unit.AbsoluteStart(); + + // Act - Create a fork that extends beyond the chunk + IGenerationUnit fork = unit.Fork(start, start + new Vec3(32, 64, 32)); + + // Assert + Assert.That(fork.AbsoluteStart(), Is.EqualTo(start)); + Assert.That(fork.AbsoluteEnd(), Is.EqualTo(start + new Vec3(32, 64, 32))); + } + + [Test] + public void GeneratorTerrainProvider_GeneratesChunkWithLambda() { + // Arrange + GeneratorTerrainProvider provider = new(unit => { + // Fill bottom layer with stone + unit.Modifier().FillHeight(-64, -63, Block.Stone); + }, -64); + + ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 0, + ChunkZ = 0 + }; + + // Act + provider.GetChunk(ref chunk); + + // Assert - Check that the bottom layer is filled with stone + Assert.That(chunk.GetBlock(0, 0, 0), Is.EqualTo(Block.Stone.StateId)); + Assert.That(chunk.GetBlock(15, 0, 15), Is.EqualTo(Block.Stone.StateId)); + Assert.That(chunk.GetBlock(0, 1, 0), Is.Not.EqualTo(Block.Stone.StateId)); + } + + [Test] + public void GeneratorTerrainProvider_Fork_ModifiesOriginChunkImmediately() { + // Arrange + GeneratorTerrainProvider provider = new(unit => { + Vec3 start = unit.AbsoluteStart(); + + // Fork to create a tall structure + IGenerationUnit fork = unit.Fork(start, start + new Vec3(16, 32, 16)); + fork.Modifier().SetBlock(start + new Vec3(5, 0, 5), Block.DiamondBlock); + }, -64); + + ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 0, + ChunkZ = 0 + }; + + // Act + provider.GetChunk(ref chunk); + + // Assert - Block should be set in the origin chunk + // start = (0, -64, 0), so (5, -64, 5) is local (5, 0, 5) + Assert.That(chunk.GetBlock(5, 0, 5), Is.EqualTo(Block.DiamondBlock.StateId)); + } + + [Test] + public void GeneratorTerrainProvider_Fork_QueuesPendingModificationsForOtherChunks() { + // Arrange + Vec3 targetPos = new(20, -64, 5); // This is in chunk (1, 0), not chunk (0, 0) + + GeneratorTerrainProvider provider = new(unit => { + Vec3 start = unit.AbsoluteStart(); + + // Only from chunk (0, 0), create a fork that extends to chunk (1, 0) + if (start.X == 0 && start.Z == 0) { + IGenerationUnit fork = unit.Fork(new Vec3(0, -64, 0), new Vec3(32, 0, 16)); + fork.Modifier().SetBlock(targetPos, Block.EmeraldBlock); + } + }, -64); + + ChunkData chunk0 = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 0, + ChunkZ = 0 + }; + ChunkData chunk1 = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 1, + ChunkZ = 0 + }; + + // Act - Generate first chunk, then second chunk + provider.GetChunk(ref chunk0); + provider.GetChunk(ref chunk1); + + // Assert - The block should appear in chunk (1, 0) + // targetPos (20, -64, 5) is local (4, 0, 5) in chunk (1, 0) + Assert.That(chunk1.GetBlock(4, 0, 5), Is.EqualTo(Block.EmeraldBlock.StateId)); + } + + [Test] + public void LambdaTerrainGenerator_SnowmanExample() { + // This test replicates the Java example from the issue + + // Arrange + int snowmanCount = 0; + Random random = new(42); // Fixed seed for reproducibility + + LambdaTerrainGenerator generator = new(unit => { + Vec3 start = unit.AbsoluteStart(); + + // Create a snow carpet for the snowmen + unit.Modifier().FillHeight(-64, -60, Block.Snow); + + // Exit out if unit is not the bottom unit, and exit 5 in 6 times otherwise + if (start.Y > -64 || random.Next(6) != 0) { + return; + } + + snowmanCount++; + + // Fork this section to add a tall snowman + // Add two extra sections worth of space (32 blocks height) + IGenerationUnit fork = unit.Fork(start, start + new Vec3(16, 32, 16)); + + // Add the snowman + fork.Modifier().Fill(start, start + new Vec3(3, 19, 3), Block.PowderSnow); + fork.Modifier().SetBlock(start + new Vec3(1, 19, 1), Block.JackOLantern); + }); + + GeneratorTerrainProvider provider = new(generator, -64); + + ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 0, + ChunkZ = 0 + }; + + // Act + provider.GetChunk(ref chunk); + + // Assert - Snow carpet should be present + Assert.That(chunk.GetBlock(0, 0, 0), Is.EqualTo(Block.Snow.StateId)); + Assert.That(chunk.GetBlock(8, 3, 8), Is.EqualTo(Block.Snow.StateId)); + // Y=4 (local) = Y=-60 (absolute) should not be filled (exclusive) + Assert.That(chunk.GetBlock(0, 4, 0), Is.Not.EqualTo(Block.Snow.StateId)); + } +} From d78828851ba5812a69b0b86d072205ac45af015d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:20:07 +0000 Subject: [PATCH 3/6] Address code review feedback: use integer division and bitwise operations Co-authored-by: CoPokBl <59753822+CoPokBl@users.noreply.github.com> --- .../Server/Terrain/ChunkGenerationModifier.cs | 6 +++--- .../Implementations/Server/Terrain/GenerationUnit.cs | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Minecraft/Implementations/Server/Terrain/ChunkGenerationModifier.cs b/Minecraft/Implementations/Server/Terrain/ChunkGenerationModifier.cs index 65b0bb24..71f71465 100644 --- a/Minecraft/Implementations/Server/Terrain/ChunkGenerationModifier.cs +++ b/Minecraft/Implementations/Server/Terrain/ChunkGenerationModifier.cs @@ -28,9 +28,9 @@ public void SetBlock(Vec3 position, IBlock block) { return; } - // Convert to chunk-local coordinates - int localX = (position.X % ChunkSection.Size + ChunkSection.Size) % ChunkSection.Size; - int localZ = (position.Z % ChunkSection.Size + ChunkSection.Size) % ChunkSection.Size; + // Convert to chunk-local coordinates using bitwise AND (ChunkSection.Size is 16, a power of 2) + int localX = position.X & (ChunkSection.Size - 1); + int localZ = position.Z & (ChunkSection.Size - 1); int localY = position.Y - _minY; if (localY < 0 || localY >= _chunk.WorldHeight) { diff --git a/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs b/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs index a80f0b17..87edd74a 100644 --- a/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs +++ b/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs @@ -107,11 +107,13 @@ public IGenerationUnit Fork(Vec3 start, Vec3 end) { /// Otherwise, it queues the modification for later application. /// internal void ApplyModification(Action modification) { - // Calculate which chunks are affected - int startChunkX = (int)Math.Floor((double)_start.X / ChunkSection.Size); - int startChunkZ = (int)Math.Floor((double)_start.Z / ChunkSection.Size); - int endChunkX = (int)Math.Ceiling((double)_end.X / ChunkSection.Size); - int endChunkZ = (int)Math.Ceiling((double)_end.Z / ChunkSection.Size); + // Calculate which chunks are affected using integer division + // For negative numbers, we need floor division, not truncation + int startChunkX = _start.X >= 0 ? _start.X / ChunkSection.Size : (_start.X - ChunkSection.Size + 1) / ChunkSection.Size; + int startChunkZ = _start.Z >= 0 ? _start.Z / ChunkSection.Size : (_start.Z - ChunkSection.Size + 1) / ChunkSection.Size; + // For end, we need ceiling division + int endChunkX = _end.X > 0 ? (_end.X + ChunkSection.Size - 1) / ChunkSection.Size : _end.X / ChunkSection.Size; + int endChunkZ = _end.Z > 0 ? (_end.Z + ChunkSection.Size - 1) / ChunkSection.Size : _end.Z / ChunkSection.Size; for (int cx = startChunkX; cx < endChunkX; cx++) { for (int cz = startChunkZ; cz < endChunkZ; cz++) { From 0bae5af005738e9357c58f30eea0090f6e3768f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 04:39:12 +0000 Subject: [PATCH 4/6] Refactor: ITerrainGenerator now extends ITerrainProvider, add multi-chunk support Co-authored-by: CoPokBl <59753822+CoPokBl@users.noreply.github.com> --- .../Server/Terrain/GenerationUnit.cs | 149 +++++++++++++++++- .../Server/Terrain/ITerrainGenerator.cs | 11 +- .../Server/Terrain/LambdaTerrainGenerator.cs | 53 ++++++- .../Providers/GeneratorTerrainProvider.cs | 72 --------- Tests/GenerationUnitTest.cs | 58 +++++-- 5 files changed, 244 insertions(+), 99 deletions(-) delete mode 100644 Minecraft/Implementations/Server/Terrain/Providers/GeneratorTerrainProvider.cs diff --git a/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs b/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs index 87edd74a..9d43d062 100644 --- a/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs +++ b/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs @@ -53,7 +53,125 @@ public IGenerationModifier Modifier() { /// public IGenerationUnit Fork(Vec3 start, Vec3 end) { - return new ForkedGenerationUnit(start, end, _minY, _chunk.WorldHeight, _chunk, _pendingForkModifications); + ChunkData[] singleChunkArray = [_chunk]; + return new ForkedGenerationUnit(start, end, _minY, _chunk.WorldHeight, singleChunkArray, 0, 1, _pendingForkModifications); + } +} + +/// +/// Represents a generation unit that spans multiple chunks. +/// Created when GetChunks is called with multiple chunks at once. +/// +public class MultiChunkGenerationUnit : IGenerationUnit { + private readonly ChunkData[] _chunks; + private readonly int _startIndex; + private readonly int _count; + private readonly int _minY; + private readonly IGenerationModifier _modifier; + private readonly ConcurrentDictionary, List>> _pendingForkModifications; + + /// + /// Creates a new MultiChunkGenerationUnit spanning multiple chunks. + /// + public MultiChunkGenerationUnit(ChunkData[] chunks, int startIndex, int count, int minY, + ConcurrentDictionary, List>> pendingForkModifications) { + _chunks = chunks; + _startIndex = startIndex; + _count = count; + _minY = minY; + _pendingForkModifications = pendingForkModifications; + _modifier = new MultiChunkGenerationModifier(chunks, startIndex, count, minY); + } + + /// + public Vec3 AbsoluteStart() { + int minX = int.MaxValue; + int minZ = int.MaxValue; + + for (int i = _startIndex; i < _startIndex + _count; i++) { + minX = Math.Min(minX, _chunks[i].ChunkX); + minZ = Math.Min(minZ, _chunks[i].ChunkZ); + } + + return new Vec3( + minX * ChunkSection.Size, + _minY, + minZ * ChunkSection.Size + ); + } + + /// + public Vec3 AbsoluteEnd() { + int maxX = int.MinValue; + int maxZ = int.MinValue; + int worldHeight = _chunks[_startIndex].WorldHeight; + + for (int i = _startIndex; i < _startIndex + _count; i++) { + maxX = Math.Max(maxX, _chunks[i].ChunkX); + maxZ = Math.Max(maxZ, _chunks[i].ChunkZ); + } + + return new Vec3( + (maxX + 1) * ChunkSection.Size, + _minY + worldHeight, + (maxZ + 1) * ChunkSection.Size + ); + } + + /// + public IGenerationModifier Modifier() { + return _modifier; + } + + /// + public IGenerationUnit Fork(Vec3 start, Vec3 end) { + return new ForkedGenerationUnit(start, end, _minY, _chunks[_startIndex].WorldHeight, _chunks, _startIndex, _count, _pendingForkModifications); + } +} + +/// +/// A modifier for multi-chunk generation units that applies changes across multiple chunks. +/// +internal class MultiChunkGenerationModifier : IGenerationModifier { + private readonly ChunkData[] _chunks; + private readonly int _startIndex; + private readonly int _count; + private readonly int _minY; + private readonly ChunkGenerationModifier[] _cachedModifiers; + + public MultiChunkGenerationModifier(ChunkData[] chunks, int startIndex, int count, int minY) { + _chunks = chunks; + _startIndex = startIndex; + _count = count; + _minY = minY; + _cachedModifiers = new ChunkGenerationModifier[count]; + for (int i = 0; i < count; i++) { + _cachedModifiers[i] = new ChunkGenerationModifier(chunks[startIndex + i], minY); + } + } + + public void SetBlock(Vec3 position, IBlock block) { + for (int i = 0; i < _count; i++) { + _cachedModifiers[i].SetBlock(position, block); + } + } + + public void Fill(Vec3 start, Vec3 end, IBlock block) { + for (int i = 0; i < _count; i++) { + _cachedModifiers[i].Fill(start, end, block); + } + } + + public void FillHeight(int minY, int maxY, IBlock block) { + for (int i = 0; i < _count; i++) { + int chunkAbsoluteX = _chunks[_startIndex + i].ChunkX * ChunkSection.Size; + int chunkAbsoluteZ = _chunks[_startIndex + i].ChunkZ * ChunkSection.Size; + _cachedModifiers[i].Fill( + new Vec3(chunkAbsoluteX, minY, chunkAbsoluteZ), + new Vec3(chunkAbsoluteX + ChunkSection.Size, maxY, chunkAbsoluteZ + ChunkSection.Size), + block + ); + } } } @@ -66,7 +184,9 @@ public class ForkedGenerationUnit : IGenerationUnit { private readonly Vec3 _end; private readonly int _minY; private readonly int _worldHeight; - private readonly ChunkData _originChunk; + private readonly ChunkData[] _originChunks; + private readonly int _originStartIndex; + private readonly int _originCount; private readonly ConcurrentDictionary, List>> _pendingModifications; private readonly ForkedGenerationModifier _modifier; @@ -75,13 +195,17 @@ internal ForkedGenerationUnit( Vec3 end, int minY, int worldHeight, - ChunkData originChunk, + ChunkData[] originChunks, + int originStartIndex, + int originCount, ConcurrentDictionary, List>> pendingModifications) { _start = start; _end = end; _minY = minY; _worldHeight = worldHeight; - _originChunk = originChunk; + _originChunks = originChunks; + _originStartIndex = originStartIndex; + _originCount = originCount; _pendingModifications = pendingModifications; _modifier = new ForkedGenerationModifier(this); } @@ -98,12 +222,12 @@ internal ForkedGenerationUnit( /// public IGenerationUnit Fork(Vec3 start, Vec3 end) { // Nested forks reuse the same pending modifications dictionary - return new ForkedGenerationUnit(start, end, _minY, _worldHeight, _originChunk, _pendingModifications); + return new ForkedGenerationUnit(start, end, _minY, _worldHeight, _originChunks, _originStartIndex, _originCount, _pendingModifications); } /// /// Applies a modification to all affected chunks. - /// If the chunk is the origin chunk, it applies immediately. + /// If the chunk is in the origin chunks, it applies immediately. /// Otherwise, it queues the modification for later application. /// internal void ApplyModification(Action modification) { @@ -119,9 +243,18 @@ internal void ApplyModification(Action modification) { for (int cz = startChunkZ; cz < endChunkZ; cz++) { Vec2 chunkPos = new(cx, cz); - if (cx == _originChunk.ChunkX && cz == _originChunk.ChunkZ) { + // Check if this chunk is one of the origin chunks + ChunkData? originChunk = null; + for (int i = _originStartIndex; i < _originStartIndex + _originCount; i++) { + if (_originChunks[i].ChunkX == cx && _originChunks[i].ChunkZ == cz) { + originChunk = _originChunks[i]; + break; + } + } + + if (originChunk != null) { // Apply immediately to the origin chunk - modification(_originChunk, _minY); + modification(originChunk, _minY); } else { // Queue for later application diff --git a/Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs b/Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs index 222952fa..483c8f1d 100644 --- a/Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs +++ b/Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs @@ -2,13 +2,20 @@ namespace Minecraft.Implementations.Server.Terrain; /// /// Interface for terrain generators that work with GenerationUnits. -/// Similar to but provides a higher-level API +/// Extends to provide a higher-level API /// with support for cross-chunk modifications through forking. /// -public interface ITerrainGenerator { +public interface ITerrainGenerator : ITerrainProvider { /// /// Generates terrain for the given generation unit. + /// The generation unit may span multiple chunks when GetChunks is called. /// /// The generation unit to generate terrain for. void Generate(IGenerationUnit unit); + + /// + /// The minimum Y coordinate of the world (e.g., -64 for vanilla overworld). + /// Used to convert between absolute and chunk-local coordinates. + /// + int MinY { get; } } diff --git a/Minecraft/Implementations/Server/Terrain/LambdaTerrainGenerator.cs b/Minecraft/Implementations/Server/Terrain/LambdaTerrainGenerator.cs index cc42f8dd..7e76635e 100644 --- a/Minecraft/Implementations/Server/Terrain/LambdaTerrainGenerator.cs +++ b/Minecraft/Implementations/Server/Terrain/LambdaTerrainGenerator.cs @@ -1,3 +1,7 @@ +using System.Collections.Concurrent; +using Minecraft.Schemas.Chunks; +using Minecraft.Schemas.Vec; + namespace Minecraft.Implementations.Server.Terrain; /// @@ -21,17 +25,64 @@ namespace Minecraft.Implementations.Server.Terrain; /// public class LambdaTerrainGenerator : ITerrainGenerator { private readonly Action _generator; + private readonly ConcurrentDictionary, List>> _pendingForkModifications = new(); + + /// + public int MinY { get; } /// /// Creates a new LambdaTerrainGenerator with the specified generation action. /// /// The action that generates terrain for a generation unit. - public LambdaTerrainGenerator(Action generator) { + /// The minimum Y coordinate of the world (default: -64 for vanilla overworld). + public LambdaTerrainGenerator(Action generator, int minY = -64) { _generator = generator; + MinY = minY; } /// public void Generate(IGenerationUnit unit) { _generator(unit); } + + /// + public void GetChunk(ref ChunkData chunk) { + Vec2 chunkPos = new(chunk.ChunkX, chunk.ChunkZ); + + // Create a generation unit for this chunk + GenerationUnit unit = new(chunk, MinY, _pendingForkModifications); + + // Run the generator + Generate(unit); + + // Apply any pending fork modifications for this chunk + ApplyPendingModifications(chunk, chunkPos); + } + + /// + public void GetChunks(int start, int count, ChunkData[] chunks) { + // Create a multi-chunk generation unit that spans all the chunks + MultiChunkGenerationUnit unit = new(chunks, start, count, MinY, _pendingForkModifications); + + // Run the generator once for all chunks + Generate(unit); + + // Apply any pending fork modifications for these chunks + for (int i = start; i < start + count; i++) { + Vec2 chunkPos = new(chunks[i].ChunkX, chunks[i].ChunkZ); + ApplyPendingModifications(chunks[i], chunkPos); + } + } + + private void ApplyPendingModifications(ChunkData chunk, Vec2 chunkPos) { + if (!_pendingForkModifications.TryRemove(chunkPos, out List>? actions)) { + return; + } + + lock (actions) { + foreach (Action action in actions) { + action(chunk); + } + } + } } diff --git a/Minecraft/Implementations/Server/Terrain/Providers/GeneratorTerrainProvider.cs b/Minecraft/Implementations/Server/Terrain/Providers/GeneratorTerrainProvider.cs deleted file mode 100644 index 173db664..00000000 --- a/Minecraft/Implementations/Server/Terrain/Providers/GeneratorTerrainProvider.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Collections.Concurrent; -using Minecraft.Schemas.Chunks; -using Minecraft.Schemas.Vec; - -namespace Minecraft.Implementations.Server.Terrain.Providers; - -/// -/// A terrain provider that wraps an . -/// This allows using the higher-level GenerationUnit API with the existing -/// terrain provider infrastructure. -/// -/// -/// This provider handles pending fork modifications by applying them when chunks are generated. -/// Fork modifications that target chunks not yet generated are stored and applied later. -/// -public class GeneratorTerrainProvider : ITerrainProvider { - private readonly ITerrainGenerator _generator; - private readonly int _minY; - private readonly ConcurrentDictionary, List>> _pendingForkModifications = new(); - - /// - /// Creates a new GeneratorTerrainProvider. - /// - /// The terrain generator to use. - /// The minimum Y coordinate of the world (default: -64 for vanilla overworld). - public GeneratorTerrainProvider(ITerrainGenerator generator, int minY = -64) { - _generator = generator; - _minY = minY; - } - - /// - /// Creates a new GeneratorTerrainProvider with a lambda generator. - /// - /// The generation action. - /// The minimum Y coordinate of the world (default: -64 for vanilla overworld). - public GeneratorTerrainProvider(Action generator, int minY = -64) - : this(new LambdaTerrainGenerator(generator), minY) { - } - - /// - public void GetChunk(ref ChunkData chunk) { - Vec2 chunkPos = new(chunk.ChunkX, chunk.ChunkZ); - - // Create a generation unit for this chunk - GenerationUnit unit = new(chunk, _minY, _pendingForkModifications); - - // Run the generator - _generator.Generate(unit); - - // Apply any pending fork modifications for this chunk - ApplyPendingModifications(chunk, chunkPos); - } - - /// - public void GetChunks(int start, int count, ChunkData[] chunks) { - for (int i = start; i < start + count; i++) { - GetChunk(ref chunks[i]); - } - } - - private void ApplyPendingModifications(ChunkData chunk, Vec2 chunkPos) { - if (!_pendingForkModifications.TryRemove(chunkPos, out List>? actions)) { - return; - } - - lock (actions) { - foreach (Action action in actions) { - action(chunk); - } - } - } -} diff --git a/Tests/GenerationUnitTest.cs b/Tests/GenerationUnitTest.cs index 34c721c0..8e4b406a 100644 --- a/Tests/GenerationUnitTest.cs +++ b/Tests/GenerationUnitTest.cs @@ -1,7 +1,6 @@ using System.Collections.Concurrent; using Minecraft.Data.Generated; using Minecraft.Implementations.Server.Terrain; -using Minecraft.Implementations.Server.Terrain.Providers; using Minecraft.Schemas.Chunks; using Minecraft.Schemas.Vec; @@ -132,9 +131,9 @@ public void GenerationUnit_Fork_CreatesForkWithCorrectBounds() { } [Test] - public void GeneratorTerrainProvider_GeneratesChunkWithLambda() { + public void LambdaTerrainGenerator_GeneratesChunkWithLambda() { // Arrange - GeneratorTerrainProvider provider = new(unit => { + LambdaTerrainGenerator generator = new(unit => { // Fill bottom layer with stone unit.Modifier().FillHeight(-64, -63, Block.Stone); }, -64); @@ -144,8 +143,8 @@ public void GeneratorTerrainProvider_GeneratesChunkWithLambda() { ChunkZ = 0 }; - // Act - provider.GetChunk(ref chunk); + // Act - Use as ITerrainProvider directly + generator.GetChunk(ref chunk); // Assert - Check that the bottom layer is filled with stone Assert.That(chunk.GetBlock(0, 0, 0), Is.EqualTo(Block.Stone.StateId)); @@ -154,9 +153,9 @@ public void GeneratorTerrainProvider_GeneratesChunkWithLambda() { } [Test] - public void GeneratorTerrainProvider_Fork_ModifiesOriginChunkImmediately() { + public void LambdaTerrainGenerator_Fork_ModifiesOriginChunkImmediately() { // Arrange - GeneratorTerrainProvider provider = new(unit => { + LambdaTerrainGenerator generator = new(unit => { Vec3 start = unit.AbsoluteStart(); // Fork to create a tall structure @@ -170,7 +169,7 @@ public void GeneratorTerrainProvider_Fork_ModifiesOriginChunkImmediately() { }; // Act - provider.GetChunk(ref chunk); + generator.GetChunk(ref chunk); // Assert - Block should be set in the origin chunk // start = (0, -64, 0), so (5, -64, 5) is local (5, 0, 5) @@ -178,11 +177,11 @@ public void GeneratorTerrainProvider_Fork_ModifiesOriginChunkImmediately() { } [Test] - public void GeneratorTerrainProvider_Fork_QueuesPendingModificationsForOtherChunks() { + public void LambdaTerrainGenerator_Fork_QueuesPendingModificationsForOtherChunks() { // Arrange Vec3 targetPos = new(20, -64, 5); // This is in chunk (1, 0), not chunk (0, 0) - GeneratorTerrainProvider provider = new(unit => { + LambdaTerrainGenerator generator = new(unit => { Vec3 start = unit.AbsoluteStart(); // Only from chunk (0, 0), create a fork that extends to chunk (1, 0) @@ -202,8 +201,8 @@ public void GeneratorTerrainProvider_Fork_QueuesPendingModificationsForOtherChun }; // Act - Generate first chunk, then second chunk - provider.GetChunk(ref chunk0); - provider.GetChunk(ref chunk1); + generator.GetChunk(ref chunk0); + generator.GetChunk(ref chunk1); // Assert - The block should appear in chunk (1, 0) // targetPos (20, -64, 5) is local (4, 0, 5) in chunk (1, 0) @@ -238,9 +237,7 @@ public void LambdaTerrainGenerator_SnowmanExample() { // Add the snowman fork.Modifier().Fill(start, start + new Vec3(3, 19, 3), Block.PowderSnow); fork.Modifier().SetBlock(start + new Vec3(1, 19, 1), Block.JackOLantern); - }); - - GeneratorTerrainProvider provider = new(generator, -64); + }, -64); ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { ChunkX = 0, @@ -248,7 +245,7 @@ public void LambdaTerrainGenerator_SnowmanExample() { }; // Act - provider.GetChunk(ref chunk); + generator.GetChunk(ref chunk); // Assert - Snow carpet should be present Assert.That(chunk.GetBlock(0, 0, 0), Is.EqualTo(Block.Snow.StateId)); @@ -256,4 +253,33 @@ public void LambdaTerrainGenerator_SnowmanExample() { // Y=4 (local) = Y=-60 (absolute) should not be filled (exclusive) Assert.That(chunk.GetBlock(0, 4, 0), Is.Not.EqualTo(Block.Snow.StateId)); } + + [Test] + public void MultiChunkGenerationUnit_GetChunks_SpansMultipleChunks() { + // Arrange + LambdaTerrainGenerator generator = new(unit => { + // When called with GetChunks, the unit should span all chunks + Vec3 start = unit.AbsoluteStart(); + Vec3 end = unit.AbsoluteEnd(); + + // Fill a layer across the entire unit + unit.Modifier().FillHeight(-64, -63, Block.GoldBlock); + }, -64); + + ChunkData[] chunks = [ + new(ChunkData.VanillaOverworldHeight) { ChunkX = 0, ChunkZ = 0 }, + new(ChunkData.VanillaOverworldHeight) { ChunkX = 1, ChunkZ = 0 }, + new(ChunkData.VanillaOverworldHeight) { ChunkX = 0, ChunkZ = 1 }, + new(ChunkData.VanillaOverworldHeight) { ChunkX = 1, ChunkZ = 1 } + ]; + + // Act - Use GetChunks to generate all at once + generator.GetChunks(0, chunks.Length, chunks); + + // Assert - All chunks should have the gold block layer + foreach (ChunkData chunk in chunks) { + Assert.That(chunk.GetBlock(0, 0, 0), Is.EqualTo(Block.GoldBlock.StateId)); + Assert.That(chunk.GetBlock(15, 0, 15), Is.EqualTo(Block.GoldBlock.StateId)); + } + } } From cf2e83b82c8566d2e9f972d805c326ce33c7a8ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 05:03:17 +0000 Subject: [PATCH 5/6] Add Fork with IBlockSetter for auto-expanding bounds Co-authored-by: CoPokBl <59753822+CoPokBl@users.noreply.github.com> --- .../Server/Terrain/GenerationUnit.cs | 88 +++++++++++++++++++ .../Server/Terrain/IBlockSetter.cs | 18 ++++ .../Server/Terrain/IGenerationUnit.cs | 9 ++ Tests/GenerationUnitTest.cs | 85 ++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 Minecraft/Implementations/Server/Terrain/IBlockSetter.cs diff --git a/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs b/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs index 9d43d062..e5330b07 100644 --- a/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs +++ b/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs @@ -56,6 +56,14 @@ public IGenerationUnit Fork(Vec3 start, Vec3 end) { ChunkData[] singleChunkArray = [_chunk]; return new ForkedGenerationUnit(start, end, _minY, _chunk.WorldHeight, singleChunkArray, 0, 1, _pendingForkModifications); } + + /// + public void Fork(Action setter) { + ChunkData[] singleChunkArray = [_chunk]; + AutoExpandingBlockSetter blockSetter = new(singleChunkArray, 0, 1, _minY, _chunk.WorldHeight, _pendingForkModifications); + setter(blockSetter); + blockSetter.Apply(); + } } /// @@ -127,6 +135,13 @@ public IGenerationModifier Modifier() { public IGenerationUnit Fork(Vec3 start, Vec3 end) { return new ForkedGenerationUnit(start, end, _minY, _chunks[_startIndex].WorldHeight, _chunks, _startIndex, _count, _pendingForkModifications); } + + /// + public void Fork(Action setter) { + AutoExpandingBlockSetter blockSetter = new(_chunks, _startIndex, _count, _minY, _chunks[_startIndex].WorldHeight, _pendingForkModifications); + setter(blockSetter); + blockSetter.Apply(); + } } /// @@ -225,6 +240,13 @@ public IGenerationUnit Fork(Vec3 start, Vec3 end) { return new ForkedGenerationUnit(start, end, _minY, _worldHeight, _originChunks, _originStartIndex, _originCount, _pendingModifications); } + /// + public void Fork(Action setter) { + AutoExpandingBlockSetter blockSetter = new(_originChunks, _originStartIndex, _originCount, _minY, _worldHeight, _pendingModifications); + setter(blockSetter); + blockSetter.Apply(); + } + /// /// Applies a modification to all affected chunks. /// If the chunk is in the origin chunks, it applies immediately. @@ -298,3 +320,69 @@ public void FillHeight(int minY, int maxY, IBlock block) { Fill(new Vec3(start.X, minY, start.Z), new Vec3(end.X, maxY, end.Z), block); } } + +/// +/// A block setter that automatically tracks bounds based on the blocks set. +/// After all blocks are set, call Apply() to create the fork with the computed bounds. +/// +internal class AutoExpandingBlockSetter : IBlockSetter { + private readonly ChunkData[] _originChunks; + private readonly int _originStartIndex; + private readonly int _originCount; + private readonly int _minY; + private readonly int _worldHeight; + private readonly ConcurrentDictionary, List>> _pendingModifications; + private readonly List<(Vec3 position, IBlock block)> _pendingBlocks = []; + + private int _minX = int.MaxValue; + private int _minBlockY = int.MaxValue; + private int _minZ = int.MaxValue; + private int _maxX = int.MinValue; + private int _maxBlockY = int.MinValue; + private int _maxZ = int.MinValue; + private bool _hasBlocks; + + public AutoExpandingBlockSetter( + ChunkData[] originChunks, + int originStartIndex, + int originCount, + int minY, + int worldHeight, + ConcurrentDictionary, List>> pendingModifications) { + _originChunks = originChunks; + _originStartIndex = originStartIndex; + _originCount = originCount; + _minY = minY; + _worldHeight = worldHeight; + _pendingModifications = pendingModifications; + } + + public void SetBlock(Vec3 position, IBlock block) { + _pendingBlocks.Add((position, block)); + + // Expand bounds + _minX = Math.Min(_minX, position.X); + _minBlockY = Math.Min(_minBlockY, position.Y); + _minZ = Math.Min(_minZ, position.Z); + _maxX = Math.Max(_maxX, position.X + 1); // +1 for exclusive end + _maxBlockY = Math.Max(_maxBlockY, position.Y + 1); + _maxZ = Math.Max(_maxZ, position.Z + 1); + _hasBlocks = true; + } + + /// + /// Applies all the pending block modifications using the computed bounds. + /// + public void Apply() { + if (!_hasBlocks) return; + + Vec3 start = new(_minX, _minBlockY, _minZ); + Vec3 end = new(_maxX, _maxBlockY, _maxZ); + + ForkedGenerationUnit fork = new(start, end, _minY, _worldHeight, _originChunks, _originStartIndex, _originCount, _pendingModifications); + + foreach ((Vec3 position, IBlock block) in _pendingBlocks) { + fork.Modifier().SetBlock(position, block); + } + } +} diff --git a/Minecraft/Implementations/Server/Terrain/IBlockSetter.cs b/Minecraft/Implementations/Server/Terrain/IBlockSetter.cs new file mode 100644 index 00000000..4bbc5e19 --- /dev/null +++ b/Minecraft/Implementations/Server/Terrain/IBlockSetter.cs @@ -0,0 +1,18 @@ +using Minecraft.Data.Blocks; +using Minecraft.Schemas.Vec; + +namespace Minecraft.Implementations.Server.Terrain; + +/// +/// Interface for setting blocks during terrain generation. +/// Used with the auto-expanding fork method where the fork bounds +/// are automatically determined based on the blocks set. +/// +public interface IBlockSetter { + /// + /// Sets a block at the specified position. + /// + /// The absolute position of the block. + /// The block to set. + void SetBlock(Vec3 position, IBlock block); +} diff --git a/Minecraft/Implementations/Server/Terrain/IGenerationUnit.cs b/Minecraft/Implementations/Server/Terrain/IGenerationUnit.cs index 058acce4..6ed082d0 100644 --- a/Minecraft/Implementations/Server/Terrain/IGenerationUnit.cs +++ b/Minecraft/Implementations/Server/Terrain/IGenerationUnit.cs @@ -37,4 +37,13 @@ public interface IGenerationUnit { /// The absolute end position of the fork (exclusive). /// A new generation unit representing the forked area. IGenerationUnit Fork(Vec3 start, Vec3 end); + + /// + /// Creates a forked generation unit with automatic bounds detection. + /// The fork's bounds are automatically determined based on the blocks set using the setter. + /// This is useful when you don't know the structure's size beforehand. + /// + /// An action that receives a block setter to place blocks. The fork bounds + /// are automatically expanded to encompass all blocks set. + void Fork(Action setter); } diff --git a/Tests/GenerationUnitTest.cs b/Tests/GenerationUnitTest.cs index 8e4b406a..130838d2 100644 --- a/Tests/GenerationUnitTest.cs +++ b/Tests/GenerationUnitTest.cs @@ -282,4 +282,89 @@ public void MultiChunkGenerationUnit_GetChunks_SpansMultipleChunks() { Assert.That(chunk.GetBlock(15, 0, 15), Is.EqualTo(Block.GoldBlock.StateId)); } } + + [Test] + public void GenerationUnit_ForkWithSetter_AutoExpandsBounds() { + // Arrange + LambdaTerrainGenerator generator = new(unit => { + Vec3 start = unit.AbsoluteStart(); + + // Create a snow carpet + unit.Modifier().FillHeight(-64, -60, Block.Snow); + + // Exit out if unit is not the bottom unit + if (start.Y > -64) { + return; + } + + // Use the setter-based fork (like the Java example) + unit.Fork(setter => { + for (int x = 0; x < 3; x++) { + for (int y = 0; y < 19; y++) { + for (int z = 0; z < 3; z++) { + setter.SetBlock(start + new Vec3(x, y, z), Block.PowderSnow); + } + } + } + setter.SetBlock(start + new Vec3(1, 19, 1), Block.JackOLantern); + }); + }, -64); + + ChunkData chunk = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 0, + ChunkZ = 0 + }; + + // Act + generator.GetChunk(ref chunk); + + // Assert - Snow carpet should be present at positions not overwritten by the fork + // The fork places blocks at x=0-2, z=0-2, so x=5, z=5 should still have snow + Assert.That(chunk.GetBlock(5, 0, 5), Is.EqualTo(Block.Snow.StateId)); + Assert.That(chunk.GetBlock(10, 2, 10), Is.EqualTo(Block.Snow.StateId)); + + // Assert - Snowman body should be present (powder snow) at fork location + // start = (0, -64, 0), fork places from y=0 to y=18 relative to start + // Position (0, -64, 0) is local (0, 0, 0) - has powder snow (overwrites snow) + Assert.That(chunk.GetBlock(0, 0, 0), Is.EqualTo(Block.PowderSnow.StateId)); + Assert.That(chunk.GetBlock(2, 18, 2), Is.EqualTo(Block.PowderSnow.StateId)); // Y=-46 absolute is Y=18 local + + // Assert - Jack o'lantern head at (1, 19, 1) relative, which is (1, -64+19, 1) = (1, -45, 1) absolute = (1, 19, 1) local + Assert.That(chunk.GetBlock(1, 19, 1), Is.EqualTo(Block.JackOLantern.StateId)); + } + + [Test] + public void GenerationUnit_ForkWithSetter_CrossesChunkBoundaries() { + // Arrange + Vec3 targetPos = new(18, -64, 5); // This is in chunk (1, 0), not chunk (0, 0) + + LambdaTerrainGenerator generator = new(unit => { + Vec3 start = unit.AbsoluteStart(); + + // Only from chunk (0, 0), create a structure using setter fork + if (start.X == 0 && start.Z == 0) { + unit.Fork(setter => { + // Set a block in the adjacent chunk + setter.SetBlock(targetPos, Block.EmeraldBlock); + }); + } + }, -64); + + ChunkData chunk0 = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 0, + ChunkZ = 0 + }; + ChunkData chunk1 = new(ChunkData.VanillaOverworldHeight) { + ChunkX = 1, + ChunkZ = 0 + }; + + // Act - Generate first chunk, then second chunk + generator.GetChunk(ref chunk0); + generator.GetChunk(ref chunk1); + + // Assert - The block should appear in chunk (1, 0) + // targetPos (18, -64, 5) is local (2, 0, 5) in chunk (1, 0) + Assert.That(chunk1.GetBlock(2, 0, 5), Is.EqualTo(Block.EmeraldBlock.StateId)); + } } From 9173389623644c7547e6286341e11250932ce1ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 06:02:49 +0000 Subject: [PATCH 6/6] Add more comprehensive tests for Fork with setter across chunk boundaries Co-authored-by: CoPokBl <59753822+CoPokBl@users.noreply.github.com> --- Tests/GenerationUnitTest.cs | 64 +++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/Tests/GenerationUnitTest.cs b/Tests/GenerationUnitTest.cs index 130838d2..1cc39ce8 100644 --- a/Tests/GenerationUnitTest.cs +++ b/Tests/GenerationUnitTest.cs @@ -367,4 +367,68 @@ public void GenerationUnit_ForkWithSetter_CrossesChunkBoundaries() { // targetPos (18, -64, 5) is local (2, 0, 5) in chunk (1, 0) Assert.That(chunk1.GetBlock(2, 0, 5), Is.EqualTo(Block.EmeraldBlock.StateId)); } + + [Test] + public void GenerationUnit_ForkWithSetter_SpansMultipleChunks() { + // Arrange - Test that a fork spanning multiple chunks works correctly + LambdaTerrainGenerator generator = new(unit => { + Vec3 start = unit.AbsoluteStart(); + + // Only from chunk (0, 0), create a structure using setter fork + if (start.X == 0 && start.Z == 0) { + unit.Fork(setter => { + // Set block in origin chunk (0, 0) + setter.SetBlock(new Vec3(5, -64, 5), Block.DiamondBlock); + // Set block in adjacent chunk (1, 0) + setter.SetBlock(new Vec3(20, -64, 5), Block.EmeraldBlock); + // Set block in another adjacent chunk (0, 1) + setter.SetBlock(new Vec3(5, -64, 20), Block.GoldBlock); + }); + } + }, -64); + + ChunkData chunk00 = new(ChunkData.VanillaOverworldHeight) { ChunkX = 0, ChunkZ = 0 }; + ChunkData chunk10 = new(ChunkData.VanillaOverworldHeight) { ChunkX = 1, ChunkZ = 0 }; + ChunkData chunk01 = new(ChunkData.VanillaOverworldHeight) { ChunkX = 0, ChunkZ = 1 }; + + // Act - Generate all chunks + generator.GetChunk(ref chunk00); + generator.GetChunk(ref chunk10); + generator.GetChunk(ref chunk01); + + // Assert - Each block should appear in its respective chunk + // DiamondBlock at (5, -64, 5) in chunk (0, 0) -> local (5, 0, 5) + Assert.That(chunk00.GetBlock(5, 0, 5), Is.EqualTo(Block.DiamondBlock.StateId)); + // EmeraldBlock at (20, -64, 5) in chunk (1, 0) -> local (4, 0, 5) + Assert.That(chunk10.GetBlock(4, 0, 5), Is.EqualTo(Block.EmeraldBlock.StateId)); + // GoldBlock at (5, -64, 20) in chunk (0, 1) -> local (5, 0, 4) + Assert.That(chunk01.GetBlock(5, 0, 4), Is.EqualTo(Block.GoldBlock.StateId)); + } + + [Test] + public void GenerationUnit_ForkWithSetter_TargetChunkGeneratedFirst() { + // Arrange - Test that pending modifications work even if target chunk is generated before origin chunk + // (This tests that the pending modification is stored correctly) + LambdaTerrainGenerator generator = new(unit => { + Vec3 start = unit.AbsoluteStart(); + + // Only from chunk (0, 0), create a structure using setter fork + if (start.X == 0 && start.Z == 0) { + unit.Fork(setter => { + // Set block in adjacent chunk (1, 0) + setter.SetBlock(new Vec3(20, -64, 5), Block.EmeraldBlock); + }); + } + }, -64); + + ChunkData chunk00 = new(ChunkData.VanillaOverworldHeight) { ChunkX = 0, ChunkZ = 0 }; + ChunkData chunk10 = new(ChunkData.VanillaOverworldHeight) { ChunkX = 1, ChunkZ = 0 }; + + // Act - Generate origin chunk first (creates pending modification), then target chunk + generator.GetChunk(ref chunk00); + generator.GetChunk(ref chunk10); + + // Assert - Block should appear in chunk (1, 0) + Assert.That(chunk10.GetBlock(4, 0, 5), Is.EqualTo(Block.EmeraldBlock.StateId)); + } }