diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/ChunkBlockUpdateListener.java b/core/src/main/java/art/arcane/iris/generation/runtime/ChunkBlockUpdateListener.java new file mode 100644 index 000000000..2860edbd1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/runtime/ChunkBlockUpdateListener.java @@ -0,0 +1,53 @@ +/* + * 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.runtime; + +import art.arcane.iris.generation.block.B; +import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; + +/** + * Forwards a chunk's block writes to {@link BlockUpdater#catchBlockUpdates} at world coordinates. + * The last state seen to need neither a block update nor custom handling is remembered, so the + * long runs of plain terrain skip the per-block checks. Racing writers only ever store such states. + */ +final class ChunkBlockUpdateListener implements ColumnExtentListeningHunk.Listener { + private final BlockUpdater updater; + private final int originX; + private final int originZ; + private NativeBlockState quiet; + + ChunkBlockUpdateListener(BlockUpdater updater, int originX, int originZ) { + this.updater = updater; + this.originX = originX; + this.originZ = originZ; + } + + @Override + public void onWrite(int x, int y, int z, NativeBlockState state) { + if (state == null || state == quiet) { + return; + } + if (B.isUpdatable(state) || state.isCustom()) { + updater.catchBlockUpdates(originX + x, y, originZ + z, state); + return; + } + quiet = state; + } +} 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 70cdd6b7c..dc35693c1 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 @@ -1472,7 +1472,7 @@ public class IrisEngine implements Engine { IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) { getEngineData().getStatistics().generatedChunk(); PrecisionStopwatch p = PrecisionStopwatch.start(); - Hunk blocks = new ColumnExtentListeningHunk<>(vblocks, (xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t)); + Hunk blocks = new ColumnExtentListeningHunk<>(vblocks, new ChunkBlockUpdateListener(this, x, z)); if (getDimension().isDebugChunkCrossSections() && ((x >> 4) % getDimension().getDebugCrossSectionsMod() == 0 || (z >> 4) % getDimension().getDebugCrossSectionsMod() == 0)) { NativeBlockState crossSection = B.getState("CRYING_OBSIDIAN"); diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/ChunkBlockUpdateListenerTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/ChunkBlockUpdateListenerTest.java new file mode 100644 index 000000000..d5d992380 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/ChunkBlockUpdateListenerTest.java @@ -0,0 +1,60 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ChunkBlockUpdateListenerTest { + @Test + public void forwardsExactlyTheWritesWithBlockUpdateEffects() { + NativeBlockState stone = state(false, false); + NativeBlockState deepslate = state(false, false); + NativeBlockState water = state(true, false); + NativeBlockState custom = state(false, true); + NativeBlockState[] palette = {stone, deepslate, water, custom, null}; + Random random = new Random(42L); + List expected = new ArrayList<>(); + List actual = new ArrayList<>(); + BlockUpdater reference = effects(expected); + ChunkBlockUpdateListener listener = new ChunkBlockUpdateListener(effects(actual), 160, -48); + + for (int write = 0; write < 20_000; write++) { + NativeBlockState state = palette[random.nextInt(8) < 5 ? random.nextInt(2) : random.nextInt(palette.length)]; + int x = random.nextInt(16); + int y = random.nextInt(384); + int z = random.nextInt(16); + reference.catchBlockUpdates(160 + x, y, -48 + z, state); + listener.onWrite(x, y, z, state); + } + + assertEquals(expected, actual); + } + + private static BlockUpdater effects(List log) { + return (x, y, z, data) -> { + if (data == null) { + return; + } + if (data.isUpdatable()) { + log.add("update " + x + "," + y + "," + z); + } + if (data.isCustom()) { + log.add("custom " + (x >> 4) + "," + (z >> 4)); + } + }; + } + + private static NativeBlockState state(boolean updatable, boolean custom) { + NativeBlockState state = mock(NativeBlockState.class); + when(state.isUpdatable()).thenReturn(updatable); + when(state.isCustom()).thenReturn(custom); + return state; + } +}