Cleared records under native writes

- Added DeferredPlacementRecords to gate tracking on Identifier and TileWrapper slices
- Cleared deferred custom and tile records at positions native structure placement changed
- Implemented block change tracking in the Bukkit and modded structure policies
- Covered invalidation through the custom block materialization pass
- Covered modded wrapper change tracking and protected cells
- Passed disabled change sets in existing wrapper tests
This commit is contained in:
Brian Neumann-Fopiano
2026-10-16 19:36:35 -04:00
parent a3eccd8a65
commit ff537f4abc
7 changed files with 357 additions and 9 deletions
@@ -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() {
@@ -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() {
@@ -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();
@@ -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() {
@@ -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<Matter> 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<Matter> mantle = engine.getMantle().getMantle();
int minHeight = engine.getMinHeight();
changes.forEachChunk((chunkX, chunkZ) -> invalidateChunk(mantle, changes, chunkX, chunkZ, minHeight));
}
private static boolean hasRecords(Mantle<Matter> mantle, int chunkX, int chunkZ) {
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
synchronized (chunk) {
return hasRecords(chunk);
}
} finally {
chunk.release();
}
}
private static boolean hasRecords(MantleChunk<Matter> 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<Matter> mantle, NativeBlockChangeSet changes,
int chunkX, int chunkZ, int minHeight) {
MantleChunk<Matter> 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<Matter> 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);
}
}
}
@@ -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();
@@ -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<Long, MantleChunk<Matter>> chunks = new HashMap<>();
MantleChunk<Matter> 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<IrisServices> 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<Long, MantleChunk<Matter>> 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<Long, MantleChunk<Matter>> 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<Long, MantleChunk<Matter>> chunks, MantleDataAdapter<Matter> adapter) {
Mantle<Matter> 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<Matter> chunk(Map<Long, MantleChunk<Matter>> chunks, MantleDataAdapter<Matter> 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 <T> void setRecord(MantleChunk<Matter> chunk, int x, int worldY, int z, Class<T> 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> T record(MantleChunk<Matter> chunk, int x, int worldY, int z, Class<T> 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<Matter> adapter);
}
}