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/.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/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). diff --git a/build.gradle b/build.gradle index 487b296..9d317c9 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group 'me.marioogg' -version '1.2.1' +version '2.0-b2' repositories { maven { url 'https://maven.marioogg.dev/repository/public/' } @@ -22,11 +22,15 @@ 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 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") @@ -35,7 +39,6 @@ publishing { } } } - dependencies { compileOnly('org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT') { exclude group: 'net.md-5' 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/bukkit/BukkitCommandHandler.java b/src/main/java/me/marioogg/command/bukkit/BukkitCommandHandler.java index 599880c..6747696 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,16 @@ public class BukkitCommandHandler { @Getter private static Logger logger; + @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."; + @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; logger = LoggerFactory.getLogger(plugin.getName()); @@ -145,4 +157,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 ade57df..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,6 +4,9 @@ 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; @@ -43,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<>(); @@ -76,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()); }); @@ -152,12 +160,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 +175,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,20 +202,31 @@ 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(BukkitCommandHandler.getNoPermissionMessage()); return; } if(sender instanceof ConsoleCommandSender && playerOnly) { - sender.sendMessage(ChatColor.RED + "You must be a player to execute this command."); + sender.sendMessage(BukkitCommandHandler.getPlayerOnlyMessage()); return; } if(sender instanceof Player && consoleOnly) { - sender.sendMessage(ChatColor.RED + "This command is only executable by console."); + 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<>(); @@ -286,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 + "An internal error occurred while executing this command."); + sender.sendMessage(BukkitCommandHandler.getInternalErrorMessage()); } } } \ No newline at end of file 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..e00d183 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.getMaxValidationMessage().replace("{max}", String.valueOf(((Max) validation).value()))); + } else if (validation instanceof Matches) { + sender.sendMessage(BukkitCommandHandler.getMatchesValidationMessage()); + } + return null; + } + return result; } /** 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; } diff --git a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java index 8d1f354..150f6c8 100644 --- a/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java +++ b/src/main/java/me/marioogg/command/bungee/BungeeCommandHandler.java @@ -11,6 +11,7 @@ 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,15 +19,31 @@ 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 @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."; + @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."; - @Getter private static final Logger logger = LoggerFactory.getLogger(plugin.getDescription().getName()); + + 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) @@ -59,7 +76,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) { @@ -94,8 +111,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); + } }); } @@ -120,4 +140,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 95665d4..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; @@ -146,17 +152,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; } @@ -181,26 +187,35 @@ public int requiredArgumentsLength() { return 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; } + 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); - // Separate flag tokens from positional args Set activatedFlags = new HashSet<>(); List positionalArgs = new ArrayList<>(); for (int i = nameArgs; i < args.length; i++) { @@ -218,7 +233,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; @@ -229,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; } @@ -238,7 +252,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 +261,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()) { @@ -268,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( - "An internal error occurred while executing this command.") - .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 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..f48b2f1 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,10 @@ import lombok.Data; 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; @@ -28,7 +31,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(BungeeCommandHandler.getMinValidationMessage().replace("{min}", String.valueOf(((Min) validation).value()))); + } else if (validation instanceof Max) { + sender.sendMessage(BungeeCommandHandler.getMaxValidationMessage().replace("{max}", String.valueOf(((Max) validation).value()))); + } else if (validation instanceof Matches) { + sender.sendMessage(BungeeCommandHandler.getMatchesValidationMessage()); + } + return null; + } + return result; } public List getTabComplete() { 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; +} 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; + } +} diff --git a/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java b/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java index cd59dc2..3dbe60c 100644 --- a/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java +++ b/src/main/java/me/marioogg/command/velocity/VelocityCommandHandler.java @@ -12,12 +12,13 @@ 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; import java.util.Arrays; - /** * Handles command and processor registration for Velocity. */ @@ -27,13 +28,22 @@ public class VelocityCommandHandler { @Getter @Setter private static ProxyServer proxy; private static Logger logger; - + + @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); + @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; 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); } @@ -72,7 +82,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) { @@ -107,8 +117,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); + } }); } @@ -120,6 +133,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; } @@ -133,4 +150,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/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..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; @@ -141,17 +152,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; } @@ -176,26 +187,36 @@ public int requiredArgumentsLength() { return 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; } + 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 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) { 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 - - - - - - - - - - - - - - - - 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