Fixed mining and timeout settings

- Pending biome reads no longer cancel block breaking
- Custom loot resolves saved biome history after accepted breaks
- Native drops survive failed reads and runtime teardown without duplication
- Tree felling captures the original tool for fallback drops
- Modded block loot resolves one historical biome context
- Timeout settings persist atomically through the actual server configuration paths
- Tests cover deferred loot, teardown and timeout persistence
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-12 09:27:26 -04:00
1 parent 2145e90b1c
commit e3673afb1e
17 files changed
+1220 -329

No files matched your search

@@ -0,0 +1,132 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class IrisEngineBiomeEnvironmentAsyncTest {
@Test
public void uncachedBiomeWaitsAsynchronouslyAndUsesPreparedVolumeWithWorldHeight() throws Exception {
Fixture fixture = new Fixture();
CompletableFuture<SavedBiomeRuntime.ReadSession> pending = new CompletableFuture<>();
SavedBiomeRuntime.ReadSession session = mock(SavedBiomeRuntime.ReadSession.class);
BiomeEnvironment environment = environment(7L);
when(fixture.biomes.readChunkAsync(-2, 2)).thenReturn(pending);
when(session.resolve(-17, -59, 33, false)).thenReturn(Optional.of(environment));
CompletableFuture<BiomeEnvironment> reading = fixture.engine.getBiomeEnvironmentAsync(-17, 5, 33);
assertFalse(reading.isDone());
verify(fixture.engine, never()).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
pending.complete(session);
assertSame(environment, reading.get(5L, TimeUnit.SECONDS));
verify(session).resolve(-17, -59, 33, false);
verify(fixture.biomes, never()).resolve(anyInt(), anyInt(), anyInt(), anyBoolean());
}
@Test
public void permanentSavedBiomeFailureCompletesTheFutureExceptionally() throws Exception {
Fixture fixture = new Fixture();
SavedBiomeUnavailableException failure = new SavedBiomeUnavailableException("Saved pack is missing", false);
when(fixture.biomes.readChunkAsync(0, 0)).thenReturn(CompletableFuture.failedFuture(failure));
CompletableFuture<BiomeEnvironment> reading = fixture.engine.getBiomeEnvironmentAsync(0, 64, 0);
assertSame(failure, assertThrows(CompletionException.class, reading::join).getCause());
verify(fixture.engine, never()).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
}
@Test
public void preparedEmptyRecordFallsBackWithoutReenteringTheSavedQueryCache() throws Exception {
Fixture fixture = new Fixture();
SavedBiomeRuntime.ReadSession session = mock(SavedBiomeRuntime.ReadSession.class);
GenerationHistoryRuntimeRouter.CoordinateScope scope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
BiomeEnvironment expected = environment(0L);
when(fixture.biomes.readChunkAsync(0, 0)).thenReturn(CompletableFuture.completedFuture(session));
when(session.resolve(3, 7, 4, false)).thenReturn(Optional.empty());
when(fixture.router.openReadScope(3, 4)).thenReturn(scope);
when(fixture.engine.getBiome(3, 71, 4)).thenReturn(expected.biome());
when(fixture.engine.getRegion(3, 71, 4)).thenReturn(expected.region());
when(fixture.engine.getDimension()).thenReturn(expected.dimension());
when(fixture.engine.getData()).thenReturn(expected.data());
assertEquals(expected, fixture.engine.getBiomeEnvironmentAsync(3, 71, 4).get(5L, TimeUnit.SECONDS));
verify(scope).close();
verify(fixture.engine, never()).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
verify(fixture.biomes, never()).resolve(anyInt(), anyInt(), anyInt(), anyBoolean());
}
@Test
public void scopedLookupUsesCurrentRuntimeAndDoesNotReadSavedBiomes() throws Exception {
Fixture fixture = new Fixture();
BiomeEnvironment expected = environment(4L);
when(fixture.engine.hasGenerationRuntimeScope()).thenReturn(true);
when(fixture.engine.getBiomeEnvironment(1, 2, 3)).thenReturn(expected);
CompletableFuture<BiomeEnvironment> reading = fixture.engine.getBiomeEnvironmentAsync(1, 2, 3);
assertTrue(reading.isDone());
assertSame(expected, reading.join());
verifyNoInteractions(fixture.biomes);
}
@Test
public void immediateLookupFailureIsReturnedInTheFuture() throws Exception {
Fixture fixture = new Fixture();
IllegalStateException failure = new IllegalStateException("Generation closed");
when(fixture.engine.hasGenerationRuntimeScope()).thenReturn(true);
when(fixture.engine.getBiomeEnvironment(1, 2, 3)).thenThrow(failure);
CompletableFuture<BiomeEnvironment> reading = fixture.engine.getBiomeEnvironmentAsync(1, 2, 3);
assertSame(failure, assertThrows(CompletionException.class, reading::join).getCause());
verifyNoInteractions(fixture.biomes);
}
private static BiomeEnvironment environment(long activationId) {
return new BiomeEnvironment(activationId, mock(IrisBiome.class), mock(IrisRegion.class),
mock(IrisDimension.class), mock(IrisData.class));
}
private static final class Fixture {
private final IrisEngine engine = mock(IrisEngine.class);
private final GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
private final SavedBiomeRuntime biomes = mock(SavedBiomeRuntime.class);
private Fixture() throws Exception {
Field field = IrisEngine.class.getDeclaredField("generationHistoryRuntimeRouter");
field.setAccessible(true);
field.set(engine, router);
when(router.biomes()).thenReturn(biomes);
when(engine.getWorld()).thenReturn(IrisWorld.builder().name("test").minHeight(-64).maxHeight(320).build());
doCallRealMethod().when(engine).getBiomeEnvironmentAsync(anyInt(), anyInt(), anyInt());
}
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.world.tree.BlockDropRouter;
import org.junit.Test;
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
@@ -74,16 +75,12 @@ public class IrisWorldManagerMarkerTest {
public void deferredDropsUseTheRouteAndFallbackOnlyWhenDeclined() {
List<String> routed = new ArrayList<>();
List<String> fallback = new ArrayList<>();
BlockDropRouter router = mock(BlockDropRouter.class);
when(router.routeDrop("routed")).thenAnswer(invocation -> routed.add(invocation.getArgument(0)));
WorldBlockDropRouter.routeDrops(
List.of("routed", "fallback"),
drop -> {
if (drop.equals("routed")) {
routed.add((String) drop);
return true;
}
return false;
},
router,
fallback::add
);
@@ -2,36 +2,45 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.world.tree.BlockDropRouter;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.world.loot.IrisBlockDrops;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.platform.generation.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList;
import org.bukkit.Location;
import org.bukkit.GameMode;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockDropItemEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
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.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
@@ -43,191 +52,241 @@ import static org.mockito.Mockito.when;
public class WorldBlockDropRouterTest {
@Test
public void packWithoutDropRulesAllowsTheFirstCreativeAndSurvivalBreakWhileBiomesAreCold() {
for (GameMode mode : new GameMode[] {GameMode.CREATIVE, GameMode.SURVIVAL}) {
try (Fixture fixture = new Fixture()) {
when(fixture.data.hasBlockDropRules("minecraft:stone")).thenReturn(false);
when(fixture.data.hasBlockDropRules("minecraft:grass_block")).thenReturn(true);
when(fixture.player.getGameMode()).thenReturn(mode);
fixture.queries.when(() -> EngineBukkitOps.getBiome(fixture.engine, fixture.location))
.thenThrow(new SavedBiomeUnavailableException("Biome loading", true));
BlockBreakEvent event = fixture.event();
fixture.router.onBlockBreak(event);
assertFalse(mode.name(), event.isCancelled());
assertTrue(event.isDropItems());
fixture.queries.verifyNoInteractions();
verify(fixture.drops, never()).fillDrops(anyBoolean(), any());
assertEquals(1, fixture.delayed.size());
fixture.delayed.remove().run();
assertEquals(1, fixture.markers.size());
}
}
}
@Test
public void coldBiomeCancelsBeforeChangingDropsOrSchedulingWork() {
public void coldGrassBreakNeverCancelsOrChangesNativeDropHandling() {
try (Fixture fixture = new Fixture()) {
fixture.queries.when(() -> EngineBukkitOps.getBiome(fixture.engine, fixture.location))
.thenThrow(new SavedBiomeUnavailableException("Biome loading", true));
BlockBreakEvent event = fixture.event();
fixture.environment = new CompletableFuture<>();
BlockBreakEvent event = new BlockBreakEvent(fixture.block, fixture.player);
fixture.router.onBlockBreak(event);
assertTrue(event.isCancelled());
assertTrue(event.isDropItems());
fixture.assertNoSideEffects();
fixture.queries.verify(() -> EngineBukkitOps.getRegion(fixture.engine, fixture.location), never());
}
}
@Test
public void coldRegionCancelsBeforeFillingAlreadyMatchedBiomeDrops() {
try (Fixture fixture = new Fixture()) {
fixture.queries.when(() -> EngineBukkitOps.getRegion(fixture.engine, fixture.location))
.thenThrow(new SavedBiomeUnavailableException("Region loading", true));
BlockBreakEvent event = fixture.event();
fixture.router.onBlockBreak(event);
verify(fixture.drops).shouldDropFor(fixture.blockData, fixture.data);
assertTrue(event.isCancelled());
assertTrue(event.isDropItems());
fixture.assertNoSideEffects();
}
}
@Test
public void alreadyCancelledBreakSkipsSavedLookups() {
try (Fixture fixture = new Fixture()) {
BlockBreakEvent event = fixture.event();
event.setCancelled(true);
fixture.router.onBlockBreak(event);
assertTrue(event.isCancelled());
fixture.queries.verifyNoInteractions();
fixture.assertNoSideEffects();
}
}
@Test
public void otherWorldBreakSkipsSavedLookups() {
try (Fixture fixture = new Fixture()) {
when(fixture.block.getWorld()).thenReturn(mock(World.class));
BlockBreakEvent event = fixture.event();
fixture.router.onBlockBreak(event);
assertFalse(event.isCancelled());
fixture.queries.verifyNoInteractions();
fixture.assertNoSideEffects();
assertTrue(event.isDropItems());
verify(fixture.engine, never()).getBiomeEnvironmentAsync(anyInt(), anyInt(), anyInt());
}
}
@Test
public void freshAttemptAfterLoadingProducesDropsOnceWithoutReplayingTheCancelledEvent() {
public void firstColdBreakReplacesNativeDropsOnceAfterSavedEnvironmentArrives() {
try (Fixture fixture = new Fixture()) {
fixture.queries.when(() -> EngineBukkitOps.getBiome(fixture.engine, fixture.location))
.thenThrow(new SavedBiomeUnavailableException("Biome loading", true))
.thenReturn(fixture.biome);
BlockBreakEvent cold = fixture.event();
fixture.router.onBlockBreak(cold);
fixture.assertNoSideEffects();
BlockBreakEvent retry = fixture.event();
fixture.router.onBlockBreak(retry);
assertTrue(cold.isCancelled());
assertTrue(cold.isDropItems());
assertFalse(retry.isCancelled());
assertFalse(retry.isDropItems());
assertEquals(1, fixture.delayed.size());
fixture.environment = new CompletableFuture<>();
BlockDropItemEvent event = fixture.event();
fixture.router.onBlockDropItems(event);
assertFalse(event.isCancelled());
assertTrue(event.getItems().isEmpty());
fixture.flush();
verify(fixture.drops, never()).fillDrops(anyBoolean(), any());
fixture.environment.complete(fixture.biomeEnvironment);
fixture.flush();
verify(fixture.world).dropItemNaturally(fixture.center, fixture.custom);
verify(fixture.world, never()).dropItemNaturally(any(Location.class), eq(fixture.nativeStack));
verify(fixture.drops).fillDrops(eq(false), any());
fixture.delayed.remove().run();
fixture.router.close();
fixture.flush();
verify(fixture.world).dropItemNaturally(fixture.center, fixture.custom);
}
}
verify(fixture.world).dropItemNaturally(fixture.location.clone().add(.5, .5, .5), fixture.item);
assertEquals(1, fixture.markers.size());
@Test
public void additiveRulesPreserveServerCalculatedLootIncludingQuantityAndEnchantments() {
try (Fixture fixture = new Fixture()) {
when(fixture.drops.isReplaceVanillaDrops()).thenReturn(false);
fixture.router.onBlockDropItems(fixture.event());
fixture.flush();
verify(fixture.world).dropItemNaturally(fixture.center, fixture.nativeStack);
verify(fixture.world).dropItemNaturally(fixture.center, fixture.custom);
}
}
@Test
public void rejectedSchedulerLeavesNativeLootInItsOriginalEvent() {
try (Fixture fixture = new Fixture()) {
fixture.scheduling.when(() -> J.runAt(eq(fixture.location), any(Runnable.class), eq(1))).thenReturn(false);
BlockDropItemEvent event = fixture.event();
fixture.router.onBlockDropItems(event);
assertFalse(event.isCancelled());
assertEquals(1, event.getItems().size());
verify(fixture.drops, never()).fillDrops(anyBoolean(), any());
verify(fixture.world, never()).dropItemNaturally(any(Location.class), any(ItemStack.class));
}
}
@Test
public void noNativeItemsStillProducesMatchingCustomDrops() {
try (Fixture fixture = new Fixture()) {
BlockDropItemEvent event = fixture.event();
event.getItems().clear();
fixture.router.onBlockDropItems(event);
fixture.flush();
verify(fixture.world).dropItemNaturally(fixture.center, fixture.custom);
}
}
@Test
public void noMatchingRulesLeaveNativeEventUntouchedWithoutSavedLookups() {
try (Fixture fixture = new Fixture()) {
when(fixture.data.hasBlockDropRules(anyString())).thenReturn(false);
BlockDropItemEvent event = fixture.event();
fixture.router.onBlockDropItems(event);
assertEquals(1, event.getItems().size());
verify(fixture.engine, never()).getBiomeEnvironmentAsync(anyInt(), anyInt(), anyInt());
assertTrue(fixture.delayed.isEmpty());
}
}
@Test
public void permanentSavedBiomeFailureCancelsAndPropagates() {
verifyReportedFailure(new SavedBiomeUnavailableException("Saved biome missing", false));
}
@Test
public void loadingWithSuppressedFailureCancelsAndPropagates() {
SavedBiomeUnavailableException failure = new SavedBiomeUnavailableException("Biome loading", true);
failure.addSuppressed(new IllegalStateException("Saved storage cleanup failed"));
verifyReportedFailure(failure);
}
@Test
public void protectionProbeCancelledAfterDispatchProducesNoFinalDropsOrMarkerCleanup() {
public void cancelledDropEventRemainsCancelledAndDoesNotRollOrEmitLoot() {
try (Fixture fixture = new Fixture()) {
BlockBreakEvent probe = new ProtectionProbe(fixture.block, fixture.player);
probe.setDropItems(false);
fixture.router.onBlockBreak(probe);
assertFalse(probe.isCancelled());
assertFalse(probe.isDropItems());
assertEquals(1, fixture.delayed.size());
probe.setCancelled(true);
fixture.delayed.remove().run();
verify(fixture.world, never()).dropItemNaturally(any(Location.class), any(ItemStack.class));
assertTrue(fixture.markers.isEmpty());
verify(fixture.manager, never()).getMantle();
}
}
private void verifyReportedFailure(SavedBiomeUnavailableException failure) {
try (Fixture fixture = new Fixture()) {
fixture.queries.when(() -> EngineBukkitOps.getBiome(fixture.engine, fixture.location)).thenThrow(failure);
BlockBreakEvent event = fixture.event();
assertSame(failure, assertThrows(SavedBiomeUnavailableException.class,
() -> fixture.router.onBlockBreak(event)));
fixture.environment = new CompletableFuture<>();
BlockDropItemEvent event = fixture.event();
fixture.router.onBlockDropItems(event);
event.setCancelled(true);
fixture.environment.complete(fixture.biomeEnvironment);
fixture.flush();
assertTrue(event.isCancelled());
assertTrue(event.isDropItems());
fixture.assertNoSideEffects();
verify(fixture.drops, never()).fillDrops(anyBoolean(), any());
verify(fixture.world, never()).dropItemNaturally(any(Location.class), any(ItemStack.class));
}
}
private static final class ProtectionProbe extends BlockBreakEvent {
private ProtectionProbe(Block block, Player player) {
@Test
public void permanentFailurePreservesNormalLootWithoutBlockingOrDiscardingIt() {
try (Fixture fixture = new Fixture()) {
fixture.environment = CompletableFuture.failedFuture(new SavedBiomeUnavailableException("Saved biome missing", false));
BlockDropItemEvent event = fixture.event();
fixture.router.onBlockDropItems(event);
fixture.flush();
assertFalse(event.isCancelled());
verify(fixture.world).dropItemNaturally(fixture.center, fixture.nativeStack);
verify(fixture.drops, never()).fillDrops(anyBoolean(), any());
fixture.logging.verify(() -> IrisLogging.reportError(anyString(), any(Throwable.class)));
}
}
@Test
public void managerCloseRestoresPendingNativeLootOnlyOnce() {
try (Fixture fixture = new Fixture()) {
fixture.environment = new CompletableFuture<>();
fixture.router.onBlockDropItems(fixture.event());
fixture.router.close();
fixture.environment.complete(fixture.biomeEnvironment);
fixture.flush();
verify(fixture.world).dropItemNaturally(fixture.center, fixture.nativeStack);
verify(fixture.drops, never()).fillDrops(anyBoolean(), any());
}
}
@Test
public void historicalContextOwnsMatchingAndAllParentRules() {
try (Fixture fixture = new Fixture()) {
IrisData historicData = mock(IrisData.class);
IrisRegion historicRegion = mock(IrisRegion.class);
IrisDimension historicDimension = mock(IrisDimension.class);
IrisBlockDrops regionDrops = mock(IrisBlockDrops.class);
IrisBlockDrops dimensionDrops = mock(IrisBlockDrops.class);
when(historicRegion.getBlockDrops()).thenReturn(new KList<>(regionDrops));
when(historicDimension.getBlockDrops()).thenReturn(new KList<>(dimensionDrops));
when(fixture.drops.shouldDropFor(fixture.state, historicData)).thenReturn(true);
when(regionDrops.shouldDropFor(fixture.state, historicData)).thenReturn(true);
when(dimensionDrops.shouldDropFor(fixture.state, historicData)).thenReturn(true);
BiomeEnvironment historic = new BiomeEnvironment(2L, fixture.biome, historicRegion, historicDimension, historicData);
fixture.environment = CompletableFuture.completedFuture(historic);
fixture.router.onBlockDropItems(fixture.event());
fixture.flush();
verify(fixture.engine).openBiomeEnvironmentScope(historic);
verify(regionDrops).fillDrops(eq(false), any());
verify(dimensionDrops).fillDrops(eq(false), any());
verify(fixture.engine, never()).getDimension();
verify(fixture.drops, never()).shouldDropFor(fixture.state, fixture.data);
}
}
@Test
public void skipParentsStillPreventsRegionAndDimensionLoot() {
try (Fixture fixture = new Fixture()) {
when(fixture.drops.isSkipParents()).thenReturn(true);
fixture.router.onBlockDropItems(fixture.event());
fixture.flush();
verify(fixture.region, never()).getBlockDrops();
verify(fixture.dimension, never()).getBlockDrops();
}
}
@Test
public void routedTreeBreakPreservesInventoryDestinationWithColdMetadata() {
try (Fixture fixture = new Fixture()) {
fixture.environment = new CompletableFuture<>();
RoutedBreak event = new RoutedBreak(fixture.block, fixture.player);
fixture.router.onBlockBreak(event);
assertFalse(event.isCancelled());
assertFalse(event.isDropItems());
fixture.environment.complete(fixture.biomeEnvironment);
fixture.flush();
assertEquals(List.of(fixture.custom), event.routed);
verify(fixture.world, never()).dropItemNaturally(any(Location.class), any(ItemStack.class));
}
}
@Test
public void cancelledRoutedProbeNeverEmitsDeferredLoot() {
try (Fixture fixture = new Fixture()) {
fixture.environment = new CompletableFuture<>();
RoutedBreak event = new RoutedBreak(fixture.block, fixture.player);
fixture.router.onBlockBreak(event);
event.setCancelled(true);
fixture.environment.complete(fixture.biomeEnvironment);
fixture.flush();
assertTrue(event.routed.isEmpty());
verify(fixture.drops, never()).fillDrops(anyBoolean(), any());
assertTrue(fixture.markers.isEmpty());
}
}
private static final class RoutedBreak extends BlockBreakEvent implements BlockDropRouter {
private final List<Object> routed = new ArrayList<>();
private RoutedBreak(Block block, Player player) {
super(block, player);
}
@Override
public Collection<ItemStack> captureNativeDrops() {
return List.of();
}
@Override
public boolean routeDrop(Object drop) {
return routed.add(drop);
}
}
private static final class Fixture implements AutoCloseable {
private final IrisWorldManager manager = mock(IrisWorldManager.class);
private final Engine engine = mock(Engine.class);
private final IrisEngine engine = mock(IrisEngine.class);
private final IrisData data = mock(IrisData.class);
private final World world = mock(World.class);
private final Block block = mock(Block.class);
private final BlockData blockData = mock(BlockData.class);
private final BlockState blockState = mock(BlockState.class);
private final BlockData state = mock(BlockData.class);
private final Player player = mock(Player.class);
private final Location location = new Location(world, 16, 72, 32);
private final Location center = location.clone().add(.5, .5, .5);
private final Location nativeLocation = location.clone().add(.2, .3, .4);
private final IrisBiome biome = mock(IrisBiome.class);
private final IrisRegion region = mock(IrisRegion.class);
private final IrisDimension dimension = mock(IrisDimension.class);
private final IrisBlockDrops drops = mock(IrisBlockDrops.class);
private final ItemStack item = mock(ItemStack.class);
private final ItemStack custom = mock(ItemStack.class);
private final ItemStack nativeStack = mock(ItemStack.class);
private final BiomeEnvironment biomeEnvironment = new BiomeEnvironment(1L, biome, region, dimension, data);
private CompletableFuture<BiomeEnvironment> environment = CompletableFuture.completedFuture(biomeEnvironment);
private final WorldBlockDropRouter router = new WorldBlockDropRouter(manager);
private final Queue<Runnable> delayed = new ArrayDeque<>();
private final Queue<Runnable> markers = new ArrayDeque<>();
private final MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class);
private final MockedStatic<EngineBukkitOps> queries = mockStatic(EngineBukkitOps.class);
private final MockedStatic<J> scheduling = mockStatic(J.class);
private final MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class);
private Fixture() {
IrisWorld irisWorld = mock(IrisWorld.class);
EngineTarget target = mock(EngineTarget.class);
IrisRegion region = mock(IrisRegion.class);
IrisDimension dimension = mock(IrisDimension.class);
when(manager.getEngine()).thenReturn(engine);
when(manager.getTarget()).thenReturn(target);
when(manager.getData()).thenReturn(data);
@@ -235,56 +294,67 @@ public class WorldBlockDropRouterTest {
when(target.getWorld()).thenReturn(irisWorld);
when(engine.getWorld()).thenReturn(irisWorld);
when(irisWorld.minHeight()).thenReturn(-64);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.empty());
when(engine.getBiomeEnvironmentAsync(16, 136, 32)).thenAnswer(invocation -> environment);
when(engine.openBiomeEnvironmentScope(any(BiomeEnvironment.class))).thenReturn(() -> {});
binding.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
when(world.getUID()).thenReturn(new UUID(0L, 1L));
when(block.getWorld()).thenReturn(world);
when(block.getX()).thenReturn(16);
when(block.getY()).thenReturn(72);
when(block.getZ()).thenReturn(32);
when(block.getLocation()).thenReturn(location);
when(block.getBlockData()).thenReturn(blockData);
when(blockData.getAsString()).thenReturn("minecraft:stone");
when(block.getBlockData()).thenReturn(state);
when(state.clone()).thenReturn(state);
when(state.getAsString()).thenReturn("minecraft:grass_block");
when(blockState.getBlockData()).thenReturn(state);
when(blockState.getLocation()).thenReturn(location);
when(nativeStack.clone()).thenReturn(nativeStack);
when(biome.getBlockDrops()).thenReturn(new KList<>(drops));
when(region.getBlockDrops()).thenReturn(new KList<>());
when(dimension.getBlockDrops()).thenReturn(new KList<>());
when(drops.shouldDropFor(blockData, data)).thenReturn(true);
when(drops.shouldDropFor(state, data)).thenReturn(true);
when(drops.isReplaceVanillaDrops()).thenReturn(true);
PlayerInventory inventory = mock(PlayerInventory.class);
when(player.getInventory()).thenReturn(inventory);
when(block.getDrops(inventory.getItemInMainHand(), player)).thenReturn(List.of(nativeStack));
doAnswer(invocation -> {
KList<ItemStack> results = invocation.getArgument(1);
results.add(item);
results.add(custom);
return null;
}).when(drops).fillDrops(eq(false), any());
queries.when(() -> EngineBukkitOps.getBiome(engine, location)).thenReturn(biome);
queries.when(() -> EngineBukkitOps.getRegion(engine, location)).thenReturn(region);
when(manager.managedTask(anyString(), any(Runnable.class)))
.thenAnswer(invocation -> invocation.getArgument(1));
when(manager.managedTask(anyString(), any(Runnable.class))).thenAnswer(invocation -> invocation.getArgument(1));
scheduling.when(() -> J.runAt(eq(location), any(Runnable.class), eq(1))).thenAnswer(invocation -> {
delayed.add(invocation.getArgument(1));
return true;
});
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 1, 2)).thenReturn(true);
scheduling.when(() -> J.runRegionFuture(eq(world), eq(1), eq(2), any(Runnable.class))).thenAnswer(invocation -> {
Runnable task = invocation.getArgument(3);
task.run();
return CompletableFuture.completedFuture(null);
});
scheduling.when(() -> J.a(any(Runnable.class))).thenAnswer(invocation -> {
markers.add(invocation.getArgument(0));
return null;
});
logging.when(() -> IrisLogging.warnOnce(anyString(), anyString())).thenReturn(true);
}
private BlockBreakEvent event() {
return new BlockBreakEvent(block, player);
private BlockDropItemEvent event() {
Item item = mock(Item.class);
when(item.getItemStack()).thenReturn(nativeStack);
when(item.getLocation()).thenReturn(nativeLocation);
return new BlockDropItemEvent(block, blockState, player, new ArrayList<>(List.of(item)));
}
private void assertNoSideEffects() {
verify(drops, never()).fillDrops(anyBoolean(), any());
verify(world, never()).dropItemNaturally(any(Location.class), any(ItemStack.class));
verify(manager, never()).getMantle();
scheduling.verifyNoInteractions();
assertTrue(delayed.isEmpty());
assertTrue(markers.isEmpty());
private void flush() {
while (!delayed.isEmpty()) {
delayed.remove().run();
}
}
@Override
public void close() {
scheduling.close();
queries.close();
logging.close();
binding.close();
}
}
@@ -0,0 +1,207 @@
package art.arcane.iris.pack.datapack;
import art.arcane.iris.spi.IrisLogging;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class ServerConfiguratorTimeoutTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void spigotLiveConfigurationKeepsTheRaisedTimeoutWhenTheServerSavesItAgain() throws Exception {
Path file = temporaryFolder.newFile("custom-spigot.yml").toPath();
Files.writeString(file, "settings:\n timeout-time: 60\n sample-setting: retained\n");
YamlConfiguration live = load(file);
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
logging.when(() -> IrisLogging.warn(anyString())).thenAnswer(invocation -> {
assertEquals(300L, load(file).getLong("settings.timeout-time"));
return null;
});
ServerConfigurator.increaseKeepAliveSpigot(file, live);
assertEquals(300L, live.getLong("settings.timeout-time"));
live.set("world-settings.default.sample-world-setting", true);
live.save(file.toFile());
YamlConfiguration persisted = load(file);
assertEquals(300L, persisted.getLong("settings.timeout-time"));
assertEquals("retained", persisted.getString("settings.sample-setting"));
logging.verify(() -> IrisLogging.warn("Updated " + file + " settings.timeout-time: 60 -> 300"
+ " seconds. Takes effect after a server restart."));
}
}
@Test
public void higherSpigotTimeoutIsLeftUntouched() throws Exception {
Path file = temporaryFolder.newFile("larger-spigot.yml").toPath();
String original = "# Keep this configuration\nsettings:\n timeout-time: 600\n";
Files.writeString(file, original);
YamlConfiguration live = load(file);
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
ServerConfigurator.increaseKeepAliveSpigot(file, live);
assertEquals(600L, live.getLong("settings.timeout-time"));
assertEquals(original, Files.readString(file));
logging.verifyNoInteractions();
}
}
@Test
public void sharedSpigotConfigurationSymlinkIsPreservedWhileItsTargetIsUpdated() throws Exception {
Path directory = temporaryFolder.newFolder("shared-settings").toPath();
Path target = directory.resolve("shared-spigot.yml");
Path link = directory.resolve("spigot.yml");
Files.writeString(target, "settings:\n timeout-time: 60\n sample-setting: retained\n");
Path relativeTarget = target.getFileName();
Files.createSymbolicLink(link, relativeTarget);
YamlConfiguration live = load(link);
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
ServerConfigurator.increaseKeepAliveSpigot(link, live);
assertTrue(Files.isSymbolicLink(link));
assertEquals(relativeTarget, Files.readSymbolicLink(link));
assertEquals(300L, load(target).getLong("settings.timeout-time"));
assertEquals("retained", load(target).getString("settings.sample-setting"));
assertEquals(300L, live.getLong("settings.timeout-time"));
}
}
@Test
public void paperWatchdogPersistsToTheSpecifiedFileAndPreservesOtherSettings() throws Exception {
Path directory = temporaryFolder.newFolder("custom-paper-directory").toPath();
Path file = directory.resolve("paper-global.yml");
Files.writeString(file, "watchdog:\n early-warning-delay: 10000\n early-warning-every: 5000\n"
+ "misc:\n sample-setting: retained\n");
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
ServerConfigurator.increasePaperWatchdog(file);
YamlConfiguration persisted = load(file);
assertEquals(180000L, persisted.getLong("watchdog.early-warning-delay"));
assertEquals(5000L, persisted.getLong("watchdog.early-warning-every"));
assertEquals("retained", persisted.getString("misc.sample-setting"));
logging.verify(() -> IrisLogging.warn("Updated " + file + " watchdog.early-warning-delay: 10000 -> 180000"
+ " milliseconds. Takes effect after a server restart."));
}
}
@Test
public void higherPaperWatchdogDelayIsLeftUntouched() throws Exception {
Path file = temporaryFolder.newFile("larger-paper-global.yml").toPath();
String original = "# Keep this configuration\nwatchdog:\n early-warning-delay: 240000\n";
Files.writeString(file, original);
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
ServerConfigurator.increasePaperWatchdog(file);
assertEquals(original, Files.readString(file));
logging.verifyNoInteractions();
}
}
@Test
public void malformedPaperYamlIsPreservedAndDoesNotReportAnUpdate() throws Exception {
Path file = temporaryFolder.newFile("invalid-paper-global.yml").toPath();
String malformed = "watchdog:\n early-warning-delay: [invalid\n";
Files.writeString(file, malformed);
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
assertThrows(InvalidConfigurationException.class, () -> ServerConfigurator.increasePaperWatchdog(file));
assertEquals(malformed, Files.readString(file));
logging.verifyNoInteractions();
}
}
@Test
public void failedSpigotWriteRestoresTheLiveValueAndDoesNotReportSuccess() throws Exception {
Path blockedParent = temporaryFolder.newFile("blocked-parent").toPath();
Files.writeString(blockedParent, "retained");
YamlConfiguration live = new YamlConfiguration();
live.set("settings.timeout-time", 60L);
live.set("settings.sample-setting", "retained");
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
assertThrows(IOException.class, () -> ServerConfigurator.increaseKeepAliveSpigot(
blockedParent.resolve("spigot.yml"), live));
assertEquals(60L, live.getLong("settings.timeout-time"));
assertEquals("retained", live.getString("settings.sample-setting"));
assertEquals("retained", Files.readString(blockedParent));
logging.verifyNoInteractions();
}
}
@Test
public void actualServerOptionsResolveCustomConfigurationPaths() throws Exception {
Path directory = temporaryFolder.newFolder("custom-settings").toPath();
File spigot = directory.resolve("nested/../server-spigot.yml").toFile();
File paper = directory.resolve("nested/../paper").toFile();
Server server = mock(Server.class, withSettings().extraInterfaces(ConsoleAccess.class));
when(((ConsoleAccess) server).getServer()).thenReturn(new ConfigurationConsole(
new ConfigurationOptions(Map.of("spigot-settings", spigot, "paper-settings-directory", paper))));
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
bukkit.when(Bukkit::getServer).thenReturn(server);
assertEquals(spigot.toPath().toAbsolutePath().normalize(), ServerConfigurator.serverConfigurationOption("spigot-settings"));
assertEquals(paper.toPath().toAbsolutePath().normalize(), ServerConfigurator.serverConfigurationOption("paper-settings-directory"));
assertThrows(IOException.class, () -> ServerConfigurator.serverConfigurationOption("missing-option"));
}
}
private static YamlConfiguration load(Path file) throws IOException, InvalidConfigurationException {
YamlConfiguration configuration = new YamlConfiguration();
configuration.load(file.toFile());
return configuration;
}
public interface ConsoleAccess {
ConfigurationConsole getServer();
}
public static final class ConfigurationConsole {
public final ConfigurationOptions options;
public ConfigurationConsole(ConfigurationOptions options) {
this.options = options;
}
}
public static final class ConfigurationOptions {
private final Map<String, File> paths;
public ConfigurationOptions(Map<String, File> paths) {
this.paths = paths;
}
public File valueOf(String name) {
return paths.get(name);
}
}
}
@@ -2,6 +2,7 @@ package art.arcane.iris.platform.generation;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisWorldManager;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.ReactiveFolder;
@@ -25,11 +26,60 @@ import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class BukkitChunkGeneratorCloseTest {
@Test
public void nativeDropFlushCompletesBeforeExclusiveCloseIsDispatched() throws Exception {
CloseFixture fixture = new CloseFixture();
IrisWorldManager manager = mock(IrisWorldManager.class);
CompletableFuture<Void> flushed = new CompletableFuture<>();
when(fixture.engine.getWorldManager()).thenReturn(manager);
when(manager.flushPendingBlockDropsAsync()).thenReturn(flushed);
CompletableFuture<Void> operation = fixture.queueOperation();
CompletableFuture<Void> closed = fixture.generator.closeAsync();
assertFalse(closed.isDone());
verify(fixture.generator, never()).withExclusiveControlFuture(any(Runnable.class));
verify(fixture.engine, never()).close();
flushed.complete(null);
verify(fixture.generator).withExclusiveControlFuture(any(Runnable.class));
verify(fixture.engine, never()).close();
fixture.operation.get().run();
operation.complete(null);
closed.join();
verify(fixture.engine).close();
}
@Test
public void failedNativeDropFlushLeavesTheInitialCloseRetryable() throws Exception {
CloseFixture fixture = new CloseFixture();
IrisWorldManager manager = mock(IrisWorldManager.class);
when(fixture.engine.getWorldManager()).thenReturn(manager);
when(manager.flushPendingBlockDropsAsync()).thenReturn(
CompletableFuture.failedFuture(new IllegalStateException("Drop scheduling unavailable")),
CompletableFuture.completedFuture(null));
CompletableFuture<Void> failed = fixture.generator.closeAsync();
assertTrue(failed.isCompletedExceptionally());
assertFalse(fixture.generator.isClosing());
verify(fixture.generator, never()).withExclusiveControlFuture(any(Runnable.class));
verify(fixture.engine, never()).close();
CompletableFuture<Void> operation = fixture.queueOperation();
CompletableFuture<Void> retried = fixture.generator.closeAsync();
assertNotSame(failed, retried);
fixture.operation.get().run();
operation.complete(null);
retried.join();
assertTrue(fixture.generator.isClosing());
verify(fixture.engine).close();
}
@Test
public void failedCloseReleasesInitialEntryDeferral() throws Exception {
CloseFixture fixture = new CloseFixture();