diff --git a/API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java b/API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java
index 0b536a6d..91b3ea88 100644
--- a/API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java
+++ b/API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java
@@ -3,6 +3,7 @@
import com.tcoded.folialib.impl.PlatformScheduler;
import fr.maxlego08.menu.api.button.Button;
import fr.maxlego08.menu.api.button.ButtonOption;
+import fr.maxlego08.menu.api.button.GenericPaginationButton;
import fr.maxlego08.menu.api.checker.InventoryRequirementType;
import fr.maxlego08.menu.api.enchantment.Enchantments;
import fr.maxlego08.menu.api.engine.InventoryEngine;
@@ -12,6 +13,7 @@
import fr.maxlego08.menu.api.font.FontImage;
import fr.maxlego08.menu.api.itemstack.ItemStackSimilar;
import fr.maxlego08.menu.api.loader.MaterialLoader;
+import fr.maxlego08.menu.api.pagination.PaginationManager;
import fr.maxlego08.menu.api.utils.Message;
import fr.maxlego08.menu.api.utils.MetaUpdater;
import fr.maxlego08.menu.api.utils.Placeholders;
@@ -595,4 +597,28 @@ public interface InventoryManager extends Listener {
*
*/
MenuItemStack loadItemStack(File file, String path, Map map);
+
+ /**
+ * Provides access to the pagination manager for handling paginated content in inventories.
+ *
+ * The PaginationManager is responsible for managing multi-page inventory displays,
+ * allowing buttons to paginate through large collections of items or data. It tracks
+ * the current page for each player and manages navigation between pages.
+ *
+ * This is typically used in conjunction with {@link GenericPaginationButton} or
+ * other paginated button implementations to display collections that exceed a single
+ * inventory page's capacity.
+ *
+ * Example usage:
+ * {@code
+ * PaginationManager manager = inventoryManager.getPaginationManager();
+ * // Use manager to control pagination state
+ * }
+ *
+ * @return An instance of {@link PaginationManager} for managing pagination state in inventories.
+ * @see GenericPaginationButton
+ * @see PaginationManager
+ */
+ PaginationManager getPaginationManager();
+
}
\ No newline at end of file
diff --git a/API/src/main/java/fr/maxlego08/menu/api/MenuPlugin.java b/API/src/main/java/fr/maxlego08/menu/api/MenuPlugin.java
index 0a08a832..6da00b7e 100644
--- a/API/src/main/java/fr/maxlego08/menu/api/MenuPlugin.java
+++ b/API/src/main/java/fr/maxlego08/menu/api/MenuPlugin.java
@@ -21,6 +21,7 @@
import java.io.File;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
public interface MenuPlugin extends Plugin {
@@ -134,6 +135,8 @@ public interface MenuPlugin extends Plugin {
*/
FontImage getFontImage();
+ Optional getPacketManager();
+
/**
* Returns the data manager.
* This method returns the data manager, which is used for managing data related to players.
@@ -263,4 +266,6 @@ public interface MenuPlugin extends Plugin {
ComponentsManager getComponentsManager();
VInvManager getVInventoryManager();
+
+ String[] getClickRequirementKeys();
}
diff --git a/API/src/main/java/fr/maxlego08/menu/api/PacketManager.java b/API/src/main/java/fr/maxlego08/menu/api/PacketManager.java
new file mode 100644
index 00000000..649bc2e8
--- /dev/null
+++ b/API/src/main/java/fr/maxlego08/menu/api/PacketManager.java
@@ -0,0 +1,18 @@
+package fr.maxlego08.menu.api;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public interface PacketManager {
+
+ void onLoad();
+
+ void onEnable();
+
+ void onDisable();
+
+ void editInventoryTitleName(@NotNull Player player, @NotNull Component title);
+
+ void editInventoryTitleName(@NotNull Player player, @NotNull String title);
+}
diff --git a/API/src/main/java/fr/maxlego08/menu/api/button/GenericPaginateButton.java b/API/src/main/java/fr/maxlego08/menu/api/button/GenericPaginateButton.java
new file mode 100644
index 00000000..8895055f
--- /dev/null
+++ b/API/src/main/java/fr/maxlego08/menu/api/button/GenericPaginateButton.java
@@ -0,0 +1,136 @@
+package fr.maxlego08.menu.api.button;
+
+import fr.maxlego08.menu.api.pagination.PaginationManager;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public abstract class GenericPaginateButton extends PaginateButton {
+
+ @NotNull
+ public abstract String getContextId(@NotNull Player player);
+
+
+ @NotNull
+ public abstract PaginationManager getPaginationManager();
+
+ /**
+ * Gets the current page for this button (0-based index).
+ *
+ * @param player the player
+ * @return the current page
+ */
+ public final int getCurrentPage(@NotNull Player player) {
+ return getPaginationManager().getPage(player.getUniqueId(), getContextId(player));
+ }
+
+ /**
+ * Gets the current page (1-based index) for UI purposes.
+ *
+ * @param player the player
+ * @return the current page (1-based)
+ */
+ public final int getCurrentPageOneIndexed(@NotNull Player player) {
+ return getCurrentPage(player) + 1;
+ }
+
+ /**
+ * Sets the current page for this button.
+ *
+ * @param player the player
+ * @param page the page to set (0-based index)
+ */
+ public final void setCurrentPage(@NotNull Player player, int page) {
+ getPaginationManager().setPage(player.getUniqueId(), getContextId(player), page);
+ }
+
+ /**
+ * Advances to the next page if available.
+ *
+ * @param player the player
+ * @return true if advanced, false if already at the last page
+ */
+ public final boolean nextPage(@NotNull Player player) {
+ int currentPage = getCurrentPage(player);
+ int maxPage = getMaxPage(player);
+ if (currentPage < maxPage) {
+ getPaginationManager().nextPage(player.getUniqueId(), getContextId(player));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Goes to the previous page if available.
+ *
+ * @param player the player
+ * @return true if went back, false if already at the first page
+ */
+ public final boolean previousPage(@NotNull Player player) {
+ int currentPage = getCurrentPage(player);
+ if (currentPage > 0) {
+ getPaginationManager().previousPage(player.getUniqueId(), getContextId(player));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Resets pagination to the first page.
+ *
+ * @param player the player
+ */
+ public final void resetPagination(@NotNull Player player) {
+ getPaginationManager().reset(player.getUniqueId(), getContextId(player));
+ }
+
+ /**
+ * Calculates the maximum page number (0-based index).
+ * This caches the page size to avoid multiple getSlots() calls.
+ *
+ * @param player the player
+ * @return the maximum page
+ */
+ public final int getMaxPage(@NotNull Player player) {
+ int totalSize = getPaginationSize(player);
+ int pageSize = getSlots().size();
+ if (pageSize <= 0) return 0;
+ return Math.max(0, (totalSize - 1) / pageSize);
+ }
+
+ /**
+ * Calculates the maximum page number with pre-calculated page size (0-based index).
+ * Use this when page size is already available to avoid redundant getSlots() calls.
+ *
+ * @param player the player
+ * @param pageSize the page size
+ * @return the maximum page
+ */
+ public final int getMaxPage(@NotNull Player player, int pageSize) {
+ if (pageSize <= 0) return 0;
+ int totalSize = getPaginationSize(player);
+ return Math.max(0, (totalSize - 1) / pageSize);
+ }
+
+ /**
+ * Checks if there's a next page available.
+ *
+ * @param player the player
+ * @return true if there's a next page
+ */
+ public final boolean hasNextPage(@NotNull Player player) {
+ int currentPage = getCurrentPage(player);
+ return currentPage < getMaxPage(player);
+ }
+
+ /**
+ * Checks if there's a previous page available.
+ *
+ * @param player the player
+ * @return true if there's a previous page
+ */
+ public final boolean hasPreviousPage(@NotNull Player player) {
+ return getCurrentPage(player) > 0;
+ }
+
+}
+
diff --git a/API/src/main/java/fr/maxlego08/menu/api/button/GenericPaginationButton.java b/API/src/main/java/fr/maxlego08/menu/api/button/GenericPaginationButton.java
new file mode 100644
index 00000000..906458ab
--- /dev/null
+++ b/API/src/main/java/fr/maxlego08/menu/api/button/GenericPaginationButton.java
@@ -0,0 +1,68 @@
+package fr.maxlego08.menu.api.button;
+
+import fr.maxlego08.menu.api.engine.InventoryEngine;
+import fr.maxlego08.menu.api.engine.Pagination;
+import fr.maxlego08.menu.api.utils.Placeholders;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.List;
+
+public abstract class GenericPaginationButton extends GenericPaginateButton {
+
+ /**
+ * Gets the list of elements to paginate.
+ *
+ * @param player the player
+ * @return the list of elements
+ */
+ @NotNull
+ protected abstract List getElements(@NotNull Player player);
+
+ /**
+ * Renders a single element at the given slot.
+ *
+ * @param player the player
+ * @param inventory the inventory engine
+ * @param slot the inventory slot
+ * @param element the element to render
+ * @param placeholders the placeholders
+ */
+ protected abstract void renderElement(
+ @NotNull Player player,
+ @NotNull InventoryEngine inventory,
+ int slot,
+ @NotNull T element,
+ @NotNull Placeholders placeholders);
+
+ @Override
+ public final void onRender(@NotNull Player player, @NotNull InventoryEngine inventory) {
+ List elements = getElements(player);
+ Collection slots = getSlots();
+ int pageSize = slots.size();
+ int currentPage = getCurrentPageOneIndexed(player);
+
+ int maxPage = getMaxPage(player, pageSize);
+
+ getPaginationManager().setMaxPage(player.getUniqueId(), getContextId(player), maxPage);
+
+ Pagination pagination = new Pagination<>();
+ List paginatedElements = pagination.paginate(elements, pageSize, currentPage);
+
+ int slotIndex = 0;
+ for (Integer slot : slots) {
+ if (slotIndex >= paginatedElements.size()) break;
+
+ T element = paginatedElements.get(slotIndex);
+ Placeholders placeholders = new Placeholders();
+ placeholders.register("page", String.valueOf(currentPage));
+ placeholders.register("max_page", String.valueOf(maxPage + 1));
+
+ renderElement(player, inventory, slot, element, placeholders);
+ slotIndex++;
+ }
+ }
+}
+
+
diff --git a/API/src/main/java/fr/maxlego08/menu/api/configuration/Configuration.java b/API/src/main/java/fr/maxlego08/menu/api/configuration/Configuration.java
index 793cf7e9..d097f250 100644
--- a/API/src/main/java/fr/maxlego08/menu/api/configuration/Configuration.java
+++ b/API/src/main/java/fr/maxlego08/menu/api/configuration/Configuration.java
@@ -320,6 +320,9 @@ public class Configuration {
label = "Enable performance debug"
)
public static boolean enablePerformanceDebug = false;
+
+ public static List skipCloseActionsOnInventorySwitch = Arrays.asList("inventory", "inv", "back");
+
public static PerformanceFilterMode performanceFilterMode = PerformanceFilterMode.DISABLED;
public static List performanceFilterOperations = new ArrayList<>();
public static long performanceThresholdMs = 10;
@@ -416,6 +419,7 @@ public void load(@NotNull FileConfiguration fileConfiguration) {
enablePerformanceDebug = fileConfiguration.getBoolean(ConfigPath.ENABLE_PERFORMANCE_DEBUG.getPath(), false);
performanceThresholdMs = fileConfiguration.getLong(ConfigPath.PERFORMANCE_DEBUG_THRESHOLD_MS.getPath(), 10L);
performanceFilterOperations = fileConfiguration.getStringList(ConfigPath.PERFORMANCE_DEBUG_FILTER_OPERATIONS.getPath());
+ skipCloseActionsOnInventorySwitch = fileConfiguration.getStringList(ConfigPath.SKIP_CLOSE_ACTIONS_ON_INVENTORY_SWITCH.getPath());
try {
performanceFilterMode = PerformanceFilterMode.valueOf(fileConfiguration.getString(ConfigPath.PERFORMANCE_DEBUG_FILTER_MODE.getPath(), PerformanceFilterMode.DISABLED.name()).toUpperCase());
} catch (IllegalArgumentException e) {
@@ -470,6 +474,7 @@ public void save(@NotNull FileConfiguration fileConfiguration,@NotNull File file
fileConfiguration.set(ConfigPath.ENABLE_PLAYER_COMMANDS_AS_OP_ACTION.getPath(), enablePlayerCommandsAsOPAction);
fileConfiguration.set(ConfigPath.OP_GRANT_METHOD.getPath(), opGrantMethod.name());
fileConfiguration.set(ConfigPath.ENABLE_TOAST.getPath(), enableToast);
+ fileConfiguration.set(ConfigPath.SKIP_CLOSE_ACTIONS_ON_INVENTORY_SWITCH.getPath(), skipCloseActionsOnInventorySwitch);
fileConfiguration.set(ConfigPath.ENABLE_PACKET_EVENT_CLICK_LIMITER.getPath(), enablePacketEventClickLimiter);
fileConfiguration.set(ConfigPath.PACKET_EVENT_CLICK_LIMITER_MILLISECONDS.getPath(), packetEventClickLimiterMilliseconds);
fileConfiguration.set(ConfigPath.ENABLE_PERFORMANCE_DEBUG.getPath(), enablePerformanceDebug);
@@ -526,6 +531,7 @@ private enum ConfigPath {
ENABLE_PLAYER_COMMANDS_AS_OP_ACTION("enable-player-commands-as-op-action"),
OP_GRANT_METHOD("op-grant-method"),
ENABLE_TOAST("enable-toast"),
+ SKIP_CLOSE_ACTIONS_ON_INVENTORY_SWITCH("skip-close-actions-on-inventory-switch"),
ENABLE_PACKET_EVENT_CLICK_LIMITER("enable-packet-event-click-limiter"),
PACKET_EVENT_CLICK_LIMITER_MILLISECONDS("packet-event-click-limiter-milliseconds"),
diff --git a/API/src/main/java/fr/maxlego08/menu/api/pagination/PaginationManager.java b/API/src/main/java/fr/maxlego08/menu/api/pagination/PaginationManager.java
new file mode 100644
index 00000000..ec8e09ab
--- /dev/null
+++ b/API/src/main/java/fr/maxlego08/menu/api/pagination/PaginationManager.java
@@ -0,0 +1,120 @@
+package fr.maxlego08.menu.api.pagination;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public interface PaginationManager {
+
+ /**
+ * Gets or creates a pagination state for a player and context.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier (e.g., "job:miner", "reward:quest_1")
+ * @return the pagination state
+ */
+ @NotNull PaginationState getOrCreateState(@NotNull UUID playerId, @NotNull String contextId);
+
+ /**
+ * Gets the current page for a player and context.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier
+ * @return the current page (0-based index), or 0 if not found
+ */
+ int getPage(@NotNull UUID playerId, @NotNull String contextId);
+
+ /**
+ * Sets the current page for a player and context.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier
+ * @param page the page to set (0-based index)
+ */
+ void setPage(@NotNull UUID playerId, @NotNull String contextId, int page);
+
+ /**
+ * Increments the page for a player and context.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier
+ */
+ void nextPage(@NotNull UUID playerId, @NotNull String contextId);
+
+ /**
+ * Decrements the page for a player and context.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier
+ */
+ void previousPage(@NotNull UUID playerId, @NotNull String contextId);
+
+ /**
+ * Resets the pagination to page 0 for a player and context.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier
+ */
+ void reset(@NotNull UUID playerId, @NotNull String contextId);
+
+ /**
+ * Gets the pagination state without creating it if it doesn't exist.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier
+ * @return the pagination state, or null if not found
+ */
+ @Nullable PaginationState getState(@NotNull UUID playerId, @NotNull String contextId);
+
+ /**
+ * Returns whether a pagination state exists for a player and context.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier
+ * @return true if a state exists
+ */
+ default boolean hasState(@NotNull UUID playerId, @NotNull String contextId) {
+ return getState(playerId, contextId) != null;
+ }
+
+ /**
+ * Removes a pagination state for a player and context.
+ * Useful when the player logs out or the context is no longer needed.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier
+ */
+ void removeState(@NotNull UUID playerId, @NotNull String contextId);
+
+ /**
+ * Removes all pagination states for a player.
+ * Useful on player logout.
+ *
+ * @param playerId the player's UUID
+ */
+ void removePlayerStates(@NotNull UUID playerId);
+
+ /**
+ * Gets the maximum page for a player and context.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier
+ * @return the maximum page (0-based index), or 0 if not found
+ */
+ int getMaxPage(@NotNull UUID playerId, @NotNull String contextId);
+
+ /**
+ * Sets the maximum page for a player and context.
+ *
+ * @param playerId the player's UUID
+ * @param contextId the context identifier
+ * @param maxPage the maximum page to set (0-based index)
+ */
+ void setMaxPage(@NotNull UUID playerId, @NotNull String contextId, int maxPage);
+
+ /**
+ * Clears all pagination states.
+ */
+ void clearAll();
+}
\ No newline at end of file
diff --git a/API/src/main/java/fr/maxlego08/menu/api/pagination/PaginationState.java b/API/src/main/java/fr/maxlego08/menu/api/pagination/PaginationState.java
new file mode 100644
index 00000000..ade0eb1a
--- /dev/null
+++ b/API/src/main/java/fr/maxlego08/menu/api/pagination/PaginationState.java
@@ -0,0 +1,74 @@
+package fr.maxlego08.menu.api.pagination;
+
+public class PaginationState {
+ private int currentPage;
+ private int maxPage = 0;
+
+ public PaginationState() {
+ this(0);
+ }
+
+ public PaginationState(int page) {
+ this.currentPage = Math.max(0, page);
+ }
+
+ /**
+ * @return the current page (0-based index)
+ */
+ public int getCurrentPage() {
+ return currentPage;
+ }
+
+ /**
+ * @return the current page (1-based index for UI purposes)
+ */
+ public int getCurrentPageOneIndexed() {
+ return this.currentPage + 1;
+ }
+
+ public void setCurrentPage(int page) {
+ this.currentPage = Math.max(0, page);
+ }
+
+ public void nextPage() {
+ this.currentPage++;
+ }
+
+ public void previousPage() {
+ if (this.currentPage > 0) {
+ this.currentPage--;
+ }
+ }
+
+ /**
+ * Gets the maximum page number (0-based index).
+ *
+ * @return the maximum page
+ */
+ public int getMaxPage() {
+ return maxPage;
+ }
+
+ /**
+ * Sets the maximum page number (0-based index).
+ *
+ * @param maxPage the maximum page to set
+ */
+ public void setMaxPage(int maxPage) {
+ this.maxPage = Math.max(0, maxPage);
+ }
+
+ /**
+ * Gets the maximum page number (1-based index for UI purposes).
+ *
+ * @return the maximum page (1-based)
+ */
+ public int getMaxPageOneIndexed() {
+ return this.maxPage + 1;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PaginationState{currentPage=%d, maxPage=%d}", currentPage, maxPage);
+ }
+}
\ No newline at end of file
diff --git a/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/PacketUtils.java b/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/PacketUtils.java
index faf5a219..5149d752 100644
--- a/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/PacketUtils.java
+++ b/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/PacketUtils.java
@@ -3,28 +3,36 @@
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.EventManager;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
+import com.github.retrooper.packetevents.manager.player.PlayerManager;
+import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerOpenWindow;
import fr.maxlego08.menu.api.Inventory;
import fr.maxlego08.menu.api.InventoryListener;
import fr.maxlego08.menu.api.MenuPlugin;
+import fr.maxlego08.menu.api.PacketManager;
import fr.maxlego08.menu.api.configuration.Configuration;
import fr.maxlego08.menu.api.engine.BaseInventory;
import fr.maxlego08.menu.api.engine.InventoryEngine;
import fr.maxlego08.menu.api.engine.ItemButton;
import fr.maxlego08.menu.api.utils.CompatibilityUtil;
+import fr.maxlego08.menu.api.utils.PaperMetaUpdater;
import fr.maxlego08.menu.hooks.packetevents.listener.PacketAnimationListener;
import fr.maxlego08.menu.hooks.packetevents.listener.PacketEventClickLimiterListener;
import fr.maxlego08.menu.hooks.packetevents.listener.PacketTitleListener;
import fr.maxlego08.menu.zcore.logger.Logger;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
+import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
-public class PacketUtils implements InventoryListener {
+public class PacketUtils implements InventoryListener, PacketManager {
+ private final PlayerManager playerManager = PacketEvents.getAPI().getPlayerManager();
+
private PacketAnimationListener packetAnimationListener;
private PacketTitleListener packetTitleListener;
@@ -35,11 +43,13 @@ public PacketUtils(MenuPlugin plugin) {
this.plugin = plugin;
}
+ @Override
public void onLoad() {
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this.plugin));
PacketEvents.getAPI().load();
}
+ @Override
public void onEnable() {
PacketEvents.getAPI().init();
EventManager eventManager = PacketEvents.getAPI().getEventManager();
@@ -52,6 +62,7 @@ public void onEnable() {
}
}
+ @Override
public void onDisable() {
PacketEvents.getAPI().terminate();
}
@@ -110,4 +121,24 @@ public PacketAnimationListener getPacketAnimationListener() {
public PacketTitleListener getPacketTitleListener() {
return packetTitleListener;
}
+
+ @Override
+ public void editInventoryTitleName(@NotNull Player player, @NotNull Component title) {
+ this.packetTitleListener.getPlayerPacketInformation(player.getUniqueId()).ifPresent(playerPacketInformation -> {
+ WrapperPlayServerOpenWindow wrapperPlayServerOpenWindow = playerPacketInformation.getWrapperPlayServerOpenWindow();
+ WrapperPlayServerOpenWindow newWrapperPlayServerOpenWindow1 = new WrapperPlayServerOpenWindow(wrapperPlayServerOpenWindow.getContainerId(),
+ wrapperPlayServerOpenWindow.getType(),
+ title);
+ this.playerManager.sendPacket(player, newWrapperPlayServerOpenWindow1);
+ this.playerManager.sendPacket(player, playerPacketInformation.getWrapperPlayServerWindowItems());
+ });
+ }
+
+ @Override
+ public void editInventoryTitleName(@NotNull Player player, @NotNull String title) {
+ if (this.plugin.getMetaUpdater() instanceof PaperMetaUpdater paperMetaUpdater) {
+ Component component = paperMetaUpdater.getComponent(title);
+ this.editInventoryTitleName(player, component);
+ }
+ }
}
diff --git a/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/action/PacketEventChangeTitleName.java b/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/action/PacketEventChangeTitleName.java
index 8ea30654..b0c38dfb 100644
--- a/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/action/PacketEventChangeTitleName.java
+++ b/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/action/PacketEventChangeTitleName.java
@@ -1,42 +1,26 @@
package fr.maxlego08.menu.hooks.packetevents.action;
-import com.github.retrooper.packetevents.PacketEvents;
-import com.github.retrooper.packetevents.manager.player.PlayerManager;
-import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerOpenWindow;
+import fr.maxlego08.menu.api.PacketManager;
import fr.maxlego08.menu.api.button.Button;
import fr.maxlego08.menu.api.engine.InventoryEngine;
-import fr.maxlego08.menu.api.utils.PaperMetaUpdater;
import fr.maxlego08.menu.api.utils.Placeholders;
import fr.maxlego08.menu.common.utils.ActionHelper;
-import fr.maxlego08.menu.hooks.packetevents.listener.PacketTitleListener;
-import net.kyori.adventure.text.Component;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PacketEventChangeTitleName extends ActionHelper {
- private final PaperMetaUpdater metaUpdater;
- private final PacketTitleListener packetTitleListener;
private final String newInventoryName;
- private final PlayerManager playerManager = PacketEvents.getAPI().getPlayerManager();
+ private final PacketManager packetManager;
- public PacketEventChangeTitleName(@NotNull PaperMetaUpdater metaUpdater, PacketTitleListener packetTitleListener, String newInventoryName) {
- this.metaUpdater = metaUpdater;
- this.packetTitleListener = packetTitleListener;
+
+ public PacketEventChangeTitleName(String newInventoryName, PacketManager packetManager) {
this.newInventoryName = newInventoryName;
+ this.packetManager = packetManager;
}
@Override
protected void execute(@NotNull Player player, @Nullable Button button, @NotNull InventoryEngine inventoryEngine, @NotNull Placeholders placeholders) {
- Component component = metaUpdater.getComponent(papi(placeholders.parse(this.newInventoryName), player));
- PacketTitleListener.PlayerPacketInformation playerPacketInformation = this.packetTitleListener.getPlayerPacketInformation(player.getUniqueId());
- if (playerPacketInformation != null) {
- WrapperPlayServerOpenWindow wrapperPlayServerOpenWindow = playerPacketInformation.getWrapperPlayServerOpenWindow();
- WrapperPlayServerOpenWindow newWrapperPlayServerOpenWindow1 = new WrapperPlayServerOpenWindow(wrapperPlayServerOpenWindow.getContainerId(),
- wrapperPlayServerOpenWindow.getType(),
- component);
- this.playerManager.sendPacket(player, newWrapperPlayServerOpenWindow1);
- this.playerManager.sendPacket(player, playerPacketInformation.getWrapperPlayServerWindowItems());
- }
+ this.packetManager.editInventoryTitleName(player, papi(placeholders.parse(this.newInventoryName), player));
}
}
diff --git a/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/listener/PacketTitleListener.java b/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/listener/PacketTitleListener.java
index 6cdb40b2..8edaa243 100644
--- a/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/listener/PacketTitleListener.java
+++ b/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/listener/PacketTitleListener.java
@@ -10,6 +10,7 @@
import java.util.HashMap;
import java.util.Map;
+import java.util.Optional;
import java.util.UUID;
public class PacketTitleListener implements PacketListener {
@@ -40,30 +41,35 @@ public void setWrapperPlayServerOpenWindow(WrapperPlayServerOpenWindow wrapperPl
public void onPacketSend(PacketSendEvent event) {
//TODO: Only store packets for players who have menus open from zMenu
PacketTypeCommon packetType = event.getPacketType();
- if (packetType == PacketType.Play.Server.OPEN_WINDOW){
- WrapperPlayServerOpenWindow wrapper = new WrapperPlayServerOpenWindow(event);
- Player player = event.getPlayer();
- if (player == null) return;
- UUID playerUniqueId = player.getUniqueId();
- this.playerPacketInformation.computeIfAbsent(playerUniqueId, k -> new PlayerPacketInformation())
- .setWrapperPlayServerOpenWindow(wrapper);
- } else if (packetType == PacketType.Play.Server.CLOSE_WINDOW){
- Player player = event.getPlayer();
- if (player == null) return;
- UUID playerUniqueId = player.getUniqueId();
- this.playerPacketInformation.remove(playerUniqueId);
- } else if (packetType == PacketType.Play.Server.WINDOW_ITEMS){
- WrapperPlayServerWindowItems wrapper = new WrapperPlayServerWindowItems(event);
- Player player = event.getPlayer();
- if (player == null) return;
- UUID playerUniqueId = player.getUniqueId();
- this.playerPacketInformation.computeIfAbsent(playerUniqueId, k -> new PlayerPacketInformation())
- .setWrapperPlayServerWindowItems(wrapper);
+ switch (packetType) {
+ case PacketType.Play.Server.OPEN_WINDOW -> {
+ WrapperPlayServerOpenWindow wrapper = new WrapperPlayServerOpenWindow(event);
+ Player player = event.getPlayer();
+ if (player == null) return;
+ UUID playerUniqueId = player.getUniqueId();
+ this.playerPacketInformation.computeIfAbsent(playerUniqueId, k -> new PlayerPacketInformation())
+ .setWrapperPlayServerOpenWindow(wrapper);
+ }
+ case PacketType.Play.Server.CLOSE_WINDOW -> {
+ Player player = event.getPlayer();
+ if (player == null) return;
+ UUID playerUniqueId = player.getUniqueId();
+ this.playerPacketInformation.remove(playerUniqueId);
+ }
+ case PacketType.Play.Server.WINDOW_ITEMS -> {
+ WrapperPlayServerWindowItems wrapper = new WrapperPlayServerWindowItems(event);
+ Player player = event.getPlayer();
+ if (player == null) return;
+ UUID playerUniqueId = player.getUniqueId();
+ this.playerPacketInformation.computeIfAbsent(playerUniqueId, k -> new PlayerPacketInformation())
+ .setWrapperPlayServerWindowItems(wrapper);
+ }
+ default -> {}
}
}
- public PlayerPacketInformation getPlayerPacketInformation(UUID playerUUID) {
- return this.playerPacketInformation.get(playerUUID);
+ public Optional getPlayerPacketInformation(UUID playerUUID) {
+ return Optional.ofNullable(this.playerPacketInformation.get(playerUUID));
}
diff --git a/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/loader/PacketEventChangeTitleNameLoader.java b/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/loader/PacketEventChangeTitleNameLoader.java
index 7faf1b0e..d9d2e5e6 100644
--- a/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/loader/PacketEventChangeTitleNameLoader.java
+++ b/Hooks/PacketEvents/src/main/java/fr/maxlego08/menu/hooks/packetevents/loader/PacketEventChangeTitleNameLoader.java
@@ -1,29 +1,26 @@
package fr.maxlego08.menu.hooks.packetevents.loader;
+import fr.maxlego08.menu.api.PacketManager;
import fr.maxlego08.menu.api.loader.ActionLoader;
import fr.maxlego08.menu.api.requirement.Action;
-import fr.maxlego08.menu.api.utils.PaperMetaUpdater;
import fr.maxlego08.menu.api.utils.TypedMapAccessor;
import fr.maxlego08.menu.hooks.packetevents.action.PacketEventChangeTitleName;
-import fr.maxlego08.menu.hooks.packetevents.listener.PacketTitleListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
public class PacketEventChangeTitleNameLoader extends ActionLoader {
- private final PaperMetaUpdater metaUpdater;
- private final PacketTitleListener packetTitleListener;
+ private final PacketManager packetManager;
- public PacketEventChangeTitleNameLoader(PaperMetaUpdater metaUpdater, PacketTitleListener packetTitleListener) {
+ public PacketEventChangeTitleNameLoader(PacketManager packetManager) {
super("change-title", "change-title-name");
- this.metaUpdater = metaUpdater;
- this.packetTitleListener = packetTitleListener;
+ this.packetManager = packetManager;
}
@Override
public @Nullable Action load(@NotNull String path, @NotNull TypedMapAccessor accessor, @NotNull File file) {
String newInventoryName = accessor.getString("inventory-name", "menu");
- return new PacketEventChangeTitleName(this.metaUpdater, this.packetTitleListener, newInventoryName);
+ return new PacketEventChangeTitleName(newInventoryName, this.packetManager);
}
}
diff --git a/README.md b/README.md
index 9255d970..bd480564 100644
--- a/README.md
+++ b/README.md
@@ -75,7 +75,7 @@ items:
fr.maxlego08.menu
zmenu-api
- 1.1.1.1
+ 1.1.1.2
provided
```
@@ -87,7 +87,7 @@ repositories {
}
dependencies {
- compileOnly("fr.maxlego08.menu:zmenu-api:1.1.1.1")
+ compileOnly("fr.maxlego08.menu:zmenu-api:1.1.1.2")
}
```
diff --git a/build.gradle.kts b/build.gradle.kts
index ffccb771..328d5785 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -5,7 +5,7 @@ plugins {
}
group = "fr.maxlego08.menu"
-version = "1.1.1.1"
+version = "1.1.1.2"
extra.set("targetFolder", file("target/"))
extra.set("apiFolder", file("target-api/"))
diff --git a/changelog.md b/changelog.md
index ce574a90..7e7ee0ea 100644
--- a/changelog.md
+++ b/changelog.md
@@ -42,6 +42,34 @@
# Unreleased
+# 1.1.1.2
+
+## Bug Fixes
+
+- Fixed `openWithOldInventories` method crash on 1.20 by using `CompatibilityUtil.getTopInventory()` for safe inventory access.
+- Fixed trim pattern and material validation: now uses Bukkit `Registry` instead of the hardcoded `TrimHelper`, with proper error messages listing all available patterns/materials when a key is not found.
+- Fixed null `ItemFlag` entries causing errors when applying flags to item meta.
+- Fixed `EnchantmentGlintOverrideComponent` not handling `false` values correctly — previously only `true` was applied, now both `true` and `false` are respected.
+- Fixed click requirements defaulting to an empty click list when none are specified — now defaults to all click types.
+- Fixed `AttributeWrapper` to support an optional `NamespacedKey` instead of always generating a random UUID, preventing attribute duplication on item rebuild.
+- Fixed database connection logger initialization order in `ZStorageManager`.
+- Fixed item loading from map (`loadItemStack`) to use `MenuItemStackLoader` instead of the removed `MenuItemStackFormMap` class.
+
+## Improvements
+
+- **Command Permissions**: Added dedicated permissions for `CommandMenuEditor` (`ZMENU_EDITOR`), `CommandMenuVersion` (`ZMENU_VERSION`), and `CommandMenuGiveOpenItem` (`ZMENU_GIVE_OPEN_ITEM`).
+- **API**: Added `getClickRequirementKeys()` method to `MenuPlugin` interface, allowing addons to retrieve the supported click requirement configuration keys.
+- **Default Configs**: Updated default configuration files (`pro_inventory.yml`, `playtime_reward.yml`) to use kebab-case (`view-requirement`, `click-requirement`, `open-requirement`) matching current conventions.
+- **Dependencies**: Added `adventure-text-minimessage` as a library dependency in `plugin.yml`.
+
+## Internal Changes
+
+- Removed unused `PlayerSkin` class.
+- Removed unused `MenuItemStackFormMap` class and associated `fromMap` static method.
+- Cleaned up imports and formatting across multiple files.
+
+---
+
# 1.1.1.1
## New Features
diff --git a/src/main/java/fr/maxlego08/menu/ZInventory.java b/src/main/java/fr/maxlego08/menu/ZInventory.java
index 1b3ad212..f8b51e64 100644
--- a/src/main/java/fr/maxlego08/menu/ZInventory.java
+++ b/src/main/java/fr/maxlego08/menu/ZInventory.java
@@ -5,6 +5,7 @@
import fr.maxlego08.menu.api.animation.TitleAnimation;
import fr.maxlego08.menu.api.button.Button;
import fr.maxlego08.menu.api.button.PaginateButton;
+import fr.maxlego08.menu.api.configuration.Configuration;
import fr.maxlego08.menu.api.engine.InventoryEngine;
import fr.maxlego08.menu.api.engine.InventoryResult;
import fr.maxlego08.menu.api.pattern.Pattern;
@@ -273,6 +274,7 @@ public void closeInventory(Player player, InventoryEngine inventoryDefault) {
ZMenuPlugin.getInstance().getScheduler().runAtEntityLater(player, task -> {
InventoryHolder newHolder = CompatibilityUtil.getTopInventory(player).getHolder();
+ boolean isInNewzMenuInventory = newHolder instanceof InventoryDefault;
if (newHolder != null && !(newHolder instanceof InventoryDefault)) {
clearPlayerInventoryButtons(player, inventoryDefault);
@@ -281,10 +283,18 @@ public void closeInventory(Player player, InventoryEngine inventoryDefault) {
this.clearInvType.getOnInventoryClose().accept(inventoriesPlayer, player);
}
}
+ var placeholders = new Placeholders();
+ if (isInNewzMenuInventory) {
+ for (Action action : this.closeActions) {
+ if (!Configuration.skipCloseActionsOnInventorySwitch.contains(action.getType())) {
+ action.preExecute(player, null, inventoryDefault, placeholders);
+ }
+ }
+ } else {
+ this.closeActions.forEach(action -> action.preExecute(player, null, inventoryDefault, placeholders));
+ }
}, 1);
- var placeholders = new Placeholders();
- this.closeActions.forEach(action -> action.preExecute(player, null, inventoryDefault, placeholders));
}
@Override
diff --git a/src/main/java/fr/maxlego08/menu/ZInventoryManager.java b/src/main/java/fr/maxlego08/menu/ZInventoryManager.java
index 54710f90..871eb014 100644
--- a/src/main/java/fr/maxlego08/menu/ZInventoryManager.java
+++ b/src/main/java/fr/maxlego08/menu/ZInventoryManager.java
@@ -18,6 +18,7 @@
import fr.maxlego08.menu.api.itemstack.ItemStackSimilar;
import fr.maxlego08.menu.api.loader.MaterialLoader;
import fr.maxlego08.menu.api.loader.NoneLoader;
+import fr.maxlego08.menu.api.pagination.PaginationManager;
import fr.maxlego08.menu.api.utils.*;
import fr.maxlego08.menu.button.buttons.ZNoneButton;
import fr.maxlego08.menu.button.loader.*;
@@ -43,6 +44,7 @@
import fr.maxlego08.menu.loader.actions.*;
import fr.maxlego08.menu.loader.deluxemenu.InventoryDeluxeMenuLoader;
import fr.maxlego08.menu.loader.permissible.*;
+import fr.maxlego08.menu.pagination.ZPaginationManager;
import fr.maxlego08.menu.requirement.checker.InventoryRequirementChecker;
import fr.maxlego08.menu.zcore.logger.Logger;
import fr.maxlego08.menu.zcore.logger.Logger.LogType;
@@ -75,6 +77,7 @@
import java.util.stream.Stream;
public class ZInventoryManager extends ZUtils implements InventoryManager {
+ private final PaginationManager paginationManager = new ZPaginationManager();
private final Map> inventories = new HashMap<>();
private final Map>> buttonOptions = new HashMap<>();
@@ -134,6 +137,11 @@ public MenuItemStack loadItemStack(File file, String path, Map m
return new MenuItemStackLoader(this).load(configuration, "item", file);
}
+ @Override
+ public PaginationManager getPaginationManager() {
+ return this.paginationManager;
+ }
+
@Override
public Inventory loadInventory(Plugin plugin, File file) throws InventoryException {
return this.loadInventory(plugin, file, ZInventory.class);
@@ -388,6 +396,7 @@ public void loadButtons() {
buttonManager.registerAction(new ActionBarLoader());
buttonManager.registerAction(new RefreshLoader());
buttonManager.registerAction(new RefreshInventoryLoader());
+ buttonManager.registerAction(new ResetPaginationLoader(this.paginationManager));
buttonManager.registerAction(new DiscordLoader());
buttonManager.registerAction(new DiscordComponentV2Loader());
buttonManager.registerAction(new TeleportLoader(this.plugin));
@@ -401,8 +410,9 @@ public void loadButtons() {
buttonManager.registerAction(new DialogLoader(this.plugin, this.plugin.getDialogManager()));
}
if (this.plugin.isEnable(Plugins.PACKETEVENTS)) {
- if (this.plugin.getMetaUpdater() instanceof PaperMetaUpdater paperMetaUpdater)
- buttonManager.registerAction(new PacketEventChangeTitleNameLoader(paperMetaUpdater, this.plugin.getPacketUtils().getPacketTitleListener()));
+
+ Optional packetManager = this.plugin.getPacketManager();
+ packetManager.ifPresent(manager -> buttonManager.registerAction(new PacketEventChangeTitleNameLoader(manager)));
}
// Loading ButtonLoader
@@ -420,6 +430,8 @@ public void loadButtons() {
buttonManager.register(new MainMenuLoader(this.plugin));
buttonManager.register(new JumpLoader(this.plugin));
buttonManager.register(new SwitchLoader(this.plugin));
+ buttonManager.register(new PaginationNextButtonLoader(this.plugin));
+ buttonManager.register(new PaginationPreviousButtonLoader(this.plugin));
// Loading Button Dialog
// Register Button Dialog Body
@@ -815,6 +827,11 @@ public List loadClicks(List loadClicks) {
}
}
});
+
+ if (clickTypes.isEmpty()) { // Use all clicks by default
+ clickTypes.addAll(Configuration.allClicksType);
+ }
+
return clickTypes;
}
diff --git a/src/main/java/fr/maxlego08/menu/ZMenuPlugin.java b/src/main/java/fr/maxlego08/menu/ZMenuPlugin.java
index 1e8a2dab..7562b40e 100644
--- a/src/main/java/fr/maxlego08/menu/ZMenuPlugin.java
+++ b/src/main/java/fr/maxlego08/menu/ZMenuPlugin.java
@@ -118,7 +118,7 @@ public class ZMenuPlugin extends ZPlugin implements MenuPlugin {
private DupeManager dupeManager;
private FontImage fontImage = new EmptyFont();
private MetaUpdater metaUpdater = new ClassicMeta();
- private PacketUtils packetUtils;
+ private PacketManager packetManager;
public static ZMenuPlugin getInstance() {
return instance;
@@ -127,8 +127,10 @@ public static ZMenuPlugin getInstance() {
@Override
public void onLoad() {
if (this.isActive(Plugins.PACKETEVENTS)) {
- this.packetUtils = new PacketUtils(this);
- this.packetUtils.onLoad();
+ this.packetManager = new PacketUtils(this);
+ }
+ if (this.packetManager != null) {
+ this.packetManager.onLoad();
}
}
@@ -140,8 +142,8 @@ public void onEnable() {
this.saveDefaultConfig();
Configuration.getInstance().load(getConfig());
- if (this.packetUtils != null) {
- this.packetUtils.onEnable();
+ if (this.packetManager != null) {
+ this.packetManager.onEnable();
}
this.scheduler = this.foliaLib.getScheduler();
@@ -189,14 +191,14 @@ public void onEnable() {
servicesManager.register(Enchantments.class, this.enchantments, this, ServicePriority.Highest);
servicesManager.register(TitleAnimationManager.class, this.titleAnimationManager, this, ServicePriority.Highest);
- if (this.isPaperOrFolia() && NmsVersion.getCurrentVersion().isDialogsVersion()){
- if (Configuration.enableMiniMessageFormat){
+ if (this.isPaperOrFolia() && NmsVersion.getCurrentVersion().isDialogsVersion()) {
+ if (Configuration.enableMiniMessageFormat) {
Logger.info("Paper server detected, loading Dialogs support");
ConfigManager configManager = new ConfigManager(this);
this.dialogManager = new ZDialogManager(this, configManager);
servicesManager.register(DialogManager.class, this.dialogManager, this, ServicePriority.Highest);
ConfigDialogBuilder configDialogBuilder = new ConfigDialogBuilder("zMenu Config", "zMenu Configuration");
- configManager.registerConfig(configDialogBuilder,Configuration.class, this);
+ configManager.registerConfig(configDialogBuilder, Configuration.class, this);
} else {
Logger.info("Paper server detected but MiniMessage format is disabled, Dialogs support will not be loaded. Enable MiniMessage format in config.yml to use Dialogs.");
}
@@ -366,7 +368,7 @@ private void registerHooks() {
this.inventoryManager.registerMaterialLoader(new MMOItemsLoader());
this.getLogger().info("Registered MMOItems material loader");
}
- if (this.isActive(Plugins.PACKETEVENTS)){
+ if (this.isActive(Plugins.PACKETEVENTS)) {
this.titleAnimationManager.registerLoader("packet-events", new PacketEventTitleAnimationLoader());
}
}
@@ -407,15 +409,16 @@ private List getInventoriesFiles() {
@Override
public void onDisable() {
- if (this.packetUtils != null)
- this.packetUtils.onDisable();
+ if (this.packetManager != null) {
+ this.packetManager.onDisable();
+ }
this.preDisable();
if (this.vinventoryManager != null) this.vinventoryManager.close();
this.inventoriesPlayer.restoreAllInventories();
- Configuration.getInstance().save(getConfig(), this.configFile);
+ Configuration.getInstance().save(getConfig(), this.configFile);
YamlFileCache.clearCache();
@@ -496,6 +499,11 @@ public ComponentsManager getComponentsManager() {
return this.componentsManager;
}
+ @Override
+ public String[] getClickRequirementKeys() {
+ return new String[]{"click_requirement.", "click-requirement.", "click_requirements.", "click-requirements.", "clicks_requirement.", "clicks-requirement.", "clicks_requirements.", "clicks-requirements."};
+ }
+
@Override
public StorageManager getStorageManager() {
return this.storageManager;
@@ -531,6 +539,12 @@ public MenuItemStack loadItemStack(YamlConfiguration configuration, String path,
return this.inventoryManager.loadItemStack(configuration, path, file);
}
+ @Override
+ public Optional getPacketManager() {
+ return Optional.ofNullable(this.packetManager);
+
+ }
+
/**
* Returns the class that will manage the website
*
@@ -549,10 +563,6 @@ public CommandMenu getCommandMenu() {
return commandMenu;
}
- public PacketUtils getPacketUtils() {
- return packetUtils;
- }
-
@Override
public DataManager getDataManager() {
return dataManager;
diff --git a/src/main/java/fr/maxlego08/menu/button/buttons/PaginationButton.java b/src/main/java/fr/maxlego08/menu/button/buttons/PaginationButton.java
new file mode 100644
index 00000000..dc416e2b
--- /dev/null
+++ b/src/main/java/fr/maxlego08/menu/button/buttons/PaginationButton.java
@@ -0,0 +1,42 @@
+package fr.maxlego08.menu.button.buttons;
+
+import fr.maxlego08.menu.api.MenuPlugin;
+import fr.maxlego08.menu.api.button.Button;
+import fr.maxlego08.menu.api.button.GenericPaginateButton;
+import fr.maxlego08.menu.api.engine.InventoryEngine;
+import fr.maxlego08.menu.api.pagination.PaginationManager;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public abstract class PaginationButton extends Button {
+
+ protected final MenuPlugin plugin;
+ protected final PaginationManager manager;
+ protected final String contextId;
+
+ public PaginationButton(@NotNull MenuPlugin plugin, @NotNull String contextId) {
+ this.plugin = plugin;
+ this.manager = plugin.getInventoryManager().getPaginationManager();
+ this.contextId = contextId;
+ }
+
+ @Nullable
+ protected GenericPaginateButton findPaginateButton(@NotNull InventoryEngine inventory, @NotNull Player player) {
+ for (Button button : inventory.getButtons()) {
+ if (button instanceof GenericPaginateButton paginate && paginate.getContextId(player).equals(this.contextId)) {
+ return paginate;
+ }
+ }
+ return null;
+ }
+
+ protected void refreshInventory(@NotNull Player player) {
+ this.plugin.getInventoryManager().updateInventory(player, this.plugin);
+ }
+
+ @Override
+ public boolean isPermanent() {
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/maxlego08/menu/button/buttons/PaginationNextButton.java b/src/main/java/fr/maxlego08/menu/button/buttons/PaginationNextButton.java
new file mode 100644
index 00000000..c480f55d
--- /dev/null
+++ b/src/main/java/fr/maxlego08/menu/button/buttons/PaginationNextButton.java
@@ -0,0 +1,37 @@
+package fr.maxlego08.menu.button.buttons;
+
+import fr.maxlego08.menu.api.MenuPlugin;
+import fr.maxlego08.menu.api.button.GenericPaginateButton;
+import fr.maxlego08.menu.api.engine.InventoryEngine;
+import fr.maxlego08.menu.api.utils.Placeholders;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.jetbrains.annotations.NotNull;
+
+public class PaginationNextButton extends PaginationButton {
+
+ public PaginationNextButton(@NotNull MenuPlugin plugin, @NotNull String contextId) {
+ super(plugin, contextId);
+ }
+
+ protected void onNextPage(@NotNull Player player, @NotNull InventoryEngine inventory) {
+ refreshInventory(player);
+ }
+
+ protected void onCannotNextPage(@NotNull Player player, @NotNull InventoryEngine inventory) {
+ }
+
+ @Override
+ public void onClick(@NotNull Player player, @NotNull InventoryClickEvent event, @NotNull InventoryEngine inventory, int slot, @NotNull Placeholders placeholders) {
+ GenericPaginateButton paginateButton = findPaginateButton(inventory, player);
+ if (paginateButton == null) return;
+
+ int currentPage = this.manager.getPage(player.getUniqueId(), this.contextId);
+ if (currentPage < paginateButton.getMaxPage(player)) {
+ this.manager.nextPage(player.getUniqueId(), this.contextId);
+ onNextPage(player, inventory);
+ } else {
+ onCannotNextPage(player, inventory);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/maxlego08/menu/button/buttons/PaginationPreviousButton.java b/src/main/java/fr/maxlego08/menu/button/buttons/PaginationPreviousButton.java
new file mode 100644
index 00000000..5762a41c
--- /dev/null
+++ b/src/main/java/fr/maxlego08/menu/button/buttons/PaginationPreviousButton.java
@@ -0,0 +1,33 @@
+package fr.maxlego08.menu.button.buttons;
+
+import fr.maxlego08.menu.api.MenuPlugin;
+import fr.maxlego08.menu.api.engine.InventoryEngine;
+import fr.maxlego08.menu.api.utils.Placeholders;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.jetbrains.annotations.NotNull;
+
+public class PaginationPreviousButton extends PaginationButton {
+
+ public PaginationPreviousButton(@NotNull MenuPlugin plugin, @NotNull String contextId) {
+ super(plugin, contextId);
+ }
+
+ protected void onPreviousPage(@NotNull Player player, @NotNull InventoryEngine inventory) {
+ refreshInventory(player);
+ }
+
+ protected void onCannotPreviousPage(@NotNull Player player, @NotNull InventoryEngine inventory) {
+ }
+
+ @Override
+ public void onClick(@NotNull Player player, @NotNull InventoryClickEvent event, @NotNull InventoryEngine inventory, int slot, @NotNull Placeholders placeholders) {
+ int currentPage = this.manager.getPage(player.getUniqueId(), this.contextId);
+ if (currentPage > 0) {
+ this.manager.previousPage(player.getUniqueId(), this.contextId);
+ onPreviousPage(player, inventory);
+ } else {
+ onCannotPreviousPage(player, inventory);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/maxlego08/menu/button/loader/PaginationNextButtonLoader.java b/src/main/java/fr/maxlego08/menu/button/loader/PaginationNextButtonLoader.java
new file mode 100644
index 00000000..f42bb371
--- /dev/null
+++ b/src/main/java/fr/maxlego08/menu/button/loader/PaginationNextButtonLoader.java
@@ -0,0 +1,28 @@
+package fr.maxlego08.menu.button.loader;
+
+import fr.maxlego08.menu.api.MenuPlugin;
+import fr.maxlego08.menu.api.button.Button;
+import fr.maxlego08.menu.api.button.DefaultButtonValue;
+import fr.maxlego08.menu.api.loader.ButtonLoader;
+import fr.maxlego08.menu.button.buttons.PaginationNextButton;
+import fr.maxlego08.menu.zcore.logger.Logger;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class PaginationNextButtonLoader extends ButtonLoader {
+
+ public PaginationNextButtonLoader(MenuPlugin plugin) {
+ super(plugin, "pagination_next");
+ }
+
+ @Override
+ public @Nullable Button load(@NotNull YamlConfiguration configuration, @NotNull String path, @NotNull DefaultButtonValue defaultButtonValue) {
+ String contextId = configuration.getString(path + "context-id");
+ if (contextId == null) {
+ Logger.info("Context-id is required for pagination_next button at path: " + path);
+ return null;
+ }
+ return new PaginationNextButton((MenuPlugin) this.plugin, contextId);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/maxlego08/menu/button/loader/PaginationPreviousButtonLoader.java b/src/main/java/fr/maxlego08/menu/button/loader/PaginationPreviousButtonLoader.java
new file mode 100644
index 00000000..c1855fbf
--- /dev/null
+++ b/src/main/java/fr/maxlego08/menu/button/loader/PaginationPreviousButtonLoader.java
@@ -0,0 +1,28 @@
+package fr.maxlego08.menu.button.loader;
+
+import fr.maxlego08.menu.api.MenuPlugin;
+import fr.maxlego08.menu.api.button.Button;
+import fr.maxlego08.menu.api.button.DefaultButtonValue;
+import fr.maxlego08.menu.api.loader.ButtonLoader;
+import fr.maxlego08.menu.button.buttons.PaginationPreviousButton;
+import fr.maxlego08.menu.zcore.logger.Logger;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class PaginationPreviousButtonLoader extends ButtonLoader {
+
+ public PaginationPreviousButtonLoader(MenuPlugin plugin) {
+ super(plugin, "pagination_previous");
+ }
+
+ @Override
+ public @Nullable Button load(@NotNull YamlConfiguration configuration, @NotNull String path, @NotNull DefaultButtonValue defaultButtonValue) {
+ String contextId = configuration.getString(path + "context-id");
+ if (contextId == null) {
+ Logger.info("Context-id is required for pagination_previous button at path: " + path);
+ return null;
+ }
+ return new PaginationPreviousButton((MenuPlugin) this.plugin, contextId);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/fr/maxlego08/menu/inventory/VInventoryManager.java b/src/main/java/fr/maxlego08/menu/inventory/VInventoryManager.java
index ed3959f7..46bd716b 100644
--- a/src/main/java/fr/maxlego08/menu/inventory/VInventoryManager.java
+++ b/src/main/java/fr/maxlego08/menu/inventory/VInventoryManager.java
@@ -267,5 +267,6 @@ protected void onConnect(PlayerJoinEvent event, Player player) {
@Override
protected void onQuit(PlayerQuitEvent event, Player player) {
this.cooldownClick.remove(player.getUniqueId());
+ this.plugin.getInventoryManager().getPaginationManager().removePlayerStates(player.getUniqueId());
}
}
diff --git a/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java b/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java
index c0c79688..1fc78027 100644
--- a/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java
+++ b/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java
@@ -321,7 +321,7 @@ public void displayFinalButton(@NotNull Button button, @NotNull Placeholders pla
ItemButton itemButton = this.addItem(button.isPlayerInventory(), slot, itemStack);
perfDebug.end();
- if (itemButton != null && button.isClickable()) {
+ if (button.isClickable()) {
itemButton.setClick(event -> {
if (event.getClick() == ClickType.DOUBLE_CLICK) return;
diff --git a/src/main/java/fr/maxlego08/menu/loader/ZButtonLoader.java b/src/main/java/fr/maxlego08/menu/loader/ZButtonLoader.java
index c71d68d5..54b21fbb 100644
--- a/src/main/java/fr/maxlego08/menu/loader/ZButtonLoader.java
+++ b/src/main/java/fr/maxlego08/menu/loader/ZButtonLoader.java
@@ -390,7 +390,7 @@ public Button load(@NonNull YamlConfiguration configuration, @NonNull String pat
loadRefreshRequirements(button, configuration, path, file);
// Load actions
boolean stopOnEmpty = configuration.getBoolean(path + "stop-on-empty", true);
- List actions = buttonManager.loadActions((List