From b6ec088b7c10f7fb34cd828708dcbd661f4ae596 Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:58:11 +0100 Subject: [PATCH 01/27] removed: todo markdown file --- todo.md | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 todo.md diff --git a/todo.md b/todo.md deleted file mode 100644 index 1b9dc50..0000000 --- a/todo.md +++ /dev/null @@ -1,10 +0,0 @@ -## To do -- [x] Add Folia Support -- [x] Add BungeeCord Support -- [x] Add Velocity Support -- [x] Add a proper wiki instead of dropping everything on here -- [x] Adding a configurable no permission message (just using spigot's.) -- [x] Adding a custom tab complete system -- [ ] Adding the "hidden" setting for Velocity and BungeeCord -- [x] Testing the new flags feature -- [ ] Testing new Bungeecord node \ No newline at end of file From 44d267d74d9dbea139b899259e34582218977fc3 Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:58:27 +0100 Subject: [PATCH 02/27] docs: fixed indentation --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 998d738..913c0ec 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # MC Command Framework - ### DISCLAIMER: I did not make nor helped in the initial development of this project. All the credit goes to [ashtton](https://github.com/ashtton) and his [original project](https://github.com/ashtton/spigot-command-api). What I'll actually do is maintain this plugin updated and according to my needs, while keeping it public, as far as providing builds on my Maven repository (check below). From b0227d9afc3729e2e211c85a9d42893ef81f2b33 Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:59:03 +0100 Subject: [PATCH 03/27] feat: added customizable error messages for bukkit --- .../command/bukkit/BukkitCommandHandler.java | 10 ++++++++++ .../marioogg/command/bukkit/node/CommandNode.java | 15 ++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java b/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java index 599880c..7eff2c5 100644 --- a/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java +++ b/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java @@ -2,6 +2,7 @@ import com.google.common.reflect.ClassPath; import lombok.Getter; +import lombok.Setter; import lombok.SneakyThrows; import me.marioogg.command.Command; import me.marioogg.command.Subcommand; @@ -10,6 +11,7 @@ import me.marioogg.command.bukkit.node.CommandNode; import me.marioogg.command.bukkit.parameter.ParamProcessor; import me.marioogg.command.bukkit.parameter.Processor; +import org.bukkit.ChatColor; import org.bukkit.plugin.Plugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,6 +28,13 @@ public class BukkitCommandHandler { @Getter private static Logger logger; + @Setter + @Getter private static String noPermissionMessage = ChatColor.RED + "I'm sorry, but you do not have permission to perform this command."; + @Getter private static String playerOnlyMessage = ChatColor.RED + "You must be a player to execute this command."; + @Getter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; + @Getter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; + + public static void setPlugin(Plugin plugin) { BukkitCommandHandler.plugin = plugin; logger = LoggerFactory.getLogger(plugin.getName()); @@ -145,4 +154,5 @@ private static boolean isInstantiable(Class clazz) { && Arrays.stream(clazz.getDeclaredConstructors()) .anyMatch(c -> c.getParameterCount() == 0); } + } diff --git a/src/main/java/me/marioogg/command/bukkit/node/CommandNode.java b/src/main/java/me/marioogg/command/bukkit/node/CommandNode.java index ade57df..d9a2d9b 100644 --- a/src/main/java/me/marioogg/command/bukkit/node/CommandNode.java +++ b/src/main/java/me/marioogg/command/bukkit/node/CommandNode.java @@ -10,6 +10,7 @@ import me.marioogg.command.bukkit.parameter.Param; import me.marioogg.command.bukkit.parameter.ParamProcessor; import me.marioogg.command.bukkit.scheduler.SchedulerUtil; +import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; @@ -152,12 +153,12 @@ public int getMatchProbability(CommandSender sender, String label, String[] args public void sendUsageMessage(CommandSender sender) { if(consoleOnly && sender instanceof Player) { - sender.sendMessage(ChatColor.RED + "This command can only be executed by console."); + sender.sendMessage(BukkitCommandHandler.getConsoleOnlyMessage()); return; } if(playerOnly && sender instanceof ConsoleCommandSender) { - sender.sendMessage(ChatColor.RED + "You must be a player to execute this command."); + sender.sendMessage(BukkitCommandHandler.getPlayerOnlyMessage()); return; } @@ -167,7 +168,7 @@ public void sendUsageMessage(CommandSender sender) { } if(!permission.isEmpty() && !sender.hasPermission(permission)) { - sender.sendMessage(ChatColor.RED + "I'm sorry, you do not have permission to execute this command."); + sender.sendMessage(BukkitCommandHandler.getNoPermissionMessage()); return; } @@ -194,17 +195,17 @@ public int requiredArgumentsLength() { public void execute(CommandSender sender, String[] args) { if(!permission.isEmpty() && !sender.hasPermission(permission)) { - sender.sendMessage(ChatColor.RED + "I'm sorry, but you do not have permission to perform this command."); + sender.sendMessage(ChatColor.RED + BukkitCommandHandler.getNoPermissionMessage()); return; } if(sender instanceof ConsoleCommandSender && playerOnly) { - sender.sendMessage(ChatColor.RED + "You must be a player to execute this command."); + sender.sendMessage(ChatColor.RED + BukkitCommandHandler.getPlayerOnlyMessage()); return; } if(sender instanceof Player && consoleOnly) { - sender.sendMessage(ChatColor.RED + "This command is only executable by console."); + sender.sendMessage(ChatColor.RED + BukkitCommandHandler.getConsoleOnlyMessage()); return; } @@ -286,7 +287,7 @@ private void invokeMethod(CommandSender sender, List params) { } catch (IllegalAccessException | InvocationTargetException e) { Throwable cause = (e instanceof InvocationTargetException) ? e.getCause() : e; log.error("An exception occurred while executing command '{}' (Sender: {})", names.get(0), sender.getName(), cause); - sender.sendMessage(ChatColor.RED + "An internal error occurred while executing this command."); + sender.sendMessage(ChatColor.RED + BukkitCommandHandler.getInternalErrorMessage()); } } } \ No newline at end of file From dee5f0947162266c37595f03f85ab6e7b95015af Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:06:40 +0100 Subject: [PATCH 04/27] fix: added setters for the Bukkit error messages --- .../marioogg/command/bukkit/BukkitCommandHandler.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java b/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java index 7eff2c5..5e1c1ca 100644 --- a/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java +++ b/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java @@ -28,11 +28,11 @@ public class BukkitCommandHandler { @Getter private static Logger logger; - @Setter - @Getter private static String noPermissionMessage = ChatColor.RED + "I'm sorry, but you do not have permission to perform this command."; - @Getter private static String playerOnlyMessage = ChatColor.RED + "You must be a player to execute this command."; - @Getter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; - @Getter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; + + @Setter @Getter private static String noPermissionMessage = ChatColor.RED + "I'm sorry, but you do not have permission to perform this command."; + @Setter @Getter private static String playerOnlyMessage = ChatColor.RED + "You must be a player to execute this command."; + @Setter @Getter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; + @Setter @Getter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; public static void setPlugin(Plugin plugin) { From ad817d5d9ff9a14dc025b901c21c2bb40755f092 Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:07:04 +0100 Subject: [PATCH 05/27] feat: added customizable error messages for bungee --- .../command/bungee/BungeeCommandHandler.java | 6 ++++++ .../command/bungee/node/BungeeCommandNode.java | 18 +++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java index 8d1f354..a168aed 100644 --- a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java +++ b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java @@ -12,6 +12,7 @@ import me.marioogg.command.common.help.Help; import me.marioogg.command.common.help.HelpNode; import net.md_5.bungee.api.plugin.Plugin; +import org.bukkit.ChatColor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +26,11 @@ public class BungeeCommandHandler { @Getter private static final Logger logger = LoggerFactory.getLogger(plugin.getDescription().getName()); + @Setter @Getter private static String noPermissionMessage = ChatColor.RED + "I'm sorry, but you do not have permission to perform this command."; + @Setter @Getter private static String playerOnlyMessage = ChatColor.RED + "You must be a player to execute this command."; + @Setter @Getter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; + @Setter @Getter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; + @SneakyThrows public static void registerCommands(String path, Plugin plugin) { ClassPath.from(plugin.getClass().getClassLoader()).getAllClasses().stream() diff --git a/src/main/java/me/marioogg/command/bungee/node/BungeeCommandNode.java b/src/main/java/me/marioogg/command/bungee/node/BungeeCommandNode.java index 95665d4..da5e6fe 100644 --- a/src/main/java/me/marioogg/command/bungee/node/BungeeCommandNode.java +++ b/src/main/java/me/marioogg/command/bungee/node/BungeeCommandNode.java @@ -146,17 +146,17 @@ public int getMatchProbability(CommandSender sender, String label, String[] args public void sendUsageMessage(CommandSender sender) { if (consoleOnly && sender instanceof ProxiedPlayer) { - sender.sendMessage(new TextComponent(ChatColor.RED + "This command can only be executed by console.")); + sender.sendMessage(new TextComponent(BungeeCommandHandler.getConsoleOnlyMessage())); return; } if (playerOnly && !(sender instanceof ProxiedPlayer)) { - sender.sendMessage(new TextComponent(ChatColor.RED + "You must be a player to execute this command.")); + sender.sendMessage(new TextComponent(BungeeCommandHandler.getPlayerOnlyMessage())); return; } if (!permission.isEmpty() && !sender.hasPermission(permission)) { - sender.sendMessage(new TextComponent(ChatColor.RED + "I'm sorry, you do not have permission to execute this command.")); + sender.sendMessage(new TextComponent(BungeeCommandHandler.getNoPermissionMessage())); return; } @@ -184,23 +184,22 @@ public int requiredArgumentsLength() { @SneakyThrows public void execute(CommandSender sender, String[] args) { if (!permission.isEmpty() && !sender.hasPermission(permission)) { - sender.sendMessage(new TextComponent(ChatColor.RED + "I'm sorry, although you do not have permission to execute this command.")); + sender.sendMessage(new TextComponent(BungeeCommandHandler.getNoPermissionMessage())); return; } if (!(sender instanceof ProxiedPlayer) && playerOnly) { - sender.sendMessage(new TextComponent(ChatColor.RED + "You must be a player to execute this command.")); + sender.sendMessage(new TextComponent(BungeeCommandHandler.getPlayerOnlyMessage())); return; } if (sender instanceof ProxiedPlayer && consoleOnly) { - sender.sendMessage(new TextComponent(ChatColor.RED + "This command is only executable by console.")); + sender.sendMessage(new TextComponent(BungeeCommandHandler.getConsoleOnlyMessage())); return; } int nameArgs = (names.get(0).split(" ").length - 1); - // Separate flag tokens from positional args Set activatedFlags = new HashSet<>(); List positionalArgs = new ArrayList<>(); for (int i = nameArgs; i < args.length; i++) { @@ -218,7 +217,6 @@ public void execute(CommandSender sender, String[] args) { return; } - // Build positional objects List positionalObjects = new ArrayList<>(); for (int i = 0; i < positionalArgs.size(); i++) { if (parameters.size() < i + 1) break; @@ -238,7 +236,6 @@ public void execute(CommandSender sender, String[] args) { positionalObjects.add(object); } - // Fill in missing optional positional args for (int i = positionalObjects.size(); i < parameters.size(); i++) { ArgumentNode argumentNode = parameters.get(i); if (argumentNode.getDefaultValue() == null) { @@ -248,7 +245,6 @@ public void execute(CommandSender sender, String[] args) { } } - // Build final invocation list in method parameter declaration order List objects = new ArrayList<>(); int positionalIndex = 0; for (java.lang.reflect.Parameter mp : method.getParameters()) { @@ -276,7 +272,7 @@ public void execute(CommandSender sender, String[] args) { Throwable cause = (e instanceof InvocationTargetException) ? e.getCause() : e; log.error("An exception occurred while executing command '{}' (Sender: {})", names.get(0), sender.getName(), cause); sender.sendMessage(new ComponentBuilder( - "An internal error occurred while executing this command.") + BungeeCommandHandler.getInternalErrorMessage()) .color(ChatColor.RED) .bold(true) .create()); From ecd502cc46b1f5bcc9978c9330ec32d4b6e66f3a Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:11:40 +0100 Subject: [PATCH 06/27] fix: changed the import from CraftBukkit to BungeeCord chatcolor --- .../java/me/marioogg/command/bungee/BungeeCommandHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java index a168aed..4994ec9 100644 --- a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java +++ b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java @@ -11,8 +11,8 @@ import me.marioogg.command.Subcommand; import me.marioogg.command.common.help.Help; import me.marioogg.command.common.help.HelpNode; +import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.plugin.Plugin; -import org.bukkit.ChatColor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From 96cae0bd369709cb4629c858598c512b622aad50 Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:14:59 +0100 Subject: [PATCH 07/27] feat: added customizable error messages for velocity --- .../command/velocity/VelocityCommandHandler.java | 7 +++++++ .../command/velocity/command/VelocityRawCommand.java | 4 ++-- .../command/velocity/node/VelocityCommandNode.java | 12 ++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java b/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java index cd59dc2..0aab83c 100644 --- a/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java +++ b/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java @@ -12,6 +12,8 @@ import me.marioogg.command.velocity.node.VelocityCommandNode; import me.marioogg.command.velocity.parameter.VelocityParamProcessor; import me.marioogg.command.velocity.parameter.VelocityProcessor; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,6 +29,11 @@ public class VelocityCommandHandler { @Getter @Setter private static ProxyServer proxy; private static Logger logger; + + @Setter @Getter private static Component noPermissionMessage = Component.text("I'm sorry, but you do not have permission to perform this command.", NamedTextColor.RED); + @Setter @Getter private static Component playerOnlyMessage = Component.text("You must be a player to execute this command.", NamedTextColor.RED); + @Setter @Getter private static Component consoleOnlyMessage = Component.text("This command can only be executed by console.", NamedTextColor.RED); + @Setter @Getter private static Component internalErrorMessage = Component.text("An internal error occurred while executing this command.", NamedTextColor.RED); public static void init(Object plugin, ProxyServer proxy) { VelocityCommandHandler.plugin = plugin; diff --git a/src/main/java/me/marioogg/command/velocity/command/VelocityRawCommand.java b/src/main/java/me/marioogg/command/velocity/command/VelocityRawCommand.java index b6c54e7..fbb2550 100644 --- a/src/main/java/me/marioogg/command/velocity/command/VelocityRawCommand.java +++ b/src/main/java/me/marioogg/command/velocity/command/VelocityRawCommand.java @@ -42,7 +42,7 @@ public void execute(Invocation invocation) { List sortedNodes = VelocityCommandNode.getNodes().stream() .sorted(Comparator.comparingInt(node -> node.getMatchProbability(source, root, args, false))) - .collect(Collectors.toList()); + .toList(); VelocityCommandNode node = sortedNodes.get(sortedNodes.size() - 1); @@ -55,7 +55,7 @@ public void execute(Invocation invocation) { HelpNode helpNode = node.getHelpNodes().get(0); if (!helpNode.getPermission().isEmpty() && !source.hasPermission(helpNode.getPermission())) { - source.sendMessage(Component.text("I'm sorry, although you do not have permission to execute this command.", NamedTextColor.RED)); + source.sendMessage(VelocityCommandHandler.getNoPermissionMessage()); return; } diff --git a/src/main/java/me/marioogg/command/velocity/node/VelocityCommandNode.java b/src/main/java/me/marioogg/command/velocity/node/VelocityCommandNode.java index afc1841..fb17e1f 100644 --- a/src/main/java/me/marioogg/command/velocity/node/VelocityCommandNode.java +++ b/src/main/java/me/marioogg/command/velocity/node/VelocityCommandNode.java @@ -141,17 +141,17 @@ public int getMatchProbability(CommandSource source, String label, String[] args public void sendUsageMessage(CommandSource source) { if (consoleOnly && source instanceof Player) { - source.sendMessage(Component.text("This command can only be executed by console.", NamedTextColor.RED)); + source.sendMessage(VelocityCommandHandler.getConsoleOnlyMessage()); return; } if (playerOnly && !(source instanceof Player)) { - source.sendMessage(Component.text("You must be a player to execute this command.", NamedTextColor.RED)); + source.sendMessage(VelocityCommandHandler.getPlayerOnlyMessage()); return; } if (!permission.isEmpty() && !source.hasPermission(permission)) { - source.sendMessage(Component.text("I'm sorry, you do not have permission to execute this command.", NamedTextColor.RED)); + source.sendMessage(VelocityCommandHandler.getNoPermissionMessage()); return; } @@ -179,17 +179,17 @@ public int requiredArgumentsLength() { @SneakyThrows public void execute(CommandSource source, String[] args) { if (!permission.isEmpty() && !source.hasPermission(permission)) { - source.sendMessage(Component.text("I'm sorry, although you do not have permission to execute this command.", NamedTextColor.RED)); + source.sendMessage(VelocityCommandHandler.getNoPermissionMessage()); return; } if (!(source instanceof Player) && playerOnly) { - source.sendMessage(Component.text("You must be a player to execute this command.", NamedTextColor.RED)); + source.sendMessage(VelocityCommandHandler.getPlayerOnlyMessage()); return; } if (source instanceof Player && consoleOnly) { - source.sendMessage(Component.text("This command is only executable by console.", NamedTextColor.RED)); + source.sendMessage(VelocityCommandHandler.getConsoleOnlyMessage()); return; } From 78adcf4b37c630a65beaafdd3f824c973e65afd8 Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:15:57 +0100 Subject: [PATCH 08/27] fix: fixed hardcoded no permission messages in bungee and bukkit --- src/main/java/me/marioogg/command/bukkit/BukkitCommand.java | 2 +- src/main/java/me/marioogg/command/bungee/BungeeCommand.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/me/marioogg/command/bukkit/BukkitCommand.java b/src/main/java/me/marioogg/command/bukkit/BukkitCommand.java index da3ac97..825cce9 100644 --- a/src/main/java/me/marioogg/command/bukkit/BukkitCommand.java +++ b/src/main/java/me/marioogg/command/bukkit/BukkitCommand.java @@ -51,7 +51,7 @@ public boolean execute(CommandSender sender, String label, String[] args) { HelpNode helpNode = node.getHelpNodes().get(0); if(!helpNode.getPermission().isEmpty() && !sender.hasPermission(helpNode.getPermission())) { - sender.sendMessage(ChatColor.RED + "I'm sorry, although you do not have permission to execute this command."); + sender.sendMessage(BukkitCommandHandler.getNoPermissionMessage()); return false; } diff --git a/src/main/java/me/marioogg/command/bungee/BungeeCommand.java b/src/main/java/me/marioogg/command/bungee/BungeeCommand.java index 6a793c9..695f3eb 100644 --- a/src/main/java/me/marioogg/command/bungee/BungeeCommand.java +++ b/src/main/java/me/marioogg/command/bungee/BungeeCommand.java @@ -47,7 +47,7 @@ public void execute(CommandSender sender, String[] args) { HelpNode helpNode = node.getHelpNodes().get(0); if (!helpNode.getPermission().isEmpty() && !sender.hasPermission(helpNode.getPermission())) { - sender.sendMessage(new TextComponent(ChatColor.RED + "I'm sorry, although you do not have permission to execute this command.")); + sender.sendMessage(new TextComponent(BungeeCommandHandler.getNoPermissionMessage())); return; } From bebeb70fdbaa9e7500f1568b0d0b210429f151df Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:16:53 +0100 Subject: [PATCH 09/27] feat: added cooldown system --- .../command/common/cooldown/Cooldown.java | 13 ++++++ .../common/cooldown/CooldownManager.java | 46 +++++++++++++++++++ .../command/common/cooldown/CooldownNode.java | 12 +++++ 3 files changed, 71 insertions(+) create mode 100644 src/main/java/me/marioogg/command/common/cooldown/Cooldown.java create mode 100644 src/main/java/me/marioogg/command/common/cooldown/CooldownManager.java create mode 100644 src/main/java/me/marioogg/command/common/cooldown/CooldownNode.java diff --git a/src/main/java/me/marioogg/command/common/cooldown/Cooldown.java b/src/main/java/me/marioogg/command/common/cooldown/Cooldown.java new file mode 100644 index 0000000..fb2c28d --- /dev/null +++ b/src/main/java/me/marioogg/command/common/cooldown/Cooldown.java @@ -0,0 +1,13 @@ +package me.marioogg.command.common.cooldown; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Cooldown { + int seconds(); + String bypassPermission() default ""; +} \ No newline at end of file diff --git a/src/main/java/me/marioogg/command/common/cooldown/CooldownManager.java b/src/main/java/me/marioogg/command/common/cooldown/CooldownManager.java new file mode 100644 index 0000000..51b6df9 --- /dev/null +++ b/src/main/java/me/marioogg/command/common/cooldown/CooldownManager.java @@ -0,0 +1,46 @@ +package me.marioogg.command.common.cooldown; + +import java.util.HashMap; +import java.util.UUID; + +/** + * Manages per-player, per-command cooldown state. + */ +public class CooldownManager { + private static final HashMap> cooldowns = new HashMap<>(); + + /** + * Returns true if the player is still on cooldown for the given command. + */ + public static boolean isOnCooldown(UUID uuid, String commandName) { + HashMap commandCooldowns = cooldowns.get(commandName); + if (commandCooldowns == null) return false; + + Long expiry = commandCooldowns.get(uuid); + if (expiry == null) return false; + + return System.currentTimeMillis() < expiry; + } + + /** + * Registers a cooldown for the player on the given command. + */ + public static void setCooldown(UUID uuid, String commandName, int seconds) { + cooldowns + .computeIfAbsent(commandName, k -> new HashMap<>()) + .put(uuid, System.currentTimeMillis() + (seconds * 1000L)); + } + + /** + * Returns the remaining cooldown in seconds, or 0 if none. + */ + public static long getRemainingSeconds(UUID uuid, String commandName) { + HashMap commandCooldowns = cooldowns.get(commandName); + if (commandCooldowns == null) return 0; + + Long expiry = commandCooldowns.get(uuid); + if (expiry == null) return 0; + + return Math.max(0, (expiry - System.currentTimeMillis()) / 1000); + } +} \ No newline at end of file diff --git a/src/main/java/me/marioogg/command/common/cooldown/CooldownNode.java b/src/main/java/me/marioogg/command/common/cooldown/CooldownNode.java new file mode 100644 index 0000000..1b26fe1 --- /dev/null +++ b/src/main/java/me/marioogg/command/common/cooldown/CooldownNode.java @@ -0,0 +1,12 @@ +package me.marioogg.command.common.cooldown; + +import lombok.Data; + +/** + * Stores cooldown metadata read from a {@link Cooldown} annotation. + */ +@Data +public class CooldownNode { + private final int seconds; + private final String bypassPermission; +} From 1a44e20f22e191e96e51a50b0840d35e2e1da2b2 Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:17:11 +0100 Subject: [PATCH 10/27] feat: implemented cooldown into Bukkit --- .../command/bukkit/BukkitCommandHandler.java | 14 ++++------ .../command/bukkit/node/CommandNode.java | 28 +++++++++++++++---- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java b/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java index 5e1c1ca..1ed06fd 100644 --- a/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java +++ b/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java @@ -28,12 +28,11 @@ public class BukkitCommandHandler { @Getter private static Logger logger; - - @Setter @Getter private static String noPermissionMessage = ChatColor.RED + "I'm sorry, but you do not have permission to perform this command."; - @Setter @Getter private static String playerOnlyMessage = ChatColor.RED + "You must be a player to execute this command."; - @Setter @Getter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; - @Setter @Getter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; - + @Getter @Setter private static String noPermissionMessage = ChatColor.RED + "I'm sorry, but you do not have permission to perform this command."; + @Getter @Setter private static String playerOnlyMessage = ChatColor.RED + "You must be a player to execute this command."; + @Getter @Setter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; + @Getter @Setter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; + @Getter @Setter private static String cooldownMessage = ChatColor.RED + "You must wait {seconds} more second(s) before using this command again."; public static void setPlugin(Plugin plugin) { BukkitCommandHandler.plugin = plugin; @@ -154,5 +153,4 @@ private static boolean isInstantiable(Class clazz) { && Arrays.stream(clazz.getDeclaredConstructors()) .anyMatch(c -> c.getParameterCount() == 0); } - -} +} \ No newline at end of file diff --git a/src/main/java/me/marioogg/command/bukkit/node/CommandNode.java b/src/main/java/me/marioogg/command/bukkit/node/CommandNode.java index d9a2d9b..7866760 100644 --- a/src/main/java/me/marioogg/command/bukkit/node/CommandNode.java +++ b/src/main/java/me/marioogg/command/bukkit/node/CommandNode.java @@ -4,13 +4,15 @@ import me.marioogg.command.Command; import me.marioogg.command.bukkit.BukkitCommandHandler; import me.marioogg.command.bukkit.BukkitCommand; +import me.marioogg.command.common.cooldown.Cooldown; +import me.marioogg.command.common.cooldown.CooldownManager; +import me.marioogg.command.common.cooldown.CooldownNode; import me.marioogg.command.common.flag.Flag; import me.marioogg.command.common.flag.FlagNode; import me.marioogg.command.common.help.HelpNode; import me.marioogg.command.bukkit.parameter.Param; import me.marioogg.command.bukkit.parameter.ParamProcessor; import me.marioogg.command.bukkit.scheduler.SchedulerUtil; -import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; @@ -44,6 +46,8 @@ public class CommandNode { private final Object parentClass; private final Method method; + private final CooldownNode cooldownNode; + private final List parameters = new ArrayList<>(); private final List flagNodes = new ArrayList<>(); private final List helpNodes = new ArrayList<>(); @@ -77,6 +81,9 @@ public CommandNode(Object parentClass, Method method, Command command) { flagNodes.add(new FlagNode(flag, parameter)); }); + Cooldown cooldown = method.getAnnotation(Cooldown.class); + this.cooldownNode = cooldown != null ? new CooldownNode(cooldown.seconds(), cooldown.bypassPermission()) : null; + names.forEach(name -> { if(!BukkitCommand.getCommands().containsKey(name.split(" ")[0].toLowerCase())) new BukkitCommand(name.split(" ")[0].toLowerCase()); }); @@ -195,20 +202,31 @@ public int requiredArgumentsLength() { public void execute(CommandSender sender, String[] args) { if(!permission.isEmpty() && !sender.hasPermission(permission)) { - sender.sendMessage(ChatColor.RED + BukkitCommandHandler.getNoPermissionMessage()); + sender.sendMessage(BukkitCommandHandler.getNoPermissionMessage()); return; } if(sender instanceof ConsoleCommandSender && playerOnly) { - sender.sendMessage(ChatColor.RED + BukkitCommandHandler.getPlayerOnlyMessage()); + sender.sendMessage(BukkitCommandHandler.getPlayerOnlyMessage()); return; } if(sender instanceof Player && consoleOnly) { - sender.sendMessage(ChatColor.RED + BukkitCommandHandler.getConsoleOnlyMessage()); + sender.sendMessage(BukkitCommandHandler.getConsoleOnlyMessage()); return; } + if (cooldownNode != null && sender instanceof Player player) { + if (cooldownNode.getBypassPermission().isEmpty() || !player.hasPermission(cooldownNode.getBypassPermission())) { + if (CooldownManager.isOnCooldown(player.getUniqueId(), names.get(0))) { + long remaining = CooldownManager.getRemainingSeconds(player.getUniqueId(), names.get(0)); + sender.sendMessage(BukkitCommandHandler.getCooldownMessage().replace("{seconds}", String.valueOf(remaining))); + return; + } + CooldownManager.setCooldown(player.getUniqueId(), names.get(0), cooldownNode.getSeconds()); + } + } + int nameArgs = (names.get(0).split(" ").length - 1); Set activatedFlags = new HashSet<>(); @@ -287,7 +305,7 @@ private void invokeMethod(CommandSender sender, List params) { } catch (IllegalAccessException | InvocationTargetException e) { Throwable cause = (e instanceof InvocationTargetException) ? e.getCause() : e; log.error("An exception occurred while executing command '{}' (Sender: {})", names.get(0), sender.getName(), cause); - sender.sendMessage(ChatColor.RED + BukkitCommandHandler.getInternalErrorMessage()); + sender.sendMessage(BukkitCommandHandler.getInternalErrorMessage()); } } } \ No newline at end of file From 81d622647fbfdd923e6dffea4100c574e2998983 Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:20:27 +0100 Subject: [PATCH 11/27] feat: implemented cooldown into Bungee --- .../command/bungee/BungeeCommandHandler.java | 30 ++++++--- .../bungee/node/BungeeCommandNode.java | 62 ++++++++++++------- 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java index 4994ec9..d8dcdd0 100644 --- a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java +++ b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java @@ -19,20 +19,27 @@ import java.util.Arrays; /** - * Handles command and processor registration for Bungee. + * Handles command and processor registration for BungeeCord. */ public class BungeeCommandHandler { + @Getter @Setter private static Plugin plugin; + @Getter private static Logger logger; - @Getter private static final Logger logger = LoggerFactory.getLogger(plugin.getDescription().getName()); + @Getter @Setter private static String noPermissionMessage = ChatColor.RED + "I'm sorry, but you do not have permission to perform this command."; + @Getter @Setter private static String playerOnlyMessage = ChatColor.RED + "You must be a player to execute this command."; + @Getter @Setter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; + @Getter @Setter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; + @Getter @Setter private static String cooldownMessage = ChatColor.RED + "You must wait {seconds} more second(s) before using this command again."; - @Setter @Getter private static String noPermissionMessage = ChatColor.RED + "I'm sorry, but you do not have permission to perform this command."; - @Setter @Getter private static String playerOnlyMessage = ChatColor.RED + "You must be a player to execute this command."; - @Setter @Getter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; - @Setter @Getter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; + public static void setPlugin(Plugin plugin) { + BungeeCommandHandler.plugin = plugin; + logger = LoggerFactory.getLogger(plugin.getDescription().getName()); + } @SneakyThrows public static void registerCommands(String path, Plugin plugin) { + BungeeCommandHandler.setPlugin(plugin); ClassPath.from(plugin.getClass().getClassLoader()).getAllClasses().stream() .filter(info -> info.getPackageName().startsWith(path)) .map(ClassPath.ClassInfo::load) @@ -65,7 +72,7 @@ public static void registerCommands(Object commandClass) { Subcommand subcommand = commandClass.getClass().getAnnotation(Subcommand.class); Arrays.stream(commandClass.getClass().getDeclaredMethods()).forEach(method -> { - me.marioogg.command.Command command = method.getAnnotation(me.marioogg.command.Command.class); + Command command = method.getAnnotation(Command.class); if (command == null) return; if (subcommand != null) { @@ -100,8 +107,11 @@ public static void registerProcessors(String path, Plugin plugin) { .filter(info -> info.getPackageName().startsWith(path)) .filter(info -> info.load().getSuperclass().equals(BungeeProcessor.class)) .forEach(info -> { - try { BungeeParamProcessor.createProcessor((BungeeProcessor) info.load().getDeclaredConstructor().newInstance()); - } catch (Exception e) { logger.error("Error registering command processors: ", e); } + try { + BungeeParamProcessor.createProcessor((BungeeProcessor) info.load().getDeclaredConstructor().newInstance()); + } catch (Exception e) { + logger.error("Error registering command processors: ", e); + } }); } @@ -126,4 +136,4 @@ private static Command buildDerivedCommand(Command original, String[] newNames) public boolean hidden() { return original.hidden(); } }; } -} +} \ No newline at end of file diff --git a/src/main/java/me/marioogg/command/bungee/node/BungeeCommandNode.java b/src/main/java/me/marioogg/command/bungee/node/BungeeCommandNode.java index da5e6fe..cd6f4e4 100644 --- a/src/main/java/me/marioogg/command/bungee/node/BungeeCommandNode.java +++ b/src/main/java/me/marioogg/command/bungee/node/BungeeCommandNode.java @@ -1,19 +1,20 @@ package me.marioogg.command.bungee.node; import lombok.Getter; -import lombok.SneakyThrows; import me.marioogg.command.Command; import me.marioogg.command.bungee.BungeeCommandHandler; import me.marioogg.command.bungee.BungeeCommand; import me.marioogg.command.bungee.parameter.BungeeParamProcessor; +import me.marioogg.command.bukkit.node.ArgumentNode; +import me.marioogg.command.bukkit.parameter.Param; +import me.marioogg.command.common.cooldown.Cooldown; +import me.marioogg.command.common.cooldown.CooldownManager; +import me.marioogg.command.common.cooldown.CooldownNode; import me.marioogg.command.common.flag.Flag; import me.marioogg.command.common.flag.FlagNode; import me.marioogg.command.common.help.HelpNode; -import me.marioogg.command.bukkit.node.ArgumentNode; -import me.marioogg.command.bukkit.parameter.Param; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.connection.ProxiedPlayer; import org.slf4j.Logger; @@ -24,7 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger; /** - * Stores and executes Bungee command metadata. + * Stores and executes BungeeCord command metadata. */ @Getter public class BungeeCommandNode { @@ -43,6 +44,8 @@ public class BungeeCommandNode { private final Object parentClass; private final Method method; + private final CooldownNode cooldownNode; + private final List parameters = new ArrayList<>(); private final List flagNodes = new ArrayList<>(); private final List helpNodes = new ArrayList<>(); @@ -72,6 +75,9 @@ public BungeeCommandNode(Object parentClass, Method method, Command command) { flagNodes.add(new FlagNode(flag, parameter)); }); + Cooldown cooldown = method.getAnnotation(Cooldown.class); + this.cooldownNode = cooldown != null ? new CooldownNode(cooldown.seconds(), cooldown.bypassPermission()) : null; + names.forEach(name -> { if (!BungeeCommand.getCommands().containsKey(name.split(" ")[0].toLowerCase())) new BungeeCommand(name.split(" ")[0].toLowerCase()); @@ -98,9 +104,9 @@ public int getMatchProbability(CommandSender sender, String label, String[] args if (name.equalsIgnoreCase(nameLabel.toString().trim())) { int requiredParameters = (int) this.parameters.stream().filter(ArgumentNode::isRequired).count(); int flagCount = 0; - for(String arg : args) { + for (String arg : args) { final String a = arg; - if(flagNodes.stream().anyMatch(fn -> fn.matches(a))) flagCount++; + if (flagNodes.stream().anyMatch(fn -> fn.matches(a))) flagCount++; } int actualLength = args.length - (nameLength - 1) - flagCount; @@ -181,7 +187,6 @@ public int requiredArgumentsLength() { return requiredArgumentsLength; } - @SneakyThrows public void execute(CommandSender sender, String[] args) { if (!permission.isEmpty() && !sender.hasPermission(permission)) { sender.sendMessage(new TextComponent(BungeeCommandHandler.getNoPermissionMessage())); @@ -198,6 +203,17 @@ public void execute(CommandSender sender, String[] args) { return; } + if (cooldownNode != null && sender instanceof ProxiedPlayer player) { + if (cooldownNode.getBypassPermission().isEmpty() || !player.hasPermission(cooldownNode.getBypassPermission())) { + if (CooldownManager.isOnCooldown(player.getUniqueId(), names.get(0))) { + long remaining = CooldownManager.getRemainingSeconds(player.getUniqueId(), names.get(0)); + sender.sendMessage(new TextComponent(BungeeCommandHandler.getCooldownMessage().replace("{seconds}", String.valueOf(remaining)))); + return; + } + CooldownManager.setCooldown(player.getUniqueId(), names.get(0), cooldownNode.getSeconds()); + } + } + int nameArgs = (names.get(0).split(" ").length - 1); Set activatedFlags = new HashSet<>(); @@ -227,7 +243,7 @@ public void execute(CommandSender sender, String[] args) { for (int x = i; x < positionalArgs.size(); x++) { stringBuilder.append(positionalArgs.get(x)).append(" "); } - positionalObjects.add(stringBuilder.substring(0, stringBuilder.toString().length() - 1)); + positionalObjects.add(stringBuilder.toString().trim()); break; } @@ -264,22 +280,20 @@ public void execute(CommandSender sender, String[] args) { } if (async) { - final List asyncObjects = objects; - BungeeCommandHandler.getPlugin().getProxy().getScheduler().runAsync(BungeeCommandHandler.getPlugin(), () -> { - try { - method.invoke(parentClass, asyncObjects.toArray()); - } catch (IllegalAccessException | InvocationTargetException e) { - Throwable cause = (e instanceof InvocationTargetException) ? e.getCause() : e; - log.error("An exception occurred while executing command '{}' (Sender: {})", names.get(0), sender.getName(), cause); - sender.sendMessage(new ComponentBuilder( - BungeeCommandHandler.getInternalErrorMessage()) - .color(ChatColor.RED) - .bold(true) - .create()); - }}); + BungeeCommandHandler.getPlugin().getProxy().getScheduler().runAsync(BungeeCommandHandler.getPlugin(), () -> invokeMethod(sender, objects)); return; } - method.invoke(parentClass, objects.toArray()); + invokeMethod(sender, objects); + } + + private void invokeMethod(CommandSender sender, List params) { + try { + method.invoke(parentClass, params.toArray()); + } catch (IllegalAccessException | InvocationTargetException e) { + Throwable cause = (e instanceof InvocationTargetException) ? e.getCause() : e; + log.error("An exception occurred while executing command '{}' (Sender: {})", names.get(0), sender.getName(), cause); + sender.sendMessage(new TextComponent(BungeeCommandHandler.getInternalErrorMessage())); + } } -} +} \ No newline at end of file From 29ce9ca186b00dd465afa669298e36be8e7ca3e0 Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:20:47 +0100 Subject: [PATCH 12/27] feat: implemented cooldown into Velocity --- .../velocity/VelocityCommandHandler.java | 29 +++++---- .../velocity/node/VelocityCommandNode.java | 62 +++++++++++++------ 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java b/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java index 0aab83c..99dfad0 100644 --- a/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java +++ b/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java @@ -19,7 +19,6 @@ import java.util.Arrays; - /** * Handles command and processor registration for Velocity. */ @@ -30,17 +29,18 @@ public class VelocityCommandHandler { private static Logger logger; - @Setter @Getter private static Component noPermissionMessage = Component.text("I'm sorry, but you do not have permission to perform this command.", NamedTextColor.RED); - @Setter @Getter private static Component playerOnlyMessage = Component.text("You must be a player to execute this command.", NamedTextColor.RED); - @Setter @Getter private static Component consoleOnlyMessage = Component.text("This command can only be executed by console.", NamedTextColor.RED); - @Setter @Getter private static Component internalErrorMessage = Component.text("An internal error occurred while executing this command.", NamedTextColor.RED); - + @Getter @Setter private static Component noPermissionMessage = Component.text("I'm sorry, but you do not have permission to perform this command.", NamedTextColor.RED); + @Getter @Setter private static Component playerOnlyMessage = Component.text("You must be a player to execute this command.", NamedTextColor.RED); + @Getter @Setter private static Component consoleOnlyMessage = Component.text("This command can only be executed by console.", NamedTextColor.RED); + @Getter @Setter private static Component internalErrorMessage = Component.text("An internal error occurred while executing this command.", NamedTextColor.RED); + @Getter @Setter private static Component cooldownMessage = Component.text("You must wait {seconds} more second(s) before using this command again.", NamedTextColor.RED); + public static void init(Object plugin, ProxyServer proxy) { VelocityCommandHandler.plugin = plugin; VelocityCommandHandler.proxy = proxy; String pluginName = proxy.getPluginManager().fromInstance(plugin) .map(container -> container.getDescription().getId()) - .orElse(plugin.getClass().getSimpleName()); //backup because in some old versions it breaks + .orElse(plugin.getClass().getSimpleName()); VelocityCommandHandler.logger = LoggerFactory.getLogger(pluginName); } @@ -79,7 +79,7 @@ private static void registerCommands(Object commandClass) { Subcommand subcommand = commandClass.getClass().getAnnotation(Subcommand.class); Arrays.stream(commandClass.getClass().getDeclaredMethods()).forEach(method -> { - me.marioogg.command.Command command = method.getAnnotation(me.marioogg.command.Command.class); + Command command = method.getAnnotation(Command.class); if (command == null) return; if (subcommand != null) { @@ -114,8 +114,11 @@ public static void registerProcessors(String path, Object plugin) { .filter(info -> info.getPackageName().startsWith(path)) .filter(info -> info.load().getSuperclass().equals(VelocityProcessor.class)) .forEach(info -> { - try { VelocityParamProcessor.createProcessor((VelocityProcessor) info.load().getDeclaredConstructor().newInstance()); - } catch (Exception e) { logger.error("Error registering command processors: ", e); } + try { + VelocityParamProcessor.createProcessor((VelocityProcessor) info.load().getDeclaredConstructor().newInstance()); + } catch (Exception e) { + logger.error("Error registering command processors: ", e); + } }); } @@ -127,6 +130,10 @@ public static void registerProcessors(VelocityProcessor... processors) { Arrays.stream(processors).forEach(VelocityCommandHandler::registerProcessor); } + public static Logger getLogger() { + return logger; + } + private static Command buildDerivedCommand(Command original, String[] newNames) { return new Command() { public Class annotationType() { return Command.class; } @@ -140,4 +147,4 @@ private static Command buildDerivedCommand(Command original, String[] newNames) public boolean hidden() { return original.hidden(); } }; } -} +} \ No newline at end of file diff --git a/src/main/java/me/marioogg/command/velocity/node/VelocityCommandNode.java b/src/main/java/me/marioogg/command/velocity/node/VelocityCommandNode.java index fb17e1f..1bffc88 100644 --- a/src/main/java/me/marioogg/command/velocity/node/VelocityCommandNode.java +++ b/src/main/java/me/marioogg/command/velocity/node/VelocityCommandNode.java @@ -1,13 +1,15 @@ package me.marioogg.command.velocity.node; import lombok.Getter; -import lombok.SneakyThrows; import me.marioogg.command.Command; +import me.marioogg.command.bukkit.node.ArgumentNode; +import me.marioogg.command.bukkit.parameter.Param; +import me.marioogg.command.common.cooldown.Cooldown; +import me.marioogg.command.common.cooldown.CooldownManager; +import me.marioogg.command.common.cooldown.CooldownNode; import me.marioogg.command.common.flag.Flag; import me.marioogg.command.common.flag.FlagNode; import me.marioogg.command.common.help.HelpNode; -import me.marioogg.command.bukkit.node.ArgumentNode; -import me.marioogg.command.bukkit.parameter.Param; import me.marioogg.command.velocity.VelocityCommandHandler; import me.marioogg.command.velocity.command.VelocityRawCommand; import me.marioogg.command.velocity.parameter.VelocityParamProcessor; @@ -15,7 +17,9 @@ import com.velocitypowered.api.proxy.Player; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import org.slf4j.Logger; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @@ -24,9 +28,11 @@ * Stores and executes Velocity command metadata. */ @Getter -public class VelocityCommandNode{ +public class VelocityCommandNode { @Getter private static final List nodes = new ArrayList<>(); + private static final Logger log = VelocityCommandHandler.getLogger(); + private final ArrayList names = new ArrayList<>(); private final String permission; private final String description; @@ -38,6 +44,8 @@ public class VelocityCommandNode{ private final Object parentClass; private final Method method; + private final CooldownNode cooldownNode; + private final List parameters = new ArrayList<>(); private final List flagNodes = new ArrayList<>(); private final List helpNodes = new ArrayList<>(); @@ -67,6 +75,9 @@ public VelocityCommandNode(Object parentClass, Method method, Command command) { flagNodes.add(new FlagNode(flag, parameter)); }); + Cooldown cooldown = method.getAnnotation(Cooldown.class); + this.cooldownNode = cooldown != null ? new CooldownNode(cooldown.seconds(), cooldown.bypassPermission()) : null; + names.forEach(name -> { if (!VelocityRawCommand.getCommands().containsKey(name.split(" ")[0].toLowerCase())) new VelocityRawCommand(name.split(" ")[0].toLowerCase()); @@ -93,9 +104,9 @@ public int getMatchProbability(CommandSource source, String label, String[] args if (name.equalsIgnoreCase(nameLabel.toString().trim())) { int requiredParameters = (int) this.parameters.stream().filter(ArgumentNode::isRequired).count(); int flagCount = 0; - for(String arg : args) { + for (String arg : args) { final String a = arg; - if(flagNodes.stream().anyMatch(fn -> fn.matches(a))) flagCount++; + if (flagNodes.stream().anyMatch(fn -> fn.matches(a))) flagCount++; } int actualLength = args.length - (nameLength - 1) - flagCount; @@ -176,7 +187,6 @@ public int requiredArgumentsLength() { return requiredArgumentsLength; } - @SneakyThrows public void execute(CommandSource source, String[] args) { if (!permission.isEmpty() && !source.hasPermission(permission)) { source.sendMessage(VelocityCommandHandler.getNoPermissionMessage()); @@ -193,9 +203,20 @@ public void execute(CommandSource source, String[] args) { return; } + if (cooldownNode != null && source instanceof Player player) { + if (cooldownNode.getBypassPermission().isEmpty() || !player.hasPermission(cooldownNode.getBypassPermission())) { + if (CooldownManager.isOnCooldown(player.getUniqueId(), names.get(0))) { + long remaining = CooldownManager.getRemainingSeconds(player.getUniqueId(), names.get(0)); + source.sendMessage(VelocityCommandHandler.getCooldownMessage() + .replaceText(config -> config.matchLiteral("{seconds}").replacement(String.valueOf(remaining)))); + return; + } + CooldownManager.setCooldown(player.getUniqueId(), names.get(0), cooldownNode.getSeconds()); + } + } + int nameArgs = (names.get(0).split(" ").length - 1); - // Separate flag tokens from positional args Set activatedFlags = new HashSet<>(); List positionalArgs = new ArrayList<>(); for (int i = nameArgs; i < args.length; i++) { @@ -213,7 +234,6 @@ public void execute(CommandSource source, String[] args) { return; } - // Build positional objects List positionalObjects = new ArrayList<>(); for (int i = 0; i < positionalArgs.size(); i++) { if (parameters.size() < i + 1) break; @@ -224,7 +244,7 @@ public void execute(CommandSource source, String[] args) { for (int x = i; x < positionalArgs.size(); x++) { stringBuilder.append(positionalArgs.get(x)).append(" "); } - positionalObjects.add(stringBuilder.substring(0, stringBuilder.toString().length() - 1)); + positionalObjects.add(stringBuilder.toString().trim()); break; } @@ -233,7 +253,6 @@ public void execute(CommandSource source, String[] args) { positionalObjects.add(object); } - // Fill in missing optional positional args for (int i = positionalObjects.size(); i < parameters.size(); i++) { ArgumentNode argumentNode = parameters.get(i); if (argumentNode.getDefaultValue() == null) { @@ -243,7 +262,6 @@ public void execute(CommandSource source, String[] args) { } } - // Build final invocation list in method parameter declaration order List objects = new ArrayList<>(); int positionalIndex = 0; for (java.lang.reflect.Parameter mp : method.getParameters()) { @@ -263,14 +281,22 @@ public void execute(CommandSource source, String[] args) { } if (async) { - final List asyncObjects = objects; VelocityCommandHandler.getProxy().getScheduler() - .buildTask(VelocityCommandHandler.getPlugin(), () -> { - try { method.invoke(parentClass, asyncObjects.toArray()); } catch (Exception e) { e.printStackTrace(); } - }).schedule(); + .buildTask(VelocityCommandHandler.getPlugin(), () -> invokeMethod(source, objects)) + .schedule(); return; } - method.invoke(parentClass, objects.toArray()); + invokeMethod(source, objects); + } + + private void invokeMethod(CommandSource source, List params) { + try { + method.invoke(parentClass, params.toArray()); + } catch (IllegalAccessException | InvocationTargetException e) { + Throwable cause = (e instanceof InvocationTargetException) ? e.getCause() : e; + log.error("An exception occurred while executing command '{}' (Sender: {})", names.get(0), source, cause); + source.sendMessage(VelocityCommandHandler.getInternalErrorMessage()); + } } -} +} \ No newline at end of file From 59c8100232ea6a49860264f330fc6c7aee0e94ca Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:21:32 +0100 Subject: [PATCH 13/27] chore: bump version to 2.0-b1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 487b296..3abc2b2 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group 'me.marioogg' -version '1.2.1' +version '2.0-b1' repositories { maven { url 'https://maven.marioogg.dev/repository/public/' } From ae3361c7e57ac8944e4ddbc90cd2915ac63fa18a Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:25:29 +0100 Subject: [PATCH 14/27] chore: update publish.yml and build.gradle for version 2.0 for CI testing --- .github/workflows/publish.yml | 1 + build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 90c0ea9..e2fa500 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - ver/2.0 jobs: publish: diff --git a/build.gradle b/build.gradle index 3abc2b2..ed1ab0c 100644 --- a/build.gradle +++ b/build.gradle @@ -23,8 +23,8 @@ publishing { repositories { maven { def isSnapshot = version.toString().endsWith("SNAPSHOT") - def releasesRepoUrl = "https://maven.marioogg.dev/repository/releases" - def snapshotsRepoUrl = "https://maven.marioogg.dev/repository/snapshots" + def releasesRepoUrl = "https://maven.marioogg.dev/repository/private" + def snapshotsRepoUrl = "https://maven.marioogg.dev/repository/private" url = uri(isSnapshot ? snapshotsRepoUrl : releasesRepoUrl) From 7b0a9bcb03d2221945aa1ff250793ff07697f634 Mon Sep 17 00:00:00 2001 From: mario <106925545+marioogg@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:25:40 +0100 Subject: [PATCH 15/27] chore: update .gitignore to include command.iml and /.todo/ --- .gitignore | 3 ++- src/main/resources/log4j2.xml | 22 ---------------------- 2 files changed, 2 insertions(+), 23 deletions(-) delete mode 100644 src/main/resources/log4j2.xml diff --git a/.gitignore b/.gitignore index e9a9693..c5d43e0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ build/ target/ qodana.yaml todo.md -command.iml \ No newline at end of file +command.iml +/.todo/ diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml deleted file mode 100644 index 111e82c..0000000 --- a/src/main/resources/log4j2.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - %d{HH:mm:ss} [%t] %-5level %logger{36} - %msg%n - - - - - - - - - - - - - - - - From d5d5117b6322b11db1f9d730712ff1d7d1b85d4d Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:46:38 +0200 Subject: [PATCH 16/27] chore (docs): added proper automatic repo selection --- build.gradle | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index ed1ab0c..fb79b18 100644 --- a/build.gradle +++ b/build.gradle @@ -22,19 +22,21 @@ publishing { repositories { maven { - def isSnapshot = version.toString().endsWith("SNAPSHOT") - def releasesRepoUrl = "https://maven.marioogg.dev/repository/private" - def snapshotsRepoUrl = "https://maven.marioogg.dev/repository/private" + def versionString = version.toString() + def isBeta = versionString.contains("-b") + def isSnapshot = versionString.endsWith("SNAPSHOT") - url = uri(isSnapshot ? snapshotsRepoUrl : releasesRepoUrl) + def privateRepo = "https://maven.marioogg.dev/repository/private" + def snapshotRepo = "https://maven.marioogg.dev/repository/snapshots" + def releaseRepoUrl = "https://maven.marioogg.dev/repository/releases" + + url = uri(isBeta ? privateRepo : isSnapshot ? snapshotRepo : releaseRepoUrl) credentials { username = System.getenv("MAVEN_USERNAME") password = System.getenv("MAVEN_PASSWORD") } } - } -} dependencies { compileOnly('org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT') { From d82d599042664ed40d8535bd3b807b2535881e5f Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:06:00 +0200 Subject: [PATCH 17/27] feat: added validation parameter system (min, max and regex match) --- .../command/common/validation/Matches.java | 12 +++++++++++ .../command/common/validation/Max.java | 12 +++++++++++ .../command/common/validation/Min.java | 13 ++++++++++++ .../common/validation/ValidationResult.java | 20 +++++++++++++++++++ .../command/common/validation/Validator.java | 20 +++++++++++++++++++ 5 files changed, 77 insertions(+) create mode 100644 src/main/java/me/marioogg/command/common/validation/Matches.java create mode 100644 src/main/java/me/marioogg/command/common/validation/Max.java create mode 100644 src/main/java/me/marioogg/command/common/validation/Min.java create mode 100644 src/main/java/me/marioogg/command/common/validation/ValidationResult.java create mode 100644 src/main/java/me/marioogg/command/common/validation/Validator.java diff --git a/src/main/java/me/marioogg/command/common/validation/Matches.java b/src/main/java/me/marioogg/command/common/validation/Matches.java new file mode 100644 index 0000000..31e714e --- /dev/null +++ b/src/main/java/me/marioogg/command/common/validation/Matches.java @@ -0,0 +1,12 @@ +package me.marioogg.command.common.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Matches { + String value(); +} diff --git a/src/main/java/me/marioogg/command/common/validation/Max.java b/src/main/java/me/marioogg/command/common/validation/Max.java new file mode 100644 index 0000000..11cf476 --- /dev/null +++ b/src/main/java/me/marioogg/command/common/validation/Max.java @@ -0,0 +1,12 @@ +package me.marioogg.command.common.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Max { + int value(); +} diff --git a/src/main/java/me/marioogg/command/common/validation/Min.java b/src/main/java/me/marioogg/command/common/validation/Min.java new file mode 100644 index 0000000..34e03be --- /dev/null +++ b/src/main/java/me/marioogg/command/common/validation/Min.java @@ -0,0 +1,13 @@ +package me.marioogg.command.common.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Min { + int value(); +} + diff --git a/src/main/java/me/marioogg/command/common/validation/ValidationResult.java b/src/main/java/me/marioogg/command/common/validation/ValidationResult.java new file mode 100644 index 0000000..efffd34 --- /dev/null +++ b/src/main/java/me/marioogg/command/common/validation/ValidationResult.java @@ -0,0 +1,20 @@ +package me.marioogg.command.common.validation; + +import lombok.Getter; + +@Getter +public class ValidationResult { + public static final ValidationResult OK = new ValidationResult(true, null); + + private final boolean valid; + private final String failReason; + + private ValidationResult(boolean valid, String failReason) { + this.valid = valid; + this.failReason = failReason; + } + + public static ValidationResult fail(String reason) { + return new ValidationResult(false, reason); + } +} diff --git a/src/main/java/me/marioogg/command/common/validation/Validator.java b/src/main/java/me/marioogg/command/common/validation/Validator.java new file mode 100644 index 0000000..e37f143 --- /dev/null +++ b/src/main/java/me/marioogg/command/common/validation/Validator.java @@ -0,0 +1,20 @@ +package me.marioogg.command.common.validation; + +import java.lang.reflect.Parameter; + +public class Validator { + public static ValidationResult validate(Parameter parameter, Object value) { + Min min = parameter.getAnnotation(Min.class); + Max max = parameter.getAnnotation(Max.class); + Matches matches = parameter.getAnnotation(Matches.class); + + if (min != null && value instanceof Number n && n.longValue() < min.value()) + return ValidationResult.fail("min"); + if (max != null && value instanceof Number n && n.longValue() > max.value()) + return ValidationResult.fail("max"); + if (matches != null && value instanceof String s && !s.matches(matches.value())) + return ValidationResult.fail("matches"); + + return ValidationResult.OK; + } +} From 1fa056324c02aebba9303ef9a041637ac3c635d3 Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:06:40 +0200 Subject: [PATCH 18/27] feat: implemented validation on bukkit --- .../command/bukkit/BukkitCommandHandler.java | 4 ++++ .../bukkit/parameter/ParamProcessor.java | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java b/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java index 1ed06fd..6747696 100644 --- a/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java +++ b/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java @@ -33,6 +33,10 @@ public class BukkitCommandHandler { @Getter @Setter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; @Getter @Setter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; @Getter @Setter private static String cooldownMessage = ChatColor.RED + "You must wait {seconds} more second(s) before using this command again."; + @Getter @Setter private static String minValidationMessage = ChatColor.RED + "The value must be at least {min}."; + @Getter @Setter private static String maxValidationMessage = ChatColor.RED + "The value must be at most {max}."; + @Getter @Setter private static String matchesValidationMessage = ChatColor.RED + "Invalid format."; + public static void setPlugin(Plugin plugin) { BukkitCommandHandler.plugin = plugin; diff --git a/src/main/java/me/marioogg/command/bukkit/parameter/ParamProcessor.java b/src/main/java/me/marioogg/command/bukkit/parameter/ParamProcessor.java index 6a07c81..69fd2a0 100644 --- a/src/main/java/me/marioogg/command/bukkit/parameter/ParamProcessor.java +++ b/src/main/java/me/marioogg/command/bukkit/parameter/ParamProcessor.java @@ -2,15 +2,18 @@ import lombok.Data; import lombok.Getter; +import me.marioogg.command.bukkit.BukkitCommandHandler; import me.marioogg.command.common.duration.Duration; import me.marioogg.command.bukkit.node.ArgumentNode; import me.marioogg.command.bukkit.parameter.impl.*; +import me.marioogg.command.common.validation.*; import org.bukkit.ChatColor; import org.bukkit.GameMode; import org.bukkit.OfflinePlayer; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.slf4j.Logger; import java.util.ArrayList; import java.util.HashMap; @@ -24,6 +27,8 @@ public class ParamProcessor { @Getter private static final HashMap, Processor> processors = new HashMap<>(); private static boolean loaded = false; + private final Logger logger = BukkitCommandHandler.getLogger(); + private final ArgumentNode node; private final String supplied; private final CommandSender sender; @@ -38,7 +43,21 @@ public Object get() { Processor processor = processors.get(node.getParameter().getType()); if(processor == null) return supplied; - return processor.process(sender, supplied); + Object result = processor.process(sender, supplied); + if (result == null) return null; + + ValidationResult validation = Validator.validate(node.getParameter(), result); + if (!validation.isValid()) { + if (validation instanceof Min) { + sender.sendMessage(BukkitCommandHandler.getMinValidationMessage().replace("{min}", String.valueOf(((Min) validation).value()))); + } else if (validation instanceof Max) { + sender.sendMessage(BukkitCommandHandler.getMinValidationMessage().replace("{max}", String.valueOf(((Max) validation).value()))); + } else if (validation instanceof Matches) { + sender.sendMessage(BukkitCommandHandler.getMatchesValidationMessage()); + } + return null; + } + return result; } /** From 388af29e059abf5f3aeaf9a46c27d747f4ed2b78 Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:09:01 +0200 Subject: [PATCH 19/27] feat: implemented validation on bungee --- .../command/bungee/BungeeCommandHandler.java | 4 ++++ .../bungee/parameter/BungeeParamProcessor.java | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java index d8dcdd0..f63b8e3 100644 --- a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java +++ b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java @@ -31,6 +31,10 @@ public class BungeeCommandHandler { @Getter @Setter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; @Getter @Setter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; @Getter @Setter private static String cooldownMessage = ChatColor.RED + "You must wait {seconds} more second(s) before using this command again."; + @Getter @Setter private static String minValidationMessage = org.bukkit.ChatColor.RED + "The value must be at least {min}."; + @Getter @Setter private static String maxValidationMessage = org.bukkit.ChatColor.RED + "The value must be at most {max}."; + @Getter @Setter private static String matchesValidationMessage = org.bukkit.ChatColor.RED + "Invalid format."; + public static void setPlugin(Plugin plugin) { BungeeCommandHandler.plugin = plugin; diff --git a/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java b/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java index 00c9b24..817f373 100644 --- a/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java +++ b/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java @@ -2,7 +2,9 @@ import lombok.Data; import lombok.Getter; +import me.marioogg.command.bukkit.BukkitCommandHandler; import me.marioogg.command.bukkit.node.ArgumentNode; +import me.marioogg.command.common.validation.*; import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.chat.TextComponent; @@ -28,7 +30,21 @@ public Object get() { BungeeProcessor processor = processors.get(node.getParameter().getType()); if (processor == null) return supplied; - return processor.process(sender, supplied); + Object result = processor.process(sender, supplied); + if (result == null) return null; + + ValidationResult validation = Validator.validate(node.getParameter(), result); + if (!validation.isValid()) { + if (validation instanceof Min) { + sender.sendMessage(BukkitCommandHandler.getMinValidationMessage().replace("{min}", String.valueOf(((Min) validation).value()))); + } else if (validation instanceof Max) { + sender.sendMessage(BukkitCommandHandler.getMinValidationMessage().replace("{max}", String.valueOf(((Max) validation).value()))); + } else if (validation instanceof Matches) { + sender.sendMessage(BukkitCommandHandler.getMatchesValidationMessage()); + } + return null; + } + return result; } public List getTabComplete() { From 9276dc89347ff8bffa1899af462bf9c788379b32 Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:12:21 +0200 Subject: [PATCH 20/27] fix: changed wrong module class usage from BungeeParamProcessor.java --- .../command/bungee/parameter/BungeeParamProcessor.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java b/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java index 817f373..93ca548 100644 --- a/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java +++ b/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java @@ -4,6 +4,7 @@ import lombok.Getter; import me.marioogg.command.bukkit.BukkitCommandHandler; import me.marioogg.command.bukkit.node.ArgumentNode; +import me.marioogg.command.bungee.BungeeCommandHandler; import me.marioogg.command.common.validation.*; import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.chat.TextComponent; @@ -36,11 +37,11 @@ public Object get() { ValidationResult validation = Validator.validate(node.getParameter(), result); if (!validation.isValid()) { if (validation instanceof Min) { - sender.sendMessage(BukkitCommandHandler.getMinValidationMessage().replace("{min}", String.valueOf(((Min) validation).value()))); + sender.sendMessage(BungeeCommandHandler.getMinValidationMessage().replace("{min}", String.valueOf(((Min) validation).value()))); } else if (validation instanceof Max) { - sender.sendMessage(BukkitCommandHandler.getMinValidationMessage().replace("{max}", String.valueOf(((Max) validation).value()))); + sender.sendMessage(BungeeCommandHandler.getMinValidationMessage().replace("{max}", String.valueOf(((Max) validation).value()))); } else if (validation instanceof Matches) { - sender.sendMessage(BukkitCommandHandler.getMatchesValidationMessage()); + sender.sendMessage(BungeeCommandHandler.getMatchesValidationMessage()); } return null; } From 756bdebf3bf2be7bab9514af42f876dc8456cc40 Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:16:05 +0200 Subject: [PATCH 21/27] feat: implemented validation on velocity --- .../velocity/VelocityCommandHandler.java | 3 ++ .../parameter/VelocityParamProcessor.java | 32 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java b/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java index 99dfad0..3dbe60c 100644 --- a/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java +++ b/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java @@ -34,6 +34,9 @@ public class VelocityCommandHandler { @Getter @Setter private static Component consoleOnlyMessage = Component.text("This command can only be executed by console.", NamedTextColor.RED); @Getter @Setter private static Component internalErrorMessage = Component.text("An internal error occurred while executing this command.", NamedTextColor.RED); @Getter @Setter private static Component cooldownMessage = Component.text("You must wait {seconds} more second(s) before using this command again.", NamedTextColor.RED); + @Getter @Setter private static Component minValidationMessage = Component.text("The value must be at least {min}.", NamedTextColor.RED); + @Getter @Setter private static Component maxValidationMessage = Component.text("The value must be at most {max}.", NamedTextColor.RED); + @Getter @Setter private static Component matchesValidationMessage = Component.text("Invalid format.", NamedTextColor.RED); public static void init(Object plugin, ProxyServer proxy) { VelocityCommandHandler.plugin = plugin; diff --git a/src/main/java/me/marioogg/command/velocity/parameter/VelocityParamProcessor.java b/src/main/java/me/marioogg/command/velocity/parameter/VelocityParamProcessor.java index f311dcb..7dbc50c 100644 --- a/src/main/java/me/marioogg/command/velocity/parameter/VelocityParamProcessor.java +++ b/src/main/java/me/marioogg/command/velocity/parameter/VelocityParamProcessor.java @@ -4,6 +4,8 @@ import lombok.Getter; import me.marioogg.command.bukkit.node.ArgumentNode; import com.velocitypowered.api.command.CommandSource; +import me.marioogg.command.common.validation.*; +import me.marioogg.command.velocity.VelocityCommandHandler; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -27,7 +29,29 @@ public Object get() { if (!loaded) loadProcessors(); VelocityProcessor processor = processors.get(node.getParameter().getType()); if (processor == null) return supplied; - return processor.process(source, supplied); + Object result = processor.process(source, supplied); + if (result == null) return null; + + ValidationResult validation = Validator.validate(node.getParameter(), result); + if (!validation.isValid()) { + if (validation instanceof Min) { + source.sendMessage(replacePlaceholder( + VelocityCommandHandler.getMinValidationMessage(), + "{min}", + ((Min) validation).value() + )); + } else if (validation instanceof Max) { + source.sendMessage(replacePlaceholder( + VelocityCommandHandler.getMaxValidationMessage(), + "{max}", + ((Max) validation).value() + )); + } else if (validation instanceof Matches) { + source.sendMessage(VelocityCommandHandler.getMatchesValidationMessage()); + } + return null; + } + return result; } public List getTabComplete() { @@ -41,6 +65,12 @@ public static void createProcessor(VelocityProcessor processor) { processors.put(processor.getType(), processor); } + private static Component replacePlaceholder(Component template, String placeholder, Object value) { + return template.replaceText(config -> config + .matchLiteral(placeholder) + .replacement(String.valueOf(value))); + } + public static void loadProcessors() { loaded = true; processors.put(int.class, new VelocityProcessor(int.class) { From 0dce82c1346bf6e7bd71a3905e06023d6ec1412b Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:16:30 +0200 Subject: [PATCH 22/27] fix: referencing the Min message wrongly --- .../marioogg/command/bukkit/parameter/ParamProcessor.java | 2 +- .../me/marioogg/command/bungee/BungeeCommandHandler.java | 6 +++--- .../command/bungee/parameter/BungeeParamProcessor.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/me/marioogg/command/bukkit/parameter/ParamProcessor.java b/src/main/java/me/marioogg/command/bukkit/parameter/ParamProcessor.java index 69fd2a0..e00d183 100644 --- a/src/main/java/me/marioogg/command/bukkit/parameter/ParamProcessor.java +++ b/src/main/java/me/marioogg/command/bukkit/parameter/ParamProcessor.java @@ -51,7 +51,7 @@ public Object get() { if (validation instanceof Min) { sender.sendMessage(BukkitCommandHandler.getMinValidationMessage().replace("{min}", String.valueOf(((Min) validation).value()))); } else if (validation instanceof Max) { - sender.sendMessage(BukkitCommandHandler.getMinValidationMessage().replace("{max}", String.valueOf(((Max) validation).value()))); + sender.sendMessage(BukkitCommandHandler.getMaxValidationMessage().replace("{max}", String.valueOf(((Max) validation).value()))); } else if (validation instanceof Matches) { sender.sendMessage(BukkitCommandHandler.getMatchesValidationMessage()); } diff --git a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java index f63b8e3..150f6c8 100644 --- a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java +++ b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java @@ -31,9 +31,9 @@ public class BungeeCommandHandler { @Getter @Setter private static String consoleOnlyMessage = ChatColor.RED + "This command can only be executed by console."; @Getter @Setter private static String internalErrorMessage = ChatColor.RED + "An internal error occurred while executing this command."; @Getter @Setter private static String cooldownMessage = ChatColor.RED + "You must wait {seconds} more second(s) before using this command again."; - @Getter @Setter private static String minValidationMessage = org.bukkit.ChatColor.RED + "The value must be at least {min}."; - @Getter @Setter private static String maxValidationMessage = org.bukkit.ChatColor.RED + "The value must be at most {max}."; - @Getter @Setter private static String matchesValidationMessage = org.bukkit.ChatColor.RED + "Invalid format."; + @Getter @Setter private static String minValidationMessage = ChatColor.RED + "The value must be at least {min}."; + @Getter @Setter private static String maxValidationMessage = ChatColor.RED + "The value must be at most {max}."; + @Getter @Setter private static String matchesValidationMessage = ChatColor.RED + "Invalid format."; public static void setPlugin(Plugin plugin) { diff --git a/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java b/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java index 93ca548..f48b2f1 100644 --- a/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java +++ b/src/main/java/me/marioogg/command/bungee/parameter/BungeeParamProcessor.java @@ -39,7 +39,7 @@ public Object get() { if (validation instanceof Min) { sender.sendMessage(BungeeCommandHandler.getMinValidationMessage().replace("{min}", String.valueOf(((Min) validation).value()))); } else if (validation instanceof Max) { - sender.sendMessage(BungeeCommandHandler.getMinValidationMessage().replace("{max}", String.valueOf(((Max) validation).value()))); + sender.sendMessage(BungeeCommandHandler.getMaxValidationMessage().replace("{max}", String.valueOf(((Max) validation).value()))); } else if (validation instanceof Matches) { sender.sendMessage(BungeeCommandHandler.getMatchesValidationMessage()); } From a74d53e5601a9569107312df2a3fca8e074f9144 Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:22:01 +0200 Subject: [PATCH 23/27] chore (build): bump ver to 2.0-b2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fb79b18..86e2152 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group 'me.marioogg' -version '2.0-b1' +version '2.0-b2' repositories { maven { url 'https://maven.marioogg.dev/repository/public/' } From a477f46aac2e3e646dc2e1a772837e666987d561 Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:25:08 +0200 Subject: [PATCH 24/27] fix (build): syntax error --- build.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 86e2152..9d317c9 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ publishing { maven { def versionString = version.toString() def isBeta = versionString.contains("-b") - def isSnapshot = versionString.endsWith("SNAPSHOT") + def isSnapshot = versionString.endsWith("SNAPSHOT") def privateRepo = "https://maven.marioogg.dev/repository/private" def snapshotRepo = "https://maven.marioogg.dev/repository/snapshots" @@ -37,7 +37,8 @@ publishing { password = System.getenv("MAVEN_PASSWORD") } } - + } +} dependencies { compileOnly('org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT') { exclude group: 'net.md-5' From bed67e7aaf401f477a004e58d8fb14e0e0d14611 Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:40:48 +0200 Subject: [PATCH 25/27] feat: add release drafter configuration for automated release notes --- .github/release-drafter.yml | 33 +++++++++++++++++++++++++++ .github/workflows/release-drafter.yml | 17 ++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/release-drafter.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..70354f3 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,33 @@ +name-template: 'v$RESOLVED_VERSION ๐Ÿš€' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '๐Ÿš€ Features' + labels: + - 'feat' + - title: '๐Ÿ› Bug Fixes' + labels: + - 'fix' + - title: '๐Ÿ›  Refactors' + labels: + - 'refactor' + - title: '๐Ÿงน Chore & Maintenance' + labels: + - 'chore' + - 'build' + - title: '๐Ÿ“ Documentation' + labels: + - 'docs' + - title: '๐Ÿ”ฅ Removed' + labels: + - 'removed' +change-template: '- $TITLE ($SHORT_SHA) by @$AUTHOR' +template: | + ## Changelog + $CHANGES + + --- + Special thanks to all contributors! ๐Ÿ™Œ + $CONTRIBUTORS + + --- + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...$NEXT_TAG \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..af4bd4d --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,17 @@ +name: Release Drafter + +on: + push: + branches: + - main + - ver/2.0 + pull_request: + types: [opened, reopened, synchronize] + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 5b1e8b4ef1451c8009af5e65458f1d93602b98be Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:01:32 +0200 Subject: [PATCH 26/27] feat: add release drafter configuration for automated release notes --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index af4bd4d..66522fb 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -3,7 +3,7 @@ name: Release Drafter on: push: branches: - - main + - master - ver/2.0 pull_request: types: [opened, reopened, synchronize] From dc2ed3f0afbeb272a5f47ab436581c926df8524c Mon Sep 17 00:00:00 2001 From: marioogg <106925545+marioogg@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:12:11 +0200 Subject: [PATCH 27/27] feat: add release drafter configuration for automated release notes --- .github/release-drafter.yml | 44 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 70354f3..d451b07 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,33 +1,31 @@ name-template: 'v$RESOLVED_VERSION ๐Ÿš€' tag-template: 'v$RESOLVED_VERSION' + +version-resolver: + major: + labels: ['major'] + minor: + labels: ['feat'] + patch: + labels: ['fix'] + default: patch + categories: - title: '๐Ÿš€ Features' - labels: - - 'feat' + labels: ['feat'] - title: '๐Ÿ› Bug Fixes' - labels: - - 'fix' + labels: ['fix'] - title: '๐Ÿ›  Refactors' - labels: - - 'refactor' + labels: ['refactor'] - title: '๐Ÿงน Chore & Maintenance' - labels: - - 'chore' - - 'build' + labels: ['chore', 'build'] - title: '๐Ÿ“ Documentation' - labels: - - 'docs' + labels: ['docs'] - title: '๐Ÿ”ฅ Removed' - labels: - - 'removed' -change-template: '- $TITLE ($SHORT_SHA) by @$AUTHOR' + labels: ['removed'] + +change-template: '- $TITLE ($CHANGESET) by @$AUTHOR' + template: | - ## Changelog - $CHANGES - - --- - Special thanks to all contributors! ๐Ÿ™Œ - $CONTRIBUTORS - - --- - **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...$NEXT_TAG \ No newline at end of file + ## What's Changed + $CHANGES \ No newline at end of file