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));
+ }
}