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