diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnBiomeHunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnBiomeHunk.java new file mode 100644 index 000000000..8872b4c07 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnBiomeHunk.java @@ -0,0 +1,112 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.generation.chunk; + +import art.arcane.iris.platform.bukkit.BukkitBiome; +import art.arcane.volmlib.nativelib.terrain.NativeBiome; +import art.arcane.volmlib.util.hunk.HunkMutationSupport; +import art.arcane.volmlib.util.hunk.storage.StorageHunk; +import org.bukkit.block.Biome; + +/** + * Biome output for one Bukkit chunk, read back only by the generation stages. The biome actuator writes whole + * columns, so storage stays at one entry per column until a partial column write arrives; that write switches + * the hunk to full resolution. Reads, clamping and the plains fallback match {@link LinkedTerrainChunk}. + */ +public final class ColumnBiomeHunk extends StorageHunk { + private static final int CHUNK_SIZE = 16; + + private final NativeBiome[] columns = new NativeBiome[CHUNK_SIZE * CHUNK_SIZE]; + private NativeBiome[] cells; + private volatile NativeBiome defaultBiome; + + public ColumnBiomeHunk(int height) { + super(CHUNK_SIZE, height, CHUNK_SIZE); + } + + @Override + public void set(int x1, int y1, int z1, int x2, int y2, int z2, NativeBiome biome) { + if (x1 == x2 && z1 == z2 && y1 == 0 && y2 == getHeight() - 1) { + fillColumn(x1, z1, LinkedTerrainChunk.canonicalBiome(biome)); + return; + } + + HunkMutationSupport.setRangeInclusive(this, x1, y1, z1, x2, y2, z2, biome); + } + + @Override + public void setRaw(int x, int y, int z, NativeBiome biome) { + NativeBiome[] storage = cells == null ? expand() : cells; + storage[cellIndex(x, y, z)] = LinkedTerrainChunk.canonicalBiome(biome); + } + + @Override + public NativeBiome getRaw(int x, int y, int z) { + NativeBiome[] storage = cells; + NativeBiome biome = storage == null ? columns[columnIndex(x, z)] : storage[cellIndex(x, y, z)]; + if (biome != null) { + return biome; + } + NativeBiome fallback = defaultBiome; + if (fallback == null) { + fallback = BukkitBiome.of(Biome.PLAINS); + defaultBiome = fallback; + } + return fallback; + } + + private void fillColumn(int x, int z, NativeBiome biome) { + NativeBiome[] storage = cells; + if (storage == null) { + columns[columnIndex(x, z)] = biome; + return; + } + int stride = CHUNK_SIZE * CHUNK_SIZE; + int index = columnIndex(x, z); + for (int y = 0; y < getHeight(); y++) { + storage[index] = biome; + index += stride; + } + } + + private NativeBiome[] expand() { + NativeBiome[] storage = new NativeBiome[CHUNK_SIZE * getHeight() * CHUNK_SIZE]; + int stride = CHUNK_SIZE * CHUNK_SIZE; + for (int column = 0; column < stride; column++) { + NativeBiome biome = columns[column]; + if (biome == null) { + continue; + } + for (int index = column; index < storage.length; index += stride) { + storage[index] = biome; + } + } + cells = storage; + return storage; + } + + private static int columnIndex(int x, int z) { + return ((z & (CHUNK_SIZE - 1)) * CHUNK_SIZE) + (x & (CHUNK_SIZE - 1)); + } + + private int cellIndex(int x, int y, int z) { + int clampedY = Math.max(0, Math.min(getHeight() - 1, y)); + return (clampedY * CHUNK_SIZE * CHUNK_SIZE) + columnIndex(x, z); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/LinkedTerrainChunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/LinkedTerrainChunk.java index 8451bdcfe..8b6d37c41 100644 --- a/core/src/main/java/art/arcane/iris/generation/chunk/LinkedTerrainChunk.java +++ b/core/src/main/java/art/arcane/iris/generation/chunk/LinkedTerrainChunk.java @@ -130,7 +130,7 @@ public class LinkedTerrainChunk implements TerrainChunk { return (clampedY * CHUNK_SIZE + clampedZ) * CHUNK_SIZE + clampedX; } - private static NativeBiome canonicalBiome(NativeBiome biome) { + static NativeBiome canonicalBiome(NativeBiome biome) { if (biome instanceof BukkitBiome) { return biome; } diff --git a/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java index 556dcce12..7c04a6142 100644 --- a/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java @@ -72,7 +72,7 @@ import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.hunk.Hunk; -import art.arcane.iris.generation.chunk.TerrainChunkBiomeHunkView; +import art.arcane.iris.generation.chunk.ColumnBiomeHunk; import art.arcane.iris.generation.chunk.ChunkDataHunkHolder; import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.IrisContext; @@ -1298,12 +1298,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun try { Engine engine = getEngine(world); StudioGenerator selected = computeStudioGenerator(); - TerrainChunk tc = TerrainChunk.create(d); if (selected != null) { - selected.generateChunk(engine, tc, x, z); + selected.generateChunk(engine, TerrainChunk.create(d), x, z); } else { ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d); - Hunk biomes = new TerrainChunkBiomeHunkView(tc); + Hunk biomes = new ColumnBiomeHunk(d.getMaxHeight() - d.getMinHeight()); try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openGenerationHistoryCoordinateScope(engine, x << 4, z << 4); GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage"); diff --git a/core/src/test/java/art/arcane/iris/generation/chunk/LinkedTerrainChunkTest.java b/core/src/test/java/art/arcane/iris/generation/chunk/LinkedTerrainChunkTest.java index 54c386389..bc67df495 100644 --- a/core/src/test/java/art/arcane/iris/generation/chunk/LinkedTerrainChunkTest.java +++ b/core/src/test/java/art/arcane/iris/generation/chunk/LinkedTerrainChunkTest.java @@ -18,6 +18,7 @@ import org.junit.Test; import org.mockito.MockedStatic; import java.lang.reflect.Proxy; +import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -110,6 +111,48 @@ public class LinkedTerrainChunkTest { } } + @Test + public void columnBiomeHunkMatchesTheLinkedChunkView() { + NativeBiome plains = BukkitBiome.of(Biome.PLAINS); + NativeBiome desert = BukkitBiome.of(Biome.DESERT); + NativeBiome alternate = mock(NativeBiome.class); + when(alternate.nativeHandle()).thenReturn(Biome.DESERT); + NativeBiome[] palette = {plains, desert, alternate}; + for (int trial = 0; trial < 12; trial++) { + Random random = new Random(9127L + trial); + TerrainChunkBiomeHunkView expected = new TerrainChunkBiomeHunkView(chunk()); + ColumnBiomeHunk actual = new ColumnBiomeHunk(16); + int operations = 1 + random.nextInt(80); + for (int operation = 0; operation < operations; operation++) { + NativeBiome biome = palette[random.nextInt(palette.length)]; + int x = random.nextInt(16); + int z = random.nextInt(16); + int kind = random.nextInt(trial % 3 == 0 ? 1 : 3); + if (kind == 0) { + expected.set(x, 0, z, x, 15, z, biome); + actual.set(x, 0, z, x, 15, z, biome); + } else if (kind == 1) { + int minimumY = random.nextInt(16); + int maximumY = minimumY + random.nextInt(16 - minimumY); + expected.set(x, minimumY, z, x, maximumY, z, biome); + actual.set(x, minimumY, z, x, maximumY, z, biome); + } else { + int y = random.nextInt(20) - 2; + int rawX = x + (random.nextBoolean() ? 16 : 0); + expected.setRaw(rawX, y, z, biome); + actual.setRaw(rawX, y, z, biome); + } + } + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = -2; y < 18; y++) { + assertSame("biome " + x + "," + y + "," + z, expected.getRaw(x, y, z), actual.getRaw(x, y, z)); + } + } + } + } + } + private static LinkedTerrainChunk chunk() { ChunkData data = mock(ChunkData.class); when(data.getMinHeight()).thenReturn(-16);