diff --git a/Minecraft/Implementations/Server/Terrain/ChunkGenerationModifier.cs b/Minecraft/Implementations/Server/Terrain/ChunkGenerationModifier.cs
new file mode 100644
index 00000000..71f71465
--- /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 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) {
+ 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..e5330b07
--- /dev/null
+++ b/Minecraft/Implementations/Server/Terrain/GenerationUnit.cs
@@ -0,0 +1,388 @@
+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) {
+ 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();
+ }
+}
+
+///
+/// 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);
+ }
+
+ ///
+ public void Fork(Action setter) {
+ AutoExpandingBlockSetter blockSetter = new(_chunks, _startIndex, _count, _minY, _chunks[_startIndex].WorldHeight, _pendingForkModifications);
+ setter(blockSetter);
+ blockSetter.Apply();
+ }
+}
+
+///
+/// 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
+ );
+ }
+ }
+}
+
+///
+/// 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[] _originChunks;
+ private readonly int _originStartIndex;
+ private readonly int _originCount;
+ private readonly ConcurrentDictionary, List>> _pendingModifications;
+ private readonly ForkedGenerationModifier _modifier;
+
+ internal ForkedGenerationUnit(
+ Vec3 start,
+ Vec3 end,
+ int minY,
+ int worldHeight,
+ ChunkData[] originChunks,
+ int originStartIndex,
+ int originCount,
+ ConcurrentDictionary, List>> pendingModifications) {
+ _start = start;
+ _end = end;
+ _minY = minY;
+ _worldHeight = worldHeight;
+ _originChunks = originChunks;
+ _originStartIndex = originStartIndex;
+ _originCount = originCount;
+ _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, _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.
+ /// Otherwise, it queues the modification for later application.
+ ///
+ internal void ApplyModification(Action modification) {
+ // 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++) {
+ Vec2 chunkPos = new(cx, cz);
+
+ // 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);
+ }
+ 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);
+ }
+}
+
+///
+/// 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/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..6ed082d0
--- /dev/null
+++ b/Minecraft/Implementations/Server/Terrain/IGenerationUnit.cs
@@ -0,0 +1,49 @@
+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);
+
+ ///
+ /// 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/Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs b/Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs
new file mode 100644
index 00000000..483c8f1d
--- /dev/null
+++ b/Minecraft/Implementations/Server/Terrain/ITerrainGenerator.cs
@@ -0,0 +1,21 @@
+namespace Minecraft.Implementations.Server.Terrain;
+
+///
+/// Interface for terrain generators that work with GenerationUnits.
+/// Extends to provide a higher-level API
+/// with support for cross-chunk modifications through forking.
+///
+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
new file mode 100644
index 00000000..7e76635e
--- /dev/null
+++ b/Minecraft/Implementations/Server/Terrain/LambdaTerrainGenerator.cs
@@ -0,0 +1,88 @@
+using System.Collections.Concurrent;
+using Minecraft.Schemas.Chunks;
+using Minecraft.Schemas.Vec;
+
+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;
+ 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.
+ /// 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/Tests/GenerationUnitTest.cs b/Tests/GenerationUnitTest.cs
new file mode 100644
index 00000000..1cc39ce8
--- /dev/null
+++ b/Tests/GenerationUnitTest.cs
@@ -0,0 +1,434 @@
+using System.Collections.Concurrent;
+using Minecraft.Data.Generated;
+using Minecraft.Implementations.Server.Terrain;
+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 LambdaTerrainGenerator_GeneratesChunkWithLambda() {
+ // Arrange
+ LambdaTerrainGenerator generator = 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 - 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));
+ 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 LambdaTerrainGenerator_Fork_ModifiesOriginChunkImmediately() {
+ // Arrange
+ LambdaTerrainGenerator generator = 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
+ 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)
+ Assert.That(chunk.GetBlock(5, 0, 5), Is.EqualTo(Block.DiamondBlock.StateId));
+ }
+
+ [Test]
+ public void LambdaTerrainGenerator_Fork_QueuesPendingModificationsForOtherChunks() {
+ // Arrange
+ Vec3 targetPos = new(20, -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 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
+ 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)
+ 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);
+ }, -64);
+
+ ChunkData chunk = new(ChunkData.VanillaOverworldHeight) {
+ ChunkX = 0,
+ ChunkZ = 0
+ };
+
+ // Act
+ generator.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));
+ }
+
+ [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));
+ }
+ }
+
+ [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));
+ }
+
+ [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));
+ }
+}