Tracked generated content ownership

- Generated furniture keeps ownership tokens for targeted removal
- Native writes invalidate pending custom content and tile records
- Regeneration and chunk updates remove invalidated content on owning threads
- Provider callbacks return ownership tokens across server platforms
- Regression fixtures cover terrain replacement and restart cleanup
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-18 17:58:30 -04:00
1 parent ed5483d75a
commit 34417b2f5b
53 files changed
+2082 -110

No files matched your search

@@ -0,0 +1,154 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisEngineMantle;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.pack.loading.IrisData;
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.volmlib.nativelib.terrain.NativeBlockPoint;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class DeferredPlacementRegenerationTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final Identifier ID = Identifier.fromString("craftengine:example/chair");
private static final CustomBlockOwnership.Entry ORIGINAL = new CustomBlockOwnership.Entry(ID, "original", false);
private static final NativeBlockPoint POSITION = new NativeBlockPoint(3, 70, 5);
private Mantle<Matter> mantle;
private MantleChunk<Matter> chunk;
@Before
@SuppressWarnings("unchecked")
public void setUp() {
PreObjectMatterTest.setUpBukkit();
IrisMatterSupport.ensureRegistered();
chunk = spy(new MantleChunk<>(24, 1, -1,
IrisEngineMantle.createRuntimeDataAdapter(mock(IrisData.class)), MantleHooks.NONE));
mantle = mock(Mantle.class);
when(mantle.useChunk(1, -1)).thenAnswer(invocation -> chunk.use());
chunk.getOrCreate(POSITION.y() >> 4).slice(CustomBlockOwnership.class)
.set(POSITION.x(), POSITION.y() & 15, POSITION.z(), new CustomBlockOwnership(List.of(ORIGINAL)));
chunk.getOrCreate(POSITION.y() >> 4).slice(Identifier.class)
.set(POSITION.x(), POSITION.y() & 15, POSITION.z(), ID);
chunk.flag(MantleFlag.OBJECT, true);
chunk.flag(MantleFlag.of(200), true);
chunk.flag(MantleFlag.TILE, true);
chunk.flag(MantleFlag.CUSTOM, true);
chunk.flag(MantleFlag.UPDATE, true);
chunk.flag(MantleFlag.ETCHED, true);
chunk.flag(MantleFlag.INITIAL_SPAWNED_MARKER, true);
}
@Test
public void marginResetKeepsOwnershipAndPreventsDuplicateMaterialization() {
CustomBlockOwnership before = ownership();
DeferredPlacementRecords.resetMantleChunk(mantle, 1, -1, false);
assertSame(before, ownership());
assertNull(chunk.get(POSITION.x(), POSITION.y(), POSITION.z(), Identifier.class));
assertFalse(chunk.isFlagged(MantleFlag.OBJECT));
assertFalse(chunk.isFlagged(MantleFlag.of(200)));
assertTrue(chunk.isFlagged(MantleFlag.TILE));
assertTrue(chunk.isFlagged(MantleFlag.CUSTOM));
assertTrue(chunk.isFlagged(MantleFlag.UPDATE));
assertTrue(chunk.isFlagged(MantleFlag.INITIAL_SPAWNED_MARKER));
AtomicInteger placements = new AtomicInteger();
chunk.raiseFlagUnchecked(MantleFlag.ETCHED, placements::incrementAndGet);
assertEquals(0, placements.get());
verify(mantle, never()).deleteChunk(1, -1);
}
@Test
public void targetResetKeepsOwnershipUntilSuccessfulLiveReplacement() {
DeferredPlacementRecords.resetMantleChunk(mantle, 1, -1, true);
assertEquals(List.of(ORIGINAL), ownership().entries());
assertFalse(chunk.isFlagged(MantleFlag.TILE));
assertFalse(chunk.isFlagged(MantleFlag.CUSTOM));
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
assertFalse(chunk.isFlagged(MantleFlag.INITIAL_SPAWNED_MARKER));
}
@Test(timeout = 5000)
public void successfulTargetReplacementInvalidatesOldTokensWithoutDeletingNewRecords() {
DeferredPlacementRecords.resetMantleChunk(mantle, 1, -1, true);
chunk.getOrCreate(POSITION.y() >> 4).slice(Identifier.class)
.set(POSITION.x(), POSITION.y() & 15, POSITION.z(), ID);
DeferredPlacementRecords.invalidateCustomBlockOwnership(chunk);
assertEquals(List.of(new CustomBlockOwnership.Entry(ID, "original", true)), ownership().entries());
assertEquals(ID, chunk.get(POSITION.x(), POSITION.y(), POSITION.z(), Identifier.class));
}
@Test
public void ownershipAddedDuringResetSurvivesInTheSameChunk() throws Exception {
CountDownLatch resetting = new CountDownLatch(1);
CountDownLatch resume = new CountDownLatch(1);
AtomicBoolean firstRead = new AtomicBoolean(true);
doAnswer(invocation -> {
if (firstRead.compareAndSet(true, false)) {
resetting.countDown();
assertTrue(resume.await(5, TimeUnit.SECONDS));
}
return invocation.callRealMethod();
}).when(chunk).get(0);
Engine engine = mock(Engine.class);
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
CustomBlockOwnership.Entry concurrent = new CustomBlockOwnership.Entry(ID, "concurrent", false);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<?> reset = executor.submit(() -> DeferredPlacementRecords.resetMantleChunk(mantle, 1, -1, false));
assertTrue(resetting.await(5, TimeUnit.SECONDS));
Future<?> remember = executor.submit(() -> DeferredPlacementRecords.remember(engine, chunk, POSITION, concurrent));
resume.countDown();
reset.get(5, TimeUnit.SECONDS);
remember.get(5, TimeUnit.SECONDS);
assertEquals(List.of(ORIGINAL, new CustomBlockOwnership.Entry(ID, "concurrent", true)), ownership().entries());
assertFalse(chunk.inUse());
} finally {
resume.countDown();
executor.shutdownNow();
}
}
private CustomBlockOwnership ownership() {
return chunk.get(POSITION.x(), POSITION.y(), POSITION.z(), CustomBlockOwnership.class);
}
}
@@ -0,0 +1,299 @@
package art.arcane.iris.platform.generation;
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
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.EnginePlatformHooks;
import art.arcane.iris.generation.runtime.IrisEngineMantle;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.integration.ExternalDataSVC;
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.spi.PlatformScheduler;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.storage.matter.CustomBlockOwnershipMatter;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import art.arcane.iris.world.storage.matter.PreObjectMatterTest;
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
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.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class CustomBlockOwnershipTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final int MIN_HEIGHT = -64;
private static final NativeBlockPoint WORLD_POSITION = new NativeBlockPoint(19, 70, -11);
private static final NativeBlockPoint MANTLE_POSITION = new NativeBlockPoint(3, 134, 5);
private static final CustomBlockOwnership.Entry ORIGINAL = new CustomBlockOwnership.Entry(
Identifier.fromString("craftengine:example/chair"), "original-entity", false);
private static final CustomBlockOwnership.Entry REPLACEMENT = new CustomBlockOwnership.Entry(
Identifier.fromString("craftengine:example/chair"), "replacement-entity", false);
private IrisPlatform previous;
private PlatformScheduler scheduler;
private EnginePlatformHooks hooks;
private Engine engine;
private NativeWorld world;
private MantleChunk<Matter> chunk;
@Before
@SuppressWarnings("unchecked")
public void setUp() {
PreObjectMatterTest.setUpBukkit();
IrisMatterSupport.ensureRegistered();
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);
scheduler = mock(PlatformScheduler.class);
when(platform.scheduler()).thenReturn(scheduler);
IrisPlatforms.unbind();
IrisPlatforms.bind(platform);
chunk = new MantleChunk<>(24, 1, -1,
IrisEngineMantle.createRuntimeDataAdapter(mock(IrisData.class)), MantleHooks.NONE);
Mantle<Matter> mantle = mock(Mantle.class);
when(mantle.useChunk(1, -1)).thenAnswer(invocation -> chunk.use());
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getMantle()).thenReturn(mantle);
world = mock(NativeWorld.class);
when(world.isChunkLoaded(1, -1)).thenReturn(true);
IrisWorld irisWorld = mock(IrisWorld.class);
when(irisWorld.platformWorld()).thenReturn(world);
when(irisWorld.minHeight()).thenReturn(MIN_HEIGHT);
engine = mock(Engine.class);
when(engine.getWorld()).thenReturn(irisWorld);
when(engine.getMantle()).thenReturn(engineMantle);
when(engine.getMinHeight()).thenReturn(MIN_HEIGHT);
hooks = mock(EnginePlatformHooks.class);
IrisServices.register(EnginePlatformHooks.class, hooks);
}
@After
public void tearDown() {
IrisServices.remove(EnginePlatformHooks.class);
IrisPlatforms.unbind();
if (previous != null) {
IrisPlatforms.bind(previous);
}
}
@Test
public void materializedFurnitureEnablesTrackingAndRemovesOnOwningThread() {
remember(ORIGINAL);
NativeBlockChangeSet changes = changes();
assertNotSame(NativeBlockChangeSet.disabled(), changes);
DeferredPlacementRecords.invalidate(engine, changes);
verify(hooks, never()).removeCustomBlock(any(), any(), any());
CustomBlockOwnership.Entry pending = pendingOriginal();
assertEquals(List.of(pending), ownership().entries());
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pending)).thenReturn(true);
scheduledTask().run();
verify(hooks).removeCustomBlock(engine, WORLD_POSITION, pending);
assertNull(ownership());
}
@Test
public void queuedRemovalKeepsReplacementOwnership() {
remember(ORIGINAL);
DeferredPlacementRecords.invalidate(engine, changes());
remember(REPLACEMENT);
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
scheduledTask().run();
assertEquals(List.of(REPLACEMENT), ownership().entries());
verify(hooks, never()).removeCustomBlock(eq(engine), eq(WORLD_POSITION), eq(REPLACEMENT));
}
@Test
public void unloadedChunkRetainsRemovalUntilLoadedAndRetriesDeclinedCleanup() {
remember(ORIGINAL);
DeferredPlacementRecords.invalidate(engine, changes());
when(world.isChunkLoaded(1, -1)).thenReturn(false);
scheduledTask().run();
verify(hooks, never()).removeCustomBlock(any(), any(), any());
when(world.isChunkLoaded(1, -1)).thenReturn(true);
DeferredPlacementRecords.removeInvalidated(engine, 1, -1);
assertEquals(List.of(pendingOriginal()), ownership().entries());
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
DeferredPlacementRecords.removeInvalidated(engine, 1, -1);
assertNull(ownership());
}
@Test
public void ownershipPersistsPendingAndReplacementTokens() throws Exception {
CustomBlockOwnership expected = new CustomBlockOwnership(List.of(pendingOriginal(), REPLACEMENT));
CustomBlockOwnershipMatter matter = new CustomBlockOwnershipMatter();
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
matter.writeNode(expected, new DataOutputStream(bytes));
assertEquals(expected, matter.readNode(new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))));
}
@Test
public void invalidationDuringProviderCallbackRemovesCompletedPlacement() {
chunk.getOrCreate(MANTLE_POSITION.y() >> 4).slice(Identifier.class)
.set(MANTLE_POSITION.x(), MANTLE_POSITION.y() & 15, MANTLE_POSITION.z(), ORIGINAL.identifier());
DeferredPlacementRecords.invalidate(engine, changes());
DeferredPlacementRecords.remember(engine, chunk, MANTLE_POSITION, ORIGINAL);
assertEquals(List.of(pendingOriginal()), ownership().entries());
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
scheduledTask().run();
assertNull(ownership());
}
@Test(timeout = 10000)
public void providerCallbackAllowsConcurrentInvalidationAndSkipsCanceledSnapshotEntries() {
chunk.getOrCreate(MANTLE_POSITION.y() >> 4).slice(Identifier.class)
.set(MANTLE_POSITION.x(), MANTLE_POSITION.y() & 15, MANTLE_POSITION.z(), ORIGINAL.identifier());
chunk.getOrCreate(MANTLE_POSITION.y() >> 4).slice(Identifier.class)
.set(MANTLE_POSITION.x() + 1, MANTLE_POSITION.y() & 15, MANTLE_POSITION.z(), ORIGINAL.identifier());
NativeBlockChangeSet changes = changes();
changes.add(WORLD_POSITION.x() + 1, WORLD_POSITION.y(), WORLD_POSITION.z());
World bukkitWorld = mock(World.class);
when(bukkitWorld.getMinHeight()).thenReturn(MIN_HEIGHT);
when(bukkitWorld.getMaxHeight()).thenReturn(320);
Chunk bukkitChunk = mock(Chunk.class);
when(bukkitChunk.getWorld()).thenReturn(bukkitWorld);
Block started = mock(Block.class);
Block canceled = mock(Block.class);
when(bukkitChunk.getBlock(3, 70, 5)).thenReturn(started);
when(bukkitChunk.getBlock(4, 70, 5)).thenReturn(canceled);
ExternalDataSVC provider = mock(ExternalDataSVC.class);
IrisServices.register(ExternalDataSVC.class, provider);
try (ExecutorService worker = Executors.newVirtualThreadPerTaskExecutor()) {
when(provider.processUpdate(engine, started, ORIGINAL.identifier())).thenAnswer(invocation -> {
CompletableFuture.runAsync(() -> DeferredPlacementRecords.invalidate(engine, changes), worker)
.get(3, TimeUnit.SECONDS);
return ORIGINAL;
});
EngineBukkitOps.materializeCustomBlocks(engine, bukkitChunk, chunk);
verify(provider).processUpdate(engine, started, ORIGINAL.identifier());
verify(provider, never()).processUpdate(eq(engine), eq(canceled), any());
assertEquals(List.of(pendingOriginal()), ownership().entries());
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
scheduledTask().run();
assertNull(ownership());
} finally {
IrisServices.remove(ExternalDataSVC.class);
}
}
@Test
@SuppressWarnings("unchecked")
public void queuedInvalidationKeepsSelectedMantleAndHeightAfterRuntimeChanges() {
remember(ORIGINAL);
DeferredPlacementRecords.invalidate(engine, changes());
Mantle<Matter> replacementMantle = mock(Mantle.class);
EngineMantle replacementRuntime = mock(EngineMantle.class);
when(replacementRuntime.getMantle()).thenReturn(replacementMantle);
when(engine.getMantle()).thenReturn(replacementRuntime);
when(engine.getMinHeight()).thenReturn(0);
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
scheduledTask().run();
assertNull(ownership());
verify(hooks).removeCustomBlock(engine, WORLD_POSITION, pendingOriginal());
verify(replacementMantle, never()).useChunk(anyInt(), anyInt());
}
@Test
@SuppressWarnings("unchecked")
public void completedPlacementUsesSuppliedChunkWhenEngineSelectsAnotherMantle() {
Mantle<Matter> replacementMantle = mock(Mantle.class);
EngineMantle replacementRuntime = mock(EngineMantle.class);
when(replacementRuntime.getMantle()).thenReturn(replacementMantle);
when(engine.getMantle()).thenReturn(replacementRuntime);
DeferredPlacementRecords.remember(engine, chunk, MANTLE_POSITION, ORIGINAL);
when(engine.getMinHeight()).thenReturn(0);
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
scheduledTask().run();
assertNull(ownership());
verify(hooks).removeCustomBlock(engine, WORLD_POSITION, pendingOriginal());
verify(replacementMantle, never()).useChunk(anyInt(), anyInt());
}
private void remember(CustomBlockOwnership.Entry entry) {
chunk.getOrCreate(MANTLE_POSITION.y() >> 4).slice(Identifier.class)
.set(MANTLE_POSITION.x(), MANTLE_POSITION.y() & 15, MANTLE_POSITION.z(), entry.identifier());
DeferredPlacementRecords.remember(engine, chunk, MANTLE_POSITION, entry);
}
private NativeBlockChangeSet changes() {
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine,
new NativeBlockChangeSet.Bounds(1, -1, 1, -1, MIN_HEIGHT, 384));
changes.add(WORLD_POSITION.x(), WORLD_POSITION.y(), WORLD_POSITION.z());
return changes;
}
private Runnable scheduledTask() {
ArgumentCaptor<Runnable> task = ArgumentCaptor.forClass(Runnable.class);
verify(scheduler).region(eq(world), eq(1), eq(-1), task.capture());
return task.getValue();
}
private CustomBlockOwnership ownership() {
return chunk.get(MANTLE_POSITION.y() >> 4).getSlice(CustomBlockOwnership.class)
.get(MANTLE_POSITION.x(), MANTLE_POSITION.y() & 15, MANTLE_POSITION.z());
}
private static CustomBlockOwnership.Entry pendingOriginal() {
return new CustomBlockOwnership.Entry(ORIGINAL.identifier(), ORIGINAL.token(), true);
}
}
@@ -1,7 +1,9 @@
package art.arcane.iris.platform.generation;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.block.IrisCustomData;
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisEngineMantle;
@@ -12,6 +14,7 @@ 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.structure.nativegen.ImportedFeaturePolicy;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
@@ -26,19 +29,24 @@ 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.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.Optional;
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.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -105,6 +113,52 @@ public class DeferredPlacementInvalidationTest {
assertSame(NativeBlockChangeSet.disabled(),
DeferredPlacementRecords.track(engine(chunks, adapter), bounds()));
assertSame(NativeBlockChangeSet.disabled(),
DeferredPlacementRecords.positions(engine(chunks, adapter), bounds()));
});
}
@Test
public void importedFeatureCutRemovesFireRecordBeforeMaterialization() {
withMantleData(adapter -> {
Map<Long, MantleChunk<Matter>> chunks = new HashMap<>();
MantleChunk<Matter> stored = chunk(chunks, adapter, 0, 0);
Identifier fire = Identifier.fromString("craftengine:example/fire");
setRecord(stored, 3, 70, 5, Identifier.class, fire);
setRecord(stored, 4, 70, 5, Identifier.class, fire);
Engine engine = engine(chunks, adapter);
ImportedFeaturePolicy policy = new ImportedFeaturePolicy(engine);
NativeBlockChangeSet candidates = policy.trackedBlockPositions(bounds());
assertEquals(2, candidates.size());
NativeBlockChangeSet changes = NativeBlockChangeSet.create(candidates.bounds());
changes.add(3, 70, 5);
policy.invalidateBlockChanges(changes);
assertNull(record(stored, 3, 70, 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 cut = mock(Block.class);
Block extinguished = mock(Block.class);
when(cut.getType()).thenReturn(Material.AIR);
when(extinguished.getType()).thenReturn(Material.AIR);
when(bukkitChunk.getBlock(3, 70, 5)).thenReturn(cut);
when(bukkitChunk.getBlock(4, 70, 5)).thenReturn(extinguished);
BlockData carrier = mock(BlockData.class);
when(carrier.getMaterial()).thenReturn(Material.FIRE);
ExternalDataSVC provider = mock(ExternalDataSVC.class);
when(provider.getBlockData(fire)).thenReturn(Optional.of(IrisCustomData.of(carrier, fire)));
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(provider);
EngineBukkitOps.materializeCustomBlocks(engine, bukkitChunk, stored);
}
verify(provider, never()).processUpdate(eq(engine), eq(cut), any());
verify(provider).processUpdate(engine, extinguished, fire);
});
}
@@ -124,6 +178,27 @@ public class DeferredPlacementInvalidationTest {
});
}
@Test
public void importedFeatureCandidatesIncludeNeighbourOwnershipAndDeduplicateSlices() {
withMantleData(adapter -> {
Map<Long, MantleChunk<Matter>> chunks = new HashMap<>();
MantleChunk<Matter> stored = chunk(chunks, adapter, -1, 1);
Identifier identifier = Identifier.fromString("craftengine:example/chair");
setRecord(stored, 15, 70, 2, Identifier.class, identifier);
setRecord(stored, 15, 70, 2, TileWrapper.class,
new TileWrapper(new TileData("minecraft:chest", new KMap<>())));
setRecord(stored, 14, 71, 3, CustomBlockOwnership.class,
new CustomBlockOwnership(List.of(new CustomBlockOwnership.Entry(identifier, "entity-token", false))));
setRecord(chunk(chunks, adapter, 2, 0), 0, 70, 0, Identifier.class, identifier);
NativeBlockChangeSet positions = DeferredPlacementRecords.positions(engine(chunks, adapter), bounds());
assertEquals(2, positions.size());
assertTrue(positions.contains(-1, 70, 18));
assertTrue(positions.contains(-2, 71, 19));
});
}
private static NativeBlockChangeSet.Bounds bounds() {
return new NativeBlockChangeSet.Bounds(-1, -1, 1, 1, MIN_HEIGHT, 384);
}
@@ -51,6 +51,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
@@ -76,6 +77,7 @@ public class EngineBukkitOpsDeferredMaterializationTest {
iterator.accept(1, 2, 3, identifier);
return null;
}).when(stored).iterate(eq(Identifier.class), any());
doReturn(identifier).when(stored).get(1, 2, 3, Identifier.class);
Engine engine = mock(Engine.class);
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
@@ -89,6 +89,26 @@ public class EngineBukkitOpsStaleRecordTest {
verify(fixture.service).processUpdate(fixture.engine, intact, ORE);
}
@Test
public void extinguishedFireCarrierIsPlacedButCoveredFireIsSkipped() {
for (Material carrier : List.of(Material.FIRE, Material.SOUL_FIRE)) {
Fixture fixture = new Fixture();
fixture.carrier(ORE, carrier);
Block extinguished = fixture.block(1, 10, 1, Material.AIR);
Block caveAir = fixture.block(2, 10, 2, Material.CAVE_AIR);
Block wall = fixture.block(3, 10, 3, Material.COBBLESTONE);
fixture.materializeCustomBlocks(customRecords(
new Placed<>(1, 10, 1, ORE),
new Placed<>(2, 10, 2, ORE),
new Placed<>(3, 10, 3, ORE)));
verify(fixture.service).processUpdate(fixture.engine, extinguished, ORE);
verify(fixture.service).processUpdate(fixture.engine, caveAir, ORE);
verify(fixture.service, never()).processUpdate(any(), eq(wall), any());
}
}
@Test
public void airCarrierFurnitureRecordIsPlacedWhateverTheLiveBlock() {
Fixture fixture = new Fixture();
@@ -200,6 +220,9 @@ public class EngineBukkitOpsStaleRecordTest {
@SuppressWarnings("unchecked")
private static <T> MantleChunk<Matter> records(Class<T> type, Placed<T>[] records) {
MantleChunk<Matter> chunk = mock(MantleChunk.class);
for (Placed<T> record : records) {
when(chunk.get(record.x(), record.y(), record.z(), type)).thenReturn(record.value());
}
doAnswer((InvocationOnMock invocation) -> {
Consumer4<Integer, Integer, Integer, T> iterator = invocation.getArgument(1);
for (Placed<T> record : records) {