package de.themoep.inventorygui; /* * Copyright 2017 Max Lee (https://github.com/Phoenix616) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.Nameable; import org.bukkit.Sound; import org.bukkit.block.BlockState; import org.bukkit.entity.Entity; import org.bukkit.entity.HumanEntity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockDispenseEvent; import org.bukkit.event.entity.EntityDeathEvent; import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryCloseEvent; import org.bukkit.event.inventory.InventoryDragEvent; import org.bukkit.event.inventory.InventoryMoveItemEvent; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.material.MaterialData; import org.bukkit.plugin.java.JavaPlugin; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * The main library class that lets you create and manage your GUIs */ public class InventoryGui implements Listener { private final static int[] ROW_WIDTHS = {3, 5, 9}; private final static InventoryType[] INVENTORY_TYPES = { InventoryType.DROPPER, // 3*3 InventoryType.HOPPER, // 5*1 InventoryType.CHEST // 9*x }; private final static Sound CLICK_SOUND; private final static Map<String, InventoryGui> GUI_MAP = new HashMap<>(); private final static Map<UUID, ArrayDeque<InventoryGui>> GUI_HISTORY = new HashMap<>(); private final static Map<String, Pattern> PATTERN_CACHE = new HashMap<>(); private final JavaPlugin plugin; private GuiListener listener = new GuiListener(this); private String title; private final char[] slots; private final Map<Character, GuiElement> elements = new HashMap<>(); private InventoryType inventoryType; private Map<UUID, Inventory> inventories = new LinkedHashMap<>(); private InventoryHolder owner = null; private boolean listenersRegistered = false; private int pageNumber = 0; private int pageAmount = 1; private GuiElement.Action outsideAction = click -> false; private CloseAction closeAction = close -> true; private boolean silent = false; static { // Sound names changed, make it compatible with both versions Sound clickSound = null; String[] clickSounds = {"UI_BUTTON_CLICK", "CLICK"}; for (String s : clickSounds) { try { clickSound = Sound.valueOf(s.toUpperCase()); break; } catch (IllegalArgumentException ignored) {} } if (clickSound == null) { for (Sound sound : Sound.values()) { if (sound.name().contains("CLICK")) { clickSound = sound; break; } } } CLICK_SOUND = clickSound; } /** * Create a new gui with a certain setup and some elements * @param plugin Your plugin * @param owner The holder that owns this gui to retrieve it with {@link #get(InventoryHolder)}. * Can be <tt>null</tt>. * @param title The name of the GUI. This will be the title of the inventory. * @param rows How your rows are setup. Each element is getting assigned to a character. * Empty/missing ones get filled with the Filler. * @param elements The {@link GuiElement}s that the gui should have. You can also use {@link #addElement(GuiElement)} later. * @throws IllegalArgumentException Thrown when the provided rows cannot be matched to an InventoryType */ public InventoryGui(JavaPlugin plugin, InventoryHolder owner, String title, String[] rows, GuiElement... elements) { this.plugin = plugin; this.owner = owner; this.title = title; int width = ROW_WIDTHS[0]; for (String row : rows) { if (row.length() > width) { width = row.length(); } } for (int i = 0; i < ROW_WIDTHS.length && i < INVENTORY_TYPES.length; i++) { if (width < ROW_WIDTHS[i]) { width = ROW_WIDTHS[i]; } if (width == ROW_WIDTHS[i]) { inventoryType = INVENTORY_TYPES[i]; break; } } if (inventoryType == null) { throw new IllegalArgumentException("Could not match row setup to an inventory type!"); } StringBuilder slotsBuilder = new StringBuilder(); for (String row : rows) { if (row.length() < width) { double side = (width - row.length()) / 2; for (int i = 0; i < Math.floor(side); i++) { slotsBuilder.append(" "); } slotsBuilder.append(row); for (int i = 0; i < Math.ceil(side); i++) { slotsBuilder.append(" "); } } else if (row.length() == width) { slotsBuilder.append(row); } else { slotsBuilder.append(row.substring(0, width)); } } slots = slotsBuilder.toString().toCharArray(); addElements(elements); } /** * The simplest way to create a new gui. It has no owner and elements are optional. * @param plugin Your plugin * @param title The name of the GUI. This will be the title of the inventory. * @param rows How your rows are setup. Each element is getting assigned to a character. * Empty/missing ones get filled with the Filler. * @param elements The {@link GuiElement}s that the gui should have. You can also use {@link #addElement(GuiElement)} later. * @throws IllegalArgumentException Thrown when the provided rows cannot be matched to an InventoryType */ public InventoryGui(JavaPlugin plugin, String title, String[] rows, GuiElement... elements) { this(plugin, null, title, rows, elements); } /** * Create a new gui that has no owner with a certain setup and some elements * @param plugin Your plugin * @param owner The holder that owns this gui to retrieve it with {@link #get(InventoryHolder)}. * Can be <tt>null</tt>. * @param title The name of the GUI. This will be the title of the inventory. * @param rows How your rows are setup. Each element is getting assigned to a character. * Empty/missing ones get filled with the Filler. * @param elements The {@link GuiElement}s that the gui should have. You can also use {@link #addElement(GuiElement)} later. * @throws IllegalArgumentException Thrown when the provided rows cannot be matched to an InventoryType */ public InventoryGui(JavaPlugin plugin, InventoryHolder owner, String title, String[] rows, Collection<GuiElement> elements) { this(plugin, owner, title, rows); addElements(elements); } /** * Add an element to the gui * @param element The {@link GuiElement} to add */ public void addElement(GuiElement element) { elements.put(element.getSlotChar(), element); element.setGui(this); element.setSlots(getSlots(element.getSlotChar())); } private int[] getSlots(char slotChar) { ArrayList<Integer> slotList = new ArrayList<>(); for (int i = 0; i < slots.length; i++) { if (slots[i] == slotChar) { slotList.add(i); } } return slotList.stream().mapToInt(Integer::intValue).toArray(); } /** * Create and add a {@link StaticGuiElement} in one quick method. * @param slotChar The character to replace in the gui setup string * @param item The item that should be displayed * @param action The {@link de.themoep.inventorygui.GuiElement.Action} to run when the player clicks on this element * @param text The text to display on this element, placeholders are automatically * replaced, see {@link InventoryGui#replaceVars} for a list of the * placeholder variables. Empty text strings are also filter out, use * a single space if you want to add an empty line!<br> * If it's not set/empty the item's default name will be used */ public void addElement(char slotChar, ItemStack item, GuiElement.Action action, String... text) { addElement(new StaticGuiElement(slotChar, item, action, text)); } /** * Create and add a {@link StaticGuiElement} that has no action. * @param slotChar The character to replace in the gui setup string * @param item The item that should be displayed * @param text The text to display on this element, placeholders are automatically * replaced, see {@link InventoryGui#replaceVars} for a list of the * placeholder variables. Empty text strings are also filter out, use * a single space if you want to add an empty line!<br> * If it's not set/empty the item's default name will be used */ public void addElement(char slotChar, ItemStack item, String... text) { addElement(new StaticGuiElement(slotChar, item, null, text)); } /** * Create and add a {@link StaticGuiElement} in one quick method. * @param slotChar The character to replace in the gui setup string * @param materialData The {@link MaterialData} of the item of tihs element * @param action The {@link de.themoep.inventorygui.GuiElement.Action} to run when the player clicks on this element * @param text The text to display on this element, placeholders are automatically * replaced, see {@link InventoryGui#replaceVars} for a list of the * placeholder variables. Empty text strings are also filter out, use * a single space if you want to add an empty line!<br> * If it's not set/empty the item's default name will be used */ public void addElement(char slotChar, MaterialData materialData, GuiElement.Action action, String... text) { addElement(slotChar, materialData.toItemStack(1), action, text); } /** * Create and add a {@link StaticGuiElement} * @param slotChar The character to replace in the gui setup string * @param material The {@link Material} that the item should have * @param data The <tt>byte</tt> representation of the material data of this element * @param action The {@link GuiElement.Action} to run when the player clicks on this element * @param text The text to display on this element, placeholders are automatically * replaced, see {@link InventoryGui#replaceVars} for a list of the * placeholder variables. Empty text strings are also filter out, use * a single space if you want to add an empty line!<br> * If it's not set/empty the item's default name will be used */ public void addElement(char slotChar, Material material, byte data, GuiElement.Action action, String... text) { addElement(slotChar, new MaterialData(material, data), action, text); } /** * Create and add a {@link StaticGuiElement} * @param slotChar The character to replace in the gui setup string * @param material The {@link Material} that the item should have * @param action The {@link GuiElement.Action} to run when the player clicks on this element * @param text The text to display on this element, placeholders are automatically * replaced, see {@link InventoryGui#replaceVars} for a list of the * placeholder variables. Empty text strings are also filter out, use * a single space if you want to add an empty line!<br> * If it's not set/empty the item's default name will be used */ public void addElement(char slotChar, Material material, GuiElement.Action action, String... text) { addElement(slotChar, material, (byte) 0, action, text); } /** * Add multiple elements to the gui * @param elements The {@link GuiElement}s to add */ public void addElements(GuiElement... elements) { for (GuiElement element : elements) { addElement(element); } } /** * Add multiple elements to the gui * @param elements The {@link GuiElement}s to add */ public void addElements(Collection<GuiElement> elements) { for (GuiElement element : elements) { addElement(element); } } /** * Set the filler element for empty slots * @param item The item for the filler element */ public void setFiller(ItemStack item) { addElement(new StaticGuiElement(' ', item, " ")); } /** * Get the filler element * @return The filler element for empty slots */ public GuiElement getFiller() { return elements.get(' '); } /** * Get the number of the page that this gui is on. zero indexed. Only affects group elements. * @return The page number */ public int getPageNumber() { return pageNumber; } /** * Set the number of the page that this gui is on. zero indexed. Only affects group elements. * @param pageNumber The page number to set */ public void setPageNumber(int pageNumber) { this.pageNumber = pageNumber; draw(); } /** * Get the amount of pages that this GUI has * @return The amount of pages */ public int getPageAmount() { return pageAmount; } private void calculatePageAmount() { for (GuiElement element : elements.values()) { int amount = 0; if (element instanceof GuiElementGroup) { amount = ((GuiElementGroup) element).size(); } else if (element instanceof GuiStorageElement) { amount = ((GuiStorageElement) element).getStorage().getSize(); } if (amount > 0 && (pageAmount - 1) * element.getSlots().length < amount) { pageAmount = (int) Math.ceil((double) amount / element.getSlots().length); } } } private void registerListeners() { if (listenersRegistered) { return; } plugin.getServer().getPluginManager().registerEvents(listener, plugin); listenersRegistered = true; } private void unregisterListeners() { listener.unregister(); listenersRegistered = false; } /** * Show this GUI to a player * @param player The Player to show the GUI to */ public void show(HumanEntity player) { show(player, true); } /** * Show this GUI to a player * @param player The Player to show the GUI to * @param checkOpen Whether or not it should check if this gui is already open */ public void show(HumanEntity player, boolean checkOpen) { draw(player); if (!checkOpen || !this.equals(getOpen(player))) { if (player.getOpenInventory().getType() != InventoryType.CRAFTING) { // If the player already has a gui open then we assume that the call was from that gui. // In order to not close it in a InventoryClickEvent listener (which will lead to errors) // we delay the opening for one tick to run after it finished processing the event plugin.getServer().getScheduler().runTask(plugin, () -> { Inventory inventory = getInventory(player); if (inventory != null) { addHistory(player, this); player.openInventory(inventory); } }); } else { Inventory inventory = getInventory(player); if (inventory != null) { clearHistory(player); addHistory(player, this); player.openInventory(inventory); } } } } /** * Build the gui */ public void build() { build(owner); } /** * Set the gui's owner and build it * @param owner The {@link InventoryHolder} that owns the gui */ public void build(InventoryHolder owner) { setOwner(owner); registerListeners(); calculatePageAmount(); } /** * Draw the elements in the inventory. This can be used to manually refresh the gui. */ public void draw() { for (UUID playerId : inventories.keySet()) { Player player = plugin.getServer().getPlayer(playerId); if (player != null) { draw(player); } } } /** * Draw the elements in the inventory. This can be used to manually refresh the gui. * @param who For who to draw the GUI */ public void draw(HumanEntity who) { Inventory inventory = getInventory(who); if (inventory == null) { build(); if (slots.length != inventoryType.getDefaultSize()) { inventory = plugin.getServer().createInventory(new Holder(this), slots.length, replaceVars(title)); } else { inventory = plugin.getServer().createInventory(new Holder(this), inventoryType, replaceVars(title)); } inventories.put(who != null ? who.getUniqueId() : null, inventory); } else { inventory.clear(); } for (int i = 0; i < inventory.getSize(); i++) { GuiElement element = getElement(i); if (element == null) { element = getFiller(); } if (element != null) { inventory.setItem(i, element.getItem(who, i)); } } } /** * Closes the GUI for everyone viewing it */ public void close() { close(true); } /** * Close the GUI for everyone viewing it * @param clearHistory Whether or not to close the GUI completely (by clearing the history) */ public void close(boolean clearHistory) { for (Inventory inventory : inventories.values()) { for (HumanEntity viewer : new ArrayList<>(inventory.getViewers())) { if (clearHistory) { clearHistory(viewer); } viewer.closeInventory(); } } } /** * Destroy this GUI. This unregisters all listeners and removes it from the GUI_MAP */ public void destroy() { destroy(true); } private void destroy(boolean closeInventories) { if (closeInventories) { close(); } for (Inventory inventory : inventories.values()) { inventory.clear(); } inventories.clear(); unregisterListeners(); removeFromMap(); } /** * Add a new history entry to the end of the history * @param player The player to add the history entry for * @param gui The GUI to add to the history */ public static void addHistory(HumanEntity player, InventoryGui gui) { GUI_HISTORY.putIfAbsent(player.getUniqueId(), new ArrayDeque<>()); Deque<InventoryGui> history = getHistory(player); if (history.peekLast() != gui) { history.add(gui); } } /** * Get the history of a player * @param player The player to get the history for * @return The history as a deque of InventoryGuis; * returns an empty one and not <tt>null</tt>! */ public static Deque<InventoryGui> getHistory(HumanEntity player) { return GUI_HISTORY.getOrDefault(player.getUniqueId(), new ArrayDeque<>()); } /** * Go back one entry in the history * @param player The player to show the previous gui to * @return <tt>true</tt> if there was a gui to show; <tt>false</tt> if not */ public static boolean goBack(HumanEntity player) { Deque<InventoryGui> history = getHistory(player); history.pollLast(); if (history.isEmpty()) { return false; } InventoryGui previous = history.peekLast(); if (previous != null) { previous.show(player, false); } return true; } /** * Clear the history of a player * @param player The player to clear the history for * @return The history */ public static Deque<InventoryGui> clearHistory(HumanEntity player) { if (GUI_HISTORY.containsKey(player.getUniqueId())) { return GUI_HISTORY.remove(player.getUniqueId()); } return new ArrayDeque<>(); } /** * Get element in a certain slot * @param slot The slot to get the element from * @return The GuiElement or <tt>null</tt> if the slot is empty/there wasn't one */ public GuiElement getElement(int slot) { return slot < 0 || slot >= slots.length ? null : elements.get(slots[slot]); } /** * Set the owner of this GUI. Will remove the previous assignment. * @param owner The owner of the GUI */ public void setOwner(InventoryHolder owner) { removeFromMap(); this.owner = owner; if (owner instanceof Entity) { GUI_MAP.put(((Entity) owner).getUniqueId().toString(), this); } else if (owner instanceof BlockState) { GUI_MAP.put(((BlockState) owner).getLocation().toString(), this); } } /** * Get the owner of this GUI. Will be null if th GUI doesn't have one * @return The InventoryHolder of this GUI */ public InventoryHolder getOwner() { return owner; } /** * Check whether or not the Owner of this GUI is real or fake * @return <tt>true</tt> if the owner is a real world InventoryHolder; <tt>false</tt> if it is null */ public boolean hasRealOwner() { return owner != null; } /** * Get the Action that is run when clicked outside of the inventory * @return The Action for when the player clicks outside the inventory; can be null */ public GuiElement.Action getOutsideAction() { return outsideAction; } /** * Set the Action that is run when clicked outside of the inventory * @param outsideAction The Action for when the player clicks outside the inventory; can be null */ public void setOutsideAction(GuiElement.Action outsideAction) { this.outsideAction = outsideAction; } /** * Get the action that is run when this GUI is closed * @return The action for when the player closes this inventory; can be null */ public CloseAction getCloseAction() { return closeAction; } /** * Set the action that is run when this GUI is closed; it should return true if the GUI should go back * @param closeAction The action for when the player closes this inventory; can be null */ public void setCloseAction(CloseAction closeAction) { this.closeAction = closeAction; } /** * Get whether or not this GUI should make a sound when interacting with elements that make sound * @return Whether or not to make a sound when interacted with */ public boolean isSilent() { return silent; } /** * Set whether or not this GUI should make a sound when interacting with elements that make sound * @param silent Whether or not to make a sound when interacted with */ public void setSilent(boolean silent) { this.silent = silent; } private void removeFromMap() { if (owner instanceof Entity) { GUI_MAP.remove(((Entity) owner).getUniqueId().toString(), this); } else if (owner instanceof BlockState) { GUI_MAP.remove(((BlockState) owner).getLocation().toString(), this); } } /** * Get the GUI registered to an InventoryHolder * @param holder The InventoryHolder to get the GUI for * @return The InventoryGui registered to it or <tt>null</tt> if none was registered to it */ public static InventoryGui get(InventoryHolder holder) { if (holder instanceof Entity) { return GUI_MAP.get(((Entity) holder).getUniqueId().toString()); } else if (holder instanceof BlockState) { return GUI_MAP.get(((BlockState) holder).getLocation().toString()); } return null; } /** * Get the GUI that a player has currently open * @param player The Player to get the GUI for * @return The InventoryGui that the player has open */ public static InventoryGui getOpen(HumanEntity player) { return getHistory(player).peekLast(); } /** * Get the title of the gui * @return The title of the gui */ public String getTitle() { return title; } /** * Set the title of the gui * @param title The {@link String} that should be the title of the gui */ public void setTitle(String title) { this.title = title; } /** * Play a click sound e.g. when an element acts as a button */ public void playClickSound() { if (isSilent()) return; for (Inventory inventory : inventories.values()) { for (HumanEntity humanEntity : inventory.getViewers()) { if (humanEntity instanceof Player) { ((Player) humanEntity).playSound(humanEntity.getEyeLocation(), CLICK_SOUND, 1, 1); } } } } /** * Get the inventory. Package scope as it should only be used by InventoryGui.Holder * @return The GUI's generated inventory */ Inventory getInventory() { return getInventory(null); } /** * Get the inventory of a certain player * @param who The player, if null it will try to return the inventory created first or null if none was created * @return The GUI's generated inventory, null if none was found */ private Inventory getInventory(HumanEntity who) { return who != null ? inventories.get(who.getUniqueId()) : (inventories.isEmpty() ? null : inventories.values().iterator().next()); } /** * All the listeners that InventoryGui needs to work */ public class GuiListener implements Listener { private final InventoryGui gui; public GuiListener(InventoryGui gui) { this.gui = gui; } @EventHandler private void onInventoryClick(InventoryClickEvent event) { if (event.getInventory().equals(getInventory(event.getWhoClicked()))) { int slot = -1; if (event.getRawSlot() < event.getView().getTopInventory().getSize()) { slot = event.getRawSlot(); } else if (event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY) { slot = event.getInventory().firstEmpty(); } GuiElement.Action action = null; GuiElement element = null; if (slot >= 0) { element = getElement(slot); if (element != null) { action = element.getAction(event.getWhoClicked()); } } else if (slot == -999) { action = outsideAction; } else { // Click was neither for the top inventory or outside // E.g. click is in the bottom inventory if (event.getAction() == InventoryAction.COLLECT_TO_CURSOR) { simulateCollectToCursor(new GuiElement.Click(gui, slot, null, event.getClick(), event)); } return; } try { if (action == null || action.onClick(new GuiElement.Click(gui, slot, element, event.getClick(), event))) { event.setCancelled(true); if (event.getWhoClicked() instanceof Player) { ((Player) event.getWhoClicked()).updateInventory(); } } if (action != null) { // Let's assume something changed and re-draw all currently shown inventories for (UUID playerId : inventories.keySet()) { if (!event.getWhoClicked().getUniqueId().equals(playerId)) { Player player = plugin.getServer().getPlayer(playerId); if (player != null) { draw(player); } } } } } catch (Throwable t) { event.setCancelled(true); if (event.getWhoClicked() instanceof Player) { ((Player) event.getWhoClicked()).updateInventory(); } plugin.getLogger().log(Level.SEVERE, "Exception while trying to run action for click on " + (element != null ? element.getClass().getSimpleName() : "empty element") + " in slot " + event.getRawSlot() + " of " + gui.getTitle() + " GUI!"); t.printStackTrace(); } } else if (hasRealOwner() && owner.equals(event.getInventory().getHolder())) { // Click into inventory by same owner but not the inventory of the GUI // Assume that the underlying inventory changed and redraw the GUI plugin.getServer().getScheduler().runTask(plugin, (Runnable) gui::draw); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onInventoryDrag(InventoryDragEvent event) { Inventory inventory = getInventory(event.getWhoClicked()); if (event.getInventory().equals(inventory)) { int rest = 0; Map<Integer, ItemStack> resetSlots = new HashMap<>(); for (Map.Entry<Integer, ItemStack> items : event.getNewItems().entrySet()) { if (items.getKey() < inventory.getSize()) { GuiElement element = getElement(items.getKey()); if (!(element instanceof GuiStorageElement) || !((GuiStorageElement) element).setStorageItem(items.getKey(), items.getValue())) { ItemStack slotItem = event.getInventory().getItem(items.getKey()); if (!items.getValue().isSimilar(slotItem)) { rest += items.getValue().getAmount(); } else if (slotItem != null) { rest += items.getValue().getAmount() - slotItem.getAmount(); } //items.getValue().setAmount(0); // can't change resulting items :/ resetSlots.put(items.getKey(), event.getInventory().getItem(items.getKey())); // reset them manually } } } plugin.getServer().getScheduler().runTask(plugin, () -> { for (Map.Entry<Integer, ItemStack> items : resetSlots.entrySet()) { event.getView().getTopInventory().setItem(items.getKey(), items.getValue()); } }); if (rest > 0) { int cursorAmount = event.getCursor() != null ? event.getCursor().getAmount() : 0; if (!event.getOldCursor().isSimilar(event.getCursor())) { event.setCursor(event.getOldCursor()); cursorAmount = 0; } int newCursorAmount = cursorAmount + rest; if (newCursorAmount <= event.getCursor().getMaxStackSize()) { event.getCursor().setAmount(newCursorAmount); } else { event.getCursor().setAmount(event.getCursor().getMaxStackSize()); ItemStack add = event.getCursor().clone(); int addAmount = newCursorAmount - event.getCursor().getMaxStackSize(); if (addAmount > 0) { add.setAmount(addAmount); for (ItemStack drop : event.getWhoClicked().getInventory().addItem(add).values()) { event.getWhoClicked().getLocation().getWorld().dropItem(event.getWhoClicked().getLocation(), drop); } } } } } } @EventHandler(priority = EventPriority.MONITOR) public void onInventoryClose(InventoryCloseEvent event) { Inventory inventory = getInventory(event.getPlayer()); if (event.getInventory().equals(inventory)) { // go back. that checks if the player is in gui and has history if (gui.equals(getOpen(event.getPlayer()))) { if (closeAction == null || closeAction.onClose(new Close(event.getPlayer(), gui, event))) { goBack(event.getPlayer()); } else { clearHistory(event.getPlayer()); } } if (inventories.size() <= 1) { destroy(false); } else { inventory.clear(); for (HumanEntity viewer : inventory.getViewers()) { if (viewer != event.getPlayer()) { viewer.closeInventory(); } } inventories.remove(event.getPlayer().getUniqueId()); } } } @EventHandler(priority = EventPriority.MONITOR) public void onInventoryMoveItem(InventoryMoveItemEvent event) { if (hasRealOwner() && (owner.equals(event.getDestination().getHolder()) || owner.equals(event.getSource().getHolder()))) { plugin.getServer().getScheduler().runTask(plugin, (Runnable) gui::draw); } } @EventHandler(priority = EventPriority.MONITOR) public void onDispense(BlockDispenseEvent event) { if (hasRealOwner() && owner.equals(event.getBlock().getState())) { plugin.getServer().getScheduler().runTask(plugin, (Runnable) gui::draw); } } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockBreak(BlockBreakEvent event) { if (hasRealOwner() && owner.equals(event.getBlock().getState())) { destroy(); } } @EventHandler(priority = EventPriority.MONITOR) public void onEntityDeath(EntityDeathEvent event) { if (hasRealOwner() && owner.equals(event.getEntity())) { destroy(); } } public void unregister() { InventoryClickEvent.getHandlerList().unregister(this); InventoryDragEvent.getHandlerList().unregister(this); InventoryCloseEvent.getHandlerList().unregister(this); InventoryMoveItemEvent.getHandlerList().unregister(this); BlockDispenseEvent.getHandlerList().unregister(this); BlockBreakEvent.getHandlerList().unregister(this); EntityDeathEvent.getHandlerList().unregister(this); } } /** * Fake InventoryHolder for the GUIs */ public static class Holder implements InventoryHolder { private InventoryGui gui; public Holder(InventoryGui gui) { this.gui = gui; } @Override public Inventory getInventory() { return gui.getInventory(); } public InventoryGui getGui() { return gui; } } public static interface CloseAction { /** * Executed when a player closes a GUI inventory * @param close The close object holding information about this close * @return Whether or not the close should go back or not */ boolean onClose(Close close); } public static class Close { private final HumanEntity player; private final InventoryGui gui; private final InventoryCloseEvent event; public Close(HumanEntity player, InventoryGui gui, InventoryCloseEvent event) { this.player = player; this.gui = gui; this.event = event; } public HumanEntity getPlayer() { return player; } public InventoryGui getGui() { return gui; } public InventoryCloseEvent getEvent() { return event; } } /** * Set the text of an item using the display name and the lore. * Also replaces any placeholders in the text and filters out empty lines. * Use a single space to create an emtpy line. * @param item The {@link ItemStack} to set the text for * @param text The text lines to set */ public void setItemText(ItemStack item, String... text) { if (item != null && text != null && text.length > 0) { ItemMeta meta = item.getItemMeta(); if (meta != null) { String combined = replaceVars(Arrays.stream(text) .filter(Objects::nonNull) .filter(s -> !s.isEmpty()) .collect(Collectors.joining("\n"))); String[] lines = combined.split("\n"); meta.setDisplayName(lines[0]); if (lines.length > 1) { meta.setLore(Arrays.asList(Arrays.copyOfRange(lines, 1, lines.length))); } else { meta.setLore(null); } item.setItemMeta(meta); } } } /** * Replace some placeholders in the with values regarding the gui's state. Replaced color codes.<br> * The placeholders are:<br> * <tt>%plugin%</tt> - The name of the plugin that this gui is from.<br> * <tt>%owner%</tt> - The name of the owner of this gui. Will be an empty string when the owner is null.<br> * <tt>%title%</tt> - The title of this GUI.<br> * <tt>%page%</tt> - The current page that this gui is on.<br> * <tt>%nextpage%</tt> - The next page. "none" if there is no next page.<br> * <tt>%prevpage%</tt> - The previous page. "none" if there is no previous page.<br> * <tt>%pages%</tt> - The amount of pages that this gui has. * @param text The text to replace the placeholders in * @param replacements Additional repplacements. i = placeholder, i+1 = replacements * @return The text with all placeholders replaced */ public String replaceVars(String text, String... replacements) { text = replace(replace(text, replacements), "plugin", plugin.getName(), "owner", owner instanceof Nameable ? ((Nameable) owner).getCustomName() : "", "title", title, "page", String.valueOf(getPageNumber() + 1), "nextpage", getPageNumber() + 1 < getPageAmount() ? String.valueOf(getPageNumber() + 2) : "none", "prevpage", getPageNumber() > 0 ? String.valueOf(getPageNumber()) : "none", "pages", String.valueOf(getPageAmount()) ); return ChatColor.translateAlternateColorCodes('&', text); } /** * Replace placeholders in a string * @param string The string to replace in * @param replacements What to replace the placeholders with. The n-th index is the placeholder, the n+1-th the value. * @return The string with all placeholders replaced (using the configured placeholder prefix and suffix) */ private String replace(String string, String... replacements) { for (int i = 0; i + 1 < replacements.length; i+=2) { if (replacements[i] == null) { continue; } String placeholder = "%" + replacements[i] + "%"; Pattern pattern = PATTERN_CACHE.get(placeholder); if (pattern == null) { PATTERN_CACHE.put(placeholder, pattern = Pattern.compile(placeholder, Pattern.LITERAL)); } string = pattern.matcher(string).replaceAll(Matcher.quoteReplacement(replacements[i+1] != null ? replacements[i+1] : "null")); } return string; } /** * Simulate the collecting to the cursor while respecting elements that can't be modified * @param click The click that startet it all */ void simulateCollectToCursor(GuiElement.Click click) { ItemStack newCursor = click.getEvent().getCursor().clone(); boolean itemInGui = false; for (int i = 0; i < click.getEvent().getView().getTopInventory().getSize(); i++) { if (i != click.getEvent().getRawSlot()) { ItemStack viewItem = click.getEvent().getView().getTopInventory().getItem(i); if (newCursor.isSimilar(viewItem)) { itemInGui = true; } GuiElement element = getElement(i); if (element instanceof GuiStorageElement) { GuiStorageElement storageElement = (GuiStorageElement) element; ItemStack otherStorageItem = storageElement.getStorageItem(i); if (addToStack(newCursor, otherStorageItem)) { if (otherStorageItem.getAmount() == 0) { otherStorageItem = null; } storageElement.setStorageItem(i, otherStorageItem); if (newCursor.getAmount() == newCursor.getMaxStackSize()) { break; } } } } } if (itemInGui) { click.getEvent().setCurrentItem(null); click.getEvent().setCancelled(true); if (click.getEvent().getWhoClicked() instanceof Player) { ((Player) click.getEvent().getWhoClicked()).updateInventory(); } if (click.getElement() instanceof GuiStorageElement) { ((GuiStorageElement) click.getElement()).setStorageItem(click.getSlot(), null); } if (newCursor.getAmount() < newCursor.getMaxStackSize()) { Inventory bottomInventory = click.getEvent().getView().getBottomInventory(); for (ItemStack bottomIem : bottomInventory) { if (addToStack(newCursor, bottomIem)) { if (newCursor.getAmount() == newCursor.getMaxStackSize()) { break; } } } } click.getEvent().setCursor(newCursor); draw(); } } /** * Add items to a stack up to the max stack size * @param item The base item * @param add The item stack to add * @return <tt>true</tt> if the stack is finished; <tt>false</tt> if these stacks can't be merged */ private static boolean addToStack(ItemStack item, ItemStack add) { if (item.isSimilar(add)) { int newAmount = item.getAmount() + add.getAmount(); if (newAmount >= item.getMaxStackSize()) { item.setAmount(item.getMaxStackSize()); add.setAmount(newAmount - item.getAmount()); } else { item.setAmount(newAmount); add.setAmount(0); } return true; } return false; } }