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>) configuration.getList(path + "actions", new ArrayList<>()), path + "actions", file, actionPatterns,true,stopOnEmpty); + List actions = buttonManager.loadActions((List>) configuration.getList(path + "actions", new ArrayList<>()), path + "actions", file, actionPatterns, true, stopOnEmpty); button.setActions(actions); @@ -436,8 +436,9 @@ private void loadDefaultPatternValues(YamlConfiguration patternConfig, Map actionPatterns) throws InventoryException { - String[] sectionStrings = {"click_requirement.", "click-requirement.", "click_requirements.", "click-requirements.", "clicks_requirement.", "clicks-requirement.", "clicks_requirements.", "clicks-requirements."}; + private void loadClickRequirements(Button button, YamlConfiguration configuration, String path, File file, List actionPatterns) throws InventoryException { + + String[] sectionStrings = this.plugin.getClickRequirementKeys(); ConfigurationSection section = null; String sectionString = ""; for (String string : sectionStrings) { @@ -450,7 +451,7 @@ private void loadClickRequirements(Button button, YamlConfiguration configuratio Loader loader = new RequirementLoader(this.plugin); List requirements = new ArrayList<>(); for (String key : section.getKeys(false)) { - requirements.add(loader.load(configuration, path + sectionString + key + ".", file,actionPatterns)); + requirements.add(loader.load(configuration, path + sectionString + key + ".", file, actionPatterns)); } button.setClickRequirements(requirements); } diff --git a/src/main/java/fr/maxlego08/menu/loader/actions/ResetPaginationLoader.java b/src/main/java/fr/maxlego08/menu/loader/actions/ResetPaginationLoader.java new file mode 100644 index 00000000..2688258c --- /dev/null +++ b/src/main/java/fr/maxlego08/menu/loader/actions/ResetPaginationLoader.java @@ -0,0 +1,64 @@ +package fr.maxlego08.menu.loader.actions; + +import fr.maxlego08.menu.api.loader.ActionLoader; +import fr.maxlego08.menu.api.pagination.PaginationManager; +import fr.maxlego08.menu.api.requirement.Action; +import fr.maxlego08.menu.api.utils.TypedMapAccessor; +import fr.maxlego08.menu.requirement.actions.ResetPaginationAction; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class ResetPaginationLoader extends ActionLoader { + private final PaginationManager paginationManager; + + public ResetPaginationLoader(PaginationManager paginationManager) { + super("reset-pagination"); + this.paginationManager = paginationManager; + } + + @Override + public @Nullable Action load(@NonNull String path, @NonNull TypedMapAccessor accessor, @NonNull File file) { + String typeStr = accessor.getString("type", "context").toLowerCase(); + + try { + ResetPaginationAction.ResetType resetType = ResetPaginationAction.ResetType.valueOf(typeStr.toUpperCase()); + + if (resetType == ResetPaginationAction.ResetType.CONTEXT) { + List contextIds = loadContextIds(accessor); + if (contextIds.isEmpty()) { + return null; + } + return new ResetPaginationAction(this.paginationManager, resetType, contextIds); + } else if (resetType == ResetPaginationAction.ResetType.ALL) { + return new ResetPaginationAction(this.paginationManager, resetType, null); + } + } catch (IllegalArgumentException e) { + return null; + } + + return null; + } + + private List loadContextIds(@NonNull TypedMapAccessor accessor) { + List contextIds = new ArrayList<>(); + + List contextIdList = accessor.getStringList("context-ids"); + if (!contextIdList.isEmpty()) { + contextIds.addAll(contextIdList); + } + + if (contextIds.isEmpty()) { + String contextId = accessor.getString("context-id"); + if (contextId != null && !contextId.isEmpty()) { + contextIds.add(contextId); + } + } + + return contextIds; + } +} + diff --git a/src/main/java/fr/maxlego08/menu/pagination/ZPaginationManager.java b/src/main/java/fr/maxlego08/menu/pagination/ZPaginationManager.java new file mode 100644 index 00000000..68b7859f --- /dev/null +++ b/src/main/java/fr/maxlego08/menu/pagination/ZPaginationManager.java @@ -0,0 +1,80 @@ +package fr.maxlego08.menu.pagination; + +import fr.maxlego08.menu.api.pagination.PaginationManager; +import fr.maxlego08.menu.api.pagination.PaginationState; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class ZPaginationManager implements PaginationManager { + private final Map> paginationStates = new ConcurrentHashMap<>(); + + @Override + public @NotNull PaginationState getOrCreateState(@NotNull UUID playerId, @NotNull String contextId) { + return paginationStates + .computeIfAbsent(playerId, k -> new ConcurrentHashMap<>()) + .computeIfAbsent(contextId, k -> new PaginationState()); + } + + @Override + public int getPage(@NotNull UUID playerId, @NotNull String contextId) { + PaginationState state = getState(playerId, contextId); + return state != null ? state.getCurrentPage() : 0; + } + + @Override + public void setPage(@NotNull UUID playerId, @NotNull String contextId, int page) { + getOrCreateState(playerId, contextId).setCurrentPage(page); + } + + @Override + public void nextPage(@NotNull UUID playerId, @NotNull String contextId) { + getOrCreateState(playerId, contextId).nextPage(); + } + + @Override + public void previousPage(@NotNull UUID playerId, @NotNull String contextId) { + getOrCreateState(playerId, contextId).previousPage(); + } + + @Override + public void reset(@NotNull UUID playerId, @NotNull String contextId) { + removeState(playerId, contextId); + } + + @Override + public int getMaxPage(@NotNull UUID playerId, @NotNull String contextId) { + PaginationState state = getState(playerId, contextId); + return state != null ? state.getMaxPage() : 0; + } + + @Override + public void setMaxPage(@NotNull UUID playerId, @NotNull String contextId, int maxPage) { + getOrCreateState(playerId, contextId).setMaxPage(maxPage); + } + + @Override + public @Nullable PaginationState getState(@NotNull UUID playerId, @NotNull String contextId) { + Map playerStates = paginationStates.get(playerId); + return playerStates != null ? playerStates.get(contextId) : null; + } + + @Override + public void removeState(@NotNull UUID playerId, @NotNull String contextId) { + Map playerStates = paginationStates.get(playerId); + if (playerStates != null) playerStates.remove(contextId); + } + + @Override + public void removePlayerStates(@NotNull UUID playerId) { + paginationStates.remove(playerId); + } + + @Override + public void clearAll() { + paginationStates.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/fr/maxlego08/menu/placeholder/MenuPlaceholders.java b/src/main/java/fr/maxlego08/menu/placeholder/MenuPlaceholders.java index 1d661600..8cf969bb 100644 --- a/src/main/java/fr/maxlego08/menu/placeholder/MenuPlaceholders.java +++ b/src/main/java/fr/maxlego08/menu/placeholder/MenuPlaceholders.java @@ -20,11 +20,56 @@ public void register(MenuPlugin plugin) { fr.maxlego08.menu.api.placeholder.LocalPlaceholder placeholder = fr.maxlego08.menu.api.placeholder.LocalPlaceholder.getInstance(); var inventoryManager = plugin.getInventoryManager(); + var paginationManager = inventoryManager.getPaginationManager(); + placeholder.register("test", (a, b) -> "&ctest"); + placeholder.register("player_page", (player, s) -> String.valueOf(inventoryManager.getPage(player))); placeholder.register("player_next_page", (player, s) -> String.valueOf(inventoryManager.getPage(player) + 1)); placeholder.register("player_previous_page", (player, s) -> String.valueOf(inventoryManager.getPage(player) - 1)); placeholder.register("player_max_page", (player, s) -> String.valueOf(inventoryManager.getMaxPage(player))); + + placeholder.register("pagination_page_", (player, contextId) -> { + if (paginationManager == null) return "0"; + return String.valueOf(paginationManager.getPage(player.getUniqueId(), contextId)); + }); + + placeholder.register("pagination_page_one_indexed_", (player, contextId) -> { + if (paginationManager == null) return "0"; + return String.valueOf(paginationManager.getPage(player.getUniqueId(), contextId) + 1); + }); + + placeholder.register("pagination_next_page_", (player, contextId) -> { + if (paginationManager == null) return "0"; + return String.valueOf(paginationManager.getPage(player.getUniqueId(), contextId) + 2); + }); + + placeholder.register("pagination_previous_page_", (player, contextId) -> { + if (paginationManager == null) return "0"; + int currentPage = paginationManager.getPage(player.getUniqueId(), contextId); + return String.valueOf(Math.max(0, currentPage - 1)); + }); + + placeholder.register("pagination_max_page_", (player, contextId) -> { + if (paginationManager == null) return "1"; + + String actualContextId = contextId; + int defaultMaxPage = 0; + + if (contextId.contains(":")) { + String[] parts = contextId.split(":", 2); + actualContextId = parts[0]; + try { + defaultMaxPage = Integer.parseInt(parts[1]); + } catch (NumberFormatException ignored) { + } + } + + int storedMaxPage = paginationManager.getMaxPage(player.getUniqueId(), actualContextId); + int maxPage = Math.max(storedMaxPage, defaultMaxPage); + return String.valueOf(maxPage + 1); + }); + placeholder.register("player_previous_inventories", (playeofflinePlayer, s) -> { if (playeofflinePlayer.isOnline()) { Player player = playeofflinePlayer.getPlayer(); diff --git a/src/main/java/fr/maxlego08/menu/requirement/actions/ResetPaginationAction.java b/src/main/java/fr/maxlego08/menu/requirement/actions/ResetPaginationAction.java new file mode 100644 index 00000000..6354eb70 --- /dev/null +++ b/src/main/java/fr/maxlego08/menu/requirement/actions/ResetPaginationAction.java @@ -0,0 +1,74 @@ +package fr.maxlego08.menu.requirement.actions; + +import fr.maxlego08.menu.api.button.Button; +import fr.maxlego08.menu.api.engine.InventoryEngine; +import fr.maxlego08.menu.api.pagination.PaginationManager; +import fr.maxlego08.menu.api.requirement.Action; +import fr.maxlego08.menu.api.utils.Placeholders; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class ResetPaginationAction extends Action { + public enum ResetType { + /** + * Reset a specific pagination context by ID + */ + CONTEXT, + /** + * Reset all pagination contexts for the player + */ + ALL + } + + private final PaginationManager paginationManager; + private final ResetType resetType; + private final List<@NotNull String> contextIds; + + public ResetPaginationAction(@NotNull PaginationManager paginationManager, @NotNull ResetType resetType, @Nullable List<@NotNull String> contextIds) { + this.paginationManager = paginationManager; + this.resetType = resetType; + this.contextIds = contextIds != null ? new ArrayList<>(contextIds) : new ArrayList<>(); + } + + @Override + public void execute(@NotNull Player player, @Nullable Button button, @NotNull InventoryEngine inventoryEngine, @NotNull Placeholders placeholders) { + switch (this.resetType) { + case CONTEXT -> { + for (String contextId : this.contextIds) { + if (!contextId.isEmpty()) { + this.paginationManager.reset(player.getUniqueId(), inventoryEngine.getPlugin().parse(player, placeholders.parse(contextId))); + } + } + } + case ALL -> this.paginationManager.removePlayerStates(player.getUniqueId()); + } + } + + @NotNull + public ResetType getResetType() { + return this.resetType; + } + + @NotNull + public List getContextIds() { + return new ArrayList<>(this.contextIds); + } + + @Nullable + public String getContextId() { + return this.contextIds.isEmpty() ? null : this.contextIds.getFirst(); + } + + @Override + public String toString() { + return "ResetPaginationAction{" + + "resetType=" + resetType + + ", contextIds=" + contextIds + + '}'; + } +} + diff --git a/src/main/java/fr/maxlego08/menu/requirement/checker/InventoryRequirementChecker.java b/src/main/java/fr/maxlego08/menu/requirement/checker/InventoryRequirementChecker.java index 265f04ec..3b4f40b8 100644 --- a/src/main/java/fr/maxlego08/menu/requirement/checker/InventoryRequirementChecker.java +++ b/src/main/java/fr/maxlego08/menu/requirement/checker/InventoryRequirementChecker.java @@ -179,7 +179,7 @@ private void checkButton(YamlConfiguration configuration, InventoryLoadRequireme */ private void checkClickRequirements(YamlConfiguration configuration, InventoryLoadRequirement inventoryLoadRequirement, String path) { - String[] sectionStrings = {"click_requirement.", "click-requirement.", "click_requirements.", "click-requirements.", "clicks_requirement.", "clicks-requirement.", "clicks_requirements.", "clicks-requirements."}; + String[] sectionStrings = this.plugin.getClickRequirementKeys(); ConfigurationSection section = null; String sectionString = ""; for (String string : sectionStrings) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index afba7f9d..d1bd45fb 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -97,6 +97,15 @@ enable-player-commands-as-op-action: false # This setting only applies when enable-player-commands-as-op-action is true. op-grant-method: ATTACHMENT +# Skip close actions on inventory switch. +# Prevents certain action types from executing when a player opens a new zMenu inventory. +# This prevents actions like "open another inventory" or "go back" from reopening old menus. +# List the action types you want to skip (e.g., "inventory", "inv", "back"). +skip-close-actions-on-inventory-switch: + - inventory + - inv + - back + # Enable FastEvent system. # Replaces some Bukkit events with a faster alternative. Enables better performance at the cost of API changes. # Refer to documentation before enabling this. diff --git a/src/main/resources/inventories/pro_inventory.yml b/src/main/resources/inventories/pro_inventory.yml index a8567154..63c6ac0b 100644 --- a/src/main/resources/inventories/pro_inventory.yml +++ b/src/main/resources/inventories/pro_inventory.yml @@ -43,7 +43,7 @@ name: "#454545sʜᴏᴘ" # # In the example if below, if the player to the permission "zmenu.shop.show" then the inventory will open, otherwise a message will be sent # -open_requirement: +open-requirement: requirements: - type: permission permission: "zmenu.shop.show" @@ -85,9 +85,9 @@ items: - 35-44 # slot 35 to 44 # - # The item if below will use the click_requirement and view_requirement. - # You will have the same configuration items as for the open_requirement. - # Attention, for the click_requirement you will have to define several requirements according to your need. + # The item if below will use the click-requirement and view-requirement. + # You will have the same configuration items as for the open-requirement. + # Attention, for the click-requirement you will have to define several requirements according to your need. # You also need to specify clicks. So you can create multiple requirements for each click. # # In the example if below, if the player has enough money, and he has the zmenu.shop.use permission then the item will be displayed, otherwise another item will be displayed. @@ -96,7 +96,7 @@ items: # You want to make a shop with zMenu ? Use zShop https://www.spigotmc.org/resources/zshop-1-8-1-20-advanced-inventory-plugin.74073/ shop1: slot: 22 - view_requirement: + view-requirement: requirements: - type: placeholder placeholder: "%vault_eco_balance%" @@ -104,7 +104,7 @@ items: action: SUPERIOR_OR_EQUAL - type: permission permission: "zmenu.shop.use" - click_requirement: + click-requirement: purchase: clicks: - ANY # or ALL for all clicks type diff --git a/src/main/resources/patterns/playtime_reward.yml b/src/main/resources/patterns/playtime_reward.yml index 336a8ec0..3d9c1547 100644 --- a/src/main/resources/patterns/playtime_reward.yml +++ b/src/main/resources/patterns/playtime_reward.yml @@ -4,7 +4,7 @@ button: updateOnClick: true page: '%page%' slot: '%slot%' - view_requirement: + view-requirement: requirements: - type: placeholder placeholder: "%zmenu_player_value_playtime_level%" @@ -27,7 +27,7 @@ button: sound: ENTITY_VILLAGER_NO else: updateOnClick: true - view_requirement: + view-requirement: requirements: - type: placeholder placeholder: "%zmenu_statistic_hours_played%" @@ -50,7 +50,7 @@ button: - '#ff0000✘ ʏᴏᴜ ᴅᴏɴ’ᴛ ʜᴀᴠᴇ ᴇɴᴏᴜɢʜ ᴘʟᴀʏ ᴛɪᴍᴇ' else: updateOnClick: true - view_requirement: + view-requirement: requirements: - type: placeholder placeholder: "%zmenu_player_value_playtime_level%" @@ -72,7 +72,7 @@ button: updateOnClick: true refreshOnClick: true - click_requirement: + click-requirement: right_click: clicks: - ALL