CrantEngine Tweak and a bit more.

- Fixed CE support
- Added some checks for things like paint and so on in the 3d Generator.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-08 19:22:55 -04:00
1 parent 0782bbf0b4
commit 4de1aea8b9
44 files changed
+3161 -244

No files matched your search

@@ -7,6 +7,8 @@ import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.framework.GenerationTransitionGate;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
@@ -35,7 +37,14 @@ import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
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.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -195,6 +204,70 @@ public class BoardSVCSavedBiomeTest {
}
}
@Test
public void ordinaryRefreshLeavesOwnerThreadAvailableForStudioCheckpoint() throws Exception {
GenerationSessionManager sessions = new GenerationSessionManager(true);
AtomicInteger height = new AtomicInteger(70);
AtomicInteger checkpoints = new AtomicInteger();
ExecutorService owner = Executors.newSingleThreadExecutor();
ExecutorService reloader = Executors.newSingleThreadExecutor();
Fixture fixture = null;
try {
fixture = owner.submit(() -> {
Fixture created = new Fixture();
when(created.engine.getGenerationSessions()).thenReturn(sessions);
when(created.engine.getHeight(8, 8)).thenAnswer(invocation -> {
try (GenerationTransitionGate.Participation ignored = sessions.transitionGate().enter()) {
return height.get();
}
});
created.service.updatePlayer(created.player);
return created;
}).get(3L, TimeUnit.SECONDS);
Fixture board = fixture;
List<String> previous = board.service.getLines(board.player);
assertTrue(board.lines().contains("70"));
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
try (GenerationTransitionGate.Transition ignored = sessions.transitionGate().beginTransition(2_000L)) {
sessions.sealAndAwait("Studio generation cutover", 2_000L);
Future<?> refresh = owner.submit(board::tick);
owner.submit(checkpoints::incrementAndGet).get(2L, TimeUnit.SECONDS);
refresh.get(2L, TimeUnit.SECONDS);
assertEquals(previous, board.service.getLines(board.player));
verify(board.engine, times(2)).getGeneratedPerSecond();
verify(board.engine, times(1)).getBiomeOrMantleEnvironment(8, 160, 8);
verify(board.engine, times(1)).getHeight(8, 8);
height.set(85);
sessions.activateNextSession();
} catch (Exception failure) {
throw new IllegalStateException(failure);
}
}, reloader);
cutover.get(3L, TimeUnit.SECONDS);
assertEquals(1, checkpoints.get());
owner.submit(board::tick).get(2L, TimeUnit.SECONDS);
assertTrue(board.lines().contains("85"));
assertEquals(1, board.ticks.size());
assertEquals(0, sessions.activeLeases());
verify(board.engine, times(2)).getBiomeOrMantleEnvironment(8, 160, 8);
verify(board.engine, times(2)).getHeight(8, 8);
} finally {
try {
if (fixture != null) {
Fixture board = fixture;
owner.submit(board::close).get(2L, TimeUnit.SECONDS);
}
} finally {
owner.shutdownNow();
reloader.shutdownNow();
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS));
}
}
}
private static BiomeEnvironment environment(String regionName, String biomeName) {
IrisRegion region = mock(IrisRegion.class);
IrisBiome biome = mock(IrisBiome.class);
@@ -15,6 +15,7 @@ import org.junit.Test;
import java.lang.reflect.Field;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Optional;
import java.util.Set;
import static org.junit.Assert.assertEquals;
@@ -29,6 +30,43 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ExternalDataSVCBlockRoutingTest {
@Test
public void directPlacementRetainsQualifiedProviderAndProperties() throws Exception {
Identifier nativeId = Identifier.fromString("default:palm_log");
Identifier stateId = Identifier.fromString("default:palm_log[axis=x]");
ExternalDataProvider provider = provider("CraftEngine", nativeId);
Block block = mock(Block.class);
when(provider.placeBlock(block, stateId)).thenReturn(true);
assertEquals(true, service(provider).placeBlock(block,
Identifier.fromString("craftengine:default/palm_log[axis=x]")));
verify(provider).placeBlock(block, stateId);
}
@Test
public void capturedBlocksRetainSemanticPropertiesAndProviderIdentity() throws Exception {
Identifier id = Identifier.fromString("example:palm_log[axis=x]");
ExternalDataProvider provider = provider("CraftEngine", id);
BlockData base = mock(BlockData.class);
when(base.getAsString()).thenReturn("craftengine:custom_18");
when(provider.identifyBlock(base)).thenReturn(Optional.of(id));
ExternalDataSVC service = service(provider);
IrisCustomData captured = (IrisCustomData) service.captureBlockData(base);
assertSame(base, captured.getBase());
assertEquals("craftengine:example/palm_log[axis=x]", captured.getCustom().toString());
assertSame(captured, service.captureBlockData(captured));
}
@Test
public void vanillaCaptureKeepsOriginalBlockData() throws Exception {
ExternalDataProvider provider = provider("CraftEngine", Identifier.fromString("example:wood"));
BlockData base = mock(BlockData.class);
assertSame(base, service(provider).captureBlockData(base));
}
@Test
public void nativeLookupPinsProviderAndRetainsPropertiesUntilPlacement() throws Exception {
Identifier nativeId = Identifier.fromString("example:wood");
@@ -0,0 +1,67 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class ObjectSVCUndoCustomBlockTest {
@Test
public void undoRestoresSemanticCustomStateOnItsOwningRegion() {
Block block = mock(Block.class);
Location location = new Location(mock(World.class), 7, 80, -9);
when(block.getLocation()).thenReturn(location);
Identifier id = new Identifier("craftengine", "test/lamp[lit=false]");
BlockData custom = IrisCustomData.of(mock(BlockData.class), id);
ExternalDataSVC external = mock(ExternalDataSVC.class);
when(external.placeBlock(block, id)).thenReturn(true);
List<Runnable> ownerTasks = new ArrayList<>();
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class);
MockedStatic<J> scheduling = mockStatic(J.class)) {
services.when(() -> IrisServices.getOrNull(ExternalDataSVC.class)).thenReturn(external);
scheduling.when(() -> J.runGlobal(any(Runnable.class))).thenAnswer(invocation -> {
invocation.getArgument(0, Runnable.class).run();
return true;
});
scheduling.when(() -> J.s(any(Runnable.class))).thenAnswer(invocation -> {
invocation.getArgument(0, Runnable.class).run();
return null;
});
scheduling.when(() -> J.runAt(any(Location.class), any(Runnable.class))).thenAnswer(invocation -> {
ownerTasks.add(invocation.getArgument(1, Runnable.class));
return true;
});
ObjectSVC undo = new ObjectSVC();
undo.addChanges(new HashMap<>(Map.of(block, custom)));
undo.revertChanges(1);
assertEquals(1, ownerTasks.size());
verifyNoInteractions(external);
ownerTasks.getFirst().run();
verify(external).placeBlock(block, id);
verify(block, times(0)).setBlockData(any(BlockData.class), anyBoolean());
}
}
}
@@ -0,0 +1,287 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedWorldManager;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineEffectsProvider;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.framework.GenerationTransitionGate;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisServices;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
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 EngineRuntimeBuilderStudioServiceTest {
@Test
public void replacementInterceptsCommandsWhileOldManagerRetires() throws Exception {
ExecutorService owner = Executors.newSingleThreadExecutor();
try (Fixture fixture = new Fixture()) {
CompletableFuture<Boolean> teleport = new CompletableFuture<>();
AtomicInteger requests = new AtomicInteger();
fixture.previous.afterUnregister = () -> {
try {
owner.submit(() -> fixture.teleportDuringRetirement(teleport, requests))
.get(2L, TimeUnit.SECONDS);
} catch (Exception failure) {
throw new IllegalStateException(failure);
}
};
try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) {
fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L);
fixture.builder.refreshStudioRuntimeServices();
}
assertSame(fixture.replacement, fixture.engine.runtime.worldManager());
assertEquals(List.of(fixture.replacement), fixture.registered);
assertEquals(1, requests.get());
assertFalse(teleport.isDone());
assertTrue(teleport.complete(true));
fixture.previous.tick.run();
fixture.replacement.tick.run();
assertEquals(0, fixture.previous.updates);
assertEquals(1, fixture.replacement.updates);
verify(fixture.previousEffects).close();
verify(fixture.nextEffects, never()).close();
} finally {
owner.shutdownNow();
assertTrue(owner.awaitTermination(2L, TimeUnit.SECONDS));
}
}
@Test
public void failedReplacementStartKeepsThePreviousListenerRegistered() throws Exception {
try (Fixture fixture = new Fixture()) {
fixture.replacement.failRegistration = true;
EngineRuntime previous = fixture.engine.runtime;
try (GenerationTransitionGate.Transition ignored = fixture.sessions.transitionGate().beginTransition(2_000L)) {
fixture.sessions.sealAndAwait("Studio generation cutover", 2_000L);
assertThrows(IllegalStateException.class, fixture.builder::refreshStudioRuntimeServices);
}
assertSame(previous, fixture.engine.runtime);
assertEquals(List.of(fixture.previous), fixture.registered);
assertEquals(0, fixture.previous.closes);
assertEquals(1, fixture.replacement.closes);
assertEquals(1L, fixture.sessions.currentSessionId());
verify(fixture.previousEffects, never()).close();
verify(fixture.nextEffects).close();
}
}
private static final class Fixture implements AutoCloseable {
private final IrisEngine engine = mock(IrisEngine.class);
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
private final AtomicBoolean closing = new AtomicBoolean(true);
private final List<TestWorldManager> registered = new CopyOnWriteArrayList<>();
private final EngineEffects previousEffects = mock(EngineEffects.class);
private final EngineEffects nextEffects = mock(EngineEffects.class);
private final IrisWorld irisWorld = mock(IrisWorld.class);
private final World world = mock(World.class);
private final TestWorldManager previous;
private final TestWorldManager replacement;
private final EngineRuntimeBuilder builder;
private final MockedStatic<IrisServices> services;
private Fixture() throws Exception {
EngineTarget target = mock(EngineTarget.class);
when(engine.getTarget()).thenReturn(target);
when(target.getWorld()).thenReturn(irisWorld);
when(engine.isStudio()).thenReturn(true);
when(engine.isClosing()).thenAnswer(invocation -> closing.get());
when(engine.isShuttingDown()).thenCallRealMethod();
when(engine.getClosing()).thenReturn(closing);
when(engine.getGenerationSessions()).thenReturn(sessions);
engine.lifecycleState = IrisEngine.LifecycleState.HOTLOADING;
EngineBackgroundTasks background = mock(EngineBackgroundTasks.class);
when(background.scheduleTrackedTask(any(Runnable.class))).thenReturn(true);
Field backgroundField = IrisEngine.class.getDeclaredField("backgroundTasks");
backgroundField.setAccessible(true);
backgroundField.set(engine, background);
previous = new TestWorldManager(engine, registered);
replacement = new TestWorldManager(engine, registered);
engine.runtime = new EngineRuntime(mock(GenerationRuntime.class), previousEffects, previous);
previous.start();
builder = new EngineRuntimeBuilder(engine);
EngineEffectsProvider effectsProvider = mock(EngineEffectsProvider.class);
EngineWorldManagerProvider managerProvider = mock(EngineWorldManagerProvider.class);
when(effectsProvider.create(engine)).thenReturn(nextEffects);
when(managerProvider.create(engine)).thenReturn(replacement);
services = mockStatic(IrisServices.class);
services.when(() -> IrisServices.get(EngineEffectsProvider.class)).thenReturn(effectsProvider);
services.when(() -> IrisServices.get(EngineWorldManagerProvider.class)).thenReturn(managerProvider);
}
private void teleportDuringRetirement(CompletableFuture<Boolean> teleport, AtomicInteger requests) {
Player player = mock(Player.class);
Location destination = new Location(world, 1024.5D, 309D, 0.5D, 45F, 10F);
PlayerTeleportEvent event = new PlayerTeleportEvent(player,
new Location(world, 0.5D, 100D, 0.5D), destination,
PlayerTeleportEvent.TeleportCause.COMMAND);
try (MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class)) {
platform.when(BukkitPlatform::isPaperServer).thenReturn(true);
binding.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
platform.when(() -> BukkitPlatform.teleportAsync(same(player), any(Location.class),
eq(PlayerTeleportEvent.TeleportCause.COMMAND)))
.thenAnswer(invocation -> {
assertTrue(event.isCancelled());
Location requested = invocation.getArgument(1, Location.class);
assertEquals(destination, requested);
assertNotSame(destination, requested);
requests.incrementAndGet();
return teleport;
});
assertEquals(List.of(replacement), registered);
for (TestWorldManager manager : registered) {
if (!event.isCancelled()) {
manager.on(event);
}
}
assertTrue(event.isCancelled());
previous.tick.run();
replacement.tick.run();
assertEquals(0, previous.updates);
assertEquals(0, replacement.updates);
assertEquals(0, sessions.activeLeases());
}
}
@Override
public void close() {
previous.afterUnregister = null;
previous.close();
replacement.close();
services.close();
}
}
private static final class TestWorldManager extends EngineAssignedWorldManager {
private final List<TestWorldManager> registered;
private final WorldTeleportWarmup warmup = new WorldTeleportWarmup();
private Runnable tick;
private Runnable afterUnregister;
private boolean failRegistration;
private int updates;
private int closes;
private TestWorldManager(Engine engine, List<TestWorldManager> registered) {
super(engine);
this.registered = registered;
}
@Override
public void close() {
closes++;
super.close();
}
@Override
protected void registerManagerListener() {
registered.add(this);
if (failRegistration) {
throw new IllegalStateException("listener registration failed");
}
}
@Override
protected void unregisterManagerListener() {
registered.remove(this);
if (afterUnregister != null) {
afterUnregister.run();
}
}
@Override
protected int scheduleManagerTick(Runnable tick) {
this.tick = tick;
return 42;
}
@Override
protected void cancelManagerTick(int scheduledTaskId) {
}
@Override
public int getEntityCount() {
return 0;
}
@Override
public int getChunkCount() {
return 0;
}
@Override
public double getEntitySaturation() {
return 0D;
}
@Override
public void onTick() {
updates++;
}
@Override
public void onSave() {
}
@Override
public void onBlockBreak(BlockBreakEvent event) {
}
@Override
public void onBlockPlace(BlockPlaceEvent event) {
}
@Override
public void onChunkLoad(Chunk chunk, boolean generated) {
}
@Override
public void onChunkUnload(Chunk chunk) {
}
@Override
public void teleportAsync(PlayerTeleportEvent event) {
warmup.teleportAsync(event);
}
}
}
@@ -6,8 +6,38 @@ import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisEngineLifecycleGateTest {
@Test
public void studioSessionSealDoesNotMeanWorldShutdown() {
IrisEngine engine = mock(IrisEngine.class);
when(engine.isShuttingDown()).thenCallRealMethod();
when(engine.isClosing()).thenReturn(true);
engine.lifecycleState = IrisEngine.LifecycleState.HOTLOADING;
assertTrue(engine.isClosing());
assertFalse(engine.isShuttingDown());
engine.lifecycleState = IrisEngine.LifecycleState.CLOSING;
assertTrue(engine.isShuttingDown());
engine.lifecycleState = IrisEngine.LifecycleState.CLOSED;
assertTrue(engine.isShuttingDown());
engine.lifecycleState = IrisEngine.LifecycleState.FAILED;
assertTrue(engine.isShuttingDown());
}
@Test
public void closedWorldRejectsNavigationRegardlessOfLifecyclePublication() {
IrisEngine engine = mock(IrisEngine.class);
when(engine.isShuttingDown()).thenCallRealMethod();
engine.lifecycleState = IrisEngine.LifecycleState.RUNNING;
engine.closed = true;
assertTrue(engine.isShuttingDown());
}
@Test
public void incompleteBackgroundDrainBlocksResourceRelease() {
EngineBackgroundTasks.BackgroundTaskDrain drain = new EngineBackgroundTasks.BackgroundTaskDrain(
@@ -12,10 +12,20 @@ import org.bukkit.event.player.PlayerTeleportEvent;
import org.junit.Test;
import org.mockito.MockedStatic;
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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
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 EngineAssignedWorldManagerTeleportTest {
@@ -30,6 +40,68 @@ public class EngineAssignedWorldManagerTeleportTest {
}
}
@Test
public void commandTeleportStillConvertsDuringSealedStudioCutover() throws Exception {
ExecutorService reloader = Executors.newSingleThreadExecutor();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
try (TeleportHarness harness = new TeleportHarness(false, true)) {
when(harness.engine.isClosing()).thenReturn(true);
when(harness.engine.isShuttingDown()).thenReturn(false);
Location destination = harness.event.getTo();
Future<?> cutover = reloader.submit(() -> {
try (GenerationTransitionGate.Transition ignored = harness.sessions.transitionGate().beginTransition(2_000L)) {
harness.sessions.sealAndAwait("Studio generation cutover", 2_000L);
entered.countDown();
assertTrue(release.await(2L, TimeUnit.SECONDS));
harness.sessions.activateNextSession();
} catch (Exception failure) {
throw new IllegalStateException(failure);
}
});
assertTrue(entered.await(2L, TimeUnit.SECONDS));
harness.manager.on(harness.event);
assertSame(harness.event, harness.manager.teleportEvent);
assertSame(destination, harness.manager.teleportEvent.getTo());
assertEquals(PlayerTeleportEvent.TeleportCause.COMMAND, harness.manager.teleportEvent.getCause());
assertEquals(0, harness.sessions.activeLeases());
release.countDown();
cutover.get(2L, TimeUnit.SECONDS);
} finally {
release.countDown();
reloader.shutdownNow();
assertTrue(reloader.awaitTermination(2L, TimeUnit.SECONDS));
}
}
@Test
public void closingStudioCancelsCommandInsteadOfFallingThroughToVanilla() {
try (TeleportHarness harness = new TeleportHarness(false, true)) {
when(harness.engine.isClosing()).thenReturn(true);
when(harness.engine.isShuttingDown()).thenReturn(true);
harness.manager.on(harness.event);
verify(harness.event).setCancelled(true);
assertNull(harness.manager.teleportEvent);
}
}
@Test
public void closedStudioCancelsEvenAnAlreadyLoadedDestination() {
try (TeleportHarness harness = new TeleportHarness(true, true)) {
when(harness.engine.isClosed()).thenReturn(true);
when(harness.engine.isShuttingDown()).thenReturn(true);
harness.manager.on(harness.event);
verify(harness.event).setCancelled(true);
assertNull(harness.manager.teleportEvent);
}
}
@Test
public void loadedIrisDestinationContinuesWithoutInterception() {
TeleportHarness harness = new TeleportHarness(true, true);
@@ -80,16 +152,20 @@ public class EngineAssignedWorldManagerTeleportTest {
public void otherWorldDestinationContinuesWithoutInterception() {
TeleportHarness harness = new TeleportHarness(false, true);
World otherWorld = mock(World.class);
when(harness.engine.isClosing()).thenReturn(true);
when(harness.engine.isShuttingDown()).thenReturn(true);
when(harness.event.getTo()).thenReturn(new Location(otherWorld, 400.5D, 96D, 2.5D));
try (harness) {
harness.manager.on(harness.event);
assertNull(harness.manager.teleportEvent);
verify(harness.event, never()).setCancelled(true);
}
}
private static final class TeleportHarness implements AutoCloseable {
private final GenerationSessionManager sessions = new GenerationSessionManager(true);
private final Engine engine;
private final TestWorldManager manager;
private final PlayerTeleportEvent event;
@@ -106,6 +182,7 @@ public class EngineAssignedWorldManagerTeleportTest {
when(engine.getTarget()).thenReturn(target);
when(engine.isStudio()).thenReturn(true);
when(engine.getGenerationSessions()).thenReturn(sessions);
when(target.getWorld()).thenReturn(irisWorld);
when(event.getTo()).thenReturn(destination);
when(event.getCause()).thenReturn(PlayerTeleportEvent.TeleportCause.COMMAND);
@@ -116,10 +193,12 @@ public class EngineAssignedWorldManagerTeleportTest {
binding = mockStatic(BukkitWorldBinding.class);
binding.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
manager = new TestWorldManager(engine);
manager.start();
}
@Override
public void close() {
manager.close();
binding.close();
platform.close();
}
@@ -133,9 +212,20 @@ public class EngineAssignedWorldManagerTeleportTest {
}
@Override
protected boolean runManagerTask(String operation, Runnable task) {
task.run();
return true;
protected void registerManagerListener() {
}
@Override
protected int scheduleManagerTick(Runnable tick) {
return 0;
}
@Override
protected void unregisterManagerListener() {
}
@Override
protected void cancelManagerTick(int scheduledTaskId) {
}
@Override
@@ -0,0 +1,224 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.testsupport.DurabilityMode;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.lang.reflect.Field;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class BoundarySamplingConcurrencyTest {
private static final BoundaryColumnGeometry GEOMETRY = new BoundaryColumnGeometry(0,
List.of(new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID,
"", false)), new int[]{65}, new short[]{0});
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void simultaneousQueriesBuildOneCandidateIndex() throws Exception {
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
CountDownLatch scanning = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
when(snapshot.nearestCandidatesForChunk(0, 0, 32)).thenAnswer(invocation -> {
scanning.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
return List.of(signature(0, 0));
});
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot);
ExecutorService workers = Executors.newFixedThreadPool(8);
CyclicBarrier start = new CyclicBarrier(8);
List<Future<BoundaryGeometryInfluence>> queries = new ArrayList<>();
try {
for (int index = 0; index < 8; index++) {
queries.add(workers.submit(() -> {
start.await(5, TimeUnit.SECONDS);
return sampler.geometryAt(1, 1);
}));
}
assertTrue(scanning.await(5, TimeUnit.SECONDS));
release.countDown();
BoundaryGeometryInfluence expected = queries.getFirst().get(5, TimeUnit.SECONDS);
for (Future<BoundaryGeometryInfluence> query : queries) {
assertEquals(expected, query.get(5, TimeUnit.SECONDS));
}
verify(snapshot, times(1)).nearestCandidatesForChunk(0, 0, 32);
assertEquals(1L, sampler.candidateBuildCount());
} finally {
release.countDown();
workers.shutdownNow();
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void failedCandidateBuildCanBeRetried() {
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
when(snapshot.nearestCandidatesForChunk(0, 0, 32))
.thenThrow(new IllegalStateException("Unavailable boundary shard"))
.thenReturn(List.of(signature(0, 0)));
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot);
assertThrows(IllegalStateException.class, () -> sampler.sample(1, 1));
assertEquals(64D, sampler.sample(1, 1).historicalSurfaceHeight(), 0D);
verify(snapshot, times(2)).nearestCandidatesForChunk(0, 0, 32);
}
@Test
public void generationWindowReusesCandidatesAndEvictsPastItsChunkBound() {
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
when(snapshot.nearestCandidatesForChunk(anyInt(), anyInt(), anyInt())).thenReturn(List.of());
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot);
for (int chunk = 0; chunk < 512; chunk++) {
sampler.geometryAt(chunk * 16, 0);
}
sampler.geometryAt(0, 0);
assertEquals(512L, sampler.candidateBuildCount());
sampler.geometryAt(512 * 16, 0);
sampler.geometryAt(16, 0);
assertEquals(514L, sampler.candidateBuildCount());
}
@Test
public void denseCandidateIndexesRespectTheReferenceBudget() {
List<TerrainBoundarySignature> dense = new ArrayList<>();
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
dense.add(signature(x, z));
}
}
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
when(snapshot.nearestCandidatesForChunk(anyInt(), anyInt(), anyInt())).thenReturn(dense);
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot);
for (int chunk = 0; chunk < 33; chunk++) {
sampler.geometryAt(chunk * 16, 0);
}
sampler.geometryAt(0, 0);
assertEquals(34L, sampler.candidateBuildCount());
}
@Test
public void activeGeometryColumnKeepsItsCandidatesAfterSharedCacheEviction() throws Exception {
TerrainBoundarySignatureStore.Snapshot snapshot = mock(TerrainBoundarySignatureStore.Snapshot.class);
when(snapshot.nearestCandidatesForChunk(anyInt(), anyInt(), anyInt())).thenReturn(List.of(signature(0, 0)));
TransitionBoundarySampler sampler = new TransitionBoundarySampler(32, snapshot);
BoundaryGeometryInfluence first = sampler.geometryAt(1, 1);
ExecutorService other = Executors.newSingleThreadExecutor();
try {
other.submit(() -> {
for (int chunk = 1; chunk <= 512; chunk++) {
sampler.geometryAt(chunk * 16, 0);
}
}).get(5, TimeUnit.SECONDS);
assertEquals(first, sampler.geometryAt(1, 1));
verify(snapshot, times(1)).nearestCandidatesForChunk(0, 0, 32);
} finally {
other.shutdownNow();
assertTrue(other.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void distinctBoundaryScansDoNotHoldTheShardCacheMonitor() throws Exception {
TerrainBoundarySignatureStore store = new TerrainBoundarySignatureStore(
temporaryFolder.newFolder("parallel-scans").toPath());
TerrainBoundarySignatureStore.Snapshot snapshot = store.publish(1L,
List.of(signature(0, 0), signature(512, 512)));
snapshot.signatureAt(0, 0).orElseThrow();
snapshot.signatureAt(512, 512).orElseThrow();
Map<Long, List<TerrainBoundarySignature>> cache = shardCache(snapshot);
BlockingSignatures blocked = new BlockingSignatures(cache.get(0L));
cache.put(0L, blocked);
ExecutorService workers = Executors.newFixedThreadPool(2);
try {
Future<List<TerrainBoundarySignature>> first = workers.submit(
() -> snapshot.nearestCandidatesForChunk(0, 0, 16));
assertTrue(blocked.entered.await(5, TimeUnit.SECONDS));
Future<List<TerrainBoundarySignature>> distant = workers.submit(
() -> snapshot.nearestCandidatesForChunk(32, 32, 16));
assertEquals(512, distant.get(3, TimeUnit.SECONDS).getFirst().blockX());
assertFalse(first.isDone());
assertTrue(workers.submit(() -> snapshot.intersectsTerrainBand(
new TerrainBoundarySignatureStore.BlockBounds(512, 512, 527, 527), 16))
.get(3, TimeUnit.SECONDS));
blocked.release.countDown();
assertEquals(0, first.get(3, TimeUnit.SECONDS).getFirst().blockX());
assertEquals(2L, snapshot.shardLoadCount());
} finally {
blocked.release.countDown();
workers.shutdownNow();
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
}
}
@SuppressWarnings("unchecked")
private static Map<Long, List<TerrainBoundarySignature>> shardCache(
TerrainBoundarySignatureStore.Snapshot snapshot) throws Exception {
Field sourceField = TerrainBoundarySignatureStore.Snapshot.class.getDeclaredField("source");
sourceField.setAccessible(true);
Object source = sourceField.get(snapshot);
Field cacheField = source.getClass().getDeclaredField("cache");
cacheField.setAccessible(true);
return (Map<Long, List<TerrainBoundarySignature>>) cacheField.get(source);
}
private static TerrainBoundarySignature signature(int x, int z) {
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(x, z, 64, 64, OptionalInt.empty(), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(0, 4, 0),
new TerrainBoundarySignature.BiomeEncoding(List.of(), new short[0])),
GEOMETRY);
}
private static final class BlockingSignatures extends AbstractList<TerrainBoundarySignature> {
private final List<TerrainBoundarySignature> values;
private final CountDownLatch entered = new CountDownLatch(1);
private final CountDownLatch release = new CountDownLatch(1);
private BlockingSignatures(List<TerrainBoundarySignature> values) {
this.values = values;
}
@Override
public TerrainBoundarySignature get(int index) {
entered.countDown();
try {
assertTrue(release.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new AssertionError(interruption);
}
return values.get(index);
}
@Override
public int size() {
return values.size();
}
}
}
@@ -262,6 +262,76 @@ public class GenerationBoundaryStoreTest {
}
}
@Test
public void repeatedRegionQueriesDoNotWaitForBoundaryIteration() throws Exception {
GenerationBoundaryStore store = new GenerationBoundaryStore(
temporaryFolder.newFolder("boundary-warm-region").toPath());
GenerationBoundary boundary = store.publishPacked(1L,
new long[]{GenerationBoundary.packChunk(-1, -1)});
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
ExecutorService queries = Executors.newSingleThreadExecutor();
ExecutorService iterator = Executors.newSingleThreadExecutor();
try {
assertTrue(queries.submit(() -> boundary.isHistoricalChunk(-1, -1)).get(5, TimeUnit.SECONDS));
Future<?> iteration = iterator.submit(() -> {
boundary.forEachHistoricalChunk((x, z) -> {
entered.countDown();
try {
assertTrue(release.await(5, TimeUnit.SECONDS));
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted boundary iteration", interruption);
}
});
return null;
});
assertTrue(entered.await(5, TimeUnit.SECONDS));
Future<Boolean> warm = queries.submit(() -> {
for (int x = -32; x < -1; x++) {
assertFalse(boundary.isHistoricalChunk(x, -1));
}
return boundary.isHistoricalChunk(-1, -1);
});
assertTrue(warm.get(3, TimeUnit.SECONDS));
assertFalse(iteration.isDone());
release.countDown();
iteration.get(5, TimeUnit.SECONDS);
assertEquals(1, boundary.cachedRegionCount());
} finally {
release.countDown();
queries.shutdownNow();
iterator.shutdownNow();
assertTrue(queries.awaitTermination(5, TimeUnit.SECONDS));
assertTrue(iterator.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void lastRegionChangesPreserveMissingBitsAndCoordinateExtremes() throws Exception {
GenerationBoundaryStore store = new GenerationBoundaryStore(
temporaryFolder.newFolder("boundary-region-transitions").toPath());
List<GenerationBoundary.ChunkCoordinate> chunks = List.of(
new GenerationBoundary.ChunkCoordinate(-33, -1),
new GenerationBoundary.ChunkCoordinate(-32, -32),
new GenerationBoundary.ChunkCoordinate(0, 0),
new GenerationBoundary.ChunkCoordinate(31, -32),
new GenerationBoundary.ChunkCoordinate(32, 31),
new GenerationBoundary.ChunkCoordinate(Integer.MIN_VALUE, Integer.MAX_VALUE),
new GenerationBoundary.ChunkCoordinate(Integer.MAX_VALUE, Integer.MIN_VALUE));
GenerationBoundary boundary = store.publish(1L, chunks);
for (int pass = 0; pass < 3; pass++) {
for (GenerationBoundary.ChunkCoordinate chunk : chunks) {
assertTrue(boundary.isHistoricalChunk(chunk.chunkX(), chunk.chunkZ()));
int neighbor = chunk.chunkX() == Integer.MAX_VALUE ? chunk.chunkX() - 1 : chunk.chunkX() + 1;
assertFalse(boundary.isHistoricalChunk(neighbor, chunk.chunkZ()));
assertFalse(boundary.isHistoricalChunk(1024, -5555));
assertTrue(boundary.isHistoricalChunk(chunk.chunkX(), chunk.chunkZ()));
}
}
assertTrue(boundary.cachedRegionCount() <= 64);
}
@Test
public void knownRegionHitsKeepAccessOrderAndEvictedMasksAreRevalidated() throws Exception {
GenerationBoundaryStore store = new GenerationBoundaryStore(temporaryFolder.newFolder("boundary-cache-revalidation").toPath());
@@ -0,0 +1,72 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisObjectDirectPlacementTest {
@Test
public void bothDirectPlacementMethodsUseProviderWithoutPassingProxyToBukkit() {
BlockData base = mock(BlockData.class);
Identifier id = new Identifier("craftengine", "test/lamp[lit=true]");
IrisCustomData custom = IrisCustomData.of(base, id);
IrisObject object = object(custom);
World world = mock(World.class);
Block block = mock(Block.class);
when(world.getBlockAt(any(Location.class))).thenReturn(block);
ExternalDataSVC external = mock(ExternalDataSVC.class);
when(external.placeBlock(block, id)).thenReturn(true);
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
services.when(() -> IrisServices.getOrNull(ExternalDataSVC.class)).thenReturn(external);
object.place(new Location(world, 7, 80, -9));
object.placeCenterY(new Location(world, 7, 80, -9));
verify(external, times(2)).placeBlock(block, id);
verify(block, times(0)).setBlockData(any(BlockData.class), anyBoolean());
}
}
@Test
public void unsupportedProviderPlacesOnlyBaseDataAndVanillaStillPlacesNormally() {
BlockData base = mock(BlockData.class);
Identifier id = new Identifier("craftengine", "test/chair[variant=alpha]");
World world = mock(World.class);
Block block = mock(Block.class);
when(world.getBlockAt(any(Location.class))).thenReturn(block);
ExternalDataSVC external = mock(ExternalDataSVC.class);
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
services.when(() -> IrisServices.getOrNull(ExternalDataSVC.class)).thenReturn(external);
object(IrisCustomData.of(base, id)).place(new Location(world, 7, 80, -9));
object(base).placeCenterY(new Location(world, 7, 80, -9));
verify(external).placeBlock(block, id);
verify(block, times(2)).setBlockData(base, false);
}
}
private static IrisObject object(BlockData data) {
IrisObject object = new IrisObject(1, 1, 1);
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.nativeHandle()).thenReturn(data);
object.setUnsigned(0, 0, 0, state);
return object;
}
}
@@ -0,0 +1,158 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisObjectTransientBlockTest {
private static final List<String> PALETTE = List.of(
"minecraft:moving_piston[facing=down,type=normal]", "minecraft:oak_log[axis=y]",
"minecraft:oak_leaves[persistent=true]", "minecraft:chest[facing=north,type=single,waterlogged=false]",
"minecraft:piston[facing=north,extended=false]", "minecraft:sticky_piston[facing=north,extended=false]",
"minecraft:jigsaw[orientation=north_up]");
private static final TypeToken<KMap<String, Object>> TILE_PROPERTIES = new TypeToken<>() { };
@Rule
public final PlatformBinding platform = PlatformBinding.mockPlatform();
private TileData.TileReader previousReader;
@Before
public void bindStatesAndTiles() {
when(platform.registries().blockOrNull(anyString(), eq(false)))
.thenAnswer(invocation -> state(invocation.getArgument(0)));
Gson gson = new Gson();
previousReader = TileData.bindPlatformReader(input -> new TileData(input.readUTF(),
gson.fromJson(input.readUTF(), TILE_PROPERTIES)));
}
@After
public void restoreTiles() {
TileData.restorePlatformReader(previousReader);
IrisObjectScale.invalidate(null);
}
@Test
public void bothObjectFormatsOmitTransientCellsAndTheirTiles() throws Throwable {
for (boolean legacy : new boolean[]{false, true}) {
IrisObject object = loadFixture(legacy);
assertEquals(5, object.getBlocks().size());
assertNull(object.getBlocks().get(new IrisBlockVector(0, 0, 0)));
assertNull(object.getBlocks().get(new IrisBlockVector(6, 0, 0)));
assertEquals("minecraft:oak_log", object.getBlocks().get(new IrisBlockVector(1, 0, 0)).materialKey());
assertEquals("minecraft:oak_leaves", object.getBlocks().get(new IrisBlockVector(2, 0, 0)).materialKey());
assertEquals("minecraft:piston", object.getBlocks().get(new IrisBlockVector(4, 0, 0)).materialKey());
assertEquals("minecraft:sticky_piston", object.getBlocks().get(new IrisBlockVector(5, 0, 0)).materialKey());
assertEquals(1, object.getStates().size());
assertEquals("chest contents", object.getStates().get(new IrisBlockVector(3, 0, 0))
.getProperties().get("payload"));
}
}
@Test
public void scalingLoadedObjectsKeepsRealBlockEntitiesWithoutRevivingTransientCells() throws Throwable {
IrisObject source = loadFixture(false);
IrisObjectPlacement placement = new IrisObjectPlacement();
for (double factor : new double[]{0.5D, 1D, 2D}) {
IrisObject object = placement.scaleObject(new RNG(1337), source,
new IrisDimension().setAllObjectScaleFactor(factor));
assertEquals(factor == 2D ? 8 : 1, object.getStates().size());
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : object.getBlocks()) {
assertFalse(entry.getValue().materialKey().equals("minecraft:moving_piston"));
if (entry.getValue().materialKey().equals("minecraft:chest")) {
assertNotNull(object.getStates().get(entry.getKey()));
assertEquals("chest contents", object.getStates().get(entry.getKey()).getProperties().get("payload"));
}
}
}
assertEquals(5, source.getBlocks().size());
assertEquals(1, source.getStates().size());
}
private static IrisObject loadFixture(boolean legacy) throws Throwable {
IrisObject object = new IrisObject();
ByteArrayInputStream input = new ByteArrayInputStream(fixture(legacy));
if (legacy) {
IrisObjectIO.readLegacy(object, input);
} else {
IrisObjectIO.read(object, input);
}
return object;
}
private static byte[] fixture(boolean legacy) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
output.writeInt(16);
output.writeInt(4);
output.writeInt(4);
if (!legacy) {
output.writeUTF("Iris V2 IOB;");
output.writeShort(PALETTE.size());
for (String key : PALETTE) {
output.writeUTF(key);
}
}
output.writeInt(PALETTE.size());
for (int index = 0; index < PALETTE.size(); index++) {
writePosition(output, index);
if (legacy) {
output.writeUTF(PALETTE.get(index));
} else {
output.writeShort(index);
}
}
output.writeInt(4);
writeTile(output, 0, "minecraft:moving_piston", "transient");
writeTile(output, 3, "minecraft:chest", "chest contents");
writeTile(output, 6, "minecraft:jigsaw", "marker");
writeTile(output, 7, "minecraft:chest", "orphan");
}
return bytes.toByteArray();
}
private static void writeTile(DataOutputStream output, int x, String material, String payload) throws IOException {
writePosition(output, x);
output.writeUTF(material);
output.writeUTF("{\"payload\":\"" + payload + "\"}");
}
private static void writePosition(DataOutputStream output, int x) throws IOException {
output.writeShort(x);
output.writeShort(0);
output.writeShort(0);
}
private static PlatformBlockState state(String key) {
String material = key.substring(0, key.indexOf('['));
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn(key);
when(state.materialKey()).thenReturn(material);
return state;
}
}
@@ -313,6 +313,68 @@ public class BukkitChunkGeneratorGenerationStageGateTest {
}
}
@Test
public void timedExclusiveRetainsPriorityDuringStaggeredGenerationDrain() throws Exception {
BukkitChunkGenerator.GenerationStageGate gate =
new BukkitChunkGenerator.GenerationStageGate(3, () -> false);
BukkitChunkGenerator.GenerationStagePermit first = gate.acquireStage("first-active");
BukkitChunkGenerator.GenerationStagePermit last = gate.acquireStage("last-active");
CompletableFuture<Void> outward = new CompletableFuture<>();
CountDownLatch exclusiveEntered = new CountDownLatch(1);
CountDownLatch releaseExclusive = new CountDownLatch(1);
CountDownLatch readerEntered = new CountDownLatch(1);
List<String> order = new CopyOnWriteArrayList<>();
ExecutorService executor = Executors.newFixedThreadPool(3);
try {
Future<?> exclusive = executor.submit(() -> BukkitChunkGenerator.completeExclusiveControlFuture(
gate,
() -> {
order.add("exclusive");
exclusiveEntered.countDown();
try {
assertTrue(releaseExclusive.await(2L, TimeUnit.SECONDS));
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException(failure);
}
},
outward,
2L,
TimeUnit.SECONDS));
awaitQueueLength(gate, 1);
Runnable reader = () -> {
try (BukkitChunkGenerator.GenerationStagePermit ignored = gate.acquireStage("later-reader")) {
order.add("reader");
readerEntered.countDown();
}
};
Future<?> firstReader = executor.submit(reader);
Future<?> secondReader = executor.submit(reader);
awaitQueueLength(gate, 3);
assertFalse(readerEntered.await(150L, TimeUnit.MILLISECONDS));
first.close();
assertFalse(readerEntered.await(150L, TimeUnit.MILLISECONDS));
last.close();
assertTrue(exclusiveEntered.await(2L, TimeUnit.SECONDS));
assertEquals(1L, readerEntered.getCount());
releaseExclusive.countDown();
exclusive.get(2L, TimeUnit.SECONDS);
outward.get(2L, TimeUnit.SECONDS);
firstReader.get(2L, TimeUnit.SECONDS);
secondReader.get(2L, TimeUnit.SECONDS);
assertEquals(List.of("exclusive", "reader", "reader"), order);
assertEquals(3, gate.availablePermits());
} finally {
first.close();
last.close();
releaseExclusive.countDown();
executor.shutdownNow();
assertTrue(executor.awaitTermination(2L, TimeUnit.SECONDS));
}
}
@Test
public void timedExclusiveControlDoesNotReleaseAnUnacquiredPermit() throws Exception {
AtomicBoolean closing = new AtomicBoolean(false);
@@ -355,16 +417,19 @@ public class BukkitChunkGeneratorGenerationStageGateTest {
BukkitChunkGenerator.GenerationStagePermit stage = gate.acquireStage("cancellation-holder");
CompletableFuture<Void> outward = new CompletableFuture<>();
AtomicBoolean operationRan = new AtomicBoolean(false);
AtomicBoolean interruptedAfterCancellation = new AtomicBoolean(true);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<?> operation = executor.submit(() ->
BukkitChunkGenerator.completeExclusiveControlFuture(
gate,
() -> operationRan.set(true),
outward,
30L,
TimeUnit.SECONDS));
Future<?> operation = executor.submit(() -> {
BukkitChunkGenerator.completeExclusiveControlFuture(
gate,
() -> operationRan.set(true),
outward,
30L,
TimeUnit.SECONDS);
interruptedAfterCancellation.set(Thread.currentThread().isInterrupted());
});
awaitQueueLength(gate, 1);
assertTrue(outward.cancel(true));
@@ -372,6 +437,7 @@ public class BukkitChunkGeneratorGenerationStageGateTest {
assertTrue(outward.isCancelled());
assertFalse(operationRan.get());
assertFalse(interruptedAfterCancellation.get());
assertEquals(0, gate.availablePermits());
} finally {
stage.close();
@@ -380,6 +446,80 @@ public class BukkitChunkGeneratorGenerationStageGateTest {
}
}
@Test
public void externalInterruptStillRestoresTheAcquisitionWorkerFlag() throws Exception {
BukkitChunkGenerator.GenerationStageGate gate =
new BukkitChunkGenerator.GenerationStageGate(1, () -> false);
BukkitChunkGenerator.GenerationStagePermit stage = gate.acquireStage("interrupt-holder");
CompletableFuture<Void> outward = new CompletableFuture<>();
AtomicReference<Thread> worker = new AtomicReference<>();
AtomicBoolean interruptedAfterAcquisition = new AtomicBoolean(false);
AtomicBoolean operationRan = new AtomicBoolean(false);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<?> operation = executor.submit(() -> {
worker.set(Thread.currentThread());
BukkitChunkGenerator.completeExclusiveControlFuture(gate, () -> operationRan.set(true),
outward, 30L, TimeUnit.SECONDS);
interruptedAfterAcquisition.set(Thread.currentThread().isInterrupted());
});
awaitQueueLength(gate, 1);
worker.get().interrupt();
operation.get(2L, TimeUnit.SECONDS);
ExecutionException failure = assertThrows(ExecutionException.class,
() -> outward.get(2L, TimeUnit.SECONDS));
assertTrue(failure.getCause() instanceof InterruptedException);
assertTrue(interruptedAfterAcquisition.get());
assertFalse(operationRan.get());
assertEquals(0, gate.availablePermits());
} finally {
stage.close();
executor.shutdownNow();
assertTrue(executor.awaitTermination(2L, TimeUnit.SECONDS));
}
}
@Test
public void cancellationAfterAcquisitionDoesNotInterruptTheRunningOperation() throws Exception {
BukkitChunkGenerator.GenerationStageGate gate =
new BukkitChunkGenerator.GenerationStageGate(1, () -> false);
CompletableFuture<Void> outward = new CompletableFuture<>();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicBoolean interrupted = new AtomicBoolean(false);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<?> operation = executor.submit(() -> {
BukkitChunkGenerator.completeExclusiveControlFuture(gate, () -> {
entered.countDown();
try {
assertTrue(release.await(2L, TimeUnit.SECONDS));
} catch (InterruptedException failure) {
interrupted.set(true);
Thread.currentThread().interrupt();
}
}, outward, 30L, TimeUnit.SECONDS);
interrupted.compareAndSet(false, Thread.currentThread().isInterrupted());
});
assertTrue(entered.await(2L, TimeUnit.SECONDS));
assertTrue(outward.cancel(true));
assertEquals(0, gate.availablePermits());
release.countDown();
operation.get(2L, TimeUnit.SECONDS);
assertFalse(interrupted.get());
assertTrue(outward.isCancelled());
assertEquals(1, gate.availablePermits());
} finally {
release.countDown();
executor.shutdownNow();
assertTrue(executor.awaitTermination(2L, TimeUnit.SECONDS));
}
}
@Test
public void queuedStageRemainsAdmittedWhileShutdownIsOnlyQuiesced() throws Exception {
AtomicBoolean closing = new AtomicBoolean(false);
@@ -0,0 +1,159 @@
package art.arcane.iris.platform.bukkit;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.testsupport.BukkitTestServer;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.data.registry.RegistryUtil;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mockStatic;
public class BukkitCustomBlockTransformTest {
@BeforeClass
public static void installServer() {
BukkitTestServer.install();
try (MockedStatic<RegistryUtil> registry = mockStatic(RegistryUtil.class)) {
registry.when(() -> RegistryUtil.find(Material.class, "grass", "short_grass")).thenReturn(Material.SHORT_GRASS);
BukkitBlockResolution.getAir();
}
}
@Test
public void waterloggingUpdatesSemanticIdentityAndResolvedCarrier() {
IrisCustomData original = custom("craftengine:forest/amber_slab[axis=x,waterlogged=false]", 1);
IrisCustomData expected = custom("craftengine:forest/amber_slab[axis=x,waterlogged=true]", 2);
PlatformBlockState state = BukkitBlockState.of(original);
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
resolveTo(resolution, expected);
PlatformBlockState result = state.withProperty("waterlogged", "true");
assertSame(expected, result.nativeHandle());
assertEquals(expected.getCustom().toString(), result.deferredPlacementKey());
assertEquals(expected.getBase().getAsString(), result.placementBaseState().key());
assertEquals("craftengine:forest/amber_slab[axis=x,waterlogged=false]", state.key());
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
}
}
@Test
public void customPropertyRejectsAnUnresolvableValue() {
PlatformBlockState state = BukkitBlockState.of(custom("craftengine:forest/amber_log[axis=x]", 1));
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
() -> state.withProperty("axis", "invalid"));
assertTrue(error.getMessage().contains("axis=invalid"));
assertEquals("craftengine:forest/amber_log[axis=x]", state.key());
resolution.verify(() -> BukkitBlockResolution.resolveOrNull("craftengine:forest/amber_log[axis=invalid]"));
}
}
@Test
public void semanticFacingRotatesWithoutDirectionalCarrier() {
IrisCustomData original = custom("craftengine:forest/amber_lamp[facing=north,waterlogged=true]", 1);
IrisCustomData expected = custom("craftengine:forest/amber_lamp[facing=west,waterlogged=true]", 2);
PlatformBlockState state = BukkitBlockState.of(original);
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
resolveTo(resolution, expected);
PlatformBlockState result = IrisObjectRotation.of(0, 90, 0).rotate(state, 0, 0, 0);
assertSame(expected, result.nativeHandle());
assertEquals(expected.getCustom().toString(), result.key());
assertEquals("craftengine:forest/amber_lamp[facing=north,waterlogged=true]", state.key());
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
}
}
@Test
public void semanticAxisRotatesThroughRawBlockDataEntryPoint() {
IrisCustomData original = custom("craftengine:forest/amber_log[axis=x,waterlogged=false]", 1);
IrisCustomData expected = custom("craftengine:forest/amber_log[axis=z,waterlogged=false]", 2);
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
resolveTo(resolution, expected);
BlockData result = IrisObjectRotation.of(0, 90, 0).rotate(original, 0, 0, 0);
assertSame(expected, result);
assertEquals("craftengine:forest/amber_log[axis=x,waterlogged=false]", original.getCustom().toString());
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
}
}
@Test
public void semanticRotationPreservesSixteenWayOrientation() {
PlatformBlockState state = BukkitBlockState.of(custom("craftengine:forest/amber_sign[rotation=0]", 1));
IrisCustomData expected = custom("craftengine:forest/amber_sign[rotation=12]", 2);
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
resolveTo(resolution, expected);
PlatformBlockState result = IrisObjectRotation.of(0, 90, 0).rotate(state, 0, 0, 0);
assertSame(expected, result.nativeHandle());
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
}
}
@Test
public void semanticConnectionsRotateTogether() {
PlatformBlockState state = BukkitBlockState.of(custom(
"craftengine:forest/amber_fence[north=true,east=false,south=false,west=false]", 1));
IrisCustomData expected = custom(
"craftengine:forest/amber_fence[north=false,east=false,south=false,west=true]", 2);
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
resolveTo(resolution, expected);
PlatformBlockState result = IrisObjectRotation.of(0, 90, 0).rotate(state, 0, 0, 0);
assertSame(expected, result.nativeHandle());
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
}
}
@Test
public void unsupportedRotationRetainsTheOriginalCustomBlock() {
PlatformBlockState state = BukkitBlockState.of(custom("craftengine:forest/amber_lamp[facing=north]", 1));
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
assertSame(state, IrisObjectRotation.of(90, 0, 0).rotate(state, 0, 0, 0));
resolution.verify(() -> BukkitBlockResolution.resolveOrNull(anyString()));
}
}
@Test
public void unchangedSemanticAxisDoesNotResolveOrMutateCarrier() {
PlatformBlockState state = BukkitBlockState.of(custom("craftengine:forest/amber_log[axis=y]", 1));
try (MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class)) {
assertSame(state, IrisObjectRotation.of(0, 90, 0).rotate(state, 0, 0, 0));
resolution.verifyNoInteractions();
}
}
private static IrisCustomData custom(String key, int note) {
BlockData carrier = BukkitTestServer.blockData("minecraft:note_block[instrument=harp,note=" + note + ",powered=false]");
return IrisCustomData.of(carrier, Identifier.fromString(key));
}
private static void resolveTo(MockedStatic<BukkitBlockResolution> resolution, IrisCustomData expected) {
resolution.when(() -> BukkitBlockResolution.resolveOrNull(anyString())).thenAnswer(invocation -> {
Identifier actual = Identifier.fromString(invocation.getArgument(0, String.class));
assertEquals(ExternalDataSVC.parseState(expected.getCustom()), ExternalDataSVC.parseState(actual));
return expected;
});
}
}