diff --git a/src/main/java/de/studiocode/invui/gui/SlotElement.java b/src/main/java/de/studiocode/invui/gui/SlotElement.java index 06938ba..a1cbbb1 100644 --- a/src/main/java/de/studiocode/invui/gui/SlotElement.java +++ b/src/main/java/de/studiocode/invui/gui/SlotElement.java @@ -31,7 +31,7 @@ public interface SlotElement { public ItemStack getItemStack(UUID viewerUUID) { return item.getItemBuilder().buildFor(viewerUUID); } - + @Override public SlotElement getHoldingElement() { return this; @@ -45,30 +45,30 @@ public interface SlotElement { class VISlotElement implements SlotElement { private final VirtualInventory virtualInventory; - private final int index; + private final int slot; - public VISlotElement(VirtualInventory virtualInventory, int index) { + public VISlotElement(VirtualInventory virtualInventory, int slot) { this.virtualInventory = virtualInventory; - this.index = index; + this.slot = slot; } public VirtualInventory getVirtualInventory() { return virtualInventory; } - public int getIndex() { - return index; + public int getSlot() { + return slot; } public ItemStack getItemStack() { - return virtualInventory.getItemStack(index); + return virtualInventory.getUnsafeItemStack(slot); } @Override public ItemStack getItemStack(UUID viewerUUID) { return getItemStack(); } - + @Override public SlotElement getHoldingElement() { return this; @@ -107,7 +107,7 @@ public interface SlotElement { else return below; } } - + @Override public ItemStack getItemStack(UUID viewerUUID) { return getHoldingElement().getItemStack(viewerUUID); diff --git a/src/main/java/de/studiocode/invui/gui/impl/IndexedGUI.java b/src/main/java/de/studiocode/invui/gui/impl/IndexedGUI.java index 74da14e..dd3c70e 100644 --- a/src/main/java/de/studiocode/invui/gui/impl/IndexedGUI.java +++ b/src/main/java/de/studiocode/invui/gui/impl/IndexedGUI.java @@ -19,6 +19,7 @@ import de.studiocode.invui.window.Window; import de.studiocode.invui.window.WindowManager; import de.studiocode.invui.window.impl.merged.MergedWindow; import de.studiocode.invui.window.impl.merged.split.SplitWindow; +import de.studiocode.invui.window.impl.single.SingleWindow; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryClickEvent; @@ -67,18 +68,19 @@ abstract class IndexedGUI implements GUI { } else event.setCancelled(true); // Only VISlotElements have allowed interactions } + @SuppressWarnings("deprecation") private void handleVISlotElementClick(VISlotElement element, InventoryClickEvent event) { VirtualInventory virtualInventory = element.getVirtualInventory(); - int index = element.getIndex(); + int slot = element.getSlot(); Player player = (Player) event.getWhoClicked(); ItemStack cursor = event.getCursor(); ItemStack clicked = event.getCurrentItem(); - + UpdateReason updateReason = new PlayerUpdateReason(player, event); - if (virtualInventory.isSynced(index, clicked)) { - boolean cancelled = false; + if (virtualInventory.isSynced(slot, clicked)) { + boolean cancel = false; switch (event.getAction()) { @@ -90,82 +92,126 @@ abstract class IndexedGUI implements GUI { case DROP_ONE_SLOT: case PICKUP_ONE: - cancelled = virtualInventory.removeOne(updateReason, index); + cancel = virtualInventory.changeItemAmount(updateReason, slot, -1) != -1; break; case DROP_ALL_SLOT: case PICKUP_ALL: - cancelled = virtualInventory.removeItem(updateReason, index); + cancel = !virtualInventory.setItemStack(updateReason, slot, null); + // set null break; case PICKUP_HALF: - cancelled = virtualInventory.removeHalf(updateReason, index); - break; - - case PLACE_ALL: - cancelled = virtualInventory.place(updateReason, index, cursor); - break; - - case PLACE_ONE: - cancelled = virtualInventory.placeOne(updateReason, index, cursor); - break; - - case PLACE_SOME: - cancelled = virtualInventory.setToMaxAmount(updateReason, index); - break; - - case SWAP_WITH_CURSOR: - cancelled = virtualInventory.setItemStack(updateReason, index, event.getCursor()); - break; + int amount = virtualInventory.getAmount(slot); + int halfAmount = amount / 2; + int newAmount = virtualInventory.changeItemAmount(updateReason, slot, halfAmount); - case COLLECT_TO_CURSOR: - cancelled = true; - ItemStack newCursor = cursor.clone(); - newCursor.setAmount(virtualInventory.collectToCursor(updateReason, newCursor)); - player.setItemOnCursor(newCursor); - break; - - case MOVE_TO_OTHER_INVENTORY: - cancelled = true; - Window window = WindowManager.getInstance().findOpenWindow(player).orElse(null); - - ItemStack invStack = virtualInventory.getItemStack(index); - ItemUpdateEvent updateEvent = virtualInventory.createAndCallEvent(index, updateReason, invStack, null); - - if (!updateEvent.isCancelled()) { - int leftOverAmount; - if (window instanceof MergedWindow) { - GUI otherGui; - if (window instanceof SplitWindow) { - SplitWindow splitWindow = (SplitWindow) window; - GUI[] guis = splitWindow.getGuis(); - otherGui = guis[0] == this ? guis[1] : guis[0]; - } else { - otherGui = this; - } - - leftOverAmount = ((IndexedGUI) otherGui).putIntoVirtualInventories(updateReason, invStack, virtualInventory); - } else { - leftOverAmount = 0; - HashMap leftover = event.getWhoClicked().getInventory().addItem(virtualInventory.getItemStack(index)); - if (!leftover.isEmpty()) leftOverAmount = leftover.get(0).getAmount(); + // amount did not change as predicted + if (newAmount != halfAmount) { + cancel = true; + + // action wasn't completely cancelled + if (newAmount != amount) { + int cursorAmount = amount - newAmount; + cancel = true; + ItemStack newCursorStack = clicked.clone(); + newCursorStack.setAmount(cursorAmount); + event.setCursor(newCursorStack); } - virtualInventory.setAmountSilently(index, leftOverAmount); } break; + case PLACE_SOME: + case PLACE_ALL: + int amountLeft = virtualInventory.putItemStack(updateReason, slot, cursor); + if (amountLeft > 0) { + cancel = true; + if (amountLeft != cursor.getAmount()) + cursor.setAmount(amountLeft); + } + break; + + case PLACE_ONE: + ItemStack itemStack = cursor.clone(); + itemStack.setAmount(1); + cancel = virtualInventory.putItemStack(updateReason, slot, itemStack) != 0; + break; + + case SWAP_WITH_CURSOR: + cancel = !virtualInventory.setItemStack(updateReason, slot, event.getCursor()); + break; + + case COLLECT_TO_CURSOR: + cancel = true; + ItemStack newCursorStack = cursor.clone(); + newCursorStack.setAmount(virtualInventory.collectToCursor(updateReason, newCursorStack)); + event.setCursor(newCursorStack); + break; + + case MOVE_TO_OTHER_INVENTORY: + cancel = true; + handleMoveToOtherInventory(player, event, virtualInventory, slot, updateReason); + break; + + case HOTBAR_MOVE_AND_READD: + case HOTBAR_SWAP: + cancel = handleHotbarSwap(player, event, virtualInventory, slot, updateReason); + break; + default: - // TODO: Hotbar swap // action not supported - cancelled = true; + cancel = true; break; } - if (cancelled) event.setCancelled(true); + event.setCancelled(cancel); } else event.setCancelled(true); } + private void handleMoveToOtherInventory(Player player, InventoryClickEvent event, VirtualInventory inventory, int slot, UpdateReason reason) { + Window window = WindowManager.getInstance().findOpenWindow(player).orElse(null); + + ItemStack invStack = inventory.getItemStack(slot); + ItemUpdateEvent updateEvent = inventory.callUpdateEvent(reason, slot, invStack, null); + + if (!updateEvent.isCancelled()) { + int leftOverAmount; + if (window instanceof MergedWindow) { + GUI otherGui; + if (window instanceof SplitWindow) { + SplitWindow splitWindow = (SplitWindow) window; + GUI[] guis = splitWindow.getGuis(); + otherGui = guis[0] == this ? guis[1] : guis[0]; + } else { + otherGui = this; + } + + leftOverAmount = ((IndexedGUI) otherGui).putIntoVirtualInventories(reason, invStack, inventory); + } else { + leftOverAmount = 0; + HashMap leftover = event.getWhoClicked().getInventory().addItem(inventory.getItemStack(slot)); + if (!leftover.isEmpty()) leftOverAmount = leftover.get(0).getAmount(); + } + + invStack.setAmount(leftOverAmount); + inventory.setItemStackSilently(slot, invStack); + } + } + + private boolean handleHotbarSwap(Player player, InventoryClickEvent event, VirtualInventory inventory, int slot, UpdateReason reason) { + Window window = WindowManager.getInstance().findOpenWindow(player).orElse(null); + if (window instanceof SingleWindow) { + int hotbarButton = event.getHotbarButton(); + ItemStack hotbarItem = player.getInventory().getItem(hotbarButton); + if (hotbarItem != null) hotbarItem = hotbarItem.clone(); + + return !inventory.setItemStack(reason, slot, hotbarItem); + } // TODO: add support for merged windows + + return true; + } + @Override public boolean handleItemDrag(UpdateReason updateReason, int slot, ItemStack oldStack, ItemStack newStack) { SlotElement element = getSlotElement(slot); @@ -173,13 +219,13 @@ abstract class IndexedGUI implements GUI { if (element instanceof VISlotElement) { VISlotElement viSlotElement = ((VISlotElement) element); VirtualInventory virtualInventory = viSlotElement.getVirtualInventory(); - int viIndex = viSlotElement.getIndex(); - if (virtualInventory.isSynced(viIndex, oldStack)) { - return virtualInventory.setItemStack(updateReason, viIndex, newStack); + int viSlot = viSlotElement.getSlot(); + if (virtualInventory.isSynced(viSlot, oldStack)) { + return virtualInventory.setItemStack(updateReason, viSlot, newStack); } } - return true; + return false; } @Override diff --git a/src/main/java/de/studiocode/invui/virtualinventory/VirtualInventory.java b/src/main/java/de/studiocode/invui/virtualinventory/VirtualInventory.java index 1ea9a98..4eb66ef 100644 --- a/src/main/java/de/studiocode/invui/virtualinventory/VirtualInventory.java +++ b/src/main/java/de/studiocode/invui/virtualinventory/VirtualInventory.java @@ -6,6 +6,7 @@ import de.studiocode.invui.virtualinventory.event.ItemUpdateEvent; import de.studiocode.invui.virtualinventory.event.UpdateReason; import de.studiocode.invui.window.Window; import org.bukkit.Bukkit; +import org.bukkit.Material; import org.bukkit.configuration.serialization.ConfigurationSerializable; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; @@ -15,44 +16,43 @@ import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.function.Consumer; -// TODO: clean up +import static java.lang.Math.min; + public class VirtualInventory implements ConfigurationSerializable { - private final Set windows = new HashSet<>(); - private final UUID uuid; private int size; private ItemStack[] items; + private int[] stackSizes; + private final Set windows = new HashSet<>(); private Consumer itemUpdateHandler; /** - * Creates a new {@link VirtualInventory}. + * Constructs a new {@link VirtualInventory} * - * @param uuid The {@link UUID} this {@link VirtualInventory} should have. - * Can be null, only used for serialization. - * @param size The size of the {@link VirtualInventory} - * @param items An array of {@link ItemStack} which reflects the contents of this - * {@link VirtualInventory}, therefore the length of that array has - * to be the same as size. + * @param uuid The {@link UUID} of this {@link VirtualInventory}. Can be null, only used for serialization. + * @param size The amount of slots this {@link VirtualInventory} has. + * @param items A predefined array of content. Can be null. Will not get copied! + * @param stackSizes An array of maximum allowed stack sizes for the each slot in the {@link VirtualInventory}. */ - public VirtualInventory(@Nullable UUID uuid, int size, @NotNull ItemStack[] items) { - if (size < 1) throw new IllegalArgumentException("size cannot be smaller than 1"); - if (items.length != size) throw new IllegalArgumentException("items length has to be the same as size"); - + public VirtualInventory(@Nullable UUID uuid, int size, @Nullable ItemStack[] items, int[] stackSizes) { this.uuid = uuid; this.size = size; - this.items = items; + this.items = items == null ? new ItemStack[size] : items; + if (stackSizes == null) { + this.stackSizes = new int[size]; + Arrays.fill(this.stackSizes, 64); + } else this.stackSizes = stackSizes; } /** - * Creates a new {@link VirtualInventory}. + * Constructs a new {@link VirtualInventory} * - * @param uuid The {@link UUID} this {@link VirtualInventory} should have. - * Can be null, only used for serialization. - * @param size The size of the {@link VirtualInventory} + * @param uuid The {@link UUID} of this {@link VirtualInventory}. Can be null, only used for serialization. + * @param size The amount of slots this {@link VirtualInventory} has. */ public VirtualInventory(@Nullable UUID uuid, int size) { - this(uuid, size, new ItemStack[size]); + this(uuid, size, null, null); } /** @@ -63,281 +63,342 @@ public class VirtualInventory implements ConfigurationSerializable { */ public static VirtualInventory deserialize(@NotNull Map args) { //noinspection unchecked - return new VirtualInventory(UUID.fromString((String) args.get("uuid")), - (int) args.get("size"), ((ArrayList) args.get("items")).toArray(new ItemStack[0])); + return new VirtualInventory( + UUID.fromString((String) args.get("uuid")), + (int) args.get("size"), + ((ArrayList) args.get("items")).toArray(new ItemStack[0]), + ((ArrayList) args.get("stackSizes")).stream().mapToInt(Integer::intValue).toArray() + ); + } + + /** + * Serializes this {@link VirtualInventory} to a {@link Map} + * + * @return A {@link Map} that contains the serialized data of this {@link VirtualInventory} + */ + @NotNull + @Override + public Map serialize() { + Map result = new LinkedHashMap<>(); + result.put("uuid", uuid.toString()); + result.put("size", size); + result.put("stackSizes", stackSizes); + result.put("items", items); + return result; + } + + /** + * Gets a set of {@link Window}s that display this {@link VirtualInventory}. + * + * @return An unmodifiable view of the set that contains all {@link Window}s that display + * content of this {@link VirtualInventory}. + */ + public Set getWindows() { + return Collections.unmodifiableSet(windows); + } + + /** + * Adds a {@link Window} to the set of {@link Window}s, telling the {@link VirtualInventory} that + * its contents are now being displayed in that {@link Window}. + * + * @param window The {@link Window} to be added. + */ + public void addWindow(Window window) { + windows.add(window); + } + + /** + * Removes a {@link Window} from the set of {@link Window}s, telling the {@link VirtualInventory} that + * its contents are no longer being displayed in that {@link Window}. + * + * @param window The {@link Window} to be removed. + */ + public void removeWindow(Window window) { + windows.remove(window); + } + + /** + * Notifies all {@link Window}s displaying this {@link VirtualInventory} to update their + * representative {@link ItemStack}s. + * This method should only be called manually in very specific cases like when the + * {@link ItemMeta} of an {@link ItemStack} in this inventory has changed. + */ + public void notifyWindows() { + Bukkit.getScheduler().runTask(InvUI.getInstance().getPlugin(), () -> + windows.forEach(window -> window.handleVirtualInventoryUpdate(this))); + } + + /** + * Changes the size of the {@link VirtualInventory}. + * {@link ItemStack}s in slots which are no longer valid will be removed from the {@link VirtualInventory}. + * This method does not call an event. + * + * @param size The new size of the {@link VirtualInventory} + */ + public void resize(int size) { + this.size = size; + this.items = Arrays.copyOf(items, size); + this.stackSizes = Arrays.copyOf(stackSizes, size); + } + + /** + * Sets a handler which is called every time something gets updated in the {@link VirtualInventory}. + * + * @param itemUpdateHandler The new item update handler + */ + public void setItemUpdateHandler(Consumer itemUpdateHandler) { + this.itemUpdateHandler = itemUpdateHandler; + } + + /** + * Gets the {@link UUID} of this {@link VirtualInventory}. + * + * @return The {@link UUID} + */ + public UUID getUuid() { + return uuid; } /** * Gets the size of this {@link VirtualInventory}. * - * @return The size of this {@link VirtualInventory} + * @return How many slots this {@link VirtualInventory} has. */ public int getSize() { return size; } /** - * Gets a deep copy of the {@link ItemStack}s in this {@link VirtualInventory} + * Gets a copy of the contents of this {@link VirtualInventory}. * - * @return A copy of the {@link ItemStack}s in this {@link VirtualInventory} + * @return A deep copy of the {@link ItemStack}s this {@link VirtualInventory} contains. */ public ItemStack[] getItems() { - return Arrays.stream(items) - .map(itemStack -> itemStack != null ? itemStack.clone() : null) - .toArray(ItemStack[]::new); + return Arrays.stream(items).map(item -> item != null ? item.clone() : null).toArray(ItemStack[]::new); } /** - * Changes the size of this {@link VirtualInventory}, removing - * existing {@link ItemStack}s reduced. + * Gets a clone of the {@link ItemStack} on that slot. * - * @param size The new size of this {@link VirtualInventory} + * @param slot The slot + * @return The {@link ItemStack} on the given slot */ - public void resize(int size) { - this.size = size; - this.items = Arrays.copyOf(items, size); + public ItemStack getItemStack(int slot) { + ItemStack itemStack = items[slot]; + return itemStack != null ? itemStack.clone() : null; } /** - * Checks if the {@link ItemStack} on that slot index is the same + * Returns the actual {@link ItemStack} on that slot. + *
+ * Not a clone, should be handled carefully as changes done on that item will not call any + * Window updates (and create inconsistency between server and client), + * in which case a manual call of {@link #notifyWindows} is needed. + *
+ * Modifying this {@link ItemStack} will not call an {@link ItemUpdateEvent}. + * + * @param slot The slot + * @return The actual {@link ItemStack} on that slot + */ + public ItemStack getUnsafeItemStack(int slot) { + return items[slot]; + } + + /** + * Gets the amount of items on a slot. + * + * @param slot The slot + * @return The amount of items on that slot + */ + public int getAmount(int slot) { + ItemStack currentStack = items[slot]; + return currentStack != null ? currentStack.getAmount() : 0; + } + + /** + * Gets the maximum stack size for a specific slot. If there is an {@link ItemStack} on that + * slot, the returned value will be the minimum of both the slot limit and {@link Material#getMaxStackSize()}. + * + * @param slot The slot + * @param alternative The alternative maximum stack size if no {@link ItemStack} is placed on that slot. + * Should probably be the max stack size of the {@link Material} that will be added. + * @return The current maximum allowed stack size on the specific slot. + */ + public int getMaxStackSize(int slot, int alternative) { + ItemStack currentItem = items[slot]; + int slotMaxStackSize = stackSizes == null ? 64 : stackSizes[slot]; + if (alternative != -1) + return min(currentItem != null ? currentItem.getMaxStackSize() : alternative, slotMaxStackSize); + else return slotMaxStackSize; + } + + /** + * Sets all the maximum allowed stack sizes + * + * @param maxStackSizes All max stack sizes + */ + public void setMaxStackSizes(int[] maxStackSizes) { + this.stackSizes = maxStackSizes; + } + + /** + * Sets the maximum allowed stack size on a specific slot. + * + * @param slot The slot + * @param maxStackSize The max stack size + */ + public void setMaxStackSize(int slot, int maxStackSize) { + stackSizes[slot] = maxStackSize; + } + + /** + * Creates an {@link ItemUpdateEvent} and calls the {@link #itemUpdateHandler} to handle it. + * + * @param updateReason The {@link UpdateReason} + * @param slot The slot of the affected {@link ItemStack} + * @param previousItemStack The {@link ItemStack} that was previously on that slot + * @param newItemStack The {@link ItemStack} that will be on that slot + * @return The {@link ItemUpdateEvent} after it has been handled by the {@link #itemUpdateHandler} + */ + public ItemUpdateEvent callUpdateEvent(@Nullable UpdateReason updateReason, int slot, @Nullable ItemStack previousItemStack, @Nullable ItemStack newItemStack) { + ItemUpdateEvent event = new ItemUpdateEvent(this, slot, updateReason, previousItemStack, newItemStack); + if (itemUpdateHandler != null) itemUpdateHandler.accept(event); + return event; + } + + /** + * Checks if the {@link ItemStack} on that slot is the same * as the assumed {@link ItemStack} provided as parameter. * - * @param index The slot index + * @param slot The slot * @param assumedStack The assumed {@link ItemStack} * @return If the {@link ItemStack} on that slot is the same as the assumed {@link ItemStack} */ - public boolean isSynced(int index, ItemStack assumedStack) { - ItemStack actualStack = items[index]; + public boolean isSynced(int slot, ItemStack assumedStack) { + ItemStack actualStack = items[slot]; return (actualStack == null && assumedStack == null) || (actualStack != null && actualStack.equals(assumedStack)); } /** - * Sets an {@link ItemStack} on a specific slot. + * Changes the {@link ItemStack} on a specific slot to that one, regardless of what was + * previously on that slot. + *
+ * This method does not call an {@link ItemUpdateEvent} and ignores the maximum allowed stack size of + * both the {@link Material} and the slot. + *
+ * This method will always be successful. * - * @param updateReason The reason for item update, can be null. - * @param index The slot index - * @param itemStack The {@link ItemStack} that should be put on that slot - * @return If the action has been cancelled + * @param slot The slot + * @param itemStack The {@link ItemStack} to set. */ - public boolean setItemStack(@Nullable UpdateReason updateReason, int index, ItemStack itemStack) { - ItemStack newStack = itemStack.clone(); - ItemUpdateEvent event = createAndCallEvent(index, updateReason, items[index], newStack); + public void setItemStackSilently(int slot, @Nullable ItemStack itemStack) { + if (itemStack != null && itemStack.getAmount() == 0) items[slot] = null; + else items[slot] = itemStack; + notifyWindows(); + } + + /** + * Changes the {@link ItemStack} on a specific slot to that one, regardless of what was + * previously on that slot. + *
+ * This method ignores the maximum allowed stack size of both the {@link Material} and the slot. + * + * @param updateReason The reason used in the {@link ItemUpdateEvent}. + * @param slot The slot + * @param itemStack The {@link ItemStack} to set. + * @return If the action was successful + */ + public boolean forceSetItemStack(@Nullable UpdateReason updateReason, int slot, @Nullable ItemStack itemStack) { + ItemUpdateEvent event = callUpdateEvent(updateReason, slot, items[slot], itemStack); if (!event.isCancelled()) { - items[index] = newStack; - notifyWindows(); - - return false; + setItemStackSilently(slot, event.getNewItemStack()); + return true; } - - return true; + return false; } /** - * Gets the {@link ItemStack} on a specific slot. + * Changes the {@link ItemStack} on a specific slot to the given one, regardless of what previously was on + * that slot. + *
+ * This method will fail if the given {@link ItemStack} does not completely fit inside because of the + * maximum allowed stack size. * - * @param index The slot index - * @return The {@link ItemStack} on that slot + * @param updateReason The reason used in the {@link ItemUpdateEvent}. + * @param slot The slot + * @param itemStack The {@link ItemStack} to set. + * @return If the action was successful */ - public ItemStack getItemStack(int index) { - return items[index]; + public boolean setItemStack(@Nullable UpdateReason updateReason, int slot, @Nullable ItemStack itemStack) { + int maxStackSize = getMaxStackSize(slot, itemStack != null ? itemStack.getMaxStackSize() : -1); + if (itemStack != null && itemStack.getAmount() > maxStackSize) return false; + return forceSetItemStack(updateReason, slot, itemStack); } /** - * Checks if there is an {@link ItemStack} on a specific slot. + * Adds an {@link ItemStack} on a specific slot. * - * @param index The slot index - * @return If there is an {@link ItemStack} on that slot + * @param updateReason The reason used in the {@link ItemUpdateEvent}. + * @param slot The slot + * @param itemStack The {@link ItemStack} to add. + * @return The amount of items that did not fit on that slot. */ - public boolean hasItemStack(int index) { - return items[index] != null; - } - - /** - * Sets an {@link ItemStack} on a specific slot or adds the amount - * if there already is an {@link ItemStack} on that slot. - * - * @param updateReason The reason for item update, can be null. - * @param index The slot index - * @param itemStack The {@link ItemStack} to place - * @return If the action has been cancelled - */ - public boolean place(@Nullable UpdateReason updateReason, int index, ItemStack itemStack) { - ItemStack currentStack = items[index]; - - ItemStack newStack; - if (currentStack == null) { - newStack = itemStack.clone(); - } else { - newStack = currentStack.clone(); - newStack.setAmount(newStack.getAmount() + itemStack.getAmount()); - } - - ItemUpdateEvent event = createAndCallEvent(index, updateReason, currentStack, newStack); - if (!event.isCancelled()) { - items[index] = newStack; - notifyWindows(); - - return false; - } - - return true; - } - - /** - * Puts on of an {@link ItemStack} on a specific slots or adds one - * if there is already an {@link ItemStack} on that slot. - * - * @param updateReason The reason for item update, can be null. - * @param index The slot index - * @param itemStack The {@link ItemStack} to place one of - * @return If the action has been cancelled - */ - public boolean placeOne(@Nullable UpdateReason updateReason, int index, ItemStack itemStack) { - ItemStack currentStack = items[index]; - - ItemStack newStack; - if (currentStack == null) { - newStack = itemStack.clone(); - newStack.setAmount(1); - } else { - newStack = currentStack.clone(); - newStack.setAmount(newStack.getAmount() + 1); - } - - ItemUpdateEvent event = createAndCallEvent(index, updateReason, currentStack, newStack); - if (!event.isCancelled()) { - items[index] = newStack; - notifyWindows(); - - return false; - } - - return true; - } - - /** - * Changes the amount of an {@link ItemStack} on a specific slot without calling the {@link ItemUpdateEvent} - * - * @param index The slot index - * @param amount The new amount - */ - public void setAmountSilently(int index, int amount) { - ItemStack currentStack = items[index]; - if (currentStack != null) { - if (amount == 0) items[index] = null; - else currentStack.setAmount(amount); - notifyWindows(); - } - } - - /** - * Changes the amount of an {@link ItemStack} on a specific slot - * to the {@link ItemStack}'s {@link ItemStack#getMaxStackSize()}. - * - * @param updateReason The reason for item update, can be null. - * @param index The slot index - * @return If the action has been cancelled - */ - public boolean setToMaxAmount(@Nullable UpdateReason updateReason, int index) { - ItemStack currentStack = items[index]; - if (currentStack != null) { - ItemStack newStack = currentStack.clone(); - newStack.setAmount(newStack.getMaxStackSize()); - - ItemUpdateEvent event = createAndCallEvent(index, updateReason, currentStack, newStack); - if (!event.isCancelled()) { - items[index] = newStack; - notifyWindows(); + public int putItemStack(@Nullable UpdateReason updateReason, int slot, @NotNull ItemStack itemStack) { + ItemStack currentStack = items[slot]; + if (currentStack == null || currentStack.isSimilar(itemStack)) { + int currentAmount = currentStack == null ? 0 : currentStack.getAmount(); + int maxStackSize = getMaxStackSize(slot, itemStack.getMaxStackSize()); + if (currentAmount < maxStackSize) { + ItemStack newItemStack = itemStack.clone(); + newItemStack.setAmount(min(currentAmount + itemStack.getAmount(), maxStackSize)); - return false; + ItemUpdateEvent event = callUpdateEvent(updateReason, slot, currentStack, newItemStack); + if (!event.isCancelled()) { + newItemStack = event.getNewItemStack(); + items[slot] = newItemStack; + notifyWindows(); + + return itemStack.getAmount() - (newItemStack.getAmount() - currentAmount); + } } } - return true; + return itemStack.getAmount(); } /** - * Removes an {@link ItemStack} on a specific slot from - * the {@link VirtualInventory}. + * Changes the amount of an {@link ItemStack} on a slot to the given value. * - * @param updateReason The reason for item update, can be null. - * @param index The slot index - * @return If the action has been cancelled + * @param updateReason The reason used in the {@link ItemUpdateEvent}. + * @param slot The slot + * @param amount The amount to change to. + * @return The amount that it actually changed to. + * @throws IllegalStateException If there is no ItemStack on that slot. */ - public boolean removeItem(@Nullable UpdateReason updateReason, int index) { - ItemStack currentStack = items[index]; - if (currentStack != null) { - - ItemUpdateEvent event = createAndCallEvent(index, updateReason, currentStack, null); - if (!event.isCancelled()) { - items[index] = null; - notifyWindows(); - - return false; - } + public int changeItemAmount(@Nullable UpdateReason updateReason, int slot, int amount) { + ItemStack currentStack = items[slot]; + if (currentStack == null) throw new IllegalStateException("There is currently no ItemStack on that slot"); + int maxStackSize = getMaxStackSize(slot, -1); + + ItemStack newItemStack; + if (amount != 0) { + newItemStack = currentStack.clone(); + newItemStack.setAmount(min(amount, maxStackSize)); + } else { + newItemStack = null; } - return true; - } - - /** - * Removes one from an {@link ItemStack} on a specific slot. - * - * @param updateReason The reason for item update, can be null. - * @param index The slot index - * @return If the action has been cancelled - */ - public boolean removeOne(@Nullable UpdateReason updateReason, int index) { - ItemStack currentStack = items[index]; - if (currentStack != null) { - int newAmount = currentStack.getAmount() - 1; + ItemUpdateEvent event = callUpdateEvent(updateReason, slot, currentStack, newItemStack); + if (!event.isCancelled()) { + newItemStack = event.getNewItemStack(); + items[slot] = newItemStack; + notifyWindows(); - if (newAmount > 0) { - ItemStack newStack = currentStack.clone(); - newStack.setAmount(newAmount); - - ItemUpdateEvent event = createAndCallEvent(index, updateReason, currentStack, newStack); - if (!event.isCancelled()) { - items[index] = newStack; - notifyWindows(); - - return false; - } - } else return removeItem(updateReason, index); + return newItemStack != null ? newItemStack.getAmount() : 0; } - return true; - } - - /** - * Removes half of the {@link ItemStack} on a specific slot. - * - * @param updateReason The reason for item update, can be null. - * @param index The slot index - * @return If the action has been cancelled - */ - public boolean removeHalf(@Nullable UpdateReason updateReason, int index) { - ItemStack currentStack = items[index]; - if (currentStack != null) { - int newAmount = currentStack.getAmount() / 2; - - if (newAmount > 0) { - ItemStack newStack = currentStack.clone(); - newStack.setAmount(newAmount); - - ItemUpdateEvent event = createAndCallEvent(index, updateReason, currentStack, newStack); - if (!event.isCancelled()) { - items[index] = newStack; - notifyWindows(); - - return false; - } - - } else return removeItem(updateReason, index); - } - - return true; + return amount; } /** @@ -345,9 +406,9 @@ public class VirtualInventory implements ConfigurationSerializable { * This method does not work the same way as Bukkit's addItem method * as it respects the max stack size of the item type. * - * @param updateReason The reason for item update, can be null. + * @param updateReason The reason used in the {@link ItemUpdateEvent}. * @param itemStack The {@link ItemStack} to add - * @return The amount of items that couldn't be added + * @return The amount of items that didn't fit * @see #simulateAdd(ItemStack) * @see #simulateMultiAdd(List) */ @@ -357,14 +418,19 @@ public class VirtualInventory implements ConfigurationSerializable { // find all slots where the item partially fits and add it there for (int partialSlot : findPartialSlots(itemStack)) { - amountLeft = addTo(updateReason, partialSlot, amountLeft); + ItemStack stackToPut = itemStack.clone(); + stackToPut.setAmount(amountLeft); + amountLeft = putItemStack(updateReason, partialSlot, stackToPut); + if (amountLeft == 0) break; } // find all empty slots and put the item there for (int emptySlot : ArrayUtils.findEmptyIndices(items)) { + ItemStack stackToPut = itemStack.clone(); + stackToPut.setAmount(amountLeft); + amountLeft = putItemStack(updateReason, emptySlot, stackToPut); if (amountLeft == 0) break; - amountLeft = addToEmpty(updateReason, emptySlot, itemStack, amountLeft); } // if items have been added, notify windows @@ -383,18 +449,19 @@ public class VirtualInventory implements ConfigurationSerializable { * @return How many items wouldn't fit in the inventory when added */ public int simulateAdd(ItemStack itemStack) { - int maxStackSize = itemStack.getMaxStackSize(); int amountLeft = itemStack.getAmount(); // find all slots where the item partially fits for (int partialSlot : findPartialSlots(itemStack)) { ItemStack partialItem = items[partialSlot]; + int maxStackSize = getMaxStackSize(partialSlot, -1); amountLeft = Math.max(0, amountLeft - (maxStackSize - partialItem.getAmount())); if (amountLeft == 0) break; } // remaining items would be added to empty slots - for (int ignored : ArrayUtils.findEmptyIndices(items)) { + for (int emptySlot : ArrayUtils.findEmptyIndices(items)) { + int maxStackSize = getMaxStackSize(emptySlot, itemStack.getMaxStackSize()); amountLeft -= Math.min(amountLeft, maxStackSize); } @@ -415,7 +482,7 @@ public class VirtualInventory implements ConfigurationSerializable { public int[] simulateMultiAdd(List itemStacks) { if (itemStacks.size() < 2) throw new IllegalArgumentException("Illegal amount of ItemStacks in List"); - VirtualInventory copiedInv = new VirtualInventory(null, size, getItems()); + VirtualInventory copiedInv = new VirtualInventory(null, size, getItems(), stackSizes.clone()); int[] result = new int[itemStacks.size()]; for (int index = 0; index != itemStacks.size(); index++) { result[index] = copiedInv.addItem(null, itemStacks.get(index)); @@ -425,18 +492,13 @@ public class VirtualInventory implements ConfigurationSerializable { } /** - * Checks if the {@link VirtualInventory} could theoretically hold the - * provided {@link ItemStack}. + * Finds all {@link ItemStack}s similar to the provided {@link ItemStack} and removes them from + * their slot until the maximum stack size of the {@link Material} is reached. * - * @param itemStacks The {@link ItemStack}s - * @return If the {@link VirtualInventory} can fit all these items + * @param updateReason The reason used in the {@link ItemUpdateEvent}. + * @param itemStack The {@link ItemStack} to find matches to + * @return The amount of collected items */ - public boolean canHold(List itemStacks) { - if (itemStacks.size() == 0) return true; - else if (itemStacks.size() == 1) return simulateAdd(itemStacks.get(0)) == 0; - else return Arrays.stream(simulateMultiAdd(itemStacks)).allMatch(i -> i == 0); - } - public int collectToCursor(@Nullable UpdateReason updateReason, ItemStack itemStack) { int amount = itemStack.getAmount(); int maxStackSize = itemStack.getMaxStackSize(); @@ -444,15 +506,13 @@ public class VirtualInventory implements ConfigurationSerializable { // find partial slots and take items from there for (int partialSlot : findPartialSlots(itemStack)) { amount += takeFrom(updateReason, partialSlot, maxStackSize - amount); - if (amount == maxStackSize) break; + if (amount == maxStackSize) return amount; } - // if only taking from partial stacks wasn't enough, take from a full slot - if (amount < itemStack.getMaxStackSize()) { - int fullSlot = findFullSlot(itemStack); - if (fullSlot != -1) { - amount += takeFrom(updateReason, fullSlot, maxStackSize - amount); - } + // only taking from partial stacks wasn't enough, take from a full slot + for (int fullSlot : findFullSlots(itemStack)) { + amount += takeFrom(updateReason, fullSlot, maxStackSize - amount); + if (amount == maxStackSize) return amount; } } @@ -461,52 +521,28 @@ public class VirtualInventory implements ConfigurationSerializable { private List findPartialSlots(ItemStack itemStack) { List partialSlots = new ArrayList<>(); - for (int i = 0; i < items.length; i++) { - ItemStack currentStack = items[i]; - if (currentStack != null && currentStack.getAmount() < currentStack.getMaxStackSize() - && currentStack.isSimilar(itemStack)) partialSlots.add(i); + for (int slot = 0; slot < size; slot++) { + ItemStack currentStack = items[slot]; + if (itemStack.isSimilar(currentStack)) { + int maxStackSize = getMaxStackSize(slot, -1); + if (currentStack.getAmount() < maxStackSize) partialSlots.add(slot); + } } return partialSlots; } - private int findFullSlot(ItemStack itemStack) { - for (int i = 0; i < items.length; i++) { - ItemStack currentStack = items[i]; - if (currentStack != null - && currentStack.getAmount() == currentStack.getMaxStackSize() - && currentStack.isSimilar(itemStack)) return i; + private List findFullSlots(ItemStack itemStack) { + List fullSlots = new ArrayList<>(); + for (int slot = 0; slot < size; slot++) { + ItemStack currentStack = items[slot]; + if (itemStack.isSimilar(currentStack)) { + int maxStackSize = getMaxStackSize(slot, -1); + if (currentStack.getAmount() == maxStackSize) fullSlots.add(slot); + } } - return -1; - } - - private int addTo(@Nullable UpdateReason updateReason, int index, int amount) { - ItemStack itemStack = items[index]; - - int maxAddable = Math.min(itemStack.getMaxStackSize() - itemStack.getAmount(), amount); - - int currentAmount = itemStack.getAmount(); - int newAmount = currentAmount + maxAddable; - - ItemStack newStack = itemStack.clone(); - newStack.setAmount(newAmount); - - ItemUpdateEvent event = createAndCallEvent(index, updateReason, itemStack, newStack); - if (!event.isCancelled()) { - items[index] = newStack; - notifyWindows(); - return amount - maxAddable; - } else return amount; - } - - private int addToEmpty(@Nullable UpdateReason updateReason, int index, @NotNull ItemStack type, int amount) { - int maxAddable = Math.min(type.getType().getMaxStackSize(), amount); - ItemStack newStack = type.clone(); - newStack.setAmount(maxAddable); - - if (setItemStack(updateReason, index, newStack)) return amount; - else return amount - maxAddable; + return fullSlots; } private int takeFrom(@Nullable UpdateReason updateReason, int index, int maxTake) { @@ -520,7 +556,7 @@ public class VirtualInventory implements ConfigurationSerializable { newStack.setAmount(amount - take); } else newStack = null; - ItemUpdateEvent event = createAndCallEvent(index, updateReason, itemStack, newStack); + ItemUpdateEvent event = callUpdateEvent(updateReason, index, itemStack, newStack); if (!event.isCancelled()) { items[index] = newStack; notifyWindows(); @@ -530,94 +566,4 @@ public class VirtualInventory implements ConfigurationSerializable { return 0; } - /** - * Adds a {@link Window} to the window set, telling the {@link VirtualInventory} that it is - * currently being displayed in that {@link Window}. - * - * @param window The {@link Window} the {@link VirtualInventory} is currently displayed in. - */ - public void addWindow(Window window) { - windows.add(window); - } - - /** - * Removes an {@link Window} from the window set, telling the {@link VirtualInventory} that it - * is no longer being displayed in that {@link Window}. - * - * @param window The {@link Window} the {@link VirtualInventory} is no longer displayed in. - */ - public void removeWindow(Window window) { - windows.remove(window); - } - - /** - * Gets an immutable view of the {@link Set} that contains all the {@link Window}s that - * display this {@link VirtualInventory}. - * - * @return An UnmodifiableSet of all the {@link Window}s that show this {@link VirtualInventory}. - */ - public Set getWindows() { - return Collections.unmodifiableSet(windows); - } - - /** - * Notifies all {@link Window}s displaying this {@link VirtualInventory} to update their - * representative {@link ItemStack}s. - * This method should only be called manually in very specific cases like when the - * {@link ItemMeta} of an {@link ItemStack} in this inventory has changed. - */ - public void notifyWindows() { - Bukkit.getScheduler().runTask(InvUI.getInstance().getPlugin(), () -> - windows.forEach(window -> window.handleVirtualInventoryUpdate(this))); - } - - /** - * Creates an {@link ItemUpdateEvent} and calls the {@link #itemUpdateHandler} to handle it. - * - * @param index The slot index of the affected {@link ItemStack} - * @param updateReason The {@link UpdateReason} - * @param previousItemStack The {@link ItemStack} that was previously on that slot - * @param newItemStack The {@link ItemStack} that will be on that slot - * @return The {@link ItemUpdateEvent} after it has been handled by the {@link #itemUpdateHandler} - */ - public ItemUpdateEvent createAndCallEvent(int index, UpdateReason updateReason, ItemStack previousItemStack, ItemStack newItemStack) { - ItemUpdateEvent event = new ItemUpdateEvent(this, index, updateReason, previousItemStack, newItemStack); - if (itemUpdateHandler != null) itemUpdateHandler.accept(event); - return event; - } - - /** - * Gets the {@link UUID} of this {@link VirtualInventory}. - * - * @return The {@link UUID} of this {@link VirtualInventory} - */ - public UUID getUuid() { - return uuid; - } - - /** - * Sets the item update handler which will get called every time - * an item gets updated in this {@link VirtualInventory}. - * - * @param itemUpdateHandler The item update handler - */ - public void setItemUpdateHandler(Consumer itemUpdateHandler) { - this.itemUpdateHandler = itemUpdateHandler; - } - - /** - * Serializes this {@link VirtualInventory} to a {@link Map} - * - * @return A {@link Map} that contains the serialized data of this {@link VirtualInventory} - */ - @NotNull - @Override - public Map serialize() { - Map result = new LinkedHashMap<>(); - result.put("uuid", uuid.toString()); - result.put("size", size); - result.put("items", items); - return result; - } - } diff --git a/src/main/java/de/studiocode/invui/virtualinventory/VirtualInventoryManager.java b/src/main/java/de/studiocode/invui/virtualinventory/VirtualInventoryManager.java index 3ced4b1..17e8941 100644 --- a/src/main/java/de/studiocode/invui/virtualinventory/VirtualInventoryManager.java +++ b/src/main/java/de/studiocode/invui/virtualinventory/VirtualInventoryManager.java @@ -3,6 +3,7 @@ package de.studiocode.invui.virtualinventory; import de.studiocode.invui.InvUI; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -44,6 +45,16 @@ public class VirtualInventoryManager { return virtualInventory; } + public VirtualInventory createNew(@NotNull UUID uuid, int size, ItemStack[] items, int[] stackSizes) { + if (inventories.containsKey(uuid)) + throw new IllegalArgumentException("A Virtual Inventory with that UUID already exists"); + + VirtualInventory virtualInventory = new VirtualInventory(uuid, size, items, stackSizes); + inventories.put(uuid, virtualInventory); + + return virtualInventory; + } + public VirtualInventory getByUuid(@NotNull UUID uuid) { return inventories.get(uuid); } @@ -53,6 +64,11 @@ public class VirtualInventoryManager { return virtualInventory == null ? createNew(uuid, size) : virtualInventory; } + public VirtualInventory getOrCreate(UUID uuid, int size, ItemStack[] items, int[] stackSizes) { + VirtualInventory virtualInventory = getByUuid(uuid); + return virtualInventory == null ? createNew(uuid, size, items, stackSizes) : virtualInventory; + } + public void remove(VirtualInventory virtualInventory) { inventories.remove(virtualInventory.getUuid(), virtualInventory); getSaveFile(virtualInventory).delete(); diff --git a/src/main/java/de/studiocode/invui/virtualinventory/event/ItemUpdateEvent.java b/src/main/java/de/studiocode/invui/virtualinventory/event/ItemUpdateEvent.java index 8188e4b..83a5cb1 100644 --- a/src/main/java/de/studiocode/invui/virtualinventory/event/ItemUpdateEvent.java +++ b/src/main/java/de/studiocode/invui/virtualinventory/event/ItemUpdateEvent.java @@ -13,7 +13,7 @@ public class ItemUpdateEvent { private final VirtualInventory virtualInventory; private final ItemStack previousItemStack; - private final ItemStack newItemStack; + private ItemStack newItemStack; private final UpdateReason updateReason; private final int slot; @@ -76,6 +76,17 @@ public class ItemUpdateEvent { return newItemStack; } + /** + * Change the {@link ItemStack} that will appear in the {@link VirtualInventory} + * to a different one. + * + * @param newItemStack The {@link ItemStack} to appear in the {@link VirtualInventory} + * if the {@link ItemUpdateEvent} is not cancelled. + */ + public void setNewItemStack(@Nullable ItemStack newItemStack) { + this.newItemStack = newItemStack; + } + /** * Gets the slot that is affected. * diff --git a/src/main/java/de/studiocode/invui/window/impl/BaseWindow.java b/src/main/java/de/studiocode/invui/window/impl/BaseWindow.java index e7e1514..8edd2b1 100644 --- a/src/main/java/de/studiocode/invui/window/impl/BaseWindow.java +++ b/src/main/java/de/studiocode/invui/window/impl/BaseWindow.java @@ -106,7 +106,7 @@ public abstract class BaseWindow implements Window { // get the GUI at that index and ask for permission to drag an Item there Pair pair = getGuiAt(rawSlot); - if (pair != null && pair.getFirst().handleItemDrag(updateReason, pair.getSecond(), currentStack, newItems.get(rawSlot))) { + if (pair != null && !pair.getFirst().handleItemDrag(updateReason, pair.getSecond(), currentStack, newItems.get(rawSlot))) { // the drag was cancelled int currentAmount = currentStack == null ? 0 : currentStack.getAmount(); int newAmount = newItems.get(rawSlot).getAmount();