mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
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:
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(
|
||||
|
||||
+93
-3
@@ -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
|
||||
|
||||
+224
@@ -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;
|
||||
}
|
||||
}
|
||||
+147
-7
@@ -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);
|
||||
|
||||
+159
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user