diff --git a/config/checkstyle/suppression.xml b/config/checkstyle/suppression.xml
index 343b3f35..cdf4a9ce 100644
--- a/config/checkstyle/suppression.xml
+++ b/config/checkstyle/suppression.xml
@@ -8,6 +8,8 @@
+
+
diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/menu/FillerMask.java b/eco-api/src/main/java/com/willfp/eco/core/gui/menu/FillerMask.java
new file mode 100644
index 00000000..8c858fb6
--- /dev/null
+++ b/eco-api/src/main/java/com/willfp/eco/core/gui/menu/FillerMask.java
@@ -0,0 +1,52 @@
+package com.willfp.eco.core.gui.menu;
+
+import com.willfp.eco.internal.gui.FillerSlot;
+import lombok.Getter;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+
+public class FillerMask {
+ @Getter
+ private final FillerSlot[][] mask;
+
+ public FillerMask(@NotNull final Material material,
+ @NotNull final Menu menu,
+ @NotNull final String... pattern) {
+ if (pattern.length != menu.getRows()) {
+ throw new IllegalArgumentException("Invalid amount of rows specified in pattern!");
+ }
+
+ if (material == Material.AIR) {
+ throw new IllegalArgumentException("Material cannot be air!");
+ }
+
+ mask = new FillerSlot[menu.getRows()][9];
+
+ ItemStack itemStack = new ItemStack(material);
+ ItemMeta meta = itemStack.getItemMeta();
+ assert meta != null;
+ meta.setDisplayName("");
+ itemStack.setItemMeta(meta);
+
+ int row = 0;
+
+ for (String patternRow : pattern) {
+ int column = 0;
+ if (pattern.length != 9) {
+ throw new IllegalArgumentException("Invalid amount of columns in pattern!");
+ }
+ for (char c : patternRow.toCharArray()) {
+ if (c == '0') {
+ mask[row][column] = null;
+ } else if (c == '1') {
+ mask[row][column] = new FillerSlot(itemStack);
+ } else {
+ throw new IllegalArgumentException("Invalid character in pattern! (Must only be 0 and 1)");
+ }
+ }
+ row++;
+ }
+ }
+}
diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/menu/Menu.java b/eco-api/src/main/java/com/willfp/eco/core/gui/menu/Menu.java
new file mode 100644
index 00000000..b29daaf8
--- /dev/null
+++ b/eco-api/src/main/java/com/willfp/eco/core/gui/menu/Menu.java
@@ -0,0 +1,95 @@
+package com.willfp.eco.core.gui.menu;
+
+import com.willfp.eco.core.gui.slot.Slot;
+import com.willfp.eco.internal.gui.EcoMenu;
+import com.willfp.eco.internal.gui.FillerSlot;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.function.Consumer;
+
+public interface Menu {
+ int getRows();
+
+ Slot getSlot(int row,
+ int column);
+
+ String getTitle();
+
+ Inventory open(@NotNull Player player);
+
+ static Builder builder(final int rows) {
+ return new Builder(rows);
+ }
+
+ class Builder {
+ private final int rows;
+ private String title = "Menu";
+ private Slot[][] maskSlots;
+ private final Slot[][] slots;
+ private Consumer onClose = (event) -> {
+ };
+
+ Builder(final int rows) {
+ this.rows = rows;
+ this.slots = new Slot[rows][9];
+ this.maskSlots = new Slot[rows][9];
+ }
+
+ public Builder setTitle(@NotNull final String title) {
+ this.title = title;
+ return this;
+ }
+
+ public Builder setSlot(final int row,
+ final int column,
+ @NotNull final Slot slot) {
+ if (row < 0 || row > this.rows - 1) {
+ throw new IllegalArgumentException("Invalid row number!");
+ }
+
+ if (column < 0 || column > 8) {
+ throw new IllegalArgumentException("Invalid column number!");
+ }
+
+ slots[row][column] = slot;
+ return this;
+ }
+
+ public Builder setMask(@NotNull final FillerMask mask) {
+ this.maskSlots = mask.getMask();
+ return this;
+ }
+
+ public Builder onClose(@NotNull final Consumer action) {
+ this.onClose = action;
+ return this;
+ }
+
+ public Menu build() {
+ Slot[][] finalSlots = maskSlots;
+ for (int i = 0; i < slots.length; i++) {
+ for (int j = 0; j < slots[i].length; j++) {
+ Slot slot = slots[i][j];
+ if (slot != null) {
+ finalSlots[i][j] = slot;
+ }
+ }
+ }
+
+ for (int i = 0; i < finalSlots.length; i++) {
+ for (int j = 0; j < finalSlots[i].length; j++) {
+ if (finalSlots[i][j] == null) {
+ finalSlots[i][j] = new FillerSlot(new ItemStack(Material.AIR));
+ }
+ }
+ }
+
+ return new EcoMenu(rows, finalSlots, title, onClose);
+ }
+ }
+}
diff --git a/eco-api/src/main/java/com/willfp/eco/core/gui/slot/Slot.java b/eco-api/src/main/java/com/willfp/eco/core/gui/slot/Slot.java
new file mode 100644
index 00000000..e04c2ce6
--- /dev/null
+++ b/eco-api/src/main/java/com/willfp/eco/core/gui/slot/Slot.java
@@ -0,0 +1,63 @@
+package com.willfp.eco.core.gui.slot;
+
+import com.willfp.eco.internal.gui.EcoSlot;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.function.BiConsumer;
+
+public interface Slot {
+ ItemStack getItemStack();
+
+ static Builder builder(@NotNull final ItemStack itemStack) {
+ return new Builder(itemStack);
+ }
+
+ class Builder {
+ private final ItemStack itemStack;
+
+ private BiConsumer onLeftClick = null;
+
+ private BiConsumer onRightClick = null;
+
+ private BiConsumer onShiftLeftClick = null;
+
+ private BiConsumer onShiftRightClick = null;
+
+ private BiConsumer onMiddleClick = null;
+
+ Builder(@NotNull final ItemStack itemStack) {
+ this.itemStack = itemStack;
+ }
+
+ public Builder onLeftClick(@NotNull final BiConsumer action) {
+ this.onLeftClick = action;
+ return this;
+ }
+
+ public Builder onRightClick(@NotNull final BiConsumer action) {
+ this.onRightClick = action;
+ return this;
+ }
+
+ public Builder onShiftLeftClick(@NotNull final BiConsumer action) {
+ this.onShiftLeftClick = action;
+ return this;
+ }
+
+ public Builder onShiftRightClick(@NotNull final BiConsumer action) {
+ this.onShiftRightClick = action;
+ return this;
+ }
+
+ public Builder onMiddleClick(@NotNull final BiConsumer action) {
+ this.onMiddleClick = action;
+ return this;
+ }
+
+ public Slot build() {
+ return new EcoSlot(itemStack, onLeftClick, onRightClick, onShiftLeftClick, onShiftRightClick, onMiddleClick);
+ }
+ }
+}
diff --git a/eco-api/src/main/java/com/willfp/eco/internal/gui/EcoMenu.java b/eco-api/src/main/java/com/willfp/eco/internal/gui/EcoMenu.java
new file mode 100644
index 00000000..a9bfcc4d
--- /dev/null
+++ b/eco-api/src/main/java/com/willfp/eco/internal/gui/EcoMenu.java
@@ -0,0 +1,60 @@
+package com.willfp.eco.internal.gui;
+
+import com.willfp.eco.core.gui.menu.Menu;
+import com.willfp.eco.core.gui.slot.Slot;
+import lombok.Getter;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.function.Consumer;
+
+public class EcoMenu implements Menu {
+ @Getter
+ private final int rows;
+
+ private final Slot[][] slots;
+
+ @Getter
+ private final String title;
+
+ private final Consumer onClose;
+
+ public EcoMenu(final int rows,
+ @NotNull final Slot[][] slots,
+ @NotNull final String title,
+ @NotNull final Consumer onClose) {
+ this.rows = rows;
+ this.slots = slots;
+ this.title = title;
+ this.onClose = onClose;
+ }
+
+ @Override
+ public Slot getSlot(final int row,
+ final int column) {
+ if (row < 0 || row > this.rows - 1) {
+ throw new IllegalArgumentException("Invalid row number!");
+ }
+
+ if (column < 0 || column > 8) {
+ throw new IllegalArgumentException("Invalid column number!");
+ }
+
+ return slots[row][column];
+ }
+
+ @Override
+ public Inventory open(@NotNull final Player player) {
+ Inventory inventory = Bukkit.createInventory(null, rows * 9, title);
+ player.openInventory(inventory);
+ MenuHandler.registerMenu(inventory, this);
+ return inventory;
+ }
+
+ public void handleClose(@NotNull final InventoryCloseEvent event) {
+ onClose.accept(event);
+ }
+}
diff --git a/eco-api/src/main/java/com/willfp/eco/internal/gui/EcoSlot.java b/eco-api/src/main/java/com/willfp/eco/internal/gui/EcoSlot.java
new file mode 100644
index 00000000..cc6ee9c1
--- /dev/null
+++ b/eco-api/src/main/java/com/willfp/eco/internal/gui/EcoSlot.java
@@ -0,0 +1,61 @@
+package com.willfp.eco.internal.gui;
+
+import com.willfp.eco.core.gui.slot.Slot;
+import lombok.Getter;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.function.BiConsumer;
+
+public class EcoSlot implements Slot {
+ @Getter
+ private final ItemStack itemStack;
+
+ private final BiConsumer onLeftClick;
+
+ private final BiConsumer onRightClick;
+
+ private final BiConsumer onShiftLeftClick;
+
+ private final BiConsumer onShiftRightClick;
+
+ private final BiConsumer onMiddleClick;
+
+ public EcoSlot(@NotNull final ItemStack itemStack,
+ @Nullable final BiConsumer onLeftClick,
+ @Nullable final BiConsumer onRightClick,
+ @Nullable final BiConsumer onShiftLeftClick,
+ @Nullable final BiConsumer onShiftRightClick,
+ @Nullable final BiConsumer onMiddleClick) {
+ this.itemStack = itemStack;
+ this.onLeftClick = onLeftClick == null ? ((event, slot) -> { }) : onLeftClick;
+ this.onRightClick = onRightClick == null ? ((event, slot) -> { }) : onRightClick;
+ this.onShiftLeftClick = onShiftLeftClick == null ? ((event, slot) -> { }) : onShiftLeftClick;
+ this.onShiftRightClick = onShiftRightClick == null ? ((event, slot) -> { }) : onShiftRightClick;
+ this.onMiddleClick = onMiddleClick == null ? ((event, slot) -> { }) : onMiddleClick;
+ }
+
+ public void handleInventoryClick(@NotNull final InventoryClickEvent event) {
+ switch (event.getClick()) {
+ case LEFT:
+ this.onLeftClick.accept(event, this);
+ break;
+ case RIGHT:
+ this.onRightClick.accept(event, this);
+ break;
+ case SHIFT_LEFT:
+ this.onShiftLeftClick.accept(event, this);
+ break;
+ case SHIFT_RIGHT:
+ this.onShiftRightClick.accept(event, this);
+ break;
+ case MIDDLE:
+ this.onMiddleClick.accept(event, this);
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/eco-api/src/main/java/com/willfp/eco/internal/gui/FillerSlot.java b/eco-api/src/main/java/com/willfp/eco/internal/gui/FillerSlot.java
new file mode 100644
index 00000000..cdd5224a
--- /dev/null
+++ b/eco-api/src/main/java/com/willfp/eco/internal/gui/FillerSlot.java
@@ -0,0 +1,15 @@
+package com.willfp.eco.internal.gui;
+
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+public class FillerSlot extends EcoSlot {
+ /**
+ * Create new filler slot.
+ *
+ * @param itemStack The ItemStack.
+ */
+ public FillerSlot(@NotNull final ItemStack itemStack) {
+ super(itemStack, null, null, null, null, null);
+ }
+}
diff --git a/eco-api/src/main/java/com/willfp/eco/internal/gui/MenuHandler.java b/eco-api/src/main/java/com/willfp/eco/internal/gui/MenuHandler.java
new file mode 100644
index 00000000..50de1cc7
--- /dev/null
+++ b/eco-api/src/main/java/com/willfp/eco/internal/gui/MenuHandler.java
@@ -0,0 +1,29 @@
+package com.willfp.eco.internal.gui;
+
+import com.willfp.eco.core.gui.menu.Menu;
+import lombok.experimental.UtilityClass;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@UtilityClass
+public class MenuHandler {
+ private static final Map MENUS = new HashMap<>();
+
+ public void registerMenu(@NotNull final Inventory inventory,
+ @NotNull final Menu menu) {
+ MENUS.put(inventory, menu);
+ }
+
+ public void unregisterMenu(@NotNull final Inventory inventory) {
+ MENUS.remove(inventory);
+ }
+
+ @Nullable
+ public Menu getMenu(@NotNull final Inventory inventory) {
+ return MENUS.get(inventory);
+ }
+}
diff --git a/eco-core/core-plugin/src/main/java/com/willfp/eco/spigot/EcoSpigotPlugin.java b/eco-core/core-plugin/src/main/java/com/willfp/eco/spigot/EcoSpigotPlugin.java
index 6f8b6a4c..f0914d04 100644
--- a/eco-core/core-plugin/src/main/java/com/willfp/eco/spigot/EcoSpigotPlugin.java
+++ b/eco-core/core-plugin/src/main/java/com/willfp/eco/spigot/EcoSpigotPlugin.java
@@ -25,6 +25,7 @@ import com.willfp.eco.spigot.eventlisteners.ArmorListener;
import com.willfp.eco.spigot.eventlisteners.DispenserArmorListener;
import com.willfp.eco.spigot.eventlisteners.EntityDeathByEntityListeners;
import com.willfp.eco.spigot.eventlisteners.NaturalExpGainListeners;
+import com.willfp.eco.spigot.gui.GUIListener;
import com.willfp.eco.spigot.integrations.anticheat.*;
import com.willfp.eco.spigot.integrations.antigrief.AntigriefCombatLogX;
import com.willfp.eco.spigot.integrations.antigrief.AntigriefFactionsUUID;
@@ -83,11 +84,6 @@ public class EcoSpigotPlugin extends EcoPlugin {
@Override
public void enable() {
new CollatedRunnable(this);
- this.getEventManager().registerListener(new NaturalExpGainListeners());
- this.getEventManager().registerListener(new ArmorListener());
- this.getEventManager().registerListener(new DispenserArmorListener());
- this.getEventManager().registerListener(new EntityDeathByEntityListeners(this));
- this.getEventManager().registerListener(new ShapedRecipeListener());
}
@Override
@@ -157,7 +153,14 @@ public class EcoSpigotPlugin extends EcoPlugin {
@Override
public List getListeners() {
- return new ArrayList<>();
+ return Arrays.asList(
+ new NaturalExpGainListeners(),
+ new ArmorListener(),
+ new DispenserArmorListener(),
+ new EntityDeathByEntityListeners(this),
+ new ShapedRecipeListener(),
+ new GUIListener(this)
+ );
}
@Override
diff --git a/eco-core/core-plugin/src/main/java/com/willfp/eco/spigot/gui/GUIListener.java b/eco-core/core-plugin/src/main/java/com/willfp/eco/spigot/gui/GUIListener.java
new file mode 100644
index 00000000..6ae40529
--- /dev/null
+++ b/eco-core/core-plugin/src/main/java/com/willfp/eco/spigot/gui/GUIListener.java
@@ -0,0 +1,63 @@
+package com.willfp.eco.spigot.gui;
+
+import com.willfp.eco.core.EcoPlugin;
+import com.willfp.eco.core.PluginDependent;
+import com.willfp.eco.core.gui.menu.Menu;
+import com.willfp.eco.internal.gui.EcoMenu;
+import com.willfp.eco.internal.gui.EcoSlot;
+import com.willfp.eco.internal.gui.MenuHandler;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.jetbrains.annotations.NotNull;
+
+public class GUIListener extends PluginDependent implements Listener {
+ /**
+ * Pass an {@link EcoPlugin} in order to interface with it.
+ *
+ * @param plugin The plugin to manage.
+ */
+ public GUIListener(@NotNull final EcoPlugin plugin) {
+ super(plugin);
+ }
+
+ @EventHandler
+ public void handleSlotClick(@NotNull final InventoryClickEvent event) {
+ if (!(event.getWhoClicked() instanceof Player)) {
+ return;
+ }
+
+ if (event.getClickedInventory() == null) {
+ return;
+ }
+ Menu menu = MenuHandler.getMenu(event.getClickedInventory());
+ if (menu == null) {
+ return;
+ }
+
+ int row = Math.floorDiv(event.getSlot(), 9);
+ int column = event.getSlot() - (row * 9);
+
+ EcoSlot slot = (EcoSlot) menu.getSlot(row, column);
+ event.setCancelled(true);
+ slot.handleInventoryClick(event);
+ }
+
+ @EventHandler
+ public void handleClose(@NotNull final InventoryCloseEvent event) {
+ if (!(event.getPlayer() instanceof Player)) {
+ return;
+ }
+
+ EcoMenu menu = (EcoMenu) MenuHandler.getMenu(event.getInventory());
+ if (menu == null) {
+ return;
+ }
+
+ menu.handleClose(event);
+
+ this.getPlugin().getScheduler().run(() -> MenuHandler.unregisterMenu(event.getInventory()));
+ }
+}