From 1752bbd658fde6181130417cf31a0a6f5412934b Mon Sep 17 00:00:00 2001 From: R00tB33rMan Date: Wed, 20 Aug 2025 00:27:26 -0400 Subject: [PATCH 01/13] Implement UniversalScheduler Folia Support --- build.gradle.kts | 1 + gradle/libs.versions.toml | 2 +- .../extendedclip/deluxemenus/DeluxeMenus.java | 19 +- .../deluxemenus/action/ClickActionTask.java | 656 +++++++++--------- .../command/DeluxeMenusCommand.java | 1 - .../command/subcommand/ExecuteCommand.java | 1 - .../command/subcommand/RefreshCommand.java | 2 +- .../deluxemenus/config/DeluxeMenusConfig.java | 2 +- .../deluxemenus/dupe/DupeFixer.java | 28 +- .../events/DeluxeMenusOpenMenuEvent.java | 1 - .../deluxemenus/hooks/BaseHeadHook.java | 1 - .../hooks/ExecutableItemsHook.java | 1 - .../deluxemenus/hooks/ItemsAdderHook.java | 1 - .../deluxemenus/hooks/MMOItemsHook.java | 4 +- .../deluxemenus/hooks/NamedHeadHook.java | 1 - .../deluxemenus/listener/PlayerListener.java | 5 +- .../extendedclip/deluxemenus/menu/Menu.java | 39 +- .../deluxemenus/menu/MenuHolder.java | 157 +++-- .../deluxemenus/menu/MenuItem.java | 12 +- .../requirement/JavascriptRequirement.java | 2 +- .../scheduler/UniversalRunnable.java | 176 +++++ .../scheduler/UniversalScheduler.java | 42 ++ .../scheduler/bukkit/BukkitScheduledTask.java | 71 ++ .../scheduler/bukkit/BukkitScheduler.java | 129 ++++ .../scheduler/folia/FoliaScheduledTask.java | 57 ++ .../scheduler/folia/FoliaScheduler.java | 220 ++++++ .../scheduler/paper/PaperScheduler.java | 41 ++ .../scheduling/schedulers/TaskScheduler.java | 346 +++++++++ .../scheduling/tasks/MyScheduledTask.java | 54 ++ .../deluxemenus/scheduler/utils/JavaUtil.java | 12 + .../updatechecker/UpdateChecker.java | 19 +- .../deluxemenus/utils/ItemUtils.java | 1 - .../deluxemenus/utils/StringUtils.java | 1 - src/main/resources/plugin.yml | 3 +- 34 files changed, 1624 insertions(+), 484 deletions(-) create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalRunnable.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalScheduler.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduledTask.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduler.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduledTask.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduler.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/paper/PaperScheduler.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/schedulers/TaskScheduler.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/tasks/MyScheduledTask.java create mode 100644 src/main/java/com/extendedclip/deluxemenus/scheduler/utils/JavaUtil.java diff --git a/build.gradle.kts b/build.gradle.kts index 643c40c2..70dcaa54 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,6 +20,7 @@ repositories { maven("https://nexus.phoenixdevt.fr/repository/maven-public/") maven("https://repo.nexomc.com/releases/") maven("https://repo.oraxen.com/releases") + maven("https://repo.papermc.io/repository/maven-public/") maven("https://jitpack.io") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd6d8d7d..12eab0ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ adventure-minimessage = "4.24.0" [libraries] # Compile only -spigot = { module = "org.spigotmc:spigot-api", version.ref = "spigot" } +spigot = { module = "io.papermc.paper:paper-api", version.ref = "spigot" } vault = { module = "com.github.milkbowl:VaultAPI", version.ref = "vault" } authlib = { module = "com.mojang:authlib", version.ref = "authlib" } headdb = { module = "com.arcaniax:HeadDatabase-API", version.ref = "headdb" } diff --git a/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java b/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java index f812f937..7196d51e 100644 --- a/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java +++ b/src/main/java/com/extendedclip/deluxemenus/DeluxeMenus.java @@ -15,6 +15,8 @@ import com.extendedclip.deluxemenus.nbt.NbtProvider; import com.extendedclip.deluxemenus.persistentmeta.PersistentMetaHandler; import com.extendedclip.deluxemenus.placeholder.Expansion; +import com.extendedclip.deluxemenus.scheduler.UniversalScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.updatechecker.UpdateChecker; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.Messages; @@ -35,7 +37,13 @@ import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; @@ -59,6 +67,9 @@ public class DeluxeMenus extends JavaPlugin { private final GeneralConfig generalConfig = new GeneralConfig(this); private DeluxeMenusConfig menuConfig; + @NotNull + private final TaskScheduler scheduler = UniversalScheduler.getScheduler(this); + @Override public void onLoad() { if (NbtProvider.isAvailable()) { @@ -113,7 +124,7 @@ public void onEnable() { public void onDisable() { Bukkit.getMessenger().unregisterOutgoingPluginChannel(this, "BungeeCord"); - Bukkit.getScheduler().cancelTasks(this); + scheduler.cancelTasks(this); if (this.audiences != null) { this.audiences.close(); @@ -217,6 +228,10 @@ public GeneralConfig getGeneralConfig() { return generalConfig; } + public @NotNull TaskScheduler getScheduler() { + return scheduler; + } + private boolean hookIntoPlaceholderAPI() { final boolean canHook = Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null; if (!canHook) { diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index d0feb75c..6e717ba5 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -4,29 +4,18 @@ import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.persistentmeta.PersistentMetaHandler; -import com.extendedclip.deluxemenus.utils.AdventureUtils; -import com.extendedclip.deluxemenus.utils.DebugLevel; -import com.extendedclip.deluxemenus.utils.ExpUtils; -import com.extendedclip.deluxemenus.utils.SoundUtils; -import com.extendedclip.deluxemenus.utils.StringUtils; -import com.extendedclip.deluxemenus.utils.VersionHelper; +import com.extendedclip.deluxemenus.scheduler.UniversalRunnable; +import com.extendedclip.deluxemenus.utils.*; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.entity.Player; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.logging.Level; -public class ClickActionTask extends BukkitRunnable { +public class ClickActionTask extends UniversalRunnable { private final DeluxeMenus plugin; private final UUID uuid; @@ -62,417 +51,418 @@ public void run() { return; } - final Optional holder = Menu.getMenuHolder(player); - final Player target = holder.isPresent() && holder.get().getPlaceholderPlayer() != null - ? holder.get().getPlaceholderPlayer() - : player; + plugin.getScheduler().runTask(player, () -> { + final Optional holder = Menu.getMenuHolder(player); + final Player target = holder.isPresent() && holder.get().getPlaceholderPlayer() != null + ? holder.get().getPlaceholderPlayer() + : player; - final String executable = StringUtils.replacePlaceholdersAndArguments( - this.exec, - this.arguments, - target, - this.parsePlaceholdersInArguments, - this.parsePlaceholdersAfterArguments); + final String executable = StringUtils.replacePlaceholdersAndArguments( + this.exec, + this.arguments, + target, + this.parsePlaceholdersInArguments, + this.parsePlaceholdersAfterArguments); - switch (actionType) { - case META: - if (!VersionHelper.IS_PDC_VERSION || plugin.getPersistentMetaHandler() == null) { - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Meta action not supported on this server version."); - break; - } - final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().parseAndExecuteMetaActionFromString(player, executable); - switch (result) { - case INVALID_SYNTAX: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Make sure you have the right syntax."); - break; - case NEW_VALUE_IS_DIFFERENT_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! New value is a different type than the old value!"); - break; - case INVALID_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! The specified type is not supported for the specified action!"); - break; - case EXISTENT_VALUE_IS_DIFFERENT_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Existent value is a different type than the new value!"); - break; - case VALUE_NOT_FOUND: - case SUCCESS: - default: + switch (actionType) { + case META: + if (!VersionHelper.IS_PDC_VERSION || plugin.getPersistentMetaHandler() == null) { + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Meta action not supported on this server version."); break; - } - break; + } + final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().parseAndExecuteMetaActionFromString(player, executable); + switch (result) { + case INVALID_SYNTAX: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Make sure you have the right syntax."); + break; + case NEW_VALUE_IS_DIFFERENT_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! New value is a different type than the old value!"); + break; + case INVALID_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! The specified type is not supported for the specified action!"); + break; + case EXISTENT_VALUE_IS_DIFFERENT_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Existent value is a different type than the new value!"); + break; + case VALUE_NOT_FOUND: + case SUCCESS: + default: + break; + } + break; + + case PLAYER: + case PLAYER_COMMAND_EVENT: + player.chat("/" + executable); + break; - case PLAYER: - case PLAYER_COMMAND_EVENT: - player.chat("/" + executable); - break; + case PLACEHOLDER: + holder.ifPresent(it -> it.setPlaceholders(executable)); + break; - case PLACEHOLDER: - holder.ifPresent(it -> it.setPlaceholders(executable)); - break; + case CHAT: + player.chat(executable); + break; - case CHAT: - player.chat(executable); - break; + case CONSOLE: + plugin.getScheduler().runTask(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable)); + break; + + case MINI_MESSAGE: + plugin.audiences().player(player).sendMessage(MiniMessage.miniMessage().deserialize(executable)); + break; - case CONSOLE: - Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable); - break; + case MINI_BROADCAST: + plugin.audiences().all().sendMessage(MiniMessage.miniMessage().deserialize(executable)); + break; - case MINI_MESSAGE: - plugin.audiences().player(player).sendMessage(MiniMessage.miniMessage().deserialize(executable)); - break; + case MESSAGE: + player.sendMessage(StringUtils.color(executable)); + break; - case MINI_BROADCAST: - plugin.audiences().all().sendMessage(MiniMessage.miniMessage().deserialize(executable)); - break; + case LOG: + final String[] logParts = executable.split(" ", 2); - case MESSAGE: - player.sendMessage(StringUtils.color(executable)); - break; + if (logParts.length == 0 || logParts[0].isBlank()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "LOG command requires at least a message"); + break; + } - case LOG: - final String[] logParts = executable.split(" ", 2); + Level logLevel; + String message; - if (logParts.length == 0 || logParts[0].isBlank()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "LOG command requires at least a message"); + if (logParts.length == 1) { + logLevel = Level.INFO; + message = logParts[0]; + } else { + message = logParts[1]; + + try { + logLevel = Level.parse(logParts[0].toUpperCase()); + } catch (IllegalArgumentException e) { + logLevel = Level.INFO; + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Log level " + logParts[0] + " is not a valid log level! Using INFO instead."); + } + } + + plugin.getLogger().log(logLevel, String.format("[%s]: %s", holder.map(MenuHolder::getMenuName).orElse("Unknown Menu"), message)); break; - } - Level logLevel; - String message; + case BROADCAST: + Bukkit.broadcastMessage(StringUtils.color(executable)); + break; - if(logParts.length == 1) { - logLevel = Level.INFO; - message = logParts[0]; - } else { - message = logParts[1]; + case CLOSE: + Menu.closeMenu(plugin, player, true, true); + break; - try { - logLevel = Level.parse(logParts[0].toUpperCase()); - } catch (IllegalArgumentException e) { - logLevel = Level.INFO; - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Log level " + logParts[0] + " is not a valid log level! Using INFO instead."); + case OPEN_GUI_MENU: + case OPEN_MENU: + final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); + final String[] executableParts = temporaryExecutable.split(" ", 2); + + if (executableParts.length == 0) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); + break; } - } - plugin.getLogger().log(logLevel, String.format("[%s]: %s", holder.map(MenuHolder::getMenuName).orElse("Unknown Menu"), message)); - break; + final String menuName = executableParts[0]; - case BROADCAST: - Bukkit.broadcastMessage(StringUtils.color(executable)); - break; + final Optional optionalMenuToOpen = Menu.getMenuByName(menuName); - case CLOSE: - Menu.closeMenu(plugin, player, true, true); - break; + if (optionalMenuToOpen.isEmpty()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); + break; + } - case OPEN_GUI_MENU: - case OPEN_MENU: - final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); - final String[] executableParts = temporaryExecutable.split(" ", 2); + final Menu menuToOpen = optionalMenuToOpen.get(); - if (executableParts.length == 0) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); - break; - } + final List menuArgumentNames = menuToOpen.options().arguments(); - final String menuName = executableParts[0]; + String[] passedArgumentValues = null; + if (executableParts.length > 1) { + passedArgumentValues = executableParts[1].split(" "); + } - final Optional optionalMenuToOpen = Menu.getMenuByName(menuName); + if (menuArgumentNames.isEmpty()) { + if (passedArgumentValues != null && passedArgumentValues.length > 0) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" + ); + } - if (optionalMenuToOpen.isEmpty()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); - break; - } + if (holder.isEmpty()) { + menuToOpen.openMenu(player); + break; + } - final Menu menuToOpen = optionalMenuToOpen.get(); + menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); + break; + } - final List menuArgumentNames = menuToOpen.options().arguments(); + if (passedArgumentValues == null || passedArgumentValues.length == 0) { + // Replicate old behavior: If no arguments are given, open the menu with the arguments from the current menu + if (holder.isEmpty()) { + menuToOpen.openMenu(player); + break; + } - String[] passedArgumentValues = null; - if (executableParts.length > 1) { - passedArgumentValues = executableParts[1].split(" "); - } + menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); + break; + } - if (menuArgumentNames.isEmpty()) { - if (passedArgumentValues != null && passedArgumentValues.length > 0) { + if (passedArgumentValues.length < menuArgumentNames.size()) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" + "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" ); + break; } - if (holder.isEmpty()) { - menuToOpen.openMenu(player); - break; + final Map argumentsMap = new HashMap<>(); + if (holder.isPresent() && holder.get().getTypedArgs() != null) { + // Pass the arguments from the current menu to the new menu. If the new menu has arguments with the + // same name, they will be overwritten + argumentsMap.putAll(holder.get().getTypedArgs()); } - menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); - break; - } + for (int index = 0; index < menuArgumentNames.size(); index++) { + final String argumentName = menuArgumentNames.get(index); + + if (passedArgumentValues.length <= index) { + // This should never be the case! + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" + ); + break; + } + + if (menuArgumentNames.size() == index + 1) { + // If this is the last argument, get all remaining values and join them + final String lastArgumentValue = String.join(" ", Arrays.asList(passedArgumentValues).subList(index, passedArgumentValues.length)); + argumentsMap.put(argumentName, lastArgumentValue); + break; + } + + argumentsMap.put(argumentName, passedArgumentValues[index]); + } - if (passedArgumentValues == null || passedArgumentValues.length == 0) { - // Replicate old behavior: If no arguments are given, open the menu with the arguments from the current menu if (holder.isEmpty()) { - menuToOpen.openMenu(player); + menuToOpen.openMenu(player, argumentsMap, null); break; } - menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); + menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer()); break; - } - - if (passedArgumentValues.length < menuArgumentNames.size()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" - ); + + case CONNECT: + plugin.connect(player, executable); break; - } - final Map argumentsMap = new HashMap<>(); - if (holder.isPresent() && holder.get().getTypedArgs() != null) { - // Pass the arguments from the current menu to the new menu. If the new menu has arguments with the - // same name, they will be overwritten - argumentsMap.putAll(holder.get().getTypedArgs()); - } + case JSON_MESSAGE: + AdventureUtils.sendJson(plugin, player, executable); + break; - for (int index = 0; index < menuArgumentNames.size(); index++) { - final String argumentName = menuArgumentNames.get(index); + case JSON_BROADCAST: + case BROADCAST_JSON: + plugin.audiences().all().sendMessage(AdventureUtils.fromJson(executable)); + break; - if (passedArgumentValues.length <= index) { - // This should never be the case! + case REFRESH: + if (holder.isEmpty()) { plugin.debug( - DebugLevel.HIGHEST, + DebugLevel.MEDIUM, Level.WARNING, - "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" + player.getName() + " does not have menu open! Nothing to refresh!" ); break; } - if (menuArgumentNames.size() == index + 1) { - // If this is the last argument, get all remaining values and join them - final String lastArgumentValue = String.join(" ", Arrays.asList(passedArgumentValues).subList(index, passedArgumentValues.length)); - argumentsMap.put(argumentName, lastArgumentValue); + holder.get().refreshMenu(); + break; + + case TAKE_MONEY: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot take money!"); break; } - argumentsMap.put(argumentName, passedArgumentValues[index]); - } - - if (holder.isEmpty()) { - menuToOpen.openMenu(player, argumentsMap, null); - break; - } - - menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer()); - break; - - case CONNECT: - plugin.connect(player, executable); - break; - - case JSON_MESSAGE: - AdventureUtils.sendJson(plugin, player, executable); - break; - - case JSON_BROADCAST: - case BROADCAST_JSON: - plugin.audiences().all().sendMessage(AdventureUtils.fromJson(executable)); - break; - - case REFRESH: - if (holder.isEmpty()) { - plugin.debug( - DebugLevel.MEDIUM, - Level.WARNING, - player.getName() + " does not have menu open! Nothing to refresh!" - ); + try { + plugin.getVault().takeMoney(player, Double.parseDouble(executable)); + } catch (final NumberFormatException exception) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for take money action: " + executable + ", is not a valid number!" + ); + } break; - } - - holder.get().refreshMenu(); - break; - case TAKE_MONEY: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot take money!"); - break; - } - - try { - plugin.getVault().takeMoney(player, Double.parseDouble(executable)); - } catch (final NumberFormatException exception) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for take money action: " + executable + ", is not a valid number!" - ); - } - break; - - case GIVE_MONEY: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot give money!"); - break; - } - - try { - plugin.getVault().giveMoney(player, Double.parseDouble(executable)); - } catch (final NumberFormatException exception) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for give money action: " + executable + ", is not a valid number!" - ); - } - break; - - case TAKE_EXP: - case GIVE_EXP: - final String lowerCaseExecutable = executable.toLowerCase(); - - try { - if (Integer.parseInt(lowerCaseExecutable.replaceAll("l", "")) <= 0) break; - - if (actionType == ActionType.TAKE_EXP) { - ExpUtils.setExp(player, "-" + lowerCaseExecutable); + case GIVE_MONEY: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot give money!"); break; } - ExpUtils.setExp(player, lowerCaseExecutable); - break; - - } catch (final NumberFormatException exception) { - if (actionType == ActionType.TAKE_EXP) { + try { + plugin.getVault().giveMoney(player, Double.parseDouble(executable)); + } catch (final NumberFormatException exception) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Amount for take exp action: " + executable + ", is not a valid number!" + "Amount for give money action: " + executable + ", is not a valid number!" ); - break; } - - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for give exp action: " + executable + ", is not a valid number!" - ); - break; - } - - case GIVE_PERM: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Vault not hooked! Cannot give permission: " + executable + "!"); break; - } - plugin.getVault().givePermission(player, executable); - break; + case TAKE_EXP: + case GIVE_EXP: + final String lowerCaseExecutable = executable.toLowerCase(); - case TAKE_PERM: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Vault not hooked! Cannot take permission: " + executable + "!"); - break; - } + try { + if (Integer.parseInt(lowerCaseExecutable.replaceAll("l", "")) <= 0) break; - plugin.getVault().takePermission(player, executable); - break; + if (actionType == ActionType.TAKE_EXP) { + ExpUtils.setExp(player, "-" + lowerCaseExecutable); + break; + } - case BROADCAST_SOUND: - case BROADCAST_WORLD_SOUND: - case PLAY_SOUND: - final Sound sound; - float volume = 1; - float pitch = 1; + ExpUtils.setExp(player, lowerCaseExecutable); + break; - if (!executable.contains(" ")) { - try { - sound = SoundUtils.getSound(executable.toUpperCase()); - } catch (final IllegalArgumentException exception) { - plugin.printStacktrace( - "Sound name given for sound action: " + executable + ", is not a valid sound!", - exception + } catch (final NumberFormatException exception) { + if (actionType == ActionType.TAKE_EXP) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for take exp action: " + executable + ", is not a valid number!" + ); + break; + } + + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for give exp action: " + executable + ", is not a valid number!" ); break; } - } else { - String[] parts = executable.split(" ", 3); - try { - sound = SoundUtils.getSound(parts[0].toUpperCase()); - } catch (final IllegalArgumentException exception) { - plugin.printStacktrace( - "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", - exception - ); + case GIVE_PERM: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Vault not hooked! Cannot give permission: " + executable + "!"); break; } - if (parts.length == 3) { + plugin.getVault().givePermission(player, executable); + break; + + case TAKE_PERM: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Vault not hooked! Cannot take permission: " + executable + "!"); + break; + } + + plugin.getVault().takePermission(player, executable); + break; + + case BROADCAST_SOUND: + case BROADCAST_WORLD_SOUND: + case PLAY_SOUND: + final Sound sound; + float volume = 1; + float pitch = 1; + + if (!executable.contains(" ")) { + try { + sound = SoundUtils.getSound(executable.toUpperCase()); + } catch (final IllegalArgumentException exception) { + plugin.printStacktrace( + "Sound name given for sound action: " + executable + ", is not a valid sound!", + exception + ); + break; + } + } else { + String[] parts = executable.split(" ", 3); + try { - pitch = Float.parseFloat(parts[2]); + sound = SoundUtils.getSound(parts[0].toUpperCase()); + } catch (final IllegalArgumentException exception) { + plugin.printStacktrace( + "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", + exception + ); + break; + } + + if (parts.length == 3) { + try { + pitch = Float.parseFloat(parts[2]); + } catch (final NumberFormatException exception) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Pitch given for sound action: " + parts[2] + ", is not a valid number!" + ); + + plugin.printStacktrace( + "Pitch given for sound action: " + parts[2] + ", is not a valid number!", + exception + ); + } + } + + try { + volume = Float.parseFloat(parts[1]); } catch (final NumberFormatException exception) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Pitch given for sound action: " + parts[2] + ", is not a valid number!" + "Volume given for sound action: " + parts[1] + ", is not a valid number!" ); plugin.printStacktrace( - "Pitch given for sound action: " + parts[2] + ", is not a valid number!", + "Volume given for sound action: " + parts[1] + ", is not a valid number!", exception ); } } - - try { - volume = Float.parseFloat(parts[1]); - } catch (final NumberFormatException exception) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Volume given for sound action: " + parts[1] + ", is not a valid number!" - ); - - plugin.printStacktrace( - "Volume given for sound action: " + parts[1] + ", is not a valid number!", - exception - ); + switch (actionType) { + case BROADCAST_SOUND: + for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); + } + break; + + case BROADCAST_WORLD_SOUND: + for (final Player broadcastTarget : player.getWorld().getPlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); + } + break; + + case PLAY_SOUND: + player.playSound(player.getLocation(), sound, volume, pitch); + break; } - } - - switch (actionType) { - case BROADCAST_SOUND: - for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { - broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); - } - break; - - case BROADCAST_WORLD_SOUND: - for (final Player broadcastTarget : player.getWorld().getPlayers()) { - broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); - } - break; - - case PLAY_SOUND: - player.playSound(player.getLocation(), sound, volume, pitch); - break; - } - break; + break; - default: - break; - } + default: + break; + } + }); } -} \ No newline at end of file +} diff --git a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java index 9aa91a97..e360bde8 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/DeluxeMenusCommand.java @@ -2,7 +2,6 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.command.subcommand.*; -import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.Messages; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextReplacementConfig; diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ExecuteCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ExecuteCommand.java index c1ce0da1..c9dee3f2 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ExecuteCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/ExecuteCommand.java @@ -17,7 +17,6 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; public class ExecuteCommand extends SubCommand { diff --git a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/RefreshCommand.java b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/RefreshCommand.java index 162957ba..f2816c65 100644 --- a/src/main/java/com/extendedclip/deluxemenus/command/subcommand/RefreshCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/command/subcommand/RefreshCommand.java @@ -51,7 +51,7 @@ public void execute(final @NotNull CommandSender sender, final @NotNull List { - for (final ItemStack itemStack : event.getPlayer().getInventory().getContents()) { - if (itemStack == null) continue; - if (!marker.isMarked(itemStack)) continue; + plugin.getScheduler().runTaskLater(() -> { + for (final ItemStack itemStack : event.getPlayer().getInventory().getContents()) { + if (itemStack == null) continue; + if (!marker.isMarked(itemStack)) continue; - plugin.debug( - DebugLevel.LOWEST, - Level.INFO, - "Player logged in with a DeluxeMenus item in their inventory. Removing it." - ); - event.getPlayer().getInventory().remove(itemStack); - }}, - 10L + plugin.debug( + DebugLevel.LOWEST, + Level.INFO, + "Player logged in with a DeluxeMenus item in their inventory. Removing it." + ); + event.getPlayer().getInventory().remove(itemStack); + } + }, 10L ); } -} \ No newline at end of file +} diff --git a/src/main/java/com/extendedclip/deluxemenus/events/DeluxeMenusOpenMenuEvent.java b/src/main/java/com/extendedclip/deluxemenus/events/DeluxeMenusOpenMenuEvent.java index 655c4831..ec8ca03e 100644 --- a/src/main/java/com/extendedclip/deluxemenus/events/DeluxeMenusOpenMenuEvent.java +++ b/src/main/java/com/extendedclip/deluxemenus/events/DeluxeMenusOpenMenuEvent.java @@ -1,6 +1,5 @@ package com.extendedclip.deluxemenus.events; -import com.extendedclip.deluxemenus.menu.Menu; import com.extendedclip.deluxemenus.menu.MenuHolder; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/BaseHeadHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/BaseHeadHook.java index 0178ec75..5aaf2128 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/BaseHeadHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/BaseHeadHook.java @@ -5,7 +5,6 @@ import com.extendedclip.deluxemenus.utils.SkullUtils; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableItemsHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableItemsHook.java index 9bdf1266..3054c1da 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableItemsHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/ExecutableItemsHook.java @@ -5,7 +5,6 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; - import com.ssomar.score.api.executableitems.config.ExecutableItemInterface; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/ItemsAdderHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/ItemsAdderHook.java index ac3760b0..bae2d184 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/ItemsAdderHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/ItemsAdderHook.java @@ -4,7 +4,6 @@ import dev.lone.itemsadder.api.CustomStack; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java index 69d51653..f9e89b7b 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java @@ -7,10 +7,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.logging.Level; - import net.Indyuce.mmoitems.MMOItems; import net.Indyuce.mmoitems.api.Type; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -47,7 +45,7 @@ public ItemStack getItem(@NotNull final String... arguments) { ItemStack mmoItem = null; try { - mmoItem = Bukkit.getScheduler().callSyncMethod(plugin, () -> { + mmoItem = plugin.getScheduler().callSyncMethod(() -> { ItemStack item = MMOItems.plugin.getItem(itemType, splitArgs[1]); if (item == null) { diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/NamedHeadHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/NamedHeadHook.java index aa880ba5..4b306fd9 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/NamedHeadHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/NamedHeadHook.java @@ -6,7 +6,6 @@ import com.extendedclip.deluxemenus.utils.SkullUtils; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - import org.bukkit.event.EventHandler; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.ItemStack; diff --git a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java index af69231b..87a7c1f6 100644 --- a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java +++ b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java @@ -8,7 +8,6 @@ import com.extendedclip.deluxemenus.requirement.RequirementList; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -92,8 +91,8 @@ public void onClose(InventoryCloseEvent event) { final Player player = (Player) event.getPlayer(); if (Menu.isInMenu(player)) { - Menu.closeMenu(plugin, player, false); - Bukkit.getScheduler().runTaskLater(plugin, () -> { + plugin.getScheduler().runTaskLater(player, () -> { + Menu.closeMenu(plugin, player, false); Menu.cleanInventory(plugin, player); player.updateInventory(); }, 3L); diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java index 278681c4..be0de82f 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java @@ -1,8 +1,6 @@ package com.extendedclip.deluxemenus.menu; import com.extendedclip.deluxemenus.DeluxeMenus; -import com.extendedclip.deluxemenus.action.ClickHandler; -import com.extendedclip.deluxemenus.dupe.MenuItemMarker; import com.extendedclip.deluxemenus.events.DeluxeMenusOpenMenuEvent; import com.extendedclip.deluxemenus.events.DeluxeMenusPreOpenMenuEvent; import com.extendedclip.deluxemenus.menu.command.RegistrableMenuCommand; @@ -10,11 +8,18 @@ import com.extendedclip.deluxemenus.requirement.RequirementList; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.StringUtils; - -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; - import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryType; @@ -25,9 +30,9 @@ public class Menu { - private static final Map menus = new HashMap<>(); - private static final Set menuHolders = new HashSet<>(); - private static final Map lastOpenedMenus = new HashMap<>(); + private static final Map menus = new ConcurrentHashMap<>(); + private static final Set menuHolders = ConcurrentHashMap.newKeySet(); + private static final Map lastOpenedMenus = new ConcurrentHashMap<>(); private final DeluxeMenus plugin; private final MenuOptions options; @@ -50,7 +55,7 @@ public Menu( if (this.options.registerCommands()) { this.command = new RegistrableMenuCommand(plugin, this); - this.command.register(); + plugin.getScheduler().runTask(() -> this.command.register()); } menus.put(this.options.name(), this); @@ -199,7 +204,7 @@ public static void closeMenu(final @NotNull DeluxeMenus plugin, final @NotNull P } if (close) { - Bukkit.getScheduler().runTask(plugin, () -> { + plugin.getScheduler().runTask(player, () -> { player.closeInventory(); cleanInventory(plugin, player); }); @@ -274,11 +279,13 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { + plugin.getScheduler().runTaskAsynchronously(() -> { Set activeItems = new HashSet<>(); @@ -383,7 +390,7 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { + plugin.getScheduler().runTask(viewer, () -> { if(options.refresh()) { holder.startRefreshTask(); } @@ -400,7 +407,7 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { + plugin.getScheduler().runTask(viewer, () -> { DeluxeMenusOpenMenuEvent openEvent = new DeluxeMenusOpenMenuEvent(viewer, holder); Bukkit.getPluginManager().callEvent(openEvent); }); diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java index adc23192..0f216e95 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java @@ -2,20 +2,15 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.options.MenuOptions; +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; import com.extendedclip.deluxemenus.utils.StringUtils; -import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.scheduler.BukkitRunnable; -import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -29,8 +24,8 @@ public class MenuHolder implements InventoryHolder { private Player placeholderPlayer; private String menuName; private Set activeItems; - private BukkitTask updateTask = null; - private BukkitTask refreshTask = null; + private MyScheduledTask updateTask = null; + private MyScheduledTask refreshTask = null; private Inventory inventory; private boolean updating; private boolean parsePlaceholdersInArguments; @@ -55,7 +50,7 @@ public String getViewerName() { return viewer.getName(); } - public BukkitTask getUpdateTask() { + public MyScheduledTask getUpdateTask() { return updateTask; } @@ -138,45 +133,51 @@ public void refreshMenu() { setUpdating(true); - Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { + plugin.getScheduler().runTaskAsynchronously(() -> { final Set active = new HashSet<>(); + final Set slotsToClear = new HashSet<>(); for (int i = 0; i < getInventory().getSize(); i++) { TreeMap e = menu.getMenuItems().get(i); if (e == null) { - getInventory().setItem(i, null); + slotsToClear.add(i); continue; } - boolean m = false; + boolean matched = false; for (MenuItem item : e.values()) { if (item.options().viewRequirements().isPresent()) { if (item.options().viewRequirements().get().evaluate(this)) { - m = true; + matched = true; active.add(item); break; } } else { - m = true; + matched = true; active.add(item); break; } } - if (!m) { - getInventory().setItem(i, null); + if (!matched) { + slotsToClear.add(i); } } if (active.isEmpty()) { - Menu.closeMenu(plugin, getViewer(), true); + plugin.getScheduler().runTask(getViewer(), () -> Menu.closeMenu(plugin, getViewer(), true)); + return; } - Bukkit.getScheduler().runTask(plugin, () -> { + plugin.getScheduler().runTask(getViewer(), () -> { + + for (int slot : slotsToClear) { + getInventory().setItem(slot, null); + } boolean update = false; @@ -201,7 +202,7 @@ public void refreshMenu() { if (update && updateTask == null) { startUpdatePlaceholdersTask(); - } else if(!update && updateTask != null) { + } else if (!update && updateTask != null) { stopPlaceholderUpdate(); } @@ -221,7 +222,7 @@ public void stopPlaceholderUpdate() { } public void stopRefreshTask() { - if(refreshTask != null) { + if (refreshTask != null) { try { refreshTask.cancel(); } catch (Exception ignored) { @@ -231,20 +232,21 @@ public void stopRefreshTask() { } public void startRefreshTask() { - if(refreshTask != null) { + if (refreshTask != null) { stopRefreshTask(); } - refreshTask = new BukkitRunnable() { - @Override - public void run() { - refreshMenu(); - } - }.runTaskTimerAsynchronously(plugin, 20L, - 20L * Menu.getMenuByName(menuName) - .map(Menu::options) - .map(MenuOptions::refreshInterval) - .orElse(10)); + long initialDelay = 20L; + long period = 20L * Menu.getMenuByName(menuName) + .map(Menu::options) + .map(MenuOptions::refreshInterval) + .orElse(10); + + refreshTask = plugin.getScheduler().runTaskTimerAsynchronously( + this::refreshMenu, + initialDelay, + period + ); } public void startUpdatePlaceholdersTask() { @@ -253,69 +255,70 @@ public void startUpdatePlaceholdersTask() { stopPlaceholderUpdate(); } - updateTask = new BukkitRunnable() { + long initialDelay = 20L; + long period = 20L * Menu.getMenuByName(menuName) + .map(Menu::options) + .map(MenuOptions::updateInterval) + .orElse(10); - @Override - public void run() { + updateTask = plugin.getScheduler().runTaskTimer( + getViewer(), + () -> { - if (updating) { - return; - } + if (updating) { + return; + } - Set items = getActiveItems(); + Set items = getActiveItems(); - if (items == null) { - return; - } + if (items == null) { + return; + } - for (MenuItem item : items) { + for (MenuItem item : items) { - if (item.options().updatePlaceholders()) { + if (item.options().updatePlaceholders()) { - ItemStack i = inventory.getItem(item.options().slot()); + ItemStack i = inventory.getItem(item.options().slot()); - if (i == null) { - continue; - } - - int amt = i.getAmount(); + if (i == null) { + continue; + } - if (item.options().dynamicAmount().isPresent()) { - try { - amt = Integer.parseInt(setPlaceholdersAndArguments(item.options().dynamicAmount().get())); - if (amt <= 0) { - amt = 1; + int amt = i.getAmount(); + + if (item.options().dynamicAmount().isPresent()) { + try { + amt = Integer.parseInt(setPlaceholdersAndArguments(item.options().dynamicAmount().get())); + if (amt <= 0) { + amt = 1; + } + } catch (Exception exception) { + plugin.printStacktrace( + "Something went wrong while updating item in slot " + item.options().slot() + + ". Invalid dynamic amount: " + setPlaceholdersAndArguments(item.options().dynamicAmount().get()), + exception + ); } - } catch (Exception exception) { - plugin.printStacktrace( - "Something went wrong while updating item in slot " + item.options().slot() + - ". Invalid dynamic amount: " + setPlaceholdersAndArguments(item.options().dynamicAmount().get()), - exception - ); } - } - ItemMeta meta = i.getItemMeta(); + ItemMeta meta = i.getItemMeta(); - if (item.options().displayNameHasPlaceholders() && item.options().displayName().isPresent()) { - meta.setDisplayName(StringUtils.color(setPlaceholdersAndArguments(item.options().displayName().get()))); - } + if (item.options().displayNameHasPlaceholders() && item.options().displayName().isPresent()) { + meta.setDisplayName(StringUtils.color(setPlaceholdersAndArguments(item.options().displayName().get()))); + } - if (item.options().loreHasPlaceholders()) { - meta.setLore(item.getMenuItemLore(getHolder(), item.options().lore())); - } + if (item.options().loreHasPlaceholders()) { + meta.setLore(item.getMenuItemLore(getHolder(), item.options().lore())); + } - i.setItemMeta(meta); - i.setAmount(amt); + i.setItemMeta(meta); + i.setAmount(amt); + } } - } - } - }.runTaskTimerAsynchronously(plugin, 20L, - 20L * Menu.getMenuByName(menuName) - .map(Menu::options) - .map(MenuOptions::updateInterval) - .orElse(10)); + }, initialDelay, period + ); } public boolean isUpdating() { diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java b/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java index 2e49d5cd..5b0a8680 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/MenuItem.java @@ -2,10 +2,10 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.hooks.ItemHook; +import com.extendedclip.deluxemenus.menu.options.CustomModelDataComponent; import com.extendedclip.deluxemenus.menu.options.HeadType; import com.extendedclip.deluxemenus.menu.options.LoreAppendMode; import com.extendedclip.deluxemenus.menu.options.MenuItemOptions; -import com.extendedclip.deluxemenus.menu.options.CustomModelDataComponent; import com.extendedclip.deluxemenus.nbt.NbtProvider; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.ItemUtils; @@ -15,8 +15,8 @@ import org.bukkit.Color; import org.bukkit.FireworkEffect; import org.bukkit.Material; -import org.bukkit.Registry; import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.block.Banner; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.type.Light; @@ -45,14 +45,14 @@ import java.io.ByteArrayInputStream; import java.io.IOException; -import java.util.Base64; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Optional; import java.util.Objects; +import java.util.Optional; import java.util.logging.Level; import java.util.stream.Collectors; @@ -89,6 +89,7 @@ public static ItemStack base64ToItemStack(String data) { return null; } } + public ItemStack getItemStack(@NotNull final MenuHolder holder) { final Player viewer = holder.getViewer(); @@ -112,7 +113,6 @@ public ItemStack getItemStack(@NotNull final MenuHolder holder) { } } - if (ItemUtils.isPlayerItem(lowercaseStringMaterial)) { final ItemStack playerItem = INVENTORY_ITEM_ACCESSORS.get(lowercaseStringMaterial).apply(viewer.getInventory()); diff --git a/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java b/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java index 56a67760..9fcc1ff2 100644 --- a/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java +++ b/src/main/java/com/extendedclip/deluxemenus/requirement/JavascriptRequirement.java @@ -3,10 +3,10 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.utils.DebugLevel; -import java.util.logging.Level; import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.ServicePriority; diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalRunnable.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalRunnable.java new file mode 100644 index 00000000..adcb1cff --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalRunnable.java @@ -0,0 +1,176 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler; + +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import org.bukkit.plugin.Plugin; + +/** Just modified BukkitRunnable */ +public abstract class UniversalRunnable implements Runnable { + MyScheduledTask task; + + public synchronized void cancel() throws IllegalStateException { + checkScheduled(); + task.cancel(); + } + + /** + * Returns true if this task has been cancelled. + * + * @return true if the task has been cancelled + * @throws IllegalStateException if task was not scheduled yet + */ + public synchronized boolean isCancelled() throws IllegalStateException { + checkScheduled(); + return task.isCancelled(); + } + + /** + * Schedules this in the Bukkit scheduler to run on next tick. + * + * @param plugin the reference to the plugin scheduling task + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTask(Runnable) + */ + + public synchronized MyScheduledTask runTask(Plugin plugin) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTask(this)); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Schedules this in the Bukkit scheduler to run asynchronously. + * + * @param plugin the reference to the plugin scheduling task + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTaskAsynchronously(Runnable) + */ + + public synchronized MyScheduledTask runTaskAsynchronously(Plugin plugin) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTaskAsynchronously(this)); + } + + /** + * Schedules this to run after the specified number of server ticks. + * + * @param plugin the reference to the plugin scheduling task + * @param delay the ticks to wait before running the task + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTaskLater(Runnable, long) + */ + + public synchronized MyScheduledTask runTaskLater(Plugin plugin, long delay) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTaskLater(this, delay)); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Schedules this to run asynchronously after the specified number of + * server ticks. + * + * @param plugin the reference to the plugin scheduling task + * @param delay the ticks to wait before running the task + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTaskLaterAsynchronously(Runnable, long) + */ + + public synchronized MyScheduledTask runTaskLaterAsynchronously(Plugin plugin, long delay) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTaskLaterAsynchronously(this, delay)); + } + + /** + * Schedules this to repeatedly run until cancelled, starting after the + * specified number of server ticks. + * + * @param plugin the reference to the plugin scheduling task + * @param delay the ticks to wait before running the task + * @param period the ticks to wait between runs + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTaskTimer(Runnable, long, long) + */ + + public synchronized MyScheduledTask runTaskTimer(Plugin plugin, long delay, long period) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTaskTimer(this, delay, period)); + } + + /** + * Asynchronous tasks should never access any API in Bukkit. Great care + * should be taken to assure the thread-safety of asynchronous tasks. + *

+ * Schedules this to repeatedly run asynchronously until cancelled, + * starting after the specified number of server ticks. + * + * @param plugin the reference to the plugin scheduling task + * @param delay the ticks to wait before running the task for the first + * time + * @param period the ticks to wait between runs + * @return {@link MyScheduledTask} + * @throws IllegalArgumentException if plugin is null + * @throws IllegalStateException if this was already scheduled + * @see TaskScheduler#runTaskTimerAsynchronously(Runnable, long, long) + */ + + public synchronized MyScheduledTask runTaskTimerAsynchronously(Plugin plugin, long delay, long period) throws IllegalArgumentException, IllegalStateException { + checkNotYetScheduled(); + return setupTask(UniversalScheduler.getScheduler(plugin).runTaskTimerAsynchronously(this, delay, period)); + } + + private void checkScheduled() { + if (task == null) { + throw new IllegalStateException("Not scheduled yet"); + } + } + + private void checkNotYetScheduled() { + if (task != null) { + throw new IllegalStateException("Already scheduled"); + } + } + + private MyScheduledTask setupTask(final MyScheduledTask task) { + this.task = task; + return task; + } + +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalScheduler.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalScheduler.java new file mode 100644 index 00000000..8949a140 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/UniversalScheduler.java @@ -0,0 +1,42 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler; + +import com.extendedclip.deluxemenus.scheduler.bukkit.BukkitScheduler; +import com.extendedclip.deluxemenus.scheduler.folia.FoliaScheduler; +import com.extendedclip.deluxemenus.scheduler.paper.PaperScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.scheduler.utils.JavaUtil; +import org.bukkit.plugin.Plugin; + +public class UniversalScheduler { + private static final boolean IS_FOLIA = JavaUtil.classExists("io.papermc.paper.threadedregions.RegionizedServer"); + private static final boolean IS_CANVAS = JavaUtil.classExists("io.canvasmc.canvas.server.ThreadedServer"); + private static final boolean IS_EXPANDED_SCHEDULING_AVAILABLE = JavaUtil.classExists("io.papermc.paper.threadedregions.scheduler.ScheduledTask"); + + public static TaskScheduler getScheduler(Plugin plugin) { + return IS_FOLIA || IS_CANVAS ? new FoliaScheduler(plugin) : (IS_EXPANDED_SCHEDULING_AVAILABLE ? new PaperScheduler(plugin) : new BukkitScheduler(plugin)); + } + +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduledTask.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduledTask.java new file mode 100644 index 00000000..b79deffb --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduledTask.java @@ -0,0 +1,71 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.bukkit; + +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; + +public class BukkitScheduledTask implements MyScheduledTask { + + BukkitTask task; + + boolean isRepeating; + + public BukkitScheduledTask(final BukkitTask task) { + this.task = task; + this.isRepeating = false; + } + + public BukkitScheduledTask(final BukkitTask task, boolean isRepeating) { + this.task = task; + this.isRepeating = isRepeating; + } + + @Override + public void cancel() { + task.cancel(); + } + + @Override + public boolean isCancelled() { + return task.isCancelled(); + } + + @Override + public Plugin getOwningPlugin() { + return task.getOwner(); + } + + @Override + public boolean isCurrentlyRunning() { + return Bukkit.getServer().getScheduler().isCurrentlyRunning(this.task.getTaskId()); //There's no other way. Fuck bukkit + } + + @Override + public boolean isRepeatingTask() { + return isRepeating; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduler.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduler.java new file mode 100644 index 00000000..4c562dd0 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/bukkit/BukkitScheduler.java @@ -0,0 +1,129 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.bukkit; + +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +public class BukkitScheduler implements TaskScheduler { + final Plugin plugin; + + public BukkitScheduler(Plugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean isGlobalThread() { + return Bukkit.getServer().isPrimaryThread(); + } + + @Override + public boolean isEntityThread(Entity entity) { + return Bukkit.getServer().isPrimaryThread(); + } + + @Override + public boolean isRegionThread(Location location) { + return Bukkit.getServer().isPrimaryThread(); + } + + @Override + public MyScheduledTask runTask(Runnable runnable) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTask(plugin, runnable)); + } + + @Override + public MyScheduledTask runTaskLater(Runnable runnable, long delay) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskLater(plugin, runnable, delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Runnable runnable, long delay, long period) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskTimer(plugin, runnable, delay, period)); + } + + @Override + public MyScheduledTask runTaskAsynchronously(Runnable runnable) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable)); + } + + @Override + public MyScheduledTask runTaskLaterAsynchronously(Runnable runnable, long delay) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, delay)); + } + + @Override + public MyScheduledTask runTaskTimerAsynchronously(Runnable runnable, long delay, long period) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, runnable, delay, period)); + } + + //Useless? Or... + public MyScheduledTask runTask(Plugin plugin, Runnable runnable) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTask(plugin, runnable)); + } + + @Override + public MyScheduledTask runTaskLater(Plugin plugin, Runnable runnable, long delay) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskLater(plugin, runnable, delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Plugin plugin, Runnable runnable, long delay, long period) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskTimer(plugin, runnable, delay, period)); + } + + @Override + public MyScheduledTask runTaskAsynchronously(Plugin plugin, Runnable runnable) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable)); + } + + @Override + public MyScheduledTask runTaskLaterAsynchronously(Plugin plugin, Runnable runnable, long delay) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, delay)); + } + + @Override + public MyScheduledTask runTaskTimerAsynchronously(Plugin plugin, Runnable runnable, long delay, long period) { + return new BukkitScheduledTask(Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, runnable, delay, period)); + } + + @Override + public void execute(Runnable runnable) { + Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, runnable); + } + + @Override + public void cancelTasks() { + Bukkit.getScheduler().cancelTasks(plugin); + } + + @Override + public void cancelTasks(Plugin plugin) { + Bukkit.getScheduler().cancelTasks(plugin); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduledTask.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduledTask.java new file mode 100644 index 00000000..aaa517af --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduledTask.java @@ -0,0 +1,57 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.folia; + +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import io.papermc.paper.threadedregions.scheduler.ScheduledTask; +import org.bukkit.plugin.Plugin; + +public class FoliaScheduledTask implements MyScheduledTask { + private final ScheduledTask task; + + public FoliaScheduledTask(final ScheduledTask task) { + this.task = task; + } + + public void cancel() { + this.task.cancel(); + } + + public boolean isCancelled() { + return this.task.isCancelled(); + } + + public Plugin getOwningPlugin() { + return this.task.getOwningPlugin(); + } + + public boolean isCurrentlyRunning() { + final ScheduledTask.ExecutionState state = this.task.getExecutionState(); + return state == ScheduledTask.ExecutionState.RUNNING || state == ScheduledTask.ExecutionState.CANCELLED_RUNNING; + } + + public boolean isRepeatingTask() { + return this.task.isRepeatingTask(); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduler.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduler.java new file mode 100644 index 00000000..cbc3d26c --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/folia/FoliaScheduler.java @@ -0,0 +1,220 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.folia; + +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import io.papermc.paper.threadedregions.scheduler.AsyncScheduler; +import io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler; +import io.papermc.paper.threadedregions.scheduler.RegionScheduler; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +import java.util.concurrent.TimeUnit; + +public class FoliaScheduler implements TaskScheduler { + + final Plugin plugin; + + public FoliaScheduler(Plugin plugin) { + this.plugin = plugin; + } + + private final RegionScheduler regionScheduler = Bukkit.getServer().getRegionScheduler(); + private final GlobalRegionScheduler globalRegionScheduler = Bukkit.getServer().getGlobalRegionScheduler(); + private final AsyncScheduler asyncScheduler = Bukkit.getServer().getAsyncScheduler(); + + @Override + public boolean isGlobalThread() { + return Bukkit.getServer().isGlobalTickThread(); + } + + @Override + public boolean isTickThread() { + return Bukkit.getServer().isPrimaryThread(); // The Paper implementation checks whether this is a tick thread, this method exists to avoid confusion. + } + + @Override + public boolean isEntityThread(Entity entity) { + return Bukkit.getServer().isOwnedByCurrentRegion(entity); + } + + @Override + public boolean isRegionThread(Location location) { + return Bukkit.getServer().isOwnedByCurrentRegion(location); + } + + @Override + public MyScheduledTask runTask(Runnable runnable) { + return new FoliaScheduledTask(globalRegionScheduler.run(plugin, task -> runnable.run())); + } + + @Override + public MyScheduledTask runTaskLater(Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + if (delay <= 0) { + return runTask(runnable); + } + return new FoliaScheduledTask(globalRegionScheduler.runDelayed(plugin, task -> runnable.run(), delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Runnable runnable, long delay, long period) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(globalRegionScheduler.runAtFixedRate(plugin, task -> runnable.run(), delay, period)); + } + + @Override + public MyScheduledTask runTask(Plugin plugin, Runnable runnable) { + return new FoliaScheduledTask(globalRegionScheduler.run(plugin, task -> runnable.run())); + } + + @Override + public MyScheduledTask runTaskLater(Plugin plugin, Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + if (delay <= 0) { + return runTask(plugin, runnable); + } + return new FoliaScheduledTask(globalRegionScheduler.runDelayed(plugin, task -> runnable.run(), delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Plugin plugin, Runnable runnable, long delay, long period) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(globalRegionScheduler.runAtFixedRate(plugin, task -> runnable.run(), delay, period)); + } + + @Override + public MyScheduledTask runTask(Location location, Runnable runnable) { + return new FoliaScheduledTask(regionScheduler.run(plugin, location, task -> runnable.run())); + } + + @Override + public MyScheduledTask runTaskLater(Location location, Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + if (delay <= 0) { + return runTask(runnable); + } + return new FoliaScheduledTask(regionScheduler.runDelayed(plugin, location, task -> runnable.run(), delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Location location, Runnable runnable, long delay, long period) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(regionScheduler.runAtFixedRate(plugin, location, task -> runnable.run(), delay, period)); + } + + @Override + public MyScheduledTask runTask(Entity entity, Runnable runnable) { + return new FoliaScheduledTask(entity.getScheduler().run(plugin, task -> runnable.run(), null)); + } + + @Override + public MyScheduledTask runTaskLater(Entity entity, Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + if (delay <= 0) { + return runTask(entity, runnable); + } + return new FoliaScheduledTask(entity.getScheduler().runDelayed(plugin, task -> runnable.run(), null, delay)); + } + + @Override + public MyScheduledTask runTaskTimer(Entity entity, Runnable runnable, long delay, long period) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(entity.getScheduler().runAtFixedRate(plugin, task -> runnable.run(), null, delay, period)); + } + + @Override + public MyScheduledTask runTaskAsynchronously(Runnable runnable) { + return new FoliaScheduledTask(asyncScheduler.runNow(plugin, task -> runnable.run())); + } + + @Override + public MyScheduledTask runTaskLaterAsynchronously(Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(asyncScheduler.runDelayed(plugin, task -> runnable.run(), delay * 50L, TimeUnit.MILLISECONDS)); + } + + @Override + public MyScheduledTask runTaskTimerAsynchronously(Runnable runnable, long delay, long period) { + return new FoliaScheduledTask(asyncScheduler.runAtFixedRate(plugin, task -> runnable.run(), delay * 50, period * 50, TimeUnit.MILLISECONDS)); + } + + @Override + public MyScheduledTask runTaskAsynchronously(Plugin plugin, Runnable runnable) { + return new FoliaScheduledTask(asyncScheduler.runNow(plugin, task -> runnable.run())); + } + + @Override + public MyScheduledTask runTaskLaterAsynchronously(Plugin plugin, Runnable runnable, long delay) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(asyncScheduler.runDelayed(plugin, task -> runnable.run(), delay * 50L, TimeUnit.MILLISECONDS)); + } + + @Override + public MyScheduledTask runTaskTimerAsynchronously(Plugin plugin, Runnable runnable, long delay, long period) { + //Folia exception: Delay ticks may not be <= 0 + delay = getOneIfNotPositive(delay); + return new FoliaScheduledTask(asyncScheduler.runAtFixedRate(plugin, task -> runnable.run(), delay * 50, period * 50, TimeUnit.MILLISECONDS)); + } + + @Override + public void execute(Runnable runnable) { + globalRegionScheduler.execute(plugin, runnable); + } + + @Override + public void execute(Location location, Runnable runnable) { + regionScheduler.execute(plugin, location, runnable); + } + + @Override + public void execute(Entity entity, Runnable runnable) { + entity.getScheduler().execute(plugin, runnable, null, 1L); + } + + @Override + public void cancelTasks() { + globalRegionScheduler.cancelTasks(plugin); + asyncScheduler.cancelTasks(plugin); + } + + @Override + public void cancelTasks(Plugin plugin) { + globalRegionScheduler.cancelTasks(plugin); + asyncScheduler.cancelTasks(plugin); + } + + private long getOneIfNotPositive(long x) { + return x <= 0 ? 1L : x; + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/paper/PaperScheduler.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/paper/PaperScheduler.java new file mode 100644 index 00000000..6920ff97 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/paper/PaperScheduler.java @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.paper; + +import com.extendedclip.deluxemenus.scheduler.folia.FoliaScheduler; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +// Thanks to Towny +public class PaperScheduler extends FoliaScheduler { + public PaperScheduler(Plugin plugin) { + super(plugin); + } + + @Override + public boolean isGlobalThread() { + // isGlobalThread does not exist on paper, match the bukkit task scheduler's behaviour. + return Bukkit.getServer().isPrimaryThread(); + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/schedulers/TaskScheduler.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/schedulers/TaskScheduler.java new file mode 100644 index 00000000..22b77198 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/schedulers/TaskScheduler.java @@ -0,0 +1,346 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.scheduling.schedulers; + +import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.Plugin; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +public interface TaskScheduler { + + /** + * Folia: Returns whether the current thread is ticking the global region
+ * Paper & Bukkit: Returns {@link org.bukkit.Server#isPrimaryThread} + */ + boolean isGlobalThread(); + + /** + * @return {@link org.bukkit.Server#isPrimaryThread} + */ + default boolean isTickThread() { + return Bukkit.getServer().isPrimaryThread(); + } + + /** + * Folia & Paper: Returns whether the current thread is ticking a region and that the region + * being ticked owns the specified entity. Note that this function is the only appropriate method of + * checking for ownership of an entity, as retrieving the entity's location is undefined unless the + * entity is owned by the current region + *

+ * Bukkit: returns {@link org.bukkit.Server#isPrimaryThread} + * + * @param entity Specified entity + */ + boolean isEntityThread(Entity entity); + + /** + * Folia & Paper: Returns whether the current thread is ticking a region and that the region + * being ticked owns the chunk at the specified world and block position as included in the specified location + *

+ * Bukkit: returns {@link org.bukkit.Server#isPrimaryThread} + * + * @param location Specified location, must have a non-null world. + */ + boolean isRegionThread(Location location); + + /** + * Schedules a task to be executed on the next tick
+ * Folia & Paper: ...on the global region
+ * Bukkit: ...on the main thread + * + * @param runnable The task to execute + */ + MyScheduledTask runTask(Runnable runnable); + + /** + * Schedules a task to be executed after the specified delay in ticks
+ * Folia & Paper: ...on the global region
+ * Bukkit: ...on the main thread + * + * @param runnable The task to execute + * @param delay The delay, in ticks + */ + MyScheduledTask runTaskLater(Runnable runnable, long delay); + + /** + * Schedules a repeating task to be executed after the initial delay with the specified period
+ * Folia & Paper: ...on the global region
+ * Bukkit: ...on the main thread + * + * @param runnable The task to execute + * @param delay The initial delay, in ticks. + * @param period The period, in ticks. + */ + MyScheduledTask runTaskTimer(Runnable runnable, long delay, long period); + + /** + * Deprecated: use {@link #runTask(Runnable)} + */ + @Deprecated + default MyScheduledTask runTask(Plugin plugin, Runnable runnable) { + return runTask(runnable); + } + + /** + * Deprecated: use {@link #runTaskLater(Runnable, long)} + */ + @Deprecated + default MyScheduledTask runTaskLater(Plugin plugin, Runnable runnable, long delay) { + return runTaskLater(runnable, delay); + } + + /** + * Deprecated: use {@link #runTaskTimer(Runnable, long, long)} + */ + @Deprecated + default MyScheduledTask runTaskTimer(Plugin plugin, Runnable runnable, long delay, long period) { + return runTaskTimer(runnable, delay, period); + } + + /** + * Folia & Paper: Schedules a task to be executed on the region which owns the location on the next tick + *

+ * Bukkit: same as {@link #runTask(Runnable)} + * + * @param location The location which the region executing should own + * @param runnable The task to execute + */ + default MyScheduledTask runTask(Location location, Runnable runnable) { + return runTask(runnable); + } + + /** + * Folia & Paper: Schedules a task to be executed on the region which owns the location after the + * specified delay in ticks + *

+ * Bukkit: same as {@link #runTaskLater(Runnable, long)} + * + * @param location The location which the region executing should own + * @param runnable The task to execute + * @param delay The delay, in ticks. + */ + default MyScheduledTask runTaskLater(Location location, Runnable runnable, long delay) { + return runTaskLater(runnable, delay); + } + + /** + * Folia & Paper: Schedules a repeating task to be executed on the region which owns the location + * after the initial delay with the specified period + *

+ * Bukkit: same as {@link #runTaskTimer(Runnable, long, long)} + * + * @param location The location which the region executing should own + * @param runnable The task to execute + * @param delay The initial delay, in ticks. + * @param period The period, in ticks. + */ + default MyScheduledTask runTaskTimer(Location location, Runnable runnable, long delay, long period) { + return runTaskTimer(runnable, delay, period); + } + + /** + * Deprecated: use {@link #runTaskLater(Runnable, long)} + */ + @Deprecated + default MyScheduledTask scheduleSyncDelayedTask(Runnable runnable, long delay) { + return runTaskLater(runnable, delay); + } + + /** + * Deprecated: use {@link #execute(Runnable)} or {@link #runTask(Runnable)} + */ + @Deprecated + default MyScheduledTask scheduleSyncDelayedTask(Runnable runnable) { + return runTask(runnable); + } + + /** + * Deprecated: use {@link #runTaskTimer(Runnable, long, long)} + */ + @Deprecated + default MyScheduledTask scheduleSyncRepeatingTask(Runnable runnable, long delay, long period) { + return runTaskTimer(runnable, delay, period); + } + + /** + * Folia & Paper: Schedules a task to be executed on the region which owns the location + * of given entity on the next tick + *

+ * Bukkit: same as {@link #runTask(Runnable)} + * + * @param entity The entity whose location the region executing should own + * @param runnable The task to execute + */ + default MyScheduledTask runTask(Entity entity, Runnable runnable) { + return runTask(runnable); + } + + /** + * Folia & Paper: Schedules a task to be executed on the region which owns the location + * of given entity after the specified delay in ticks + *

+ * Bukkit: same as {@link #runTaskLater(Runnable, long)} + * + * @param entity The entity whose location the region executing should own + * @param runnable The task to execute + * @param delay The delay, in ticks. + */ + default MyScheduledTask runTaskLater(Entity entity, Runnable runnable, long delay) { + return runTaskLater(runnable, delay); + } + + /** + * Folia & Paper: Schedules a repeating task to be executed on the region which owns the + * location of given entity after the initial delay with the specified period + *

+ * Bukkit: same as {@link #runTaskTimer(Runnable, long, long)} + * + * @param entity The entity whose location the region executing should own + * @param runnable The task to execute + * @param delay The initial delay, in ticks. + * @param period The period, in ticks. + */ + default MyScheduledTask runTaskTimer(Entity entity, Runnable runnable, long delay, long period) { + return runTaskTimer(runnable, delay, period); + } + + /** + * Schedules the specified task to be executed asynchronously immediately + * + * @param runnable The task to execute + * @return The {@link MyScheduledTask} that represents the scheduled task + */ + MyScheduledTask runTaskAsynchronously(Runnable runnable); + + /** + * Schedules the specified task to be executed asynchronously after the time delay has passed + * + * @param runnable The task to execute + * @param delay The time delay to pass before the task should be executed + * @return The {@link MyScheduledTask} that represents the scheduled task + */ + MyScheduledTask runTaskLaterAsynchronously(Runnable runnable, long delay); + + /** + * Schedules the specified task to be executed asynchronously after the initial delay has passed, + * and then periodically executed with the specified period + * + * @param runnable The task to execute + * @param delay The time delay to pass before the first execution of the task, in ticks + * @param period The time between task executions after the first execution of the task, in ticks + * @return The {@link MyScheduledTask} that represents the scheduled task + */ + MyScheduledTask runTaskTimerAsynchronously(Runnable runnable, long delay, long period); + + /** + * Deprecated: use {@link #runTaskAsynchronously(Runnable)} + */ + @Deprecated + default MyScheduledTask runTaskAsynchronously(Plugin plugin, Runnable runnable) { + return runTaskAsynchronously(runnable); + } + + /** + * Deprecated: use {@link #runTaskLaterAsynchronously(Runnable, long)} + */ + @Deprecated + default MyScheduledTask runTaskLaterAsynchronously(Plugin plugin, Runnable runnable, long delay) { + return runTaskLaterAsynchronously(runnable, delay); + } + + /** + * Deprecated: use {@link #runTaskTimerAsynchronously(Runnable, long, long)} + */ + @Deprecated + default MyScheduledTask runTaskTimerAsynchronously(Plugin plugin, Runnable runnable, long delay, long period) { + return runTaskTimerAsynchronously(runnable, delay, period); + } + + /** + * Calls a method on the main thread and returns a Future object. This task will be executed + * by the main(Bukkit)/global(Folia&Paper) server thread. + *

+ * Note: The Future.get() methods must NOT be called from the main thread. + *

+ * Note2: There is at least an average of 10ms latency until the isDone() method returns true. + * + * @param task Task to be executed + */ + default Future callSyncMethod(final Callable task) { + CompletableFuture completableFuture = new CompletableFuture<>(); + execute(() -> { + try { + completableFuture.complete(task.call()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return completableFuture; + } + + /** + * Schedules a task to be executed on the global region + * + * @param runnable The task to execute + */ + void execute(Runnable runnable); + + /** + * Schedules a task to be executed on the region which owns the location + * + * @param location The location which the region executing should own + * @param runnable The task to execute + */ + default void execute(Location location, Runnable runnable) { + execute(runnable); + } + + /** + * Schedules a task to be executed on the region which owns the location of given entity + * + * @param entity The entity which location the region executing should own + * @param runnable The task to execute + */ + default void execute(Entity entity, Runnable runnable) { + execute(runnable); + } + + /** + * Attempts to cancel all tasks scheduled by this plugin + */ + void cancelTasks(); + + /** + * Attempts to cancel all tasks scheduled by the specified plugin + * + * @param plugin specified plugin + */ + void cancelTasks(Plugin plugin); +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/tasks/MyScheduledTask.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/tasks/MyScheduledTask.java new file mode 100644 index 00000000..47ae40fa --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/scheduling/tasks/MyScheduledTask.java @@ -0,0 +1,54 @@ +/* + * MIT License + * + * Copyright (c) 2023 Sevastjan + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.extendedclip.deluxemenus.scheduler.scheduling.tasks; + +import org.bukkit.plugin.Plugin; + +public interface MyScheduledTask { + + /** + * Cancels executing task + */ + void cancel(); + + /** + * @return true if task is cancelled, false otherwise + */ + boolean isCancelled(); + + /** + * @return The plugin under which the task was scheduled. + */ + Plugin getOwningPlugin(); + + /** + * @return true if task is currently executing, false otherwise + */ + boolean isCurrentlyRunning(); + + /** + * @return true if task is repeating, false otherwise + */ + boolean isRepeatingTask(); +} diff --git a/src/main/java/com/extendedclip/deluxemenus/scheduler/utils/JavaUtil.java b/src/main/java/com/extendedclip/deluxemenus/scheduler/utils/JavaUtil.java new file mode 100644 index 00000000..b5b85152 --- /dev/null +++ b/src/main/java/com/extendedclip/deluxemenus/scheduler/utils/JavaUtil.java @@ -0,0 +1,12 @@ +package com.extendedclip.deluxemenus.scheduler.utils; + +public class JavaUtil { + public static boolean classExists(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java b/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java index e30fe77b..4db347af 100644 --- a/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java +++ b/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java @@ -15,7 +15,6 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; public class UpdateChecker extends Listener { @@ -32,21 +31,11 @@ public class UpdateChecker extends Listener { public UpdateChecker(final @NotNull DeluxeMenus instance) { super(instance); - new BukkitRunnable() { - @Override - public void run() { - if (check()) { - new BukkitRunnable() { - - @Override - public void run() { - register(); - } - }.runTask(plugin); - } + plugin.getScheduler().runTaskAsynchronously(() -> { + if (check()) { + plugin.getScheduler().runTask(this::register); } - - }.runTaskAsynchronously(plugin); + }); } @EventHandler(priority = EventPriority.MONITOR) diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java index 870e8312..b9cab3cc 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java @@ -33,7 +33,6 @@ public static boolean isPlaceholderOption(@NotNull final String material) { /** * Checks if the string starts with the substring "stack-". The check is case-insensitive. * - * @param itemstack The string to check * @return true if the string starts with "stack-", false otherwise */ public static boolean isItemStackOption(@NotNull final String material) { diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java index 6ae95804..81fd30f8 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/StringUtils.java @@ -3,7 +3,6 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; - import me.clip.placeholderapi.PlaceholderAPI; import net.md_5.bungee.api.ChatColor; import org.bukkit.Color; diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 22d79f63..8c7c2d83 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,8 @@ -api-version: 1.13 name: DeluxeMenus main: com.extendedclip.deluxemenus.DeluxeMenus version: ${version} +api-version: 1.13 +folia-supported: true authors: [ HelpChat ] softdepend: [ PlaceholderAPI, Vault, HeadDatabase, ItemsAdder, Nexo, Oraxen, ExecutableItems, ExecutableBlocks, Score, SimpleItemGenerator, MMOItems ] description: All in one inventory menu system From 3ee0889da39c4a103aafff8bd13ec50915f17685 Mon Sep 17 00:00:00 2001 From: R00tB33rMan Date: Wed, 20 Aug 2025 00:39:26 -0400 Subject: [PATCH 02/13] Revert import modification --- .../deluxemenus/action/ClickActionTask.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index 6e717ba5..aedeb629 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -5,14 +5,24 @@ import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.persistentmeta.PersistentMetaHandler; import com.extendedclip.deluxemenus.scheduler.UniversalRunnable; -import com.extendedclip.deluxemenus.utils.*; +import com.extendedclip.deluxemenus.utils.AdventureUtils; +import com.extendedclip.deluxemenus.utils.DebugLevel; +import com.extendedclip.deluxemenus.utils.ExpUtils; +import com.extendedclip.deluxemenus.utils.SoundUtils; +import com.extendedclip.deluxemenus.utils.StringUtils; +import com.extendedclip.deluxemenus.utils.VersionHelper; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import java.util.logging.Level; public class ClickActionTask extends UniversalRunnable { From 7f75a295dc38d972ebd07b57fac5ede4b614228e Mon Sep 17 00:00:00 2001 From: R00tB33rMan Date: Wed, 20 Aug 2025 00:53:29 -0400 Subject: [PATCH 03/13] Codestyle change --- src/main/java/com/extendedclip/deluxemenus/menu/Menu.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java index be0de82f..0c81fe11 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java @@ -391,7 +391,7 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { - if(options.refresh()) { + if (options.refresh()) { holder.startRefreshTask(); } From a6ee4bba0f4d962e0704dbfa9ca75595a11b0ad3 Mon Sep 17 00:00:00 2001 From: R00tB33rMan Date: Fri, 22 Aug 2025 20:22:36 -0400 Subject: [PATCH 04/13] Correct/adjust Javadoc --- src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java b/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java index b9cab3cc..08de3186 100644 --- a/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java +++ b/src/main/java/com/extendedclip/deluxemenus/utils/ItemUtils.java @@ -33,6 +33,7 @@ public static boolean isPlaceholderOption(@NotNull final String material) { /** * Checks if the string starts with the substring "stack-". The check is case-insensitive. * + * @param material The string to check * @return true if the string starts with "stack-", false otherwise */ public static boolean isItemStackOption(@NotNull final String material) { From cd40a017273a665dc000931a0edba9b84a2e83e1 Mon Sep 17 00:00:00 2001 From: R00tB33rMan Date: Fri, 22 Aug 2025 21:05:00 -0400 Subject: [PATCH 05/13] Correct scheduling format for all classes --- .../deluxemenus/action/ClickActionTask.java | 7 +++-- .../deluxemenus/dupe/DupeFixer.java | 5 ++- .../deluxemenus/hooks/MMOItemsHook.java | 5 ++- .../deluxemenus/listener/PlayerListener.java | 5 ++- .../extendedclip/deluxemenus/menu/Menu.java | 31 ++++++++++--------- .../deluxemenus/menu/MenuHolder.java | 14 ++++++--- .../updatechecker/UpdateChecker.java | 11 ++++--- 7 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index aedeb629..a493cd1a 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -5,6 +5,7 @@ import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.persistentmeta.PersistentMetaHandler; import com.extendedclip.deluxemenus.scheduler.UniversalRunnable; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.utils.AdventureUtils; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.ExpUtils; @@ -28,6 +29,7 @@ public class ClickActionTask extends UniversalRunnable { private final DeluxeMenus plugin; + private final TaskScheduler scheduler; private final UUID uuid; private final ActionType actionType; private final String exec; @@ -46,6 +48,7 @@ public ClickActionTask( final boolean parsePlaceholdersAfterArguments ) { this.plugin = plugin; + this.scheduler = plugin.getScheduler(); this.uuid = uuid; this.actionType = actionType; this.exec = exec; @@ -61,7 +64,7 @@ public void run() { return; } - plugin.getScheduler().runTask(player, () -> { + scheduler.runTask(player, () -> { final Optional holder = Menu.getMenuHolder(player); final Player target = holder.isPresent() && holder.get().getPlaceholderPlayer() != null @@ -116,7 +119,7 @@ public void run() { break; case CONSOLE: - plugin.getScheduler().runTask(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable)); + scheduler.runTask(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable)); break; case MINI_MESSAGE: diff --git a/src/main/java/com/extendedclip/deluxemenus/dupe/DupeFixer.java b/src/main/java/com/extendedclip/deluxemenus/dupe/DupeFixer.java index 4b8739cf..14193418 100644 --- a/src/main/java/com/extendedclip/deluxemenus/dupe/DupeFixer.java +++ b/src/main/java/com/extendedclip/deluxemenus/dupe/DupeFixer.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.listener.Listener; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.utils.DebugLevel; import org.bukkit.event.EventHandler; import org.bukkit.event.entity.EntityPickupItemEvent; @@ -18,10 +19,12 @@ */ public class DupeFixer extends Listener { + private final TaskScheduler scheduler; private final MenuItemMarker marker; public DupeFixer(@NotNull final DeluxeMenus plugin, @NotNull final MenuItemMarker marker) { super(plugin); + this.scheduler = plugin.getScheduler(); this.marker = marker; } @@ -55,7 +58,7 @@ private void onDrop(@NotNull final PlayerDropItemEvent event) { @EventHandler private void onLogin(@NotNull final PlayerJoinEvent event) { - plugin.getScheduler().runTaskLater(() -> { + scheduler.runTaskLater(() -> { for (final ItemStack itemStack : event.getPlayer().getInventory().getContents()) { if (itemStack == null) continue; if (!marker.isMarked(itemStack)) continue; diff --git a/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java b/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java index f9e89b7b..ba570781 100644 --- a/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java +++ b/src/main/java/com/extendedclip/deluxemenus/hooks/MMOItemsHook.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.cache.SimpleCache; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.utils.DebugLevel; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -17,9 +18,11 @@ public class MMOItemsHook implements ItemHook, SimpleCache { private final Map cache = new ConcurrentHashMap<>(); private final DeluxeMenus plugin; + private final TaskScheduler scheduler; public MMOItemsHook(final @NotNull DeluxeMenus plugin) { this.plugin = plugin; + this.scheduler = plugin.getScheduler(); } @Override @@ -45,7 +48,7 @@ public ItemStack getItem(@NotNull final String... arguments) { ItemStack mmoItem = null; try { - mmoItem = plugin.getScheduler().callSyncMethod(() -> { + mmoItem = scheduler.callSyncMethod(() -> { ItemStack item = MMOItems.plugin.getItem(itemType, splitArgs[1]); if (item == null) { diff --git a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java index 87a7c1f6..89b417b2 100644 --- a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java +++ b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java @@ -6,6 +6,7 @@ import com.extendedclip.deluxemenus.menu.MenuHolder; import com.extendedclip.deluxemenus.menu.MenuItem; import com.extendedclip.deluxemenus.requirement.RequirementList; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.bukkit.entity.Player; @@ -25,6 +26,7 @@ public class PlayerListener extends Listener { + private final TaskScheduler scheduler; private final Cache cache = CacheBuilder.newBuilder().expireAfterWrite(75, TimeUnit.MILLISECONDS).build(); // This is so dumb. Mojang fix your shit. @@ -32,6 +34,7 @@ public class PlayerListener extends Listener { public PlayerListener(@NotNull final DeluxeMenus plugin) { super(plugin); + this.scheduler = plugin.getScheduler(); } @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) @@ -91,7 +94,7 @@ public void onClose(InventoryCloseEvent event) { final Player player = (Player) event.getPlayer(); if (Menu.isInMenu(player)) { - plugin.getScheduler().runTaskLater(player, () -> { + scheduler.runTaskLater(player, () -> { Menu.closeMenu(plugin, player, false); Menu.cleanInventory(plugin, player); player.updateInventory(); diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java index 0c81fe11..6ec238bc 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java @@ -6,6 +6,7 @@ import com.extendedclip.deluxemenus.menu.command.RegistrableMenuCommand; import com.extendedclip.deluxemenus.menu.options.MenuOptions; import com.extendedclip.deluxemenus.requirement.RequirementList; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.StringUtils; import java.util.ArrayList; @@ -35,6 +36,7 @@ public class Menu { private static final Map lastOpenedMenus = new ConcurrentHashMap<>(); private final DeluxeMenus plugin; + private final TaskScheduler scheduler; private final MenuOptions options; private final Map> items; // menu path starting from the plugin directory @@ -49,13 +51,14 @@ public Menu( final @NotNull String path ) { this.plugin = plugin; + this.scheduler = plugin.getScheduler(); this.options = options; this.items = items; this.path = path; if (this.options.registerCommands()) { this.command = new RegistrableMenuCommand(plugin, this); - plugin.getScheduler().runTask(() -> this.command.register()); + scheduler.runTask(() -> this.command.register()); } menus.put(this.options.name(), this); @@ -301,7 +304,7 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { + scheduler.runTaskAsynchronously(() -> { Set activeItems = new HashSet<>(); @@ -390,7 +393,7 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { + scheduler.runTask(viewer, () -> { if (options.refresh()) { holder.startRefreshTask(); } @@ -402,17 +405,17 @@ public void openMenu(final @NotNull Player viewer, final @Nullable Map { - DeluxeMenusOpenMenuEvent openEvent = new DeluxeMenusOpenMenuEvent(viewer, holder); - Bukkit.getPluginManager().callEvent(openEvent); - }); - }); - } + if (updatePlaceholders) { + holder.startUpdatePlaceholdersTask(); + } + }); + + scheduler.runTask(viewer, () -> { + DeluxeMenusOpenMenuEvent openEvent = new DeluxeMenusOpenMenuEvent(viewer, holder); + Bukkit.getPluginManager().callEvent(openEvent); + }); + }); + } public void refreshForAll() { menuHolders.stream().filter(menuHolder -> menuHolder.getMenuName().equalsIgnoreCase(options.name())).forEach(MenuHolder::refreshMenu); diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java index 0f216e95..5dc3222a 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.options.MenuOptions; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.scheduler.scheduling.tasks.MyScheduledTask; import com.extendedclip.deluxemenus.utils.StringUtils; import org.bukkit.entity.Player; @@ -19,6 +20,7 @@ public class MenuHolder implements InventoryHolder { private final DeluxeMenus plugin; + private final TaskScheduler scheduler; private final Player viewer; private Player placeholderPlayer; @@ -34,12 +36,14 @@ public class MenuHolder implements InventoryHolder { public MenuHolder(final @NotNull DeluxeMenus plugin, final @NotNull Player viewer) { this.plugin = plugin; + this.scheduler = plugin.getScheduler(); this.viewer = viewer; } public MenuHolder(final @NotNull DeluxeMenus plugin, final @NotNull Player viewer, final @NotNull String menuName, final @NotNull Set<@NotNull MenuItem> activeItems, final @NotNull Inventory inventory) { this.plugin = plugin; + this.scheduler = plugin.getScheduler(); this.viewer = viewer; this.menuName = menuName; this.activeItems = activeItems; @@ -133,7 +137,7 @@ public void refreshMenu() { setUpdating(true); - plugin.getScheduler().runTaskAsynchronously(() -> { + scheduler.runTaskAsynchronously(() -> { final Set active = new HashSet<>(); final Set slotsToClear = new HashSet<>(); @@ -169,11 +173,11 @@ public void refreshMenu() { } if (active.isEmpty()) { - plugin.getScheduler().runTask(getViewer(), () -> Menu.closeMenu(plugin, getViewer(), true)); + scheduler.runTask(getViewer(), () -> Menu.closeMenu(plugin, getViewer(), true)); return; } - plugin.getScheduler().runTask(getViewer(), () -> { + scheduler.runTask(getViewer(), () -> { for (int slot : slotsToClear) { getInventory().setItem(slot, null); @@ -242,7 +246,7 @@ public void startRefreshTask() { .map(MenuOptions::refreshInterval) .orElse(10); - refreshTask = plugin.getScheduler().runTaskTimerAsynchronously( + refreshTask = scheduler.runTaskTimerAsynchronously( this::refreshMenu, initialDelay, period @@ -261,7 +265,7 @@ public void startUpdatePlaceholdersTask() { .map(MenuOptions::updateInterval) .orElse(10); - updateTask = plugin.getScheduler().runTaskTimer( + updateTask = scheduler.runTaskTimer( getViewer(), () -> { diff --git a/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java b/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java index 4db347af..4ebd0d6d 100644 --- a/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java +++ b/src/main/java/com/extendedclip/deluxemenus/updatechecker/UpdateChecker.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.listener.Listener; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.utils.DebugLevel; import com.extendedclip.deluxemenus.utils.Messages; import java.io.BufferedReader; @@ -24,16 +25,18 @@ public class UpdateChecker extends Listener { private static final TextReplacementConfig.Builder CURRENT_VERSION_REPLACER_BUILDER = TextReplacementConfig.builder().matchLiteral(""); - final int resourceId = 11734; + private static final int RESOURCE_ID = 11734; + private final TaskScheduler scheduler; private String latestVersion = null; private boolean updateAvailable = false; public UpdateChecker(final @NotNull DeluxeMenus instance) { super(instance); + this.scheduler = plugin.getScheduler(); - plugin.getScheduler().runTaskAsynchronously(() -> { + scheduler.runTaskAsynchronously(() -> { if (check()) { - plugin.getScheduler().runTask(this::register); + scheduler.runTask(this::register); } }); } @@ -63,7 +66,7 @@ public void onJoin(final @NotNull PlayerJoinEvent event) { private String getSpigotVersion() { try { HttpURLConnection connection = (HttpURLConnection) new URL( - "https://api.spigotmc.org/legacy/update.php?resource=" + resourceId).openConnection(); + "https://api.spigotmc.org/legacy/update.php?resource=" + RESOURCE_ID).openConnection(); connection.setDoOutput(true); connection.setRequestMethod("GET"); return new BufferedReader(new InputStreamReader(connection.getInputStream())).readLine(); From c0fd08dbb3c091d24828a1cbe65cd665434e0781 Mon Sep 17 00:00:00 2001 From: r00tb33rman Date: Sat, 13 Sep 2025 23:29:50 -0400 Subject: [PATCH 06/13] Make click actions "secure," so duplicate actions cannot be executed whilst a Folia server is lagging --- .../deluxemenus/action/ClickAction.java | 1 - .../deluxemenus/action/ClickActionTask.java | 652 +++++++++--------- .../deluxemenus/listener/PlayerListener.java | 3 +- 3 files changed, 336 insertions(+), 320 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java index 59d61685..ace0bf84 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickAction.java @@ -76,7 +76,6 @@ public void setDelay(@Nullable final String delay) { this.delay = delay; } - /** * Get the unparsed chance of this action. * diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index a493cd1a..86555602 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -24,10 +24,13 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; public class ClickActionTask extends UniversalRunnable { + private static final ConcurrentHashMap IN_ACTION = new ConcurrentHashMap<>(); + private final DeluxeMenus plugin; private final TaskScheduler scheduler; private final UUID uuid; @@ -37,6 +40,7 @@ public class ClickActionTask extends UniversalRunnable { private final Map arguments; private final boolean parsePlaceholdersInArguments; private final boolean parsePlaceholdersAfterArguments; + private boolean actionHandled = false; public ClickActionTask( @NotNull final DeluxeMenus plugin, @@ -64,417 +68,431 @@ public void run() { return; } + if (this.actionHandled) { + return; + } + + final String inActionKey = this.uuid + ":" + this.actionType.name() + ":" + this.exec; + if (IN_ACTION.putIfAbsent(inActionKey, Boolean.TRUE) != null) { + return; + } + + this.actionHandled = true; + scheduler.runTask(player, () -> { + try { + final Optional holder = Menu.getMenuHolder(player); + final Player target = holder.isPresent() && holder.get().getPlaceholderPlayer() != null + ? holder.get().getPlaceholderPlayer() + : player; + + final String executable = StringUtils.replacePlaceholdersAndArguments( + this.exec, + this.arguments, + target, + this.parsePlaceholdersInArguments, + this.parsePlaceholdersAfterArguments); + + switch (actionType) { + case META: + if (!VersionHelper.IS_PDC_VERSION || plugin.getPersistentMetaHandler() == null) { + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Meta action not supported on this server version."); + break; + } + final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().parseAndExecuteMetaActionFromString(player, executable); + switch (result) { + case INVALID_SYNTAX: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Make sure you have the right syntax."); + break; + case NEW_VALUE_IS_DIFFERENT_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! New value is a different type than the old value!"); + break; + case INVALID_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! The specified type is not supported for the specified action!"); + break; + case EXISTENT_VALUE_IS_DIFFERENT_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Existent value is a different type than the new value!"); + break; + case VALUE_NOT_FOUND: + case SUCCESS: + default: + break; + } + break; - final Optional holder = Menu.getMenuHolder(player); - final Player target = holder.isPresent() && holder.get().getPlaceholderPlayer() != null - ? holder.get().getPlaceholderPlayer() - : player; - - final String executable = StringUtils.replacePlaceholdersAndArguments( - this.exec, - this.arguments, - target, - this.parsePlaceholdersInArguments, - this.parsePlaceholdersAfterArguments); - - switch (actionType) { - case META: - if (!VersionHelper.IS_PDC_VERSION || plugin.getPersistentMetaHandler() == null) { - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Meta action not supported on this server version."); + case PLAYER: + case PLAYER_COMMAND_EVENT: + player.chat("/" + executable); + break; + + case PLACEHOLDER: + holder.ifPresent(it -> it.setPlaceholders(executable)); + break; + + case CHAT: + player.chat(executable); + break; + + case CONSOLE: + scheduler.runTask(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable)); + break; + + case MINI_MESSAGE: + plugin.audiences().player(player).sendMessage(MiniMessage.miniMessage().deserialize(executable)); break; - } - final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().parseAndExecuteMetaActionFromString(player, executable); - switch (result) { - case INVALID_SYNTAX: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Make sure you have the right syntax."); - break; - case NEW_VALUE_IS_DIFFERENT_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! New value is a different type than the old value!"); - break; - case INVALID_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! The specified type is not supported for the specified action!"); - break; - case EXISTENT_VALUE_IS_DIFFERENT_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Existent value is a different type than the new value!"); - break; - case VALUE_NOT_FOUND: - case SUCCESS: - default: - break; - } - break; - case PLAYER: - case PLAYER_COMMAND_EVENT: - player.chat("/" + executable); - break; + case MINI_BROADCAST: + plugin.audiences().all().sendMessage(MiniMessage.miniMessage().deserialize(executable)); + break; - case PLACEHOLDER: - holder.ifPresent(it -> it.setPlaceholders(executable)); - break; + case MESSAGE: + player.sendMessage(StringUtils.color(executable)); + break; - case CHAT: - player.chat(executable); - break; + case LOG: + final String[] logParts = executable.split(" ", 2); - case CONSOLE: - scheduler.runTask(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable)); - break; + if (logParts.length == 0 || logParts[0].isBlank()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "LOG command requires at least a message"); + break; + } - case MINI_MESSAGE: - plugin.audiences().player(player).sendMessage(MiniMessage.miniMessage().deserialize(executable)); - break; + Level logLevel; + String message; - case MINI_BROADCAST: - plugin.audiences().all().sendMessage(MiniMessage.miniMessage().deserialize(executable)); - break; + if (logParts.length == 1) { + logLevel = Level.INFO; + message = logParts[0]; + } else { + message = logParts[1]; - case MESSAGE: - player.sendMessage(StringUtils.color(executable)); - break; + try { + logLevel = Level.parse(logParts[0].toUpperCase()); + } catch (IllegalArgumentException e) { + logLevel = Level.INFO; + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Log level " + logParts[0] + " is not a valid log level! Using INFO instead."); + } + } - case LOG: - final String[] logParts = executable.split(" ", 2); + plugin.getLogger().log(logLevel, String.format("[%s]: %s", holder.map(MenuHolder::getMenuName).orElse("Unknown Menu"), message)); + break; - if (logParts.length == 0 || logParts[0].isBlank()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "LOG command requires at least a message"); + case BROADCAST: + Bukkit.broadcastMessage(StringUtils.color(executable)); break; - } - Level logLevel; - String message; + case CLOSE: + Menu.closeMenu(plugin, player, true, true); + break; - if (logParts.length == 1) { - logLevel = Level.INFO; - message = logParts[0]; - } else { - message = logParts[1]; + case OPEN_GUI_MENU: + case OPEN_MENU: + final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); + final String[] executableParts = temporaryExecutable.split(" ", 2); - try { - logLevel = Level.parse(logParts[0].toUpperCase()); - } catch (IllegalArgumentException e) { - logLevel = Level.INFO; - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Log level " + logParts[0] + " is not a valid log level! Using INFO instead."); + if (executableParts.length == 0) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); + break; } - } - plugin.getLogger().log(logLevel, String.format("[%s]: %s", holder.map(MenuHolder::getMenuName).orElse("Unknown Menu"), message)); - break; + final String menuName = executableParts[0]; - case BROADCAST: - Bukkit.broadcastMessage(StringUtils.color(executable)); - break; + final Optional

optionalMenuToOpen = Menu.getMenuByName(menuName); - case CLOSE: - Menu.closeMenu(plugin, player, true, true); - break; + if (optionalMenuToOpen.isEmpty()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); + break; + } - case OPEN_GUI_MENU: - case OPEN_MENU: - final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); - final String[] executableParts = temporaryExecutable.split(" ", 2); + final Menu menuToOpen = optionalMenuToOpen.get(); - if (executableParts.length == 0) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); - break; - } + final List menuArgumentNames = menuToOpen.options().arguments(); - final String menuName = executableParts[0]; + String[] passedArgumentValues = null; + if (executableParts.length > 1) { + passedArgumentValues = executableParts[1].split(" "); + } - final Optional optionalMenuToOpen = Menu.getMenuByName(menuName); + if (menuArgumentNames.isEmpty()) { + if (passedArgumentValues != null && passedArgumentValues.length > 0) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" + ); + } - if (optionalMenuToOpen.isEmpty()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); - break; - } + if (holder.isEmpty()) { + menuToOpen.openMenu(player); + break; + } - final Menu menuToOpen = optionalMenuToOpen.get(); + menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); + break; + } - final List menuArgumentNames = menuToOpen.options().arguments(); + if (passedArgumentValues == null || passedArgumentValues.length == 0) { + // Replicate old behavior: If no arguments are given, open the menu with the arguments from the current menu + if (holder.isEmpty()) { + menuToOpen.openMenu(player); + break; + } - String[] passedArgumentValues = null; - if (executableParts.length > 1) { - passedArgumentValues = executableParts[1].split(" "); - } + menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); + break; + } - if (menuArgumentNames.isEmpty()) { - if (passedArgumentValues != null && passedArgumentValues.length > 0) { + if (passedArgumentValues.length < menuArgumentNames.size()) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" + "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" ); + break; } - if (holder.isEmpty()) { - menuToOpen.openMenu(player); - break; + final Map argumentsMap = new HashMap<>(); + if (holder.isPresent() && holder.get().getTypedArgs() != null) { + // Pass the arguments from the current menu to the new menu. If the new menu has arguments with the + // same name, they will be overwritten + argumentsMap.putAll(holder.get().getTypedArgs()); } - menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); - break; - } + for (int index = 0; index < menuArgumentNames.size(); index++) { + final String argumentName = menuArgumentNames.get(index); + + if (passedArgumentValues.length <= index) { + // This should never be the case! + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" + ); + break; + } + + if (menuArgumentNames.size() == index + 1) { + // If this is the last argument, get all remaining values and join them + final String lastArgumentValue = String.join(" ", Arrays.asList(passedArgumentValues).subList(index, passedArgumentValues.length)); + argumentsMap.put(argumentName, lastArgumentValue); + break; + } + + argumentsMap.put(argumentName, passedArgumentValues[index]); + } - if (passedArgumentValues == null || passedArgumentValues.length == 0) { - // Replicate old behavior: If no arguments are given, open the menu with the arguments from the current menu if (holder.isEmpty()) { - menuToOpen.openMenu(player); + menuToOpen.openMenu(player, argumentsMap, null); break; } - menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); + menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer()); break; - } - - if (passedArgumentValues.length < menuArgumentNames.size()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" - ); + + case CONNECT: + plugin.connect(player, executable); break; - } - final Map argumentsMap = new HashMap<>(); - if (holder.isPresent() && holder.get().getTypedArgs() != null) { - // Pass the arguments from the current menu to the new menu. If the new menu has arguments with the - // same name, they will be overwritten - argumentsMap.putAll(holder.get().getTypedArgs()); - } + case JSON_MESSAGE: + AdventureUtils.sendJson(plugin, player, executable); + break; - for (int index = 0; index < menuArgumentNames.size(); index++) { - final String argumentName = menuArgumentNames.get(index); + case JSON_BROADCAST: + case BROADCAST_JSON: + plugin.audiences().all().sendMessage(AdventureUtils.fromJson(executable)); + break; - if (passedArgumentValues.length <= index) { - // This should never be the case! + case REFRESH: + if (holder.isEmpty()) { plugin.debug( - DebugLevel.HIGHEST, + DebugLevel.MEDIUM, Level.WARNING, - "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" + player.getName() + " does not have menu open! Nothing to refresh!" ); break; } - if (menuArgumentNames.size() == index + 1) { - // If this is the last argument, get all remaining values and join them - final String lastArgumentValue = String.join(" ", Arrays.asList(passedArgumentValues).subList(index, passedArgumentValues.length)); - argumentsMap.put(argumentName, lastArgumentValue); + holder.get().refreshMenu(); + break; + + case TAKE_MONEY: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot take money!"); break; } - argumentsMap.put(argumentName, passedArgumentValues[index]); - } - - if (holder.isEmpty()) { - menuToOpen.openMenu(player, argumentsMap, null); - break; - } - - menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer()); - break; - - case CONNECT: - plugin.connect(player, executable); - break; - - case JSON_MESSAGE: - AdventureUtils.sendJson(plugin, player, executable); - break; - - case JSON_BROADCAST: - case BROADCAST_JSON: - plugin.audiences().all().sendMessage(AdventureUtils.fromJson(executable)); - break; - - case REFRESH: - if (holder.isEmpty()) { - plugin.debug( - DebugLevel.MEDIUM, - Level.WARNING, - player.getName() + " does not have menu open! Nothing to refresh!" - ); + try { + plugin.getVault().takeMoney(player, Double.parseDouble(executable)); + } catch (final NumberFormatException exception) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for take money action: " + executable + ", is not a valid number!" + ); + } break; - } - - holder.get().refreshMenu(); - break; - case TAKE_MONEY: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot take money!"); - break; - } - - try { - plugin.getVault().takeMoney(player, Double.parseDouble(executable)); - } catch (final NumberFormatException exception) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for take money action: " + executable + ", is not a valid number!" - ); - } - break; - - case GIVE_MONEY: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot give money!"); - break; - } - - try { - plugin.getVault().giveMoney(player, Double.parseDouble(executable)); - } catch (final NumberFormatException exception) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for give money action: " + executable + ", is not a valid number!" - ); - } - break; - - case TAKE_EXP: - case GIVE_EXP: - final String lowerCaseExecutable = executable.toLowerCase(); - - try { - if (Integer.parseInt(lowerCaseExecutable.replaceAll("l", "")) <= 0) break; - - if (actionType == ActionType.TAKE_EXP) { - ExpUtils.setExp(player, "-" + lowerCaseExecutable); + case GIVE_MONEY: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot give money!"); break; } - ExpUtils.setExp(player, lowerCaseExecutable); - break; - - } catch (final NumberFormatException exception) { - if (actionType == ActionType.TAKE_EXP) { + try { + plugin.getVault().giveMoney(player, Double.parseDouble(executable)); + } catch (final NumberFormatException exception) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Amount for take exp action: " + executable + ", is not a valid number!" + "Amount for give money action: " + executable + ", is not a valid number!" ); - break; } - - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for give exp action: " + executable + ", is not a valid number!" - ); break; - } - - case GIVE_PERM: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Vault not hooked! Cannot give permission: " + executable + "!"); - break; - } - plugin.getVault().givePermission(player, executable); - break; + case TAKE_EXP: + case GIVE_EXP: + final String lowerCaseExecutable = executable.toLowerCase(); - case TAKE_PERM: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Vault not hooked! Cannot take permission: " + executable + "!"); - break; - } + try { + if (Integer.parseInt(lowerCaseExecutable.replaceAll("l", "")) <= 0) break; + + if (actionType == ActionType.TAKE_EXP) { + ExpUtils.setExp(player, "-" + lowerCaseExecutable); + break; + } - plugin.getVault().takePermission(player, executable); - break; + ExpUtils.setExp(player, lowerCaseExecutable); + break; - case BROADCAST_SOUND: - case BROADCAST_WORLD_SOUND: - case PLAY_SOUND: - final Sound sound; - float volume = 1; - float pitch = 1; + } catch (final NumberFormatException exception) { + if (actionType == ActionType.TAKE_EXP) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for take exp action: " + executable + ", is not a valid number!" + ); + break; + } - if (!executable.contains(" ")) { - try { - sound = SoundUtils.getSound(executable.toUpperCase()); - } catch (final IllegalArgumentException exception) { - plugin.printStacktrace( - "Sound name given for sound action: " + executable + ", is not a valid sound!", - exception + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for give exp action: " + executable + ", is not a valid number!" ); break; } - } else { - String[] parts = executable.split(" ", 3); - try { - sound = SoundUtils.getSound(parts[0].toUpperCase()); - } catch (final IllegalArgumentException exception) { - plugin.printStacktrace( - "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", - exception - ); + case GIVE_PERM: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Vault not hooked! Cannot give permission: " + executable + "!"); break; } - if (parts.length == 3) { + plugin.getVault().givePermission(player, executable); + break; + + case TAKE_PERM: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Vault not hooked! Cannot take permission: " + executable + "!"); + break; + } + + plugin.getVault().takePermission(player, executable); + break; + + case BROADCAST_SOUND: + case BROADCAST_WORLD_SOUND: + case PLAY_SOUND: + final Sound sound; + float volume = 1; + float pitch = 1; + + if (!executable.contains(" ")) { + try { + sound = SoundUtils.getSound(executable.toUpperCase()); + } catch (final IllegalArgumentException exception) { + plugin.printStacktrace( + "Sound name given for sound action: " + executable + ", is not a valid sound!", + exception + ); + break; + } + } else { + String[] parts = executable.split(" ", 3); + try { - pitch = Float.parseFloat(parts[2]); + sound = SoundUtils.getSound(parts[0].toUpperCase()); + } catch (final IllegalArgumentException exception) { + plugin.printStacktrace( + "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", + exception + ); + break; + } + + if (parts.length == 3) { + try { + pitch = Float.parseFloat(parts[2]); + } catch (final NumberFormatException exception) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Pitch given for sound action: " + parts[2] + ", is not a valid number!" + ); + + plugin.printStacktrace( + "Pitch given for sound action: " + parts[2] + ", is not a valid number!", + exception + ); + } + } + + try { + volume = Float.parseFloat(parts[1]); } catch (final NumberFormatException exception) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Pitch given for sound action: " + parts[2] + ", is not a valid number!" + "Volume given for sound action: " + parts[1] + ", is not a valid number!" ); plugin.printStacktrace( - "Pitch given for sound action: " + parts[2] + ", is not a valid number!", + "Volume given for sound action: " + parts[1] + ", is not a valid number!", exception ); } } - try { - volume = Float.parseFloat(parts[1]); - } catch (final NumberFormatException exception) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Volume given for sound action: " + parts[1] + ", is not a valid number!" - ); - - plugin.printStacktrace( - "Volume given for sound action: " + parts[1] + ", is not a valid number!", - exception - ); + switch (actionType) { + case BROADCAST_SOUND: + for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); + } + break; + + case BROADCAST_WORLD_SOUND: + for (final Player broadcastTarget : player.getWorld().getPlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); + } + break; + + case PLAY_SOUND: + player.playSound(player.getLocation(), sound, volume, pitch); + break; } - } - - switch (actionType) { - case BROADCAST_SOUND: - for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { - broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); - } - break; - - case BROADCAST_WORLD_SOUND: - for (final Player broadcastTarget : player.getWorld().getPlayers()) { - broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); - } - break; - - case PLAY_SOUND: - player.playSound(player.getLocation(), sound, volume, pitch); - break; - } - break; + break; - default: - break; + default: + break; + } + } finally { + IN_ACTION.remove(inActionKey); } }); } diff --git a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java index 89b417b2..f2d7d5f3 100644 --- a/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java +++ b/src/main/java/com/extendedclip/deluxemenus/listener/PlayerListener.java @@ -183,8 +183,7 @@ public void onClick(InventoryClickEvent event) { } if (event.getClick() == ClickType.MIDDLE) { - if (handleClick(player, holder, item.options().middleClickHandler(), item.options().middleClickRequirements())) { - } + handleClick(player, holder, item.options().middleClickHandler(), item.options().middleClickRequirements()); } } From 26ec6b67c21b4f28216815150b2363d0687a7002 Mon Sep 17 00:00:00 2001 From: r00tb33rman Date: Sat, 13 Sep 2025 23:47:30 -0400 Subject: [PATCH 07/13] Reference viewer directly --- .../com/extendedclip/deluxemenus/menu/MenuHolder.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java index 5dc3222a..b28dc145 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/MenuHolder.java @@ -103,7 +103,7 @@ public Optional getMenu() { } public @NotNull String setPlaceholders(final @NotNull String string) { - final Player player = this.placeholderPlayer != null ? this.placeholderPlayer : this.getViewer(); + final Player player = this.placeholderPlayer != null ? this.placeholderPlayer : this.viewer; if (player == null) { return string; } @@ -112,7 +112,7 @@ public Optional getMenu() { } public @NotNull String setArguments(final @NotNull String string) { - final Player player = this.placeholderPlayer != null ? this.placeholderPlayer : this.getViewer(); + final Player player = this.placeholderPlayer != null ? this.placeholderPlayer : this.viewer; return StringUtils.replaceArguments( string, @@ -173,11 +173,11 @@ public void refreshMenu() { } if (active.isEmpty()) { - scheduler.runTask(getViewer(), () -> Menu.closeMenu(plugin, getViewer(), true)); + scheduler.runTask(viewer, () -> Menu.closeMenu(plugin, viewer, true)); return; } - scheduler.runTask(getViewer(), () -> { + scheduler.runTask(viewer, () -> { for (int slot : slotsToClear) { getInventory().setItem(slot, null); @@ -266,7 +266,7 @@ public void startUpdatePlaceholdersTask() { .orElse(10); updateTask = scheduler.runTaskTimer( - getViewer(), + viewer, () -> { if (updating) { From 02aa131a44f0f8e001bb6985d53b1546cd236e91 Mon Sep 17 00:00:00 2001 From: R00tB33rMan Date: Tue, 7 Oct 2025 07:29:05 -0400 Subject: [PATCH 08/13] This weird implementation is not a DeluxeMenus "responsibility" --- .../deluxemenus/action/ClickActionTask.java | 652 +++++++++--------- 1 file changed, 317 insertions(+), 335 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index 86555602..a493cd1a 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -24,13 +24,10 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; public class ClickActionTask extends UniversalRunnable { - private static final ConcurrentHashMap IN_ACTION = new ConcurrentHashMap<>(); - private final DeluxeMenus plugin; private final TaskScheduler scheduler; private final UUID uuid; @@ -40,7 +37,6 @@ public class ClickActionTask extends UniversalRunnable { private final Map arguments; private final boolean parsePlaceholdersInArguments; private final boolean parsePlaceholdersAfterArguments; - private boolean actionHandled = false; public ClickActionTask( @NotNull final DeluxeMenus plugin, @@ -68,431 +64,417 @@ public void run() { return; } - if (this.actionHandled) { - return; - } - - final String inActionKey = this.uuid + ":" + this.actionType.name() + ":" + this.exec; - if (IN_ACTION.putIfAbsent(inActionKey, Boolean.TRUE) != null) { - return; - } - - this.actionHandled = true; - scheduler.runTask(player, () -> { - try { - final Optional holder = Menu.getMenuHolder(player); - final Player target = holder.isPresent() && holder.get().getPlaceholderPlayer() != null - ? holder.get().getPlaceholderPlayer() - : player; - - final String executable = StringUtils.replacePlaceholdersAndArguments( - this.exec, - this.arguments, - target, - this.parsePlaceholdersInArguments, - this.parsePlaceholdersAfterArguments); - - switch (actionType) { - case META: - if (!VersionHelper.IS_PDC_VERSION || plugin.getPersistentMetaHandler() == null) { - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Meta action not supported on this server version."); - break; - } - final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().parseAndExecuteMetaActionFromString(player, executable); - switch (result) { - case INVALID_SYNTAX: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Make sure you have the right syntax."); - break; - case NEW_VALUE_IS_DIFFERENT_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! New value is a different type than the old value!"); - break; - case INVALID_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! The specified type is not supported for the specified action!"); - break; - case EXISTENT_VALUE_IS_DIFFERENT_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Existent value is a different type than the new value!"); - break; - case VALUE_NOT_FOUND: - case SUCCESS: - default: - break; - } - break; - - case PLAYER: - case PLAYER_COMMAND_EVENT: - player.chat("/" + executable); - break; - - case PLACEHOLDER: - holder.ifPresent(it -> it.setPlaceholders(executable)); - break; - - case CHAT: - player.chat(executable); - break; - - case CONSOLE: - scheduler.runTask(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable)); - break; - case MINI_MESSAGE: - plugin.audiences().player(player).sendMessage(MiniMessage.miniMessage().deserialize(executable)); + final Optional holder = Menu.getMenuHolder(player); + final Player target = holder.isPresent() && holder.get().getPlaceholderPlayer() != null + ? holder.get().getPlaceholderPlayer() + : player; + + final String executable = StringUtils.replacePlaceholdersAndArguments( + this.exec, + this.arguments, + target, + this.parsePlaceholdersInArguments, + this.parsePlaceholdersAfterArguments); + + switch (actionType) { + case META: + if (!VersionHelper.IS_PDC_VERSION || plugin.getPersistentMetaHandler() == null) { + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Meta action not supported on this server version."); break; + } + final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().parseAndExecuteMetaActionFromString(player, executable); + switch (result) { + case INVALID_SYNTAX: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Make sure you have the right syntax."); + break; + case NEW_VALUE_IS_DIFFERENT_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! New value is a different type than the old value!"); + break; + case INVALID_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! The specified type is not supported for the specified action!"); + break; + case EXISTENT_VALUE_IS_DIFFERENT_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Existent value is a different type than the new value!"); + break; + case VALUE_NOT_FOUND: + case SUCCESS: + default: + break; + } + break; - case MINI_BROADCAST: - plugin.audiences().all().sendMessage(MiniMessage.miniMessage().deserialize(executable)); - break; + case PLAYER: + case PLAYER_COMMAND_EVENT: + player.chat("/" + executable); + break; - case MESSAGE: - player.sendMessage(StringUtils.color(executable)); - break; + case PLACEHOLDER: + holder.ifPresent(it -> it.setPlaceholders(executable)); + break; - case LOG: - final String[] logParts = executable.split(" ", 2); + case CHAT: + player.chat(executable); + break; - if (logParts.length == 0 || logParts[0].isBlank()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "LOG command requires at least a message"); - break; - } + case CONSOLE: + scheduler.runTask(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable)); + break; - Level logLevel; - String message; + case MINI_MESSAGE: + plugin.audiences().player(player).sendMessage(MiniMessage.miniMessage().deserialize(executable)); + break; - if (logParts.length == 1) { - logLevel = Level.INFO; - message = logParts[0]; - } else { - message = logParts[1]; + case MINI_BROADCAST: + plugin.audiences().all().sendMessage(MiniMessage.miniMessage().deserialize(executable)); + break; - try { - logLevel = Level.parse(logParts[0].toUpperCase()); - } catch (IllegalArgumentException e) { - logLevel = Level.INFO; - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Log level " + logParts[0] + " is not a valid log level! Using INFO instead."); - } - } + case MESSAGE: + player.sendMessage(StringUtils.color(executable)); + break; - plugin.getLogger().log(logLevel, String.format("[%s]: %s", holder.map(MenuHolder::getMenuName).orElse("Unknown Menu"), message)); - break; + case LOG: + final String[] logParts = executable.split(" ", 2); - case BROADCAST: - Bukkit.broadcastMessage(StringUtils.color(executable)); + if (logParts.length == 0 || logParts[0].isBlank()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "LOG command requires at least a message"); break; + } - case CLOSE: - Menu.closeMenu(plugin, player, true, true); - break; + Level logLevel; + String message; - case OPEN_GUI_MENU: - case OPEN_MENU: - final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); - final String[] executableParts = temporaryExecutable.split(" ", 2); + if (logParts.length == 1) { + logLevel = Level.INFO; + message = logParts[0]; + } else { + message = logParts[1]; - if (executableParts.length == 0) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); - break; + try { + logLevel = Level.parse(logParts[0].toUpperCase()); + } catch (IllegalArgumentException e) { + logLevel = Level.INFO; + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Log level " + logParts[0] + " is not a valid log level! Using INFO instead."); } + } - final String menuName = executableParts[0]; + plugin.getLogger().log(logLevel, String.format("[%s]: %s", holder.map(MenuHolder::getMenuName).orElse("Unknown Menu"), message)); + break; - final Optional optionalMenuToOpen = Menu.getMenuByName(menuName); + case BROADCAST: + Bukkit.broadcastMessage(StringUtils.color(executable)); + break; - if (optionalMenuToOpen.isEmpty()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); - break; - } + case CLOSE: + Menu.closeMenu(plugin, player, true, true); + break; - final Menu menuToOpen = optionalMenuToOpen.get(); + case OPEN_GUI_MENU: + case OPEN_MENU: + final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); + final String[] executableParts = temporaryExecutable.split(" ", 2); - final List menuArgumentNames = menuToOpen.options().arguments(); + if (executableParts.length == 0) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); + break; + } - String[] passedArgumentValues = null; - if (executableParts.length > 1) { - passedArgumentValues = executableParts[1].split(" "); - } + final String menuName = executableParts[0]; - if (menuArgumentNames.isEmpty()) { - if (passedArgumentValues != null && passedArgumentValues.length > 0) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" - ); - } + final Optional optionalMenuToOpen = Menu.getMenuByName(menuName); - if (holder.isEmpty()) { - menuToOpen.openMenu(player); - break; - } + if (optionalMenuToOpen.isEmpty()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); + break; + } - menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); - break; - } + final Menu menuToOpen = optionalMenuToOpen.get(); - if (passedArgumentValues == null || passedArgumentValues.length == 0) { - // Replicate old behavior: If no arguments are given, open the menu with the arguments from the current menu - if (holder.isEmpty()) { - menuToOpen.openMenu(player); - break; - } + final List menuArgumentNames = menuToOpen.options().arguments(); - menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); - break; - } + String[] passedArgumentValues = null; + if (executableParts.length > 1) { + passedArgumentValues = executableParts[1].split(" "); + } - if (passedArgumentValues.length < menuArgumentNames.size()) { + if (menuArgumentNames.isEmpty()) { + if (passedArgumentValues != null && passedArgumentValues.length > 0) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" + "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" ); - break; - } - - final Map argumentsMap = new HashMap<>(); - if (holder.isPresent() && holder.get().getTypedArgs() != null) { - // Pass the arguments from the current menu to the new menu. If the new menu has arguments with the - // same name, they will be overwritten - argumentsMap.putAll(holder.get().getTypedArgs()); - } - - for (int index = 0; index < menuArgumentNames.size(); index++) { - final String argumentName = menuArgumentNames.get(index); - - if (passedArgumentValues.length <= index) { - // This should never be the case! - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" - ); - break; - } - - if (menuArgumentNames.size() == index + 1) { - // If this is the last argument, get all remaining values and join them - final String lastArgumentValue = String.join(" ", Arrays.asList(passedArgumentValues).subList(index, passedArgumentValues.length)); - argumentsMap.put(argumentName, lastArgumentValue); - break; - } - - argumentsMap.put(argumentName, passedArgumentValues[index]); } if (holder.isEmpty()) { - menuToOpen.openMenu(player, argumentsMap, null); + menuToOpen.openMenu(player); break; } - menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer()); - break; - - case CONNECT: - plugin.connect(player, executable); - break; - - case JSON_MESSAGE: - AdventureUtils.sendJson(plugin, player, executable); - break; - - case JSON_BROADCAST: - case BROADCAST_JSON: - plugin.audiences().all().sendMessage(AdventureUtils.fromJson(executable)); + menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); break; + } - case REFRESH: + if (passedArgumentValues == null || passedArgumentValues.length == 0) { + // Replicate old behavior: If no arguments are given, open the menu with the arguments from the current menu if (holder.isEmpty()) { - plugin.debug( - DebugLevel.MEDIUM, - Level.WARNING, - player.getName() + " does not have menu open! Nothing to refresh!" - ); + menuToOpen.openMenu(player); break; } - holder.get().refreshMenu(); + menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); break; + } + + if (passedArgumentValues.length < menuArgumentNames.size()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" + ); + break; + } - case TAKE_MONEY: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot take money!"); - break; - } + final Map argumentsMap = new HashMap<>(); + if (holder.isPresent() && holder.get().getTypedArgs() != null) { + // Pass the arguments from the current menu to the new menu. If the new menu has arguments with the + // same name, they will be overwritten + argumentsMap.putAll(holder.get().getTypedArgs()); + } - try { - plugin.getVault().takeMoney(player, Double.parseDouble(executable)); - } catch (final NumberFormatException exception) { + for (int index = 0; index < menuArgumentNames.size(); index++) { + final String argumentName = menuArgumentNames.get(index); + + if (passedArgumentValues.length <= index) { + // This should never be the case! plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Amount for take money action: " + executable + ", is not a valid number!" + "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" ); - } - break; - - case GIVE_MONEY: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot give money!"); break; } - try { - plugin.getVault().giveMoney(player, Double.parseDouble(executable)); - } catch (final NumberFormatException exception) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for give money action: " + executable + ", is not a valid number!" - ); + if (menuArgumentNames.size() == index + 1) { + // If this is the last argument, get all remaining values and join them + final String lastArgumentValue = String.join(" ", Arrays.asList(passedArgumentValues).subList(index, passedArgumentValues.length)); + argumentsMap.put(argumentName, lastArgumentValue); + break; } - break; - case TAKE_EXP: - case GIVE_EXP: - final String lowerCaseExecutable = executable.toLowerCase(); + argumentsMap.put(argumentName, passedArgumentValues[index]); + } - try { - if (Integer.parseInt(lowerCaseExecutable.replaceAll("l", "")) <= 0) break; + if (holder.isEmpty()) { + menuToOpen.openMenu(player, argumentsMap, null); + break; + } + + menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer()); + break; + + case CONNECT: + plugin.connect(player, executable); + break; + + case JSON_MESSAGE: + AdventureUtils.sendJson(plugin, player, executable); + break; + + case JSON_BROADCAST: + case BROADCAST_JSON: + plugin.audiences().all().sendMessage(AdventureUtils.fromJson(executable)); + break; + + case REFRESH: + if (holder.isEmpty()) { + plugin.debug( + DebugLevel.MEDIUM, + Level.WARNING, + player.getName() + " does not have menu open! Nothing to refresh!" + ); + break; + } - if (actionType == ActionType.TAKE_EXP) { - ExpUtils.setExp(player, "-" + lowerCaseExecutable); - break; - } + holder.get().refreshMenu(); + break; - ExpUtils.setExp(player, lowerCaseExecutable); + case TAKE_MONEY: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot take money!"); + break; + } + + try { + plugin.getVault().takeMoney(player, Double.parseDouble(executable)); + } catch (final NumberFormatException exception) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for take money action: " + executable + ", is not a valid number!" + ); + } + break; + + case GIVE_MONEY: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot give money!"); + break; + } + + try { + plugin.getVault().giveMoney(player, Double.parseDouble(executable)); + } catch (final NumberFormatException exception) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for give money action: " + executable + ", is not a valid number!" + ); + } + break; + + case TAKE_EXP: + case GIVE_EXP: + final String lowerCaseExecutable = executable.toLowerCase(); + + try { + if (Integer.parseInt(lowerCaseExecutable.replaceAll("l", "")) <= 0) break; + + if (actionType == ActionType.TAKE_EXP) { + ExpUtils.setExp(player, "-" + lowerCaseExecutable); break; + } - } catch (final NumberFormatException exception) { - if (actionType == ActionType.TAKE_EXP) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for take exp action: " + executable + ", is not a valid number!" - ); - break; - } + ExpUtils.setExp(player, lowerCaseExecutable); + break; + } catch (final NumberFormatException exception) { + if (actionType == ActionType.TAKE_EXP) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Amount for give exp action: " + executable + ", is not a valid number!" + "Amount for take exp action: " + executable + ", is not a valid number!" ); break; } - case GIVE_PERM: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Vault not hooked! Cannot give permission: " + executable + "!"); - break; - } - - plugin.getVault().givePermission(player, executable); + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for give exp action: " + executable + ", is not a valid number!" + ); + break; + } + + case GIVE_PERM: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Vault not hooked! Cannot give permission: " + executable + "!"); break; + } - case TAKE_PERM: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Vault not hooked! Cannot take permission: " + executable + "!"); - break; - } + plugin.getVault().givePermission(player, executable); + break; - plugin.getVault().takePermission(player, executable); + case TAKE_PERM: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Vault not hooked! Cannot take permission: " + executable + "!"); break; + } - case BROADCAST_SOUND: - case BROADCAST_WORLD_SOUND: - case PLAY_SOUND: - final Sound sound; - float volume = 1; - float pitch = 1; + plugin.getVault().takePermission(player, executable); + break; - if (!executable.contains(" ")) { - try { - sound = SoundUtils.getSound(executable.toUpperCase()); - } catch (final IllegalArgumentException exception) { - plugin.printStacktrace( - "Sound name given for sound action: " + executable + ", is not a valid sound!", - exception - ); - break; - } - } else { - String[] parts = executable.split(" ", 3); + case BROADCAST_SOUND: + case BROADCAST_WORLD_SOUND: + case PLAY_SOUND: + final Sound sound; + float volume = 1; + float pitch = 1; - try { - sound = SoundUtils.getSound(parts[0].toUpperCase()); - } catch (final IllegalArgumentException exception) { - plugin.printStacktrace( - "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", - exception - ); - break; - } + if (!executable.contains(" ")) { + try { + sound = SoundUtils.getSound(executable.toUpperCase()); + } catch (final IllegalArgumentException exception) { + plugin.printStacktrace( + "Sound name given for sound action: " + executable + ", is not a valid sound!", + exception + ); + break; + } + } else { + String[] parts = executable.split(" ", 3); - if (parts.length == 3) { - try { - pitch = Float.parseFloat(parts[2]); - } catch (final NumberFormatException exception) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Pitch given for sound action: " + parts[2] + ", is not a valid number!" - ); - - plugin.printStacktrace( - "Pitch given for sound action: " + parts[2] + ", is not a valid number!", - exception - ); - } - } + try { + sound = SoundUtils.getSound(parts[0].toUpperCase()); + } catch (final IllegalArgumentException exception) { + plugin.printStacktrace( + "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", + exception + ); + break; + } + if (parts.length == 3) { try { - volume = Float.parseFloat(parts[1]); + pitch = Float.parseFloat(parts[2]); } catch (final NumberFormatException exception) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Volume given for sound action: " + parts[1] + ", is not a valid number!" + "Pitch given for sound action: " + parts[2] + ", is not a valid number!" ); plugin.printStacktrace( - "Volume given for sound action: " + parts[1] + ", is not a valid number!", + "Pitch given for sound action: " + parts[2] + ", is not a valid number!", exception ); } } - switch (actionType) { - case BROADCAST_SOUND: - for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { - broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); - } - break; - - case BROADCAST_WORLD_SOUND: - for (final Player broadcastTarget : player.getWorld().getPlayers()) { - broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); - } - break; - - case PLAY_SOUND: - player.playSound(player.getLocation(), sound, volume, pitch); - break; + try { + volume = Float.parseFloat(parts[1]); + } catch (final NumberFormatException exception) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Volume given for sound action: " + parts[1] + ", is not a valid number!" + ); + + plugin.printStacktrace( + "Volume given for sound action: " + parts[1] + ", is not a valid number!", + exception + ); } - break; + } - default: - break; - } - } finally { - IN_ACTION.remove(inActionKey); + switch (actionType) { + case BROADCAST_SOUND: + for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); + } + break; + + case BROADCAST_WORLD_SOUND: + for (final Player broadcastTarget : player.getWorld().getPlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); + } + break; + + case PLAY_SOUND: + player.playSound(player.getLocation(), sound, volume, pitch); + break; + } + break; + + default: + break; } }); } From 873ca2042d04cc92959665e6f1d5da4b2816cb75 Mon Sep 17 00:00:00 2001 From: R00tB33rMan Date: Sat, 25 Oct 2025 16:07:24 -0400 Subject: [PATCH 09/13] Schedule command registration globally --- .../menu/command/RegistrableMenuCommand.java | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/command/RegistrableMenuCommand.java b/src/main/java/com/extendedclip/deluxemenus/menu/command/RegistrableMenuCommand.java index 3edf522b..85797e79 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/command/RegistrableMenuCommand.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/command/RegistrableMenuCommand.java @@ -2,6 +2,7 @@ import com.extendedclip.deluxemenus.DeluxeMenus; import com.extendedclip.deluxemenus.menu.Menu; +import com.extendedclip.deluxemenus.scheduler.scheduling.schedulers.TaskScheduler; import com.extendedclip.deluxemenus.utils.DebugLevel; import me.clip.placeholderapi.util.Msg; import org.bukkit.Bukkit; @@ -25,6 +26,7 @@ public class RegistrableMenuCommand extends Command { private static CommandMap commandMap = null; private final DeluxeMenus plugin; + private final TaskScheduler scheduler; private Menu menu; private boolean registered = false; @@ -34,6 +36,7 @@ public RegistrableMenuCommand(final @NotNull DeluxeMenus plugin, final @NotNull Menu menu) { super(menu.options().commands().isEmpty() ? menu.options().name() : menu.options().commands().get(0)); this.plugin = plugin; + this.scheduler = plugin.getScheduler(); this.menu = menu; if (menu.options().commands().size() > 1) { @@ -84,34 +87,36 @@ public boolean execute(final @NotNull CommandSender sender, final @NotNull Strin } public void register() { - if (registered) { - throw new IllegalStateException("This command was already registered!"); - } + scheduler.runTask(() -> { + if (registered) { + throw new IllegalStateException("This command was already registered!"); + } - registered = true; + registered = true; + + if (commandMap == null) { + try { + final Field f = Bukkit.getServer().getClass().getDeclaredField("commandMap"); + f.setAccessible(true); + commandMap = (CommandMap) f.get(Bukkit.getServer()); + } catch (final @NotNull Exception exception) { + plugin.printStacktrace( + "Something went wrong while trying to register command: " + this.getName(), + exception + ); + return; + } + } - if (commandMap == null) { - try { - final Field f = Bukkit.getServer().getClass().getDeclaredField("commandMap"); - f.setAccessible(true); - commandMap = (CommandMap) f.get(Bukkit.getServer()); - } catch (final @NotNull Exception exception) { - plugin.printStacktrace( - "Something went wrong while trying to register command: " + this.getName(), - exception + boolean registered = commandMap.register(FALLBACK_PREFIX, this); + if (registered) { + plugin.debug( + DebugLevel.LOW, + Level.INFO, + "Registered command: " + this.getName() + " for menu: " + menu.options().name() ); - return; } - } - - boolean registered = commandMap.register(FALLBACK_PREFIX, this); - if (registered) { - plugin.debug( - DebugLevel.LOW, - Level.INFO, - "Registered command: " + this.getName() + " for menu: " + menu.options().name() - ); - } + }); } public void unregister() { From 66837b1702116565291c513b722d3048138d3a3b Mon Sep 17 00:00:00 2001 From: RootBeer Date: Sun, 15 Mar 2026 19:24:46 -0400 Subject: [PATCH 10/13] Rebase --- .../deluxemenus/action/ClickActionTask.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index a493cd1a..53574116 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -392,9 +392,15 @@ public void run() { break; case BROADCAST_SOUND: + case BROADCAST_RAW_SOUND: case BROADCAST_WORLD_SOUND: + case BROADCAST_WORLD_RAW_SOUND: + case PLAY_RAW_SOUND: case PLAY_SOUND: - final Sound sound; + boolean isRaw = isRaw(actionType); + + Sound sound = null; + String soundName = executable; float volume = 1; float pitch = 1; @@ -455,6 +461,22 @@ public void run() { } switch (actionType) { + case BROADCAST_WORLD_RAW_SOUND: + for (final Player broadcastTarget : player.getWorld().getPlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), soundName, volume, pitch); + } + break; + + case BROADCAST_RAW_SOUND: + for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), soundName, volume, pitch); + } + break; + + case PLAY_RAW_SOUND: + player.playSound(player.getLocation(), soundName, volume, pitch); + break; + case BROADCAST_SOUND: for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); @@ -478,4 +500,8 @@ public void run() { } }); } + + private boolean isRaw(ActionType actionType) { + return actionType == ActionType.PLAY_RAW_SOUND || actionType == ActionType.BROADCAST_RAW_SOUND || actionType == ActionType.BROADCAST_WORLD_RAW_SOUND; + } } From ee277cbc5d4e911877c6fd8cf6e124d0e00da27e Mon Sep 17 00:00:00 2001 From: RootBeer Date: Mon, 20 Apr 2026 13:25:01 -0400 Subject: [PATCH 11/13] Fix compilation (this will probably be needed for upstream for newer imports) --- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 46175 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 13 ++++++------- gradlew.bat | 5 +++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ac9dd579..fffd5a9a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { java - id("com.gradleup.shadow") version("8.3.5") + id("com.gradleup.shadow") version("9.4.1") id("com.github.ben-manes.versions") version("0.51.0") } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..61285a659d17295f1de7c53e24fdf13ad755c379 100644 GIT binary patch delta 38026 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793..c61a118f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index b740cf13..adff685a 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -170,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -203,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 7101f8e4..e509b2dd 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -68,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell From 44a7cb5cc37fa536349bc4fa42cf8e0f2253d839 Mon Sep 17 00:00:00 2001 From: RootBeer Date: Mon, 20 Apr 2026 13:28:47 -0400 Subject: [PATCH 12/13] Finish rebase --- .../deluxemenus/action/ClickActionTask.java | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index 53574116..f26fe560 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -405,26 +405,31 @@ public void run() { float pitch = 1; if (!executable.contains(" ")) { - try { - sound = SoundUtils.getSound(executable.toUpperCase()); - } catch (final IllegalArgumentException exception) { - plugin.printStacktrace( - "Sound name given for sound action: " + executable + ", is not a valid sound!", - exception - ); - break; + if (!isRaw) { + try { + sound = SoundUtils.getSound(executable.toUpperCase()); + } catch (final IllegalArgumentException exception) { + plugin.printStacktrace( + "Sound name given for sound action: " + executable + ", is not a valid sound!", + exception + ); + break; + } } } else { String[] parts = executable.split(" ", 3); + soundName = parts[0]; - try { - sound = SoundUtils.getSound(parts[0].toUpperCase()); - } catch (final IllegalArgumentException exception) { - plugin.printStacktrace( - "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", - exception - ); - break; + if (!isRaw) { + try { + sound = SoundUtils.getSound(parts[0].toUpperCase()); + } catch (final IllegalArgumentException exception) { + plugin.printStacktrace( + "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", + exception + ); + break; + } } if (parts.length == 3) { @@ -478,18 +483,42 @@ public void run() { break; case BROADCAST_SOUND: + if (sound == null) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Sound name given for sound action: " + executable + ", is not a valid sound!" + ); + break; + } for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); } break; case BROADCAST_WORLD_SOUND: + if (sound == null) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Sound name given for sound action: " + executable + ", is not a valid sound!" + ); + break; + } for (final Player broadcastTarget : player.getWorld().getPlayers()) { broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); } break; case PLAY_SOUND: + if (sound == null) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Sound name given for sound action: " + executable + ", is not a valid sound!" + ); + break; + } player.playSound(player.getLocation(), sound, volume, pitch); break; } From e3d0ef39863e4815ab817fc6ed4d6fc526a038cb Mon Sep 17 00:00:00 2001 From: RootBeer Date: Mon, 20 Apr 2026 13:34:31 -0400 Subject: [PATCH 13/13] Simplify scheduling for need-only actions --- .../deluxemenus/action/ClickActionTask.java | 715 +++++++++--------- .../extendedclip/deluxemenus/menu/Menu.java | 2 +- 2 files changed, 357 insertions(+), 360 deletions(-) diff --git a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java index f26fe560..dfb3592d 100644 --- a/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java +++ b/src/main/java/com/extendedclip/deluxemenus/action/ClickActionTask.java @@ -64,470 +64,467 @@ public void run() { return; } - scheduler.runTask(player, () -> { - - final Optional holder = Menu.getMenuHolder(player); - final Player target = holder.isPresent() && holder.get().getPlaceholderPlayer() != null - ? holder.get().getPlaceholderPlayer() - : player; - - final String executable = StringUtils.replacePlaceholdersAndArguments( - this.exec, - this.arguments, - target, - this.parsePlaceholdersInArguments, - this.parsePlaceholdersAfterArguments); - - switch (actionType) { - case META: - if (!VersionHelper.IS_PDC_VERSION || plugin.getPersistentMetaHandler() == null) { - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Meta action not supported on this server version."); - break; - } - final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().parseAndExecuteMetaActionFromString(player, executable); - switch (result) { - case INVALID_SYNTAX: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Make sure you have the right syntax."); - break; - case NEW_VALUE_IS_DIFFERENT_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! New value is a different type than the old value!"); - break; - case INVALID_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! The specified type is not supported for the specified action!"); - break; - case EXISTENT_VALUE_IS_DIFFERENT_TYPE: - plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Existent value is a different type than the new value!"); - break; - case VALUE_NOT_FOUND: - case SUCCESS: - default: - break; - } - break; - - case PLAYER: - case PLAYER_COMMAND_EVENT: - player.chat("/" + executable); - break; - - case PLACEHOLDER: - holder.ifPresent(it -> it.setPlaceholders(executable)); - break; - - case CHAT: - player.chat(executable); - break; - - case CONSOLE: - scheduler.runTask(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable)); - break; - - case MINI_MESSAGE: - plugin.audiences().player(player).sendMessage(MiniMessage.miniMessage().deserialize(executable)); + final Optional holder = Menu.getMenuHolder(player); + final Player target = holder.isPresent() && holder.get().getPlaceholderPlayer() != null + ? holder.get().getPlaceholderPlayer() + : player; + + final String executable = StringUtils.replacePlaceholdersAndArguments( + this.exec, + this.arguments, + target, + this.parsePlaceholdersInArguments, + this.parsePlaceholdersAfterArguments); + + switch (actionType) { + case META: + if (!VersionHelper.IS_PDC_VERSION || plugin.getPersistentMetaHandler() == null) { + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Meta action not supported on this server version."); break; + } + final PersistentMetaHandler.OperationResult result = plugin.getPersistentMetaHandler().parseAndExecuteMetaActionFromString(player, executable); + switch (result) { + case INVALID_SYNTAX: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Make sure you have the right syntax."); + break; + case NEW_VALUE_IS_DIFFERENT_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! New value is a different type than the old value!"); + break; + case INVALID_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! The specified type is not supported for the specified action!"); + break; + case EXISTENT_VALUE_IS_DIFFERENT_TYPE: + plugin.debug(DebugLevel.HIGHEST, Level.INFO, "Invalid meta action! Existent value is a different type than the new value!"); + break; + case VALUE_NOT_FOUND: + case SUCCESS: + default: + break; + } + break; - case MINI_BROADCAST: - plugin.audiences().all().sendMessage(MiniMessage.miniMessage().deserialize(executable)); - break; + case PLAYER: + case PLAYER_COMMAND_EVENT: + player.chat("/" + executable); + break; - case MESSAGE: - player.sendMessage(StringUtils.color(executable)); - break; + case PLACEHOLDER: + holder.ifPresent(it -> it.setPlaceholders(executable)); + break; - case LOG: - final String[] logParts = executable.split(" ", 2); + case CHAT: + player.chat(executable); + break; - if (logParts.length == 0 || logParts[0].isBlank()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "LOG command requires at least a message"); - break; - } + case CONSOLE: + scheduler.runTask(() -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), executable)); + break; - Level logLevel; - String message; + case MINI_MESSAGE: + plugin.audiences().player(player).sendMessage(MiniMessage.miniMessage().deserialize(executable)); + break; - if (logParts.length == 1) { - logLevel = Level.INFO; - message = logParts[0]; - } else { - message = logParts[1]; + case MINI_BROADCAST: + plugin.audiences().all().sendMessage(MiniMessage.miniMessage().deserialize(executable)); + break; - try { - logLevel = Level.parse(logParts[0].toUpperCase()); - } catch (IllegalArgumentException e) { - logLevel = Level.INFO; - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Log level " + logParts[0] + " is not a valid log level! Using INFO instead."); - } - } + case MESSAGE: + player.sendMessage(StringUtils.color(executable)); + break; - plugin.getLogger().log(logLevel, String.format("[%s]: %s", holder.map(MenuHolder::getMenuName).orElse("Unknown Menu"), message)); - break; + case LOG: + final String[] logParts = executable.split(" ", 2); - case BROADCAST: - Bukkit.broadcastMessage(StringUtils.color(executable)); + if (logParts.length == 0 || logParts[0].isBlank()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "LOG command requires at least a message"); break; + } - case CLOSE: - Menu.closeMenu(plugin, player, true, true); - break; + Level logLevel; + String message; - case OPEN_GUI_MENU: - case OPEN_MENU: - final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); - final String[] executableParts = temporaryExecutable.split(" ", 2); + if (logParts.length == 1) { + logLevel = Level.INFO; + message = logParts[0]; + } else { + message = logParts[1]; - if (executableParts.length == 0) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); - break; + try { + logLevel = Level.parse(logParts[0].toUpperCase()); + } catch (IllegalArgumentException e) { + logLevel = Level.INFO; + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Log level " + logParts[0] + " is not a valid log level! Using INFO instead."); } + } - final String menuName = executableParts[0]; + plugin.getLogger().log(logLevel, String.format("[%s]: %s", holder.map(MenuHolder::getMenuName).orElse("Unknown Menu"), message)); + break; - final Optional optionalMenuToOpen = Menu.getMenuByName(menuName); + case BROADCAST: + Bukkit.broadcastMessage(StringUtils.color(executable)); + break; - if (optionalMenuToOpen.isEmpty()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); - break; - } + case CLOSE: + Menu.closeMenu(plugin, player, true, true); + break; - final Menu menuToOpen = optionalMenuToOpen.get(); + case OPEN_GUI_MENU: + case OPEN_MENU: + final String temporaryExecutable = executable.replaceAll("\\s+", " ").replace(" ", " "); + final String[] executableParts = temporaryExecutable.split(" ", 2); - final List menuArgumentNames = menuToOpen.options().arguments(); + if (executableParts.length == 0) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); + break; + } - String[] passedArgumentValues = null; - if (executableParts.length > 1) { - passedArgumentValues = executableParts[1].split(" "); - } + final String menuName = executableParts[0]; - if (menuArgumentNames.isEmpty()) { - if (passedArgumentValues != null && passedArgumentValues.length > 0) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" - ); - } + final Optional optionalMenuToOpen = Menu.getMenuByName(menuName); - if (holder.isEmpty()) { - menuToOpen.openMenu(player); - break; - } + if (optionalMenuToOpen.isEmpty()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Could not find and open menu " + executable); + break; + } - menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); - break; - } + final Menu menuToOpen = optionalMenuToOpen.get(); - if (passedArgumentValues == null || passedArgumentValues.length == 0) { - // Replicate old behavior: If no arguments are given, open the menu with the arguments from the current menu - if (holder.isEmpty()) { - menuToOpen.openMenu(player); - break; - } + final List menuArgumentNames = menuToOpen.options().arguments(); - menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); - break; - } + String[] passedArgumentValues = null; + if (executableParts.length > 1) { + passedArgumentValues = executableParts[1].split(" "); + } - if (passedArgumentValues.length < menuArgumentNames.size()) { + if (menuArgumentNames.isEmpty()) { + if (passedArgumentValues != null && passedArgumentValues.length > 0) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" + "Arguments were given for menu " + menuName + " in action [openguimenu] or [openmenu], but the menu does not support arguments!" ); - break; } - final Map argumentsMap = new HashMap<>(); - if (holder.isPresent() && holder.get().getTypedArgs() != null) { - // Pass the arguments from the current menu to the new menu. If the new menu has arguments with the - // same name, they will be overwritten - argumentsMap.putAll(holder.get().getTypedArgs()); + if (holder.isEmpty()) { + menuToOpen.openMenu(player); + break; } - for (int index = 0; index < menuArgumentNames.size(); index++) { - final String argumentName = menuArgumentNames.get(index); - - if (passedArgumentValues.length <= index) { - // This should never be the case! - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" - ); - break; - } - - if (menuArgumentNames.size() == index + 1) { - // If this is the last argument, get all remaining values and join them - final String lastArgumentValue = String.join(" ", Arrays.asList(passedArgumentValues).subList(index, passedArgumentValues.length)); - argumentsMap.put(argumentName, lastArgumentValue); - break; - } - - argumentsMap.put(argumentName, passedArgumentValues[index]); - } + menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); + break; + } + if (passedArgumentValues == null || passedArgumentValues.length == 0) { + // Replicate old behavior: If no arguments are given, open the menu with the arguments from the current menu if (holder.isEmpty()) { - menuToOpen.openMenu(player, argumentsMap, null); + menuToOpen.openMenu(player); break; } - menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer()); + menuToOpen.openMenu(player, holder.get().getTypedArgs(), holder.get().getPlaceholderPlayer()); break; - - case CONNECT: - plugin.connect(player, executable); + } + + if (passedArgumentValues.length < menuArgumentNames.size()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" + ); break; + } - case JSON_MESSAGE: - AdventureUtils.sendJson(plugin, player, executable); - break; + final Map argumentsMap = new HashMap<>(); + if (holder.isPresent() && holder.get().getTypedArgs() != null) { + // Pass the arguments from the current menu to the new menu. If the new menu has arguments with the + // same name, they will be overwritten + argumentsMap.putAll(holder.get().getTypedArgs()); + } - case JSON_BROADCAST: - case BROADCAST_JSON: - plugin.audiences().all().sendMessage(AdventureUtils.fromJson(executable)); - break; + for (int index = 0; index < menuArgumentNames.size(); index++) { + final String argumentName = menuArgumentNames.get(index); - case REFRESH: - if (holder.isEmpty()) { + if (passedArgumentValues.length <= index) { + // This should never be the case! plugin.debug( - DebugLevel.MEDIUM, + DebugLevel.HIGHEST, Level.WARNING, - player.getName() + " does not have menu open! Nothing to refresh!" + "Not enough arguments given for menu " + menuName + " when opening using the [openguimenu] or [openmenu] action!" ); break; } - holder.get().refreshMenu(); - break; - - case TAKE_MONEY: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot take money!"); + if (menuArgumentNames.size() == index + 1) { + // If this is the last argument, get all remaining values and join them + final String lastArgumentValue = String.join(" ", Arrays.asList(passedArgumentValues).subList(index, passedArgumentValues.length)); + argumentsMap.put(argumentName, lastArgumentValue); break; } - try { - plugin.getVault().takeMoney(player, Double.parseDouble(executable)); - } catch (final NumberFormatException exception) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Amount for take money action: " + executable + ", is not a valid number!" - ); - } + argumentsMap.put(argumentName, passedArgumentValues[index]); + } + + if (holder.isEmpty()) { + menuToOpen.openMenu(player, argumentsMap, null); + break; + } + + menuToOpen.openMenu(player, argumentsMap, holder.get().getPlaceholderPlayer()); + break; + + case CONNECT: + plugin.connect(player, executable); + break; + + case JSON_MESSAGE: + AdventureUtils.sendJson(plugin, player, executable); + break; + + case JSON_BROADCAST: + case BROADCAST_JSON: + plugin.audiences().all().sendMessage(AdventureUtils.fromJson(executable)); + break; + + case REFRESH: + if (holder.isEmpty()) { + plugin.debug( + DebugLevel.MEDIUM, + Level.WARNING, + player.getName() + " does not have menu open! Nothing to refresh!" + ); break; + } - case GIVE_MONEY: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot give money!"); + holder.get().refreshMenu(); + break; + + case TAKE_MONEY: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot take money!"); + break; + } + + try { + plugin.getVault().takeMoney(player, Double.parseDouble(executable)); + } catch (final NumberFormatException exception) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for take money action: " + executable + ", is not a valid number!" + ); + } + break; + + case GIVE_MONEY: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug(DebugLevel.HIGHEST, Level.WARNING, "Vault not hooked! Cannot give money!"); + break; + } + + try { + plugin.getVault().giveMoney(player, Double.parseDouble(executable)); + } catch (final NumberFormatException exception) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for give money action: " + executable + ", is not a valid number!" + ); + } + break; + + case TAKE_EXP: + case GIVE_EXP: + final String lowerCaseExecutable = executable.toLowerCase(); + + try { + if (Integer.parseInt(lowerCaseExecutable.replaceAll("l", "")) <= 0) break; + + if (actionType == ActionType.TAKE_EXP) { + ExpUtils.setExp(player, "-" + lowerCaseExecutable); break; } - try { - plugin.getVault().giveMoney(player, Double.parseDouble(executable)); - } catch (final NumberFormatException exception) { + ExpUtils.setExp(player, lowerCaseExecutable); + break; + + } catch (final NumberFormatException exception) { + if (actionType == ActionType.TAKE_EXP) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Amount for give money action: " + executable + ", is not a valid number!" + "Amount for take exp action: " + executable + ", is not a valid number!" ); + break; } - break; - case TAKE_EXP: - case GIVE_EXP: - final String lowerCaseExecutable = executable.toLowerCase(); + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Amount for give exp action: " + executable + ", is not a valid number!" + ); + break; + } + + case GIVE_PERM: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Vault not hooked! Cannot give permission: " + executable + "!"); + break; + } - try { - if (Integer.parseInt(lowerCaseExecutable.replaceAll("l", "")) <= 0) break; + plugin.getVault().givePermission(player, executable); + break; - if (actionType == ActionType.TAKE_EXP) { - ExpUtils.setExp(player, "-" + lowerCaseExecutable); + case TAKE_PERM: + if (plugin.getVault() == null || !plugin.getVault().hooked()) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Vault not hooked! Cannot take permission: " + executable + "!"); + break; + } + + plugin.getVault().takePermission(player, executable); + break; + + case BROADCAST_SOUND: + case BROADCAST_RAW_SOUND: + case BROADCAST_WORLD_SOUND: + case BROADCAST_WORLD_RAW_SOUND: + case PLAY_RAW_SOUND: + case PLAY_SOUND: + boolean isRaw = isRaw(actionType); + + Sound sound = null; + String soundName = executable; + float volume = 1; + float pitch = 1; + + if (!executable.contains(" ")) { + if (!isRaw) { + try { + sound = SoundUtils.getSound(executable.toUpperCase()); + } catch (final IllegalArgumentException exception) { + plugin.printStacktrace( + "Sound name given for sound action: " + executable + ", is not a valid sound!", + exception + ); break; } + } + } else { + String[] parts = executable.split(" ", 3); + soundName = parts[0]; - ExpUtils.setExp(player, lowerCaseExecutable); - break; + if (!isRaw) { + try { + sound = SoundUtils.getSound(parts[0].toUpperCase()); + } catch (final IllegalArgumentException exception) { + plugin.printStacktrace( + "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", + exception + ); + break; + } + } - } catch (final NumberFormatException exception) { - if (actionType == ActionType.TAKE_EXP) { + if (parts.length == 3) { + try { + pitch = Float.parseFloat(parts[2]); + } catch (final NumberFormatException exception) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Amount for take exp action: " + executable + ", is not a valid number!" + "Pitch given for sound action: " + parts[2] + ", is not a valid number!" + ); + + plugin.printStacktrace( + "Pitch given for sound action: " + parts[2] + ", is not a valid number!", + exception ); - break; } + } + try { + volume = Float.parseFloat(parts[1]); + } catch (final NumberFormatException exception) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Amount for give exp action: " + executable + ", is not a valid number!" + "Volume given for sound action: " + parts[1] + ", is not a valid number!" ); - break; - } - case GIVE_PERM: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Vault not hooked! Cannot give permission: " + executable + "!"); - break; + plugin.printStacktrace( + "Volume given for sound action: " + parts[1] + ", is not a valid number!", + exception + ); } + } - plugin.getVault().givePermission(player, executable); - break; - - case TAKE_PERM: - if (plugin.getVault() == null || !plugin.getVault().hooked()) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Vault not hooked! Cannot take permission: " + executable + "!"); + switch (actionType) { + case BROADCAST_WORLD_RAW_SOUND: + for (final Player broadcastTarget : player.getWorld().getPlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), soundName, volume, pitch); + } break; - } - - plugin.getVault().takePermission(player, executable); - break; - case BROADCAST_SOUND: - case BROADCAST_RAW_SOUND: - case BROADCAST_WORLD_SOUND: - case BROADCAST_WORLD_RAW_SOUND: - case PLAY_RAW_SOUND: - case PLAY_SOUND: - boolean isRaw = isRaw(actionType); - - Sound sound = null; - String soundName = executable; - float volume = 1; - float pitch = 1; - - if (!executable.contains(" ")) { - if (!isRaw) { - try { - sound = SoundUtils.getSound(executable.toUpperCase()); - } catch (final IllegalArgumentException exception) { - plugin.printStacktrace( - "Sound name given for sound action: " + executable + ", is not a valid sound!", - exception - ); - break; - } + case BROADCAST_RAW_SOUND: + for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), soundName, volume, pitch); } - } else { - String[] parts = executable.split(" ", 3); - soundName = parts[0]; - - if (!isRaw) { - try { - sound = SoundUtils.getSound(parts[0].toUpperCase()); - } catch (final IllegalArgumentException exception) { - plugin.printStacktrace( - "Sound name given for sound action: " + parts[0] + ", is not a valid sound!", - exception - ); - break; - } - } - - if (parts.length == 3) { - try { - pitch = Float.parseFloat(parts[2]); - } catch (final NumberFormatException exception) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Pitch given for sound action: " + parts[2] + ", is not a valid number!" - ); + break; - plugin.printStacktrace( - "Pitch given for sound action: " + parts[2] + ", is not a valid number!", - exception - ); - } - } + case PLAY_RAW_SOUND: + player.playSound(player.getLocation(), soundName, volume, pitch); + break; - try { - volume = Float.parseFloat(parts[1]); - } catch (final NumberFormatException exception) { + case BROADCAST_SOUND: + if (sound == null) { plugin.debug( DebugLevel.HIGHEST, Level.WARNING, - "Volume given for sound action: " + parts[1] + ", is not a valid number!" - ); - - plugin.printStacktrace( - "Volume given for sound action: " + parts[1] + ", is not a valid number!", - exception + "Sound name given for sound action: " + executable + ", is not a valid sound!" ); - } - } - - switch (actionType) { - case BROADCAST_WORLD_RAW_SOUND: - for (final Player broadcastTarget : player.getWorld().getPlayers()) { - broadcastTarget.playSound(broadcastTarget.getLocation(), soundName, volume, pitch); - } - break; - - case BROADCAST_RAW_SOUND: - for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { - broadcastTarget.playSound(broadcastTarget.getLocation(), soundName, volume, pitch); - } - break; - - case PLAY_RAW_SOUND: - player.playSound(player.getLocation(), soundName, volume, pitch); - break; - - case BROADCAST_SOUND: - if (sound == null) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Sound name given for sound action: " + executable + ", is not a valid sound!" - ); - break; - } - for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { - broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); - } break; + } + for (final Player broadcastTarget : Bukkit.getOnlinePlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); + } + break; - case BROADCAST_WORLD_SOUND: - if (sound == null) { - plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Sound name given for sound action: " + executable + ", is not a valid sound!" - ); - break; - } - for (final Player broadcastTarget : player.getWorld().getPlayers()) { - broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); - } + case BROADCAST_WORLD_SOUND: + if (sound == null) { + plugin.debug( + DebugLevel.HIGHEST, + Level.WARNING, + "Sound name given for sound action: " + executable + ", is not a valid sound!" + ); break; + } + for (final Player broadcastTarget : player.getWorld().getPlayers()) { + broadcastTarget.playSound(broadcastTarget.getLocation(), sound, volume, pitch); + } + break; - case PLAY_SOUND: + case PLAY_SOUND: if (sound == null) { plugin.debug( - DebugLevel.HIGHEST, - Level.WARNING, - "Sound name given for sound action: " + executable + ", is not a valid sound!" + DebugLevel.HIGHEST, + Level.WARNING, + "Sound name given for sound action: " + executable + ", is not a valid sound!" ); break; } player.playSound(player.getLocation(), sound, volume, pitch); break; - } - break; + } + break; - default: - break; - } - }); + default: + break; + } } private boolean isRaw(ActionType actionType) { diff --git a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java index 2b23ba2c..5056aecc 100644 --- a/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java +++ b/src/main/java/com/extendedclip/deluxemenus/menu/Menu.java @@ -58,7 +58,7 @@ public Menu( if (this.options.registerCommands()) { this.command = new RegistrableMenuCommand(plugin, this); - scheduler.runTask(() -> this.command.register()); + this.command.register(); } menus.put(this.options.name(), this);