new event McMMOModifyBlockDropItemEvent and mcMMO now modifies the drop list in BlockDropItemEvent instead of spawning items

Fixes #5214
This commit is contained in:
nossr50
2025-09-21 12:19:55 -07:00
parent 8e049822a3
commit d38d74f82a
4 changed files with 556 additions and 62 deletions

View File

@@ -1,4 +1,7 @@
Version 2.2.042 Version 2.2.042
mcMMO now listens to BlockDropItemEvent at LOW priority instead of HIGHEST
Bonus drops from mcMMO now simply modify quantity in BlockDropItemEvent
Added McMMOModifyBlockDropItemEvent event, this event is called when mcMMO modifies the quantity of an ItemStack during a BlockDropItemEvent, it is modifiable and cancellable.
You can now define custom sounds to be played in sounds.yml (Thank you JeBobs, see notes) You can now define custom sounds to be played in sounds.yml (Thank you JeBobs, see notes)
Added a cap to how much Blast Mining PVP damage can do to other players Added a cap to how much Blast Mining PVP damage can do to other players

View File

@@ -0,0 +1,226 @@
package com.gmail.nossr50.events.items;
import static java.util.Objects.requireNonNull;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.event.block.BlockDropItemEvent;
import org.jetbrains.annotations.NotNull;
/**
* Called when mcMMO is modifying the amount of bonus drops to add to an Item involved in a {@link BlockDropItemEvent}.
* <p>
* This event is called before mcMMO has modified the ItemStack quantity on the {@link Item} entity.
* <p>
* This event is called once per Item entity that is involved in the {@link BlockDropItemEvent}.
* <p>
* This event is called during mcMMO logic on the {@link BlockDropItemEvent}, and can be used to
* modify the quantity that mcMMO will add to the ItemStack.
* <p>
* This event is considered cancelled if it is either cancelled directly or if bonus drops are 0 or
* less.
*/
public class McMMOModifyBlockDropItemEvent extends Event implements Cancellable {
private final @NotNull BlockDropItemEvent blockDropItemEvent;
private final int originalBonusAmountToAdd;
private int modifiedItemStackQuantity;
private final @NotNull Item itemThatHasBonusDrops;
private boolean isCancelled = false;
private final int originalItemStackQuantity;
public McMMOModifyBlockDropItemEvent(@NotNull BlockDropItemEvent blockDropItemEvent,
@NotNull Item itemThatHasBonusDrops, int bonusDropsToAdd) {
super(false);
requireNonNull(blockDropItemEvent, "blockDropItemEvent cannot be null");
requireNonNull(itemThatHasBonusDrops, "itemThatHasBonusDrops cannot be null");
if (bonusDropsToAdd <= 0) {
throw new IllegalArgumentException("cannot instantiate a new"
+ " McMMOModifyBlockDropItemEvent with a bonusDropsToAdd that is <= 0");
}
this.blockDropItemEvent = blockDropItemEvent;
this.itemThatHasBonusDrops = itemThatHasBonusDrops;
this.originalItemStackQuantity = itemThatHasBonusDrops.getItemStack().getAmount();
this.originalBonusAmountToAdd = bonusDropsToAdd;
this.modifiedItemStackQuantity = itemThatHasBonusDrops.getItemStack().getAmount()
+ bonusDropsToAdd;
}
@Override
public boolean isCancelled() {
return isCancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.isCancelled = cancel;
}
/**
* The original BlockDropItemEvent which caused this event to be fired.
* @return the original BlockDropItemEvent
*/
public @NotNull BlockDropItemEvent getBlockDropItemEvent() {
return blockDropItemEvent;
}
/**
* The original bonus mcMMO would have added before any modifications to this event from
* other plugins.
* @return the original bonus amount to add
*/
public int getOriginalBonusAmountToAdd() {
return originalBonusAmountToAdd;
}
/**
* The Item entity that is being modified by this event.
* This item returned by this call should not be modified, it is provided as a convenience.
* @return the Item entity that is having bonus drops added to it.
*/
public @NotNull Item getItem() {
return itemThatHasBonusDrops;
}
/**
* The modified ItemStack quantity that will be set on the Item entity if this event is not
* cancelled.
*
* @return the modified ItemStack quantity that will be set on the Item entity
*/
public int getModifiedItemStackQuantity() {
return modifiedItemStackQuantity;
}
/**
* The original ItemStack quantity of the Item entity before any modifications from this event.
* This is a reflection of the state of the Item when mcMMO fired this event.
* It is possible it has modified since then, so do not rely on this value to be the current.
* @return the original ItemStack quantity of the Item entity before any modifications from this event
*/
public int getOriginalItemStackQuantity() {
return originalItemStackQuantity;
}
/**
* The amount of bonus that will be added to the ItemStack quantity if this event is not
* cancelled.
* @return the amount of bonus that will be added to the ItemStack quantity
*/
public int getBonusAmountToAdd() {
return Math.max(0, modifiedItemStackQuantity - originalItemStackQuantity);
}
/**
* Set the amount of bonus that will be added to the ItemStack quantity if this event is not
* cancelled.
* @param bonus the amount of bonus that will be added to the ItemStack quantity
* @throws IllegalArgumentException if bonus is less than 0
*/
public void setBonusAmountToAdd(int bonus) {
if (bonus < 0) throw new IllegalArgumentException("bonus must be >= 0");
this.modifiedItemStackQuantity = originalItemStackQuantity + bonus;
}
/**
* Set the modified ItemStack quantity that will be set on the Item entity if this event is not
* cancelled. This CANNOT be lower than the original quantity of the ItemStack.
* @param modifiedItemStackQuantity the modified ItemStack quantity that will be set on the Item entity
* @throws IllegalArgumentException if modifiedItemStackQuantity is less than originalItemStackQuantity
*/
public void setModifiedItemStackQuantity(int modifiedItemStackQuantity) {
if (modifiedItemStackQuantity < originalItemStackQuantity) {
throw new IllegalArgumentException(
"modifiedItemStackQuantity cannot be less than the originalItemStackQuantity");
}
this.modifiedItemStackQuantity = modifiedItemStackQuantity;
}
public boolean isEffectivelyNoBonus() {
return modifiedItemStackQuantity == originalItemStackQuantity;
}
/**
* Delegate method for {@link BlockDropItemEvent}, gets the Player that is breaking the block
* involved in this event.
*
* @return The Player that is breaking the block involved in this event
*/
public @NotNull Player getPlayer() {
return blockDropItemEvent.getPlayer();
}
/**
* Delegate method for {@link BlockDropItemEvent#getBlock()}.
* Gets the Block involved in this event.
*
* @return the Block involved in this event
*/
public @NotNull Block getBlock() {
return blockDropItemEvent.getBlock();
}
/**
* Delegate method for {@link BlockDropItemEvent#getBlockState()}.
* Gets the BlockState of the block involved in this event.
*
* @return the BlockState of the block involved in this event
*/
public @NotNull BlockState getBlockState() {
return blockDropItemEvent.getBlockState();
}
private static final @NotNull HandlerList handlers = new HandlerList();
@Override
public @NotNull HandlerList getHandlers() {
return handlers;
}
public static @NotNull HandlerList getHandlerList() {
return handlers;
}
@Override
public @NotNull String toString() {
return "McMMOModifyBlockDropItemEvent{" +
"blockDropItemEvent=" + blockDropItemEvent +
", originalBonusAmountToAdd=" + originalBonusAmountToAdd +
", modifiedItemStackQuantity=" + modifiedItemStackQuantity +
", itemThatHasBonusDrops=" + itemThatHasBonusDrops +
", isCancelled=" + isCancelled +
", originalItemStackQuantity=" + originalItemStackQuantity +
'}';
}
@Override
public final boolean equals(Object o) {
if (!(o instanceof McMMOModifyBlockDropItemEvent that)) {
return false;
}
return originalBonusAmountToAdd == that.originalBonusAmountToAdd
&& modifiedItemStackQuantity == that.modifiedItemStackQuantity
&& isCancelled == that.isCancelled
&& originalItemStackQuantity == that.originalItemStackQuantity
&& blockDropItemEvent.equals(that.blockDropItemEvent)
&& itemThatHasBonusDrops.equals(
that.itemThatHasBonusDrops);
}
@Override
public int hashCode() {
int result = blockDropItemEvent.hashCode();
result = 31 * result + originalBonusAmountToAdd;
result = 31 * result + modifiedItemStackQuantity;
result = 31 * result + itemThatHasBonusDrops.hashCode();
result = 31 * result + Boolean.hashCode(isCancelled);
result = 31 * result + originalItemStackQuantity;
return result;
}
}

View File

@@ -1,5 +1,6 @@
package com.gmail.nossr50.listeners; package com.gmail.nossr50.listeners;
import static com.gmail.nossr50.util.MetadataConstants.METADATA_KEY_BONUS_DROPS;
import static com.gmail.nossr50.util.Misc.getBlockCenter; import static com.gmail.nossr50.util.Misc.getBlockCenter;
import com.gmail.nossr50.api.ItemSpawnReason; import com.gmail.nossr50.api.ItemSpawnReason;
@@ -14,6 +15,7 @@ import com.gmail.nossr50.datatypes.skills.ToolType;
import com.gmail.nossr50.events.fake.FakeBlockBreakEvent; import com.gmail.nossr50.events.fake.FakeBlockBreakEvent;
import com.gmail.nossr50.events.fake.FakeBlockDamageEvent; import com.gmail.nossr50.events.fake.FakeBlockDamageEvent;
import com.gmail.nossr50.events.fake.FakeEvent; import com.gmail.nossr50.events.fake.FakeEvent;
import com.gmail.nossr50.events.items.McMMOModifyBlockDropItemEvent;
import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.skills.alchemy.Alchemy; import com.gmail.nossr50.skills.alchemy.Alchemy;
import com.gmail.nossr50.skills.excavation.ExcavationManager; import com.gmail.nossr50.skills.excavation.ExcavationManager;
@@ -35,6 +37,8 @@ import com.gmail.nossr50.util.sounds.SoundType;
import com.gmail.nossr50.worldguard.WorldGuardManager; import com.gmail.nossr50.worldguard.WorldGuardManager;
import com.gmail.nossr50.worldguard.WorldGuardUtils; import com.gmail.nossr50.worldguard.WorldGuardUtils;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.bukkit.GameMode; import org.bukkit.GameMode;
import org.bukkit.Location; import org.bukkit.Location;
@@ -62,6 +66,7 @@ import org.bukkit.event.block.BlockPistonRetractEvent;
import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.block.EntityBlockFormEvent; import org.bukkit.event.block.EntityBlockFormEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.metadata.MetadataValue;
public class BlockListener implements Listener { public class BlockListener implements Listener {
private final mcMMO plugin; private final mcMMO plugin;
@@ -70,88 +75,96 @@ public class BlockListener implements Listener {
this.plugin = plugin; this.plugin = plugin;
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false) @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false)
public void onBlockDropItemEvent(BlockDropItemEvent event) { public void onBlockDropItemEvent(BlockDropItemEvent event) {
//Make sure we clean up metadata on these blocks //Make sure we clean up metadata on these blocks
final Block block = event.getBlock();
if (event.isCancelled()) { if (event.isCancelled()) {
if (event.getBlock().hasMetadata(MetadataConstants.METADATA_KEY_BONUS_DROPS)) { if (block.hasMetadata(METADATA_KEY_BONUS_DROPS)) {
event.getBlock().removeMetadata(MetadataConstants.METADATA_KEY_BONUS_DROPS, plugin); block.removeMetadata(METADATA_KEY_BONUS_DROPS, plugin);
} }
return; return;
} }
int tileEntityTolerance = 1; try {
int tileEntityTolerance = 1;
// beetroot hotfix, potentially other plants may need this fix // beetroot hotfix, potentially other plants may need this fix
if (event.getBlock().getType() == Material.BEETROOTS) { final Material blockType = block.getType();
tileEntityTolerance = 2; if (blockType == Material.BEETROOTS) {
} tileEntityTolerance = 2;
//Track how many "things" are being dropped
HashSet<Material> uniqueMaterials = new HashSet<>();
boolean dontRewardTE = false; //If we suspect TEs are mixed in with other things don't reward bonus drops for anything that isn't a block
int blockCount = 0;
for (Item item : event.getItems()) {
//Track unique materials
uniqueMaterials.add(item.getItemStack().getType());
//Count blocks as a second failsafe
if (item.getItemStack().getType().isBlock()) {
blockCount++;
} }
}
if (uniqueMaterials.size() > tileEntityTolerance) { //Track how many "things" are being dropped
//Too many things are dropping, assume tile entities might be duped final Set<Material> uniqueMaterials = new HashSet<>();
//Technically this would also prevent something like coal from being bonus dropped if you placed a TE above a coal ore when mining it but that's pretty edge case and this is a good solution for now boolean dontRewardTE = false; //If we suspect TEs are mixed in with other things don't reward bonus drops for anything that isn't a block
dontRewardTE = true; int blockCount = 0;
}
//If there are more than one block in the item list we can't really trust it and will back out of rewarding bonus drops final List<Item> eventItems = event.getItems();
if (blockCount <= 1) { for (Item item : eventItems) {
for (Item item : event.getItems()) { //Track unique materials
ItemStack is = new ItemStack(item.getItemStack()); uniqueMaterials.add(item.getItemStack().getType());
if (is.getAmount() <= 0) { //Count blocks as a second failsafe
continue; if (item.getItemStack().getType().isBlock()) {
blockCount++;
} }
}
//TODO: Ignore this abomination its rewritten in 2.2 if (uniqueMaterials.size() > tileEntityTolerance) {
if (!mcMMO.p.getGeneralConfig() // Too many things are dropping, assume tile entities might be duped
.getDoubleDropsEnabled(PrimarySkillType.MINING, is.getType()) // Technically this would also prevent something like coal from being bonus dropped
&& !mcMMO.p.getGeneralConfig() // if you placed a TE above a coal ore when mining it but that's pretty edge case
.getDoubleDropsEnabled(PrimarySkillType.HERBALISM, is.getType()) // and this is a good solution for now
&& !mcMMO.p.getGeneralConfig() dontRewardTE = true;
.getDoubleDropsEnabled(PrimarySkillType.WOODCUTTING, is.getType())) { }
continue;
}
//If we suspect TEs might be duped only reward block //If there are more than one block in the item list we can't really trust it
if (dontRewardTE) { // and will back out of rewarding bonus drops
if (!is.getType().isBlock()) { if (!block.getMetadata(METADATA_KEY_BONUS_DROPS).isEmpty()) {
continue; final MetadataValue bonusDropMeta = block
} .getMetadata(METADATA_KEY_BONUS_DROPS).get(0);
} if (blockCount <= 1) {
for (final Item item : eventItems) {
final ItemStack eventItemStack = item.getItemStack();
int originalAmount = eventItemStack.getAmount();
if (event.getBlock().getMetadata(MetadataConstants.METADATA_KEY_BONUS_DROPS).size() if (eventItemStack.getAmount() <= 0) {
> 0) { continue;
final BonusDropMeta bonusDropMeta = }
(BonusDropMeta) event.getBlock().getMetadata(
MetadataConstants.METADATA_KEY_BONUS_DROPS).get(0);
int bonusCount = bonusDropMeta.asInt();
final Location centeredLocation = getBlockCenter(event.getBlock());
for (int i = 0; i < bonusCount; i++) {
ItemUtils.spawnItemNaturally(event.getPlayer(), final Material itemType = eventItemStack.getType();
centeredLocation, is, ItemSpawnReason.BONUS_DROPS); if (!mcMMO.p.getGeneralConfig()
.getDoubleDropsEnabled(PrimarySkillType.MINING, itemType)
&& !mcMMO.p.getGeneralConfig()
.getDoubleDropsEnabled(PrimarySkillType.HERBALISM, itemType)
&& !mcMMO.p.getGeneralConfig()
.getDoubleDropsEnabled(PrimarySkillType.WOODCUTTING, itemType)) {
continue;
}
//If we suspect TEs might be duped only reward block
if (dontRewardTE) {
if (!itemType.isBlock()) {
continue;
}
}
int amountToAddFromBonus = bonusDropMeta.asInt();
final McMMOModifyBlockDropItemEvent modifyBlockDropItemEvent
= new McMMOModifyBlockDropItemEvent(event, item, amountToAddFromBonus);
plugin.getServer().getPluginManager().callEvent(modifyBlockDropItemEvent);
if (!modifyBlockDropItemEvent.isCancelled()
&& modifyBlockDropItemEvent.getModifiedItemStackQuantity() > originalAmount) {
eventItemStack.setAmount(modifyBlockDropItemEvent.getModifiedItemStackQuantity());
}
} }
} }
} }
} } finally {
if (block.hasMetadata(METADATA_KEY_BONUS_DROPS)) {
if (event.getBlock().hasMetadata(MetadataConstants.METADATA_KEY_BONUS_DROPS)) { block.removeMetadata(METADATA_KEY_BONUS_DROPS, plugin);
event.getBlock().removeMetadata(MetadataConstants.METADATA_KEY_BONUS_DROPS, plugin); }
} }
} }

View File

@@ -0,0 +1,252 @@
package com.gmail.nossr50.events.items;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.block.BlockDropItemEvent;
import org.bukkit.inventory.ItemStack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class McMMOModifyBlockDropItemEventTest {
private BlockDropItemEvent blockDropItemEvent;
private Item itemEntity;
private ItemStack itemStack;
private Player player;
private Block block;
private BlockState blockState;
@BeforeEach
void setUp() {
// Mocks for delegate passthroughs
player = mock(Player.class, RETURNS_DEEP_STUBS);
block = mock(Block.class, RETURNS_DEEP_STUBS);
blockState = mock(BlockState.class, RETURNS_DEEP_STUBS);
// Primary Bukkit event mock
blockDropItemEvent = mock(BlockDropItemEvent.class, RETURNS_DEEP_STUBS);
when(blockDropItemEvent.getPlayer()).thenReturn(player);
when(blockDropItemEvent.getBlock()).thenReturn(block);
when(blockDropItemEvent.getBlockState()).thenReturn(blockState);
// Item + ItemStack mock
itemStack = mock(ItemStack.class);
when(itemStack.getAmount()).thenReturn(3); // original count
itemEntity = mock(Item.class);
when(itemEntity.getItemStack()).thenReturn(itemStack);
}
private McMMOModifyBlockDropItemEvent newEvent(int bonus) {
return new McMMOModifyBlockDropItemEvent(blockDropItemEvent, itemEntity, bonus);
}
@Nested
@DisplayName("Constructor & validation")
class ConstructorValidation {
@Test
void ctorNullEventThrows() {
assertThrows(NullPointerException.class,
() -> new McMMOModifyBlockDropItemEvent(null, itemEntity, 1));
}
@Test
void ctorNullItemThrows() {
assertThrows(NullPointerException.class,
() -> new McMMOModifyBlockDropItemEvent(blockDropItemEvent, null, 1));
}
@Test
void ctorZeroBonusThrows() {
assertThrows(IllegalArgumentException.class,
() -> new McMMOModifyBlockDropItemEvent(blockDropItemEvent, itemEntity, 0));
}
@Test
void ctorNegativeBonusThrows() {
assertThrows(IllegalArgumentException.class,
() -> new McMMOModifyBlockDropItemEvent(blockDropItemEvent, itemEntity, -5));
}
@Test
void ctorSetsOriginalsAndModifiedCorrectly() {
// original amount = 3, bonus = 2
McMMOModifyBlockDropItemEvent ev = newEvent(2);
assertEquals(3, ev.getOriginalItemStackQuantity());
assertEquals(2, ev.getOriginalBonusAmountToAdd());
assertEquals(5, ev.getModifiedItemStackQuantity());
assertFalse(ev.isCancelled());
assertFalse(ev.isEffectivelyNoBonus());
}
}
@Nested
@DisplayName("Cancellable contract")
class Cancellation {
@Test
void cancelAndUncancel() {
McMMOModifyBlockDropItemEvent ev = newEvent(1);
assertFalse(ev.isCancelled());
ev.setCancelled(true);
assertTrue(ev.isCancelled());
ev.setCancelled(false);
assertFalse(ev.isCancelled());
}
}
@Nested
@DisplayName("Delta & absolute quantity semantics")
class DeltaAndAbsolute {
@Test
void getBonusAmountToAddReflectsDifferenceFromOriginal() {
// original 3, bonus 4 => modified 7
McMMOModifyBlockDropItemEvent ev = newEvent(4);
assertEquals(4, ev.getBonusAmountToAdd());
assertEquals(7, ev.getModifiedItemStackQuantity());
}
@Test
void setBonusAmountToAddUpdatesModifiedQuantity() {
McMMOModifyBlockDropItemEvent ev = newEvent(2); // original 3 -> modified 5
ev.setBonusAmountToAdd(10); // new modified should be 13
assertEquals(13, ev.getModifiedItemStackQuantity());
assertEquals(10, ev.getBonusAmountToAdd());
assertFalse(ev.isEffectivelyNoBonus());
}
@Test
void setBonusAmountToAddNegativeThrows() {
McMMOModifyBlockDropItemEvent ev = newEvent(1);
assertThrows(IllegalArgumentException.class, () -> ev.setBonusAmountToAdd(-1));
}
@Test
void setModifiedItemStackQuantityEqualToOriginalIsNoBonus() {
McMMOModifyBlockDropItemEvent ev = newEvent(2); // 3 -> 5
ev.setModifiedItemStackQuantity(3); // back to original => no bonus
assertEquals(3, ev.getModifiedItemStackQuantity());
assertEquals(0, ev.getBonusAmountToAdd());
assertTrue(ev.isEffectivelyNoBonus());
}
@Test
void setModifiedItemStackQuantityLessThanOriginalThrows() {
McMMOModifyBlockDropItemEvent ev = newEvent(1);
assertThrows(IllegalArgumentException.class, () -> ev.setModifiedItemStackQuantity(2)); // original is 3
}
@Test
void setModifiedItemStackQuantityGreaterThanOriginalUpdatesBonus() {
McMMOModifyBlockDropItemEvent ev = newEvent(1); // original 3 -> modified 4
ev.setModifiedItemStackQuantity(12);
assertEquals(12, ev.getModifiedItemStackQuantity());
assertEquals(9, ev.getBonusAmountToAdd()); // 12 - 3
assertFalse(ev.isEffectivelyNoBonus());
}
}
@Nested
@DisplayName("Delegate passthroughs")
class Delegates {
@Test
void getPlayerPassthrough() {
McMMOModifyBlockDropItemEvent ev = newEvent(1);
assertSame(player, ev.getPlayer());
verify(blockDropItemEvent, atLeastOnce()).getPlayer();
}
@Test
void getBlockPassthrough() {
McMMOModifyBlockDropItemEvent ev = newEvent(1);
assertSame(block, ev.getBlock());
verify(blockDropItemEvent, atLeastOnce()).getBlock();
}
@Test
void getBlockStatePassthrough() {
McMMOModifyBlockDropItemEvent ev = newEvent(1);
assertSame(blockState, ev.getBlockState());
verify(blockDropItemEvent, atLeastOnce()).getBlockState();
}
@Test
void getItemReturnsOriginalItemEntity() {
McMMOModifyBlockDropItemEvent ev = newEvent(1);
assertSame(itemEntity, ev.getItem());
}
}
@Nested
@DisplayName("HandlerList plumbing")
class HandlerListTests {
@Test
void handlerList_isNonNull_andShared() {
McMMOModifyBlockDropItemEvent ev = newEvent(1);
HandlerList fromInstance = ev.getHandlers();
HandlerList fromStatic = McMMOModifyBlockDropItemEvent.getHandlerList();
assertNotNull(fromInstance);
assertNotNull(fromStatic);
// Bukkit convention: same static instance
assertSame(fromStatic, fromInstance);
}
}
@Nested
@DisplayName("Object contracts")
class ObjectContracts {
@Test
void toStringContainsKeyFields() {
McMMOModifyBlockDropItemEvent ev = newEvent(2);
String s = ev.toString();
assertNotNull(s);
assertTrue(s.contains("originalBonusAmountToAdd=2"));
assertTrue(s.contains("modifiedItemStackQuantity=5"));
}
@Test
void equalsAndHashCodeReflectState() {
// Same inputs => equal (mocks are same instances)
McMMOModifyBlockDropItemEvent a = newEvent(2);
McMMOModifyBlockDropItemEvent b = newEvent(2);
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
// Change cancellation and modified quantity => not equal
McMMOModifyBlockDropItemEvent c = newEvent(2);
c.setCancelled(true);
assertNotEquals(a, c);
McMMOModifyBlockDropItemEvent d = newEvent(2);
d.setModifiedItemStackQuantity(99);
assertNotEquals(a, d);
// Different underlying mocks => not equal
BlockDropItemEvent otherEvent = mock(BlockDropItemEvent.class, RETURNS_DEEP_STUBS);
when(otherEvent.getPlayer()).thenReturn(player);
when(otherEvent.getBlock()).thenReturn(block);
when(otherEvent.getBlockState()).thenReturn(blockState);
ItemStack otherStack = mock(ItemStack.class);
when(otherStack.getAmount()).thenReturn(3);
Item otherItem = mock(Item.class);
when(otherItem.getItemStack()).thenReturn(otherStack);
McMMOModifyBlockDropItemEvent e = new McMMOModifyBlockDropItemEvent(otherEvent, otherItem, 2);
assertNotEquals(a, e);
}
}
}