Merged container loot fixes

- Container loot rolls retain retry state and restore contents after failed writes
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-13 16:40:04 -04:00
commit 57882a3f1c
14 files changed
+875 -255

No files matched your search

@@ -175,6 +175,7 @@ public class WorldChunkMaintenanceRoutingTest {
public void failedMaterializationRetainsRetryAndReportsTheFailure() throws Exception {
try (Fixture fixture = new Fixture()) {
IllegalStateException failure = new IllegalStateException("Provider placement failed");
fixture.logging.when(() -> IrisLogging.errorOnce(anyString(), anyString())).thenReturn(true, false);
fixture.updates.when(() -> EngineBukkitOps.updateChunk(fixture.engine, fixture.chunk, fixture.savedMantle)).thenThrow(failure);
fixture.schedule();
fixture.prepare();
@@ -182,6 +183,10 @@ public class WorldChunkMaintenanceRoutingTest {
fixture.apply();
fixture.awaitRelease();
fixture.schedule();
fixture.prepare();
fixture.apply();
fixture.awaitRelease();
fixture.logging.verify(() -> IrisLogging.reportError(anyString(), eq(failure)));
fixture.schedule();
assertEquals(1, fixture.async.size());
@@ -0,0 +1,54 @@
package art.arcane.iris.platform.bukkit;
import art.arcane.volmlib.util.data.BSupport;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class BukkitStorageClassificationTest {
@Test
public void copperChestsAndExistingLootContainersReceiveStorageUpdates() {
BlockData air = blockData(Material.AIR);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
bukkit.when(() -> Bukkit.createBlockData(Material.AIR)).thenReturn(air);
BSupport<Object> support = new BSupport<Object>() {
};
for (Material material : new Material[]{Material.COPPER_CHEST, Material.EXPOSED_COPPER_CHEST,
Material.WEATHERED_COPPER_CHEST, Material.OXIDIZED_COPPER_CHEST,
Material.WAXED_COPPER_CHEST, Material.WAXED_EXPOSED_COPPER_CHEST,
Material.WAXED_WEATHERED_COPPER_CHEST, Material.WAXED_OXIDIZED_COPPER_CHEST,
Material.CHEST, Material.TRAPPED_CHEST, Material.BARREL, Material.SHULKER_BOX,
Material.BLUE_SHULKER_BOX, Material.DISPENSER, Material.DROPPER, Material.HOPPER}) {
BlockData state = blockData(material);
assertTrue(material.name(), support.isStorage(state));
assertTrue(material.name(), support.isStorageChest(state));
assertTrue(material.name(), support.isUpdatable(state));
}
for (Material material : new Material[]{Material.FURNACE, Material.BLAST_FURNACE, Material.SMOKER}) {
BlockData state = blockData(material);
assertTrue(material.name(), support.isStorage(state));
assertFalse(material.name(), support.isStorageChest(state));
}
for (Material material : new Material[]{Material.STONE, Material.AIR, Material.ENDER_CHEST,
Material.COPPER_BLOCK, Material.COPPER_CHESTPLATE}) {
BlockData state = blockData(material);
assertFalse(material.name(), support.isStorage(state));
assertFalse(material.name(), support.isStorageChest(state));
}
}
}
private static BlockData blockData(Material material) {
BlockData state = mock(BlockData.class);
when(state.getMaterial()).thenReturn(material);
return state;
}
}
@@ -1,20 +1,66 @@
package art.arcane.iris.platform.generation;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Chest;
import org.bukkit.loot.LootTable;
import org.junit.Test;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.testsupport.BukkitTestServer;
import art.arcane.iris.world.event.IrisLootEvent;
import art.arcane.iris.world.loot.InventorySlotType;
import art.arcane.iris.world.loot.IrisLootTable;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.function.Consumer4;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterUpdate;
import art.arcane.volmlib.util.matter.slices.UpdateMatter;
import io.papermc.paper.registry.RegistryAccess;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Barrel;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.DoubleChest;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Chest;
import org.bukkit.event.world.LootGenerateEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.loot.LootTable;
import org.bukkit.plugin.PluginManager;
import org.junit.Test;
import org.mockito.MockedStatic;
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.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class EngineBukkitOpsLootTest {
@@ -46,117 +92,314 @@ public class EngineBukkitOpsLootTest {
}
@Test
public void foliaLootMutationUsesOwningRegionOnly() {
World world = mock(World.class);
AtomicInteger primaryChecks = new AtomicInteger();
AtomicInteger regionSchedules = new AtomicInteger();
AtomicInteger syncSchedules = new AtomicInteger();
AtomicBoolean mutated = new AtomicBoolean();
Runnable mutation = () -> mutated.set(true);
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 12, -3, mutation);
public void failedInsertionRestoresExistingLootBeforeRetry() {
Inventory inventory = mock(Inventory.class);
AtomicReference<ItemStack[]> contents = new AtomicReference<>(new ItemStack[]{item(2), null});
AtomicBoolean reject = new AtomicBoolean(true);
when(inventory.getSize()).thenReturn(2);
when(inventory.getContents()).thenAnswer(invocation -> contents.get());
doAnswer(invocation -> {
contents.set(invocation.getArgument(0));
return null;
}).when(inventory).setContents(any(ItemStack[].class));
doAnswer(invocation -> {
ItemStack incoming = invocation.getArgument(0);
contents.get()[0].setAmount(contents.get()[0].getAmount() + incoming.getAmount());
if (reject.getAndSet(false)) {
throw new IllegalStateException("Insertion failed");
}
return new HashMap<Integer, ItemStack>();
}).when(inventory).addItem(any(ItemStack.class));
ItemStack reward = item(7);
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
task,
true,
() -> {
primaryChecks.incrementAndGet();
return true;
},
regionTask -> {
regionSchedules.incrementAndGet();
assertSame(world, regionTask.world());
assertEquals(12, regionTask.chunkX());
assertEquals(-3, regionTask.chunkZ());
assertSame(mutation, regionTask.mutation());
regionTask.mutation().run();
return true;
},
runnable -> syncSchedules.incrementAndGet()
);
assertThrows(IllegalStateException.class,
() -> EngineBukkitOps.populateInventory(null, inventory, List.of(reward), new RNG(17)));
assertTrue(scheduled);
assertTrue(mutated.get());
assertEquals(1, regionSchedules.get());
assertEquals(0, primaryChecks.get());
assertEquals(0, syncSchedules.get());
assertEquals(2, contents.get()[0].getAmount());
EngineBukkitOps.populateInventory(null, inventory, List.of(reward), new RNG(17));
int total = 0;
for (ItemStack item : contents.get()) {
total += item == null ? 0 : item.getAmount();
}
assertEquals(9, total);
assertEquals(7, reward.getAmount());
}
@Test
public void failedFoliaRegionScheduleDoesNotMutateInventoryElsewhere() {
World world = mock(World.class);
AtomicInteger primaryChecks = new AtomicInteger();
AtomicInteger syncSchedules = new AtomicInteger();
AtomicBoolean mutated = new AtomicBoolean();
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 1, 2, () -> mutated.set(true));
public void failedNativeInventoryAccessRestoresBothChestSnapshots() {
Inventory inventory = mock(Inventory.class);
DoubleChest holder = mock(DoubleChest.class);
org.bukkit.block.Chest left = mock(org.bukkit.block.Chest.class);
org.bukkit.block.Chest right = mock(org.bukkit.block.Chest.class);
Block leftBlock = mock(Block.class);
Block rightBlock = mock(Block.class);
BlockState leftSnapshot = mock(BlockState.class);
BlockState rightSnapshot = mock(BlockState.class);
when(inventory.getHolder()).thenReturn(holder);
when(holder.getLeftSide()).thenReturn(left);
when(holder.getRightSide()).thenReturn(right);
when(left.getBlock()).thenReturn(leftBlock);
when(right.getBlock()).thenReturn(rightBlock);
when(leftBlock.getState()).thenReturn(leftSnapshot);
when(rightBlock.getState()).thenReturn(rightSnapshot);
when(leftSnapshot.update(true, false)).thenReturn(true);
when(rightSnapshot.update(true, false)).thenReturn(true);
IllegalStateException failure = new IllegalStateException("Native loot failed");
doThrow(failure).when(inventory).addItem(any(ItemStack.class));
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
task,
true,
() -> {
primaryChecks.incrementAndGet();
return true;
},
regionTask -> false,
runnable -> syncSchedules.incrementAndGet()
);
assertSame(failure, assertThrows(IllegalStateException.class,
() -> EngineBukkitOps.populateInventory(null, inventory,
List.of(item(1)), new RNG(17))));
assertFalse(scheduled);
assertFalse(mutated.get());
assertEquals(0, primaryChecks.get());
assertEquals(0, syncSchedules.get());
verify(leftSnapshot).update(true, false);
verify(rightSnapshot).update(true, false);
}
@Test
public void nonFoliaPrimaryThreadMutatesInline() {
World world = mock(World.class);
AtomicInteger regionSchedules = new AtomicInteger();
AtomicInteger syncSchedules = new AtomicInteger();
AtomicBoolean mutated = new AtomicBoolean();
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 4, 5, () -> mutated.set(true));
public void failedBarrelFillRestoresItsBlockSnapshot() {
Inventory inventory = mock(Inventory.class);
Barrel holder = mock(Barrel.class);
Block block = mock(Block.class);
Barrel snapshot = mock(Barrel.class);
when(inventory.getHolder()).thenReturn(holder);
when(holder.getBlock()).thenReturn(block);
when(block.getState()).thenReturn(snapshot);
when(snapshot.update(true, false)).thenReturn(true);
IllegalStateException failure = new IllegalStateException("Native barrel loot failed");
when(inventory.getContents()).thenThrow(failure);
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
task,
false,
() -> true,
regionTask -> {
regionSchedules.incrementAndGet();
return true;
},
runnable -> syncSchedules.incrementAndGet()
);
assertSame(failure, assertThrows(IllegalStateException.class,
() -> EngineBukkitOps.populateInventory(null, inventory, List.of(), new RNG(17))));
assertTrue(scheduled);
assertTrue(mutated.get());
assertEquals(0, regionSchedules.get());
assertEquals(0, syncSchedules.get());
verify(snapshot).update(true, false);
}
@Test
public void nonFoliaAsyncCompletionUsesSyncScheduler() {
public void failedRollbackPreservesTheCauseAndRestoresTheOtherHalf() {
Inventory inventory = mock(Inventory.class);
DoubleChest holder = mock(DoubleChest.class);
org.bukkit.block.Chest left = mock(org.bukkit.block.Chest.class, RETURNS_DEEP_STUBS);
org.bukkit.block.Chest right = mock(org.bukkit.block.Chest.class, RETURNS_DEEP_STUBS);
BlockState leftSnapshot = mock(BlockState.class);
BlockState rightSnapshot = mock(BlockState.class);
when(inventory.getHolder()).thenReturn(holder);
when(holder.getLeftSide()).thenReturn(left);
when(holder.getRightSide()).thenReturn(right);
when(left.getBlock().getState()).thenReturn(leftSnapshot);
when(right.getBlock().getState()).thenReturn(rightSnapshot);
when(rightSnapshot.update(true, false)).thenReturn(true);
IllegalStateException failure = new IllegalStateException("Native loot failed");
IllegalStateException rollbackFailure = new IllegalStateException("Snapshot restore failed");
when(leftSnapshot.update(true, false)).thenThrow(rollbackFailure);
doThrow(failure).when(inventory).addItem(any(ItemStack.class));
assertSame(failure, assertThrows(IllegalStateException.class,
() -> EngineBukkitOps.populateInventory(null, inventory, List.of(item(1)), new RNG(17))));
assertEquals(1, failure.getSuppressed().length);
assertSame(rollbackFailure, failure.getSuppressed()[0]);
verify(rightSnapshot).update(true, false);
}
@Test
public void cancelledLootLeavesTheInventoryUntouched() {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
Inventory inventory = mock(Inventory.class);
World world = mock(World.class);
AtomicInteger regionSchedules = new AtomicInteger();
AtomicInteger syncSchedules = new AtomicInteger();
AtomicBoolean mutated = new AtomicBoolean();
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 6, 7, () -> mutated.set(true));
try (MockedStatic<J> scheduling = mockStatic(J.class);
MockedStatic<IrisLootEvent> events = mockStatic(IrisLootEvent.class)) {
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 2, -2)).thenReturn(true);
events.when(() -> IrisLootEvent.callLootEvent(any(), eq(inventory), eq(world), eq(33), eq(64), eq(-17)))
.thenReturn(true);
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
task,
false,
() -> false,
regionTask -> {
regionSchedules.incrementAndGet();
return true;
},
runnable -> {
syncSchedules.incrementAndGet();
runnable.run();
}
);
EngineBukkitOps.addItems(engine, false, inventory, new KList<>(), InventorySlotType.STORAGE, world, 33, 64, -17);
assertTrue(scheduled);
assertTrue(mutated.get());
assertEquals(0, regionSchedules.get());
assertEquals(1, syncSchedules.get());
verifyNoInteractions(inventory);
}
}
@Test
public void offRegionLootDoesNotRollOrTouchInventory() {
Engine engine = mock(Engine.class);
Inventory inventory = mock(Inventory.class);
World world = mock(World.class);
IrisLootTable table = mock(IrisLootTable.class);
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
assertThrows(IllegalStateException.class, () -> EngineBukkitOps.addItems(engine, false, inventory,
new KList<>(table), InventorySlotType.STORAGE, world, 33, 64, -17));
scheduling.verify(() -> J.isOwnedByCurrentRegion(world, 2, -2));
verifyNoInteractions(table, inventory, engine);
}
}
@Test
public void owningRegionLootCompletesInlineWithoutGlobalDispatch() {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
Inventory inventory = mock(Inventory.class);
World world = mock(World.class);
when(inventory.getContents()).thenReturn(new ItemStack[0]);
try (MockedStatic<J> scheduling = mockStatic(J.class);
MockedStatic<IrisLootEvent> events = mockStatic(IrisLootEvent.class)) {
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 2, -2)).thenReturn(true);
EngineBukkitOps.addItems(engine, false, inventory, new KList<>(), InventorySlotType.STORAGE, world, 33, 64, -17);
verify(inventory).setContents(any(ItemStack[].class));
scheduling.verify(() -> J.isOwnedByCurrentRegion(world, 2, -2));
scheduling.verifyNoMoreInteractions();
}
}
@Test
public void foliaLootEventRunsOnTheOwningRegionWithoutGlobalDispatch() {
World world = mock(World.class);
Inventory inventory = mock(Inventory.class);
PluginManager manager = mock(PluginManager.class);
Thread owner = Thread.currentThread();
AtomicInteger eventCalls = new AtomicInteger();
try (MockedStatic<J> scheduling = mockStatic(J.class);
MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 2, -2)).thenReturn(true);
bukkit.when(Bukkit::getPluginManager).thenReturn(manager);
doAnswer(invocation -> {
assertSame(owner, Thread.currentThread());
eventCalls.incrementAndGet();
return null;
}).when(manager).callEvent(any(LootGenerateEvent.class));
assertFalse(IrisLootEvent.callLootEvent(new KList<>(), inventory, world, 33, 64, -17));
assertEquals(1, eventCalls.get());
scheduling.verify(() -> J.isOwnedByCurrentRegion(world, 2, -2));
scheduling.verifyNoMoreInteractions();
}
}
@Test
public void lootEventRetainsListenerChangesAndCancellation() {
World world = mock(World.class);
Inventory inventory = mock(Inventory.class);
PluginManager manager = mock(PluginManager.class);
ItemStack original = item(2);
ItemStack replacement = item(3);
KList<ItemStack> items = new KList<>(original);
try (MockedStatic<J> scheduling = mockStatic(J.class);
MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 2, -2)).thenReturn(true);
bukkit.when(Bukkit::getPluginManager).thenReturn(manager);
doAnswer(invocation -> {
LootGenerateEvent event = invocation.getArgument(0);
event.setLoot(List.of(replacement));
event.setCancelled(true);
return null;
}).when(manager).callEvent(any(LootGenerateEvent.class));
assertTrue(IrisLootEvent.callLootEvent(items, inventory, world, 33, 64, -17));
assertEquals(List.of(replacement), items);
}
}
@Test
@SuppressWarnings("unchecked")
public void failedLootRollKeepsPendingMarkerUntilSuccessfulRetry() {
BukkitTestServer.install();
AtomicBoolean pending = new AtomicBoolean(true);
AtomicInteger attempts = new AtomicInteger();
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
World world = mock(World.class);
when(world.getMaxHeight()).thenReturn(256);
Chunk chunk = mock(Chunk.class);
when(chunk.getWorld()).thenReturn(world);
Block block = mock(Block.class);
BlockData state = mock(BlockData.class);
org.bukkit.block.Chest chest = mock(org.bukkit.block.Chest.class);
Inventory inventory = mock(Inventory.class);
when(chunk.getBlock(1, 64, 1)).thenReturn(block);
when(block.getBlockData()).thenReturn(state);
when(block.getX()).thenReturn(1);
when(block.getZ()).thenReturn(1);
when(block.getState()).thenReturn(chest);
when(chest.getInventory()).thenReturn(inventory);
when(inventory.getContents()).thenReturn(new ItemStack[0]);
MantleChunk<Matter> mantle = spy(new MantleChunk<>(16, 0, 0, mock(MantleDataAdapter.class), MantleHooks.NONE));
doAnswer(invocation -> {
if (pending.get()) {
Consumer4<Integer, Integer, Integer, MatterUpdate> visitor = invocation.getArgument(1);
visitor.accept(1, 64, 1, UpdateMatter.ON);
}
return null;
}).when(mantle).iterate(eq(MatterUpdate.class), any());
doAnswer(invocation -> {
pending.set(false);
return null;
}).when(mantle).deleteSlices(MatterUpdate.class);
IrisLootTable table = mock(IrisLootTable.class);
IllegalStateException failure = new IllegalStateException("Loot provider unavailable");
when(table.getLoot(false, 0L, InventorySlotType.STORAGE, world, 1, 64, 1)).thenAnswer(invocation -> {
if (attempts.getAndIncrement() < 2) {
throw failure;
}
return new KList<ItemStack>();
});
KList<IrisLootTable> tables = new KList<>(table);
PluginManager manager = mock(PluginManager.class);
try (MockedStatic<RegistryAccess> registries = mockStatic(RegistryAccess.class)) {
registries.when(RegistryAccess::registryAccess).thenReturn(BukkitTestServer.registryAccess());
try (MockedStatic<EngineBukkitOps> operations = mockStatic(EngineBukkitOps.class);
MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class);
MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<IrisLootEvent> events = mockStatic(IrisLootEvent.class);
MockedStatic<J> scheduling = mockStatic(J.class);
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
bukkit.when(Bukkit::getPluginManager).thenReturn(manager);
logging.when(() -> IrisLogging.errorOnce(anyString(), anyString())).thenReturn(true, false);
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 0, 0)).thenReturn(true);
resolution.when(() -> BukkitBlockResolution.isStorage(state)).thenReturn(true);
resolution.when(() -> BukkitBlockResolution.isStorageChest(state)).thenReturn(true);
operations.when(() -> EngineBukkitOps.isCanonicalContainer(block)).thenReturn(true);
operations.when(() -> EngineBukkitOps.getLootTables(eq(engine), any(RNG.class), eq(block), eq(mantle), eq(false))).thenReturn(tables);
operations.when(() -> EngineBukkitOps.addItems(engine, false, inventory, tables, InventorySlotType.STORAGE, world, 1, 64, 1)).thenCallRealMethod();
operations.when(() -> EngineBukkitOps.populateInventory(eq(engine), eq(inventory), any(), any())).thenCallRealMethod();
operations.when(() -> EngineBukkitOps.update(engine, 1, 64, 1, chunk, mantle)).thenCallRealMethod();
operations.when(() -> EngineBukkitOps.materializeUpdates(engine, chunk, mantle)).thenCallRealMethod();
operations.when(() -> EngineBukkitOps.materializeUpdates(eq(engine), eq(chunk), eq(mantle), any(EngineBukkitOps.UpdateDispatcher.class))).thenCallRealMethod();
operations.when(() -> EngineBukkitOps.runMaterializationPasses(eq(mantle), any(Runnable.class), any(Runnable.class), any(Runnable.class))).thenCallRealMethod();
Runnable pass = () -> EngineBukkitOps.runMaterializationPasses(mantle, () -> {}, () -> {},
() -> EngineBukkitOps.materializeUpdates(engine, chunk, mantle));
assertSame(failure, assertThrows(IllegalStateException.class, pass::run));
assertTrue(pending.get());
assertFalse(mantle.isFlagged(MantleFlag.UPDATE));
assertFalse(mantle.isFlagged(MantleFlag.ETCHED));
assertSame(failure, assertThrows(IllegalStateException.class, pass::run));
assertTrue(pending.get());
logging.verify(() -> IrisLogging.reportError(failure));
pass.run();
pass.run();
assertFalse(pending.get());
assertTrue(mantle.isFlagged(MantleFlag.UPDATE));
assertTrue(mantle.isFlagged(MantleFlag.ETCHED));
assertEquals(3, attempts.get());
}
}
}
private ItemStack item(int amount) {
ItemStack item = mock(ItemStack.class);
AtomicInteger count = new AtomicInteger(amount);
when(item.getType()).thenReturn(Material.DIAMOND);
when(item.getAmount()).thenAnswer(invocation -> count.get());
when(item.clone()).thenAnswer(invocation -> item(count.get()));
doAnswer(invocation -> {
count.set(invocation.getArgument(0));
return null;
}).when(item).setAmount(anyInt());
return item;
}
private Block chestBlock(int x, int z, Chest.Type type, BlockFace facing) {
@@ -0,0 +1,76 @@
package art.arcane.iris.world.loot;
import art.arcane.iris.spi.IrisLogging;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.inventory.ItemStack;
import org.bukkit.loot.LootTable;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.List;
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.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisVanillaLootTableTest {
@Test
public void registryReplacementRemovalAndRestorationApplyToTheNextContainer() {
NamespacedKey key = new NamespacedKey("test", "reloadable_loot");
World world = mock(World.class);
LootTable before = mock(LootTable.class);
LootTable after = mock(LootTable.class);
ItemStack previousItem = mock(ItemStack.class);
ItemStack currentItem = mock(ItemStack.class);
when(before.populateLoot(any(), any())).thenReturn(List.of(previousItem));
when(after.populateLoot(any(), any())).thenReturn(List.of(currentItem));
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
IrisVanillaLootTable table = new IrisVanillaLootTable(key.toString());
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(before);
assertSame(previousItem, table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 0, 64, 0).getFirst());
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(after);
assertSame(currentItem, table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 1, 64, 0).getFirst());
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(null);
assertTrue(table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 2, 64, 0).isEmpty());
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(after);
assertSame(currentItem, table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 3, 64, 0).getFirst());
verify(before).populateLoot(any(), any());
verify(after, times(2)).populateLoot(any(), any());
}
}
@Test
public void failedTableGenerationDoesNotRetainTheReplacedTable() {
NamespacedKey key = new NamespacedKey("test", "repaired_loot");
World world = mock(World.class);
LootTable failed = mock(LootTable.class);
LootTable repaired = mock(LootTable.class);
ItemStack item = mock(ItemStack.class);
IllegalStateException failure = new IllegalStateException("Loot generation failed");
when(failed.populateLoot(any(), any())).thenThrow(failure);
when(repaired.populateLoot(any(), any())).thenReturn(List.of(item));
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
IrisVanillaLootTable table = new IrisVanillaLootTable(key.toString());
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(failed);
assertSame(failure, assertThrows(IllegalStateException.class,
() -> table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 0, 64, 0)));
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(repaired);
assertSame(item, table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 0, 64, 0).getFirst());
}
}
}