From bc618e20ecf724b6e16b77ede517c4f0d6707bec Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 11:11:11 -0400 Subject: [PATCH] Plain chunk output buffer - ChunkDataHunkHolder stores cells in a plain array and tracks each column's highest stored y - natural cave decoration and perfection stop scanning above the column top --- .../generation/chunk/ChunkDataHunkHolder.java | 49 +++++++-- .../iris/generation/chunk/ColumnExtent.java | 33 +++++++ .../chunk/ColumnExtentListeningHunk.java | 37 +++++++ .../iris/generation/runtime/IrisEngine.java | 3 +- .../generation/stage/IrisCarveModifier.java | 5 + .../stage/IrisPerfectionModifier.java | 3 +- .../chunk/ChunkDataHunkHolderTest.java | 99 +++++++++++++++++++ .../stage/IrisNaturalCaveScanTest.java | 68 ++++++++++++- .../IrisPerfectionModifierColumnTest.java | 44 +++++++++ 9 files changed, 331 insertions(+), 10 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtent.java create mode 100644 core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java create mode 100644 core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolderTest.java diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java b/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java index a7e6d5c69..157fc8fad 100644 --- a/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java @@ -23,19 +23,35 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.iris.generation.block.BoundBlockState; import art.arcane.iris.generation.block.IrisCustomData; -import art.arcane.volmlib.util.hunk.storage.AtomicHunk; +import art.arcane.volmlib.util.hunk.storage.StorageHunk; import org.bukkit.block.data.BlockData; import org.bukkit.generator.ChunkGenerator.ChunkData; -@SuppressWarnings("ClassCanBeRecord") -public class ChunkDataHunkHolder extends AtomicHunk implements NativeBlockVolume { +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Arrays; + +/** + * Plain-array block buffer for one chunk. Stages write disjoint columns when they fan out and every hand-off + * between threads is a join, so the cells need no per-access fences. Each column tracks its highest stored y; + * a raise is a CAS so concurrent writers to one column can never lose the maximum. + */ +public class ChunkDataHunkHolder extends StorageHunk implements NativeBlockVolume, ColumnExtent { private static final BoundBlockState AIR = BoundBlockState.of("AIR"); + private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class); private final ChunkData chunk; + private final NativeBlockState[] data; + private final int[] columnTops; + private final int zStride; public ChunkDataHunkHolder(ChunkData chunk) { super(16, chunk.getMaxHeight() - chunk.getMinHeight(), 16); this.chunk = chunk; + this.zStride = 16 * getHeight(); + this.data = new NativeBlockState[zStride * 16]; + this.columnTops = new int[256]; + Arrays.fill(columnTops, -1); } @Override @@ -56,7 +72,16 @@ public class ChunkDataHunkHolder extends AtomicHunk implements if (t == null) { return; } - super.setRaw(x, y, z, t); + data[index(x, y, z)] = t; + int column = (z << 4) | x; + int top = (int) COLUMN_TOPS.getOpaque(columnTops, column); + while (y > top) { + int witness = (int) COLUMN_TOPS.compareAndExchange(columnTops, column, top, y); + if (witness == top) { + break; + } + top = witness; + } } @Override @@ -65,13 +90,23 @@ public class ChunkDataHunkHolder extends AtomicHunk implements return AIR.get(); } - NativeBlockState b = super.getRaw(x, y, z); + NativeBlockState b = data[index(x, y, z)]; return b != null ? b : AIR.get(); } + @Override public NativeBlockState getStoredRaw(int x, int y, int z) { - return super.getRaw(x, y, z); + return data[index(x, y, z)]; + } + + @Override + public int highestStoredY(int x, int z) { + return (int) COLUMN_TOPS.getOpaque(columnTops, (z << 4) | x); + } + + private int index(int x, int y, int z) { + return (z * zStride) + (y << 4) + x; } public void apply() { @@ -90,7 +125,7 @@ public class ChunkDataHunkHolder extends AtomicHunk implements int runStart = -1; for (int y = 0; y < height; y++) { - NativeBlockState state = super.getRaw(x, y, z); + NativeBlockState state = getStoredRaw(x, y, z); BlockData block = state == null ? null : (BlockData) state.nativeHandle(); // Custom wrappers are not real Bukkit data; write the vanilla base like the // NMS fast path (NMSBinding.applyChunkDataBlocks) does. diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtent.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtent.java new file mode 100644 index 000000000..8629c1ef8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtent.java @@ -0,0 +1,33 @@ +/* + * 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.volmlib.util.hunk.Hunk; + +/** + * A block volume that knows, per column, the highest y it holds a value for. Every cell above that y reads as + * unwritten, so column scans can stop there without changing what they observe. + */ +public interface ColumnExtent { + int highestStoredY(int x, int z); + + static int highestStoredY(Hunk hunk, int x, int z) { + return hunk instanceof ColumnExtent extent ? extent.highestStoredY(x, z) : hunk.getHeight() - 1; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java new file mode 100644 index 000000000..bdbf33af2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java @@ -0,0 +1,37 @@ +/* + * 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.volmlib.util.function.Consumer4; +import art.arcane.volmlib.util.hunk.Hunk; +import art.arcane.volmlib.util.hunk.view.ListeningHunk; + +public final class ColumnExtentListeningHunk extends ListeningHunk implements ColumnExtent { + private final Hunk source; + + public ColumnExtentListeningHunk(Hunk source, Consumer4 listener) { + super(source, listener); + this.source = source; + } + + @Override + public int highestStoredY(int x, int z) { + return ColumnExtent.highestStoredY(source, x, z); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java index 1a14334f6..6998949ae 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java @@ -48,6 +48,7 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.atomics.AtomicRollingSequence; import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.IrisContext; +import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.hunk.Hunk; @@ -1465,7 +1466,7 @@ public class IrisEngine implements Engine { IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) { getEngineData().getStatistics().generatedChunk(); PrecisionStopwatch p = PrecisionStopwatch.start(); - Hunk blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t)); + Hunk blocks = new ColumnExtentListeningHunk<>(vblocks, (xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t)); if (getDimension().isDebugChunkCrossSections() && ((x >> 4) % getDimension().getDebugCrossSectionsMod() == 0 || (z >> 4) % getDimension().getDebugCrossSectionsMod() == 0)) { NativeBlockState crossSection = B.getState("CRYING_OBSIDIAN"); diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java index fe845b704..5034b3d74 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java @@ -40,6 +40,7 @@ import art.arcane.iris.generation.decoration.IrisProceduralBlocks; import art.arcane.iris.generation.hydrology.IrisRiverMaterialConfig; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; +import art.arcane.iris.generation.chunk.ColumnExtent; import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.IrisContext; import art.arcane.iris.generation.block.B; @@ -896,12 +897,16 @@ public class IrisCarveModifier extends EngineAssignedModifier int width = output.getWidth(); int depth = output.getDepth(); int height = output.getHeight(); + ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; for (int localX = 0; localX < width; localX++) { for (int localZ = 0; localZ < depth; localZ++) { int worldX = blockX + localX; int worldZ = blockZ + localZ; int floor = -1; for (int y = 1; y < height; y++) { + if (extent != null && y > extent.highestStoredY(localX, localZ)) { + break; + } NativeBlockState state = output.getRaw(localX, y, localZ); if (B.isSolid(state)) { if (floor >= 0) { diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java index 73ff947e4..308b890e3 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java @@ -18,6 +18,7 @@ package art.arcane.iris.generation.stage; +import art.arcane.iris.generation.chunk.ColumnExtent; import art.arcane.iris.generation.decoration.IrisSpeleothems; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.EngineAssignedModifier; @@ -203,7 +204,7 @@ public class IrisPerfectionModifier extends EngineAssignedModifier output, int x, int z) { - for (int i = output.getHeight() - 1; i >= 0; i--) { + for (int i = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); i >= 0; i--) { NativeBlockState b = output.get(x, i, z); if (b != null) { diff --git a/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolderTest.java b/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolderTest.java new file mode 100644 index 000000000..348aaba58 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolderTest.java @@ -0,0 +1,99 @@ +package art.arcane.iris.generation.chunk; + +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import org.bukkit.generator.ChunkGenerator; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ChunkDataHunkHolderTest { + @Test + public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() { + ChunkDataHunkHolder holder = holder(64); + NativeBlockState stone = mock(NativeBlockState.class); + + assertEquals(-1, holder.highestStoredY(3, 7)); + holder.setRaw(3, 40, 7, null); + assertEquals(-1, holder.highestStoredY(3, 7)); + holder.setRaw(3, 12, 7, stone); + holder.setRaw(3, 30, 7, stone); + holder.setRaw(3, 5, 7, stone); + + assertEquals(30, holder.highestStoredY(3, 7)); + assertEquals(-1, holder.highestStoredY(7, 3)); + assertSame(stone, holder.getStoredRaw(3, 30, 7)); + assertNull(holder.getStoredRaw(3, 31, 7)); + assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(holder, (x, y, z, state) -> { + }), 3, 7)); + } + + @Test + public void cellsKeepTheZMajorLayout() { + ChunkDataHunkHolder holder = holder(32); + List states = new ArrayList<>(); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 32; y++) { + NativeBlockState state = mock(NativeBlockState.class); + states.add(state); + holder.setRaw(x, y, z, state); + } + } + } + int index = 0; + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 32; y++) { + assertSame(states.get(index++), holder.getStoredRaw(x, y, z)); + } + } + } + } + + @Test + public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException { + for (int trial = 0; trial < 20; trial++) { + ChunkDataHunkHolder holder = holder(768); + NativeBlockState stone = mock(NativeBlockState.class); + int threads = 8; + CountDownLatch start = new CountDownLatch(1); + List writers = new ArrayList<>(); + for (int thread = 0; thread < threads; thread++) { + int offset = thread; + Thread writer = new Thread(() -> { + try { + start.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + for (int y = offset; y < 768; y += threads) { + holder.setRaw(9, y, 4, stone); + } + }); + writer.start(); + writers.add(writer); + } + start.countDown(); + for (Thread writer : writers) { + writer.join(); + } + assertEquals(767, holder.highestStoredY(9, 4)); + } + } + + private static ChunkDataHunkHolder holder(int height) { + ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class); + when(chunk.getMinHeight()).thenReturn(0); + when(chunk.getMaxHeight()).thenReturn(height); + return new ChunkDataHunkHolder(chunk); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java index 961f9e38b..9b16e3bcd 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java @@ -12,6 +12,11 @@ import art.arcane.iris.generation.decoration.IrisDecorator; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.iris.generation.block.B; +import art.arcane.iris.generation.chunk.ChunkDataHunkHolder; +import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import org.bukkit.generator.ChunkGenerator; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.matter.Matter; @@ -91,6 +96,46 @@ public class IrisNaturalCaveScanTest { verify(fixture.chunk).release(); } + @Test + public void columnExtentScanMatchesFullScanWhenDecorationBuildsAboveTheColumnTop() throws Exception { + IrisPlatforms.bind(mock(IrisPlatform.class)); + try { + Fixture full = new Fixture(true); + Hunk fullOutput = full.sparseOutput(Hunk.newArrayHunk(16, 32, 16)); + List fullWrites = new ArrayList<>(); + full.modifier.decorateNaturalCaves(-32, 48, + fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key()))); + + Fixture bounded = new Fixture(true); + ChunkGenerator.ChunkData chunkData = mock(ChunkGenerator.ChunkData.class); + doReturn(0).when(chunkData).getMinHeight(); + doReturn(32).when(chunkData).getMaxHeight(); + ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunkData); + bounded.sparseOutput(holder); + List boundedWrites = new ArrayList<>(); + bounded.modifier.decorateNaturalCaves(-32, 48, new ColumnExtentListeningHunk<>(holder, + (x, y, z, state) -> boundedWrites.add(x + ":" + y + ":" + z + ":" + state.key()))); + + assertTrue("fixture must decorate a zone above the initial column top", + full.calls.contains("floor:0:0:27:1")); + assertEquals(full.calls, bounded.calls); + assertEquals(fullWrites, boundedWrites); + assertEquals(full.markers, bounded.markers); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 32; y++) { + NativeBlockState expected = fullOutput.getRaw(x, y, z); + NativeBlockState actual = holder.getStoredRaw(x, y, z); + assertEquals("block " + x + ":" + y + ":" + z, + expected == null ? null : expected.key(), actual == null ? null : actual.key()); + } + } + } + } finally { + IrisPlatforms.unbind(); + } + } + private static void verifyScan(int width, int depth) throws Exception { Fixture fixture = new Fixture(); Hunk output = fixture.output(width, depth); @@ -169,8 +214,12 @@ public class IrisNaturalCaveScanTest { private final List calls = new ArrayList<>(); private final List markers = new ArrayList<>(); - @SuppressWarnings("unchecked") private Fixture() throws Exception { + this(false); + } + + @SuppressWarnings("unchecked") + private Fixture(boolean buildAboveTop) throws Exception { Engine engine = mock(Engine.class); EngineMantle engineMantle = mock(EngineMantle.class); Mantle mantle = mock(Mantle.class); @@ -211,6 +260,9 @@ public class IrisNaturalCaveScanTest { for (int height = 20; height <= 22; height++) { output.setRaw(x, height, z, air); } + if (buildAboveTop) { + output.setRaw(x, 31, z, stone); + } } return null; }).when(surface).decorate(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), eq(biome), any(), anyInt(), anyInt()); @@ -225,6 +277,20 @@ public class IrisNaturalCaveScanTest { }).when(roof).decorate(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), eq(biome), any(), anyInt(), anyInt()); } + private > H sparseOutput(H output) { + for (int x = 0; x < output.getWidth(); x++) { + for (int z = 0; z < output.getDepth(); z++) { + for (int y = 0; y < 32; y++) { + NativeBlockState state = initialState(this, y); + if (state != air) { + output.setRaw(x, y, z, state); + } + } + } + } + return output; + } + private Hunk output(int width, int depth) { Hunk output = Hunk.newArrayHunk(width, 32, depth); for (int x = 0; x < width; x++) { diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java index 8256667a7..e5f9618f9 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java @@ -1,6 +1,8 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.generation.block.B; +import art.arcane.iris.generation.chunk.ChunkDataHunkHolder; +import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk; import art.arcane.iris.generation.concurrent.BurstExecutor; import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.decoration.IrisProceduralBlocks; @@ -13,6 +15,7 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.hunk.storage.ArrayHunk; import org.bukkit.block.data.BlockData; +import org.bukkit.generator.ChunkGenerator; import org.junit.Rule; import org.junit.Test; @@ -24,6 +27,7 @@ import java.util.Random; import java.util.concurrent.ExecutorService; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -55,6 +59,46 @@ public class IrisPerfectionModifierColumnTest { } } + @Test + public void columnExtentTopSearchMatchesFullHeightSearch() { + Fixture fixture = new Fixture(); + Random random = new Random(55031L); + NativeBlockState[] palette = {null, fixture.air, fixture.water, fixture.stone, + fixture.decorant, fixture.half, fixture.spike(true, false), fixture.spike(false, true)}; + for (int trial = 0; trial < 8; trial++) { + ChunkDataHunkHolder bounded = holder(64); + ChunkDataHunkHolder full = holder(64); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int top = random.nextInt(64); + for (int y = 0; y <= top; y++) { + NativeBlockState state = y == 0 ? fixture.stone : palette[random.nextInt(palette.length)]; + bounded.setRaw(x, y, z, state); + full.setRaw(x, y, z, state); + } + } + } + fixture.modifier.onModify(0, 0, new ColumnExtentListeningHunk<>(bounded, (x, y, z, state) -> { + }), false, null); + fixture.modifier.onModify(0, 0, full.listen((x, y, z, state) -> { + }), false, null); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 64; y++) { + assertSame("voxel " + x + "," + y + "," + z, full.getStoredRaw(x, y, z), bounded.getStoredRaw(x, y, z)); + } + } + } + } + } + + private static ChunkDataHunkHolder holder(int height) { + ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class); + when(chunk.getMinHeight()).thenReturn(0); + when(chunk.getMaxHeight()).thenReturn(height); + return new ChunkDataHunkHolder(chunk); + } + @Test public void unrelatedTallColumnsAreScannedOnceDuringRepeatedCleanup() { Fixture fixture = new Fixture();