diff --git a/API/src/main/java/fr/maxlego08/menu/api/ButtonManager.java b/API/src/main/java/fr/maxlego08/menu/api/ButtonManager.java index cf4d5b7cd..87c50b403 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/ButtonManager.java +++ b/API/src/main/java/fr/maxlego08/menu/api/ButtonManager.java @@ -9,6 +9,7 @@ import fr.maxlego08.menu.api.requirement.Action; import fr.maxlego08.menu.api.requirement.Permissible; import fr.maxlego08.menu.api.requirement.Requirement; +import fr.maxlego08.menu.api.rules.loader.RuleLoader; import fr.maxlego08.menu.api.utils.Loader; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.Plugin; @@ -110,6 +111,16 @@ public interface ButtonManager { */ void registerAction(@NotNull ActionLoader actionLoader); + /** + * Registers a {@link RuleLoader} so its rule type (and aliases) can be used in item rules, + * for example in an {@code item_drag} button or an {@code item-rule} requirement. + *

+ * Unlike {@code @AutoRuleLoader}, which only scans the zMenu package, this lets external plugins add rule types. + * + * @param ruleLoader The {@link RuleLoader} instance to register. + */ + void registerRuleLoader(@NotNull RuleLoader ruleLoader); + /** * Retrieves an {@link Optional} {@link ActionLoader} based on a specific action key. * 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 212763cf0..2d536ec06 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java +++ b/API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java @@ -15,6 +15,7 @@ import fr.maxlego08.menu.api.itemstack.MenuItemStackConversion; import fr.maxlego08.menu.api.loader.MaterialLoader; import fr.maxlego08.menu.api.pagination.PaginationManager; +import fr.maxlego08.menu.api.sound.SoundOption; import fr.maxlego08.menu.api.utils.Message; import fr.maxlego08.menu.api.utils.MetaUpdater; import fr.maxlego08.menu.api.utils.Placeholders; @@ -636,4 +637,14 @@ public interface InventoryManager extends Listener { */ PaginationManager getPaginationManager(); + String serializeItemStack(ItemStack itemStack); + + ItemStack deserializeItemStack(String serializedItemStack); + + default SoundOption createSoundOption(String soundName, String categoryName, float pitch, float volume) { + return this.createSoundOption(soundName, categoryName, pitch, volume, false); + } + + SoundOption createSoundOption(String soundName, String categoryName, float pitch, float volume, boolean isCustom); + } \ No newline at end of file diff --git a/API/src/main/java/fr/maxlego08/menu/api/button/PerformButton.java b/API/src/main/java/fr/maxlego08/menu/api/button/PerformButton.java index a4859ce66..91ea97937 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/button/PerformButton.java +++ b/API/src/main/java/fr/maxlego08/menu/api/button/PerformButton.java @@ -1,12 +1,14 @@ package fr.maxlego08.menu.api.button; import com.tcoded.folialib.impl.PlatformScheduler; +import com.tcoded.folialib.wrapper.task.WrappedTask; import fr.maxlego08.menu.api.MenuPlugin; import fr.maxlego08.menu.api.configuration.Configuration; import fr.maxlego08.menu.api.utils.Placeholders; import fr.maxlego08.menu.zcore.logger.Logger; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.jetbrains.annotations.Contract; @@ -15,6 +17,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; public abstract class PerformButton extends SlotButton { @@ -171,11 +174,12 @@ private void execute(@NotNull MenuPlugin plugin, @NotNull Player player, @NotNul player.chat("/" + plugin.parse(player, command)); } else { String finalCommand = command; - Runnable runnable = () -> Bukkit.dispatchCommand(executor, plugin.parse(player, finalCommand)); - if (plugin.isFolia()) { - if (executor instanceof Player) scheduler.runAtEntity(((Player) executor), w -> runnable.run()); - else scheduler.runNextTick(w -> runnable.run()); - } else runnable.run(); + Consumer task = w -> Bukkit.dispatchCommand(executor, plugin.parse(player, finalCommand)); + if (executor instanceof Entity entity) { + scheduler.runAtEntity(entity, task); + } else { + scheduler.runNextTick(task); + } } } catch (Exception exception) { if (Configuration.enableDebug){ diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/CompatibilityUtil.java b/API/src/main/java/fr/maxlego08/menu/api/utils/CompatibilityUtil.java index 0d4886747..7d3192a6a 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/CompatibilityUtil.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/CompatibilityUtil.java @@ -14,6 +14,22 @@ */ public class CompatibilityUtil { + /** + * getTopInventory is called on every move, click and packet, so the lookup is resolved once per view class. + * Holds null when the class has no such method. + */ + private static final ClassValue TOP_INVENTORY_METHODS = new ClassValue<>() { + @Override + protected Method computeValue(@NotNull Class type) { + try { + Method method = type.getMethod("getTopInventory"); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException | RuntimeException e) { + return null; + } + } + }; /** * In API versions 1.20.6 and earlier, InventoryView is a class. @@ -26,24 +42,22 @@ public class CompatibilityUtil { */ @Nullable public static Inventory getTopInventory(@NotNull InventoryEvent event) { - try { - Object view = event.getView(); - Method getTopInventory = view.getClass().getMethod("getTopInventory"); - getTopInventory.setAccessible(true); - return (Inventory) getTopInventory.invoke(view); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - return null; - } + return getTopInventory(event.getView()); } @Nullable public static Inventory getTopInventory(@NotNull Player player) { + return getTopInventory((Object) player.getOpenInventory()); + } + + @Nullable + private static Inventory getTopInventory(@Nullable Object view) { + if (view == null) return null; + Method getTopInventory = TOP_INVENTORY_METHODS.get(view.getClass()); + if (getTopInventory == null) return null; try { - Object view = player.getOpenInventory(); - Method getTopInventory = view.getClass().getMethod("getTopInventory"); - getTopInventory.setAccessible(true); return (Inventory) getTopInventory.invoke(view); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + } catch (InvocationTargetException | IllegalAccessException e) { return null; } } diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/OfflinePlayerCache.java b/API/src/main/java/fr/maxlego08/menu/api/utils/OfflinePlayerCache.java index 3e9444815..4f6e3e82e 100644 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/OfflinePlayerCache.java +++ b/API/src/main/java/fr/maxlego08/menu/api/utils/OfflinePlayerCache.java @@ -19,10 +19,9 @@ public class OfflinePlayerCache { */ @NotNull public static OfflinePlayer get(@NotNull String playerName) { - return offlinePlayer.computeIfAbsent(playerName, name -> { - Player player = Bukkit.getPlayer(name); - return player != null ? player : Bukkit.getOfflinePlayer(name); - }); + Player player = Bukkit.getPlayer(playerName); + if (player != null) return player; + return offlinePlayer.computeIfAbsent(playerName, Bukkit::getOfflinePlayer); } public static void clearCache(){ diff --git a/API/src/main/java/fr/maxlego08/menu/api/utils/Tuples.java b/API/src/main/java/fr/maxlego08/menu/api/utils/Tuples.java deleted file mode 100644 index ba6124606..000000000 --- a/API/src/main/java/fr/maxlego08/menu/api/utils/Tuples.java +++ /dev/null @@ -1,13 +0,0 @@ -package fr.maxlego08.menu.api.utils; - -/** - * Generic tuple class storing two values of different types. - * Useful for returning pairs from methods where a dedicated class is unnecessary. - * - * @param Type of the first element - * @param Type of the second element - * @param first The first element in the tuple. - * @param second The second element in the tuple. - */ -public record Tuples(T first, U second) { -} diff --git a/Common/src/main/java/fr/maxlego08/menu/common/utils/PapiUtils.java b/Common/src/main/java/fr/maxlego08/menu/common/utils/PapiUtils.java index d2b4de264..7598013f7 100644 --- a/Common/src/main/java/fr/maxlego08/menu/common/utils/PapiUtils.java +++ b/Common/src/main/java/fr/maxlego08/menu/common/utils/PapiUtils.java @@ -1,17 +1,22 @@ package fr.maxlego08.menu.common.utils; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import fr.maxlego08.menu.api.configuration.Configuration; import fr.maxlego08.menu.api.placeholder.Placeholder; import org.bukkit.OfflinePlayer; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; public class PapiUtils extends TranslationHelper { + private static final long MAXIMUM_CACHE_SIZE = 10_000; + private static volatile Placeholder placeholder; - private transient final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + private transient volatile Cache cache; + private transient volatile long cacheDuration = -1; private Placeholder use() { if (placeholder == null) { @@ -20,6 +25,20 @@ private Placeholder use() { return placeholder; } + private Cache getCache() { + long duration = Configuration.cachePlaceholderAPI; + Cache current = this.cache; + if (current == null || this.cacheDuration != duration) { + current = CacheBuilder.newBuilder() + .maximumSize(MAXIMUM_CACHE_SIZE) + .expireAfterWrite(Math.max(duration, 0), TimeUnit.MILLISECONDS) + .build(); + this.cache = current; + this.cacheDuration = duration; + } + return current; + } + public Object papi(Object placeHolder, OfflinePlayer player, boolean useCache) { return placeHolder instanceof String ? this.papi((String) placeHolder, player, useCache) : placeHolder; } @@ -28,22 +47,30 @@ public String papi(String placeHolder, OfflinePlayer player, boolean useCache) { if (placeHolder == null) return null; if (player == null) return placeHolder; if (!placeHolder.contains("%")) return placeHolder; - String cacheKey = placeHolder + ";" + player.getUniqueId(); - CacheEntry cachedResult = this.cache.get(cacheKey); - if (Configuration.enableCachePlaceholderAPI && cachedResult != null && cachedResult.isValid() && useCache) { - return cachedResult.value; + if (!Configuration.enableCachePlaceholderAPI) { + return this.parse(placeHolder, player); } - String result = this.use().setPlaceholders(player, placeHolder).replace("%player%", player.getName()); + Cache cache = this.getCache(); + String cacheKey = placeHolder + ";" + player.getUniqueId(); - if (Configuration.enableCachePlaceholderAPI) { - this.cache.put(cacheKey, new CacheEntry(result, System.currentTimeMillis())); + if (useCache) { + String cachedResult = cache.getIfPresent(cacheKey); + if (cachedResult != null) return cachedResult; } + String result = this.parse(placeHolder, player); + cache.put(cacheKey, result); return result; } + private String parse(String placeHolder, OfflinePlayer player) { + String result = this.use().setPlaceholders(player, placeHolder); + String playerName = player.getName(); + return playerName == null ? result : result.replace("%player%", playerName); + } + public List papi(List placeHolders, OfflinePlayer player, boolean useCache) { if (player == null) return placeHolders; List parsed = new ArrayList<>(placeHolders.size()); @@ -52,15 +79,4 @@ public List papi(List placeHolders, OfflinePlayer player, boolea } return parsed; } - - /** - * @param timeStamp Time when the cache entry was created - */ - private record CacheEntry(String value, long timeStamp) { - - public boolean isValid() { - // Check if the cache entry is still valid (not older than 1000 milliseconds) - return System.currentTimeMillis() - this.timeStamp consumer) { - return this.scheduleFix(plugin, delay, delay, consumer); - } - - /** - * Schedules a fixed-rate execution of a task associated with the given plugin. - * - * @param plugin the MenuPlugin for which the task is scheduled - * @param startAt the delay in milliseconds before the task is first executed - * @param delay the delay in milliseconds between successive task executions - * @param consumer the BiConsumer to be executed with the TimerTask and a boolean indicating the task's success - * @return the scheduled TimerTask - */ - protected TimerTask scheduleFix(MenuPlugin plugin, long startAt, long delay, BiConsumer consumer) { - TimerTask task = new TimerTask() { - @Override - public void run() { - if (!plugin.isEnabled()) { - this.cancel(); - consumer.accept(this, false); - return; - } - plugin.getScheduler().runNextTick(w -> consumer.accept(this, true)); - } - }; - TIMER.scheduleAtFixedRate(task, startAt, delay); - return task; - } - protected String color(String message) { if (message == null) return null; - if (NMSUtils.isHexColor()) { - Pattern pattern = Pattern.compile("#[a-fA-F0-9]{6}"); - Matcher matcher = pattern.matcher(message); + if (NMSUtils.isHexColor() && message.indexOf('#') >= 0) { + Matcher matcher = HEX_COLOR_PATTERN.matcher(message); + StringBuilder builder = new StringBuilder(message.length() + 32); while (matcher.find()) { - String color = message.substring(matcher.start(), matcher.end()); - message = message.replace(color, String.valueOf(net.md_5.bungee.api.ChatColor.of(color))); - matcher = pattern.matcher(message); + matcher.appendReplacement(builder, Matcher.quoteReplacement(net.md_5.bungee.api.ChatColor.of(matcher.group()).toString())); } + message = matcher.appendTail(builder).toString(); } return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', message); } public String colorReverse(String message) { - Pattern pattern = Pattern.compile(net.md_5.bungee.api.ChatColor.COLOR_CHAR + "x[a-fA-F0-9-" + net.md_5.bungee.api.ChatColor.COLOR_CHAR + "]{12}"); - Matcher matcher = pattern.matcher(message); + Matcher matcher = HEX_COLOR_REVERSE_PATTERN.matcher(message); + StringBuilder builder = new StringBuilder(message.length()); while (matcher.find()) { - String color = message.substring(matcher.start(), matcher.end()); - String colorReplace = color.replace("§x", "#"); - colorReplace = colorReplace.replace("§", ""); - message = message.replace(color, colorReplace); - matcher = pattern.matcher(message); + String colorReplace = matcher.group().replace("§x", "#").replace("§", ""); + matcher.appendReplacement(builder, Matcher.quoteReplacement(colorReplace)); } - return message.replace("§", "&"); + return matcher.appendTail(builder).toString().replace("§", "&"); } protected List color(List messages) { diff --git a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackUtils.java b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackUtils.java index 5be32e82e..134038e6d 100644 --- a/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackUtils.java +++ b/Common/src/main/java/fr/maxlego08/menu/common/utils/nms/ItemStackUtils.java @@ -10,14 +10,11 @@ import java.io.*; import java.lang.reflect.Constructor; -import java.util.HashMap; -import java.util.Map; import java.util.zip.GZIPInputStream; public class ItemStackUtils { private static final NmsVersion NMS_VERSION = NmsVersion.nmsVersion; - private static final Map itemStackSerialized = new HashMap<>(); private static final MinecraftVersion MINECRAFT_VERSION = MinecraftVersion.getCurrentVersion(); public static String serializeItemStack(ItemStack paramItemStack) { @@ -26,10 +23,6 @@ public static String serializeItemStack(ItemStack paramItemStack) { return "null"; } - if (itemStackSerialized.containsKey(paramItemStack)) { - return itemStackSerialized.get(paramItemStack); - } - if (MinecraftVersion.getCurrentVersion().isAtLeast(MinecraftVersion.parse("1.20.5"))) { return Base64ItemStack.encode(paramItemStack); } @@ -60,9 +53,7 @@ public static String serializeItemStack(ItemStack paramItemStack) { Logger.error(localException); } } - String string = Base64.encode(localByteArrayOutputStream.toByteArray()); - itemStackSerialized.put(paramItemStack, string); - return string; + return Base64.encode(localByteArrayOutputStream.toByteArray()); } public static ItemStack deserializeItemStack(String paramString) { @@ -116,8 +107,6 @@ public static ItemStack deserializeItemStack(String paramString) { } catch (Exception localException) { // Logger.error(localException); } - if (localItemStack != null && !itemStackSerialized.containsKey(localItemStack)) - itemStackSerialized.put(localItemStack, paramString); return localItemStack; } diff --git a/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java b/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java index 388bb618b..0ddeb0cc3 100644 --- a/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java +++ b/Common/src/main/java/fr/maxlego08/menu/inventory/inventories/InventoryDefault.java @@ -6,6 +6,7 @@ import fr.maxlego08.menu.api.MenuItemStack; import fr.maxlego08.menu.api.MenuPlugin; import fr.maxlego08.menu.api.button.Button; +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.engine.ItemButton; @@ -13,7 +14,6 @@ import fr.maxlego08.menu.api.inventory.ChestInventory; import fr.maxlego08.menu.api.inventory.ContainerInventory; import fr.maxlego08.menu.api.pattern.Pattern; -import fr.maxlego08.menu.api.configuration.Configuration; import fr.maxlego08.menu.api.requirement.RefreshRequirement; import fr.maxlego08.menu.api.requirement.Requirement; import fr.maxlego08.menu.api.utils.Placeholders; @@ -31,6 +31,7 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** @@ -38,7 +39,7 @@ */ public class InventoryDefault extends VInventory implements InventoryEngine { - private final Map timers = new ConcurrentHashMap<>(); + private final Map timers = new ConcurrentHashMap<>(); private PerformanceDebug perfDebug; private Inventory inventory; private List oldInventories = new ArrayList<>(); @@ -184,7 +185,7 @@ public void postOpen(MenuPlugin plugin, Player player, int page, Object[] object @Override protected void onClose(InventoryCloseEvent event, MenuPlugin plugin, Player player) { - + this.cancelTimers(); this.inventory.closeInventory(player, this); this.buttons.forEach(button -> button.onInventoryClose(player, this)); } @@ -396,19 +397,9 @@ public void displayFinalButton(@NotNull Button button, @NotNull Placeholders pla if (needRefresh || button.isUpdated()) { long interval = refreshRequirement != null ? refreshRequirement.getUpdateInterval() : this.inventory.getUpdateInterval(); - TimerTask timerTask = this.scheduleFix(this.plugin, interval, (task, canRun) -> { - if (!canRun) { - return; - } - + WrappedTask timerTask = this.plugin.getScheduler().runAtEntityTimer(this.player, () -> { if (this.isClose()) { - task.cancel(); - return; - } - - TimerTask tTask = this.timers.get(slot); - if (!task.equals(tTask)) { - task.cancel(); + this.cancelTimers(); return; } @@ -418,9 +409,12 @@ public void displayFinalButton(@NotNull Button button, @NotNull Placeholders pla } else if (button.isUpdated()) { this.handleUpdatedButton(button, itemStack, slot, placeholders); } - }); + }, interval, interval, TimeUnit.MILLISECONDS); - this.timers.put(slot, timerTask); + WrappedTask previousTask = this.timers.put(slot, timerTask); + if (previousTask != null) { + previousTask.cancel(); + } } } } @@ -428,12 +422,17 @@ public void displayFinalButton(@NotNull Button button, @NotNull Placeholders pla @Override public void cancel(int slot) { - TimerTask task = this.timers.remove(slot); + WrappedTask task = this.timers.remove(slot); if (task != null) { task.cancel(); } } + private void cancelTimers() { + this.timers.values().forEach(WrappedTask::cancel); + this.timers.clear(); + } + @Override public @NotNull PerformanceDebug getPerformanceDebug() { return this.perfDebug != null ? this.perfDebug : PerformanceDebug.disabled(); diff --git a/Hooks/MythicMobs/src/main/java/fr/maxlego08/menu/players/ZData.java b/Common/src/main/java/fr/maxlego08/menu/players/ZData.java similarity index 100% rename from Hooks/MythicMobs/src/main/java/fr/maxlego08/menu/players/ZData.java rename to Common/src/main/java/fr/maxlego08/menu/players/ZData.java diff --git a/Hooks/MythicMobs/src/main/java/fr/maxlego08/menu/hooks/mythicmobs/MythicMobsItemsLoader.java b/Hooks/MythicMobs/src/main/java/fr/maxlego08/menu/hooks/mythicmobs/MythicMobsItemsLoader.java index 337ad8ee9..da4265030 100644 --- a/Hooks/MythicMobs/src/main/java/fr/maxlego08/menu/hooks/mythicmobs/MythicMobsItemsLoader.java +++ b/Hooks/MythicMobs/src/main/java/fr/maxlego08/menu/hooks/mythicmobs/MythicMobsItemsLoader.java @@ -5,6 +5,7 @@ import fr.maxlego08.menu.api.configuration.Configuration; import fr.maxlego08.menu.api.loader.MaterialLoader; import fr.maxlego08.menu.zcore.logger.Logger; +import io.lumine.mythic.bukkit.BukkitAdapter; import io.lumine.mythic.bukkit.MythicBukkit; import io.lumine.mythic.core.items.ItemExecutor; import io.lumine.mythic.core.items.MythicItem; @@ -34,6 +35,6 @@ public ItemStack load(@NonNull Player player, @NonNull YamlConfiguration configu Logger.info("MythicMobsItemsLoader: Mythic item '" + materialString + "' not found, available items: " + this.itemManager.getItemNames()); return null; } - return mythicItem.get().getCachedBaseItem(); + return BukkitAdapter.adapt(mythicItem.get().generateItemStack(1)); } } 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 cfa33bba9..4e2212ebe 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 @@ -27,9 +27,9 @@ import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; -import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; public class PacketUtils implements InventoryListener, PacketManager { @@ -37,7 +37,7 @@ public class PacketUtils implements InventoryListener, PacketManager { private PacketTitleListener packetTitleListener; private PacketEventClickLimiterListener packetEventClickLimiterListener; - public static final Map fakeContents = new HashMap<>(); + public static final Map fakeContents = new ConcurrentHashMap<>(); private final MenuPlugin plugin; private boolean ownsApi; @@ -109,6 +109,7 @@ public void onPostEnable() { @SuppressWarnings("ConstantConditions") public void onDisable() { this.ready = false; + fakeContents.clear(); if (this.ownsApi && PacketEvents.getAPI() != null) { this.api().terminate(); } @@ -139,11 +140,6 @@ public void onInventoryPreOpen(Player player, BaseInventory inventory, int page, @Override public void onInventoryPostOpen(Player player, BaseInventory inventory) { - if (fakeContents.containsKey(inventory.getPlayer().getUniqueId())) { - FakeInventory fakeInventory = fakeContents.get(inventory.getPlayer().getUniqueId()); - Logger.info("OPEN"); - Logger.info(fakeInventory.getSlots().toString()); - } } @Override 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 82a20820e..538e2ad7d 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 @@ -7,6 +7,7 @@ import com.github.retrooper.packetevents.protocol.packettype.PacketTypeCommon; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerOpenWindow; import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWindowItems; +import fr.maxlego08.menu.hooks.packetevents.PacketUtils; import org.bukkit.entity.Player; import java.util.Map; @@ -43,6 +44,7 @@ public void onUserDisconnect(UserDisconnectEvent event) { UUID playerUniqueId = event.getUser().getUUID(); if (playerUniqueId != null) { this.playerPacketInformation.remove(playerUniqueId); + PacketUtils.fakeContents.remove(playerUniqueId); } } diff --git a/README.md b/README.md index 7b32d0f8a..8bd07322e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,17 @@ items: - "&aYou clicked the button!" ``` +### Inventory Types + +The `type` field sets the container type (default `CHEST`). + +| Type | Minimum Minecraft version | Notes | +|---|---|---| +| `CHEST` | 1.20.6 | Default. Size must be a multiple of 9. | +| `ANVIL` | 1.21 | Enables `rename-requirements`. On older versions, zMenu logs an error at load time and the menu opens as a chest, so rename requirements do not run. | + +`CRAFTING` and `PLAYER` are not supported. + ## API Usage **Maven** diff --git a/build.gradle.kts b/build.gradle.kts index 96021249c..8aa43cf17 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -101,6 +101,11 @@ allprojects { options.encoding = "UTF-8" } + tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + } + if (project.name == "API") { tasks.javadoc { options.encoding = "UTF-8" @@ -171,6 +176,7 @@ dependencies { dependencies { compileOnly(project(":Hooks:Paper-26")) + compileOnly(project(":Hooks:Paper-26-3")) } @@ -180,6 +186,9 @@ tasks { dependsOn(paper26.tasks.jar) val jarFile = paper26.tasks.jar.get().archiveFile.get().asFile from(zipTree(jarFile)) + val paper263 = project(":Hooks:Paper-26-3") + dependsOn(paper263.tasks.jar) + from(zipTree(paper263.tasks.jar.get().archiveFile.get().asFile)) relocate("com.tcoded.folialib", "fr.maxlego08.menu.hooks.folialib") relocate("fr.traqueur.currencies", "fr.maxlego08.menu.hooks.currencies") diff --git a/src/main/java/fr/maxlego08/menu/ZButtonManager.java b/src/main/java/fr/maxlego08/menu/ZButtonManager.java index 4c132858d..6599f3955 100644 --- a/src/main/java/fr/maxlego08/menu/ZButtonManager.java +++ b/src/main/java/fr/maxlego08/menu/ZButtonManager.java @@ -13,11 +13,13 @@ import fr.maxlego08.menu.api.requirement.Action; import fr.maxlego08.menu.api.requirement.Permissible; import fr.maxlego08.menu.api.requirement.Requirement; +import fr.maxlego08.menu.api.rules.loader.RuleLoader; import fr.maxlego08.menu.api.utils.Loader; import fr.maxlego08.menu.api.utils.TypedMapAccessor; import fr.maxlego08.menu.common.utils.ZUtils; import fr.maxlego08.menu.loader.RequirementLoader; import fr.maxlego08.menu.loader.ZButtonLoader; +import fr.maxlego08.menu.registry.ZRuleLoaderRegistry; import fr.maxlego08.menu.zcore.logger.Logger; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; @@ -156,11 +158,16 @@ public void registerPermissible(@NotNull PermissibleLoader permissibleLoader) { @Override public void registerAction(@NotNull ActionLoader actionLoader) { for (String value : actionLoader.getKeys()) { - this.actionsLoader.put(value, actionLoader); + this.actionsLoader.put(value.toLowerCase(Locale.ROOT), actionLoader); this.plugin.getInventoryManager().loadElement(InventoryRequirementType.ACTION, value); } } + @Override + public void registerRuleLoader(@NotNull RuleLoader ruleLoader) { + ZRuleLoaderRegistry.getInstance().register(ruleLoader); + } + @Override public @NotNull Optional getActionLoader(@NotNull String key) { return Optional.ofNullable(this.actionsLoader.getOrDefault(key.toLowerCase(Locale.ROOT), null)); diff --git a/src/main/java/fr/maxlego08/menu/ZCommandManager.java b/src/main/java/fr/maxlego08/menu/ZCommandManager.java index 01f94006d..9aeb43a81 100644 --- a/src/main/java/fr/maxlego08/menu/ZCommandManager.java +++ b/src/main/java/fr/maxlego08/menu/ZCommandManager.java @@ -27,11 +27,12 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class ZCommandManager extends ZUtils implements CommandManager { private final Map> commands = new HashMap<>(); - private final Map> playerArguments = new HashMap<>(); + private final Map> playerArguments = new ConcurrentHashMap<>(); private final List commandArgumentValidators = new ArrayList<>(); private final ZMenuPlugin plugin; @@ -107,6 +108,7 @@ public void unregisterCommands(Command command) { public void loadCommands() { this.unregisterCommands(this.plugin); + this.playerArguments.clear(); // Check if file exist File folder = new File(this.plugin.getDataFolder(), "commands"); @@ -182,10 +184,11 @@ public Optional getCommand(Inventory inventory) { @Override public void setPlayerArgument(Player player, String key, String value) { - Map arguments = this.playerArguments.getOrDefault(player.getUniqueId(), new HashMap<>()); - arguments.put(key, value); - this.playerArguments.put(player.getUniqueId(), arguments); + this.playerArguments.computeIfAbsent(player.getUniqueId(), uuid -> new HashMap<>()).put(key, value); + } + public void removePlayerArguments(UUID uuid) { + this.playerArguments.remove(uuid); } @Override diff --git a/src/main/java/fr/maxlego08/menu/ZInventoryManager.java b/src/main/java/fr/maxlego08/menu/ZInventoryManager.java index 669863675..437af0dde 100644 --- a/src/main/java/fr/maxlego08/menu/ZInventoryManager.java +++ b/src/main/java/fr/maxlego08/menu/ZInventoryManager.java @@ -1,5 +1,6 @@ package fr.maxlego08.menu; +import com.cryptomorin.xseries.XSound; import com.tcoded.folialib.impl.PlatformScheduler; import fr.maxlego08.menu.api.*; import fr.maxlego08.menu.api.annotations.*; @@ -21,6 +22,7 @@ import fr.maxlego08.menu.api.itemstack.MenuItemStackConversion; import fr.maxlego08.menu.api.loader.*; import fr.maxlego08.menu.api.pagination.PaginationManager; +import fr.maxlego08.menu.api.sound.SoundOption; import fr.maxlego08.menu.api.utils.*; import fr.maxlego08.menu.api.utils.version.VersionFilter; import fr.maxlego08.menu.button.buttons.ZNoneButton; @@ -43,6 +45,7 @@ import fr.maxlego08.menu.loader.deluxemenu.InventoryDeluxeMenuLoader; import fr.maxlego08.menu.pagination.ZPaginationManager; import fr.maxlego08.menu.requirement.checker.InventoryRequirementChecker; +import fr.maxlego08.menu.sound.ZSoundOption; import fr.maxlego08.menu.zcore.logger.Logger; import fr.maxlego08.menu.zcore.logger.Logger.LogType; import fr.maxlego08.menu.zcore.utils.PerformanceDebug; @@ -152,6 +155,24 @@ public PaginationManager getPaginationManager() { return this.paginationManager; } + @Override + public String serializeItemStack(ItemStack itemStack) { + return ItemStackUtils.serializeItemStack(itemStack); + } + + @Override + public ItemStack deserializeItemStack(String serializedItemStack) { + return ItemStackUtils.deserializeItemStack(serializedItemStack); + } + + @Override + public SoundOption createSoundOption(String soundName, String categoryName, float pitch, float volume, boolean isCustom) { + Optional xSound = XSound.of(soundName); + + return xSound.map(sound -> new ZSoundOption(sound, categoryName, soundName, pitch, volume, isCustom)).orElse(null); + + } + @Override public Inventory loadInventory(Plugin plugin, File file) throws InventoryException { return this.loadInventory(plugin, file, ZInventory.class); diff --git a/src/main/java/fr/maxlego08/menu/ZMenuPlugin.java b/src/main/java/fr/maxlego08/menu/ZMenuPlugin.java index 67dec6446..720da3e3a 100644 --- a/src/main/java/fr/maxlego08/menu/ZMenuPlugin.java +++ b/src/main/java/fr/maxlego08/menu/ZMenuPlugin.java @@ -91,8 +91,8 @@ import org.bukkit.plugin.ServicesManager; import java.io.File; -import java.util.concurrent.TimeUnit; import java.util.*; +import java.util.concurrent.TimeUnit; /** * System to create your plugins very simply Projet with https://github.com/Maxlego08/TemplatePlugin @@ -273,9 +273,7 @@ public void onEnable() { this.clientVersionManager.registerProvider(new ProtocolSupportClientVersionProvider()); }); } - if (this.isPaperOrFolia()) { - this.safeHook("Paper client version", () -> this.clientVersionManager.registerProvider(new PaperProtocolClientVersionProvider())); - } + this.safeHook("Paper client version", () -> this.clientVersionManager.registerProvider(new PaperProtocolClientVersionProvider())); this.addListener(this.clientVersionManager); servicesManager.register(ClientVersionManager.class, this.clientVersionManager, this, ServicePriority.Highest); @@ -526,6 +524,7 @@ public void onDisable() { this.safeDisable("configuration", () -> Configuration.getInstance().save(this.getConfig(), this.configFile)); this.safeDisable("file cache", YamlFileCache::clearCache); + this.safeDisable("api offline player cache", fr.maxlego08.menu.api.utils.OfflinePlayerCache::clearCache); if (this.websiteManager != null) this.safeDisable("website manager", this.websiteManager::onDisable); @@ -565,8 +564,7 @@ public void onDisable() { * @return true if dialog support can be loaded */ private boolean isDialogCapableServer() { - return this.isPaperOrFolia() - && MinecraftVersion.getCurrentVersion().isAtLeast(DIALOG_MINIMUM_SERVER_VERSION) + return MinecraftVersion.getCurrentVersion().isAtLeast(DIALOG_MINIMUM_SERVER_VERSION) && this.hasClass("io{}papermc{}paper{}registry{}data{}dialog{}action{}DialogAction"); } diff --git a/src/main/java/fr/maxlego08/menu/ZTitleAnimationManager.java b/src/main/java/fr/maxlego08/menu/ZTitleAnimationManager.java index d958eb9fd..8ce700081 100644 --- a/src/main/java/fr/maxlego08/menu/ZTitleAnimationManager.java +++ b/src/main/java/fr/maxlego08/menu/ZTitleAnimationManager.java @@ -4,12 +4,12 @@ import fr.maxlego08.menu.api.animation.TitleAnimationLoader; import org.jetbrains.annotations.NotNull; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; public class ZTitleAnimationManager implements TitleAnimationManager { - private final Map loaders = new HashMap<>(); + private final Map loaders = new LinkedHashMap<>(); public ZTitleAnimationManager() { } @@ -25,7 +25,9 @@ public boolean registerLoader(@NotNull String id, @NotNull TitleAnimationLoader @Override public Optional getLoader(@NotNull String id) { - return Optional.ofNullable(this.loaders.get(id)); + TitleAnimationLoader loader = this.loaders.get(id); + if (loader != null) return Optional.of(loader); + return this.loaders.entrySet().stream().filter(entry -> entry.getKey().equalsIgnoreCase(id)).map(Map.Entry::getValue).findFirst(); } @Override diff --git a/src/main/java/fr/maxlego08/menu/button/buttons/ZItemDragButton.java b/src/main/java/fr/maxlego08/menu/button/buttons/ZItemDragButton.java index 71db2e509..79028f2b6 100644 --- a/src/main/java/fr/maxlego08/menu/button/buttons/ZItemDragButton.java +++ b/src/main/java/fr/maxlego08/menu/button/buttons/ZItemDragButton.java @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; public class ZItemDragButton extends ItemDragButton { @@ -41,7 +42,7 @@ public class ZItemDragButton extends ItemDragButton { private boolean enableErrorItem = false; private MenuItemStack errorItems; private boolean useErrorItemCache = true; - private final Map> activeTasks = new HashMap<>(); + private final Map> activeTasks = new ConcurrentHashMap<>(); private int ticks; public ZItemDragButton(@NotNull MenuPlugin plugin) { @@ -123,6 +124,7 @@ public void onRender(Player player, InventoryEngine inventoryEngine) { @Override public void onInventoryClose(@NonNull Player player, @NonNull InventoryEngine inventory) { + this.activeTasks.remove(player.getUniqueId()); this.slots.forEach(slot -> { ItemStack itemStack = inventory.getInventory().getItem(slot); diff --git a/src/main/java/fr/maxlego08/menu/button/loader/ItemDragLoader.java b/src/main/java/fr/maxlego08/menu/button/loader/ItemDragLoader.java index 461475eb1..5b580b05e 100644 --- a/src/main/java/fr/maxlego08/menu/button/loader/ItemDragLoader.java +++ b/src/main/java/fr/maxlego08/menu/button/loader/ItemDragLoader.java @@ -12,6 +12,7 @@ import fr.maxlego08.menu.button.buttons.ZItemDragButton; import fr.maxlego08.menu.itemstack.FullSimilar; import fr.maxlego08.menu.registry.ZRuleLoaderRegistry; +import fr.maxlego08.menu.zcore.logger.Logger; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import org.jspecify.annotations.NonNull; @@ -36,7 +37,10 @@ public Button load(@NonNull YamlConfiguration configuration, @NonNull String pat if (check_element != null) { MenuItemStack menuItemStack = this.inventoryManager.loadItemStack(configuration, path + check_element + ".item.", defaultButtonValue.getFile()); String type = configuration.getString(path + check_element + ".type", "full"); - ItemStackSimilar itemStackSimilar = this.inventoryManager.getItemStackVerification(type).orElseGet(FullSimilar::new); + ItemStackSimilar itemStackSimilar = this.inventoryManager.getItemStackVerification(type).orElseGet(() -> { + Logger.info("Unknown check-item type '" + type + "' for the item_drag button " + path + " in " + defaultButtonValue.getFile().getName() + ", using 'full'.", Logger.LogType.WARNING); + return new FullSimilar(); + }); button.setCheckItem(menuItemStack, itemStackSimilar); } @@ -50,6 +54,10 @@ public Button load(@NonNull YamlConfiguration configuration, @NonNull String pat ConfigurationSection ruleSection = configuration.getConfigurationSection(path + "rule"); if (ruleSection != null) { Rule rule = ZRuleLoaderRegistry.getInstance().loadRule(ruleSection.getValues(true)); + if (rule == null || !rule.isValid()) { + Logger.info("The rule of the item_drag button " + path + " in " + defaultButtonValue.getFile().getName() + " is invalid, the button will refuse every item.", Logger.LogType.WARNING); + rule = context -> false; + } button.setRule(rule); } diff --git a/src/main/java/fr/maxlego08/menu/command/commands/CommandDumplog.java b/src/main/java/fr/maxlego08/menu/command/commands/CommandDumplog.java index 9ef006208..8ee702937 100644 --- a/src/main/java/fr/maxlego08/menu/command/commands/CommandDumplog.java +++ b/src/main/java/fr/maxlego08/menu/command/commands/CommandDumplog.java @@ -40,7 +40,7 @@ public CommandDumplog(ZMenuPlugin plugin) { this.plugin.getScheduler().runAsync(var -> { try { String url = this.uploadLog(logPath).replace("\\",""); - this.plugin.getScheduler().runNextTick(task -> MessageUtils.message(this.plugin, sender, Message.DUMPLOG_SUCCESS, "%url%", this.plugin.isSpigot() ? url : "" + url + "")); + this.plugin.getScheduler().runNextTick(task -> MessageUtils.message(this.plugin, sender, Message.DUMPLOG_SUCCESS, "%url%", "" + url + "")); if (Configuration.enableInformationMessage) { Logger.info("Log uploaded: " + url); } diff --git a/src/main/java/fr/maxlego08/menu/command/commands/reload/CommandMenuReload.java b/src/main/java/fr/maxlego08/menu/command/commands/reload/CommandMenuReload.java index 1d324473f..dba63e985 100644 --- a/src/main/java/fr/maxlego08/menu/command/commands/reload/CommandMenuReload.java +++ b/src/main/java/fr/maxlego08/menu/command/commands/reload/CommandMenuReload.java @@ -7,6 +7,7 @@ import fr.maxlego08.menu.api.configuration.Configuration; import fr.maxlego08.menu.api.pattern.PatternManager; import fr.maxlego08.menu.api.utils.Message; +import fr.maxlego08.menu.api.utils.OfflinePlayerCache; import fr.maxlego08.menu.common.enums.Permission; import fr.maxlego08.menu.common.utils.MessageUtils; import fr.maxlego08.menu.common.utils.cache.YamlFileCache; @@ -16,8 +17,6 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import java.util.Collection; - public class CommandMenuReload extends SubCommand { public CommandMenuReload(ZMenuPlugin plugin) { @@ -33,6 +32,7 @@ public CommandMenuReload(ZMenuPlugin plugin) { InventoryManager inventoryManager = this.plugin.getInventoryManager(); YamlFileCache.clearCache(); + OfflinePlayerCache.clearCache(); this.plugin.getMetaUpdater().clearCache(); this.plugin.loadGlobalPlaceholders(); @@ -56,12 +56,9 @@ public CommandMenuReload(ZMenuPlugin plugin) { ItemManager itemManager = this.plugin.getItemManager(); itemManager.reloadCustomItems(); - Collection onlinePlayers = this.plugin.getServer().getOnlinePlayers(); - this.plugin.getScheduler().runAsync(w->{ - for (Player player : onlinePlayers) { - itemManager.executeCheckInventoryItems(player); - } - }); + for (Player player : this.plugin.getServer().getOnlinePlayers()) { + this.plugin.getScheduler().runAtEntity(player, w -> itemManager.executeCheckInventoryItems(player)); + } this.plugin.getDataManager().loadDefaultValues(); diff --git a/src/main/java/fr/maxlego08/menu/command/commands/website/CommandMenuDownload.java b/src/main/java/fr/maxlego08/menu/command/commands/website/CommandMenuDownload.java index 4a62beb9c..c2e1e401f 100644 --- a/src/main/java/fr/maxlego08/menu/command/commands/website/CommandMenuDownload.java +++ b/src/main/java/fr/maxlego08/menu/command/commands/website/CommandMenuDownload.java @@ -27,10 +27,10 @@ public CommandMenuDownload(ZMenuPlugin plugin) { String link = commandDispatch.getArgument("link", String.class); boolean force = commandDispatch.getArgument("force", Boolean.class, false); - if (Configuration.enableDownloadCommand) { + if (!Configuration.enableDownloadCommand) { Logger.info(commandDispatch.getSender().getName() + " try to download the link '" + link + "' (force: " + force + ") while the command is disable !", Logger.LogType.WARNING); - MessageUtils.message(commandDispatch.getPlugin(), commandDispatch.getSender(), "&cThe command is disable for safety reason, you need to enable it in config.json."); + MessageUtils.message(commandDispatch.getPlugin(), commandDispatch.getSender(), "&cThe command is disable for safety reason, you need to enable it in config.yml."); return CommandResultType.SUCCESS; } diff --git a/src/main/java/fr/maxlego08/menu/inventory/VInventoryManager.java b/src/main/java/fr/maxlego08/menu/inventory/VInventoryManager.java index 1fcba80f0..73ab7699d 100644 --- a/src/main/java/fr/maxlego08/menu/inventory/VInventoryManager.java +++ b/src/main/java/fr/maxlego08/menu/inventory/VInventoryManager.java @@ -2,8 +2,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import fr.maxlego08.menu.zcore.logger.Logger; - +import fr.maxlego08.menu.ZCommandManager; import fr.maxlego08.menu.ZMenuPlugin; import fr.maxlego08.menu.api.VInvManager; import fr.maxlego08.menu.api.configuration.Configuration; @@ -22,17 +21,14 @@ import fr.maxlego08.menu.common.utils.nms.ItemStackUtils; import fr.maxlego08.menu.inventory.inventories.InventoryDefault; import fr.maxlego08.menu.listener.ListenerAdapter; +import fr.maxlego08.menu.zcore.logger.Logger; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityPickupItemEvent; import org.bukkit.event.entity.PlayerDeathEvent; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryDragEvent; -import org.bukkit.event.inventory.InventoryOpenEvent; -import org.bukkit.event.inventory.InventoryType; +import org.bukkit.event.inventory.*; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; @@ -371,6 +367,7 @@ protected void onDamage(EntityDamageEvent event, Player player) { * Closes the open menu when the player left the location where it was opened. */ private void checkDistance(Player player) { + if (!Configuration.closeInventoryOnMove) return; if (CompatibilityUtil.getTopInventory(player).getHolder() instanceof VInventory inventory && this.hasMovedTooFar(player, inventory)) { this.closeGhostInventory(player); } @@ -458,5 +455,8 @@ protected void onQuit(PlayerQuitEvent event, Player player) { } this.cooldownClick.remove(player.getUniqueId()); this.plugin.getInventoryManager().getPaginationManager().removePlayerStates(player.getUniqueId()); + if (this.plugin.getCommandManager() instanceof ZCommandManager commandManager) { + commandManager.removePlayerArguments(player.getUniqueId()); + } } } diff --git a/src/main/java/fr/maxlego08/menu/loader/InventoryLoader.java b/src/main/java/fr/maxlego08/menu/loader/InventoryLoader.java index 6cb9b1026..9a255fd02 100644 --- a/src/main/java/fr/maxlego08/menu/loader/InventoryLoader.java +++ b/src/main/java/fr/maxlego08/menu/loader/InventoryLoader.java @@ -322,6 +322,8 @@ private void loadTitleAnimation(ContainerInventorySetter inventory, YamlConfigur TitleAnimationLoader animationLoader = titleAnimationLoader.get(); TitleAnimation titleAnimation = animationLoader.load(configuration, titleAnimationPath, file); inventory.setTitleAnimation(titleAnimation); + } else { + Logger.info("The title animation of " + file.getName() + " was ignored: no title animation provider " + (pluginName == null ? "is registered" : "named '" + pluginName + "' is registered (is PacketEvents installed?)") + ".", Logger.LogType.WARNING); } } } diff --git a/src/main/java/fr/maxlego08/menu/loader/ZButtonLoader.java b/src/main/java/fr/maxlego08/menu/loader/ZButtonLoader.java index 917e16ef8..dcf213b21 100644 --- a/src/main/java/fr/maxlego08/menu/loader/ZButtonLoader.java +++ b/src/main/java/fr/maxlego08/menu/loader/ZButtonLoader.java @@ -46,9 +46,12 @@ import java.lang.reflect.Constructor; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class ZButtonLoader extends ZUtils implements Loader