From 247b9c315b44e4db718a91fb42e78caaee7248b9 Mon Sep 17 00:00:00 2001 From: MissingReports <96932973+MissingReports@users.noreply.github.com> Date: Fri, 17 Jan 2025 05:28:58 +0200 Subject: [PATCH] Add Slot Change Section (#181) --- .../elements/sections/SecSlotChange.java | 95 ++++++++++++++++++ .../apickledwalrus/skriptgui/gui/GUI.java | 47 ++++++++- .../skriptgui/gui/GUIEventHandler.java | 19 +++- .../skriptgui/gui/events/GUIEvents.java | 98 ++++++++++++++----- 4 files changed, 227 insertions(+), 32 deletions(-) create mode 100644 src/main/java/io/github/apickledwalrus/skriptgui/elements/sections/SecSlotChange.java diff --git a/src/main/java/io/github/apickledwalrus/skriptgui/elements/sections/SecSlotChange.java b/src/main/java/io/github/apickledwalrus/skriptgui/elements/sections/SecSlotChange.java new file mode 100644 index 0000000..5c681d9 --- /dev/null +++ b/src/main/java/io/github/apickledwalrus/skriptgui/elements/sections/SecSlotChange.java @@ -0,0 +1,95 @@ +package io.github.apickledwalrus.skriptgui.elements.sections; + +import ch.njol.skript.Skript; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Section; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.Trigger; +import ch.njol.skript.lang.TriggerItem; +import ch.njol.skript.variables.Variables; +import ch.njol.util.Kleenean; +import io.github.apickledwalrus.skriptgui.SkriptGUI; +import io.github.apickledwalrus.skriptgui.gui.GUI; +import org.bukkit.event.Event; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.eclipse.jdt.annotation.Nullable; + +import java.util.List; + +@Name("GUI Slot Change") +@Description("Sections that will run when a gui slot is changed. This section is optional.") +@Examples({ + "create a gui with virtual chest inventory with 3 rows named \"My GUI\"", + "\trun when slot 12 changes:", + "\t\tsend \"You changed slot 12!\" to player", + "\trun on slot 14 changed:", + "\t\tcancel event" +}) +@Since("1.3") +public class SecSlotChange extends Section { + + static { + Skript.registerSection(SecSlotChange.class, + "run when [gui] slot[s] %integers% change[s]", + "run when [gui] slot[s] %integers% [(are|is)] [being] changed", + "run on change of [gui] slot[s] %integers%" + ); + } + + @SuppressWarnings("NotNullFieldNotInitialized") + private Trigger trigger; + private Expression guiSlots; + + @SuppressWarnings("unchecked") + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, SectionNode sectionNode, List triggerItems) { + if (!getParser().isCurrentSection(SecCreateGUI.class)) { + Skript.error("You can't listen for changes of a slot outside of a GUI creation or editing section."); + return false; + } + + trigger = loadCode(sectionNode, "inventory click", InventoryClickEvent.class); + + guiSlots = (Expression) exprs[0]; + return true; + } + + @Override + @Nullable + public TriggerItem walk(Event e) { + GUI gui = SkriptGUI.getGUIManager().getGUI(e); + if (gui == null) + return walk(e, false); + + Integer[] slots = guiSlots.getAll(e); + + for (Integer slot : slots) { + if (slot >= 0 && slot + 1 <= gui.getInventory().getSize()) { + Object variables = Variables.copyLocalVariables(e); + GUI.SlotData slotData = gui.getSlotData(gui.convert(slot)); + if (variables != null && slotData != null) { + slotData.setRunOnChange(event -> { + Variables.setLocalVariables(event, variables); + trigger.execute(event); + }); + } else if (slotData != null) { + slotData.setRunOnChange(trigger::execute); + } + } + } + + // We don't want to execute this section + return walk(e, false); + } + + @Override + public String toString(@Nullable Event e, boolean debug) { + return "run on change of slot " + guiSlots.toString(e, debug); + } + +} diff --git a/src/main/java/io/github/apickledwalrus/skriptgui/gui/GUI.java b/src/main/java/io/github/apickledwalrus/skriptgui/gui/GUI.java index 77ce627..f242cc8 100644 --- a/src/main/java/io/github/apickledwalrus/skriptgui/gui/GUI.java +++ b/src/main/java/io/github/apickledwalrus/skriptgui/gui/GUI.java @@ -38,6 +38,15 @@ public void onClick(InventoryClickEvent e) { // Only cancel if this slot can't be removed AND all items aren't removable e.setCancelled(!isRemovable(slotData)); + // Call onChange if the slot is being changed + if (!e.isCancelled() && (e.getCursor() != null || e.getCurrentItem() != null)) { + if (e.getCursor() == null || e.getCurrentItem() == null || + !e.getCursor().isSimilar(e.getCurrentItem()) || + e.getCurrentItem().getAmount() < e.getCurrentItem().getMaxStackSize()) { + onChange(e); + } + } + Consumer runOnClick = slotData.getRunOnClick(); if (runOnClick != null) { SkriptGUI.getGUIManager().setGUI(e, GUI.this); @@ -48,6 +57,28 @@ public void onClick(InventoryClickEvent e) { } } + @Override + public void onChange(InventoryClickEvent e) { + if (isPaused() || isPaused((Player) e.getWhoClicked())) { + e.setCancelled(true); // Just in case + return; + } + + SlotData slotData = getSlotData(convert(e.getSlot())); + if (slotData != null) { + // Only cancel if this slot can't be removed AND all items aren't removable + e.setCancelled(!isRemovable(slotData)); + + Consumer runOnChange = slotData.getRunOnChange(); + if (!e.isCancelled() && runOnChange != null) { + SkriptGUI.getGUIManager().setGUI(e, GUI.this); + runOnChange.accept(e); + } + } else { // If there is no slot data, cancel if this GUI doesn't have stealable items + e.setCancelled(!isRemovable()); + } + } + @Override public void onDrag(InventoryDragEvent e) { if (isPaused() || isPaused((Player) e.getWhoClicked())) { @@ -61,6 +92,7 @@ public void onDrag(InventoryDragEvent e) { break; } } + onChange(e); } @Override @@ -477,7 +509,7 @@ public String getID() { */ public void setID(@Nullable String id) { this.id = id; - if (id == null && inventory.getViewers().size() == 0) { + if (id == null && inventory.getViewers().isEmpty()) { SkriptGUI.getGUIManager().unregister(this); clear(); } @@ -500,6 +532,8 @@ public static final class SlotData { @Nullable private Consumer runOnClick; + @Nullable + private Consumer runOnChange; private boolean removable; public SlotData(@Nullable Consumer runOnClick, boolean removable) { @@ -515,6 +549,11 @@ public Consumer getRunOnClick() { return runOnClick; } + @Nullable + public Consumer getRunOnChange() { + return runOnChange; + } + /** * Updates the consumer to run when a slot with this data is clicked. A null value may be used to remove the consumer. * @param runOnClick The consumer to run when a slot with this data is clicked. @@ -523,12 +562,16 @@ public void setRunOnClick(@Nullable Consumer runOnClick) { this.runOnClick = runOnClick; } + public void setRunOnChange(@Nullable Consumer runOnChange) { + this.runOnChange = runOnChange; + } + /** * @return Whether this item can be removed from its slot, regardless of {@link GUI#isRemovable()}. * Please note that if {@link #getRunOnClick()} returns a non-null value, this method will always return false. */ public boolean isRemovable() { - return runOnClick == null && removable; + return removable; } /** diff --git a/src/main/java/io/github/apickledwalrus/skriptgui/gui/GUIEventHandler.java b/src/main/java/io/github/apickledwalrus/skriptgui/gui/GUIEventHandler.java index 33397b1..9ec549e 100644 --- a/src/main/java/io/github/apickledwalrus/skriptgui/gui/GUIEventHandler.java +++ b/src/main/java/io/github/apickledwalrus/skriptgui/gui/GUIEventHandler.java @@ -1,10 +1,7 @@ package io.github.apickledwalrus.skriptgui.gui; import org.bukkit.entity.Player; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryDragEvent; -import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.event.inventory.*; import java.util.ArrayList; import java.util.List; @@ -68,8 +65,22 @@ public boolean isPaused(Player player) { } public abstract void onClick(InventoryClickEvent e); + public abstract void onChange(InventoryClickEvent e); public abstract void onDrag(InventoryDragEvent e); public abstract void onOpen(InventoryOpenEvent e); public abstract void onClose(InventoryCloseEvent e); + public void onChange(InventoryDragEvent e) { + for (int slot : e.getInventorySlots()) { + InventoryClickEvent clickEvent = new InventoryClickEvent( + e.getView(), + e.getView().getSlotType(slot), + slot, + ClickType.UNKNOWN, + InventoryAction.UNKNOWN + ); + onChange(clickEvent); + } + } + } diff --git a/src/main/java/io/github/apickledwalrus/skriptgui/gui/events/GUIEvents.java b/src/main/java/io/github/apickledwalrus/skriptgui/gui/events/GUIEvents.java index 98fc0a0..177dced 100644 --- a/src/main/java/io/github/apickledwalrus/skriptgui/gui/events/GUIEvents.java +++ b/src/main/java/io/github/apickledwalrus/skriptgui/gui/events/GUIEvents.java @@ -2,6 +2,7 @@ import io.github.apickledwalrus.skriptgui.SkriptGUI; import io.github.apickledwalrus.skriptgui.gui.GUI; +import io.github.apickledwalrus.skriptgui.gui.GUIEventHandler; import org.bukkit.GameMode; import org.bukkit.Material; import org.bukkit.event.EventHandler; @@ -15,6 +16,9 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import java.util.ArrayList; +import java.util.List; + public class GUIEvents implements Listener { @EventHandler(priority = EventPriority.LOWEST) @@ -43,6 +47,7 @@ public void onInventoryClick(InventoryClickEvent event) { if (gui == null) { return; } + GUIEventHandler eventHandler = gui.getEventHandler(); // Don't process unknown clicks for safety reasons - cancel them to prevent unwanted GUI changes if (event.getClick() == ClickType.UNKNOWN) { @@ -61,35 +66,33 @@ public void onInventoryClick(InventoryClickEvent event) { if (clicked != null) { Inventory guiInventory = gui.getInventory(); - if (!guiInventory.contains(clicked.getType())) { - int firstEmpty = guiInventory.firstEmpty(); - if (firstEmpty != -1 && gui.isRemovable(gui.convert(firstEmpty))) { // Safe to be moved into the GUI - return; - } - } - int size = guiInventory.getSize(); + int totalAmount = clicked.getAmount(); for (int slot = 0; slot < size; slot++) { + if (totalAmount <= 0) { + return; + } + ItemStack item = guiInventory.getItem(slot); - if (item != null && item.getType() != Material.AIR && item.isSimilar(clicked)) { + if (item != null && item.getType() != Material.AIR && item.isSimilar(clicked) && item.getAmount() < item.getMaxStackSize()) { + InventoryClickEvent clickEvent = setClickedSlot(event, slot); + if (!gui.isRemovable(gui.convert(slot))) { - if (item.getAmount() == 64) { // It wouldn't be able to combine - continue; - } event.setCancelled(true); return; + } else { + eventHandler.onChange(clickEvent); + totalAmount -= item.getMaxStackSize() - item.getAmount(); } - - if (item.getAmount() + clicked.getAmount() <= 64) { // This will only modify a modifiable slot - return; - } - } + } int firstEmpty = guiInventory.firstEmpty(); if (firstEmpty != -1 && gui.isRemovable(gui.convert(firstEmpty))) { // Safe to be moved into the GUI + InventoryClickEvent clickEvent = setClickedSlot(event, firstEmpty); + eventHandler.onChange(clickEvent); return; } @@ -101,20 +104,21 @@ public void onInventoryClick(InventoryClickEvent event) { // Only cancel if this will cause a change to the GUI itself // We are checking if our GUI contains an item that could be merged with the event item // If that item is mergeable but it isn't stealable, we will cancel the event now - Inventory guiInventory = gui.getInventory(); - int size = guiInventory.getSize(); - ItemStack cursor = event.getWhoClicked().getItemOnCursor(); - for (int slot = 0; slot < size; slot++) { - ItemStack item = guiInventory.getItem(slot); - if (item != null && item.isSimilar(cursor) && !gui.isRemovable(gui.convert(slot))) { - event.setCancelled(true); - break; - } - } + handleDoubleClick(gui, event); return; default: return; } + } else { + // Call onChange if a slot is changed due to interactions within the gui itself + if (event.getClick() == ClickType.DOUBLE_CLICK) { + if (!gui.isRemovable(gui.convert(event.getSlot()))) { // Doesn't change the slots + event.setCancelled(true); + return; + } + + handleDoubleClick(gui, event); + } } gui.getEventHandler().onClick(event); @@ -151,4 +155,46 @@ public void onInventoryClose(InventoryCloseEvent e) { } } + private void handleDoubleClick(GUI gui, InventoryClickEvent event) { + GUIEventHandler eventHandler = gui.getEventHandler(); + + Inventory guiInventory = gui.getInventory(); + int size = guiInventory.getSize(); + ItemStack cursor = event.getCursor(); + + if (cursor == null || event.getCurrentItem() != null) + return; + + int totalAmount = cursor.getAmount(); + List clickEvents = new ArrayList<>(); + for (int slot = 0; slot < size; slot++) { + ItemStack item = guiInventory.getItem(slot); + if (item != null && item.isSimilar(cursor)) { + if (!gui.isRemovable(gui.convert(slot))) { + event.setCancelled(true); + return; + } + + if (totalAmount < cursor.getMaxStackSize()) { + InventoryClickEvent clickEvent = setClickedSlot(event, slot); + clickEvents.add(clickEvent); + totalAmount += item.getAmount(); + } + } + } + for (InventoryClickEvent clickEvent : clickEvents) { + eventHandler.onChange(clickEvent); + } + } + + private static InventoryClickEvent setClickedSlot(InventoryClickEvent event, int slot) { + return new InventoryClickEvent( + event.getView(), + event.getSlotType(), + slot, + event.getClick(), + event.getAction() + ); + } + }