mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
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:
1 parent
ed5483d75a
commit
34417b2f5b
53 files changed
+2082
-110
No files matched your search
+154
@@ -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);
|
||||
}
|
||||
}
|
||||
+75
@@ -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);
|
||||
}
|
||||
|
||||
+2
@@ -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));
|
||||
|
||||
+23
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user