The class uses a singleton pattern for the two possible instances ({@code true} and {@code false}) * to ensure memory efficiency and reference equality for instances with the same boolean value.
* - *{@code
* OptionalBool condition = OptionalBool.of(someCondition);
*
@@ -218,4 +218,3 @@ public String toString() {
return "OptionalBool[" + value + "]";
}
}
-
diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts
index dccac76..a9a25a1 100644
--- a/paper/build.gradle.kts
+++ b/paper/build.gradle.kts
@@ -6,7 +6,7 @@ plugins {
var id = "plugin-engine-paper"
var domain = "gg.moonrise.engine"
-var apiVersion = "1.1.1"
+var apiVersion = "1.1.2"
repositories {
mavenCentral()
@@ -39,12 +39,23 @@ dependencies {
// Lombok
compileOnly("org.projectlombok:lombok:1.18.32")
annotationProcessor("org.projectlombok:lombok:1.18.32")
+
+ // Testing
+ testImplementation(platform("org.junit:junit-bom:5.12.2"))
+ testImplementation("org.junit.jupiter:junit-jupiter")
+ testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.76.1")
+ testImplementation("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT")
+ testImplementation("net.kyori:adventure-text-serializer-plain:4.26.1")
+ testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
+
+ withSourcesJar()
+ withJavadocJar()
}
tasks.jar {
@@ -57,12 +68,16 @@ tasks.shadowJar {
archiveClassifier.set("")
}
+tasks.test {
+ useJUnitPlatform()
+}
+
publishing {
publications {
create("mavenJava") {
- artifact(tasks.shadowJar) {
- builtBy(tasks.shadowJar)
- }
+ from(components["shadow"])
+ artifact(tasks.named("sourcesJar"))
+ artifact(tasks.named("javadocJar"))
groupId = domain
artifactId = id
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/ChestMenu.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/ChestMenu.java
index 0f8cc1c..606ebf3 100644
--- a/paper/src/main/java/gg/moonrise/engine/paper/gui/ChestMenu.java
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/ChestMenu.java
@@ -3,13 +3,16 @@
import com.google.common.base.Preconditions;
import gg.moonrise.engine.message.util.MiniMessageUtil;
import gg.moonrise.engine.paper.gui.button.Button;
+import gg.moonrise.engine.paper.gui.holder.ChestMenuHolder;
import gg.moonrise.engine.paper.gui.util.MenuInteractionUtil;
import gg.moonrise.engine.paper.gui.util.SafeUtil;
import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
@@ -20,8 +23,7 @@
/**
* Represents a chest-based GUI menu for players.
* This class manages button placement, click handling, and automatic refreshing
- * of dynamic buttons. Menus are cached per-player and automatically cleaned up
- * when closed.
+ * of dynamic buttons. Menus are routed through their inventory holder.
*/
public abstract class ChestMenu implements ChestInterface {
@@ -32,7 +34,7 @@ public abstract class ChestMenu implements ChestInterface {
private final Map buttonById = new HashMap<>();
private final Map refreshingButtons = new HashMap<>();
- protected InventoryView inventory;
+ protected Inventory inventory;
private boolean cancelClicks = true;
private final Player player;
@@ -97,7 +99,7 @@ public void onOpen(Player player, InventoryOpenEvent event) {
*/
@Override
public void onClose(Player player, InventoryCloseEvent event) {
- UserInterface.invalidateFromCache(player.getUniqueId());
+ invalidate();
}
/**
@@ -116,7 +118,7 @@ public void onClick(Player player, InventoryClickEvent event) {
*/
@Override
public void open() {
- MenuInteractionUtil.openMenu(player, this, this::refresh, () -> inventory);
+ MenuInteractionUtil.openMenu(player, this::refresh, () -> inventory);
}
/**
@@ -135,7 +137,7 @@ public void invalidate() {
@Override
public void refresh() {
if (inventory == null)
- inventory = typeFromRows(rows).create(player, title);
+ inventory = createInventory();
clearInventory(inventory);
@@ -252,13 +254,28 @@ public int getButtonCount() {
return buttons.size();
}
+ /**
+ * Get the Inventory associated with this ChestMenu
+ * @return The Inventory
+ */
+ @Override
+ public Inventory getInventory() {
+ return inventory;
+ }
+
/**
* Get the InventoryView associated with this ChestMenu
* @return The InventoryView
*/
@Override
+ @Deprecated(forRemoval = false)
public InventoryView getView() {
- return inventory;
+ if (inventory == null) return null;
+
+ InventoryView view = player.getOpenInventory();
+ if (!view.getTopInventory().equals(inventory)) return null;
+
+ return view;
}
/**
@@ -286,4 +303,11 @@ public boolean checkCancelClick(int slot) {
return MenuInteractionUtil.checkCancelClick(inventory, buttonById, slot);
}
+ private Inventory createInventory() {
+ ChestMenuHolder holder = new ChestMenuHolder(this);
+ Inventory created = Bukkit.createInventory(holder, rows * 9, title);
+ holder.setInventory(created);
+ return created;
+ }
+
}
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/HopperMenu.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/HopperMenu.java
index 8176a1a..3d5b128 100644
--- a/paper/src/main/java/gg/moonrise/engine/paper/gui/HopperMenu.java
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/HopperMenu.java
@@ -2,16 +2,19 @@
import gg.moonrise.engine.message.util.MiniMessageUtil;
import gg.moonrise.engine.paper.gui.button.Button;
+import gg.moonrise.engine.paper.gui.holder.HopperMenuHolder;
import gg.moonrise.engine.paper.gui.util.MenuInteractionUtil;
import gg.moonrise.engine.paper.gui.util.SafeUtil;
import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
+import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.MenuType;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.Contract;
@@ -20,8 +23,7 @@
/**
* Represents a chest-based GUI menu for players.
* This class manages button placement, click handling, and automatic refreshing
- * of dynamic buttons. Menus are cached per-player and automatically cleaned up
- * when closed.
+ * of dynamic buttons. Menus are routed through their inventory holder.
*/
public abstract class HopperMenu implements UserInterface {
@@ -31,7 +33,7 @@ public abstract class HopperMenu implements UserInterface {
private final Map buttonById = new HashMap<>();
private final Map refreshingButtons = new HashMap<>();
- protected InventoryView inventory;
+ protected Inventory inventory;
private boolean cancelClicks = true;
private final Player player;
@@ -81,7 +83,7 @@ public void onOpen(Player player, InventoryOpenEvent event) {
*/
@Override
public void onClose(Player player, InventoryCloseEvent event) {
- UserInterface.invalidateFromCache(player.getUniqueId());
+ invalidate();
}
/**
@@ -100,7 +102,7 @@ public void onClick(Player player, InventoryClickEvent event) {
*/
@Override
public void open() {
- MenuInteractionUtil.openMenu(player, this, this::refresh, () -> inventory);
+ MenuInteractionUtil.openMenu(player, this::refresh, () -> inventory);
}
/**
@@ -119,7 +121,7 @@ public void invalidate() {
@Override
public void refresh() {
if (inventory == null)
- inventory = MenuType.HOPPER.create(player, title);
+ inventory = createInventory();
clearInventory(inventory);
@@ -169,7 +171,7 @@ public void addButton(int slot, Button button) {
* @param button The button to add
*/
public void addButton(Button button) {
- for (int i = 0; i < inventory.getTopInventory().getSize(); i++) {
+ for (int i = 0; i < InventoryType.HOPPER.getDefaultSize(); i++) {
if (buttons.containsKey(i)) continue;
addButton(i, button);
@@ -231,13 +233,28 @@ public int getButtonCount() {
return buttons.size();
}
+ /**
+ * Get the Inventory associated with this menu
+ * @return The Inventory
+ */
+ @Override
+ public Inventory getInventory() {
+ return inventory;
+ }
+
/**
* Get the InventoryView associated with this menu
* @return The InventoryView
*/
@Override
+ @Deprecated(forRemoval = false)
public InventoryView getView() {
- return inventory;
+ if (inventory == null) return null;
+
+ InventoryView view = player.getOpenInventory();
+ if (!view.getTopInventory().equals(inventory)) return null;
+
+ return view;
}
/**
@@ -265,4 +282,11 @@ public boolean checkCancelClick(int slot) {
return MenuInteractionUtil.checkCancelClick(inventory, buttonById, slot);
}
+ private Inventory createInventory() {
+ HopperMenuHolder holder = new HopperMenuHolder(this);
+ Inventory created = Bukkit.createInventory(holder, InventoryType.HOPPER, title);
+ holder.setInventory(created);
+ return created;
+ }
+
}
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/PaginatedMenu.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/PaginatedMenu.java
index 9936335..a63324d 100644
--- a/paper/src/main/java/gg/moonrise/engine/paper/gui/PaginatedMenu.java
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/PaginatedMenu.java
@@ -3,14 +3,17 @@
import com.google.common.base.Preconditions;
import gg.moonrise.engine.message.util.MiniMessageUtil;
import gg.moonrise.engine.paper.gui.button.Button;
+import gg.moonrise.engine.paper.gui.holder.PaginatedMenuHolder;
import gg.moonrise.engine.paper.gui.util.MenuInteractionUtil;
import gg.moonrise.engine.paper.gui.util.SafeUtil;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
@@ -22,8 +25,7 @@
/**
* Represents a chest-based GUI menu for players.
* This class manages button placement, click handling, and automatic refreshing
- * of dynamic buttons. Menus are cached per-player and automatically cleaned up
- * when closed.
+ * of dynamic buttons. Menus are routed through their inventory holder.
*/
@Slf4j
public abstract class PaginatedMenu implements ChestInterface {
@@ -45,7 +47,7 @@ public abstract class PaginatedMenu implements ChestInterface {
private final List contentSlots = new ArrayList<>();
private final Map refreshingContentButtons = new HashMap<>();
- protected InventoryView inventory;
+ protected Inventory inventory;
private boolean cancelClicks = true;
/**
@@ -183,13 +185,13 @@ public void onOpen(Player player, InventoryOpenEvent event) {
/**
* Handle the event when a player closes the inventory.
- * This method invalidates the menu from the cache to free up resources.
+ * This method invalidates the menu to free up resources.
* @param player The player who closed the inventory.
* @param event The InventoryCloseEvent triggered by the player closing the inventory.
*/
@Override
public void onClose(Player player, InventoryCloseEvent event) {
- UserInterface.invalidateFromCache(player.getUniqueId());
+ invalidate();
}
/**
@@ -205,12 +207,11 @@ public void onClick(Player player, InventoryClickEvent event) {
/**
* Open the chest menu for the player.
- * This method refreshes the menu, invalidates any existing cached menu for the player,
- * caches the current menu, and opens the inventory for the player.
+ * This method refreshes the menu and opens the inventory for the player.
*/
@Override
public void open() {
- MenuInteractionUtil.openMenu(player, this, this::refresh, () -> inventory);
+ MenuInteractionUtil.openMenu(player, this::refresh, () -> inventory);
}
/**
@@ -231,7 +232,7 @@ public void invalidate() {
@Override
public void refresh() {
if (inventory == null)
- inventory = typeFromRows(rows).create(player, title);
+ inventory = createInventory();
clearInventory(inventory);
@@ -451,13 +452,28 @@ public void changePage(int page) {
refresh();
}
+ /**
+ * Get the Inventory of this menu
+ * @return The Inventory
+ */
+ @Override
+ public Inventory getInventory() {
+ return inventory;
+ }
+
/**
* Get the InventoryView of this menu
* @return The InventoryView
*/
@Override
+ @Deprecated(forRemoval = false)
public InventoryView getView() {
- return inventory;
+ if (inventory == null) return null;
+
+ InventoryView view = player.getOpenInventory();
+ if (!view.getTopInventory().equals(inventory)) return null;
+
+ return view;
}
/**
@@ -502,4 +518,11 @@ private void renderButtonToSlot(int slot, Button button) {
button.onAddToInventory(inventory);
}
+ private Inventory createInventory() {
+ PaginatedMenuHolder holder = new PaginatedMenuHolder(this);
+ Inventory created = Bukkit.createInventory(holder, rows * 9, title);
+ holder.setInventory(created);
+ return created;
+ }
+
}
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/UserInterface.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/UserInterface.java
index 932e520..77c6ec4 100644
--- a/paper/src/main/java/gg/moonrise/engine/paper/gui/UserInterface.java
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/UserInterface.java
@@ -4,20 +4,15 @@
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.UUID;
-
/**
* Represents the UserInterface interface.
*/
public interface UserInterface {
- Map CACHE = new HashMap<>();
-
/**
* Called when a player opens an inventory.
*
@@ -52,20 +47,28 @@ public interface UserInterface {
*/
void refresh();
+ /**
+ * Gets the Inventory associated with this UserInterface.
+ *
+ * @return The Inventory of this UserInterface.
+ */
+ Inventory getInventory();
+
/**
* Gets the InventoryView associated with this UserInterface.
*
* @return The InventoryView of this UserInterface.
*/
+ @Deprecated(forRemoval = false)
InventoryView getView();
/**
- * Clears the inventory view by setting all items in the top inventory to null.
- * @param view The InventoryView to clear.
+ * Clears the inventory by setting all items to null.
+ * @param inventory The Inventory to clear.
*/
- default void clearInventory(InventoryView view) {
- for (int i = 0; i < view.getTopInventory().getSize(); i++) {
- view.getTopInventory().setItem(i, null);
+ default void clearInventory(Inventory inventory) {
+ for (int i = 0; i < inventory.getSize(); i++) {
+ inventory.setItem(i, null);
}
}
@@ -86,11 +89,4 @@ default void clearInventory(InventoryView view) {
* Invalidate the current state of the UserInterface.
*/
void invalidate();
-
- static void invalidateFromCache(UUID uuid) {
- UserInterface ui = CACHE.remove(uuid);
- if (ui == null) return;
-
- ui.invalidate();
- }
}
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/button/Button.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/button/Button.java
index e872e18..f21bcd0 100644
--- a/paper/src/main/java/gg/moonrise/engine/paper/gui/button/Button.java
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/button/Button.java
@@ -2,8 +2,10 @@
import lombok.RequiredArgsConstructor;
import org.bukkit.NamespacedKey;
+import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.Contract;
@@ -18,7 +20,7 @@
@RequiredArgsConstructor
public final class Button {
- public static final NamespacedKey KEY = new NamespacedKey("skylands-ui", "button-uuid");
+ public static final NamespacedKey KEY = new NamespacedKey("engine-ui", "button-uuid");
private final Function displayItem;
private final ButtonClickAction clickAction;
@@ -26,15 +28,15 @@ public final class Button {
private final long refreshIntervalTicks;
private final boolean cancelClicks;
private long lastRefreshTime = 0L;
- private InventoryView boundingInventory;
+ private Inventory inventory;
/**
* Executes onAddToInventory.
* @param inventory the inventory
*/
- public void onAddToInventory(InventoryView inventory) {
- this.boundingInventory = inventory;
+ public void onAddToInventory(Inventory inventory) {
+ this.inventory = inventory;
}
/**
@@ -75,10 +77,26 @@ public ItemStack item(Player player) {
/**
* Gets the inventory that this button is bound to.
+ * @return the inventory
+ */
+ public Inventory inventory() {
+ return inventory;
+ }
+
+ /**
+ * Gets the inventory view that this button is bound to.
* @return the bounding inventory
*/
+ @Deprecated(forRemoval = false)
public InventoryView boundingInventory() {
- return boundingInventory;
+ if (inventory == null) return null;
+
+ for (HumanEntity viewer : inventory.getViewers()) {
+ InventoryView view = viewer.getOpenInventory();
+ if (view.getTopInventory().equals(inventory)) return view;
+ }
+
+ return null;
}
/**
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/controller/PlayerInventoryController.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/controller/PlayerInventoryController.java
index 9cee8f6..14b19ff 100644
--- a/paper/src/main/java/gg/moonrise/engine/paper/gui/controller/PlayerInventoryController.java
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/controller/PlayerInventoryController.java
@@ -1,6 +1,7 @@
package gg.moonrise.engine.paper.gui.controller;
import gg.moonrise.engine.paper.gui.UserInterface;
+import gg.moonrise.engine.paper.gui.holder.InteractiveMenuHolder;
import gg.moonrise.moss.spring.SpringComponent;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
@@ -9,9 +10,8 @@
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
-import org.bukkit.event.inventory.InventoryEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
-import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.InventoryHolder;
/**
* Represents the PlayerInventoryController class.
@@ -29,10 +29,10 @@ public class PlayerInventoryController implements Listener {
public void onOpen(InventoryOpenEvent event) {
HumanEntity client = event.getPlayer();
- UserInterface ui = getUserInterface(client, event);
- if (ui == null) return;
+ InteractiveMenuHolder> holder = getMenuHolder(event.getInventory().getHolder());
+ if (holder == null) return;
- ui.onOpen((Player) client, event);
+ holder.onOpen((Player) client, event);
}
@@ -45,10 +45,10 @@ public void onOpen(InventoryOpenEvent event) {
public void onClose(InventoryCloseEvent event) {
HumanEntity client = event.getPlayer();
- UserInterface ui = getUserInterface(client, event);
- if (ui == null) return;
+ InteractiveMenuHolder> holder = getMenuHolder(event.getInventory().getHolder());
+ if (holder == null) return;
- ui.onClose((Player) client, event);
+ holder.onClose((Player) client, event);
}
@@ -61,39 +61,22 @@ public void onClose(InventoryCloseEvent event) {
public void onClick(InventoryClickEvent event) {
HumanEntity client = event.getWhoClicked();
- UserInterface ui = getUserInterface(client, event);
- if (ui == null) return;
+ InteractiveMenuHolder> holder = getMenuHolder(event.getInventory().getHolder());
+ if (holder == null) return;
+
+ UserInterface ui = holder.getMenu();
if (ui.cancelClicks() || ui.checkCancelClick(event.getSlot())) {
event.setCancelled(true);
event.setResult(Event.Result.DENY);
}
- ui.onClick((Player) client, event);
+ holder.onClick((Player) client, event);
}
- /**
- * Retrieves the UserInterface associated with the given HumanEntity and InventoryEvent.
- *
- * @param client The HumanEntity (player) involved in the event.
- * @param event The InventoryEvent to check against.
- * @return The UserInterface if it exists and matches the event's inventory view; otherwise, null.
- */
- private UserInterface getUserInterface(HumanEntity client, InventoryEvent event) {
- UserInterface ui = UserInterface.CACHE.get(client.getUniqueId());
- if (ui == null) return null;
- if (ui.getView() == null) {
- UserInterface.invalidateFromCache(client.getUniqueId());
- return null;
- }
-
- InventoryView inventory = event.getView();
- if (!ui.getView().equals(inventory)) {
- UserInterface.invalidateFromCache(client.getUniqueId());
- return null;
- }
-
- return ui;
+ private InteractiveMenuHolder> getMenuHolder(InventoryHolder holder) {
+ if (!(holder instanceof InteractiveMenuHolder> menuHolder)) return null;
+ return menuHolder;
}
}
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/ChestMenuHolder.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/ChestMenuHolder.java
new file mode 100644
index 0000000..5c67686
--- /dev/null
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/ChestMenuHolder.java
@@ -0,0 +1,46 @@
+package gg.moonrise.engine.paper.gui.holder;
+
+import gg.moonrise.engine.paper.gui.ChestMenu;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+
+@RequiredArgsConstructor
+public final class ChestMenuHolder implements InteractiveMenuHolder {
+
+ private final ChestMenu menu;
+ private Inventory inventory;
+
+ @Override
+ public void onOpen(Player player, InventoryOpenEvent event) {
+ menu.onOpen(player, event);
+ }
+
+ @Override
+ public void onClose(Player player, InventoryCloseEvent event) {
+ menu.onClose(player, event);
+ }
+
+ @Override
+ public void onClick(Player player, InventoryClickEvent event) {
+ menu.onClick(player, event);
+ }
+
+ @Override
+ public ChestMenu getMenu() {
+ return menu;
+ }
+
+ @Override
+ public @NotNull Inventory getInventory() {
+ return inventory;
+ }
+
+ public void setInventory(Inventory inventory) {
+ this.inventory = inventory;
+ }
+}
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/HopperMenuHolder.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/HopperMenuHolder.java
new file mode 100644
index 0000000..f78fc18
--- /dev/null
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/HopperMenuHolder.java
@@ -0,0 +1,46 @@
+package gg.moonrise.engine.paper.gui.holder;
+
+import gg.moonrise.engine.paper.gui.HopperMenu;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+
+@RequiredArgsConstructor
+public final class HopperMenuHolder implements InteractiveMenuHolder {
+
+ private final HopperMenu menu;
+ private Inventory inventory;
+
+ @Override
+ public void onOpen(Player player, InventoryOpenEvent event) {
+ menu.onOpen(player, event);
+ }
+
+ @Override
+ public void onClose(Player player, InventoryCloseEvent event) {
+ menu.onClose(player, event);
+ }
+
+ @Override
+ public void onClick(Player player, InventoryClickEvent event) {
+ menu.onClick(player, event);
+ }
+
+ @Override
+ public HopperMenu getMenu() {
+ return menu;
+ }
+
+ @Override
+ public @NotNull Inventory getInventory() {
+ return inventory;
+ }
+
+ public void setInventory(Inventory inventory) {
+ this.inventory = inventory;
+ }
+}
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/InteractiveMenuHolder.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/InteractiveMenuHolder.java
new file mode 100644
index 0000000..2c2d3dc
--- /dev/null
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/InteractiveMenuHolder.java
@@ -0,0 +1,19 @@
+package gg.moonrise.engine.paper.gui.holder;
+
+import gg.moonrise.engine.paper.gui.UserInterface;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.inventory.InventoryHolder;
+
+public interface InteractiveMenuHolder extends InventoryHolder {
+
+ void onOpen(Player player, InventoryOpenEvent event);
+
+ void onClose(Player player, InventoryCloseEvent event);
+
+ void onClick(Player player, InventoryClickEvent event);
+
+ T getMenu();
+}
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/PaginatedMenuHolder.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/PaginatedMenuHolder.java
new file mode 100644
index 0000000..bc266da
--- /dev/null
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/holder/PaginatedMenuHolder.java
@@ -0,0 +1,46 @@
+package gg.moonrise.engine.paper.gui.holder;
+
+import gg.moonrise.engine.paper.gui.PaginatedMenu;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+
+@RequiredArgsConstructor
+public final class PaginatedMenuHolder implements InteractiveMenuHolder {
+
+ private final PaginatedMenu menu;
+ private Inventory inventory;
+
+ @Override
+ public void onOpen(Player player, InventoryOpenEvent event) {
+ menu.onOpen(player, event);
+ }
+
+ @Override
+ public void onClose(Player player, InventoryCloseEvent event) {
+ menu.onClose(player, event);
+ }
+
+ @Override
+ public void onClick(Player player, InventoryClickEvent event) {
+ menu.onClick(player, event);
+ }
+
+ @Override
+ public PaginatedMenu getMenu() {
+ return menu;
+ }
+
+ @Override
+ public @NotNull Inventory getInventory() {
+ return inventory;
+ }
+
+ public void setInventory(Inventory inventory) {
+ this.inventory = inventory;
+ }
+}
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/util/MenuInteractionUtil.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/util/MenuInteractionUtil.java
index 5eeddc5..dcda2d5 100644
--- a/paper/src/main/java/gg/moonrise/engine/paper/gui/util/MenuInteractionUtil.java
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/util/MenuInteractionUtil.java
@@ -1,12 +1,11 @@
package gg.moonrise.engine.paper.gui.util;
-import gg.moonrise.engine.paper.gui.UserInterface;
import gg.moonrise.engine.paper.gui.button.Button;
import gg.moonrise.engine.paper.scheduler.Scheduler;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.inventory.InventoryClickEvent;
-import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
@@ -45,25 +44,22 @@ public static void processClick(
public static void openMenu(
Player player,
- UserInterface ui,
Runnable refreshAction,
- Supplier inventorySupplier
+ Supplier inventorySupplier
) {
refreshAction.run();
- UserInterface.invalidateFromCache(player.getUniqueId());
- UserInterface.CACHE.put(player.getUniqueId(), ui);
-
Scheduler.entity(player).execute(() -> {
- InventoryView view = inventorySupplier.get();
- if (view != null) view.open();
+ Inventory inventory = inventorySupplier.get();
+ if (inventory != null) player.openInventory(inventory);
}, 1);
}
- public static void refreshButton(InventoryView inventory, int slot, Button button) {
+ public static void refreshButton(Inventory inventory, int slot, Button button) {
if (inventory == null) return;
+ if (inventory.getViewers().isEmpty()) return;
- Player player = (Player) inventory.getPlayer();
+ Player player = (Player) inventory.getViewers().getFirst();
ItemStack stack = button.item(player);
if (stack == null || stack.getType().isAir()) return;
@@ -88,7 +84,7 @@ public static void addButton(
refreshingButtons.put(slot, button);
}
- public static boolean checkCancelClick(InventoryView inventory, Map buttonById, int slot) {
+ public static boolean checkCancelClick(Inventory inventory, Map buttonById, int slot) {
if (inventory == null) return false;
ItemStack item = inventory.getItem(slot);
diff --git a/paper/src/main/java/gg/moonrise/engine/paper/gui/util/SafeUtil.java b/paper/src/main/java/gg/moonrise/engine/paper/gui/util/SafeUtil.java
index abfe706..456291d 100644
--- a/paper/src/main/java/gg/moonrise/engine/paper/gui/util/SafeUtil.java
+++ b/paper/src/main/java/gg/moonrise/engine/paper/gui/util/SafeUtil.java
@@ -1,7 +1,7 @@
package gg.moonrise.engine.paper.gui.util;
import lombok.extern.slf4j.Slf4j;
-import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
/**
@@ -12,18 +12,18 @@ public class SafeUtil {
/**
* Sets an item in the inventory at the specified slot safely.
- * @param view the inventory to set the item in
+ * @param inventory the inventory to set the item in
* @param slot the slot to set the item in
* @param item the item to set
*/
- public static void setInventoryItem(InventoryView view, int slot, ItemStack item) {
- if (view == null) return;
- if (slot < 0 || slot >= view.getTopInventory().getSize()) {
- log.warn("Invalid inventory slot {} for inventory size {}", slot, view.getTopInventory().getSize());
+ public static void setInventoryItem(Inventory inventory, int slot, ItemStack item) {
+ if (inventory == null) return;
+ if (slot < 0 || slot >= inventory.getSize()) {
+ log.warn("Invalid inventory slot {} for inventory size {}", slot, inventory.getSize());
return;
}
- view.getTopInventory().setItem(slot, item);
+ inventory.setItem(slot, item);
}
}
diff --git a/paper/src/test/java/gg/moonrise/engine/paper/cooldown/CooldownsTest.java b/paper/src/test/java/gg/moonrise/engine/paper/cooldown/CooldownsTest.java
new file mode 100644
index 0000000..79dcd1d
--- /dev/null
+++ b/paper/src/test/java/gg/moonrise/engine/paper/cooldown/CooldownsTest.java
@@ -0,0 +1,39 @@
+package gg.moonrise.engine.paper.cooldown;
+
+import gg.moonrise.engine.paper.support.MockBukkitTest;
+import net.kyori.adventure.text.Component;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.bukkit.event.player.PlayerQuitEvent.QuitReason;
+import org.junit.jupiter.api.Test;
+import org.mockbukkit.mockbukkit.entity.PlayerMock;
+
+import java.time.Duration;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CooldownsTest extends MockBukkitTest {
+
+ @Test
+ void cooldownsExpireByTime() {
+ PlayerMock player = server.addPlayer();
+ String key = "action";
+
+ Cooldowns.addCooldown(player.getUniqueId(), key, Duration.ofSeconds(1));
+ assertTrue(Cooldowns.isOnCooldown(player.getUniqueId(), key));
+
+ Cooldowns.addCooldown(player.getUniqueId(), key, -1L);
+ assertFalse(Cooldowns.isOnCooldown(player.getUniqueId(), key));
+ }
+
+ @Test
+ void quitClearsPlayerCooldowns() {
+ PlayerMock player = server.addPlayer();
+ String key = "quit-action";
+ Cooldowns.addCooldown(player.getUniqueId(), key, Duration.ofMinutes(1));
+
+ new Cooldowns().onQuit(new PlayerQuitEvent(player, Component.empty(), QuitReason.DISCONNECTED));
+
+ assertFalse(Cooldowns.isOnCooldown(player.getUniqueId(), key));
+ }
+}
diff --git a/paper/src/test/java/gg/moonrise/engine/paper/data/AABBTest.java b/paper/src/test/java/gg/moonrise/engine/paper/data/AABBTest.java
new file mode 100644
index 0000000..06056c0
--- /dev/null
+++ b/paper/src/test/java/gg/moonrise/engine/paper/data/AABBTest.java
@@ -0,0 +1,46 @@
+package gg.moonrise.engine.paper.data;
+
+import org.bukkit.util.Vector;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class AABBTest {
+
+ @Test
+ void convertsToVectorsAndArrays() {
+ AABB box = new AABB(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
+
+ assertEquals(new Vector(1.0, 2.0, 3.0), box.minVector());
+ assertEquals(new Vector(4.0, 5.0, 6.0), box.maxVector());
+ assertEquals(box.minVector(), box.toVectors().first());
+ assertEquals(box.maxVector(), box.toVectors().second());
+ assertArrayEquals(new double[]{1.0, 2.0, 3.0}, box.minArray());
+ assertArrayEquals(new double[]{4.0, 5.0, 6.0}, box.maxArray());
+ assertArrayEquals(new double[]{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, box.toArrayPair());
+ }
+
+ @Test
+ void insideCheckIncludesBoundaries() {
+ AABB box = new AABB(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
+
+ assertTrue(box.isInside(1.0, 2.0, 3.0));
+ assertTrue(box.isInside(new Vector(4.0, 5.0, 6.0)));
+ assertTrue(box.isInside(2.5, 3.5, 4.5));
+ assertFalse(box.isInside(0.99, 3.5, 4.5));
+ assertFalse(box.isInside(2.5, 5.01, 4.5));
+ }
+
+ @Test
+ void createsFromVectorsAndArrayPair() {
+ AABB fromVectors = AABB.fromVectors(new Vector(1.0, 2.0, 3.0), new Vector(4.0, 5.0, 6.0));
+ AABB fromArray = AABB.fromArrayPair(new double[]{1.0, 2.0, 3.0, 4.0, 5.0, 6.0});
+
+ assertEquals(fromVectors, fromArray);
+ assertThrows(IllegalArgumentException.class, () -> AABB.fromArrayPair(new double[]{1.0, 2.0}));
+ }
+}
diff --git a/paper/src/test/java/gg/moonrise/engine/paper/gui/ChestMenuTest.java b/paper/src/test/java/gg/moonrise/engine/paper/gui/ChestMenuTest.java
new file mode 100644
index 0000000..646138c
--- /dev/null
+++ b/paper/src/test/java/gg/moonrise/engine/paper/gui/ChestMenuTest.java
@@ -0,0 +1,72 @@
+package gg.moonrise.engine.paper.gui;
+
+import gg.moonrise.engine.paper.gui.button.Button;
+import gg.moonrise.engine.paper.support.MockBukkitTest;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.persistence.PersistentDataType;
+import org.junit.jupiter.api.Test;
+import org.mockbukkit.mockbukkit.entity.PlayerMock;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ChestMenuTest extends MockBukkitTest {
+
+ @Test
+ void addRemoveAndSlotBoundsWork() {
+ PlayerMock player = server.addPlayer();
+ TestChestMenu menu = new TestChestMenu(player, 1);
+ Button button = button(Material.DIAMOND);
+
+ menu.addButton(0, button);
+
+ assertTrue(menu.hasButton(0));
+ assertEquals(1, menu.getButtonCount());
+ assertEquals(button, menu.removeButton(0));
+ assertFalse(menu.hasButton(0));
+ assertNull(button.boundingInventory());
+ assertThrows(IllegalArgumentException.class, () -> menu.addButton(9, button));
+ }
+
+ @Test
+ void refreshWritesButtonItemsAndPersistentIds() {
+ PlayerMock player = server.addPlayer();
+ TestChestMenu menu = new TestChestMenu(player, 1);
+ Button button = Button.builder()
+ .item(viewer -> new ItemStack(Material.EMERALD))
+ .refresh(10L)
+ .build();
+
+ menu.addButton(4, button);
+ menu.refresh();
+
+ ItemStack rendered = menu.getView().getTopInventory().getItem(4);
+ assertNotNull(rendered);
+ assertEquals(Material.EMERALD, rendered.getType());
+ assertEquals(button.uuid().toString(), rendered.getPersistentDataContainer().get(Button.KEY, PersistentDataType.STRING));
+ assertEquals(menu.getView(), button.boundingInventory());
+ assertEquals(1, menu.getRefreshingButtons().size());
+ assertTrue(menu.checkCancelClick(4));
+ }
+
+ private static Button button(Material material) {
+ return Button.builder()
+ .item(player -> new ItemStack(material))
+ .build();
+ }
+
+ private static final class TestChestMenu extends ChestMenu {
+
+ private TestChestMenu(Player player, int rows) {
+ super(player, "Test", rows);
+ }
+ }
+}
diff --git a/paper/src/test/java/gg/moonrise/engine/paper/gui/HopperMenuTest.java b/paper/src/test/java/gg/moonrise/engine/paper/gui/HopperMenuTest.java
new file mode 100644
index 0000000..d93eddc
--- /dev/null
+++ b/paper/src/test/java/gg/moonrise/engine/paper/gui/HopperMenuTest.java
@@ -0,0 +1,55 @@
+package gg.moonrise.engine.paper.gui;
+
+import gg.moonrise.engine.paper.gui.button.Button;
+import gg.moonrise.engine.paper.support.MockBukkitTest;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.persistence.PersistentDataType;
+import org.junit.jupiter.api.Test;
+import org.mockbukkit.mockbukkit.entity.PlayerMock;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class HopperMenuTest extends MockBukkitTest {
+
+ @Test
+ void refreshWritesButtonItemsAndPersistentIds() {
+ PlayerMock player = server.addPlayer();
+ TestHopperMenu menu = new TestHopperMenu(player);
+ Button button = Button.builder()
+ .item(viewer -> new ItemStack(Material.HOPPER))
+ .refresh(5L)
+ .build();
+
+ menu.addButton(2, button);
+ menu.refresh();
+
+ ItemStack rendered = menu.getView().getTopInventory().getItem(2);
+ assertNotNull(rendered);
+ assertEquals(Material.HOPPER, rendered.getType());
+ assertEquals(button.uuid().toString(), rendered.getPersistentDataContainer().get(Button.KEY, PersistentDataType.STRING));
+ assertEquals(menu.getView(), button.boundingInventory());
+ assertEquals(1, menu.getRefreshingButtons().size());
+ assertTrue(menu.checkCancelClick(2));
+ }
+
+ @Test
+ void cancelClicksCanBeDisabled() {
+ TestHopperMenu menu = new TestHopperMenu(server.addPlayer());
+
+ assertTrue(menu.cancelClicks());
+ menu.cancelClicks(false);
+ assertFalse(menu.cancelClicks());
+ }
+
+ private static final class TestHopperMenu extends HopperMenu {
+
+ private TestHopperMenu(Player player) {
+ super(player, "Test");
+ }
+ }
+}
diff --git a/paper/src/test/java/gg/moonrise/engine/paper/gui/button/ButtonTest.java b/paper/src/test/java/gg/moonrise/engine/paper/gui/button/ButtonTest.java
new file mode 100644
index 0000000..4fd4ea1
--- /dev/null
+++ b/paper/src/test/java/gg/moonrise/engine/paper/gui/button/ButtonTest.java
@@ -0,0 +1,56 @@
+package gg.moonrise.engine.paper.gui.button;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ButtonTest {
+
+ @Test
+ void builderRequiresDisplayItem() {
+ assertThrows(IllegalStateException.class, () -> Button.builder().build());
+ }
+
+ @Test
+ void builderStoresDefaultsAndRefreshInterval() {
+ Button button = Button.builder()
+ .item(player -> new ItemStack(Material.DIAMOND))
+ .refresh(20L)
+ .build();
+
+ assertNotNull(button.uuid());
+ assertTrue(button.cancelClick());
+ assertEquals(20L, button.refreshIntervalTicks());
+ button.setLastRefreshTime(40L);
+ assertEquals(40L, button.lastRefreshTime());
+ }
+
+ @Test
+ void customCancelAndClickActionAreApplied() {
+ AtomicBoolean clicked = new AtomicBoolean(false);
+ AtomicReference