| package net.glowstone.net.handler.play.inv; |
| |
| import com.flowpowered.network.MessageHandler; |
| import net.glowstone.EventFactory; |
| import net.glowstone.GlowServer; |
| import net.glowstone.entity.GlowPlayer; |
| import net.glowstone.inventory.*; |
| import net.glowstone.net.GlowSession; |
| import net.glowstone.net.message.play.inv.SetWindowSlotMessage; |
| import net.glowstone.net.message.play.inv.TransactionMessage; |
| import net.glowstone.net.message.play.inv.WindowClickMessage; |
| import org.bukkit.GameMode; |
| import org.bukkit.Material; |
| import org.bukkit.event.inventory.*; |
| import org.bukkit.event.inventory.InventoryType.SlotType; |
| import org.bukkit.inventory.Inventory; |
| import org.bukkit.inventory.InventoryView; |
| import org.bukkit.inventory.ItemStack; |
| import org.bukkit.inventory.Recipe; |
| |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| public final class WindowClickHandler implements MessageHandler<GlowSession, WindowClickMessage> { |
| @Override |
| public void handle(GlowSession session, WindowClickMessage message) { |
| boolean result = false; |
| try { |
| result = process(session.getPlayer(), message); |
| } catch (IllegalArgumentException ex) { |
| GlowServer.logger.warning(session.getPlayer().getName() + ": illegal argument while handling click: " + ex); |
| } |
| if (!result) { |
| GlowServer.logger.info(session.getPlayer().getName() + ": [rejected] " + message); |
| } |
| session.send(new TransactionMessage(message.getId(), message.getTransaction(), result)); |
| } |
| |
| private boolean process(final GlowPlayer player, final WindowClickMessage message) { |
| final int viewSlot = message.getSlot(); |
| final InventoryView view = player.getOpenInventory(); |
| final GlowInventory top = (GlowInventory) view.getTopInventory(); |
| final GlowInventory bottom = (GlowInventory) view.getBottomInventory(); |
| final ItemStack slotItem = view.getItem(viewSlot); |
| final ItemStack cursor = player.getItemOnCursor(); |
| |
| // check that the player has a correct view of the item |
| if (!Objects.equals(message.getItem(), slotItem) && (message.getMode() == 0 || message.getMode() == 1)) { |
| // reject item change because of desynced inventory |
| // in mode 3 (get) and 4 (drop), client does not send item in slot under cursor |
| if (message.getMode() == 0 || message.getItem() != null) { |
| // in mode 1 (shift click), client does not send item in slot under cursor if the |
| // action did not result in any change on the client side (inventory full) or |
| // if there's an item under the cursor |
| |
| player.sendItemChange(viewSlot, slotItem); |
| return false; |
| } |
| } |
| |
| // determine inventory and slot clicked, used in some places |
| // todo: attempt to allow for users to implement their own inventory? |
| // CraftBukkit does not allow this but it may be worth the trouble for |
| // the extensibility. |
| final GlowInventory inv; |
| if (viewSlot < top.getSize()) { |
| inv = top; |
| } else { |
| inv = bottom; |
| } |
| final int invSlot = view.convertSlot(viewSlot); |
| final InventoryType.SlotType slotType = inv.getSlotType(invSlot); |
| |
| // handle dragging |
| if (message.getMode() == 5) { |
| // 5 0 * start left drag |
| // 5 1 add slot left drag |
| // 5 2 * end left drag |
| // 5 4 * start right drag |
| // 5 5 add slot right drag |
| // 5 6 * end right drag |
| |
| DragTracker drag = player.getInventory().getDragTracker(); |
| boolean right = (message.getButton() >= 4); |
| |
| switch (message.getButton()) { |
| case 0: // start left drag |
| case 4: // start right drag |
| return drag.start(right); |
| |
| case 1: // add slot left |
| case 5: // add slot right |
| return drag.addSlot(right, message.getSlot()); |
| |
| case 2: // end left drag |
| case 6: // end right drag |
| List<Integer> slots = drag.finish(right); |
| if (slots == null || cursor == null) { |
| return false; |
| } |
| |
| ItemStack newCursor = cursor.clone(); |
| Map<Integer, ItemStack> newSlots = new HashMap<>(); |
| |
| int perSlot = right ? 1 : cursor.getAmount() / slots.size(); |
| for (int dragSlot : slots) { |
| ItemStack oldItem = view.getItem(dragSlot); |
| if (oldItem == null || cursor.isSimilar(oldItem)) { |
| Inventory dragInv = dragSlot < top.getSize() ? top : bottom; |
| int oldItemAmount = oldItem == null ? 0 : oldItem.getAmount(); |
| int transfer = Math.min(Math.min(perSlot, cursor.getAmount()), maxStack(dragInv, cursor.getType()) - oldItemAmount); |
| ItemStack newItem = combine(oldItem, cursor, transfer); |
| newSlots.put(dragSlot, newItem); |
| newCursor = amountOrNull(newCursor, newCursor.getAmount() - transfer); |
| if (newCursor == null) { |
| break; |
| } |
| } |
| } |
| |
| InventoryDragEvent event = new InventoryDragEvent(view, newCursor, cursor, right, newSlots); |
| if (event.isCancelled()) { |
| return false; |
| } |
| |
| for (Map.Entry<Integer, ItemStack> entry : newSlots.entrySet()) { |
| view.setItem(entry.getKey(), entry.getValue()); |
| } |
| player.setItemOnCursor(newCursor); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // determine what action will be taken and fire event |
| final ClickType clickType = WindowClickLogic.getClickType(message.getMode(), message.getButton(), viewSlot); |
| InventoryAction action = WindowClickLogic.getAction(clickType, slotType, cursor, slotItem); |
| |
| if (clickType == ClickType.UNKNOWN || action == InventoryAction.UNKNOWN) { |
| // show a warning for unknown click type |
| GlowServer.logger.warning(player.getName() + ": mystery window click " + clickType + "/" + action + ": " + message); |
| } |
| |
| // deny CLONE_STACK for non-creative mode players |
| if (action == InventoryAction.CLONE_STACK && player.getGameMode() != GameMode.CREATIVE) { |
| action = InventoryAction.NOTHING; |
| } |
| |
| // determine whether NOTHING, HOTBAR_MOVE_AND_READD or HOTBAR_SWAP should be executed |
| if (clickType == ClickType.NUMBER_KEY) { |
| ItemStack destItem = bottom.getItem(message.getButton()); |
| if (slotItem == null) { |
| if (destItem == null) { |
| // both items are null, do nothing |
| action = InventoryAction.NOTHING; |
| } else if (!inv.itemPlaceAllowed(invSlot, destItem)) { |
| // current item is null and destItem cannot be moved into current slot |
| action = InventoryAction.NOTHING; |
| } |
| } else if (slotItem != null && destItem != null && (inv != bottom || !inv.itemPlaceAllowed(invSlot, destItem))) { |
| // target and source inventory are different or destItem cannot be placed in current slot |
| action = InventoryAction.HOTBAR_MOVE_AND_READD; |
| } |
| } |
| |
| if (WindowClickLogic.isPlaceAction(action)) { |
| // check whether item can be dropped into the clicked slot |
| if (!inv.itemPlaceAllowed(invSlot, cursor)) { |
| // placement not allowed |
| if (slotItem != null && slotItem.isSimilar(cursor)) { |
| // item in slot is the same as item on cursor |
| if (cursor.getAmount() + 1 == cursor.getMaxStackSize()) { |
| // There is still space under the cursor for one item |
| action = InventoryAction.PICKUP_ONE; |
| } else if (cursor.getAmount() < cursor.getMaxStackSize()) { |
| // There is still some space under the cursor |
| action = InventoryAction.PICKUP_SOME; |
| } |
| } else { |
| action = InventoryAction.NOTHING; |
| } |
| } |
| } |
| |
| InventoryClickEvent event = null; |
| if (top == inv && top instanceof GlowCraftingInventory && top.getSlotType(invSlot) == SlotType.RESULT) { |
| // Clicked on output slot of crafting inventory |
| if (slotItem == null) { |
| // No crafting recipe result, don't do anything |
| action = InventoryAction.NOTHING; |
| } |
| |
| int cursorAmount = cursor == null ? 0 : cursor.getAmount(); |
| if (slotItem != null && cursorAmount + slotItem.getAmount() <= slotItem.getMaxStackSize()) { |
| // if the player can take the whole result |
| if (WindowClickLogic.isPickupAction(action) || WindowClickLogic.isPlaceAction(action)) { |
| // always take the whole crafting result out of the crafting inventories |
| action = InventoryAction.PICKUP_ALL; |
| } else if (action == InventoryAction.DROP_ONE_SLOT) { |
| // always drop the whole stack, not just single items |
| action = InventoryAction.DROP_ALL_SLOT; |
| } |
| } else { |
| // if their cursor is full, do nothing |
| action = InventoryAction.NOTHING; |
| } |
| // if we do anything, call the CraftItemEvent |
| // this ignores whether the crafting process actually happens (full inventory, etc.) |
| if (action != InventoryAction.NOTHING) { |
| Recipe recipe = ((GlowCraftingInventory) inv).getRecipe(); |
| if (clickType == ClickType.NUMBER_KEY) { |
| event = new CraftItemEvent(recipe, view, slotType, viewSlot, clickType, action, message.getButton()); |
| } else { |
| event = new CraftItemEvent(recipe, view, slotType, viewSlot, clickType, action); |
| } |
| } |
| } |
| |
| if (event == null) { |
| if (clickType == ClickType.NUMBER_KEY) { |
| event = new InventoryClickEvent(view, slotType, viewSlot, clickType, action, message.getButton()); |
| } else { |
| event = new InventoryClickEvent(view, slotType, viewSlot, clickType, action); |
| } |
| } |
| |
| EventFactory.callEvent(event); |
| if (event.isCancelled()) { |
| player.getSession().send(new SetWindowSlotMessage(-1, -1, player.getItemOnCursor())); |
| if (message.getSlot() >= 0) { |
| if (inv == top) { |
| player.sendItemChange(message.getSlot(), inv.getItem(message.getSlot())); |
| } else { |
| player.sendItemChange(message.getSlot(), inv.getItem(view.convertSlot(message.getSlot()))); |
| } |
| } |
| return true; |
| } |
| |
| boolean handled = true; |
| switch (action) { |
| case NOTHING: |
| break; |
| |
| case UNKNOWN: |
| // return false rather than break - this case is "handled" but |
| // any action the client tried to take should be denied |
| return false; |
| |
| // PICKUP_* |
| case PICKUP_ALL: |
| view.setItem(viewSlot, null); |
| int cursorAmount = cursor == null ? 0 : cursor.getAmount(); |
| player.setItemOnCursor(amountOrNull(slotItem, cursorAmount + slotItem.getAmount())); |
| break; |
| case PICKUP_HALF: { |
| // pick up half (favor picking up) |
| int keepAmount = slotItem.getAmount() / 2; |
| ItemStack newCursor = slotItem.clone(); |
| newCursor.setAmount(slotItem.getAmount() - keepAmount); |
| |
| inv.setItem(invSlot, amountOrNull(slotItem, keepAmount)); |
| player.setItemOnCursor(newCursor); |
| break; |
| } |
| case PICKUP_SOME: |
| // pick up as many items as possible |
| int pickUp = Math.min(cursor.getMaxStackSize() - cursor.getAmount(), slotItem.getAmount()); |
| view.setItem(viewSlot, amountOrNull(slotItem, slotItem.getAmount() - pickUp)); |
| player.setItemOnCursor(amountOrNull(cursor, cursor.getAmount() + pickUp)); |
| break; |
| case PICKUP_ONE: |
| view.setItem(invSlot, amountOrNull(slotItem, slotItem.getAmount() - 1)); |
| player.setItemOnCursor(amountOrNull(cursor, cursor.getAmount() + 1)); |
| break; |
| |
| // PLACE_* |
| case PLACE_ALL: |
| view.setItem(viewSlot, combine(slotItem, cursor, cursor.getAmount())); |
| player.setItemOnCursor(null); |
| break; |
| case PLACE_SOME: { |
| // slotItem *should* never be null in this situation? |
| int transfer = Math.min(cursor.getAmount(), maxStack(inv, slotItem.getType()) - slotItem.getAmount()); |
| view.setItem(viewSlot, combine(slotItem, cursor, transfer)); |
| player.setItemOnCursor(amountOrNull(cursor, cursor.getAmount() - transfer)); |
| break; |
| } |
| case PLACE_ONE: |
| view.setItem(viewSlot, combine(slotItem, cursor, 1)); |
| player.setItemOnCursor(amountOrNull(cursor, cursor.getAmount() - 1)); |
| break; |
| |
| case SWAP_WITH_CURSOR: |
| view.setItem(viewSlot, cursor); |
| player.setItemOnCursor(slotItem); |
| break; |
| |
| // DROP_* |
| case DROP_ALL_CURSOR: |
| if (cursor != null) { |
| drop(player, cursor); |
| player.setItemOnCursor(null); |
| } |
| break; |
| case DROP_ONE_CURSOR: |
| if (cursor != null) { |
| drop(player, amountOrNull(cursor.clone(), 1)); |
| player.setItemOnCursor(amountOrNull(cursor, cursor.getAmount() - 1)); |
| } |
| break; |
| case DROP_ALL_SLOT: |
| if (slotItem != null) { |
| drop(player, slotItem); |
| view.setItem(viewSlot, null); |
| } |
| break; |
| case DROP_ONE_SLOT: |
| if (slotItem != null) { |
| drop(player, amountOrNull(slotItem.clone(), 1)); |
| view.setItem(viewSlot, amountOrNull(slotItem, slotItem.getAmount() - 1)); |
| } |
| break; |
| |
| // shift-click |
| case MOVE_TO_OTHER_INVENTORY: |
| if (slotItem != null) { |
| inv.handleShiftClick(player, view, viewSlot, slotItem); |
| } |
| break; |
| |
| case HOTBAR_MOVE_AND_READD: |
| case HOTBAR_SWAP: |
| GlowPlayerInventory playerInv = player.getInventory(); |
| int hotbarSlot = message.getButton(); |
| ItemStack destItem = playerInv.getItem(hotbarSlot); |
| |
| if (slotItem == null) { |
| // nothing in current slot |
| if (destItem == null) { |
| // no action |
| return false; |
| } else { |
| // move from hotbar to current slot |
| // do nothing if current slots does not accept the item |
| if (action == InventoryAction.HOTBAR_SWAP) { |
| inv.setItem(invSlot, destItem); |
| playerInv.setItem(hotbarSlot, null); |
| } |
| return true; |
| } |
| } else { |
| if (destItem == null) { |
| // move from current slot to hotbar |
| playerInv.setItem(hotbarSlot, slotItem); |
| inv.setItem(invSlot, null); |
| return true; |
| } else { |
| // both are non-null, swap them |
| playerInv.setItem(hotbarSlot, slotItem); |
| if (action == InventoryAction.HOTBAR_SWAP) { |
| inv.setItem(invSlot, destItem); |
| } else { |
| inv.setItem(invSlot, null); |
| playerInv.addItem(destItem); |
| } |
| return true; |
| } |
| } |
| |
| case CLONE_STACK: |
| // only in creative and with no item on cursor handled earlier |
| // copy and maximize item |
| ItemStack stack = slotItem.clone(); |
| stack.setAmount(stack.getType().getMaxStackSize()); |
| player.setItemOnCursor(stack); |
| break; |
| |
| case COLLECT_TO_CURSOR: |
| if (cursor == null) { |
| return false; |
| } |
| |
| int slotCount = view.countSlots(); |
| if (GlowInventoryView.isDefault(view)) { |
| // view.countSlots does not account for armor slots |
| slotCount += 4; |
| } |
| for (int i = 0; i < slotCount && cursor.getAmount() < maxStack(inv, cursor.getType()); ++i) { |
| ItemStack item = view.getItem(i); |
| SlotType type = (i < top.getSize() ? top : bottom).getSlotType(view.convertSlot(i)); |
| if (item == null || !cursor.isSimilar(item) || type == SlotType.RESULT) { |
| continue; |
| } |
| int transfer = Math.min(item.getAmount(), maxStack(inv, cursor.getType()) - cursor.getAmount()); |
| cursor.setAmount(cursor.getAmount() + transfer); |
| view.setItem(i, amountOrNull(item, item.getAmount() - transfer)); |
| } |
| break; |
| } |
| |
| if (handled && top == inv && top instanceof GlowCraftingInventory && top.getSlotType(invSlot) == SlotType.RESULT) { |
| ((GlowCraftingInventory) top).craft(); |
| } |
| |
| if (!handled) { |
| GlowServer.logger.warning(player.getName() + ": unhandled click action " + action + " for " + message); |
| } |
| |
| return handled; |
| } |
| |
| private void drop(GlowPlayer player, ItemStack stack) { |
| // drop the stack if it's valid |
| if (stack != null && stack.getAmount() > 0) { |
| player.drop(stack); |
| } |
| } |
| |
| private ItemStack combine(ItemStack slotItem, ItemStack cursor, int amount) { |
| if (slotItem == null) { |
| ItemStack stack = cursor.clone(); |
| stack.setAmount(amount); |
| return stack; |
| } else if (slotItem.isSimilar(cursor)) { |
| slotItem.setAmount(slotItem.getAmount() + amount); |
| return slotItem; |
| } else { |
| throw new IllegalArgumentException("Trying to combine dissimilar " + slotItem + " and " + cursor); |
| } |
| } |
| |
| private ItemStack amountOrNull(ItemStack original, int amount) { |
| original.setAmount(amount); |
| return amount <= 0 ? null : original; |
| } |
| |
| private int maxStack(Inventory inv, Material mat) { |
| return Math.min(inv.getMaxStackSize(), mat.getMaxStackSize()); |
| } |
| |
| } |