mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-19 23:14:10 +00:00
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:
@@ -990,6 +990,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
public void onDisable() {
|
||||
preservePendingBlockDrops();
|
||||
papiLifecycleActive = false;
|
||||
if (updateNotifier != null) {
|
||||
updateNotifier.close();
|
||||
@@ -1034,6 +1035,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
@Override
|
||||
public CompletionStage<Void> commitReload(ReloadAware.PreUnloadReason reason) {
|
||||
preservePendingBlockDrops();
|
||||
papiLifecycleActive = false;
|
||||
teardownPapi();
|
||||
if (IrisToolbelt.isServerStopping()) {
|
||||
@@ -1283,6 +1285,20 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
&& preservation.hasActiveResources();
|
||||
}
|
||||
|
||||
private void preservePendingBlockDrops() {
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||
if (generator != null && generator.getEngine() != null
|
||||
&& generator.getEngine().getWorldManager() instanceof IrisWorldManager manager) {
|
||||
manager.flushPendingBlockDropsAsync().whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
Iris.reportError("Failed to preserve pending block loot before Iris teardown.", failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||
List<World> irisWorlds = new ArrayList<>();
|
||||
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
||||
|
||||
+18
-4
@@ -3,19 +3,33 @@ package art.arcane.iris.world.tree;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
|
||||
final class RoutedBlockBreakEvent extends BlockBreakEvent implements BlockDropRouter {
|
||||
private final TreeFellerSVC service;
|
||||
private final FellingRun run;
|
||||
private final ItemStack tool;
|
||||
|
||||
RoutedBlockBreakEvent(Block block, Player player, FellingRun run, TreeFellerSVC service) {
|
||||
super(block, player);
|
||||
this.run = run;
|
||||
this.service = service;
|
||||
RoutedBlockBreakEvent(Options options) {
|
||||
super(options.block(), options.player());
|
||||
run = options.run();
|
||||
service = options.service();
|
||||
tool = Objects.requireNonNull(options.tool(), "tool").clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ItemStack> captureNativeDrops() {
|
||||
return getBlock().getDrops(tool);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean routeDrop(Object drop) {
|
||||
return service.isServiceEnabled() && run.presentation.routeDrop(drop);
|
||||
}
|
||||
|
||||
record Options(Block block, Player player, FellingRun run, TreeFellerSVC service, ItemStack tool) {
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -387,7 +387,8 @@ final class TreeFellingRunner {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockBreakEvent probe = new RoutedBlockBreakEvent(block, run.candidate.player(), run, service);
|
||||
BlockBreakEvent probe = new RoutedBlockBreakEvent(new RoutedBlockBreakEvent.Options(
|
||||
block, run.candidate.player(), run, service, reservation.toolForDrops()));
|
||||
service.managedEvents.add(probe);
|
||||
try {
|
||||
Bukkit.getPluginManager().callEvent(probe);
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package art.arcane.iris.world.tree;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
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 RoutedBlockBreakEventTest {
|
||||
@Test
|
||||
public void nativeLootUsesTheReservedToolCloneWithoutReadingPlayerInventory() {
|
||||
Block block = mock(Block.class);
|
||||
Player player = mock(Player.class);
|
||||
ItemStack reservedTool = mock(ItemStack.class);
|
||||
ItemStack capturedTool = mock(ItemStack.class);
|
||||
ItemStack drop = mock(ItemStack.class);
|
||||
Collection<ItemStack> expected = List.of(drop);
|
||||
when(reservedTool.clone()).thenReturn(capturedTool);
|
||||
when(block.getDrops(capturedTool)).thenReturn(expected);
|
||||
RoutedBlockBreakEvent event = new RoutedBlockBreakEvent(new RoutedBlockBreakEvent.Options(
|
||||
block, player, mock(FellingRun.class), mock(TreeFellerSVC.class), reservedTool));
|
||||
|
||||
reservedTool.setType(Material.AIR);
|
||||
|
||||
assertSame(expected, event.captureNativeDrops());
|
||||
verify(reservedTool).clone();
|
||||
verify(block).getDrops(capturedTool);
|
||||
verify(block, never()).getDrops(reservedTool);
|
||||
verifyNoInteractions(player);
|
||||
}
|
||||
}
|
||||
+28
-28
@@ -31,6 +31,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockDropHooks;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||
import art.arcane.iris.structure.placement.LootResolver;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
@@ -181,26 +182,30 @@ public final class ModdedBlockBreakHandler {
|
||||
NativeBlockState brokenState,
|
||||
Engine engine
|
||||
) {
|
||||
KList<IrisBlockDrops> providers = providers(engine, position, brokenState);
|
||||
if (providers.isEmpty()) {
|
||||
return Result.empty();
|
||||
}
|
||||
BiomeEnvironment environment = engine.getBiomeEnvironment(position.x(),
|
||||
position.y() - engine.getMinHeight(), position.z());
|
||||
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
|
||||
KList<IrisBlockDrops> providers = providers(environment, brokenState);
|
||||
if (providers.isEmpty()) {
|
||||
return Result.empty();
|
||||
}
|
||||
|
||||
KList<NativeItemStack> drops = new KList<>();
|
||||
boolean replaceVanillaDrops = false;
|
||||
for (IrisBlockDrops provider : providers) {
|
||||
replaceVanillaDrops |= provider.isReplaceVanillaDrops();
|
||||
for (IrisLoot loot : provider.getDrops()) {
|
||||
if (!LootResolver.oneIn(RNG.r, loot.getRarity())) {
|
||||
continue;
|
||||
}
|
||||
NativeItemStack stack = ModdedItemTranslator.stack(loot, RNG.r, ModdedItemTranslator.context(level));
|
||||
if (stack != null && !stack.isEmpty()) {
|
||||
drops.add(stack);
|
||||
KList<NativeItemStack> drops = new KList<>();
|
||||
boolean replaceVanillaDrops = false;
|
||||
for (IrisBlockDrops provider : providers) {
|
||||
replaceVanillaDrops |= provider.isReplaceVanillaDrops();
|
||||
for (IrisLoot loot : provider.getDrops()) {
|
||||
if (!LootResolver.oneIn(RNG.r, loot.getRarity())) {
|
||||
continue;
|
||||
}
|
||||
NativeItemStack stack = ModdedItemTranslator.stack(loot, RNG.r, ModdedItemTranslator.context(level));
|
||||
if (stack != null && !stack.isEmpty()) {
|
||||
drops.add(stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Result(drops, replaceVanillaDrops, null);
|
||||
}
|
||||
return new Result(drops, replaceVanillaDrops, null);
|
||||
}
|
||||
|
||||
private static void removeMarker(Engine engine, NativeBlockPoint position) {
|
||||
@@ -225,22 +230,17 @@ public final class ModdedBlockBreakHandler {
|
||||
engine.getMantle().getMantle().remove(position.x(), mantleY, position.z(), TreeBlockMaterial.class);
|
||||
}
|
||||
|
||||
private static KList<IrisBlockDrops> providers(Engine engine, NativeBlockPoint position, NativeBlockState brokenState) {
|
||||
private static KList<IrisBlockDrops> providers(BiomeEnvironment environment, NativeBlockState brokenState) {
|
||||
KList<IrisBlockDrops> providers = new KList<>();
|
||||
IrisData data = engine.getData();
|
||||
int relativeY = position.y() - engine.getMinHeight();
|
||||
IrisBiome biome = engine.getBiome(position.x(), relativeY, position.z());
|
||||
if (biome != null) {
|
||||
addMatching(providers, biome.getBlockDrops(), brokenState, data);
|
||||
}
|
||||
IrisData data = environment.data();
|
||||
IrisBiome biome = environment.biome();
|
||||
addMatching(providers, biome.getBlockDrops(), brokenState, data);
|
||||
if (skipsParents(providers)) {
|
||||
return providers;
|
||||
}
|
||||
IrisRegion region = engine.getRegion(position.x(), relativeY, position.z());
|
||||
if (region != null) {
|
||||
addMatching(providers, region.getBlockDrops(), brokenState, data);
|
||||
}
|
||||
addMatching(providers, engine.getDimension().getBlockDrops(), brokenState, data);
|
||||
IrisRegion region = environment.region();
|
||||
addMatching(providers, region.getBlockDrops(), brokenState, data);
|
||||
addMatching(providers, environment.dimension().getBlockDrops(), brokenState, data);
|
||||
return providers;
|
||||
}
|
||||
|
||||
|
||||
+121
@@ -18,6 +18,21 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.runtime.BiomeEnvironment;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
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.loot.IrisBlockDrops;
|
||||
import art.arcane.iris.world.loot.IrisLoot;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeItemStack;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldGenerators;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import net.minecraft.core.Direction;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
@@ -27,9 +42,22 @@ import org.junit.Test;
|
||||
import org.junit.BeforeClass;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
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 ModdedBlockBreakHandlerTest {
|
||||
@BeforeClass
|
||||
@@ -48,4 +76,97 @@ public class ModdedBlockBreakHandlerTest {
|
||||
assertTrue(ModdedBlockBreakHandler.matchesState(north, east, false));
|
||||
assertFalse(ModdedBlockBreakHandler.matchesState(north, ModdedBlockState.of(Blocks.COBBLESTONE.defaultBlockState(), null), false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void historicalEnvironmentSuppliesAllRulesAndScopesMatchingAndLoot() {
|
||||
try (DropFixture fixture = new DropFixture()) {
|
||||
fixture.dimensionRule.setReplaceVanillaDrops(true);
|
||||
|
||||
ModdedBlockBreakHandler.Result result = fixture.evaluate();
|
||||
|
||||
assertTrue(result.replaceVanillaDrops());
|
||||
assertEquals(3, result.drops().size());
|
||||
assertSame(fixture.item, result.drops().getFirst());
|
||||
assertFalse(fixture.scoped.get());
|
||||
verify(fixture.engine).getBiomeEnvironment(16, 136, 32);
|
||||
verify(fixture.engine).openBiomeEnvironmentScope(fixture.environment);
|
||||
verify(fixture.engine, never()).getData();
|
||||
verify(fixture.engine, never()).getDimension();
|
||||
verify(fixture.engine, never()).getBiome(anyInt(), anyInt(), anyInt());
|
||||
verify(fixture.engine, never()).getRegion(anyInt(), anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void historicalBiomeCanSkipRegionAndDimensionDrops() {
|
||||
try (DropFixture fixture = new DropFixture()) {
|
||||
fixture.biomeRule.setSkipParents(true);
|
||||
fixture.dimensionRule.setReplaceVanillaDrops(true);
|
||||
|
||||
ModdedBlockBreakHandler.Result result = fixture.evaluate();
|
||||
|
||||
assertFalse(result.replaceVanillaDrops());
|
||||
assertEquals(1, result.drops().size());
|
||||
assertFalse(fixture.scoped.get());
|
||||
verify(fixture.region, never()).getBlockDrops();
|
||||
verify(fixture.dimension, never()).getBlockDrops();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DropFixture implements AutoCloseable {
|
||||
private final NativeWorld world = mock(NativeWorld.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final IrisBiome biome = mock(IrisBiome.class);
|
||||
private final IrisRegion region = mock(IrisRegion.class);
|
||||
private final IrisDimension dimension = mock(IrisDimension.class);
|
||||
private final IrisData data = mock(IrisData.class);
|
||||
private final BiomeEnvironment environment = new BiomeEnvironment(7L, biome, region, dimension, data);
|
||||
private final ModdedBlockState state = ModdedBlockState.of(Blocks.COBBLESTONE.defaultBlockState(), null);
|
||||
private final NativeItemStack item = mock(NativeItemStack.class);
|
||||
private final IrisLoot loot = new IrisLoot().setRarity(1);
|
||||
private final IrisBlockData configuredBlock = mock(IrisBlockData.class);
|
||||
private final IrisBlockDrops biomeRule = rule();
|
||||
private final IrisBlockDrops regionRule = rule();
|
||||
private final IrisBlockDrops dimensionRule = rule();
|
||||
private final AtomicBoolean scoped = new AtomicBoolean();
|
||||
private final MockedStatic<NativeWorldGenerators> generators = mockStatic(NativeWorldGenerators.class);
|
||||
private final MockedStatic<ModdedItemTranslator> translator = mockStatic(ModdedItemTranslator.class);
|
||||
|
||||
private DropFixture() {
|
||||
IrisModdedChunkGenerator generator = mock(IrisModdedChunkGenerator.class);
|
||||
generators.when(() -> NativeWorldGenerators.find(world, IrisModdedChunkGenerator.class)).thenReturn(generator);
|
||||
when(generator.engineIfBound()).thenReturn(engine);
|
||||
when(engine.getMinHeight()).thenReturn(-64);
|
||||
when(engine.getBiomeEnvironment(16, 136, 32)).thenReturn(environment);
|
||||
when(engine.openBiomeEnvironmentScope(environment)).thenAnswer(invocation -> {
|
||||
assertTrue(scoped.compareAndSet(false, true));
|
||||
return (BiomeEnvironment.Scope) () -> scoped.set(false);
|
||||
});
|
||||
when(biome.getBlockDrops()).thenReturn(new KList<>(biomeRule));
|
||||
when(region.getBlockDrops()).thenReturn(new KList<>(regionRule));
|
||||
when(dimension.getBlockDrops()).thenReturn(new KList<>(dimensionRule));
|
||||
when(configuredBlock.getBlockData(data)).thenAnswer(invocation -> {
|
||||
assertTrue(scoped.get());
|
||||
return state;
|
||||
});
|
||||
translator.when(() -> ModdedItemTranslator.stack(eq(loot), eq(RNG.r), any())).thenAnswer(invocation -> {
|
||||
assertTrue(scoped.get());
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
private IrisBlockDrops rule() {
|
||||
return new IrisBlockDrops().setBlocks(new KList<>(configuredBlock)).setDrops(new KList<>(loot));
|
||||
}
|
||||
|
||||
private ModdedBlockBreakHandler.Result evaluate() {
|
||||
return ModdedBlockBreakHandler.evaluateManagedDrops(world, new NativeBlockPoint(16, 72, 32), state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
translator.close();
|
||||
generators.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,9 +539,20 @@ public class IrisEngine implements Engine {
|
||||
if (saved.isPresent()) {
|
||||
return saved.get();
|
||||
}
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||
openGenerationHistoryReadScope(x, z, "resolve a biome environment")) {
|
||||
return Engine.super.getBiomeEnvironment(x, y, z);
|
||||
return resolveUnsavedBiomeEnvironment(x, y, z);
|
||||
}
|
||||
|
||||
public CompletableFuture<BiomeEnvironment> getBiomeEnvironmentAsync(int x, int y, int z) {
|
||||
try {
|
||||
if (!usesSavedBiomeEnvironment()) {
|
||||
return CompletableFuture.completedFuture(getBiomeEnvironment(x, y, z));
|
||||
}
|
||||
int worldY = y + getWorld().minHeight();
|
||||
return generationHistoryRuntimeRouter.biomes().readChunkAsync(Math.floorDiv(x, 16), Math.floorDiv(z, 16))
|
||||
.thenApply(session -> session.resolve(x, worldY, z, false)
|
||||
.orElseGet(() -> resolveUnsavedBiomeEnvironment(x, y, z)));
|
||||
} catch (RuntimeException | Error failure) {
|
||||
return CompletableFuture.failedFuture(failure);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,6 +588,13 @@ public class IrisEngine implements Engine {
|
||||
return binding;
|
||||
}
|
||||
|
||||
private BiomeEnvironment resolveUnsavedBiomeEnvironment(int x, int y, int z) {
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||
openGenerationHistoryReadScope(x, z, "resolve a biome environment")) {
|
||||
return Engine.super.getBiomeEnvironment(x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<BiomeEnvironment> resolveSavedBiomeEnvironment(int x, int y, int z, boolean surface) {
|
||||
if (!usesSavedBiomeEnvironment()) {
|
||||
return Optional.empty();
|
||||
|
||||
@@ -38,11 +38,15 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockDropItemEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
@@ -267,24 +271,38 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
blockDropRouter.onBlockBreak(e);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockDropItems(BlockDropItemEvent event) {
|
||||
runManagerTask("bukkit_world_manager_block_drops", () -> blockDropRouter.onBlockDropItems(event));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockPlace(BlockPlaceEvent e) {
|
||||
blockDropRouter.onBlockPlace(e);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> flushPendingBlockDropsAsync() {
|
||||
return blockDropRouter.flushAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
Throwable failure = null;
|
||||
try {
|
||||
chunkMaintenance.close();
|
||||
} catch (Throwable e) {
|
||||
failure = e;
|
||||
failure = appendCloseFailure(failure, e);
|
||||
}
|
||||
try {
|
||||
super.close();
|
||||
} catch (Throwable e) {
|
||||
failure = appendCloseFailure(failure, e);
|
||||
}
|
||||
try {
|
||||
blockDropRouter.close();
|
||||
} catch (Throwable e) {
|
||||
failure = appendCloseFailure(failure, e);
|
||||
}
|
||||
if (!looperStopped) {
|
||||
try {
|
||||
if (looper != null) {
|
||||
|
||||
@@ -20,13 +20,8 @@ package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.world.tree.BlockDropRouter;
|
||||
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.world.loot.IrisBlockDrops;
|
||||
import art.arcane.iris.world.entity.IrisMarker;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -35,81 +30,140 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockDropItemEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Biome, region and dimension block drops for a Bukkit Iris world. The drop list is resolved on the
|
||||
* event thread, the marker cleanup runs asynchronously and the drops themselves are handed to the
|
||||
* event's own router when it offers one so a deferred break keeps its inventory destination.
|
||||
* Biome, region and dimension block drops for a Bukkit Iris world. Native loot is retained while
|
||||
* saved biome definitions load, then finalized on the block's owning thread after the break.
|
||||
*/
|
||||
final class WorldBlockDropRouter {
|
||||
private final IrisWorldManager manager;
|
||||
private final Set<DeferredDrops> pending = ConcurrentHashMap.newKeySet();
|
||||
private boolean closed;
|
||||
|
||||
WorldBlockDropRouter(IrisWorldManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
void onBlockBreak(BlockBreakEvent e) {
|
||||
if (e.isCancelled()) {
|
||||
void onBlockBreak(BlockBreakEvent event) {
|
||||
if (event.isCancelled() || !isManagedWorld(event.getBlock().getWorld())) {
|
||||
return;
|
||||
}
|
||||
if (e.getBlock().getWorld().equals(BukkitWorldBinding.world(manager.getTarget().getWorld()))) {
|
||||
int blockX = e.getBlock().getX();
|
||||
int mantleY = toMantleY(e.getBlock().getY(), manager.getEngine().getWorld().minHeight());
|
||||
int blockZ = e.getBlock().getZ();
|
||||
|
||||
List<IrisBlockDrops> dropProviders;
|
||||
try {
|
||||
dropProviders = resolveDropProviders(e);
|
||||
} catch (SavedBiomeUnavailableException unavailable) {
|
||||
e.setCancelled(true);
|
||||
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
|
||||
throw unavailable;
|
||||
}
|
||||
Location location = event.getBlock().getLocation();
|
||||
int mantleY = toMantleY(location.getBlockY(), manager.getEngine().getWorld().minHeight());
|
||||
Runnable finalizedBreak = manager.managedTask("bukkit_world_manager_block_break_finalize", () -> {
|
||||
if (event.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
KList<ItemStack> d = new KList<>();
|
||||
dropProviders.forEach(provider -> provider.fillDrops(false, d));
|
||||
|
||||
if (dropProviders.stream().anyMatch(IrisBlockDrops::isReplaceVanillaDrops)) {
|
||||
e.setDropItems(false);
|
||||
}
|
||||
|
||||
World w = e.getBlock().getWorld();
|
||||
Location blockLocation = e.getBlock().getLocation();
|
||||
Location dropLocation = blockLocation.clone().add(.5, .5, .5);
|
||||
BlockDropRouter dropRouter = e instanceof BlockDropRouter router ? router : null;
|
||||
Runnable finalizedBreak = manager.managedTask("bukkit_world_manager_block_break_finalize", () -> {
|
||||
if (e.isCancelled()) {
|
||||
J.a(manager.managedTask("bukkit_world_manager_block_break_marker", () -> {
|
||||
MatterMarker marker = manager.getMantle().get(location.getBlockX(), mantleY, location.getBlockZ(), MatterMarker.class);
|
||||
if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
|
||||
return;
|
||||
}
|
||||
J.a(manager.managedTask("bukkit_world_manager_block_break_marker", () -> {
|
||||
MatterMarker marker = manager.getMantle().get(blockX, mantleY, blockZ, MatterMarker.class);
|
||||
if (marker == null || marker.getTag().equals("cave_floor") || marker.getTag().equals("cave_ceiling")) {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisMarker mark = manager.getData().getMarkerLoader().load(marker.getTag());
|
||||
if (mark == null || mark.isRemoveOnChange()) {
|
||||
manager.getMantle().remove(blockX, mantleY, blockZ, MatterMarker.class);
|
||||
}
|
||||
}));
|
||||
routeDrops(d, dropRouter, item -> w.dropItemNaturally(dropLocation, item));
|
||||
});
|
||||
if (!J.runAt(blockLocation, finalizedBreak, 1) && !J.isFolia()) {
|
||||
J.s(finalizedBreak, 1);
|
||||
IrisMarker mark = manager.getData().getMarkerLoader().load(marker.getTag());
|
||||
if (mark == null || mark.isRemoveOnChange()) {
|
||||
manager.getMantle().remove(location.getBlockX(), mantleY, location.getBlockZ(), MatterMarker.class);
|
||||
}
|
||||
}));
|
||||
});
|
||||
schedule(location, finalizedBreak);
|
||||
if (!(event instanceof BlockDropRouter router) || !event.isDropItems()) {
|
||||
return;
|
||||
}
|
||||
synchronized (pending) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
BlockData state = event.getBlock().getBlockData().clone();
|
||||
if (!mayHaveBlockDropRules(state)) {
|
||||
return;
|
||||
}
|
||||
Location dropLocation = location.clone().add(.5, .5, .5);
|
||||
List<NativeDrop> originals = new ArrayList<>();
|
||||
for (Object nativeDrop : router.captureNativeDrops()) {
|
||||
if (nativeDrop instanceof ItemStack stack) {
|
||||
originals.add(new NativeDrop(dropLocation, stack.clone()));
|
||||
}
|
||||
}
|
||||
CompletableFuture<BiomeEnvironment> environment = environment(location);
|
||||
if (defer(new DeferredDrops(location, state, router, () -> !event.isCancelled(), originals, environment, new AtomicBoolean()))) {
|
||||
event.setDropItems(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onBlockPlace(BlockPlaceEvent e) {
|
||||
void onBlockDropItems(BlockDropItemEvent event) {
|
||||
if (event.isCancelled() || !isManagedWorld(event.getBlock().getWorld())) {
|
||||
return;
|
||||
}
|
||||
synchronized (pending) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
BlockData state = event.getBlockState().getBlockData().clone();
|
||||
if (!mayHaveBlockDropRules(state)) {
|
||||
return;
|
||||
}
|
||||
Location location = event.getBlockState().getLocation();
|
||||
CompletableFuture<BiomeEnvironment> environment = environment(location);
|
||||
List<NativeDrop> originals = new ArrayList<>(event.getItems().size());
|
||||
for (Item item : event.getItems()) {
|
||||
originals.add(new NativeDrop(location.clone().add(.5, .5, .5), item.getItemStack().clone()));
|
||||
}
|
||||
if (defer(new DeferredDrops(location, state, null, () -> !event.isCancelled(), originals, environment, new AtomicBoolean()))) {
|
||||
event.getItems().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onBlockPlace(BlockPlaceEvent event) {
|
||||
|
||||
}
|
||||
|
||||
CompletableFuture<Void> flushAsync() {
|
||||
List<DeferredDrops> outstanding;
|
||||
synchronized (pending) {
|
||||
outstanding = List.copyOf(pending);
|
||||
}
|
||||
List<CompletableFuture<Void>> restores = new ArrayList<>(outstanding.size());
|
||||
for (DeferredDrops drops : outstanding) {
|
||||
Location location = drops.location;
|
||||
restores.add(J.runRegionFuture(location.getWorld(), location.getBlockX() >> 4, location.getBlockZ() >> 4,
|
||||
() -> finish(drops, null, null)));
|
||||
}
|
||||
return CompletableFuture.allOf(restores.toArray(CompletableFuture<?>[]::new));
|
||||
}
|
||||
|
||||
void close() {
|
||||
synchronized (pending) {
|
||||
closed = true;
|
||||
}
|
||||
try {
|
||||
flushAsync().get(5L, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while preserving pending Iris block loot.", interrupted);
|
||||
} catch (ExecutionException | TimeoutException failure) {
|
||||
throw new IllegalStateException("Unable to preserve pending Iris block loot before world shutdown.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
static int toMantleY(int worldY, int minHeight) {
|
||||
@@ -136,32 +190,137 @@ final class WorldBlockDropRouter {
|
||||
return mantleY + minHeight;
|
||||
}
|
||||
|
||||
private List<IrisBlockDrops> filterDrops(KList<IrisBlockDrops> drops, BlockBreakEvent e, IrisData data) {
|
||||
return new KList<>(drops.stream().filter(d -> d.shouldDropFor(e.getBlock().getBlockData(), data)).toList());
|
||||
private boolean isManagedWorld(World world) {
|
||||
return world.equals(BukkitWorldBinding.world(manager.getTarget().getWorld()));
|
||||
}
|
||||
|
||||
private List<IrisBlockDrops> resolveDropProviders(BlockBreakEvent event) {
|
||||
if (!mayHaveBlockDropRules(BukkitBlockState.of(event.getBlock().getBlockData()).materialKey())) {
|
||||
return List.of();
|
||||
private CompletableFuture<BiomeEnvironment> environment(Location location) {
|
||||
Engine engine = manager.getEngine();
|
||||
int relativeY = toMantleY(location.getBlockY(), engine.getWorld().minHeight());
|
||||
if (engine instanceof IrisEngine iris) {
|
||||
return iris.getBiomeEnvironmentAsync(location.getBlockX(), relativeY, location.getBlockZ());
|
||||
}
|
||||
IrisBiome biome = EngineBukkitOps.getBiome(manager.getEngine(), event.getBlock().getLocation());
|
||||
List<IrisBlockDrops> providers = filterDrops(biome.getBlockDrops(), event, manager.getData());
|
||||
try {
|
||||
return CompletableFuture.completedFuture(engine.getBiomeEnvironment(location.getBlockX(), relativeY, location.getBlockZ()));
|
||||
} catch (RuntimeException failure) {
|
||||
return CompletableFuture.failedFuture(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean defer(DeferredDrops drops) {
|
||||
pending.add(drops);
|
||||
if (J.runAt(drops.location, () -> drops.environment.orTimeout(5L, TimeUnit.SECONDS).whenComplete((environment, failure) -> {
|
||||
if (drops.finished.get()) {
|
||||
return;
|
||||
}
|
||||
J.runRegionFuture(drops.location.getWorld(), drops.location.getBlockX() >> 4, drops.location.getBlockZ() >> 4,
|
||||
() -> finish(drops, environment, failure)).whenComplete((ignored, schedulingFailure) -> {
|
||||
if (schedulingFailure != null) {
|
||||
IrisLogging.reportError("Unable to finalize pending Iris block loot on its owning thread.", schedulingFailure);
|
||||
}
|
||||
});
|
||||
}), 1)) {
|
||||
return true;
|
||||
}
|
||||
pending.remove(drops);
|
||||
IrisLogging.reportError("Unable to schedule Iris block loot. Preserving normal block loot.",
|
||||
new IllegalStateException("The owning region did not accept the block-drop task."));
|
||||
return false;
|
||||
}
|
||||
|
||||
private void schedule(Location location, Runnable task) {
|
||||
if (!J.runAt(location, task, 1) && !J.isFolia()) {
|
||||
J.s(task, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void finish(DeferredDrops drops, BiomeEnvironment environment, Throwable failure) {
|
||||
if (!drops.finished.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
pending.remove(drops);
|
||||
if (!drops.allowed.getAsBoolean()) {
|
||||
return;
|
||||
}
|
||||
KList<ItemStack> custom = new KList<>();
|
||||
boolean replaceVanilla = false;
|
||||
if (failure == null && environment != null) {
|
||||
try {
|
||||
DropResult result = EngineLifecycleTasks.call(manager.getEngine(), "bukkit_world_manager_block_drop_roll",
|
||||
() -> rollDrops(drops.state, environment), null);
|
||||
if (result != null) {
|
||||
custom = result.items();
|
||||
replaceVanilla = result.replaceVanilla();
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
failure = error;
|
||||
}
|
||||
}
|
||||
if (failure != null) {
|
||||
Throwable cause = failure instanceof CompletionException && failure.getCause() != null ? failure.getCause() : failure;
|
||||
String context = "Unable to resolve Iris block drops at " + drops.location + ". Preserving normal block loot.";
|
||||
if (IrisLogging.warnOnce("block-drops:" + drops.location.getWorld().getUID() + ":" + cause.getMessage(), context)) {
|
||||
IrisLogging.reportError(context, cause);
|
||||
}
|
||||
}
|
||||
if (!replaceVanilla) {
|
||||
for (NativeDrop original : drops.originals) {
|
||||
routeDrops(List.of(original.stack()), drops.router, stack -> original.location().getWorld().dropItemNaturally(original.location(), stack));
|
||||
}
|
||||
}
|
||||
Location location = drops.location.clone().add(.5, .5, .5);
|
||||
routeDrops(custom, drops.router, stack -> location.getWorld().dropItemNaturally(location, stack));
|
||||
}
|
||||
|
||||
private DropResult rollDrops(BlockData state, BiomeEnvironment environment) {
|
||||
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(environment)) {
|
||||
KList<ItemStack> items = new KList<>();
|
||||
boolean replaceVanilla = false;
|
||||
for (IrisBlockDrops provider : resolveDropProviders(state, environment)) {
|
||||
provider.fillDrops(false, items);
|
||||
replaceVanilla |= provider.isReplaceVanillaDrops();
|
||||
}
|
||||
return new DropResult(items, replaceVanilla);
|
||||
}
|
||||
}
|
||||
|
||||
private List<IrisBlockDrops> resolveDropProviders(BlockData state, BiomeEnvironment environment) {
|
||||
List<IrisBlockDrops> providers = new ArrayList<>();
|
||||
addMatching(providers, environment.biome().getBlockDrops(), state, environment.data());
|
||||
if (providers.stream().noneMatch(IrisBlockDrops::isSkipParents)) {
|
||||
IrisRegion region = EngineBukkitOps.getRegion(manager.getEngine(), event.getBlock().getLocation());
|
||||
providers.addAll(filterDrops(region.getBlockDrops(), event, manager.getData()));
|
||||
providers.addAll(filterDrops(manager.getEngine().getDimension().getBlockDrops(), event, manager.getData()));
|
||||
addMatching(providers, environment.region().getBlockDrops(), state, environment.data());
|
||||
addMatching(providers, environment.dimension().getBlockDrops(), state, environment.data());
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
private boolean mayHaveBlockDropRules(String material) {
|
||||
private void addMatching(List<IrisBlockDrops> providers, KList<IrisBlockDrops> candidates, BlockData state, IrisData data) {
|
||||
for (IrisBlockDrops candidate : candidates) {
|
||||
if (candidate.shouldDropFor(state, data)) {
|
||||
providers.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean mayHaveBlockDropRules(BlockData state) {
|
||||
String material = BukkitBlockState.of(state).materialKey();
|
||||
if (manager.getData().hasBlockDropRules(material)) {
|
||||
return true;
|
||||
}
|
||||
if (manager.getEngine() instanceof IrisEngine engine) {
|
||||
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||
return router != null && router.biomes().mayHaveBlockDropRules(material);
|
||||
return engine.getGenerationHistoryRuntimeRouter().map(router -> router.biomes().mayHaveBlockDropRules(material)).orElse(false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private record DropResult(KList<ItemStack> items, boolean replaceVanilla) {
|
||||
}
|
||||
|
||||
private record NativeDrop(Location location, ItemStack stack) {
|
||||
}
|
||||
|
||||
private record DeferredDrops(Location location, BlockData state, BlockDropRouter router, BooleanSupplier allowed,
|
||||
List<NativeDrop> originals, CompletableFuture<BiomeEnvironment> environment,
|
||||
AtomicBoolean finished) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,14 +44,15 @@ import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.io.AtomicFileIO;
|
||||
import art.arcane.iris.platform.bootstrap.ServerProperties;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import lombok.NonNull;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -99,11 +100,17 @@ public class ServerConfigurator {
|
||||
}
|
||||
IrisSettings.IrisSettingsAutoconfiguration s = IrisSettings.get().getAutoConfiguration();
|
||||
if (s.isConfigureSpigotTimeoutTime()) {
|
||||
J.attempt(ServerConfigurator::increaseKeepAliveSpigot);
|
||||
Throwable failure = J.attemptCatch(ServerConfigurator::increaseKeepAliveSpigot);
|
||||
if (failure != null) {
|
||||
IrisLogging.reportError("Unable to persist the Spigot timeout setting.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
if (s.isConfigurePaperWatchdogDelay()) {
|
||||
J.attempt(ServerConfigurator::increasePaperWatchdog);
|
||||
if (s.isConfigurePaperWatchdogDelay() && BukkitPlatform.isPaperServer()) {
|
||||
Throwable failure = J.attemptCatch(ServerConfigurator::increasePaperWatchdog);
|
||||
if (failure != null) {
|
||||
IrisLogging.reportError("Unable to persist the Paper watchdog setting.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
if (DefaultPackBootstrapProvisioner.wasProvisionedThisStartup()) {
|
||||
@@ -230,35 +237,61 @@ public class ServerConfigurator {
|
||||
}
|
||||
}
|
||||
|
||||
private static void increaseKeepAliveSpigot() throws IOException, InvalidConfigurationException {
|
||||
File spigotConfig = new File("spigot.yml");
|
||||
FileConfiguration f = new YamlConfiguration();
|
||||
f.load(spigotConfig);
|
||||
long tt = f.getLong("settings.timeout-time");
|
||||
private static void increaseKeepAliveSpigot() throws IOException, ReflectiveOperationException {
|
||||
increaseKeepAliveSpigot(serverConfigurationOption("spigot-settings"), Bukkit.spigot().getConfig());
|
||||
}
|
||||
|
||||
long spigotTimeout = TimeUnit.MINUTES.toSeconds(5);
|
||||
static void increaseKeepAliveSpigot(Path file, YamlConfiguration configuration) throws IOException {
|
||||
long previous = configuration.getLong("settings.timeout-time");
|
||||
long minimum = TimeUnit.MINUTES.toSeconds(5);
|
||||
if (previous >= minimum) {
|
||||
return;
|
||||
}
|
||||
persistTimeout(file, configuration, "settings.timeout-time", minimum);
|
||||
IrisLogging.warn("Updated " + file + " settings.timeout-time: " + previous + " -> " + minimum
|
||||
+ " seconds. Takes effect after a server restart.");
|
||||
IrisLogging.warn("Disable autoConfiguration.configureSpigotTimeoutTime in iris.json to manage this setting yourself.");
|
||||
}
|
||||
|
||||
if (tt < spigotTimeout) {
|
||||
IrisLogging.warn("Updating spigot.yml timeout-time: " + tt + " -> " + spigotTimeout + " (5 minutes)");
|
||||
IrisLogging.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
|
||||
f.set("settings.timeout-time", spigotTimeout);
|
||||
f.save(spigotConfig);
|
||||
private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException, ReflectiveOperationException {
|
||||
increasePaperWatchdog(serverConfigurationOption("paper-settings-directory").resolve("paper-global.yml"));
|
||||
}
|
||||
|
||||
static void increasePaperWatchdog(Path file) throws IOException, InvalidConfigurationException {
|
||||
YamlConfiguration configuration = new YamlConfiguration();
|
||||
configuration.load(file.toFile());
|
||||
long previous = configuration.getLong("watchdog.early-warning-delay");
|
||||
long minimum = TimeUnit.MINUTES.toMillis(3);
|
||||
if (previous >= minimum) {
|
||||
return;
|
||||
}
|
||||
persistTimeout(file, configuration, "watchdog.early-warning-delay", minimum);
|
||||
IrisLogging.warn("Updated " + file + " watchdog.early-warning-delay: " + previous + " -> " + minimum
|
||||
+ " milliseconds. Takes effect after a server restart.");
|
||||
IrisLogging.warn("Disable autoConfiguration.configurePaperWatchdogDelay in iris.json to manage this setting yourself.");
|
||||
}
|
||||
|
||||
private static void persistTimeout(Path file, YamlConfiguration configuration, String key, long minimum) throws IOException {
|
||||
Path target = file.toRealPath();
|
||||
Object previous = configuration.isSet(key) ? configuration.get(key) : null;
|
||||
configuration.set(key, minimum);
|
||||
try {
|
||||
AtomicFileIO.writeString(target, configuration.saveToString());
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
configuration.set(key, previous);
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException {
|
||||
File spigotConfig = new File("config/paper-global.yml");
|
||||
FileConfiguration f = new YamlConfiguration();
|
||||
f.load(spigotConfig);
|
||||
long tt = f.getLong("watchdog.early-warning-delay");
|
||||
|
||||
long watchdog = TimeUnit.MINUTES.toMillis(3);
|
||||
if (tt < watchdog) {
|
||||
IrisLogging.warn("Updating paper.yml watchdog early-warning-delay: " + tt + " -> " + watchdog + " (3 minutes)");
|
||||
IrisLogging.warn("You can disable this change (autoconfigureServer) in Iris settings, then change back the value.");
|
||||
f.set("watchdog.early-warning-delay", watchdog);
|
||||
f.save(spigotConfig);
|
||||
static Path serverConfigurationOption(String name) throws ReflectiveOperationException, IOException {
|
||||
Server server = Bukkit.getServer();
|
||||
Object console = server.getClass().getMethod("getServer").invoke(server);
|
||||
Object options = console.getClass().getField("options").get(console);
|
||||
Object value = options.getClass().getMethod("valueOf", String.class).invoke(options, name);
|
||||
if (!(value instanceof File file)) {
|
||||
throw new IOException("Server option " + name + " did not provide a configuration path.");
|
||||
}
|
||||
return file.toPath().toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
public static KList<File> getDatapacksFolder() {
|
||||
|
||||
@@ -42,6 +42,7 @@ import art.arcane.iris.studio.StudioSVC;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.IrisEngine;
|
||||
import art.arcane.iris.generation.runtime.IrisWorldManager;
|
||||
import art.arcane.volmlib.util.cache.AtomicCache;
|
||||
import art.arcane.iris.generation.chunk.TerrainChunk;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
@@ -928,29 +929,36 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
boolean alreadyClosing = closing;
|
||||
closing = true;
|
||||
cancelWatcherHotload();
|
||||
AtomicBoolean dispatchAccepted = new AtomicBoolean();
|
||||
CompletableFuture<Void> operation;
|
||||
try {
|
||||
operation = withExclusiveControlFuture(() -> {
|
||||
Looper activeHotloader = hotloader;
|
||||
hotloader = null;
|
||||
if (isStudio() && activeHotloader != null) {
|
||||
activeHotloader.interrupt();
|
||||
try {
|
||||
activeHotloader.join(1000L);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
IrisLogging.reportError(e);
|
||||
CompletableFuture<Void> preservedDrops = engine != null && engine.getWorldManager() instanceof IrisWorldManager manager
|
||||
? manager.flushPendingBlockDropsAsync() : CompletableFuture.completedFuture(null);
|
||||
operation = preservedDrops.thenCompose(ignored -> {
|
||||
CompletableFuture<Void> dispatched = withExclusiveControlFuture(() -> {
|
||||
Looper activeHotloader = hotloader;
|
||||
hotloader = null;
|
||||
if (isStudio() && activeHotloader != null) {
|
||||
activeHotloader.interrupt();
|
||||
try {
|
||||
activeHotloader.join(1000L);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Engine currentEngine = engine;
|
||||
if (currentEngine != null && !currentEngine.isClosed()) {
|
||||
currentEngine.close();
|
||||
}
|
||||
folder.clear();
|
||||
populators.clear();
|
||||
closeStartupTarget();
|
||||
startupReady.completeExceptionally(new IllegalStateException("Iris generator closed during startup."));
|
||||
Engine currentEngine = engine;
|
||||
if (currentEngine != null && !currentEngine.isClosed()) {
|
||||
currentEngine.close();
|
||||
}
|
||||
folder.clear();
|
||||
populators.clear();
|
||||
closeStartupTarget();
|
||||
startupReady.completeExceptionally(new IllegalStateException("Iris generator closed during startup."));
|
||||
});
|
||||
dispatchAccepted.set(true);
|
||||
return dispatched;
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
if (!alreadyClosing) {
|
||||
@@ -969,9 +977,12 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
if (throwable == null) {
|
||||
future.complete(null);
|
||||
} else {
|
||||
// The close body already ran and tore the engine down; unlike the pre-dispatch
|
||||
// failure above, resetting the gate here would advertise a healthy generator
|
||||
// over a CLOSED or FAILED engine. Stay latched and surface the failure.
|
||||
// An accepted close may have torn the engine down. Reset the gate only when
|
||||
// exclusive cleanup was never dispatched, or a CLOSED or FAILED engine would
|
||||
// be advertised as a healthy generator.
|
||||
if (!alreadyClosing && !dispatchAccepted.get()) {
|
||||
closing = false;
|
||||
}
|
||||
closeFuture.compareAndSet(future, null);
|
||||
future.completeExceptionally(throwable);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package art.arcane.iris.world.tree;
|
||||
|
||||
@FunctionalInterface
|
||||
import java.util.Collection;
|
||||
|
||||
public interface BlockDropRouter {
|
||||
Collection<?> captureNativeDrops();
|
||||
|
||||
boolean routeDrop(Object drop);
|
||||
}
|
||||
|
||||
+132
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-7
@@ -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
|
||||
);
|
||||
|
||||
|
||||
+239
-169
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user