Fixed mining and timeout settings

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

No files matched your search

@@ -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<>();
@@ -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) {
}
}
@@ -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);
@@ -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);
}
}
@@ -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;
}
@@ -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();
}
}
}