diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java index 11a8992f7..19960216f 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_2_R1/NativeStructureWorldgenAccessTest.java @@ -3,6 +3,7 @@ package art.arcane.iris.platform.bukkit.nms.v26_2_R1; import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeStructureWorldgenAccess; import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet; import art.arcane.volmlib.util.structure.StructureTerrainMode; import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructurePostProcessor; import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator; @@ -231,7 +232,8 @@ public class NativeStructureWorldgenAccessTest { (x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> FLOOR_FIRST_FREE_Y, true, - position -> false); + position -> false, + NativeBlockChangeSet.disabled()); int x = generationCenter.getMiddleBlockX(); int z = generationCenter.getMiddleBlockZ(); @@ -249,7 +251,8 @@ public class NativeStructureWorldgenAccessTest { (x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> FLOOR_FIRST_FREE_Y, false, - position::equals); + position::equals, + NativeBlockChangeSet.disabled()); assertSame(Blocks.DIRT, access.getBlockState(position).getBlock()); assertFalse(access.ensureCanWrite(position)); @@ -320,7 +323,8 @@ public class NativeStructureWorldgenAccessTest { (x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> FLOOR_FIRST_FREE_Y, false, - position -> false); + position -> false, + NativeBlockChangeSet.disabled()); } private static ChunkPos generationCenter() { diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccessTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccessTest.java index 2d303788e..a40a5ee71 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccessTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/NativeStructureWorldgenAccessTest.java @@ -3,6 +3,7 @@ package art.arcane.iris.platform.bukkit.nms.v26_3_R1; import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureWorldgenAccess; import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet; import art.arcane.volmlib.util.structure.StructureTerrainMode; import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructurePostProcessor; import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTerrainIntegrator; @@ -231,7 +232,8 @@ public class NativeStructureWorldgenAccessTest { (x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> FLOOR_FIRST_FREE_Y, true, - position -> false); + position -> false, + NativeBlockChangeSet.disabled()); int x = generationCenter.getMiddleBlockX(); int z = generationCenter.getMiddleBlockZ(); @@ -249,7 +251,8 @@ public class NativeStructureWorldgenAccessTest { (x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> FLOOR_FIRST_FREE_Y, false, - position::equals); + position::equals, + NativeBlockChangeSet.disabled()); assertSame(Blocks.DIRT, access.getBlockState(position).getBlock()); assertFalse(access.ensureCanWrite(position)); @@ -320,7 +323,8 @@ public class NativeStructureWorldgenAccessTest { (x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> FLOOR_FIRST_FREE_Y, false, - position -> false); + position -> false, + NativeBlockChangeSet.disabled()); } private static ChunkPos generationCenter() { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructurePolicy.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructurePolicy.java index bbaef85c7..e9fc5f4e4 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructurePolicy.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedNativeStructurePolicy.java @@ -2,6 +2,7 @@ package art.arcane.iris.modded; import art.arcane.volmlib.nativelib.terrain.structure.StructureStagePolicy; import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.mantle.DeferredPlacementRecords; import art.arcane.iris.generation.terrain.IrisMaterialPalette; import art.arcane.iris.structure.nativegen.GenerationWritePolicy; import art.arcane.iris.structure.nativegen.IrisStructurePolicy; @@ -13,6 +14,7 @@ import art.arcane.iris.structure.nativegen.StructureLocateSearch; import art.arcane.iris.structure.placement.IrisStructureLocator; import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage; import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVolumeSource; +import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet; import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.structure.StructureLocateSearchAccess; @@ -107,6 +109,16 @@ final class ModdedNativeStructurePolicy implements NativeModdedStructureStage.Po context.getDimensionStackContext(), context.getMinHeight()); } + @Override + public NativeBlockChangeSet trackBlockChanges(Engine context, NativeBlockChangeSet.Bounds bounds) { + return DeferredPlacementRecords.track(context, bounds); + } + + @Override + public void invalidateBlockChanges(Engine context, NativeBlockChangeSet changes) { + DeferredPlacementRecords.invalidate(context, changes); + } + @Override public IntBinaryOperator surfaceHeight(Engine context) { return (x, z) -> Engine.hostHeight(context, x, z, true) + context.getMinHeight(); diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java index 7b91d9df7..ef89b580d 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedNativeStructureWorldgenAccessTest.java @@ -3,6 +3,7 @@ package art.arcane.iris.modded; import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedNativeStructureWorldgenAccess; import art.arcane.iris.structure.placement.IrisStructureTerrain; +import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet; import art.arcane.volmlib.util.structure.StructureTerrainMode; import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructurePostProcessor; import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator; @@ -231,7 +232,8 @@ public class ModdedNativeStructureWorldgenAccessTest { (x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> FLOOR_FIRST_FREE_Y, true, - position -> false); + position -> false, + NativeBlockChangeSet.disabled()); int x = generationCenter.getMiddleBlockX(); int z = generationCenter.getMiddleBlockZ(); @@ -249,7 +251,8 @@ public class ModdedNativeStructureWorldgenAccessTest { (x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> FLOOR_FIRST_FREE_Y, false, - position::equals); + position::equals, + NativeBlockChangeSet.disabled()); assertSame(Blocks.DIRT, access.getBlockState(position).getBlock()); assertFalse(access.ensureCanWrite(position)); @@ -270,6 +273,37 @@ public class ModdedNativeStructureWorldgenAccessTest { assertEquals(1, recording.mutations); } + @Test + public void acceptedWritesAndReportedChangesAreTrackedOutsideProtectedCells() { + RecordingDelegate recording = new RecordingDelegate(); + BlockPos protectedCell = generationCenter().getMiddleBlockPosition(70); + WorldGenLevel delegate = recording.world(); + NativeBlockChangeSet changes = NativeBlockChangeSet.create( + ModdedNativeStructureWorldgenAccess.changeBounds(delegate, generationCenter())); + ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create( + delegate, generationCenter(), + (x, z) -> SURFACE_FIRST_FREE_Y, + (x, z) -> FLOOR_FIRST_FREE_Y, + false, + protectedCell::equals, + changes); + BlockPos written = protectedCell.above(); + BlockPos removed = protectedCell.east(); + BlockPos reported = protectedCell.above(2); + + assertFalse(access.setBlock(protectedCell, Blocks.STONE.defaultBlockState(), 2)); + assertTrue(access.setBlock(written, Blocks.STONE.defaultBlockState(), 2)); + assertTrue(access.removeBlock(removed, false)); + access.recordBlockChange(reported.getX(), reported.getY(), reported.getZ()); + access.recordBlockChange(protectedCell.getX(), protectedCell.getY(), protectedCell.getZ()); + + assertEquals(3, changes.size()); + assertTrue(changes.contains(written.getX(), written.getY(), written.getZ())); + assertTrue(changes.contains(removed.getX(), removed.getY(), removed.getZ())); + assertTrue(changes.contains(reported.getX(), reported.getY(), reported.getZ())); + assertFalse(changes.contains(protectedCell.getX(), protectedCell.getY(), protectedCell.getZ())); + } + @Test public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() { RecordingDelegate recording = new RecordingDelegate(); @@ -320,7 +354,8 @@ public class ModdedNativeStructureWorldgenAccessTest { (x, z) -> SURFACE_FIRST_FREE_Y, (x, z) -> FLOOR_FIRST_FREE_Y, false, - position -> false); + position -> false, + NativeBlockChangeSet.disabled()); } private static ChunkPos generationCenter() { diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/DeferredPlacementRecords.java b/core/src/main/java/art/arcane/iris/generation/mantle/DeferredPlacementRecords.java new file mode 100644 index 000000000..88184f5b5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/mantle/DeferredPlacementRecords.java @@ -0,0 +1,90 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.integration.Identifier; +import art.arcane.iris.world.storage.matter.TileWrapper; +import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterSlice; + +public final class DeferredPlacementRecords { + private DeferredPlacementRecords() { + } + + public static NativeBlockChangeSet track(Engine engine, NativeBlockChangeSet.Bounds bounds) { + Mantle mantle = engine.getMantle().getMantle(); + for (int chunkX = bounds.minChunkX(); chunkX <= bounds.maxChunkX(); chunkX++) { + for (int chunkZ = bounds.minChunkZ(); chunkZ <= bounds.maxChunkZ(); chunkZ++) { + if (hasRecords(mantle, chunkX, chunkZ)) { + return NativeBlockChangeSet.create(bounds); + } + } + } + return NativeBlockChangeSet.disabled(); + } + + public static void invalidate(Engine engine, NativeBlockChangeSet changes) { + if (changes.isEmpty()) { + return; + } + Mantle mantle = engine.getMantle().getMantle(); + int minHeight = engine.getMinHeight(); + changes.forEachChunk((chunkX, chunkZ) -> invalidateChunk(mantle, changes, chunkX, chunkZ, minHeight)); + } + + private static boolean hasRecords(Mantle mantle, int chunkX, int chunkZ) { + MantleChunk chunk = mantle.useChunk(chunkX, chunkZ); + try { + synchronized (chunk) { + return hasRecords(chunk); + } + } finally { + chunk.release(); + } + } + + private static boolean hasRecords(MantleChunk chunk) { + for (int section = 0; section < chunk.sectionCount(); section++) { + Matter matter = chunk.get(section); + if (matter != null && (matter.hasSlice(Identifier.class) || matter.hasSlice(TileWrapper.class))) { + return true; + } + } + return false; + } + + private static void invalidateChunk(Mantle mantle, NativeBlockChangeSet changes, + int chunkX, int chunkZ, int minHeight) { + MantleChunk chunk = mantle.useChunk(chunkX, chunkZ); + try { + synchronized (chunk) { + if (!hasRecords(chunk)) { + return; + } + changes.forEachPosition(chunkX, chunkZ, (x, y, z) -> clearRecords(chunk, x & 15, y - minHeight, z & 15)); + } + } finally { + chunk.release(); + } + } + + private static void clearRecords(MantleChunk chunk, int x, int y, int z) { + if (y < 0 || (y >> 4) >= chunk.sectionCount()) { + return; + } + Matter section = chunk.get(y >> 4); + if (section == null) { + return; + } + clearRecord(section.getSlice(Identifier.class), x, y & 15, z); + clearRecord(section.getSlice(TileWrapper.class), x, y & 15, z); + } + + private static void clearRecord(MatterSlice slice, int x, int y, int z) { + if (slice != null) { + slice.set(x, y, z, null); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitStructureStagePolicy.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitStructureStagePolicy.java index 47f32c9f6..e67cc4331 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitStructureStagePolicy.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitStructureStagePolicy.java @@ -4,6 +4,7 @@ import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.volmlib.nativelib.terrain.structure.StructureCachePolicy; import art.arcane.volmlib.nativelib.terrain.structure.StructureStagePolicy; import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.mantle.DeferredPlacementRecords; import art.arcane.iris.generation.terrain.IrisMaterialPalette; import art.arcane.iris.structure.nativegen.GenerationWritePolicy; import art.arcane.iris.structure.nativegen.IrisStructurePolicy; @@ -13,6 +14,7 @@ import art.arcane.iris.structure.nativegen.NativeStructureStartPlan; import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex; import art.arcane.iris.structure.nativegen.StructureLocateSearch; import art.arcane.iris.structure.placement.IrisStructureLocator; +import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet; import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.structure.StructureLocateSearchAccess; @@ -103,6 +105,16 @@ public final class BukkitStructureStagePolicy implements StructureStagePolicy< context.getDimensionStackContext(), context.getMinHeight()); } + @Override + public NativeBlockChangeSet trackBlockChanges(Engine context, NativeBlockChangeSet.Bounds bounds) { + return DeferredPlacementRecords.track(context, bounds); + } + + @Override + public void invalidateBlockChanges(Engine context, NativeBlockChangeSet changes) { + DeferredPlacementRecords.invalidate(context, changes); + } + @Override public IntBinaryOperator surfaceHeight(Engine context) { return (x, z) -> Engine.hostHeight(context, x, z, true) + context.getMinHeight(); diff --git a/core/src/test/java/art/arcane/iris/platform/generation/DeferredPlacementInvalidationTest.java b/core/src/test/java/art/arcane/iris/platform/generation/DeferredPlacementInvalidationTest.java new file mode 100644 index 000000000..497f1fa1f --- /dev/null +++ b/core/src/test/java/art/arcane/iris/platform/generation/DeferredPlacementInvalidationTest.java @@ -0,0 +1,191 @@ +package art.arcane.iris.platform.generation; + +import art.arcane.iris.generation.block.TileData; +import art.arcane.iris.generation.mantle.DeferredPlacementRecords; +import art.arcane.iris.generation.mantle.EngineMantle; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.IrisEngineMantle; +import art.arcane.iris.integration.ExternalDataSVC; +import art.arcane.iris.integration.Identifier; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.testsupport.PlatformLeakGuard; +import art.arcane.iris.world.IrisWorld; +import art.arcane.iris.world.storage.matter.IrisMatterSupport; +import art.arcane.iris.world.storage.matter.PreObjectMatterTest; +import art.arcane.iris.world.storage.matter.TileWrapper; +import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import art.arcane.volmlib.util.collection.KMap; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter; +import art.arcane.volmlib.util.mantle.runtime.MantleHooks; +import art.arcane.volmlib.util.matter.Matter; +import org.bukkit.Chunk; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.junit.ClassRule; +import org.junit.Test; +import org.mockito.MockedStatic; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DeferredPlacementInvalidationTest { + @ClassRule + public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean(); + + private static final int MIN_HEIGHT = -64; + private static final int SECTIONS = 24; + + @Test + public void nativeWritesClearDeferredRecordsSoMaterializationSkipsThem() { + withMantleData(adapter -> { + Map> chunks = new HashMap<>(); + MantleChunk stored = chunk(chunks, adapter, 0, 0); + Identifier replaced = Identifier.fromString("craftengine:fence_post"); + Identifier kept = Identifier.fromString("craftengine:lantern"); + setRecord(stored, 3, 70, 5, Identifier.class, replaced); + setRecord(stored, 3, 76, 5, Identifier.class, kept); + setRecord(stored, 4, 70, 5, TileWrapper.class, + new TileWrapper(new TileData("minecraft:chest", new KMap<>()))); + Engine engine = engine(chunks, adapter); + + NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds()); + changes.add(3, 70, 5); + changes.add(4, 70, 5); + DeferredPlacementRecords.invalidate(engine, changes); + + assertNull(record(stored, 4, 70, 5, TileWrapper.class)); + assertEquals(kept, record(stored, 3, 76, 5, Identifier.class)); + World world = mock(World.class); + when(world.getMinHeight()).thenReturn(MIN_HEIGHT); + when(world.getMaxHeight()).thenReturn(320); + Chunk bukkitChunk = mock(Chunk.class); + when(bukkitChunk.getWorld()).thenReturn(world); + Block replacedBlock = mock(Block.class); + Block keptBlock = mock(Block.class); + when(bukkitChunk.getBlock(3, 70, 5)).thenReturn(replacedBlock); + when(bukkitChunk.getBlock(3, 76, 5)).thenReturn(keptBlock); + ExternalDataSVC provider = mock(ExternalDataSVC.class); + try (MockedStatic services = mockStatic(IrisServices.class)) { + services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(provider); + + EngineBukkitOps.materializeCustomBlocks(engine, bukkitChunk, stored); + } + + verify(provider).processUpdate(engine, keptBlock, kept); + verify(provider, never()).processUpdate(eq(engine), eq(replacedBlock), any(Identifier.class)); + }); + } + + @Test + public void regionsWithoutDeferredRecordsAreNotTracked() { + withMantleData(adapter -> { + Map> chunks = new HashMap<>(); + setRecord(chunk(chunks, adapter, 3, 0), 0, 70, 0, Identifier.class, + Identifier.fromString("craftengine:outside")); + + assertSame(NativeBlockChangeSet.disabled(), + DeferredPlacementRecords.track(engine(chunks, adapter), bounds())); + }); + } + + @Test + public void neighbouringRecordsEnableTrackingForTheWholeWriteRegion() { + withMantleData(adapter -> { + Map> chunks = new HashMap<>(); + setRecord(chunk(chunks, adapter, 1, -1), 0, 70, 0, TileWrapper.class, + new TileWrapper(new TileData("minecraft:barrel", new KMap<>()))); + + NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine(chunks, adapter), bounds()); + + assertNotSame(NativeBlockChangeSet.disabled(), changes); + changes.add(16, 70, -16); + DeferredPlacementRecords.invalidate(engine(chunks, adapter), changes); + assertNull(record(chunks.get(key(1, -1)), 0, 70, 0, TileWrapper.class)); + }); + } + + private static NativeBlockChangeSet.Bounds bounds() { + return new NativeBlockChangeSet.Bounds(-1, -1, 1, 1, MIN_HEIGHT, 384); + } + + @SuppressWarnings("unchecked") + private static Engine engine(Map> chunks, MantleDataAdapter adapter) { + Mantle mantle = mock(Mantle.class); + when(mantle.useChunk(anyInt(), anyInt())).thenAnswer(invocation -> + chunk(chunks, adapter, invocation.getArgument(0), invocation.getArgument(1)).use()); + EngineMantle engineMantle = mock(EngineMantle.class); + when(engineMantle.getMantle()).thenReturn(mantle); + IrisWorld world = mock(IrisWorld.class); + when(world.minHeight()).thenReturn(MIN_HEIGHT); + Engine engine = mock(Engine.class); + when(engine.getMantle()).thenReturn(engineMantle); + when(engine.getWorld()).thenReturn(world); + when(engine.getMinHeight()).thenReturn(MIN_HEIGHT); + return engine; + } + + private static MantleChunk chunk(Map> chunks, MantleDataAdapter adapter, + int chunkX, int chunkZ) { + return chunks.computeIfAbsent(key(chunkX, chunkZ), + ignored -> new MantleChunk<>(SECTIONS, chunkX, chunkZ, adapter, MantleHooks.NONE)); + } + + private static long key(int chunkX, int chunkZ) { + return ((long) chunkX << 32) | (chunkZ & 0xffffffffL); + } + + private static void setRecord(MantleChunk chunk, int x, int worldY, int z, Class type, T value) { + int y = worldY - MIN_HEIGHT; + chunk.getOrCreate(y >> 4).slice(type).set(x & 15, y & 15, z & 15, value); + } + + private static T record(MantleChunk chunk, int x, int worldY, int z, Class type) { + int y = worldY - MIN_HEIGHT; + return chunk.get(y >> 4).getSlice(type).get(x & 15, y & 15, z & 15); + } + + private static void withMantleData(MantleTest test) { + PreObjectMatterTest.setUpBukkit(); + IrisMatterSupport.ensureRegistered(); + IrisPlatform previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; + IrisPlatform platform = mock(IrisPlatform.class); + PlatformRegistries registries = mock(PlatformRegistries.class); + when(registries.block("AIR")).thenReturn(mock(NativeBlockState.class)); + when(platform.registries()).thenReturn(registries); + IrisPlatforms.unbind(); + IrisPlatforms.bind(platform); + try { + test.run(IrisEngineMantle.createRuntimeDataAdapter(mock(IrisData.class))); + } finally { + IrisPlatforms.unbind(); + if (previous != null) { + IrisPlatforms.bind(previous); + } + } + } + + @FunctionalInterface + private interface MantleTest { + void run(MantleDataAdapter adapter); + } +}