package tc.oc.pgm.listeners; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.Item; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryClickedEvent; import org.bukkit.event.inventory.InventoryDragEvent; import org.bukkit.event.inventory.InventoryPickupItemEvent; import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerPickupItemEvent; import org.bukkit.inventory.*; import tc.oc.commons.bukkit.inventory.InventorySlot; import tc.oc.pgm.PGM; import tc.oc.pgm.events.ItemTransferEvent; import tc.oc.pgm.events.PlayerItemTransferEvent; import java.util.Map; import java.util.Optional; public class ItemTransferListener implements Listener { // Track players dropping an item stack from within an inventory GUI private boolean ignoreNextDropEvent; private boolean collectToCursor; private Inventory getLocalInventory(InventoryView view, int rawSlot) { int cookedSlot = view.convertSlot(rawSlot); if(cookedSlot == rawSlot) { return view.getTopInventory(); } else { return view.getBottomInventory(); } } private static Inventory getOtherInventory(InventoryView view, Inventory inventory) { if(view.getTopInventory() == inventory) { return view.getBottomInventory(); } else { return view.getTopInventory(); } } private static int getQuantityPlaceable(ItemStack stack, Inventory inventory) { int transferrable = 0; for(ItemStack slotStack : inventory.contents()) { if(slotStack == null) { return stack.getAmount(); } else if(slotStack.isSimilar(stack)) { transferrable += stack.getMaxStackSize() - slotStack.getAmount(); if(transferrable >= stack.getAmount()) { return stack.getAmount(); } } } return transferrable; } private void dropFromPlayer(Player player, ItemStack stack) { Item entity = player.getWorld().dropItem(player.getEyeLocation(), stack); entity.setVelocity(player.getLocation().getDirection().multiply(0.3)); } private void callEvent(ItemTransferEvent event) { Bukkit.getPluginManager().callEvent(event); } @EventHandler public void onPlayerPickupItem(PlayerPickupItemEvent event) { // When this event is fired, the ItemStack in the Item being picked up is temporarily // set to the amount that will actually be picked up, while the difference from the // actual amount in the stack is available from getRemaining(). When the event returns, // the original amount is restored to the stack, meaning that we can't change the amount // from inside the event, so instead we replace the entire stack. int initialQuantity = event.getItem().getItemStack().getAmount(); PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent( event, ItemTransferEvent.Type.PICKUP, event.getPlayer(), Optional.empty(), Optional.of(new InventorySlot<>(event.getPlayer().getInventory())), event.getItem().getItemStack(), event.getItem(), initialQuantity, event.getPlayer().getOpenInventory().getCursor() ); this.callEvent(transferEvent); int quantity = Math.min(transferEvent.getQuantity(), initialQuantity); if(!event.isCancelled() && quantity < initialQuantity) { event.setCancelled(true); if(quantity > 0) { ItemStack stack = event.getItem().getItemStack().clone(); stack.setAmount(stack.getAmount() - quantity); event.getItem().setItemStack(stack); stack = stack.clone(); stack.setAmount(quantity); event.getPlayer().getInventory().addItem(stack); event.getPlayer().playSound(event.getPlayer().getLocation(), Sound.ENTITY_ITEM_PICKUP, 1, 1); } } } @EventHandler public void onBlockPickupItem(InventoryPickupItemEvent event) { int initialQuantity = getQuantityPlaceable(event.getItem().getItemStack(), event.getInventory()); ItemTransferEvent transferEvent = new ItemTransferEvent( event, ItemTransferEvent.Type.PICKUP, Optional.empty(), Optional.of(new InventorySlot<>(event.getInventory())), event.getItem().getItemStack(), event.getItem(), initialQuantity ); this.callEvent(transferEvent); if(initialQuantity != transferEvent.getQuantity() && !event.isCancelled()) { event.setCancelled(true); ItemStack stack = event.getItem().getItemStack(); stack.setAmount(stack.getAmount() - transferEvent.getQuantity()); stack = stack.clone(); stack.setAmount(transferEvent.getQuantity()); event.getInventory().addItem(stack); } } @EventHandler public void onPlayerClickInventory(InventoryClickEvent event) { // Ignored actions switch(event.getAction()) { case CLONE_STACK: // Out of scope case COLLECT_TO_CURSOR: // Handled by InventoryClickedEvent case NOTHING: case UNKNOWN: return; } // Get the player who clicked if(!(event.getWhoClicked() instanceof Player)) { // Can this happen? return; } Player player = (Player) event.getWhoClicked(); // In a dual-inventory view, InventoryClickEvent.getInventory() always returns the top inventory, // so to figure out which one was actually clicked, we compare the view slot with the inv slot. // If they are the same, the click is in the top inv, because it is always mapped to view slot 0. // Otherwise, the click is in the bottom inv. This is the Bukkit recommended way to do this. Inventory inventory = getLocalInventory(event.getView(), event.getRawSlot()); if(event.getAction() == InventoryAction.SWAP_WITH_CURSOR) { // Click on a stack while already holding a stack, fire two events for the stacks being swapped if(inventory.getHolder() == player) { // Swap with own inventory is not a transfer return; } boolean cancelled = event.isCancelled(); int quantity = event.getCurrentItem().getAmount(); // The take event has no items on the cursor, because those will be placed by the second event PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent( event, ItemTransferEvent.Type.TAKE, player, Optional.of(InventorySlot.fromEvent(event)), Optional.empty(), event.getCurrentItem(), null, quantity, null ); this.callEvent(transferEvent); cancelled = cancelled | event.isCancelled() | quantity != transferEvent.getQuantity(); // Remove the item from the inventory so handlers of the second event can see that it is gone ItemStack oldInvStack = event.getCurrentItem(); event.setCurrentItem(null); quantity = event.getCursor().getAmount(); transferEvent = new PlayerItemTransferEvent( event, ItemTransferEvent.Type.PLACE, player, Optional.empty(), Optional.of(InventorySlot.fromEvent(event)), event.getCursor(), null, event.getCursor().getAmount(), event.getCursor() ); event.setCancelled(cancelled | event.isCancelled() | quantity != transferEvent.getQuantity()); // Replace the old item in the inventory event.setCurrentItem(oldInvStack); return; } // The remaining actions will generate one event at most ItemTransferEvent.Type type; Inventory fromInventory = null; Integer fromSlot = null; Inventory toInventory = null; Integer toSlot = null; ItemStack itemStack; // Determine inv, slot, and stack switch(event.getAction()) { case PICKUP_ALL: case PICKUP_SOME: case PICKUP_HALF: case PICKUP_ONE: if(inventory.getHolder() == player) { // Taking from own inventory is not a transfer return; } type = ItemTransferEvent.Type.TAKE; itemStack = event.getCurrentItem(); fromInventory = inventory; fromSlot = event.getSlot(); break; case PLACE_ALL: case PLACE_SOME: case PLACE_ONE: if(inventory.getHolder() == player) { // Placing in own inventory is not a transfer return; } type = ItemTransferEvent.Type.PLACE; itemStack = event.getCursor(); toInventory = inventory; toSlot = event.getSlot(); break; case DROP_ONE_SLOT: case DROP_ALL_SLOT: type = ItemTransferEvent.Type.DROP; itemStack = event.getCurrentItem(); fromInventory = inventory; fromSlot = event.getSlot(); break; case DROP_ONE_CURSOR: case DROP_ALL_CURSOR: type = ItemTransferEvent.Type.DROP; itemStack = event.getCursor(); break; case MOVE_TO_OTHER_INVENTORY: itemStack = event.getCurrentItem(); fromInventory = inventory; fromSlot = event.getSlot(); toInventory = getOtherInventory(event.getView(), fromInventory); if(toInventory == null || fromInventory.getHolder() == toInventory.getHolder()) { // shift-click to hotbar/armor slots return; } if(fromInventory.getHolder() == player && toInventory.getHolder() != player) { type = ItemTransferEvent.Type.PLACE; } else if (fromInventory.getHolder() != player && toInventory.getHolder() == player) { type = ItemTransferEvent.Type.TAKE; } else { type = ItemTransferEvent.Type.TRANSFER; } break; case HOTBAR_SWAP: case HOTBAR_MOVE_AND_READD: // Use a hotkey to move a stack in or out of a hotbar slot. If moving a stack into a // hotbar slot that is already occupied by an incompatible stack, that stack will be // moved to the slot under the cursor, if that slot is in the player's inventory, // otherwise it will be moved to the first available slot in the player's inventory. if(inventory.getHolder() == player) { // Ignore intra-inventory swap return; } if(event.getCurrentItem() != null && event.getCurrentItem().getType() != Material.AIR) { // Moving an item onto the hotbar type = ItemTransferEvent.Type.TAKE; itemStack = event.getCurrentItem(); fromInventory = inventory; fromSlot = event.getSlot(); toInventory = player.getInventory(); toSlot = event.getHotbarButton(); } else { itemStack = player.getInventory().getItem(event.getHotbarButton()); if(itemStack == null || itemStack.getType() == Material.AIR) { return; } // Moving an item out of the hotbar type = ItemTransferEvent.Type.PLACE; fromInventory = player.getInventory(); fromSlot = event.getHotbarButton(); toInventory = inventory; toSlot = event.getSlot(); } break; default: PGM.get().getLogger().warning("ItemTransferListener.onPlayerClickItem: Unhandled action " + event.getAction()); return; } int initialQuantity = 0; // Determine quantity switch(event.getAction()) { case PICKUP_ALL: // left-click stack with empty cursor case DROP_ALL_SLOT: // press control-drop key while hovering over stack case HOTBAR_SWAP: case HOTBAR_MOVE_AND_READD: initialQuantity = event.getCurrentItem().getAmount(); break; case PLACE_ALL: // left-click with cursor stack on empty slot or matching stack with enough space case DROP_ALL_CURSOR: // left-click outside of window with cursor stack initialQuantity = event.getCursor().getAmount(); break; case PICKUP_SOME: // left/right-click oversized stack with empty cursor initialQuantity = Math.min(event.getCurrentItem().getAmount(), event.getCurrentItem().getMaxStackSize()); break; case PLACE_SOME: // left-click with cursor stack on undersized-slot (e.g. beacon) or matching stack without enough space initialQuantity = Math.min(event.getCursor().getAmount(), Math.min(event.getCursor().getMaxStackSize(), event.getInventory().getMaxStackSize())); ItemStack existingStack = event.getCurrentItem(); if(existingStack != null) { initialQuantity -= existingStack.getAmount(); } break; case PICKUP_HALF: // right-click stack with empty cursor (rounds up) initialQuantity = (event.getCurrentItem().getAmount() + 1) / 2; break; case PICKUP_ONE: // same cause as PICKUP_SOME case PLACE_ONE: // right-click with cursor stack on slot/stack with space case DROP_ONE_CURSOR: // right-click outside of window with cursor stack case DROP_ONE_SLOT: // press drop key while hovering over stack initialQuantity = 1; break; case MOVE_TO_OTHER_INVENTORY: // shift-click in a dual-inventory view initialQuantity = getQuantityPlaceable(event.getCurrentItem(), toInventory); break; } if(initialQuantity <= 0) { return; } final Integer finalFromSlot = fromSlot, finalToSlot = toSlot; PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent(event, type, player, Optional.ofNullable(fromInventory).map(inv -> InventorySlot.fromInventoryIndex(inv, finalFromSlot)), Optional.ofNullable(toInventory).map(inv -> InventorySlot.fromInventoryIndex(inv, finalToSlot)), itemStack, null, initialQuantity, event.getCursor()); this.callEvent(transferEvent); int quantity = Math.min(transferEvent.getQuantity(), initialQuantity); if(quantity < initialQuantity) { event.setCancelled(true); if(quantity > 0) { ItemStack item; ItemStack otherItem; switch(event.getAction()) { case PICKUP_ALL: case PICKUP_SOME: case PICKUP_HALF: case PICKUP_ONE: item = event.getCurrentItem(); item.setAmount(item.getAmount() - quantity); otherItem = item.clone(); otherItem.setAmount(quantity); event.getView().setCursor(otherItem); break; case PLACE_ALL: case PLACE_SOME: case PLACE_ONE: otherItem = event.getCursor(); otherItem.setAmount(otherItem.getAmount() - quantity); event.getView().setCursor(otherItem); item = event.getCurrentItem(); if(item == null || item.getType() == Material.AIR) { item = otherItem.clone(); item.setAmount(quantity); event.setCurrentItem(item); PGM.get().getLogger().info("Placing " + item + " in slot " + event.getRawSlot()); } else { item.setAmount(item.getAmount() + quantity); } break; case DROP_ALL_CURSOR: case DROP_ONE_CURSOR: otherItem = event.getCursor(); otherItem.setAmount(otherItem.getAmount() - quantity); event.getView().setCursor(otherItem); item = otherItem.clone(); item.setAmount(quantity); this.dropFromPlayer(player, item); break; case DROP_ALL_SLOT: case DROP_ONE_SLOT: item = event.getCurrentItem(); item.setAmount(item.getAmount() - quantity); item = item.clone(); item.setAmount(quantity); this.dropFromPlayer(player, item); break; case MOVE_TO_OTHER_INVENTORY: if(toInventory != null) { item = event.getCurrentItem(); item.setAmount(item.getAmount() - quantity); item = item.clone(); item.setAmount(quantity); toInventory.addItem(item); } break; case HOTBAR_SWAP: case HOTBAR_MOVE_AND_READD: otherItem = player.getInventory().getItem(event.getHotbarButton()); item = event.getCurrentItem(); if(item != null && item.getType() != Material.AIR) { // Move item onto hotbar item.setAmount(item.getAmount() - quantity); item = item.clone(); item.setAmount(quantity); player.getInventory().setItem(event.getHotbarButton(), item); if(otherItem != null) { player.getInventory().addItem(otherItem); } } else if(otherItem != null && otherItem.getType() != Material.AIR) { // Move item off of hotbar otherItem.setAmount(otherItem.getAmount() - quantity); otherItem = otherItem.clone(); otherItem.setAmount(quantity); event.setCurrentItem(otherItem); } break; } } } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void setIgnoreDropFlag(InventoryClickEvent event) { switch(event.getAction()) { case DROP_ALL_CURSOR: case DROP_ONE_CURSOR: case DROP_ALL_SLOT: case DROP_ONE_SLOT: // Make a note to ignore the PlayerDropItemEvent that will follow this one this.ignoreNextDropEvent = true; break; } } @EventHandler public void onPlayerDropItem(PlayerDropItemEvent event) { if(this.ignoreNextDropEvent) { this.ignoreNextDropEvent = false; } else { // If the ignore flag is clear, this drop was caused by something other than // an inventory click (e.g. drop key, death, etc), so an event has not yet been fired int initialQuantity = event.getItemDrop().getItemStack().getAmount(); ItemStack stack = event.getItemDrop().getItemStack(); PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent(event, ItemTransferEvent.Type.DROP, event.getPlayer(), Optional.of(new InventorySlot(event.getPlayer().getInventory())), Optional.empty(), stack, event.getItemDrop(), initialQuantity, event.getPlayer().getOpenInventory().getCursor()); this.callEvent(transferEvent); if(!transferEvent.isCancelled() && transferEvent.getQuantity() < initialQuantity) { int diff = initialQuantity - transferEvent.getQuantity(); stack.setAmount(stack.getAmount() - diff); stack = stack.clone(); stack.setAmount(diff); event.getPlayer().getInventory().addItem(stack); } } } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void collectToCursor(InventoryClickEvent event) { // If this hasn't been cancelled yet, cancel it so our implementation can take over if(event.getAction() == InventoryAction.COLLECT_TO_CURSOR) { event.setCancelled(true); this.collectToCursor = true; } } @EventHandler public void onPlayerInventoryClicked(InventoryClickedEvent event) { // Control-double-click on a stack, all similar stacks are moved to the cursor, up to the max stack size // We cancel all of these and redo them ourselves. We have to do it from a InventoryClickedEvent because // we can't make the necessary changes from inside a InventoryClickEvent. if(this.collectToCursor) { this.collectToCursor = false; if(!(event.getWhoClicked() instanceof Player)) { return; } Player player = (Player) event.getWhoClicked(); ItemStack cursor = event.getCursor().clone(); for(int pass = 0; pass < 2; pass++) { for(int rawSlot = 0; rawSlot < event.getView().countSlots(); rawSlot++) { if(cursor.getAmount() >= cursor.getMaxStackSize()) { // If the gathered stack is full, we're done break; } ItemStack stack = event.getView().getItem(rawSlot); // First pass takes incomplete stacks, second pass takes complete ones if(cursor.isSimilar(stack) && ((pass == 0 && stack.getAmount() < stack.getMaxStackSize()) || (pass == 1 && stack.getAmount() >= stack.getMaxStackSize()))) { // Calculate how much can be collected from this stack // If it is the output slot of a transaction preview, 0 int quantity = event.getView().getTopInventory() instanceof CraftingInventory && event.getView().convertSlot(rawSlot) == 0 || event.getView().getTopInventory() instanceof MerchantInventory && event.getView().convertSlot(rawSlot) == 2 ? 0 : Math.min(stack.getAmount(), cursor.getMaxStackSize() - cursor.getAmount()); Inventory localInventory = getLocalInventory(event.getView(), rawSlot); if(localInventory.getHolder() != player) { // If stack comes from an external inventory, fire a transfer event PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent(event, ItemTransferEvent.Type.TAKE, player, Optional.of(InventorySlot.fromInventoryIndex(localInventory, event.getView().convertSlot(rawSlot))), Optional.empty(), stack, null, quantity, cursor); this.callEvent(transferEvent); if(transferEvent.isCancelled()) { // If the event is cancelled, don't transfer from this slot quantity = 0; } else { quantity = transferEvent.getQuantity(); } } if(quantity > 0) { // Collect items from this stack to the cursor cursor.setAmount(cursor.getAmount() + quantity); if(quantity == stack.getAmount()) { event.getView().setItem(rawSlot, null); } else { stack.setAmount(stack.getAmount() - quantity); } } } } } event.getView().setCursor(cursor); player.updateInventory(); } } @EventHandler(ignoreCancelled = true) public void onPlayerDragInventory(InventoryDragEvent event) { // This is when you spread items evenly across slots by dragging if(!(event.getWhoClicked() instanceof Player)) { return; } Player player = (Player) event.getWhoClicked(); ItemStack transferred = event.getOldCursor().clone(); transferred.setAmount(0); Inventory externalInventory = null; for(Map.Entry<Integer, ItemStack> entry : event.getNewItems().entrySet()) { Inventory inventory = getLocalInventory(event.getView(), entry.getKey()); if(inventory.getHolder() != player) { // Add stacks to the total if they are dragged over an external inventory externalInventory = inventory; transferred.setAmount(transferred.getAmount() + entry.getValue().getAmount()); } } if(externalInventory != null) { int initialQuantity = transferred.getAmount(); PlayerItemTransferEvent transferEvent = new PlayerItemTransferEvent( event, ItemTransferEvent.Type.PLACE, player, Optional.empty(), Optional.of(new InventorySlot<>(externalInventory)), transferred, null, initialQuantity, event.getOldCursor() ); this.callEvent(transferEvent); if(initialQuantity != transferEvent.getQuantity()) { // If the quantity changes, we have to cancel the entire drag, // because bukkit does not let us modify the dragged stacks. event.setCancelled(true); } } } }